diff --git a/tools/workspace-plugin/src/generators/normalize-package-dependencies/index.spec.ts b/tools/workspace-plugin/src/generators/normalize-package-dependencies/index.spec.ts index e1b52b2352fb8..c30f467694adc 100644 --- a/tools/workspace-plugin/src/generators/normalize-package-dependencies/index.spec.ts +++ b/tools/workspace-plugin/src/generators/normalize-package-dependencies/index.spec.ts @@ -46,36 +46,59 @@ describe('normalize-package-dependencies generator', () => { tree = createTreeWithEmptyWorkspace(); tree = createProject(tree, { projectName: 'react-one', + projectVersion: '1.0.0', deps: { dev: { '@proj/build-tool': '^1.0.0' }, prod: { react: '17.x.x' }, peer: {} }, tags: ['platform:any'], }); tree = createProject(tree, { projectName: 'react-two', + projectVersion: '1.0.0', deps: { dev: { '@proj/build-tool': '^1.0.0' }, prod: { react: '17.x.x', '@proj/react-one': '^1.0.0' }, peer: {} }, tags: ['platform:any', 'scope:two'], }); tree = createProject(tree, { projectName: 'react-three', + projectVersion: '0.1.0', deps: { dev: {}, - prod: { react: '17.x.x', '@proj/react-two': '^1.0.0', '@proj/react-four': '1.0.0-beta.17' }, + prod: { + react: '17.x.x', + '@proj/react-two': '^1.0.0', + '@proj/react-four': '1.0.0-beta.17', + '@proj/react-five': '9.0.0-alpha.17', + }, peer: {}, }, tags: ['platform:any', 'scope:two'], }); tree = createProject(tree, { projectName: 'react-four', + projectVersion: '1.0.0-beta.17', deps: { dev: {}, prod: { react: '17.x.x' }, peer: {} }, tags: ['platform:any', 'scope:two'], }); + tree = createProject(tree, { + projectName: 'react-five', + projectVersion: '9.0.0-alpha.17', + deps: { dev: {}, prod: { react: '17.x.x' }, peer: {} }, + tags: ['platform:any', 'scope:two'], + }); + tree = createProject(tree, { + projectName: 'web-component-one', + projectVersion: '3.0.0-beta.17', + deps: { dev: {}, prod: { 'lit-element': '3.x.x' }, peer: {} }, + tags: ['platform:web', 'scope:web-components'], + }); tree = createProject(tree, { projectName: 'build-tool', + projectVersion: '0.0.1', deps: { dev: {}, prod: { nx: '16.x.x' }, peer: {} }, tags: ['platform:node', 'scope:tools'], }); tree = createProject(tree, { projectName: 'react-app', projectType: 'application', + projectVersion: '0.0.1', deps: { dev: { '@proj/build-tool': '^1.0.0' }, prod: { @@ -83,6 +106,8 @@ describe('normalize-package-dependencies generator', () => { '@proj/react-one': '^1.0.0', '@proj/react-two': '^1.0.0', '@proj/react-four': '1.0.0-beta.17', + '@proj/react-five': '9.0.0-alpha.17', + '@proj/web-component-one': '3.0.0-beta.17', }, peer: {}, }, @@ -117,6 +142,8 @@ describe('normalize-package-dependencies generator', () => { '@proj/react-one': '^1.0.0', '@proj/react-two': '^1.0.0', '@proj/react-four': '1.0.0-beta.17', + '@proj/react-five': '9.0.0-alpha.17', + '@proj/web-component-one': '3.0.0-beta.17', }); expect(reactApp.devDependencies).toEqual({ '@proj/build-tool': '^1.0.0', @@ -145,20 +172,24 @@ describe('normalize-package-dependencies generator', () => { " - @proj/react-one@^1.0.0", " - @proj/react-two@^1.0.0", " - @proj/react-four@1.0.0-beta.17", + " - @proj/react-five@9.0.0-alpha.17", + " - @proj/web-component-one@3.0.0-beta.17", "", ] `); expect(infoLogSpy.mock.calls.flat()).toMatchInlineSnapshot(` Array [ - "All these dependencies version should be specified as '*' or '>=9.0.0-alpha' ", - "Fix this by running 'nx g @fluentui/workspace-plugin:normalize-package-dependencies'", + "All these dependencies version should be specified as '*' or '>={MAJOR}.0.0-alpha' (NOTE: 'MAJOR' equals to specified package major version)", + "🛠️ FIX: run 'nx g @fluentui/workspace-plugin:normalize-package-dependencies'", ] `); }); it(`should report if prerelease package range changed to normal release version`, async () => { + // normalize versions await generator(tree, {}); + // change version of pre-release package to zero based major standard release updateProject(tree, { projectName: 'react-four', version: '0.1.0' }); await expect(generator(tree, { verify: true })).rejects.toThrowErrorMatchingInlineSnapshot( @@ -168,7 +199,7 @@ describe('normalize-package-dependencies generator', () => { expect(logLogSpy.mock.calls.flat()).toMatchInlineSnapshot(` Array [ "@proj/react-app has following dependency version issues:", - " - @proj/react-four@>=9.0.0-alpha", + " - @proj/react-four@>=1.0.0-alpha", "", ] `); @@ -189,10 +220,15 @@ describe('normalize-package-dependencies generator', () => { expect(reactApp.dependencies).toEqual({ react: '17.x.x', + // workspace dependencies '@proj/react-one': '*', '@proj/react-two': '*', + // non workspace dependency - different version of original workspace installed from npm '@proj/react-three': '^0.1.0', - '@proj/react-four': '>=9.0.0-alpha', + // workspace dependencies pre-releases + '@proj/react-five': '>=9.0.0-alpha', + '@proj/react-four': '>=1.0.0-alpha', + '@proj/web-component-one': '>=3.0.0-alpha', }); expect(reactApp.devDependencies).toEqual({ '@proj/build-tool': '*', @@ -206,10 +242,13 @@ describe('normalize-package-dependencies generator', () => { expect(reactApp.dependencies).toEqual( expect.objectContaining({ - '@proj/react-four': '>=9.0.0-alpha', + '@proj/react-four': '>=1.0.0-alpha', + '@proj/react-five': '>=9.0.0-alpha', + '@proj/web-component-one': '>=3.0.0-alpha', }), ); + // change version of pre-release package to zero based major standard release updateProject(tree, { projectName: 'react-four', version: '0.1.0' }); await generator(tree, {}); @@ -224,10 +263,12 @@ describe('normalize-package-dependencies generator', () => { }); it(`should revert incorrect beachball bump change version on pre-release package`, async () => { + // simulate beachball bumping version of pre-release package to incorrect version updateProject(tree, { projectName: 'react-app', dependencies: { - '@proj/react-four': '1.0.0-beta.17 <9.0.0', + '@proj/web-component-one': '3.0.0-beta.18 <3.0.0', + '@proj/react-five': '9.0.0-alpha.18 <9.0.0', }, }); @@ -237,7 +278,8 @@ describe('normalize-package-dependencies generator', () => { expect(reactApp.dependencies).toEqual( expect.objectContaining({ - '@proj/react-four': '>=9.0.0-alpha', + '@proj/web-component-one': '>=3.0.0-alpha', + '@proj/react-five': '>=9.0.0-alpha', }), ); }); @@ -273,6 +315,7 @@ describe('normalize-package-dependencies generator', () => { '@proj/react-one': '^0.1.0', '@proj/react-two': '^1.0.0', '@proj/react-four': '1.0.0-beta.17', + '@proj/react-five': '9.0.0-alpha.17', }); expect(reactThree.devDependencies).toEqual({ '@proj/build-tool': '^0.1.0' }); }); @@ -327,17 +370,19 @@ function createProject( options: { projectName: string; projectType?: 'application' | 'library'; + projectVersion: string; tags?: string[]; deps: { prod: Record; dev: Record; peer: Record }; }, ) { - const { projectName, deps, tags, projectType = 'library' } = options; + const { projectName, projectVersion, deps, tags, projectType = 'library' } = options; const packageName = `@proj/${projectName}`; const rootPath = `packages/${projectName}`; writeJson(tree, `packages/${projectName}/package.json`, { name: packageName, + version: projectVersion, dependencies: { ...deps.prod }, devDependencies: { ...deps.dev }, peerDependencies: { ...deps.peer }, diff --git a/tools/workspace-plugin/src/generators/normalize-package-dependencies/index.ts b/tools/workspace-plugin/src/generators/normalize-package-dependencies/index.ts index 6bcaca365f372..0fa9a90727e3a 100644 --- a/tools/workspace-plugin/src/generators/normalize-package-dependencies/index.ts +++ b/tools/workspace-plugin/src/generators/normalize-package-dependencies/index.ts @@ -9,7 +9,6 @@ import { logger, createProjectGraphAsync, ProjectGraph, - readProjectConfiguration, } from '@nx/devkit'; import chalk from 'chalk'; import semver from 'semver'; @@ -20,8 +19,8 @@ import { PackageJson } from '../../types'; type ProjectIssues = { [projectName: string]: { [depName: string]: string } }; const NORMALIZED_INNER_WORKSPACE_VERSION = '*'; -const NORMALIZED_PRERELEASE_RANGE_VERSION = '>=9.0.0-alpha'; -const BEACHBALL_UNWANTED_PRERELEASE_RANGE_VERSION_REGEXP = /<9.0.0$/; +const NORMALIZED_PRERELEASE_RANGE_VERSION_REGEXP = /^>=\d\.\d\.\d-alpha$/; +const BEACHBALL_UNWANTED_PRERELEASE_RANGE_VERSION_REGEXP = /\s<\d\.0\.0$/; export default async function (tree: Tree, schema: NormalizePackageDependenciesGeneratorSchema) { const normalizedOptions = normalizeOptions(tree, schema); @@ -33,15 +32,16 @@ export default async function (tree: Tree, schema: NormalizePackageDependenciesG projects.forEach(projectConfig => { if (normalizedOptions.verify) { - const foundIssues = getPackageJsonDependenciesIssues(tree, projectConfig, graph); + const foundIssues = getPackageJsonDependenciesIssues(tree, { allProjects: projects, projectConfig, graph }); if (foundIssues) { issues[projectConfig.name!] = foundIssues; } + return; } - normalizePackageJsonDependencies(tree, projectConfig, graph); + normalizePackageJsonDependencies(tree, { allProjects: projects, projectConfig, graph }); }); reportPackageJsonDependenciesIssues(issues); @@ -49,7 +49,15 @@ export default async function (tree: Tree, schema: NormalizePackageDependenciesG await formatFiles(tree); } -function normalizePackageJsonDependencies(tree: Tree, projectConfig: ProjectConfiguration, graph: ProjectGraph) { +function normalizePackageJsonDependencies( + tree: Tree, + options: { + allProjects: ReturnType; + projectConfig: ProjectConfiguration; + graph: ProjectGraph; + }, +) { + const { allProjects, graph, projectConfig } = options; const projectDependencies = getProjectDependenciesFromGraph(projectConfig.name!, graph); const packageJsonPath = joinPathFragments(projectConfig.root, 'package.json'); @@ -74,7 +82,7 @@ function normalizePackageJsonDependencies(tree: Tree, projectConfig: ProjectConf for (const packageName in deps) { if (isProjectDependencyAnWorkspaceProject(graph, packageName, projectDependencies)) { - const { updated } = getVersion(tree, deps, packageName); + const { updated } = getVersion(tree, { allProjects, deps, packageName }); deps[packageName] = updated; } } @@ -98,14 +106,22 @@ function reportPackageJsonDependenciesIssues(issues: ProjectIssues) { }); logger.info( - `All these dependencies version should be specified as '${NORMALIZED_INNER_WORKSPACE_VERSION}' or '${NORMALIZED_PRERELEASE_RANGE_VERSION}' `, + `All these dependencies version should be specified as '${NORMALIZED_INNER_WORKSPACE_VERSION}' or '>={MAJOR}.0.0-alpha' (NOTE: 'MAJOR' equals to specified package major version)`, ); - logger.info(`Fix this by running 'nx g @fluentui/workspace-plugin:normalize-package-dependencies'`); + logger.info(`🛠️ FIX: run 'nx g @fluentui/workspace-plugin:normalize-package-dependencies'`); throw new Error('package dependency violations found'); } -function getVersion(tree: Tree, deps: Record, packageName: string) { +function getVersion( + tree: Tree, + options: { + allProjects: ReturnType; + deps: Record; + packageName: string; + }, +) { + const { allProjects, deps, packageName } = options; const current = deps[packageName]; const updated = getUpdatedVersion(current); @@ -114,20 +130,25 @@ function getVersion(tree: Tree, deps: Record, packageName: strin return { updated, match }; function getUpdatedVersion(currentVersion: string) { - if (BEACHBALL_UNWANTED_PRERELEASE_RANGE_VERSION_REGEXP.test(current)) { - return NORMALIZED_PRERELEASE_RANGE_VERSION; + if (BEACHBALL_UNWANTED_PRERELEASE_RANGE_VERSION_REGEXP.test(currentVersion)) { + return transformVersionToPreReleaseWorkspaceRange(currentVersion); } - if (currentVersion === NORMALIZED_PRERELEASE_RANGE_VERSION) { - const prereleasePkg = readProjectConfiguration(tree, packageName); + if (NORMALIZED_PRERELEASE_RANGE_VERSION_REGEXP.test(currentVersion)) { + const prereleasePkg = allProjects.get(packageName); + if (!prereleasePkg) { + throw new Error(`Package ${packageName} not found in the workspace`); + } const prereleasePkgJson = readJson(tree, joinPathFragments(prereleasePkg.root, 'package.json')); const isPrerelease = semver.prerelease(prereleasePkgJson.version) !== null; - return isPrerelease ? NORMALIZED_PRERELEASE_RANGE_VERSION : NORMALIZED_INNER_WORKSPACE_VERSION; + return isPrerelease + ? transformVersionToPreReleaseWorkspaceRange(currentVersion) + : NORMALIZED_INNER_WORKSPACE_VERSION; } - if (semver.prerelease(current)) { - return NORMALIZED_PRERELEASE_RANGE_VERSION; + if (semver.prerelease(currentVersion)) { + return transformVersionToPreReleaseWorkspaceRange(currentVersion); } return NORMALIZED_INNER_WORKSPACE_VERSION; @@ -136,12 +157,15 @@ function getVersion(tree: Tree, deps: Record, packageName: strin function getPackageJsonDependenciesIssues( tree: Tree, - projectConfig: ProjectConfiguration, - graph: ProjectGraph, + options: { + allProjects: ReturnType; + projectConfig: ProjectConfiguration; + graph: ProjectGraph; + }, ): Record | null { + const { allProjects, projectConfig, graph } = options; const projectDependencies = getProjectDependenciesFromGraph(projectConfig.name!, graph); - const packageJsonPath = joinPathFragments(projectConfig.root, 'package.json'); - const packageJson = readJson(tree, packageJsonPath); + const packageJson = readJson(tree, joinPathFragments(projectConfig.root, 'package.json')); let issues: Record | null = null; checkDepType(packageJson, 'devDependencies'); @@ -161,7 +185,7 @@ function getPackageJsonDependenciesIssues( // eslint-disable-next-line guard-for-in for (const packageName in deps) { - const { match } = getVersion(tree, deps, packageName); + const { match } = getVersion(tree, { allProjects, deps, packageName }); if (isProjectDependencyAnWorkspaceProject(graph, packageName, projectDependencies) && !match) { issues = issues ?? {}; @@ -199,3 +223,13 @@ function normalizeOptions(tree: Tree, schema: NormalizePackageDependenciesGenera ...options, }; } + +function transformVersionToPreReleaseWorkspaceRange(version: string) { + const coercedVersion = semver.coerce(version); + + if (!coercedVersion) { + throw new Error(`Invalid version: ${version}`); + } + + return `>=${coercedVersion.major}.${coercedVersion.minor}.${coercedVersion.patch}-alpha`; +}