Skip to content

Commit 2057573

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 2057573

File tree

8 files changed

+91
-33
lines changed

8 files changed

+91
-33
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: 26 additions & 16 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);
@@ -298,19 +297,21 @@ 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 {
306+
packageManager?: string;
307+
};
308+
result = getPackageManager(pkgJson);
309+
} catch {}
310+
311+
if (result) {
312+
return result;
313+
}
311314

312-
let result: PackageManager | undefined;
313-
const { workspace: localWorkspace, globalConfiguration: globalWorkspace } = this.context;
314315
if (localWorkspace) {
315316
const project = getProjectByCwd(localWorkspace);
316317
if (project) {
@@ -320,10 +321,19 @@ export class PackageManagerUtils {
320321
result ??= getPackageManager(localWorkspace.extensions['cli']);
321322
}
322323

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

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

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.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)