Skip to content

Commit 92d5c46

Browse files
committed
feat(@schematics/angular): set packageManager in package.json on new projects
When creating a new project, the `packageManager` field in `package.json` will be automatically set to the package manager used to create the project. This helps to ensure that the same package manager is used by all developers working on the project. The package manager is detected in the following order: 1. `packageManager` field in `package.json` 2. Angular CLI configuration (`angular.json`) 3. Lock files (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`)
1 parent 881e42d commit 92d5c46

File tree

7 files changed

+72
-18
lines changed

7 files changed

+72
-18
lines changed

packages/angular/cli/src/commands/new/cli.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ export default class NewCommandModule
7373
defaults,
7474
});
7575
workflow.registry.addSmartDefaultProvider('ng-cli-version', () => VERSION.full);
76+
workflow.registry.addSmartDefaultProvider(
77+
'packageManager',
78+
() => this.context.packageManager.name,
79+
);
7680

7781
return this.runSchematic({
7882
collectionName,

packages/angular/cli/src/utilities/package-manager.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { isJsonObject, json } from '@angular-devkit/core';
1010
import { execSync, spawn } from 'node:child_process';
11-
import { promises as fs, readdirSync, realpathSync, rmSync } from 'node:fs';
11+
import { promises as fs, readFileSync, readdirSync, realpathSync, rmSync } from 'node:fs';
1212
import { tmpdir } from 'node:os';
1313
import { join } from 'node:path';
1414
import { PackageManager } from '../../lib/config/workspace-schema';
@@ -233,7 +233,6 @@ export class PackageManagerUtils {
233233
}
234234

235235
const filesInRoot = readdirSync(this.context.root);
236-
237236
const hasNpmLock = this.hasLockfile(PackageManager.Npm, filesInRoot);
238237
const hasYarnLock = this.hasLockfile(PackageManager.Yarn, filesInRoot);
239238
const hasPnpmLock = this.hasLockfile(PackageManager.Pnpm, filesInRoot);
@@ -311,6 +310,18 @@ export class PackageManagerUtils {
311310

312311
let result: PackageManager | undefined;
313312
const { workspace: localWorkspace, globalConfiguration: globalWorkspace } = this.context;
313+
try {
314+
const pkgJson = JSON.parse(
315+
readFileSync(join(this.context.root, 'package.json'), 'utf-8'),
316+
) as {
317+
packageManager?: string;
318+
};
319+
const result = getPackageManager(pkgJson);
320+
if (result) {
321+
return result;
322+
}
323+
} catch {}
324+
314325
if (localWorkspace) {
315326
const project = getProjectByCwd(localWorkspace);
316327
if (project) {

packages/schematics/angular/workspace/files/package.json.template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
]
2222
},
2323
"private": true,
24+
<% if (packageManagerWithVersion) { %>"packageManager": "<%= packageManagerWithVersion %>",<% } %>
2425
"dependencies": {
2526
"@angular/common": "<%= latestVersions.Angular %>",
2627
"@angular/compiler": "<%= latestVersions.Angular %>",

packages/schematics/angular/workspace/index.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,46 @@ import {
1616
strings,
1717
url,
1818
} from '@angular-devkit/schematics';
19+
import { execSync } from 'node:child_process';
1920
import { latestVersions } from '../utility/latest-versions';
2021
import { Schema as WorkspaceOptions } from './schema';
2122

2223
export default function (options: WorkspaceOptions): Rule {
23-
return mergeWith(
24-
apply(url('./files'), [
25-
options.minimal ? filter((path) => !path.endsWith('editorconfig.template')) : noop(),
26-
applyTemplates({
27-
utils: strings,
28-
...options,
29-
'dot': '.',
30-
latestVersions,
31-
}),
32-
]),
33-
);
24+
return () => {
25+
const packageManager = options.packageManager;
26+
let packageManagerWithVersion: string | undefined;
27+
28+
if (packageManager) {
29+
let packageManagerVersion: string | undefined;
30+
try {
31+
packageManagerVersion = execSync(`${packageManager} --version`, {
32+
encoding: 'utf8',
33+
stdio: 'pipe',
34+
env: {
35+
...process.env,
36+
// NPM updater notifier will prevents the child process from closing until it timeout after 3 minutes.
37+
NO_UPDATE_NOTIFIER: '1',
38+
NPM_CONFIG_UPDATE_NOTIFIER: 'false',
39+
},
40+
}).trim();
41+
} catch {}
42+
43+
if (packageManagerVersion) {
44+
packageManagerWithVersion = `${packageManager}@${packageManagerVersion}`;
45+
}
46+
}
47+
48+
return mergeWith(
49+
apply(url('./files'), [
50+
options.minimal ? filter((path) => !path.endsWith('editorconfig.template')) : noop(),
51+
applyTemplates({
52+
utils: strings,
53+
...options,
54+
'dot': '.',
55+
latestVersions,
56+
packageManagerWithVersion,
57+
}),
58+
]),
59+
);
60+
};
3461
}

packages/schematics/angular/workspace/schema.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@
4040
"packageManager": {
4141
"description": "The package manager to use for installing dependencies.",
4242
"type": "string",
43-
"enum": ["npm", "yarn", "pnpm", "bun"]
43+
"enum": ["npm", "yarn", "pnpm", "bun"],
44+
"$default": {
45+
"$source": "packageManager"
46+
}
4447
}
4548
},
4649
"required": ["name", "version"]

tests/legacy-cli/e2e/initialize/500-create-project.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { join } from 'node:path';
22
import { getGlobalVariable } from '../utils/env';
33
import { expectFileToExist } from '../utils/fs';
44
import { gitClean } from '../utils/git';
5-
import { setRegistry as setNPMConfigRegistry } from '../utils/packages';
5+
import { getActivePackageManager, setRegistry as setNPMConfigRegistry } from '../utils/packages';
66
import { ng } from '../utils/process';
77
import { prepareProjectForE2e, updateJsonFile } from '../utils/project';
88

@@ -20,7 +20,15 @@ export default async function () {
2020
// Ensure local test registry is used when outside a project
2121
await setNPMConfigRegistry(true);
2222

23-
await ng('new', 'test-project', '--skip-install', '--no-zoneless');
23+
await ng(
24+
'new',
25+
'test-project',
26+
'--skip-install',
27+
'--no-zoneless',
28+
'--package-manager',
29+
getActivePackageManager(),
30+
);
31+
2432
await expectFileToExist(join(process.cwd(), 'test-project'));
2533
process.chdir('./test-project');
2634

tests/legacy-cli/e2e/setup/100-global-cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { globalNpm } from '../utils/process';
55
const PACKAGE_MANAGER_VERSION = {
66
'npm': '10.8.1',
77
'yarn': '1.22.22',
8-
'pnpm': '9.3.0',
9-
'bun': '1.1.13',
8+
'pnpm': '10.17.1',
9+
'bun': '1.2.22',
1010
};
1111

1212
export default async function () {

0 commit comments

Comments
 (0)