From 042049c7857b64e1de30f151d2da29abfe795e80 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Thu, 22 Aug 2024 17:21:49 -0400 Subject: [PATCH] feat(js): generate package.json with overrides and resolutions (#27601) This PR ensures that `overrides` and `resolutions` are in the generated package.json file as well. If they are missing, then using `--frozen-lockfile` will fail due to mismatched overrides in the lockfile. Also adds a `skipOverrides` flag to the affected executors and plugins -- same as `skipPackageManger` that was added previously. Affected executors/plugins: - `@nx/vite:build` - `@nx/webpack:webpack` - `@nx/remix:build` - `@nx/next:build` - `NxAppWebpackPlugin` ## Current Behavior ## Expected Behavior ## Related Issue(s) Fixes #26884 --- .../packages/next/executors/build.json | 4 + .../packages/remix/executors/build.json | 4 + .../packages/vite/executors/build.json | 4 + .../packages/webpack/executors/webpack.json | 4 + .../packages/webpack/webpack-plugins.md | 12 + .../next/src/executors/build/build.impl.ts | 1 + packages/next/src/executors/build/schema.json | 4 + packages/next/src/utils/types.ts | 1 + .../package-json/create-package-json.spec.ts | 221 ++++++++++++++++++ .../js/package-json/create-package-json.ts | 29 +++ packages/nx/src/utils/package-json.ts | 3 + .../remix/src/executors/build/schema.json | 4 + .../vite/src/executors/build/build.impl.ts | 1 + packages/vite/src/executors/build/schema.d.ts | 1 + packages/vite/src/executors/build/schema.json | 4 + .../webpack/src/executors/webpack/schema.json | 4 + .../nx-app-webpack-plugin-options.ts | 4 + 17 files changed, 305 insertions(+) diff --git a/docs/generated/packages/next/executors/build.json b/docs/generated/packages/next/executors/build.json index 44e44d0383e94..01041725c83b9 100644 --- a/docs/generated/packages/next/executors/build.json +++ b/docs/generated/packages/next/executors/build.json @@ -61,6 +61,10 @@ "default": false, "x-priority": "internal" }, + "skipOverrides": { + "type": "boolean", + "description": "Do not add a `overrides` and `resolutions` entries to the generated package.json file. Only works in conjunction with `generatePackageJson` option." + }, "skipPackageManager": { "type": "boolean", "description": "Do not add a `packageManager` entry to the generated package.json file." diff --git a/docs/generated/packages/remix/executors/build.json b/docs/generated/packages/remix/executors/build.json index 5d39ca806573d..0c9c424ca93c5 100644 --- a/docs/generated/packages/remix/executors/build.json +++ b/docs/generated/packages/remix/executors/build.json @@ -31,6 +31,10 @@ "description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.", "default": false }, + "skipOverrides": { + "type": "boolean", + "description": "Do not add a `overrides` and `resolutions` entries to the generated package.json file. Only works in conjunction with `generatePackageJson` option." + }, "skipPackageManager": { "type": "boolean", "description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option." diff --git a/docs/generated/packages/vite/executors/build.json b/docs/generated/packages/vite/executors/build.json index 7cdc0f4ec63bd..0e8a9ab68e720 100644 --- a/docs/generated/packages/vite/executors/build.json +++ b/docs/generated/packages/vite/executors/build.json @@ -51,6 +51,10 @@ "description": "Include devDependencies in the generated package.json.", "type": "boolean" }, + "skipOverrides": { + "type": "boolean", + "description": "Do not add a `overrides` and `resolutions` entries to the generated package.json file. Only works in conjunction with `generatePackageJson` option." + }, "skipPackageManager": { "type": "boolean", "description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option." diff --git a/docs/generated/packages/webpack/executors/webpack.json b/docs/generated/packages/webpack/executors/webpack.json index b9c11c75fa199..c78900b81fec1 100644 --- a/docs/generated/packages/webpack/executors/webpack.json +++ b/docs/generated/packages/webpack/executors/webpack.json @@ -239,6 +239,10 @@ "type": "boolean", "description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated." }, + "skipOverrides": { + "type": "boolean", + "description": "Do not add a `overrides` and `resolutions` entries to the generated package.json file. Only works in conjunction with `generatePackageJson` option." + }, "skipPackageManager": { "type": "boolean", "description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option." diff --git a/docs/shared/packages/webpack/webpack-plugins.md b/docs/shared/packages/webpack/webpack-plugins.md index 459918490c538..f9045c4ebf3fd 100644 --- a/docs/shared/packages/webpack/webpack-plugins.md +++ b/docs/shared/packages/webpack/webpack-plugins.md @@ -215,6 +215,18 @@ Type: `string[]` External scripts that will be included before the main application entry. +##### skipOverrides + +Type: `boolean` + +Do not add a `overrides` and `resolutions` entries to the generated package.json file. Only works in conjunction with `generatePackageJson` option. + +##### skipPackageManager + +Type: `boolean` + +Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option. + ##### skipTypeChecking Type: `boolean` diff --git a/packages/next/src/executors/build/build.impl.ts b/packages/next/src/executors/build/build.impl.ts index 656a50dd70b4a..32afbb6021a98 100644 --- a/packages/next/src/executors/build/build.impl.ts +++ b/packages/next/src/executors/build/build.impl.ts @@ -81,6 +81,7 @@ export default async function buildExecutor( target: context.targetName, root: context.root, isProduction: !options.includeDevDependenciesInPackageJson, // By default we remove devDependencies since this is a production build. + skipOverrides: options.skipOverrides, skipPackageManager: options.skipPackageManager, } ); diff --git a/packages/next/src/executors/build/schema.json b/packages/next/src/executors/build/schema.json index 2b2db783c751f..b456213b83e47 100644 --- a/packages/next/src/executors/build/schema.json +++ b/packages/next/src/executors/build/schema.json @@ -58,6 +58,10 @@ "default": false, "x-priority": "internal" }, + "skipOverrides": { + "type": "boolean", + "description": "Do not add a `overrides` and `resolutions` entries to the generated package.json file. Only works in conjunction with `generatePackageJson` option." + }, "skipPackageManager": { "type": "boolean", "description": "Do not add a `packageManager` entry to the generated package.json file." diff --git a/packages/next/src/utils/types.ts b/packages/next/src/utils/types.ts index 3254052b28899..da5f3c3ed5248 100644 --- a/packages/next/src/utils/types.ts +++ b/packages/next/src/utils/types.ts @@ -39,6 +39,7 @@ export interface NextBuildBuilderOptions { nextConfig?: string; outputPath: string; profile?: boolean; + skipOverrides?: boolean; skipPackageManager?: boolean; watch?: boolean; } diff --git a/packages/nx/src/plugins/js/package-json/create-package-json.spec.ts b/packages/nx/src/plugins/js/package-json/create-package-json.spec.ts index a5f4a8172973c..6804a61615d4d 100644 --- a/packages/nx/src/plugins/js/package-json/create-package-json.spec.ts +++ b/packages/nx/src/plugins/js/package-json/create-package-json.spec.ts @@ -779,6 +779,227 @@ describe('createPackageJson', () => { /Package Manager Mismatch/ ); }); + + it('should add overrides (pnpm)', () => { + spies.push( + jest + .spyOn(fs, 'existsSync') + .mockImplementation( + (path) => + path === 'libs/lib1/package.json' || + path === 'apps/app1/package.json' || + path === 'package.json' + ) + ); + spies.push( + jest + .spyOn(fileutilsModule, 'readJsonFile') + .mockImplementation((path) => { + if (path === 'package.json') { + return { + ...rootPackageJson(), + pnpm: { + overrides: { + foo: '1.0.0', + }, + }, + }; + } + if (path === 'libs/lib1/package.json') { + return projectPackageJson(); + } + if (path === 'apps/app1/package.json') { + return { + ...projectPackageJson(), + pnpm: { + overrides: { + foo: '2.0.0', + bar: '1.0.0', + }, + }, + }; + } + }) + ); + + expect( + createPackageJson('lib1', graph, { + root: '', + }) + ).toEqual({ + dependencies: { + random: '1.0.0', + typescript: '^4.8.4', + }, + name: 'other-name', + version: '1.2.3', + pnpm: { + overrides: { + foo: '1.0.0', + }, + }, + }); + expect( + createPackageJson('app1', graph, { + root: '', + }) + ).toEqual({ + dependencies: { + random: '1.0.0', + typescript: '^4.8.4', + }, + name: 'other-name', + version: '1.2.3', + pnpm: { + overrides: { + foo: '2.0.0', + bar: '1.0.0', + }, + }, + }); + }); + + it('should add overrides (npm)', () => { + spies.push( + jest + .spyOn(fs, 'existsSync') + .mockImplementation( + (path) => + path === 'libs/lib1/package.json' || + path === 'apps/app1/package.json' || + path === 'package.json' + ) + ); + spies.push( + jest + .spyOn(fileutilsModule, 'readJsonFile') + .mockImplementation((path) => { + if (path === 'package.json') { + return { + ...rootPackageJson(), + overrides: { + foo: '1.0.0', + }, + }; + } + if (path === 'libs/lib1/package.json') { + return projectPackageJson(); + } + if (path === 'apps/app1/package.json') { + return { + ...projectPackageJson(), + overrides: { + foo: '2.0.0', + bar: '1.0.0', + }, + }; + } + }) + ); + + expect( + createPackageJson('lib1', graph, { + root: '', + }) + ).toEqual({ + dependencies: { + random: '1.0.0', + typescript: '^4.8.4', + }, + name: 'other-name', + version: '1.2.3', + overrides: { + foo: '1.0.0', + }, + }); + expect( + createPackageJson('app1', graph, { + root: '', + }) + ).toEqual({ + dependencies: { + random: '1.0.0', + typescript: '^4.8.4', + }, + name: 'other-name', + version: '1.2.3', + overrides: { + foo: '2.0.0', + bar: '1.0.0', + }, + }); + }); + + it('should add resolutions (yarn)', () => { + spies.push( + jest + .spyOn(fs, 'existsSync') + .mockImplementation( + (path) => + path === 'libs/lib1/package.json' || + path === 'apps/app1/package.json' || + path === 'package.json' + ) + ); + spies.push( + jest + .spyOn(fileutilsModule, 'readJsonFile') + .mockImplementation((path) => { + if (path === 'package.json') { + return { + ...rootPackageJson(), + resolutions: { + foo: '1.0.0', + }, + }; + } + if (path === 'libs/lib1/package.json') { + return projectPackageJson(); + } + if (path === 'apps/app1/package.json') { + return { + ...projectPackageJson(), + resolutions: { + foo: '2.0.0', + bar: '1.0.0', + }, + }; + } + }) + ); + + expect( + createPackageJson('lib1', graph, { + root: '', + }) + ).toEqual({ + dependencies: { + random: '1.0.0', + typescript: '^4.8.4', + }, + name: 'other-name', + version: '1.2.3', + resolutions: { + foo: '1.0.0', + }, + }); + expect( + createPackageJson('app1', graph, { + root: '', + }) + ).toEqual({ + dependencies: { + random: '1.0.0', + typescript: '^4.8.4', + }, + name: 'other-name', + version: '1.2.3', + resolutions: { + foo: '2.0.0', + bar: '1.0.0', + }, + }); + }); }); }); diff --git a/packages/nx/src/plugins/js/package-json/create-package-json.ts b/packages/nx/src/plugins/js/package-json/create-package-json.ts index 2402bb1d2f147..4877239d521a3 100644 --- a/packages/nx/src/plugins/js/package-json/create-package-json.ts +++ b/packages/nx/src/plugins/js/package-json/create-package-json.ts @@ -39,6 +39,7 @@ export function createPackageJson( isProduction?: boolean; helperDependencies?: string[]; skipPackageManager?: boolean; + skipOverrides?: boolean; } = {}, fileMap: ProjectFileMap = null ): PackageJson { @@ -198,6 +199,34 @@ export function createPackageJson( packageJson.packageManager = rootPackageJson.packageManager; } + // region Overrides/Resolutions + + // npm + if (rootPackageJson.overrides && !options.skipOverrides) { + packageJson.overrides = { + ...rootPackageJson.overrides, + ...packageJson.overrides, + }; + } + + // pnpm + if (rootPackageJson.pnpm?.overrides && !options.skipOverrides) { + packageJson.pnpm ??= {}; + packageJson.pnpm.overrides = { + ...rootPackageJson.pnpm.overrides, + ...packageJson.pnpm.overrides, + }; + } + + // yarn + if (rootPackageJson.resolutions && !options.skipOverrides) { + packageJson.resolutions = { + ...rootPackageJson.resolutions, + ...packageJson.resolutions, + }; + } + // endregion Overrides/Resolutions + return packageJson; } diff --git a/packages/nx/src/utils/package-json.ts b/packages/nx/src/utils/package-json.ts index a08f12ad06287..fd657fc54b172 100644 --- a/packages/nx/src/utils/package-json.ts +++ b/packages/nx/src/utils/package-json.ts @@ -54,6 +54,9 @@ export interface PackageJson { peerDependencies?: Record; peerDependenciesMeta?: Record; resolutions?: Record; + pnpm?: { + overrides?: PackageOverride; + }; overrides?: PackageOverride; bin?: Record | string; workspaces?: diff --git a/packages/remix/src/executors/build/schema.json b/packages/remix/src/executors/build/schema.json index 0dc22e43a88d6..c467bf67ebc27 100644 --- a/packages/remix/src/executors/build/schema.json +++ b/packages/remix/src/executors/build/schema.json @@ -28,6 +28,10 @@ "description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.", "default": false }, + "skipOverrides": { + "type": "boolean", + "description": "Do not add a `overrides` and `resolutions` entries to the generated package.json file. Only works in conjunction with `generatePackageJson` option." + }, "skipPackageManager": { "type": "boolean", "description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option." diff --git a/packages/vite/src/executors/build/build.impl.ts b/packages/vite/src/executors/build/build.impl.ts index 7299261a5cb4b..277371a623914 100644 --- a/packages/vite/src/executors/build/build.impl.ts +++ b/packages/vite/src/executors/build/build.impl.ts @@ -126,6 +126,7 @@ export async function* viteBuildExecutor( target: context.targetName, root: context.root, isProduction: !options.includeDevDependenciesInPackageJson, // By default we remove devDependencies since this is a production build. + skipOverrides: options.skipOverrides, skipPackageManager: options.skipPackageManager, } ); diff --git a/packages/vite/src/executors/build/schema.d.ts b/packages/vite/src/executors/build/schema.d.ts index 4e8b9873995aa..6262c7ffb4b9b 100644 --- a/packages/vite/src/executors/build/schema.d.ts +++ b/packages/vite/src/executors/build/schema.d.ts @@ -4,6 +4,7 @@ export interface ViteBuildExecutorOptions { generatePackageJson?: boolean; includeDevDependenciesInPackageJson?: boolean; outputPath?: string; + skipOverrides?: boolean; skipPackageManager?: boolean; skipTypeCheck?: boolean; tsConfig?: string; diff --git a/packages/vite/src/executors/build/schema.json b/packages/vite/src/executors/build/schema.json index 19d416b7c0740..1bb6d3f48e05f 100644 --- a/packages/vite/src/executors/build/schema.json +++ b/packages/vite/src/executors/build/schema.json @@ -60,6 +60,10 @@ "description": "Include devDependencies in the generated package.json.", "type": "boolean" }, + "skipOverrides": { + "type": "boolean", + "description": "Do not add a `overrides` and `resolutions` entries to the generated package.json file. Only works in conjunction with `generatePackageJson` option." + }, "skipPackageManager": { "type": "boolean", "description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option." diff --git a/packages/webpack/src/executors/webpack/schema.json b/packages/webpack/src/executors/webpack/schema.json index 1fb76b0e46334..0beb2cc19063c 100644 --- a/packages/webpack/src/executors/webpack/schema.json +++ b/packages/webpack/src/executors/webpack/schema.json @@ -163,6 +163,10 @@ "type": "boolean", "description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated." }, + "skipOverrides": { + "type": "boolean", + "description": "Do not add a `overrides` and `resolutions` entries to the generated package.json file. Only works in conjunction with `generatePackageJson` option." + }, "skipPackageManager": { "type": "boolean", "description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option." diff --git a/packages/webpack/src/plugins/nx-webpack-plugin/nx-app-webpack-plugin-options.ts b/packages/webpack/src/plugins/nx-webpack-plugin/nx-app-webpack-plugin-options.ts index 44a730358e3fb..2adde5f110ff0 100644 --- a/packages/webpack/src/plugins/nx-webpack-plugin/nx-app-webpack-plugin-options.ts +++ b/packages/webpack/src/plugins/nx-webpack-plugin/nx-app-webpack-plugin-options.ts @@ -167,6 +167,10 @@ export interface NxAppWebpackPluginOptions { * External scripts that will be included before the main application entry. */ scripts?: Array; + /** + * Do not add a `overrides` and `resolutions` entries to the generated package.json file. Only works in conjunction with `generatePackageJson` option. + */ + skipOverrides?: boolean; /** * Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option. */