From e8647df08aaba3c19ef680fe96c36697738220d6 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Thu, 27 Feb 2025 16:50:32 -0500 Subject: [PATCH] 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. --- .../migrations/19.6.0-package-updates.json | 172 +++++++++ .../migrations/20.2.0-package-updates.json | 139 ++++++++ nx.json | 80 +++++ packages/storybook/migrations.json | 332 ++++++++++++++++++ .../migration-groups/index.spec.ts | 161 +++++++++ .../migration-groups/index.ts | 127 +++++++ .../migration-groups/schema.json | 20 ++ 7 files changed, 1031 insertions(+) create mode 100644 tools/workspace-plugin/src/conformance-rules/migration-groups/index.spec.ts create mode 100644 tools/workspace-plugin/src/conformance-rules/migration-groups/index.ts create mode 100644 tools/workspace-plugin/src/conformance-rules/migration-groups/schema.json diff --git a/docs/generated/packages/storybook/migrations/19.6.0-package-updates.json b/docs/generated/packages/storybook/migrations/19.6.0-package-updates.json index 6c3ee0be47..e37a34eba2 100644 --- a/docs/generated/packages/storybook/migrations/19.6.0-package-updates.json +++ b/docs/generated/packages/storybook/migrations/19.6.0-package-updates.json @@ -23,6 +23,178 @@ "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": { "version": "^0.19.0", "alwaysAddToPackageJson": false diff --git a/docs/generated/packages/storybook/migrations/20.2.0-package-updates.json b/docs/generated/packages/storybook/migrations/20.2.0-package-updates.json index 05f0b2028c..58efc9c70e 100644 --- a/docs/generated/packages/storybook/migrations/20.2.0-package-updates.json +++ b/docs/generated/packages/storybook/migrations/20.2.0-package-updates.json @@ -102,6 +102,145 @@ "@storybook/vue3-vite": { "version": "^8.4.6", "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": [], diff --git a/nx.json b/nx.json index 500733ac36..20962cc15e 100644 --- a/nx.json +++ b/nx.json @@ -289,6 +289,86 @@ "!webpack", "!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" + ] + ] + } } ] } diff --git a/packages/storybook/migrations.json b/packages/storybook/migrations.json index d25d1ff7ef..9161120856 100644 --- a/packages/storybook/migrations.json +++ b/packages/storybook/migrations.json @@ -284,6 +284,190 @@ "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": { "version": "^0.19.0", "alwaysAddToPackageJson": false @@ -396,6 +580,154 @@ "@storybook/vue3-vite": { "version": "^8.4.6", "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 } } } diff --git a/tools/workspace-plugin/src/conformance-rules/migration-groups/index.spec.ts b/tools/workspace-plugin/src/conformance-rules/migration-groups/index.spec.ts new file mode 100644 index 0000000000..2c1d0ac7ac --- /dev/null +++ b/tools/workspace-plugin/src/conformance-rules/migration-groups/index.spec.ts @@ -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); + }); + }); +}); diff --git a/tools/workspace-plugin/src/conformance-rules/migration-groups/index.ts b/tools/workspace-plugin/src/conformance-rules/migration-groups/index.ts new file mode 100644 index 0000000000..c4229f57e6 --- /dev/null +++ b/tools/workspace-plugin/src/conformance-rules/migration-groups/index.ts @@ -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; + versionRange?: string; +}; + +export default createConformanceRule({ + 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, + 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( + 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; +} diff --git a/tools/workspace-plugin/src/conformance-rules/migration-groups/schema.json b/tools/workspace-plugin/src/conformance-rules/migration-groups/schema.json new file mode 100644 index 0000000000..6dad7088e5 --- /dev/null +++ b/tools/workspace-plugin/src/conformance-rules/migration-groups/schema.json @@ -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"] +}