fix(storybook): fix package.json updates so @storybook packages are in sync during migration (#30191)

This PR is the same as https://github.com/nrwl/nx/pull/30187 but for
`@storybook` packages. We want to make sure that workspaces that have
other `@storybook/*` packages installed have their versions updated
along with the packages we use. Otherwise version mismatches can lead to
errors due to changing APIs.

This PR also adds a conformance rule that prevents mistakes from going
out in future migrations.
This commit is contained in:
Jack Hsu 2025-02-27 16:50:32 -05:00 committed by GitHub
parent 8b11d8bfe5
commit e8647df08a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1031 additions and 0 deletions

View File

@ -23,6 +23,178 @@
"alwaysAddToPackageJson": true "alwaysAddToPackageJson": true
}, },
"storybook": { "version": "^8.2.8", "alwaysAddToPackageJson": true }, "storybook": { "version": "^8.2.8", "alwaysAddToPackageJson": true },
"@storybook/addon-controls": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-jest": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-mdx-gfm": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-onboarding": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-themes": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/blocks": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/builder-manager": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/builder-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/cli": { "version": "^8.2.8", "alwaysAddToPackageJson": false },
"@storybook/components": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/core": { "version": "^8.2.8", "alwaysAddToPackageJson": false },
"@storybook/core-common": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/core-events": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/core-webpack": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/csf-tools": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/html": { "version": "^8.2.8", "alwaysAddToPackageJson": false },
"@storybook/html-vite": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/html-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/manager": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/manager-api": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/nextjs": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preact": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preact-vite": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preact-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preset-create-react-app": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preset-html-webpack": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preset-preact-webpack": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preset-react-webpack": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preset-server-webpack": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preset-vue3-webpack": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/react-vite": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/react-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/router": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/server": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/server-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/svelte": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/svelte-vite": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/sveltekit": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/theming": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/types": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/vue3": { "version": "^8.2.8", "alwaysAddToPackageJson": false },
"@storybook/vue3-vite": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/vue3-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/web-components": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/web-components-vite": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/web-components-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/test-runner": { "@storybook/test-runner": {
"version": "^0.19.0", "version": "^0.19.0",
"alwaysAddToPackageJson": false "alwaysAddToPackageJson": false

View File

@ -102,6 +102,145 @@
"@storybook/vue3-vite": { "@storybook/vue3-vite": {
"version": "^8.4.6", "version": "^8.4.6",
"alwaysAddToPackageJson": false "alwaysAddToPackageJson": false
},
"@storybook/addon-onboarding": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/addon-themes": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/blocks": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/builder-manager": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/builder-webpack5": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/cli": { "version": "^8.4.6", "alwaysAddToPackageJson": false },
"@storybook/components": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/core": { "version": "^8.4.6", "alwaysAddToPackageJson": false },
"@storybook/core-common": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/core-events": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/core-webpack": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/csf-tools": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/html": { "version": "^8.4.6", "alwaysAddToPackageJson": false },
"@storybook/html-vite": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/html-webpack5": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/manager": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/manager-api": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/nextjs": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preact": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preact-vite": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preact-webpack5": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preset-create-react-app": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preset-html-webpack": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preset-preact-webpack": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preset-react-webpack": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preset-server-webpack": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preset-vue3-webpack": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/router": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/server": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/server-webpack5": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/svelte": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/svelte-vite": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/sveltekit": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/theming": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/types": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/vue3-webpack5": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/web-components": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
} }
}, },
"aliases": [], "aliases": [],

80
nx.json
View File

