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 5281011f1c..c0c3c19bc6 100644 --- a/packages/@o3r/components/builders/metadata-check/index.it.spec.ts +++ b/packages/@o3r/components/builders/metadata-check/index.it.spec.ts @@ -191,11 +191,12 @@ async function writeFileAsJSON(path: string, content: object) { } const initTest = async ( - allowBreakingChanges: boolean, newMetadata: ComponentConfigOutput[], migrationData: MigrationFile, - packageNameSuffix: string + packageNameSuffix: string, + options?: { allowBreakingChanges?: boolean; prerelease?: string } ) => { + const { allowBreakingChanges = false, prerelease } = options || {}; const { workspacePath, appName, applicationPath, o3rVersion, isYarnTest } = o3rEnvironment.testEnvironment; const execAppOptions = { ...getDefaultExecSyncOptions(), cwd: applicationPath }; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; @@ -253,7 +254,8 @@ const initTest = async ( latestVersion = baseVersion; } - const bumpedVersion = inc(latestVersion, 'patch'); + const prereleaseSuffix = prerelease ? `-${prerelease}.0` : ''; + const bumpedVersion = inc(latestVersion.replace(/-.*$/, ''), 'patch') + prereleaseSuffix; const args = getPackageManager() === 'yarn' ? [] : ['--no-git-tag-version', '-f']; packageManagerVersion(bumpedVersion, args, execAppOptions); @@ -270,10 +272,23 @@ const initTest = async ( describe('check metadata migration', () => { test('should not throw', async () => { await initTest( - true, newConfigurationMetadata, defaultMigrationData, - 'allow-breaking-changes' + 'allow-breaking-changes', + { allowBreakingChanges: true } + ); + const { workspacePath, appName } = o3rEnvironment.testEnvironment; + const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; + + expect(() => packageManagerExec({ script: 'ng', args: ['run', `${appName}:check-metadata`] }, execAppOptionsWorkspace)).not.toThrow(); + }); + + test('should not throw on prerelease', async () => { + await initTest( + newConfigurationMetadata, + defaultMigrationData, + 'allow-breaking-changes-prerelease', + { allowBreakingChanges: true, prerelease: 'rc' } ); const { workspacePath, appName } = o3rEnvironment.testEnvironment; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; @@ -283,13 +298,13 @@ describe('check metadata migration', () => { test('should throw because no migration data', async () => { await initTest( - true, newConfigurationMetadata, { ...defaultMigrationData, changes: [] }, - 'no-migration-data' + 'no-migration-data', + { allowBreakingChanges: true } ); const { workspacePath, appName } = o3rEnvironment.testEnvironment; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; @@ -310,7 +325,6 @@ describe('check metadata migration', () => { test('should throw because migration data invalid', async () => { await initTest( - true, [newConfigurationMetadata[0]], { ...defaultMigrationData, @@ -322,7 +336,8 @@ describe('check metadata migration', () => { } })) }, - 'invalid-data' + 'invalid-data', + { allowBreakingChanges: true } ); const { workspacePath, appName } = o3rEnvironment.testEnvironment; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; @@ -343,13 +358,13 @@ describe('check metadata migration', () => { test('should throw because breaking changes are not allowed', async () => { await initTest( - false, newConfigurationMetadata, { ...defaultMigrationData, changes: [] }, - 'breaking-changes' + 'breaking-changes', + { allowBreakingChanges: false } ); const { workspacePath, appName } = o3rEnvironment.testEnvironment; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; diff --git a/packages/@o3r/extractors/src/core/comparator/package-managers-extractors/custom-npm-semver-resolver.ts b/packages/@o3r/extractors/src/core/comparator/package-managers-extractors/custom-npm-semver-resolver.ts new file mode 100644 index 0000000000..d35a136150 --- /dev/null +++ b/packages/@o3r/extractors/src/core/comparator/package-managers-extractors/custom-npm-semver-resolver.ts @@ -0,0 +1,56 @@ +import { npmHttpUtils, NpmSemverFetcher, NpmSemverResolver } from '@yarnpkg/plugin-npm'; +import { Descriptor, miscUtils, Package, ResolveOptions, structUtils } from '@yarnpkg/core'; +import { Range, SemVer, valid } from 'semver'; + +/** + * Unexposed constant from yarn https://github.com/yarnpkg/berry/blob/master/packages/plugin-npm/sources/constants.ts + */ +const PROTOCOL = `npm:`; + +/** + * Extends NpmSeverResolver to support prerelease tags + * Check original code from https://github.com/yarnpkg/berry/blob/master/packages/plugin-npm/sources/NpmSemverResolver.ts + */ +export class CustomNpmSemverResolver extends NpmSemverResolver { + /** @inheritDoc */ + public override async getCandidates(descriptor: Descriptor, _dependencies: Record, opts: ResolveOptions) { + const range = new Range(descriptor.range.slice(PROTOCOL.length), {includePrerelease: true}); + const registryData = await npmHttpUtils.getPackageMetadata(descriptor, { + cache: opts.fetchOptions?.cache, + project: opts.project, + version: valid(range.raw) ? range.raw : undefined + }); + + const candidates = miscUtils.mapAndFilter(Object.keys(registryData.versions), (version) => { + try { + const candidate = new SemVer(version, {includePrerelease: true}); + if (range.test(candidate)) { + return candidate; + } + } catch { } + + return miscUtils.mapAndFilter.skip; + }); + + const noDeprecatedCandidates = candidates.filter((version) => !registryData.versions[version.raw].deprecated); + + // If there are versions that aren't deprecated, use them + const finalCandidates = noDeprecatedCandidates.length > 0 + ? noDeprecatedCandidates + : candidates; + + finalCandidates.sort((a, b) => -a.compare(b)); + + return finalCandidates.map(version => { + const versionLocator = structUtils.makeLocator(descriptor, `${PROTOCOL}${version.raw}`); + const archiveUrl = registryData.versions[version.raw].dist.tarball; + + if (NpmSemverFetcher.isConventionalTarballUrl(versionLocator, archiveUrl, {configuration: opts.project.configuration})) { + return versionLocator; + } else { + // eslint-disable-next-line @typescript-eslint/naming-convention + return structUtils.bindLocator(versionLocator, {__archiveUrl: archiveUrl}); + } + }); + } +} diff --git a/packages/@o3r/extractors/src/core/comparator/package-managers-extractors/npm-file-extractor.helper.ts b/packages/@o3r/extractors/src/core/comparator/package-managers-extractors/npm-file-extractor.helper.ts index 6db7826ec9..6fc458a913 100644 --- a/packages/@o3r/extractors/src/core/comparator/package-managers-extractors/npm-file-extractor.helper.ts +++ b/packages/@o3r/extractors/src/core/comparator/package-managers-extractors/npm-file-extractor.helper.ts @@ -31,18 +31,22 @@ export async function getFilesFromRegistry(packageDescriptor: string, paths: str const tempDirPath = join(tmpdir(), tempDirName); let extractedFiles: { [key: string]: string } = {}; mkdirSync(tempDirPath); + const [,packageName,packageRange] = sanitizeInput(packageDescriptor).match(/^(.*?)(?:\b@(.+))?$/) || []; try { const npmViewCmd = runAndThrowOnError( - `npm view "${sanitizeInput(packageDescriptor)}" version --json`, + `npm view "${packageName}" versions --json`, { shell: true, encoding: 'utf8' } ); - const versions = JSON.parse(npmViewCmd.stdout.trim()) as string[] | string; + let versions = JSON.parse(npmViewCmd.stdout.trim()) as string[] | string; if (typeof versions !== 'string') { + if (packageRange) { + const range = new semver.Range(packageRange, {includePrerelease: true}); + versions = versions.filter((v) => range.test(v)); + } versions.sort((a, b) => semver.compare(b, a)); } const latestVersion = typeof versions === 'string' ? versions : versions[0]; - const packageName = packageDescriptor.replace(/\b@[^@]+$/, ''); const npmPackCmd = runAndThrowOnError( `npm pack "${packageName}@${sanitizeInput(latestVersion)}" --pack-destination "${pathToPosix(tempDirPath)}"`, diff --git a/packages/@o3r/extractors/src/core/comparator/package-managers-extractors/yarn2-file-extractor.helper.ts b/packages/@o3r/extractors/src/core/comparator/package-managers-extractors/yarn2-file-extractor.helper.ts index 2b73808585..b9ee043515 100644 --- a/packages/@o3r/extractors/src/core/comparator/package-managers-extractors/yarn2-file-extractor.helper.ts +++ b/packages/@o3r/extractors/src/core/comparator/package-managers-extractors/yarn2-file-extractor.helper.ts @@ -18,6 +18,7 @@ import { npath } from '@yarnpkg/fslib'; import yarnNpmPlugin from '@yarnpkg/plugin-npm'; import { join } from 'node:path'; import { O3rCliError } from '@o3r/schematics'; +import { CustomNpmSemverResolver } from './custom-npm-semver-resolver'; // Class copied from https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/MultiResolver.ts // because it is not exposed in @yarnpkg/core @@ -146,7 +147,7 @@ async function fetchPackage(project: Project, descriptor: Descriptor): Promise new resolver()) + ([CustomNpmSemverResolver, ...yarnNpmPlugin.resolvers || []]).map((resolver) => new resolver()) ); const multiFetcher = new MultiFetcher( // eslint-disable-next-line new-cap 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 cf38a546cd..8cac55688d 100644 --- a/packages/@o3r/localization/builders/metadata-check/index.it.spec.ts +++ b/packages/@o3r/localization/builders/metadata-check/index.it.spec.ts @@ -67,11 +67,12 @@ async function writeFileAsJSON(path: string, content: object) { } const initTest = async ( - allowBreakingChanges: boolean, newMetadata: LocalizationMetadata, migrationData: MigrationFile, - packageNameSuffix: string + packageNameSuffix: string, + options?: { allowBreakingChanges?: boolean; prerelease?: string } ) => { + const { allowBreakingChanges = false, prerelease } = options || {}; const { workspacePath, appName, applicationPath, o3rVersion, isYarnTest } = o3rEnvironment.testEnvironment; const execAppOptions = { ...getDefaultExecSyncOptions(), cwd: applicationPath }; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; @@ -129,7 +130,8 @@ const initTest = async ( latestVersion = baseVersion; } - const bumpedVersion = inc(latestVersion, 'patch'); + const prereleaseSuffix = prerelease ? `-${prerelease}.0` : ''; + const bumpedVersion = inc(latestVersion.replace(/-.*$/, ''), 'patch') + prereleaseSuffix; const args = getPackageManager() === 'yarn' ? [] : ['--no-git-tag-version', '-f']; packageManagerVersion(bumpedVersion, args, execAppOptions); @@ -146,10 +148,23 @@ const initTest = async ( describe('check metadata migration', () => { test('should not throw', async () => { await initTest( - true, newLocalizationMetadata, defaultMigrationData, - 'allow-breaking-changes' + 'allow-breaking-changes', + { allowBreakingChanges: true } + ); + const { workspacePath, appName } = o3rEnvironment.testEnvironment; + const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; + + expect(() => packageManagerExec({ script: 'ng', args: ['run', `${appName}:check-metadata`] }, execAppOptionsWorkspace)).not.toThrow(); + }); + + test('should not throw on prerelease', async () => { + await initTest( + newLocalizationMetadata, + defaultMigrationData, + 'allow-breaking-changes-prerelease', + { allowBreakingChanges: true, prerelease: 'rc' } ); const { workspacePath, appName } = o3rEnvironment.testEnvironment; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; @@ -159,13 +174,13 @@ describe('check metadata migration', () => { test('should throw because no migration data', async () => { await initTest( - true, newLocalizationMetadata, { ...defaultMigrationData, changes: [] }, - 'no-migration-data' + 'no-migration-data', + { allowBreakingChanges: true } ); const { workspacePath, appName } = o3rEnvironment.testEnvironment; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; @@ -185,7 +200,6 @@ describe('check metadata migration', () => { test('should throw because migration data invalid', async () => { await initTest( - true, [newLocalizationMetadata[0]], { ...defaultMigrationData, @@ -197,7 +211,8 @@ describe('check metadata migration', () => { } })) }, - 'invalid-data' + 'invalid-data', + { allowBreakingChanges: true } ); const { workspacePath, appName } = o3rEnvironment.testEnvironment; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; @@ -217,13 +232,13 @@ describe('check metadata migration', () => { test('should throw because breaking changes are not allowed', async () => { await initTest( - false, newLocalizationMetadata, { ...defaultMigrationData, changes: [] }, - 'breaking-changes' + 'breaking-changes', + { allowBreakingChanges: false } ); const { workspacePath, appName } = o3rEnvironment.testEnvironment; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; 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 4ceebcc23e..5f76ac05fe 100644 --- a/packages/@o3r/styling/builders/metadata-check/index.it.spec.ts +++ b/packages/@o3r/styling/builders/metadata-check/index.it.spec.ts @@ -75,11 +75,12 @@ async function writeFileAsJSON(path: string, content: object) { } const initTest = async ( - allowBreakingChanges: boolean, newMetadata: CssMetadata, migrationData: MigrationFile, - packageNameSuffix: string + packageNameSuffix: string, + options?: { allowBreakingChanges?: boolean; prerelease?: string } ) => { + const { allowBreakingChanges = false, prerelease } = options || {}; const { workspacePath, appName, applicationPath, o3rVersion, isYarnTest } = o3rEnvironment.testEnvironment; const execAppOptions = { ...getDefaultExecSyncOptions(), cwd: applicationPath }; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; @@ -137,7 +138,8 @@ const initTest = async ( latestVersion = baseVersion; } - const bumpedVersion = inc(latestVersion, 'patch'); + const prereleaseSuffix = prerelease ? `-${prerelease}.0` : ''; + const bumpedVersion = inc(latestVersion.replace(/-.*$/, ''), 'patch') + prereleaseSuffix; const args = getPackageManager() === 'yarn' ? [] : ['--no-git-tag-version', '-f']; packageManagerVersion(bumpedVersion, args, execAppOptions); @@ -154,10 +156,23 @@ const initTest = async ( describe('check metadata migration', () => { test('should not throw', async () => { await initTest( - true, newStylingMetadata, defaultMigrationData, - 'allow-breaking-changes' + 'allow-breaking-changes', + { allowBreakingChanges: true } + ); + const { workspacePath, appName } = o3rEnvironment.testEnvironment; + const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; + + expect(() => packageManagerExec({ script: 'ng', args: ['run', `${appName}:check-metadata`] }, execAppOptionsWorkspace)).not.toThrow(); + }); + + test('should not throw on prerelease', async () => { + await initTest( + newStylingMetadata, + defaultMigrationData, + 'allow-breaking-changes-prerelease', + { allowBreakingChanges: true, prerelease: 'rc' } ); const { workspacePath, appName } = o3rEnvironment.testEnvironment; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; @@ -167,13 +182,13 @@ describe('check metadata migration', () => { test('should throw because no migration data', async () => { await initTest( - true, newStylingMetadata, { ...defaultMigrationData, changes: [] }, - 'no-migration-data' + 'no-migration-data', + { allowBreakingChanges: true } ); const { workspacePath, appName } = o3rEnvironment.testEnvironment; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; @@ -193,7 +208,6 @@ describe('check metadata migration', () => { test('should throw because migration data invalid', async () => { await initTest( - true, { variables: { unchangedVariableName: newStylingMetadata.variables[unchangedVariableName] @@ -209,7 +223,8 @@ describe('check metadata migration', () => { } })) }, - 'invalid-data' + 'invalid-data', + { allowBreakingChanges: true } ); const { workspacePath, appName } = o3rEnvironment.testEnvironment; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath }; @@ -229,13 +244,13 @@ describe('check metadata migration', () => { test('should throw because breaking changes are not allowed', async () => { await initTest( - false, newStylingMetadata, { ...defaultMigrationData, changes: [] }, - 'breaking-changes' + 'breaking-changes', + { allowBreakingChanges: false } ); const { workspacePath, appName } = o3rEnvironment.testEnvironment; const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath };