diff --git a/docs/cms-adapters/CMS_ADAPTERS.md b/docs/cms-adapters/CMS_ADAPTERS.md index b2b7acd3d6..cf49c8cdc4 100644 --- a/docs/cms-adapters/CMS_ADAPTERS.md +++ b/docs/cms-adapters/CMS_ADAPTERS.md @@ -339,3 +339,58 @@ Make sure to expose them in the bundled application by adding them in the `files ``` Also make sure to place them in a folder name `migration-scripts` in your packaged app or to set the `migrationScriptFolder` in your `cms.json`. + +### Case of dependencies on libraries + +If the libraries that you use provide migration scripts, you need to aggregate them with your own to make the metadata-checks pass. + +You will need to specify in your migration script those libraries with their current version. + +```json5 +{ + "$schema": "https://raw.githubusercontent.com/AmadeusITGroup/otter/main/packages/@o3r/extractors/schemas/migration.metadata.schema.json", + "version": "10.0.0", + // List of libraries with migration scripts that your project depend on + "libraries": { + "@mylib/lib": "1.0.0" + }, + "changes": [ + // The changes specific to your project + ] +} +``` + +Then you can automate the aggregation of migration scripts by adding the `@o3r/extractors:aggregate-migration-scripts` builder in your `angular.json` file as follows: +```json5 +{ + // ..., + "projects": { + // ..., + "": { + // ..., + "architect": { + "aggregate-migration-scripts": { + "builder": "@o3r/extractors:aggregate-migration-scripts", + "options": { + "migrationDataPath": "./migration-scripts/src/MIGRATION-*.json", + "outputDirectory": "./migration-scripts/dist" + } + }, + "check-localization-migration-metadata": { + "builder": "@o3r/localization:check-localization-migration-metadata", + "options": { + "migrationDataPath": "./migration-scripts/dist/MIGRATION-*.json" + } + } + } + } + } +} +``` + +Calling the `aggregate-migration-scripts` builder will generate the full migration-scripts including the ones from the libraries. + +In the previous example, you should include the output of the aggregate in the packaged application instead of the original migration scripts (`./migration-scripts/dist` instead of `./mìgration-scripts/src`). + +> [!WARNING] +> The migration scripts of the libraries need be placed in the `./migration-scripts/` folder at the root the library package. diff --git a/packages/@o3r/components/builders/metadata-check/index.it.spec.ts b/packages/@o3r/components/builders/metadata-check/index.it.spec.ts index 4f0945bc2a..5281011f1c 100644 --- a/packages/@o3r/components/builders/metadata-check/index.it.spec.ts +++ b/packages/@o3r/components/builders/metadata-check/index.it.spec.ts @@ -12,12 +12,11 @@ import { getLatestPackageVersion, packageManagerAdd, packageManagerExec, - packageManagerPublish, - packageManagerVersion + packageManagerVersion, + publishToVerdaccio } from '@o3r/test-helpers'; -import { execFileSync } from 'node:child_process'; -import { promises, readFileSync } from 'node:fs'; -import { join } from 'node:path'; +import { existsSync, promises, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; import { inc } from 'semver'; import type { ComponentConfigOutput, ConfigProperty } from '@o3r/components'; import type { MigrationConfigData } from './helpers/config-metadata-comparison.helper'; @@ -25,7 +24,7 @@ import { getExternalDependenciesVersionRange } from '@o3r/schematics'; const baseVersion = '1.2.0'; const version = '1.3.0'; -const migrationDataFileName = `MIGRATION-${version}.json`; +const migrationDataFileName = `migration-scripts/MIGRATION-${version}.json`; const metadataFileName = 'component.config.metadata.json'; const defaultMigrationData: MigrationFile = { @@ -184,8 +183,11 @@ const newConfigurationMetadata: ComponentConfigOutput[] = [ createConfig('@new/lib9', 'MyConfig9', ['prop9']) ]; -function writeFileAsJSON(path: string, content: object) { - return promises.writeFile(path, JSON.stringify(content), { encoding: 'utf8' }); +async function writeFileAsJSON(path: string, content: object) { + if (!existsSync(dirname(path))) { + await promises.mkdir(dirname(path), {recursive: true}); + } + await promises.writeFile(path, JSON.stringify(content), { encoding: 'utf8' }); } const initTest = async ( @@ -197,8 +199,8 @@ const initTest = async ( const { workspacePath, appName, applicationPath, o3rVersion, isYarnTest } = o3rEnvironment.testEnvironment; const execAppOptions = { ...getDefaultExecSyncOptions(), cwd: applicationPath }; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; - packageManagerAdd(`@o3r/components@${o3rVersion}`, execAppOptionsWorkspace); - packageManagerAdd(`@o3r/extractors@${o3rVersion}`, execAppOptionsWorkspace); + packageManagerExec({script: 'ng', args: ['add', `@o3r/extractors@${o3rVersion}`, '--skip-confirmation', '--project-name', appName]}, execAppOptionsWorkspace); + packageManagerExec({script: 'ng', args: ['add', `@o3r/components@${o3rVersion}`, '--skip-confirmation', '--project-name', appName]}, execAppOptionsWorkspace); const versions = getExternalDependenciesVersionRange([ 'semver', ...(isYarnTest ? [ @@ -212,6 +214,7 @@ const initTest = async ( warn: jest.fn() } as any); Object.entries(versions).forEach(([pkgName, pkgVersion]) => packageManagerAdd(`${pkgName}@${pkgVersion}`, execAppOptionsWorkspace)); + const npmIgnorePath = join(applicationPath, '.npmignore'); const packageJsonPath = join(applicationPath, 'package.json'); const angularJsonPath = join(workspacePath, 'angular.json'); const metadataPath = join(applicationPath, metadataFileName); @@ -223,7 +226,7 @@ const initTest = async ( builder: '@o3r/components:check-config-migration-metadata', options: { allowBreakingChanges, - migrationDataPath: `**/MIGRATION-*.json` + migrationDataPath: `apps/test-app/migration-scripts/MIGRATION-*.json` } }; angularJson.projects[appName].architect['check-metadata'] = builderConfig; @@ -238,6 +241,7 @@ const initTest = async ( private: false }; await writeFileAsJSON(packageJsonPath, packageJson); + await promises.writeFile(npmIgnorePath, ''); // Set old metadata and publish to registry await writeFileAsJSON(metadataPath, previousConfigurationMetadata); @@ -254,7 +258,7 @@ const initTest = async ( const args = getPackageManager() === 'yarn' ? [] : ['--no-git-tag-version', '-f']; packageManagerVersion(bumpedVersion, args, execAppOptions); - packageManagerPublish([], execAppOptions); + await publishToVerdaccio(execAppOptions); // Override with new metadata for comparison await writeFileAsJSON(metadataPath, newMetadata); @@ -264,27 +268,6 @@ const initTest = async ( }; describe('check metadata migration', () => { - beforeEach(async () => { - const { applicationPath } = o3rEnvironment.testEnvironment; - const execAppOptions = { ...getDefaultExecSyncOptions(), cwd: applicationPath, shell: true }; - await promises.copyFile( - join(__dirname, '..', '..', '..', '..', '..', '.verdaccio', 'conf', '.npmrc'), - join(applicationPath, '.npmrc') - ); - execFileSync('npx', [ - '--yes', - 'npm-cli-login', - '-u', - 'verdaccio', - '-p', - 'verdaccio', - '-e', - 'test@test.com', - '-r', - 'http://127.0.0.1:4873' - ], execAppOptions); - }); - test('should not throw', async () => { await initTest( true, diff --git a/packages/@o3r/components/schematics/cms-adapter/index.ts b/packages/@o3r/components/schematics/cms-adapter/index.ts index 42814f1132..d5fb2b3a31 100644 --- a/packages/@o3r/components/schematics/cms-adapter/index.ts +++ b/packages/@o3r/components/schematics/cms-adapter/index.ts @@ -15,7 +15,6 @@ function updateCmsAdapterFn(options: { projectName?: string | undefined }): Rule /** * Add cms extractors builder into the angular.json * @param tree - * @param _context * @param context */ const editAngularJson = (tree: Tree, context: SchematicContext) => { @@ -42,7 +41,7 @@ function updateCmsAdapterFn(options: { projectName?: string | undefined }): Rule workspaceProject.architect['check-config-migration-metadata'] ||= { builder: '@o3r/components:check-config-migration-metadata', options: { - migrationDataPath: 'MIGRATION-*.json' + migrationDataPath: 'migration-scripts/dist/MIGRATION-*.json' } }; @@ -54,7 +53,6 @@ function updateCmsAdapterFn(options: { projectName?: string | undefined }): Rule /** * Add cms extractors scripts into the package.json * @param tree - * @param _context * @param context */ const addExtractorsScripts = (tree: Tree, context: SchematicContext) => { diff --git a/packages/@o3r/components/schematics/index.it.spec.ts b/packages/@o3r/components/schematics/index.it.spec.ts index 438211b39f..71b2677985 100644 --- a/packages/@o3r/components/schematics/index.it.spec.ts +++ b/packages/@o3r/components/schematics/index.it.spec.ts @@ -29,7 +29,8 @@ describe('ng add components', () => { expect(diff.added).toContain('apps/test-app/cms.json'); expect(diff.added).toContain('apps/test-app/placeholders.metadata.json'); expect(diff.added).toContain('apps/test-app/tsconfig.cms.json'); - expect(diff.added.length).toBe(3); + expect(diff.added).toContain('apps/test-app/migration-scripts/README.md'); + expect(diff.added.length).toBe(4); [libraryPath, ...untouchedProjectsPaths].forEach(untouchedProject => { expect(diff.all.some(file => file.startsWith(path.posix.relative(workspacePath, untouchedProject)))).toBe(false); @@ -53,7 +54,8 @@ describe('ng add components', () => { expect(diff.added).toContain('libs/test-lib/cms.json'); expect(diff.added).toContain('libs/test-lib/placeholders.metadata.json'); expect(diff.added).toContain('libs/test-lib/tsconfig.cms.json'); - expect(diff.added.length).toBe(3); + expect(diff.added).toContain('libs/test-lib/migration-scripts/README.md'); + expect(diff.added.length).toBe(4); [applicationPath, ...untouchedProjectsPaths].forEach(untouchedProject => { expect(diff.all.some(file => file.startsWith(path.posix.relative(workspacePath, untouchedProject)))).toBe(false); diff --git a/packages/@o3r/extractors/builders.json b/packages/@o3r/extractors/builders.json new file mode 100644 index 0000000000..54b092ebde --- /dev/null +++ b/packages/@o3r/extractors/builders.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/angular/angular-cli/master/packages/angular_devkit/architect/src/builders-schema.json", + "builders": { + "aggregate-migration-scripts": { + "implementation": "./builders/aggregate-migration-scripts/", + "schema": "./builders/aggregate-migration-scripts/schema.json", + "description": "Aggregate migration scripts" + } + } +} diff --git a/packages/@o3r/extractors/builders/aggregate-migration-scripts/index.it.spec.ts b/packages/@o3r/extractors/builders/aggregate-migration-scripts/index.it.spec.ts new file mode 100644 index 0000000000..176c01eac5 --- /dev/null +++ b/packages/@o3r/extractors/builders/aggregate-migration-scripts/index.it.spec.ts @@ -0,0 +1,96 @@ +/** + * Test environment exported by O3rEnvironment, must be first line of the file + * @jest-environment @o3r/test-helpers/jest-environment + * @jest-environment-o3r-app-folder test-app-extractors-aggregate-migration-scripts + */ +const o3rEnvironment = globalThis.o3rEnvironment; + +import { + getDefaultExecSyncOptions, getLatestPackageVersion, + packageManagerAdd, + packageManagerExec, + publishToVerdaccio +} from '@o3r/test-helpers'; +import { promises, readFileSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { inc } from 'semver'; + +const migrationDataMocksPath = join(__dirname, '..', '..', 'testing', 'mocks', 'migration-scripts'); + +function writeFileAsJSON(path: string, content: object) { + return promises.writeFile(path, JSON.stringify(content), { encoding: 'utf8' }); +} + +async function expectFileToMatchMock(realPath: string, mockPath: string) { + expect(await promises.readFile(realPath, {encoding: 'utf8'})).toEqual(await promises.readFile(mockPath, {encoding: 'utf8'})); +} + +async function publishLibrary(cwd: string) { + const libraryPath = join(cwd, 'mylib'); + const execAppOptions = {...getDefaultExecSyncOptions(), cwd: libraryPath}; + const libMigrationDataPath = join(libraryPath, 'migration-scripts'); + await promises.mkdir(libMigrationDataPath, {recursive: true}); + + let latestVersion; + try { + latestVersion = getLatestPackageVersion('@o3r/my-lib', { + ...execAppOptions, + registry: o3rEnvironment.testEnvironment.registry + }); + } catch { + latestVersion = '4.0.0'; + } + const bumpedVersion = inc(latestVersion, 'patch'); + + await promises.writeFile(join(libraryPath, 'package.json'), `{"name": "@o3r/my-lib", "version": "${bumpedVersion}"}`); + await promises.copyFile(join(migrationDataMocksPath, 'lib', 'migration-2.0.json'), join(libMigrationDataPath, 'migration-2.0.json')); + await promises.copyFile(join(migrationDataMocksPath, 'lib', 'migration-2.5.json'), join(libMigrationDataPath, 'migration-2.5.json')); + await promises.copyFile(join(migrationDataMocksPath, 'lib', 'migration-4.0.json'), join(libMigrationDataPath, 'migration-4.0.json')); + + await publishToVerdaccio(execAppOptions); +} + +describe('aggregate migration scripts', () => { + let migrationDataSrcPath: string; + let migrationDataDestPath: string; + + beforeEach(async () => { + const { workspacePath, appName, applicationPath } = o3rEnvironment.testEnvironment; + const angularJsonPath = join(workspacePath, 'angular.json'); + migrationDataSrcPath = join(applicationPath, 'migration-scripts', 'src'); + migrationDataDestPath = join(applicationPath, 'migration-scripts', 'dist'); + // Add builder options + const angularJson = JSON.parse(readFileSync(angularJsonPath, { encoding: 'utf8' }).toString()); + const builderConfig = { + builder: '@o3r/extractors:aggregate-migration-scripts', + options: { + migrationDataPath: relative(workspacePath, `${migrationDataSrcPath}/migration-*.json`).replace(/[\\/]/g, '/'), + outputDirectory: relative(workspacePath, migrationDataDestPath).replace(/[\\/]/g, '/') + } + }; + angularJson.projects[appName].architect['aggregate-migration-scripts'] = builderConfig; + await writeFileAsJSON(angularJsonPath, angularJson); + + await publishLibrary(workspacePath); + }); + + test('should create migration scripts including lib content', async () => { + const { workspacePath, appName, applicationPath, o3rExactVersion } = o3rEnvironment.testEnvironment; + const execAppOptions = { ...getDefaultExecSyncOptions(), cwd: applicationPath }; + const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; + + packageManagerExec({script: 'ng', args: ['add', `@o3r/extractors@${o3rExactVersion}`, '--skip-confirmation', '--project-name', appName]}, execAppOptionsWorkspace); + packageManagerAdd('@o3r/my-lib', execAppOptionsWorkspace); + packageManagerAdd('@o3r/my-lib', execAppOptions); + + await promises.mkdir(migrationDataSrcPath, {recursive: true}); + await promises.copyFile(join(migrationDataMocksPath, 'migration-1.0.json'), join(migrationDataSrcPath, 'migration-1.0.json')); + await promises.copyFile(join(migrationDataMocksPath, 'migration-1.5.json'), join(migrationDataSrcPath, 'migration-1.5.json')); + await promises.copyFile(join(migrationDataMocksPath, 'migration-2.0.json'), join(migrationDataSrcPath, 'migration-2.0.json')); + + expect(() => packageManagerExec({ script: 'ng', args: ['run', `${appName}:aggregate-migration-scripts`] }, execAppOptionsWorkspace)).not.toThrow(); + await expectFileToMatchMock(`${migrationDataDestPath}/migration-1.0.json`, `${migrationDataMocksPath}/expected/migration-1.0.json`); + await expectFileToMatchMock(`${migrationDataDestPath}/migration-1.5.json`, `${migrationDataMocksPath}/expected/migration-1.5.json`); + await expectFileToMatchMock(`${migrationDataDestPath}/migration-2.0.json`, `${migrationDataMocksPath}/expected/migration-2.0.json`); + }); +}); diff --git a/packages/@o3r/extractors/builders/aggregate-migration-scripts/index.spec.ts b/packages/@o3r/extractors/builders/aggregate-migration-scripts/index.spec.ts new file mode 100644 index 0000000000..6935203461 --- /dev/null +++ b/packages/@o3r/extractors/builders/aggregate-migration-scripts/index.spec.ts @@ -0,0 +1,91 @@ +import { Architect } from '@angular-devkit/architect'; +import { TestingArchitectHost } from '@angular-devkit/architect/testing'; +import { schema } from '@angular-devkit/core'; +import { cleanVirtualFileSystem, useVirtualFileSystem } from '@o3r/test-helpers'; +import * as fs from 'node:fs'; +import { join, resolve } from 'node:path'; +import { AggregateMigrationScriptsSchema } from './schema'; + +describe('Aggregate migration scripts', () => { + const workspaceRoot = join('..', '..', '..', '..', '..'); + let architect: Architect; + let architectHost: TestingArchitectHost; + let virtualFileSystem: typeof fs; + const migrationScriptMocksPath = join(__dirname, '../../testing/mocks/migration-scripts'); + const copyMockFile = async (virtualPath: string, realPath: string) => + await virtualFileSystem.promises.writeFile(virtualPath, await fs.promises.readFile(join(migrationScriptMocksPath, realPath), {encoding: 'utf8'})); + const expectFileToMatchMock = async (virtualPath: string, realPath: string) => + expect(await virtualFileSystem.promises.readFile(virtualPath, {encoding: 'utf8'})) + .toEqual(await fs.promises.readFile(join(migrationScriptMocksPath, realPath), {encoding: 'utf8'})); + + beforeEach(() => { + virtualFileSystem = useVirtualFileSystem(); + + const registry = new schema.CoreSchemaRegistry(); + registry.addPostTransform(schema.transforms.addUndefinedDefaults); + architectHost = new TestingArchitectHost(resolve(__dirname, workspaceRoot), __dirname); + architect = new Architect(architectHost, registry); + architectHost.addBuilder('.:aggregate-migration-scripts', require('./index').default); + }); + afterEach(() => { + cleanVirtualFileSystem(); + }); + + it('should aggregate the migration scripts', async () => { + await virtualFileSystem.promises.mkdir('app-migration-scripts', {recursive: true}); + await copyMockFile('app-migration-scripts/migration-1.0.json','migration-1.0.json'); + await copyMockFile('app-migration-scripts/migration-1.5.json','migration-1.5.json'); + await copyMockFile('app-migration-scripts/migration-2.0.json', 'migration-2.0.json'); + + await virtualFileSystem.promises.mkdir('node_modules/@o3r/my-lib/migration-scripts', {recursive: true}); + await virtualFileSystem.promises.writeFile('node_modules/@o3r/my-lib/package.json', '{}'); + await copyMockFile('node_modules/@o3r/my-lib/migration-scripts/migration-2.0.json', 'lib/migration-2.0.json'); + await copyMockFile('node_modules/@o3r/my-lib/migration-scripts/migration-2.5.json', 'lib/migration-2.5.json'); + await copyMockFile('node_modules/@o3r/my-lib/migration-scripts/migration-4.0.json', 'lib/migration-4.0.json'); + + const options: AggregateMigrationScriptsSchema = { + migrationDataPath: './app-migration-scripts/*.json', + outputDirectory: './dist-migration-scripts', + librariesDirectory: 'node_modules' + }; + const run = await architect.scheduleBuilder('.:aggregate-migration-scripts', options); + const output = await run.result; + expect(output.error).toBeUndefined(); + expect(output.success).toBe(true); + await run.stop(); + + await expectFileToMatchMock('./dist-migration-scripts/migration-1.0.json', 'expected/migration-1.0.json'); + await expectFileToMatchMock('./dist-migration-scripts/migration-1.5.json', 'expected/migration-1.5.json'); + await expectFileToMatchMock('./dist-migration-scripts/migration-2.0.json', 'expected/migration-2.0.json'); + }); + + it('should throw if library cannot be found', async () => { + await virtualFileSystem.promises.mkdir('app-migration-scripts', {recursive: true}); + await copyMockFile('app-migration-scripts/migration-1.0.json','migration-1.0.json'); + await copyMockFile('app-migration-scripts/migration-2.0.json', 'migration-2.0.json'); + + const options: AggregateMigrationScriptsSchema = { + migrationDataPath: './app-migration-scripts/*.json', + outputDirectory: './dist-migration-scripts', + librariesDirectory: 'no_libraries_to_be_found_here' + }; + const run = await architect.scheduleBuilder('.:aggregate-migration-scripts', options); + const output = await run.result; + expect(output.error).toBe(`Error: Library @o3r/my-lib not found at ${options.librariesDirectory}/@o3r/my-lib`); + expect(output.success).toBe(false); + await run.stop(); + }); + + it('should do nothing if no migration-scripts are found', async () => { + const options: AggregateMigrationScriptsSchema = { + migrationDataPath: './no_migration_scripts_to_be_found_here/*.json', + outputDirectory: './dist-migration-scripts', + librariesDirectory: 'node_modules' + }; + const run = await architect.scheduleBuilder('.:aggregate-migration-scripts', options); + const output = await run.result; + expect(output.error).toBeUndefined(); + expect(output.success).toBe(true); + await run.stop(); + }); +}); diff --git a/packages/@o3r/extractors/builders/aggregate-migration-scripts/index.ts b/packages/@o3r/extractors/builders/aggregate-migration-scripts/index.ts new file mode 100644 index 0000000000..168d756222 --- /dev/null +++ b/packages/@o3r/extractors/builders/aggregate-migration-scripts/index.ts @@ -0,0 +1,175 @@ +import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; +import { createBuilderWithMetricsIfInstalled, type MigrationFile } from '@o3r/extractors'; +import * as fs from 'node:fs'; +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { basename, dirname, join, posix, resolve } from 'node:path'; +import * as globby from 'globby'; +import * as semver from 'semver'; +import { AggregateMigrationScriptsSchema } from './schema'; + +const STEPS = [ + 'Find all migration files', + 'Fetch migration files from libraries and aggregate the changes', + 'Write the output' +]; + +/** The content of the migration file plus the filename */ +interface MigrationFileEntry { + fileName: string; + migrationObject: MigrationFile; +} + +/** A map that defines for each library the history of versions defined in all the original migration scripts */ +interface LibraryVersions { + [libName: string]: {libVersion: string; appVersion: string}[]; +} + +/** + * Get the content of all migration files matching a glob + * @param glob + */ +const getMigrationFiles = async (glob: string | string[]): Promise => { + return await Promise.all((await globby(glob, {fs})) + .map(async (fileName) => ({ + fileName, + migrationObject: JSON.parse(await readFile(fileName, {encoding: 'utf8'})) + })) + ); +}; + +/** + * Check if the lib version is valid + * @param libVersion + */ +const isValidLibVersion = (libVersion: Partial): libVersion is LibraryVersions[string][number] => + !!libVersion.libVersion && !!libVersion.appVersion; + +/** + * Get the history of versions for each library in chronological order + * @param migrationFiles + */ +const getLibrariesVersions = (migrationFiles: MigrationFileEntry[]): LibraryVersions => { + const libraries: LibraryVersions = {}; + for (const file of migrationFiles) { + if (file.migrationObject.libraries) { + for (const [lib, version] of Object.entries(file.migrationObject.libraries)) { + if (!libraries[lib]) { + libraries[lib] = []; + } + const libVersion = { + libVersion: semver.coerce(version)?.raw, + appVersion: semver.coerce(file.migrationObject.version)?.raw + }; + + if (isValidLibVersion(libVersion) && !libraries[lib].some((e) => e.libVersion === libVersion.libVersion && e.appVersion === libVersion.appVersion)) { + libraries[lib].push(libVersion); + } + } + } + } + // Sort and dedupe versions + Object.entries(libraries).forEach(([libName, libVersions]) => { + libVersions.sort((a, b) => semver.compare(a.appVersion, b.appVersion)); + libraries[libName] = libVersions.filter((libVersion, index) => + index < 1 || libVersions[index - 1].libVersion !== libVersion.libVersion); + }); + return libraries; +}; + +/** + * Write the migration files on disk + * @param migrationFiles + * @param destinationPath + */ +const writeMigrationFiles = async (migrationFiles: MigrationFileEntry[], destinationPath: string) => { + const outputDirectory = resolve(destinationPath); + if (fs.existsSync(outputDirectory)) { + await rm(outputDirectory, {recursive: true}); + } + + for (const file of migrationFiles) { + const distFilePath = join(outputDirectory, basename(file.fileName)); + if (!fs.existsSync(dirname(distFilePath))) { + await mkdir(dirname(distFilePath), {recursive: true}); + } + await writeFile(distFilePath, JSON.stringify(file.migrationObject, null, 2) + '\n'); + } +}; + +/** + * Aggregate the relevant changes from libraries into the original migration files + * @param migrationFiles + * @param librariesVersions + * @param resolver + */ +const aggregateLibrariesChangesIntoMigrationFiles = async (migrationFiles: MigrationFileEntry[], librariesVersions: LibraryVersions, resolver: (lib: string) => string) => { + for (const file of migrationFiles) { + if (file.migrationObject.libraries) { + const appVersion = semver.coerce(file.migrationObject.version)?.raw; + for (const [lib, version] of Object.entries(file.migrationObject.libraries)) { + const libIndex = librariesVersions[lib].findIndex((l) => l.appVersion === appVersion); + if (libIndex > 0) { + const newLibVersion = semver.coerce(version)?.raw; + const previousLibVersion = librariesVersions[lib][libIndex - 1].libVersion; + if (previousLibVersion !== newLibVersion) { + const libRange = new semver.Range(`>${previousLibVersion} <=${newLibVersion}`); + const libPath = dirname(resolver(`${lib}/package.json`)).replace(/[\\/]/g, '/'); + if (!fs.existsSync(libPath)) { + throw new Error(`Library ${lib} not found at ${libPath}`); + } + const libMigrationFiles = await getMigrationFiles(posix.join(libPath, 'migration-scripts', '**', '*.json')); + libMigrationFiles.forEach((libMigrationFile) => { + if (semver.satisfies(libMigrationFile.migrationObject.version, libRange)) { + file.migrationObject.changes = [ + ...file.migrationObject.changes ?? [], + ...libMigrationFile.migrationObject.changes ?? [] + ]; + } + }); + } + } + } + } + } +}; + +export default createBuilder(createBuilderWithMetricsIfInstalled(async (options, context): Promise => { + context.reportRunning(); + try { + let stepNumber = 1; + context.reportProgress(stepNumber, STEPS.length, STEPS[stepNumber - 1]); + const migrationFiles = await getMigrationFiles(options.migrationDataPath); + if (migrationFiles.length < 1) { + context.logger.info(`No migration files found, skipping aggregation`); + return { + success: true + }; + } + + stepNumber++; + context.reportProgress(stepNumber, STEPS.length, STEPS[stepNumber - 1]); + const libResolver = options.librariesDirectory ? (lib: string) => posix.join(options.librariesDirectory!, lib) : require.resolve.bind(require); + const librariesVersions = getLibrariesVersions(migrationFiles); + if (Object.keys(librariesVersions).length < 1) { + return { + success: true + }; + } + await aggregateLibrariesChangesIntoMigrationFiles(migrationFiles, librariesVersions, libResolver); + + stepNumber++; + context.reportProgress(stepNumber, STEPS.length, STEPS[stepNumber - 1]); + await writeMigrationFiles(migrationFiles, options.outputDirectory); + + context.logger.info(`Migration files written successfully`); + + return { + success: true + }; + } catch (err) { + return { + success: false, + error: String(err) + }; + } +})); diff --git a/packages/@o3r/extractors/builders/aggregate-migration-scripts/schema.json b/packages/@o3r/extractors/builders/aggregate-migration-scripts/schema.json new file mode 100644 index 0000000000..58b55e8d6a --- /dev/null +++ b/packages/@o3r/extractors/builders/aggregate-migration-scripts/schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "$id": "AggregateMigrationScriptsSchema", + "title": "Aggregate migration scripts", + "description": "Combine the local migration scripts of the current project with all the migration scripts of its dependencies.", + "properties": { + "migrationDataPath": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "description": "Glob of the migration files to use.", + "default": [ + "./migration-scripts/**/*.json", + "!./migration-scripts/dist" + ] + }, + "outputDirectory": { + "type": "string", + "description": "Path where the aggregated migration files should be written.", + "default": "./migration-scripts/dist" + }, + "librariesDirectory": { + "type": "string", + "description": "Optional path where the libraries can be found (default to `require.resolve`)" + } + }, + "additionalProperties": false +} diff --git a/packages/@o3r/extractors/builders/aggregate-migration-scripts/schema.ts b/packages/@o3r/extractors/builders/aggregate-migration-scripts/schema.ts new file mode 100644 index 0000000000..2c28c811be --- /dev/null +++ b/packages/@o3r/extractors/builders/aggregate-migration-scripts/schema.ts @@ -0,0 +1,16 @@ +import type { JsonObject } from '@angular-devkit/core'; + +/** Combine the local migration scripts of the current project with all the migration scripts of its dependencies. */ +export type AggregateMigrationScriptsSchema = JsonObject & { + /** Glob of the migration files to use. */ + migrationDataPath: string | string[]; + + /** Path where the aggregated migration files should be written. */ + outputDirectory: string; + + /** + * Optional path where the libraries can be found + * @default module.paths + */ + librariesDirectory?: string; +}; diff --git a/packages/@o3r/extractors/package.json b/packages/@o3r/extractors/package.json index 4cf3c70558..73573f6c50 100644 --- a/packages/@o3r/extractors/package.json +++ b/packages/@o3r/extractors/package.json @@ -16,7 +16,7 @@ "ng": "yarn nx", "build": "yarn nx build extractors", "postbuild": "patch-package-json-main", - "prepare:build:builders": "yarn cpy 'schematics/**/*.json' 'schematics/**/templates/**' dist/schematics && yarn cpy '{collection,migration}.json' dist && yarn cpy 'schemas/*.json' 'dist/schemas'", + "prepare:build:builders": "yarn cpy 'builders/**/*.json' dist/builders && yarn cpy 'schematics/**/*.json' 'schematics/**/templates/**' dist/schematics && yarn cpy '{builders,collection,migration}.json' dist && yarn cpy 'schemas/*.json' 'dist/schemas'", "build:builders": "tsc -b tsconfig.builders.json --pretty && yarn generate-cjs-manifest", "prepare:publish": "prepare-publish ./dist" }, @@ -146,6 +146,7 @@ "engines": { "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, + "builders": "./builders.json", "schematics": "./collection.json", "ng-update": { "migrations": "./migration.json" diff --git a/packages/@o3r/extractors/schemas/migration.metadata.schema.json b/packages/@o3r/extractors/schemas/migration.metadata.schema.json index 9b702cf51e..2351275139 100644 --- a/packages/@o3r/extractors/schemas/migration.metadata.schema.json +++ b/packages/@o3r/extractors/schemas/migration.metadata.schema.json @@ -12,6 +12,13 @@ "type": "string", "description": "Version of the documented migration" }, + "libraries": { + "type": "object", + "description": "Map of dependencies for which the migration scripts should be checked with their associated version", + "additionalProperties": { + "type": "string" + } + }, "changes": { "type": "array", "description": "List of all the changes contained in this version", diff --git a/packages/@o3r/extractors/schematics/cms-adapter/index.ts b/packages/@o3r/extractors/schematics/cms-adapter/index.ts index b04f24ea7a..daa9a2d630 100644 --- a/packages/@o3r/extractors/schematics/cms-adapter/index.ts +++ b/packages/@o3r/extractors/schematics/cms-adapter/index.ts @@ -49,8 +49,40 @@ export function updateCmsAdapter(options: { projectName?: string | undefined }, return ignorePatterns(tree, [{ description: 'CMS metadata files', patterns: ['/*.metadata.json'] }]); }; + /** + * Add aggregate-migration-scripts builder into the angular.json + * @param tree + * @param context + */ + const editAngularJson = (tree: Tree, context: SchematicContext) => { + const workspace = getWorkspaceConfig(tree); + const workspaceProject = options.projectName ? workspace?.projects[options.projectName] : undefined; + + if (!workspace || !workspaceProject) { + context.logger.error('No project detected, the extractors builders will not be added'); + return tree; + } + + if (!workspaceProject.architect) { + workspaceProject.architect = {}; + } + + workspaceProject.architect['aggregate-migration-scripts'] ||= { + builder: '@o3r/extractors:aggregate-migration-scripts', + options: { + migrationDataPath: 'migration-scripts/src/MIGRATION-*.json', + outputDirectory: 'migration-scripts/dist' + } + }; + + workspace.projects[options.projectName!] = workspaceProject; + tree.overwrite('/angular.json', JSON.stringify(workspace, null, 2)); + return tree; + }; + return chain([ generateTsConfig, - ignoreMetadataFiles + ignoreMetadataFiles, + editAngularJson ]); } diff --git a/packages/@o3r/extractors/schematics/cms-adapter/templates/migration-scripts/README.md b/packages/@o3r/extractors/schematics/cms-adapter/templates/migration-scripts/README.md new file mode 100644 index 0000000000..aa393d0236 --- /dev/null +++ b/packages/@o3r/extractors/schematics/cms-adapter/templates/migration-scripts/README.md @@ -0,0 +1,9 @@ +# Migration scripts + +You can place here your migration scripts to document the breaking changes in your metadata. + +The versioned files should be placed in `./src` folder. + +Running the `aggregate-migration-scripts` command will create the `./dist` which will also include the migration-scripts from libraries. + +Please refer to [the documentation on CMS Adapters](https://github.com/AmadeusITGroup/otter/blob/main/docs/cms-adapters/CMS_ADAPTERS.md). diff --git a/packages/@o3r/extractors/schematics/index.it.spec.ts b/packages/@o3r/extractors/schematics/index.it.spec.ts index a96dfded0c..e6a6d4922a 100644 --- a/packages/@o3r/extractors/schematics/index.it.spec.ts +++ b/packages/@o3r/extractors/schematics/index.it.spec.ts @@ -23,6 +23,7 @@ describe('ng add extractors', () => { const diff = getGitDiff(workspacePath); expect(diff.modified.sort()).toEqual([ + 'angular.json', '.gitignore', 'package.json', 'apps/test-app/package.json', @@ -31,7 +32,8 @@ describe('ng add extractors', () => { expect(diff.added.sort()).toEqual([ 'apps/test-app/cms.json', 'apps/test-app/placeholders.metadata.json', - 'apps/test-app/tsconfig.cms.json' + 'apps/test-app/tsconfig.cms.json', + 'apps/test-app/migration-scripts/README.md' ].sort()); [libraryPath, ...untouchedProjectsPaths].forEach(untouchedProject => { @@ -51,6 +53,7 @@ describe('ng add extractors', () => { const diff = getGitDiff(workspacePath); expect(diff.modified.sort()).toEqual([ + 'angular.json', '.gitignore', 'package.json', 'libs/test-lib/package.json', @@ -59,7 +62,8 @@ describe('ng add extractors', () => { expect(diff.added.sort()).toEqual([ 'libs/test-lib/cms.json', 'libs/test-lib/placeholders.metadata.json', - 'libs/test-lib/tsconfig.cms.json' + 'libs/test-lib/tsconfig.cms.json', + 'libs/test-lib/migration-scripts/README.md' ].sort()); [applicationPath, ...untouchedProjectsPaths].forEach(untouchedProject => { diff --git a/packages/@o3r/extractors/schematics/ng-add/index.ts b/packages/@o3r/extractors/schematics/ng-add/index.ts index b3320c7273..eb813cb3ae 100644 --- a/packages/@o3r/extractors/schematics/ng-add/index.ts +++ b/packages/@o3r/extractors/schematics/ng-add/index.ts @@ -4,6 +4,10 @@ import * as path from 'node:path'; import { updateCmsAdapter } from '../cms-adapter'; import type { NgAddSchematicsSchema } from './schema'; +const dependenciesToInstall = [ + 'semver' +]; + const reportMissingSchematicsDep = (logger: { error: (message: string) => any }) => (reason: any) => { logger.error(`[ERROR]: Adding @o3r/extractors has failed. If the error is related to missing @o3r dependencies you need to install '@o3r/core' to be able to use the localization package. Please run 'ng add @o3r/core' . @@ -16,8 +20,17 @@ Otherwise, use the error message as guidance.`); * @param options */ function ngAddFn(options: NgAddSchematicsSchema): Rule { - return async (tree) => { - const { getPackageInstallConfig, getProjectNewDependenciesTypes, setupDependencies, getO3rPeerDeps, getWorkspaceConfig } = await import('@o3r/schematics'); + return async (tree, context) => { + const { + getExternalDependenciesVersionRange, + getPackageInstallConfig, + getProjectNewDependenciesTypes, + setupDependencies, + getO3rPeerDeps, + getWorkspaceConfig + } = await import('@o3r/schematics'); + // eslint-disable-next-line @typescript-eslint/naming-convention + const { NodeDependencyType } = await import('@schematics/angular/utility/dependencies'); const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); const depsInfo = getO3rPeerDeps(packageJsonPath); const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; @@ -31,6 +44,14 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { }; return acc; }, getPackageInstallConfig(packageJsonPath, tree, options.projectName, true, !!options.exactO3rVersion)); + Object.entries(getExternalDependenciesVersionRange(dependenciesToInstall, packageJsonPath, context.logger)).forEach(([dep, range]) => { + dependencies[dep] = { + inManifest: [{ + range, + types: [NodeDependencyType.Dev] + }] + }; + }); return chain([ setupDependencies({ projectName: options.projectName, diff --git a/packages/@o3r/extractors/src/core/comparator/metadata-comparator.interface.ts b/packages/@o3r/extractors/src/core/comparator/metadata-comparator.interface.ts index 3e232b5885..c15c7e9d74 100644 --- a/packages/@o3r/extractors/src/core/comparator/metadata-comparator.interface.ts +++ b/packages/@o3r/extractors/src/core/comparator/metadata-comparator.interface.ts @@ -53,6 +53,9 @@ export interface MigrationFile { /** Version of the documented migration */ version: string; + /** Map of dependencies for which the migration scripts should be checked with their associated version */ + libraries?: Record; + /** List of all the changes contained in this version */ changes: MigrationData[]; } diff --git a/packages/@o3r/extractors/src/core/comparator/metadata-comparison.helper.ts b/packages/@o3r/extractors/src/core/comparator/metadata-comparison.helper.ts index 4c8d89364f..7b71b36090 100644 --- a/packages/@o3r/extractors/src/core/comparator/metadata-comparison.helper.ts +++ b/packages/@o3r/extractors/src/core/comparator/metadata-comparison.helper.ts @@ -2,9 +2,11 @@ import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; import type { JsonObject } from '@angular-devkit/core'; import { getPackageManagerInfo, O3rCliError, type PackageManagerOptions, type SupportedPackageManagers, type WorkspaceSchema } from '@o3r/schematics'; import { sync as globbySync } from 'globby'; -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync, promises, readFileSync } from 'node:fs'; import { EOL } from 'node:os'; import { join, posix } from 'node:path'; +import { coerce, Range, satisfies } from 'semver'; +import type { PackageJson } from 'type-fest'; import type { MetadataComparator, MigrationData, MigrationFile, MigrationMetadataCheckBuilderOptions } from './metadata-comparator.interface'; import { getFilesFromRegistry, getLatestMigrationMetadataFile, getLocalMetadataFile, getVersionRangeFromLatestVersion } from './metadata-files.helper'; @@ -117,6 +119,17 @@ Detection of package manager runner will fallback on the one used to execute the const migrationData = getLocalMetadataFile>(migrationFilePath); + // Check for libraries versions mismatches + if (migrationData.libraries?.length) { + await Promise.all(Object.entries(migrationData.libraries).map(async ([libName, libVersion]) => { + const libPackageJson = JSON.parse(await promises.readFile(require.resolve(`${libName}/package.json`), {encoding: 'utf8'})) as PackageJson; + const libRange = new Range(`~${coerce(libVersion)?.raw}`); + if (!satisfies(libPackageJson.version!, libRange)) { + context.logger.warn(`The version of the library "${libName}": ${libVersion} specified in your latest migration files doesn't match the installed version: ${libPackageJson.version}`); + } + })); + } + const packageLocator = `${packageJson.name as string}@${previousVersion}`; context.logger.info(`Fetching ${packageLocator} from the registry.`); const previousFile = await getFilesFromRegistry(packageLocator, [options.metadataPath], packageManager, context.workspaceRoot); diff --git a/packages/@o3r/extractors/testing/jest.config.ut.builders.js b/packages/@o3r/extractors/testing/jest.config.ut.builders.js index aa8c5dc6af..240be10b0f 100644 --- a/packages/@o3r/extractors/testing/jest.config.ut.builders.js +++ b/packages/@o3r/extractors/testing/jest.config.ut.builders.js @@ -8,6 +8,11 @@ module.exports = { displayName: `${require('../package.json').name}/builders`, rootDir, setupFilesAfterEnv: ['/testing/setup-jest.builders.ts'], + fakeTimers: { + enableGlobally: true, + // This is needed to prevent timeout on builders tests + advanceTimers: true + }, testPathIgnorePatterns: [ '/.*/templates/.*', '/src/.*', diff --git a/packages/@o3r/extractors/testing/mocks/migration-scripts/expected/migration-1.0.json b/packages/@o3r/extractors/testing/mocks/migration-scripts/expected/migration-1.0.json new file mode 100644 index 0000000000..a5440c9954 --- /dev/null +++ b/packages/@o3r/extractors/testing/mocks/migration-scripts/expected/migration-1.0.json @@ -0,0 +1,17 @@ +{ + "version": "1.0.0", + "libraries": { + "@o3r/my-lib": "2.0" + }, + "changes": [ + { + "contentType": "STYLE", + "before": { + "name": "old-var1" + }, + "after": { + "name": "new-var1" + } + } + ] +} diff --git a/packages/@o3r/extractors/testing/mocks/migration-scripts/expected/migration-1.5.json b/packages/@o3r/extractors/testing/mocks/migration-scripts/expected/migration-1.5.json new file mode 100644 index 0000000000..2c544a7891 --- /dev/null +++ b/packages/@o3r/extractors/testing/mocks/migration-scripts/expected/migration-1.5.json @@ -0,0 +1,6 @@ +{ + "version": "1.5.0", + "libraries": { + "@o3r/my-lib": "2.0" + } +} diff --git a/packages/@o3r/extractors/testing/mocks/migration-scripts/expected/migration-2.0.json b/packages/@o3r/extractors/testing/mocks/migration-scripts/expected/migration-2.0.json new file mode 100644 index 0000000000..063ee204a2 --- /dev/null +++ b/packages/@o3r/extractors/testing/mocks/migration-scripts/expected/migration-2.0.json @@ -0,0 +1,26 @@ +{ + "version": "2.0.0", + "libraries": { + "@o3r/my-lib": "3.0" + }, + "changes": [ + { + "contentType": "STYLE", + "before": { + "name": "old-var2" + }, + "after": { + "name": "new-var2" + } + }, + { + "contentType": "STYLE", + "before": { + "name": "new-lib-var2.0" + }, + "after": { + "name": "new-lib-var2.5" + } + } + ] +} diff --git a/packages/@o3r/extractors/testing/mocks/migration-scripts/lib/migration-2.0.json b/packages/@o3r/extractors/testing/mocks/migration-scripts/lib/migration-2.0.json new file mode 100644 index 0000000000..6357f1ef10 --- /dev/null +++ b/packages/@o3r/extractors/testing/mocks/migration-scripts/lib/migration-2.0.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.0", + "changes": [ + { + "contentType": "STYLE", + "before": { + "name": "old-lib-var" + }, + "after": { + "name": "new-lib-var2.0" + } + } + ] +} diff --git a/packages/@o3r/extractors/testing/mocks/migration-scripts/lib/migration-2.5.json b/packages/@o3r/extractors/testing/mocks/migration-scripts/lib/migration-2.5.json new file mode 100644 index 0000000000..29023c5da9 --- /dev/null +++ b/packages/@o3r/extractors/testing/mocks/migration-scripts/lib/migration-2.5.json @@ -0,0 +1,14 @@ +{ + "version": "2.5.0", + "changes": [ + { + "contentType": "STYLE", + "before": { + "name": "new-lib-var2.0" + }, + "after": { + "name": "new-lib-var2.5" + } + } + ] +} diff --git a/packages/@o3r/extractors/testing/mocks/migration-scripts/lib/migration-4.0.json b/packages/@o3r/extractors/testing/mocks/migration-scripts/lib/migration-4.0.json new file mode 100644 index 0000000000..b0072ec360 --- /dev/null +++ b/packages/@o3r/extractors/testing/mocks/migration-scripts/lib/migration-4.0.json @@ -0,0 +1,14 @@ +{ + "version": "4.0.0", + "changes": [ + { + "contentType": "STYLE", + "before": { + "name": "new-lib-var2.5" + }, + "after": { + "name": "new-lib-var4.0" + } + } + ] +} diff --git a/packages/@o3r/extractors/testing/mocks/migration-scripts/migration-1.0.json b/packages/@o3r/extractors/testing/mocks/migration-scripts/migration-1.0.json new file mode 100644 index 0000000000..a5440c9954 --- /dev/null +++ b/packages/@o3r/extractors/testing/mocks/migration-scripts/migration-1.0.json @@ -0,0 +1,17 @@ +{ + "version": "1.0.0", + "libraries": { + "@o3r/my-lib": "2.0" + }, + "changes": [ + { + "contentType": "STYLE", + "before": { + "name": "old-var1" + }, + "after": { + "name": "new-var1" + } + } + ] +} diff --git a/packages/@o3r/extractors/testing/mocks/migration-scripts/migration-1.5.json b/packages/@o3r/extractors/testing/mocks/migration-scripts/migration-1.5.json new file mode 100644 index 0000000000..2c544a7891 --- /dev/null +++ b/packages/@o3r/extractors/testing/mocks/migration-scripts/migration-1.5.json @@ -0,0 +1,6 @@ +{ + "version": "1.5.0", + "libraries": { + "@o3r/my-lib": "2.0" + } +} diff --git a/packages/@o3r/extractors/testing/mocks/migration-scripts/migration-2.0.json b/packages/@o3r/extractors/testing/mocks/migration-scripts/migration-2.0.json new file mode 100644 index 0000000000..55a560838a --- /dev/null +++ b/packages/@o3r/extractors/testing/mocks/migration-scripts/migration-2.0.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "libraries": { + "@o3r/my-lib": "3.0" + }, + "changes": [ + { + "contentType": "STYLE", + "before": { + "name": "old-var2" + }, + "after": { + "name": "new-var2" + } + } + ] +} diff --git a/packages/@o3r/extractors/tsconfig.build.json b/packages/@o3r/extractors/tsconfig.build.json index 1480057065..703c0a458f 100644 --- a/packages/@o3r/extractors/tsconfig.build.json +++ b/packages/@o3r/extractors/tsconfig.build.json @@ -6,7 +6,7 @@ "composite": true, "module": "CommonJS", "outDir": "./dist", - "rootDir": "src", + "rootDir": ".", "tsBuildInfoFile": "build/.tsbuildinfo" }, "include": [ diff --git a/packages/@o3r/extractors/tsconfig.builders.json b/packages/@o3r/extractors/tsconfig.builders.json index 459ed573ce..a37cc24404 100644 --- a/packages/@o3r/extractors/tsconfig.builders.json +++ b/packages/@o3r/extractors/tsconfig.builders.json @@ -8,11 +8,18 @@ "rootDir": ".", "tsBuildInfoFile": "build/.tsbuildinfo.builders" }, + "references": [ + { + "path": "./tsconfig.build.json" + } + ], "include": [ + "builders/**/*.ts", "schematics/**/*.ts" ], "exclude": [ "**/*.spec.ts", + "builders/**/templates/**", "schematics/**/templates/**" ] } diff --git a/packages/@o3r/localization/builders/metadata-check/index.it.spec.ts b/packages/@o3r/localization/builders/metadata-check/index.it.spec.ts index 7b2ddfda77..cf38a546cd 100644 --- a/packages/@o3r/localization/builders/metadata-check/index.it.spec.ts +++ b/packages/@o3r/localization/builders/metadata-check/index.it.spec.ts @@ -12,12 +12,11 @@ import { getLatestPackageVersion, packageManagerAdd, packageManagerExec, - packageManagerPublish, - packageManagerVersion + packageManagerVersion, + publishToVerdaccio } from '@o3r/test-helpers'; -import { execFileSync } from 'node:child_process'; -import { promises, readFileSync } from 'node:fs'; -import { join } from 'node:path'; +import { existsSync, promises, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; import { inc } from 'semver'; import type { JSONLocalization, LocalizationMetadata } from '@o3r/localization'; import type { MigrationLocalizationMetadata } from './helpers/localization-metadata-comparison.helper'; @@ -25,7 +24,7 @@ import { getExternalDependenciesVersionRange } from '@o3r/schematics'; const baseVersion = '1.2.0'; const version = '1.3.0'; -const migrationDataFileName = `MIGRATION-${version}.json`; +const migrationDataFileName = `migration-scripts/MIGRATION-${version}.json`; const metadataFileName = 'localisation.metadata.json'; const defaultMigrationData: MigrationFile = { @@ -60,8 +59,11 @@ const newLocalizationMetadata: LocalizationMetadata = [ createLoc('new-localization.key1') ]; -function writeFileAsJSON(path: string, content: object) { - return promises.writeFile(path, JSON.stringify(content), { encoding: 'utf8' }); +async function writeFileAsJSON(path: string, content: object) { + if (!existsSync(dirname(path))) { + await promises.mkdir(dirname(path), {recursive: true}); + } + await promises.writeFile(path, JSON.stringify(content), { encoding: 'utf8' }); } const initTest = async ( @@ -73,8 +75,8 @@ const initTest = async ( const { workspacePath, appName, applicationPath, o3rVersion, isYarnTest } = o3rEnvironment.testEnvironment; const execAppOptions = { ...getDefaultExecSyncOptions(), cwd: applicationPath }; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; - packageManagerAdd(`@o3r/localization@${o3rVersion}`, execAppOptionsWorkspace); - packageManagerAdd(`@o3r/extractors@${o3rVersion}`, execAppOptionsWorkspace); + packageManagerExec({script: 'ng', args: ['add', `@o3r/extractors@${o3rVersion}`, '--skip-confirmation', '--project-name', appName]}, execAppOptionsWorkspace); + packageManagerExec({script: 'ng', args: ['add', `@o3r/localization@${o3rVersion}`, '--skip-confirmation', '--project-name', appName]}, execAppOptionsWorkspace); const versions = getExternalDependenciesVersionRange([ 'semver', ...(isYarnTest ? [ @@ -88,6 +90,7 @@ const initTest = async ( warn: jest.fn() } as any); Object.entries(versions).forEach(([pkgName, pkgVersion]) => packageManagerAdd(`${pkgName}@${pkgVersion}`, execAppOptionsWorkspace)); + const npmIgnorePath = join(applicationPath, '.npmignore'); const packageJsonPath = join(applicationPath, 'package.json'); const angularJsonPath = join(workspacePath, 'angular.json'); const metadataPath = join(applicationPath, metadataFileName); @@ -99,7 +102,7 @@ const initTest = async ( builder: '@o3r/localization:check-localization-migration-metadata', options: { allowBreakingChanges, - migrationDataPath: `**/MIGRATION-*.json` + migrationDataPath: `apps/test-app/migration-scripts/MIGRATION-*.json` } }; angularJson.projects[appName].architect['check-metadata'] = builderConfig; @@ -114,6 +117,7 @@ const initTest = async ( private: false }; await writeFileAsJSON(packageJsonPath, packageJson); + await promises.writeFile(npmIgnorePath, ''); // Set old metadata and publish to registry await writeFileAsJSON(metadataPath, previousLocalizationMetadata); @@ -130,7 +134,7 @@ const initTest = async ( const args = getPackageManager() === 'yarn' ? [] : ['--no-git-tag-version', '-f']; packageManagerVersion(bumpedVersion, args, execAppOptions); - packageManagerPublish([], execAppOptions); + await publishToVerdaccio(execAppOptions); // Override with new metadata for comparison await writeFileAsJSON(metadataPath, newMetadata); @@ -140,27 +144,6 @@ const initTest = async ( }; describe('check metadata migration', () => { - beforeEach(async () => { - const { applicationPath } = o3rEnvironment.testEnvironment; - const execAppOptions = { ...getDefaultExecSyncOptions(), cwd: applicationPath, shell: true }; - await promises.copyFile( - join(__dirname, '..', '..', '..', '..', '..', '.verdaccio', 'conf', '.npmrc'), - join(applicationPath, '.npmrc') - ); - execFileSync('npx', [ - '--yes', - 'npm-cli-login', - '-u', - 'verdaccio', - '-p', - 'verdaccio', - '-e', - 'test@test.com', - '-r', - 'http://127.0.0.1:4873' - ], execAppOptions); - }); - test('should not throw', async () => { await initTest( true, diff --git a/packages/@o3r/localization/schematics/cms-adapter/index.ts b/packages/@o3r/localization/schematics/cms-adapter/index.ts index 09b24f0dda..e6b3972f52 100644 --- a/packages/@o3r/localization/schematics/cms-adapter/index.ts +++ b/packages/@o3r/localization/schematics/cms-adapter/index.ts @@ -15,7 +15,6 @@ function updateCmsAdapterFn(options: { projectName?: string | undefined }): Rule /** * Add cms extractors builder into the angular.json * @param tree - * @param _context * @param context */ const editAngularJson = (tree: Tree, context: SchematicContext) => { @@ -41,7 +40,7 @@ function updateCmsAdapterFn(options: { projectName?: string | undefined }): Rule workspaceProject.architect['check-localization-migration-metadata'] ||= { builder: '@o3r/localization:check-localization-migration-metadata', options: { - migrationDataPath: 'MIGRATION-*.json' + migrationDataPath: 'migration-scripts/dist/MIGRATION-*.json' } }; @@ -53,7 +52,6 @@ function updateCmsAdapterFn(options: { projectName?: string | undefined }): Rule /** * Add cms extractors scripts into the package.json * @param tree - * @param _context * @param context */ const addExtractorsScripts = (tree: Tree, context: SchematicContext) => { diff --git a/packages/@o3r/localization/schematics/index.it.spec.ts b/packages/@o3r/localization/schematics/index.it.spec.ts index fcbec9475f..2c0f4020a2 100644 --- a/packages/@o3r/localization/schematics/index.it.spec.ts +++ b/packages/@o3r/localization/schematics/index.it.spec.ts @@ -29,7 +29,7 @@ describe('ng add otter localization', () => { const diff = getGitDiff(workspacePath); expect(diff.modified.length).toBe(9); - expect(diff.added.length).toBe(15); + expect(diff.added.length).toBe(16); expect(diff.added).toContain(path.join(relativeApplicationPath, 'src/components/test-component/test-component.localization.json').replace(/[\\/]+/g, '/')); expect(diff.added).toContain(path.join(relativeApplicationPath, 'src/components/test-component/test-component.translation.ts').replace(/[\\/]+/g, '/')); @@ -64,10 +64,14 @@ describe('ng add otter localization', () => { expect(diff.modified).toContain(modifiedFile); }); expect(diff.modified.length).toBe(modifiedFiles.length); - - expect(diff.added).toContain(path.join(relativeLibraryPath, 'src/components/test-component/test-component.localization.json').replace(/[\\/]+/g, '/')); - expect(diff.added).toContain(path.join(relativeLibraryPath, 'src/components/test-component/test-component.translation.ts').replace(/[\\/]+/g, '/')); - expect(diff.added.length).toBe(13); + const addedFiles = [ + path.join(relativeLibraryPath, 'src/components/test-component/test-component.localization.json').replace(/[\\/]+/g, '/'), + path.join(relativeLibraryPath, 'src/components/test-component/test-component.translation.ts').replace(/[\\/]+/g, '/') + ]; + addedFiles.forEach((addedFile) => { + expect(diff.added).toContain(addedFile); + }); + expect(diff.added.length).toBe(addedFiles.length + 12); [applicationPath, ...untouchedProjectsPaths].forEach(untouchedProject => { expect(diff.all.some(file => file.startsWith(path.posix.relative(workspacePath, untouchedProject)))).toBe(false); diff --git a/packages/@o3r/rules-engine/schematics/index.it.spec.ts b/packages/@o3r/rules-engine/schematics/index.it.spec.ts index fe2f3108eb..a08e370576 100644 --- a/packages/@o3r/rules-engine/schematics/index.it.spec.ts +++ b/packages/@o3r/rules-engine/schematics/index.it.spec.ts @@ -28,12 +28,19 @@ describe('ng add rules-engine', () => { await addImportToAppModule(applicationPath, 'TestComponentModule', 'src/components/test-component'); const diff = getGitDiff(workspacePath); - expect(diff.added).toContain('apps/test-app/cms.json'); - expect(diff.added).toContain('apps/test-app/placeholders.metadata.json'); - expect(diff.added).toContain('apps/test-app/tsconfig.cms.json'); - expect(diff.added.length).toBe(12); - expect(diff.modified).toContain('apps/test-app/src/app/app.config.ts'); - expect(diff.modified.length).toBe(8); + const expectedAddedFiles = [ + 'apps/test-app/cms.json', + 'apps/test-app/placeholders.metadata.json', + 'apps/test-app/tsconfig.cms.json', + 'apps/test-app/migration-scripts/README.md' + ]; + expectedAddedFiles.forEach((file) => expect(diff.added).toContain(file)); + expect(diff.added.length).toBe(expectedAddedFiles.length + 9); // TODO define what are the remaining added files + const expectedModifiedFiles = [ + 'apps/test-app/src/app/app.config.ts' + ]; + expectedModifiedFiles.forEach((file) => expect(diff.modified).toContain(file)); + expect(diff.modified.length).toBe(expectedModifiedFiles.length + 7); // TODO define what are these modified files [libraryPath, ...untouchedProjectsPaths].forEach(untouchedProject => { expect(diff.all.some(file => file.startsWith(path.posix.relative(workspacePath, untouchedProject)))).toBe(false); @@ -56,7 +63,8 @@ describe('ng add rules-engine', () => { expect(diff.added).toContain('libs/test-lib/cms.json'); expect(diff.added).toContain('libs/test-lib/placeholders.metadata.json'); expect(diff.added).toContain('libs/test-lib/tsconfig.cms.json'); - expect(diff.added.length).toBe(12); + expect(diff.added).toContain('libs/test-lib/migration-scripts/README.md'); + expect(diff.added.length).toBe(13); expect(diff.modified).toContain('angular.json'); expect(diff.modified).toContain('package.json'); expect(diff.modified).toContain('libs/test-lib/package.json'); diff --git a/packages/@o3r/styling/builders/metadata-check/index.it.spec.ts b/packages/@o3r/styling/builders/metadata-check/index.it.spec.ts index 7a954f25a9..4ceebcc23e 100644 --- a/packages/@o3r/styling/builders/metadata-check/index.it.spec.ts +++ b/packages/@o3r/styling/builders/metadata-check/index.it.spec.ts @@ -12,12 +12,11 @@ import { getLatestPackageVersion, packageManagerAdd, packageManagerExec, - packageManagerPublish, - packageManagerVersion + packageManagerVersion, + publishToVerdaccio } from '@o3r/test-helpers'; -import { execFileSync } from 'node:child_process'; -import { promises, readFileSync } from 'node:fs'; -import { join } from 'node:path'; +import { existsSync, promises, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; import { inc } from 'semver'; import type { CssMetadata, CssVariable } from '@o3r/styling'; import type { MigrationStylingData } from './helpers/styling-metadata-comparison.helper'; @@ -25,7 +24,7 @@ import { getExternalDependenciesVersionRange } from '@o3r/schematics'; const baseVersion = '1.2.0'; const version = '1.3.0'; -const migrationDataFileName = `MIGRATION-${version}.json`; +const migrationDataFileName = `migration-scripts/MIGRATION-${version}.json`; const metadataFileName = 'style.metadata.json'; const defaultMigrationData: MigrationFile = { @@ -68,8 +67,11 @@ const newStylingMetadata: CssMetadata = { /* eslint-enable @typescript-eslint/naming-convention */ -function writeFileAsJSON(path: string, content: object) { - return promises.writeFile(path, JSON.stringify(content), { encoding: 'utf8' }); +async function writeFileAsJSON(path: string, content: object) { + if (!existsSync(dirname(path))) { + await promises.mkdir(dirname(path), {recursive: true}); + } + await promises.writeFile(path, JSON.stringify(content), { encoding: 'utf8' }); } const initTest = async ( @@ -81,8 +83,8 @@ const initTest = async ( const { workspacePath, appName, applicationPath, o3rVersion, isYarnTest } = o3rEnvironment.testEnvironment; const execAppOptions = { ...getDefaultExecSyncOptions(), cwd: applicationPath }; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; - packageManagerAdd(`@o3r/styling@${o3rVersion}`, execAppOptionsWorkspace); - packageManagerAdd(`@o3r/extractors@${o3rVersion}`, execAppOptionsWorkspace); + packageManagerExec({script: 'ng', args: ['add', `@o3r/extractors@${o3rVersion}`, '--skip-confirmation', '--project-name', appName]}, execAppOptionsWorkspace); + packageManagerExec({script: 'ng', args: ['add', `@o3r/styling@${o3rVersion}`, '--skip-confirmation', '--project-name', appName]}, execAppOptionsWorkspace); const versions = getExternalDependenciesVersionRange([ 'semver', ...(isYarnTest ? [ @@ -96,6 +98,7 @@ const initTest = async ( warn: jest.fn() } as any); Object.entries(versions).forEach(([pkgName, pkgVersion]) => packageManagerAdd(`${pkgName}@${pkgVersion}`, execAppOptionsWorkspace)); + const npmIgnorePath = join(applicationPath, '.npmignore'); const packageJsonPath = join(applicationPath, 'package.json'); const angularJsonPath = join(workspacePath, 'angular.json'); const metadataPath = join(applicationPath, metadataFileName); @@ -107,7 +110,7 @@ const initTest = async ( builder: '@o3r/styling:check-style-migration-metadata', options: { allowBreakingChanges, - migrationDataPath: `**/MIGRATION-*.json` + migrationDataPath: `apps/test-app/migration-scripts/MIGRATION-*.json` } }; angularJson.projects[appName].architect['check-metadata'] = builderConfig; @@ -122,6 +125,7 @@ const initTest = async ( private: false }; await writeFileAsJSON(packageJsonPath, packageJson); + await promises.writeFile(npmIgnorePath, ''); // Set old metadata and publish to registry await writeFileAsJSON(metadataPath, previousStylingMetadata); @@ -138,7 +142,7 @@ const initTest = async ( const args = getPackageManager() === 'yarn' ? [] : ['--no-git-tag-version', '-f']; packageManagerVersion(bumpedVersion, args, execAppOptions); - packageManagerPublish([], execAppOptions); + await publishToVerdaccio(execAppOptions); // Override with new metadata for comparison await writeFileAsJSON(metadataPath, newMetadata); @@ -148,27 +152,6 @@ const initTest = async ( }; describe('check metadata migration', () => { - beforeEach(async () => { - const { applicationPath } = o3rEnvironment.testEnvironment; - const execAppOptions = { ...getDefaultExecSyncOptions(), cwd: applicationPath, shell: true }; - await promises.copyFile( - join(__dirname, '..', '..', '..', '..', '..', '.verdaccio', 'conf', '.npmrc'), - join(applicationPath, '.npmrc') - ); - execFileSync('npx', [ - '--yes', - 'npm-cli-login', - '-u', - 'verdaccio', - '-p', - 'verdaccio', - '-e', - 'test@test.com', - '-r', - 'http://127.0.0.1:4873' - ], execAppOptions); - }); - test('should not throw', async () => { await initTest( true, diff --git a/packages/@o3r/styling/schematics/cms-adapter/index.ts b/packages/@o3r/styling/schematics/cms-adapter/index.ts index 86c9cc4bf5..225b1bccb4 100644 --- a/packages/@o3r/styling/schematics/cms-adapter/index.ts +++ b/packages/@o3r/styling/schematics/cms-adapter/index.ts @@ -15,7 +15,6 @@ function updateCmsAdapterFn(options: { projectName?: string | undefined }): Rule /** * Add cms extractors builder into the angular.json * @param tree - * @param _context * @param context */ const editAngularJson = (tree: Tree, context: SchematicContext) => { @@ -43,7 +42,7 @@ function updateCmsAdapterFn(options: { projectName?: string | undefined }): Rule workspaceProject.architect['check-style-migration-metadata'] ||= { builder: '@o3r/styling:check-style-migration-metadata', options: { - migrationDataPath: 'MIGRATION-*.json' + migrationDataPath: 'migration-scripts/dist/MIGRATION-*.json' } }; @@ -55,7 +54,6 @@ function updateCmsAdapterFn(options: { projectName?: string | undefined }): Rule /** * Add cms extractors scripts into the package.json * @param tree - * @param _context * @param context */ const addExtractorsScripts = (tree: Tree, context: SchematicContext) => { diff --git a/packages/@o3r/styling/schematics/index.it.spec.ts b/packages/@o3r/styling/schematics/index.it.spec.ts index 2f60fca8c1..8f0de91cc1 100644 --- a/packages/@o3r/styling/schematics/index.it.spec.ts +++ b/packages/@o3r/styling/schematics/index.it.spec.ts @@ -29,10 +29,13 @@ describe('ng add styling', () => { await addImportToAppModule(applicationPath, 'TestComponentModule', 'src/components/test-component'); const diff = getGitDiff(execAppOptions.cwd); - expect(diff.added.length).toBe(17); - expect(diff.added).toContain(path.join(relativeApplicationPath, 'src/components/test-component/test-component.style.theme.scss').replace(/[\\/]+/g, '/')); - - expect(diff.modified.length).toBe(7); + const expectedAddedFiles = [ + path.join(relativeApplicationPath, 'src/components/test-component/test-component.style.theme.scss').replace(/[\\/]+/g, '/'), + 'apps/test-app/migration-scripts/README.md' + ]; + expectedAddedFiles.forEach((file) => expect(diff.added).toContain(file)); + expect(diff.added.length).toBe(expectedAddedFiles.length + 16); // TODO define what are the remaining added files + expect(diff.modified.length).toBe(7); // TODO define what are these modified files [libraryPath, ...untouchedProjectsPaths].forEach(untouchedProject => { expect(diff.all.some(file => file.startsWith(path.posix.relative(workspacePath, untouchedProject)))).toBe(false); @@ -54,7 +57,7 @@ describe('ng add styling', () => { packageManagerExec({script: 'ng', args: ['g', '@o3r/styling:add-theming', '--path', filePath]}, execAppOptions); const diff = getGitDiff(execAppOptions.cwd); - expect(diff.added.length).toBe(13); + expect(diff.added.length).toBe(14); expect(diff.added).toContain(path.join(relativeLibraryPath, 'src/components/test-component/test-component.style.theme.scss').replace(/[\\/]+/g, '/')); expect(diff.modified.length).toBe(5); diff --git a/packages/@o3r/test-helpers/src/jest-environment/index.ts b/packages/@o3r/test-helpers/src/jest-environment/index.ts index 6f81ce0abd..f05051dddc 100644 --- a/packages/@o3r/test-helpers/src/jest-environment/index.ts +++ b/packages/@o3r/test-helpers/src/jest-environment/index.ts @@ -4,7 +4,7 @@ import { TestEnvironment as NodeTestEnvironment } from 'jest-environment-node'; import { execSync } from 'node:child_process'; import pidFromPort from 'pid-from-port'; import { rm } from 'node:fs/promises'; -import path from 'node:path'; +import { join } from 'node:path'; import { prepareTestEnv, type PrepareTestEnvType } from '../prepare-test-env'; /** @@ -17,7 +17,7 @@ declare global { var o3rEnvironment: {testEnvironment: TestEnvironment}; } -const rootFolder = path.join(__dirname, '..', '..', '..', '..'); +const rootFolder = join(__dirname, '..', '..', '..', '..'); /** * Custom Jest environment used to manage test environments with Verdaccio setup diff --git a/packages/@o3r/test-helpers/src/prepare-test-env.ts b/packages/@o3r/test-helpers/src/prepare-test-env.ts index 13b04f8440..957fabb25c 100644 --- a/packages/@o3r/test-helpers/src/prepare-test-env.ts +++ b/packages/@o3r/test-helpers/src/prepare-test-env.ts @@ -73,7 +73,11 @@ export async function prepareTestEnv(folderName: string, options?: PrepareTestEn return Promise.resolve(); }, {lockFilePath: `${itTestsFolderPath}.lock`, cwd: path.join(rootFolderPath, '..'), appDirectory: 'it-tests'}); } - const o3rExactVersion = getLatestPackageVersion('@o3r/create', { ...execAppOptions, cwd: itTestsFolderPath }).replace(/\s/g, ''); + const o3rExactVersion = getLatestPackageVersion('@o3r/create', { + ...execAppOptions, + cwd: itTestsFolderPath, + registry + }); // Remove existing app if (existsSync(workspacePath)) { diff --git a/packages/@o3r/test-helpers/src/utilities/package-manager.ts b/packages/@o3r/test-helpers/src/utilities/package-manager.ts index e62559e245..b869b131c6 100644 --- a/packages/@o3r/test-helpers/src/utilities/package-manager.ts +++ b/packages/@o3r/test-helpers/src/utilities/package-manager.ts @@ -302,8 +302,13 @@ export function setPackagerManagerConfig(options: PackageManagerConfig, execAppO * @param packageName * @param execAppOptions */ -export function getLatestPackageVersion(packageName: string, execAppOptions?: Partial) { - return execFileSync('npm', ['info', packageName, 'version'], { +export function getLatestPackageVersion(packageName: string, execAppOptions?: Partial & {registry?: string}) { + return execFileSync('npm', [ + 'info', + packageName, + 'version', + ...execAppOptions?.registry ? ['--registry', execAppOptions.registry] : [] + ], { ...execAppOptions, stdio: 'pipe', encoding: 'utf8', diff --git a/packages/@o3r/test-helpers/src/utilities/verdaccio.ts b/packages/@o3r/test-helpers/src/utilities/verdaccio.ts index 723d6f2503..60f8284e51 100644 --- a/packages/@o3r/test-helpers/src/utilities/verdaccio.ts +++ b/packages/@o3r/test-helpers/src/utilities/verdaccio.ts @@ -1,6 +1,8 @@ -import { execSync } from 'node:child_process'; -import * as path from 'node:path'; +import { execFileSync, execSync, type ExecSyncOptions } from 'node:child_process'; +import { existsSync, promises } from 'node:fs'; +import { join } from 'node:path'; import pidFromPort from 'pid-from-port'; +import { packageManagerPublish } from './package-manager'; /** * Set up a local npm registry inside a docker image before the tests. @@ -9,7 +11,7 @@ import pidFromPort from 'pid-from-port'; */ export function setupLocalRegistry() { let shouldHandleVerdaccio = false; - const rootFolder = path.join(__dirname, '..', '..', '..', '..'); + const rootFolder = join(__dirname, '..', '..', '..', '..'); beforeAll(async () => { try { @@ -27,3 +29,37 @@ export function setupLocalRegistry() { } }); } + +/** + * Publish the package in working directory (or options.cwd) to Verdaccio + * @param options + */ +export async function publishToVerdaccio(options: ExecSyncOptions) { + const registry = 'http://127.0.0.1:4873'; + const npmrcLoggedTarget = join(options.cwd?.toString() || process.cwd(), '.npmrc-logged'); + const npmrcLoggedSource = join(__dirname, '..', '..', '..', '..', '..', '.verdaccio', 'conf', '.npmrc-logged'); + // Try to reuse the authentication at repository level + if (existsSync(npmrcLoggedSource)) { + await promises.copyFile(npmrcLoggedSource, npmrcLoggedTarget); + } else { + // Create a new authentication at project level + if (!existsSync(npmrcLoggedTarget)) { + await promises.writeFile(npmrcLoggedTarget, `registry=${registry}/`); + } + execFileSync('npx', [ + '--yes', + 'npm-cli-login', + '-u', 'verdaccio', + '-p', 'verdaccio', + '-e', 'test@test.com', + '-r', registry, + '--config-path', npmrcLoggedTarget + ], {...options, shell: true}); + } + packageManagerPublish([ + '--registry', registry, + '--userconfig', npmrcLoggedTarget, + '--no-workspaces' + ], options); + return true; +}