@ -289,6 +289,86 @@
"!webpack", "!webpack",
"!workspace" "!workspace"
] ]
},
{
"rule": "@nx/workspace-plugin/conformance-rules/migration-groups",
"options": {
"versionRange": ">= 19.8",
"groups": [
[
"angular-eslint",
"@angular-eslint/eslint-plugin",
"@angular-eslint/eslint-plugin-template",
"@angular-eslint/template-parser",
"@angular-eslint/utils",
"@angular-eslint/schematics",
"@angular-eslint/test-utils",
"@angular-eslint/builder",
"@angular-eslint/bundled-angular-compiler"
],
[
"typescript-eslint",
"@typescript-eslint/eslint-plugin",
"@typescript-eslint/parser",
"@typescript-eslint/utils",
"@typescript-eslint/rule-tester",
"@typescript-eslint/scope-manager",
"@typescript-eslint/typescript-estree"
],
[
"@storybook/addon-controls",
"@storybook/addon-essentials",
"@storybook/addon-jest",
"@storybook/addon-mdx-gfm",
"@storybook/addon-onboarding",
"@storybook/addon-themes",
"@storybook/angular",
"@storybook/blocks",
"@storybook/builder-manager",
"@storybook/builder-webpack5",
"@storybook/cli",
"@storybook/components",
"@storybook/core",
"@storybook/core-common",
"@storybook/core-events",
"@storybook/core-server",
"@storybook/core-webpack",
"@storybook/csf-tools",
"@storybook/html",
"@storybook/html-vite",
"@storybook/html-webpack5",
"@storybook/manager",
"@storybook/manager-api",
"@storybook/nextjs",
"@storybook/preact",
"@storybook/preact-vite",
"@storybook/preact-webpack5",
"@storybook/preset-create-react-app",
"@storybook/preset-html-webpack",
"@storybook/preset-preact-webpack",
"@storybook/preset-react-webpack",
"@storybook/preset-server-webpack",
"@storybook/preset-vue3-webpack",
"@storybook/react",
"@storybook/react-vite",
"@storybook/react-webpack5",
"@storybook/router",
"@storybook/server",
"@storybook/server-webpack5",
"@storybook/svelte",
"@storybook/svelte-vite",
"@storybook/sveltekit",
"@storybook/theming",
"@storybook/types",
"@storybook/vue3",
"@storybook/vue3-vite",
"@storybook/vue3-webpack5",
"@storybook/web-components",
"@storybook/web-components-vite",
"@storybook/web-components-webpack5"
]
]
}
} }
] ]
} }

View File

@ -284,6 +284,190 @@
"version": "^8.2.8", "version": "^8.2.8",
"alwaysAddToPackageJson": true "alwaysAddToPackageJson": true
}, },
"@storybook/addon-controls": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-jest": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-mdx-gfm": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-onboarding": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/addon-themes": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/blocks": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/builder-manager": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/builder-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/cli": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/components": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/core": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/core-common": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/core-events": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/core-webpack": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/csf-tools": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/html": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/html-vite": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/html-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/manager": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/manager-api": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/nextjs": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preact": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preact-vite": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preact-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preset-create-react-app": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preset-html-webpack": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preset-preact-webpack": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preset-react-webpack": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preset-server-webpack": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/preset-vue3-webpack": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/react-vite": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/react-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/router": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/server": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/server-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/svelte": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/svelte-vite": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/sveltekit": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/theming": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/types": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/vue3": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/vue3-vite": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/vue3-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/web-components": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/web-components-vite": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/web-components-webpack5": {
"version": "^8.2.8",
"alwaysAddToPackageJson": false
},
"@storybook/test-runner": { "@storybook/test-runner": {
"version": "^0.19.0", "version": "^0.19.0",
"alwaysAddToPackageJson": false "alwaysAddToPackageJson": false
@ -396,6 +580,154 @@
"@storybook/vue3-vite": { "@storybook/vue3-vite": {
"version": "^8.4.6", "version": "^8.4.6",
"alwaysAddToPackageJson": false "alwaysAddToPackageJson": false
},
"@storybook/addon-onboarding": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/addon-themes": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/blocks": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/builder-manager": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/builder-webpack5": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/cli": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/components": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/core": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/core-common": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/core-events": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/core-webpack": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/csf-tools": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/html": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/html-vite": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/html-webpack5": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/manager": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/manager-api": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/nextjs": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preact": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preact-vite": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preact-webpack5": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preset-create-react-app": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preset-html-webpack": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preset-preact-webpack": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preset-react-webpack": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preset-server-webpack": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/preset-vue3-webpack": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/router": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/server": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/server-webpack5": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/svelte": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/svelte-vite": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/sveltekit": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/theming": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/types": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/vue3-webpack5": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
},
"@storybook/web-components": {
"version": "^8.4.6",
"alwaysAddToPackageJson": false
} }
} }
} }

