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`


<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #26884
This commit is contained in:
Jack Hsu 2024-08-22 17:21:49 -04:00 committed by GitHub
parent 35899e3a25
commit 042049c785
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 305 additions and 0 deletions

View File

@ -61,6 +61,10 @@
"default": false, "default": false,
"x-priority": "internal" "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": { "skipPackageManager": {
"type": "boolean", "type": "boolean",
"description": "Do not add a `packageManager` entry to the generated package.json file." "description": "Do not add a `packageManager` entry to the generated package.json file."

View File

@ -31,6 +31,10 @@
"description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.", "description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.",
"default": false "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": { "skipPackageManager": {
"type": "boolean", "type": "boolean",
"description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option." "description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option."

View File

@ -51,6 +51,10 @@
"description": "Include devDependencies in the generated package.json.", "description": "Include devDependencies in the generated package.json.",
"type": "boolean" "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": { "skipPackageManager": {
"type": "boolean", "type": "boolean",
"description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option." "description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option."

View File

@ -239,6 +239,10 @@
"type": "boolean", "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." "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": { "skipPackageManager": {
"type": "boolean", "type": "boolean",
"description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option." "description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option."

View File

@ -215,6 +215,18 @@ Type: `string[]`
External scripts that will be included before the main application entry. 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 ##### skipTypeChecking
Type: `boolean` Type: `boolean`

View File

@ -81,6 +81,7 @@ export default async function buildExecutor(
target: context.targetName, target: context.targetName,
root: context.root, root: context.root,
isProduction: !options.includeDevDependenciesInPackageJson, // By default we remove devDependencies since this is a production build. isProduction: !options.includeDevDependenciesInPackageJson, // By default we remove devDependencies since this is a production build.
skipOverrides: options.skipOverrides,
skipPackageManager: options.skipPackageManager, skipPackageManager: options.skipPackageManager,
} }
); );

View File

@ -58,6 +58,10 @@
"default": false, "default": false,
"x-priority": "internal" "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": { "skipPackageManager": {
"type": "boolean", "type": "boolean",
"description": "Do not add a `packageManager` entry to the generated package.json file." "description": "Do not add a `packageManager` entry to the generated package.json file."

View File

@ -39,6 +39,7 @@ export interface NextBuildBuilderOptions {
nextConfig?: string; nextConfig?: string;
outputPath: string; outputPath: string;
profile?: boolean; profile?: boolean;
skipOverrides?: boolean;
skipPackageManager?: boolean; skipPackageManager?: boolean;
watch?: boolean; watch?: boolean;
} }

View File

@ -779,6 +779,227 @@ describe('createPackageJson', () => {
/Package Manager Mismatch/ /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',
},
});
});
}); });
}); });

View File

@ -39,6 +39,7 @@ export function createPackageJson(
isProduction?: boolean; isProduction?: boolean;
helperDependencies?: string[]; helperDependencies?: string[];
skipPackageManager?: boolean; skipPackageManager?: boolean;
skipOverrides?: boolean;
} = {}, } = {},
fileMap: ProjectFileMap = null fileMap: ProjectFileMap = null
): PackageJson { ): PackageJson {
@ -198,6 +199,34 @@ export function createPackageJson(
packageJson.packageManager = rootPackageJson.packageManager; 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; return packageJson;
} }

View File

@ -54,6 +54,9 @@ export interface PackageJson {
peerDependencies?: Record<string, string>; peerDependencies?: Record<string, string>;
peerDependenciesMeta?: Record<string, { optional: boolean }>; peerDependenciesMeta?: Record<string, { optional: boolean }>;
resolutions?: Record<string, string>; resolutions?: Record<string, string>;
pnpm?: {
overrides?: PackageOverride;
};
overrides?: PackageOverride; overrides?: PackageOverride;
bin?: Record<string, string> | string; bin?: Record<string, string> | string;
workspaces?: workspaces?:

View File

@ -28,6 +28,10 @@
"description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.", "description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.",
"default": false "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": { "skipPackageManager": {
"type": "boolean", "type": "boolean",
"description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option." "description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option."

View File

@ -126,6 +126,7 @@ export async function* viteBuildExecutor(
target: context.targetName, target: context.targetName,
root: context.root, root: context.root,
isProduction: !options.includeDevDependenciesInPackageJson, // By default we remove devDependencies since this is a production build. isProduction: !options.includeDevDependenciesInPackageJson, // By default we remove devDependencies since this is a production build.
skipOverrides: options.skipOverrides,
skipPackageManager: options.skipPackageManager, skipPackageManager: options.skipPackageManager,
} }
); );

View File

@ -4,6 +4,7 @@ export interface ViteBuildExecutorOptions {
generatePackageJson?: boolean; generatePackageJson?: boolean;
includeDevDependenciesInPackageJson?: boolean; includeDevDependenciesInPackageJson?: boolean;
outputPath?: string; outputPath?: string;
skipOverrides?: boolean;
skipPackageManager?: boolean; skipPackageManager?: boolean;
skipTypeCheck?: boolean; skipTypeCheck?: boolean;
tsConfig?: string; tsConfig?: string;

View File

@ -60,6 +60,10 @@
"description": "Include devDependencies in the generated package.json.", "description": "Include devDependencies in the generated package.json.",
"type": "boolean" "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": { "skipPackageManager": {
"type": "boolean", "type": "boolean",
"description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option." "description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option."

View File

@ -163,6 +163,10 @@
"type": "boolean", "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." "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": { "skipPackageManager": {
"type": "boolean", "type": "boolean",
"description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option." "description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option."

View File

@ -167,6 +167,10 @@ export interface NxAppWebpackPluginOptions {
* External scripts that will be included before the main application entry. * External scripts that will be included before the main application entry.
*/ */
scripts?: Array<ExtraEntryPointClass | string>; scripts?: Array<ExtraEntryPointClass | string>;
/**
* 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. * Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option.
*/ */