Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,43 +46,68 @@ 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: {
react: '17.x.x',
'@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: {},
},
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -145,20 +172,24 @@ describe('normalize-package-dependencies generator', () => {
" - @proj/react-one@^1.0.0",
" - @proj/react-two@^1.0.0",
" - @proj/[email protected]",
" - @proj/[email protected]",
" - @proj/[email protected]",
"",
]
`);
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(
Expand All @@ -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",
"",
]
`);
Expand All @@ -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': '*',
Expand All @@ -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, {});
Expand All @@ -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',
},
});

Expand All @@ -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',
}),
);
});
Expand Down Expand Up @@ -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' });
});
Expand Down Expand Up @@ -327,17 +370,19 @@ function createProject(
options: {
projectName: string;
projectType?: 'application' | 'library';
projectVersion: string;
tags?: string[];
deps: { prod: Record<string, string>; dev: Record<string, string>; peer: Record<string, string> };
},
) {
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 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
logger,
createProjectGraphAsync,
ProjectGraph,
readProjectConfiguration,
} from '@nx/devkit';
import chalk from 'chalk';
import semver from 'semver';
Expand All @@ -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);
Expand All @@ -33,23 +32,32 @@ 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);

await formatFiles(tree);
}

function normalizePackageJsonDependencies(tree: Tree, projectConfig: ProjectConfiguration, graph: ProjectGraph) {
function normalizePackageJsonDependencies(
tree: Tree,
options: {
allProjects: ReturnType<typeof getProjects>;
projectConfig: ProjectConfiguration;
graph: ProjectGraph;
},
) {
const { allProjects, graph, projectConfig } = options;
const projectDependencies = getProjectDependenciesFromGraph(projectConfig.name!, graph);
const packageJsonPath = joinPathFragments(projectConfig.root, 'package.json');

Expand All @@ -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;
}
}
Expand All @@ -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<string, string>, packageName: string) {
function getVersion(
tree: Tree,
options: {
allProjects: ReturnType<typeof getProjects>;
deps: Record<string, string>;
packageName: string;
},
) {
const { allProjects, deps, packageName } = options;
const current = deps[packageName];
const updated = getUpdatedVersion(current);

Expand All @@ -114,20 +130,25 @@ function getVersion(tree: Tree, deps: Record<string, string>, 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<PackageJson>(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;
Expand All @@ -136,12 +157,15 @@ function getVersion(tree: Tree, deps: Record<string, string>, packageName: strin

function getPackageJsonDependenciesIssues(
tree: Tree,
projectConfig: ProjectConfiguration,
graph: ProjectGraph,
options: {
allProjects: ReturnType<typeof getProjects>;
projectConfig: ProjectConfiguration;
graph: ProjectGraph;
},
): Record<string, string> | null {
const { allProjects, projectConfig, graph } = options;
const projectDependencies = getProjectDependenciesFromGraph(projectConfig.name!, graph);
const packageJsonPath = joinPathFragments(projectConfig.root, 'package.json');
const packageJson = readJson<PackageJson>(tree, packageJsonPath);
const packageJson = readJson<PackageJson>(tree, joinPathFragments(projectConfig.root, 'package.json'));

let issues: Record<string, string> | null = null;
checkDepType(packageJson, 'devDependencies');
Expand All @@ -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 ?? {};
Expand Down Expand Up @@ -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`;
}