View File

@ -0,0 +1,161 @@
const mockExistsSync = jest.fn();
jest.mock('node:fs', () => {
return {
...jest.requireActual('node:fs'),
existsSync: mockExistsSync,
};
});
import { validateMigrations } from './index';
describe('migration-groups', () => {
afterEach(() => {
jest.resetAllMocks();
});
// Unit test the core implementation details of validating the project package.json
describe('validateMigrations()', () => {
it('should return no violations when migrations do not include packageJsonUpdates', () => {
const migrations = {};
const sourceProject = 'test-project';
const sourceProjectRoot = '/path/to/test-project';
const violations = validateMigrations(
migrations,
sourceProject,
`${sourceProjectRoot}/migrations.json`,
{ groups: [['@acme/foo', '@acme/bar']] }
);
expect(violations).toHaveLength(0);
});
it('should return no violations for a valid packageJsonUpdates', () => {
const migrations = {
packageJsonUpdates: {
'0.0.1': {
version: '0.0.1',
packages: {
'@acme/foo': {
version: '1.0.0',
},
'@acme/bar': {
version: '1.0.0',
},
},
},
},
};
const sourceProject = 'test-project';
const sourceProjectRoot = '/path/to/test-project';
const violations = validateMigrations(
migrations,
sourceProject,
`${sourceProjectRoot}/migrations.json`,
{ groups: [['@acme/foo', '@acme/bar']] }
);
expect(violations).toHaveLength(0);
});
it('should return violations for missing packages in a group', () => {
const migrations = {
packageJsonUpdates: {
'0.0.1': {
version: '0.0.1',
packages: {
'@acme/foo': {
version: '1.0.0',
},
},
},
},
};
const sourceProject = 'test-project';
const sourceProjectRoot = '/path/to/test-project';
const violations = validateMigrations(
migrations,
sourceProject,
`${sourceProjectRoot}/migrations.json`,
{ groups: [['@acme/foo', '@acme/bar']] }
);
expect(violations).toMatchInlineSnapshot(`
[
{
"file": "/path/to/test-project/migrations.json",
"message": "Package.json updates for "0.0.1" is missing packages in a group: @acme/bar. Versions of packages in a group must have their versions synced. Version: 1.0.0.
",
"sourceProject": "test-project",
},
]
`);
});
it('should return violations for mismatched versions for packages in a group', () => {
const migrations = {
packageJsonUpdates: {
'0.0.1': {
version: '0.0.1',
packages: {
'@acme/foo': {
version: '1.0.0',
},
'@acme/bar': {
version: '~1.0.0',
},
},
},
},
};
const sourceProject = 'test-project';
const sourceProjectRoot = '/path/to/test-project';
const violations = validateMigrations(
migrations,
sourceProject,
`${sourceProjectRoot}/migrations.json`,
{ groups: [['@acme/foo', '@acme/bar']] }
);
expect(violations).toMatchInlineSnapshot(`
[
{
"file": "/path/to/test-project/migrations.json",
"message": "Package.json updates for "0.0.1" has mismatched versions in a package group: 1.0.0, ~1.0.0. Versions of packages in a group must be in sync. Packages in the group: @acme/foo, @acme/bar",
"sourceProject": "test-project",
},
]
`);
});
it('should ignore migrations not matching versionRange', () => {
const migrations = {
packageJsonUpdates: {
'0.0.1': {
version: '0.0.1',
packages: {
'@acme/foo': {
version: '1.0.0',
},
},
},
'1.0.0': {
version: '1.0.0',
packages: {
'@acme/foo': {
version: '1.0.0',
},
'@acme/bar': {
version: '1.0.0',
},
},
},
},
};
const sourceProject = 'test-project';
const sourceProjectRoot = '/path/to/test-project';
const violations = validateMigrations(
migrations,
sourceProject,
`${sourceProjectRoot}/migrations.json`,
{ groups: [['@acme/foo', '@acme/bar']], versionRange: '>= 1' }
);
expect(violations).toHaveLength(0);
});
});
});

