Skip to content

Commit 9f255f2

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 3af4dcb commit 9f255f2

File tree

8 files changed

+89
-34
lines changed

8 files changed

+89
-34
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: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { isJsonObject, json } from '@angular-devkit/core';
9+
import { JsonValue, isJsonObject } 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);
@@ -298,19 +297,19 @@ export class PackageManagerUtils {
298297
}
299298

300299
private getConfiguredPackageManager(): PackageManager | undefined {
301-
const getPackageManager = (source: json.JsonValue | undefined): PackageManager | undefined => {
302-
if (source && isJsonObject(source)) {
303-
const value = source['packageManager'];
304-
if (typeof value === 'string') {
305-
return value as PackageManager;
306-
}
307-
}
300+
const { workspace: localWorkspace, globalConfiguration: globalWorkspace } = this.context;
301+
let result: PackageManager | undefined;
308302

309-
return undefined;
310-
};
303+
try {
304+
const packageJsonPath = join(this.context.root, 'package.json');
305+
const pkgJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as JsonValue;
306+
result = getPackageManager(pkgJson);
307+
} catch {}
308+
309+
if (result) {
310+
return result;
311+
}
311312

312-
let result: PackageManager | undefined;
313-
const { workspace: localWorkspace, globalConfiguration: globalWorkspace } = this.context;
314313
if (localWorkspace) {
315314
const project = getProjectByCwd(localWorkspace);
316315
if (project) {
@@ -320,10 +319,19 @@ export class PackageManagerUtils {
320319
result ??= getPackageManager(localWorkspace.extensions['cli']);
321320
}
322321

323-
if (!result) {
324-
result = getPackageManager(globalWorkspace.extensions['cli']);
325-
}
322+
result ??= getPackageManager(globalWorkspace.extensions['cli']);
326323

327324
return result;
328325
}
329326
}
327+
328+
function getPackageManager(source: JsonValue | undefined): PackageManager | undefined {
329+
if (source && isJsonObject(source)) {
330+
const value = source['packageManager'];
331+
if (typeof value === 'string') {
332+
return value.split('@', 1)[0] as PackageManager;
333+
}
334+
}
335+
336+
return undefined;
337+
}

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: 9 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,14 @@ 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');
23+
await ng(
24+
'new',
25+
'test-project',
26+
'--skip-install',
27+
'--package-manager',
28+
getActivePackageManager(),
29+
);
30+
2431
await expectFileToExist(join(process.cwd(), 'test-project'));
2532
process.chdir('./test-project');
2633

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.21',
1010
};
1111

1212
export default async function () {
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import { copyFile } from 'node:fs/promises';
12
import { assetDir } from '../../../utils/assets';
23
import { expectFileToExist } from '../../../utils/fs';
34
import { ng } from '../../../utils/process';
45

56
export default async function () {
6-
await ng('add', assetDir('add-collection.tgz'), '--name=blah', '--skip-confirmation');
7+
// Avoids ERR_PNPM_ENAMETOOLONG errors.
8+
const tarball = './add-collection.tgz';
9+
await copyFile(assetDir(tarball), tarball);
10+
11+
await ng('add', tarball, '--name=blah', '--skip-confirmation');
712
await expectFileToExist('blah');
813
}

0 commit comments

Comments
 (0)