View File

@ -0,0 +1,127 @@
import { readJsonFile, workspaceRoot } from '@nx/devkit';
import {
createConformanceRule,
type ProjectFilesViolation,
} from '@nx/powerpack-conformance';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { satisfies } from 'semver';
type Options = {
groups: Array<string[]>;
versionRange?: string;
};
export default createConformanceRule<Options>({
name: 'migration-groups',
category: 'consistency',
description:
'Ensures that packageJsonUpdates in migrations.json have all packages included from groups. e.g. @typescript-eslint/* packages must be in sync',
reporter: 'project-files-reporter',
implementation: async ({ projectGraph, ruleOptions }) => {
const violations: ProjectFilesViolation[] = [];
for (const project of Object.values(projectGraph.nodes)) {
if (
project.name !== 'angular' &&
project.name !== 'eslint' &&
project.name !== 'storybook'
)
continue;
const migrationsPath = join(
workspaceRoot,
project.data.root,
'migrations.json'
);
if (existsSync(migrationsPath)) {
const migrations = readJsonFile(migrationsPath);
violations.push(
...validateMigrations(
migrations,
project.name,
migrationsPath,
ruleOptions
)
);
}
}
return {
severity: 'high',
details: {
violations,
},
};
},
});
export function validateMigrations(
migrations: Record<string, unknown>,
sourceProject: string,
migrationsPath: string,
options: Options
): ProjectFilesViolation[] {
if (!migrations.packageJsonUpdates) return [];
const violations: ProjectFilesViolation[] = [];
// Check that if package updates include one package in the group, then:
// 1. They all have the same version
// 2. Every package from group is included
for (const [key, value] of Object.entries(migrations.packageJsonUpdates)) {
if (!value.packages || !value.version) continue;
if (
options.versionRange &&
!satisfies(value.version, options.versionRange, {
includePrerelease: true,
})
)
continue;
const packages = Object.keys(value.packages);
for (const group of options.groups) {
if (!group.some((pkg) => packages.includes(pkg))) continue;
const versions = new Set<string>(
group.map((pkg) => value.packages[pkg]?.version).filter(Boolean)
);
if (versions.size > 1) {
violations.push({
message: `Package.json updates for "${key}" has mismatched versions in a package group: ${Array.from(
versions
).join(
', '
)}. Versions of packages in a group must be in sync. Packages in the group: ${group.join(
', '
)}`,
sourceProject,
file: migrationsPath,
});
}
const result = group.reduce(
(acc, pkg) => {
if (packages.includes(pkg)) acc.present.push(pkg);
else acc.missing.push(pkg);
return acc;
},
{ missing: [] as string[], present: [] as string[] }
);
if (result.missing.length > 0) {
violations.push({
message: `Package.json updates for "${key}" is missing packages in a group: ${result.missing.join(
', '
)}. Versions of packages in a group must have their versions synced. ${
versions.size === 1
? `Version: ${Array.from(versions)[0]}.`
: `Versions: ${Array.from(versions).join(',')} (choose one).`
}
`,
sourceProject,
file: migrationsPath,
});
}
}
}
return violations;
}

View File

@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"groups": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "string"
}
}
},
"versionRange": {
"type": "string"
}
},
"additionalProperties": false,
"required": ["groups"]
}