Skip to content

Commit 4f01af8

Browse files
feat(cli): add package manager option to cdk init (#961)
Fixes #940 Adds `--package-manager` option to `cdk init` for choosing npm, yarn, or pnpm when initializing TypeScript/JavaScript projects. ### Usage ```bash cdk init app -l ts --package-manager npm cdk init app -l ts --package-manager yarn cdk init app -l ts --package-manager pnpm cdk init app -l ts --package-manager bun ``` --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Co-authored-by: Momo Kornher <kornherm@amazon.co.uk>
1 parent 3ca8b70 commit 4f01af8

File tree

9 files changed

+198
-8
lines changed

9 files changed

+198
-8
lines changed

packages/aws-cdk/lib/cli/cli-config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CliHelpers, type CliConfig } from '@aws-cdk/user-input-gen';
44
import * as cdk_from_cfn from 'cdk-from-cfn';
55
import { StackActivityProgress } from '../commands/deploy';
66
import { availableInitLanguages } from '../commands/init';
7+
import { JS_PACKAGE_MANAGERS } from '../commands/init/package-manager';
78
import { getLanguageAlias } from '../commands/language';
89

910
export const YARGS_HELPERS = new CliHelpers('./util/yargs-helpers');
@@ -407,6 +408,7 @@ export async function makeConfig(): Promise<CliConfig> {
407408
'lib-version': { type: 'string', alias: 'V', default: undefined, desc: 'The version of the CDK library (aws-cdk-lib) to initialize built-in templates with. Defaults to the version that was current when this CLI was built.' },
408409
'from-path': { type: 'string', desc: 'Path to a local custom template directory or multi-template repository', requiresArg: true, conflicts: ['lib-version'] },
409410
'template-path': { type: 'string', desc: 'Path to a specific template within a multi-template repository', requiresArg: true },
411+
'package-manager': { type: 'string', desc: 'The package manager to use to install dependencies. Only applicable for TypeScript and JavaScript projects. Defaults to npm in TypeScript and JavaScript projects.', choices: JS_PACKAGE_MANAGERS },
410412
},
411413
implies: { 'template-path': 'from-path' },
412414
},

packages/aws-cdk/lib/cli/cli-type-registry.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,16 @@
903903
"type": "string",
904904
"desc": "Path to a specific template within a multi-template repository",
905905
"requiresArg": true
906+
},
907+
"package-manager": {
908+
"type": "string",
909+
"desc": "The package manager to use to install dependencies. Only applicable for TypeScript and JavaScript projects. Defaults to npm in TypeScript and JavaScript projects.",
910+
"choices": [
911+
"npm",
912+
"yarn",
913+
"pnpm",
914+
"bun"
915+
]
906916
}
907917
},
908918
"implies": {

packages/aws-cdk/lib/cli/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
565565
libVersion: args.libVersion,
566566
fromPath: args['from-path'],
567567
templatePath: args['template-path'],
568+
packageManager: args['package-manager'],
568569
});
569570
}
570571
case 'migrate':

packages/aws-cdk/lib/cli/convert-to-user-input.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ export function convertYargsToUserInput(args: any): UserInput {
250250
libVersion: args.libVersion,
251251
fromPath: args.fromPath,
252252
templatePath: args.templatePath,
253+
packageManager: args.packageManager,
253254
TEMPLATE: args.TEMPLATE,
254255
};
255256
break;
@@ -493,6 +494,7 @@ export function convertConfigToUserInput(config: any): UserInput {
493494
libVersion: config.init?.libVersion,
494495
fromPath: config.init?.fromPath,
495496
templatePath: config.init?.templatePath,
497+
packageManager: config.init?.packageManager,
496498
};
497499
const migrateOptions = {
498500
stackName: config.migrate?.stackName,

packages/aws-cdk/lib/cli/parse-command-line-arguments.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,12 @@ export function parseCommandLineArguments(args: Array<string>): any {
894894
type: 'string',
895895
desc: 'Path to a specific template within a multi-template repository',
896896
requiresArg: true,
897+
})
898+
.option('package-manager', {
899+
default: undefined,
900+
type: 'string',
901+
desc: 'The package manager to use to install dependencies. Only applicable for TypeScript and JavaScript projects. Defaults to npm in TypeScript and JavaScript projects.',
902+
choices: ['npm', 'yarn', 'pnpm', 'bun'],
897903
}),
898904
)
899905
.command('migrate', 'Migrate existing AWS resources into a CDK app', (yargs: Argv) =>

packages/aws-cdk/lib/cli/user-input.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,13 @@ export interface InitOptions {
14081408
*/
14091409
readonly templatePath?: string;
14101410

1411+
/**
1412+
* The package manager to use to install dependencies. Only applicable for TypeScript and JavaScript projects. Defaults to npm in TypeScript and JavaScript projects.
1413+
*
1414+
* @default - undefined
1415+
*/
1416+
readonly packageManager?: string;
1417+
14111418
/**
14121419
* Positional argument for init
14131420
*/

packages/aws-cdk/lib/commands/init/init.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { versionNumber } from '../../cli/version';
1010
import { cdkHomeDir, formatErrorMessage, rangeFromSemver } from '../../util';
1111
import type { LanguageInfo } from '../language';
1212
import { getLanguageAlias, getLanguageExtensions, SUPPORTED_LANGUAGES } from '../language';
13+
import type { JsPackageManager } from './package-manager';
1314

1415
/* eslint-disable @typescript-eslint/no-var-requires */ // Packages don't have @types module
1516
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -76,13 +77,21 @@ export interface CliInitOptions {
7677
*/
7778
readonly templatePath?: string;
7879

80+
/**
81+
* The package manager to use for installing dependencies. Only applicable for TypeScript and JavaScript projects.
82+
* @default - If specified language is 'typescript' or 'javascript', 'npm' is selected. Otherwise, no package manager is used.
83+
*/
84+
readonly packageManager?: JsPackageManager;
85+
7986
readonly ioHelper: IoHelper;
8087
}
8188

8289
/**
8390
* Initialize a CDK package in the current directory
8491
*/
8592
export async function cliInit(options: CliInitOptions) {
93+
await ensureValidCliInitOptions(options, options.ioHelper);
94+
8695
const ioHelper = options.ioHelper;
8796
const canUseNetwork = options.canUseNetwork ?? true;
8897
const generateOnly = options.generateOnly ?? false;
@@ -116,9 +125,19 @@ export async function cliInit(options: CliInitOptions) {
116125
options.stackName,
117126
options.migrate,
118127
options.libVersion,
128+
options.packageManager,
119129
);
120130
}
121131

132+
/**
133+
* Validate CLI init options and handle invalid or incompatible option combinations
134+
*/
135+
async function ensureValidCliInitOptions(options: CliInitOptions, ioHelper: IoHelper) {
136+
if (options.packageManager && !['javascript', 'typescript'].includes(options.language ?? '')) {
137+
await ioHelper.defaults.warn(`--package-manager option is only applicable for JavaScript and TypeScript projects. Ignoring the provided value: ${options.packageManager}`);
138+
}
139+
}
140+
122141
/**
123142
* Load a local custom template from file system path
124143
* @param fromPath - Path to the local template directory or multi-template repository
@@ -653,6 +672,7 @@ async function initializeProject(
653672
stackName?: string,
654673
migrate?: boolean,
655674
cdkVersion?: string,
675+
packageManager?: JsPackageManager,
656676
) {
657677
// Step 1: Ensure target directory is empty
658678
await assertIsEmptyDirectory(workDir);
@@ -675,7 +695,7 @@ async function initializeProject(
675695
await initializeGitRepository(ioHelper, workDir);
676696

677697
// Step 4: Post-install steps
678-
await postInstall(ioHelper, language, canUseNetwork, workDir);
698+
await postInstall(ioHelper, language, canUseNetwork, workDir, packageManager);
679699
}
680700

681701
await ioHelper.defaults.info('✅ All done!');
@@ -728,12 +748,12 @@ async function initializeGitRepository(ioHelper: IoHelper, workDir: string) {
728748
}
729749
}
730750

731-
async function postInstall(ioHelper: IoHelper, language: string, canUseNetwork: boolean, workDir: string) {
751+
async function postInstall(ioHelper: IoHelper, language: string, canUseNetwork: boolean, workDir: string, packageManager?: JsPackageManager) {
732752
switch (language) {
733753
case 'javascript':
734-
return postInstallJavascript(ioHelper, canUseNetwork, workDir);
754+
return postInstallJavascript(ioHelper, canUseNetwork, workDir, packageManager);
735755
case 'typescript':
736-
return postInstallTypescript(ioHelper, canUseNetwork, workDir);
756+
return postInstallTypescript(ioHelper, canUseNetwork, workDir, packageManager);
737757
case 'java':
738758
return postInstallJava(ioHelper, canUseNetwork, workDir);
739759
case 'python':
@@ -747,12 +767,12 @@ async function postInstall(ioHelper: IoHelper, language: string, canUseNetwork:
747767
}
748768
}
749769

750-
async function postInstallJavascript(ioHelper: IoHelper, canUseNetwork: boolean, cwd: string) {
751-
return postInstallTypescript(ioHelper, canUseNetwork, cwd);
770+
async function postInstallJavascript(ioHelper: IoHelper, canUseNetwork: boolean, cwd: string, packageManager?: JsPackageManager) {
771+
return postInstallTypescript(ioHelper, canUseNetwork, cwd, packageManager);
752772
}
753773

754-
async function postInstallTypescript(ioHelper: IoHelper, canUseNetwork: boolean, cwd: string) {
755-
const command = 'npm';
774+
async function postInstallTypescript(ioHelper: IoHelper, canUseNetwork: boolean, cwd: string, packageManager?: JsPackageManager) {
775+
const command = packageManager ?? 'npm';
756776

757777
if (!canUseNetwork) {
758778
await ioHelper.defaults.warn(`Please run '${command} install'!`);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const JS_PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'bun'] as const;
2+
3+
export type JsPackageManager = (typeof JS_PACKAGE_MANAGERS)[number];

packages/aws-cdk/test/commands/init.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import * as child_process from 'child_process';
12
import * as os from 'os';
23
import * as path from 'path';
34
import * as cxapi from '@aws-cdk/cx-api';
45
import * as fs from 'fs-extra';
56
import { availableInitLanguages, availableInitTemplates, cliInit, currentlyRecommendedAwsCdkLibFlags, expandPlaceholders, printAvailableTemplates } from '../../lib/commands/init';
7+
import type { JsPackageManager } from '../../lib/commands/init/package-manager';
68
import { createSingleLanguageTemplate, createMultiLanguageTemplate, createMultiTemplateRepository } from '../_fixtures/init-templates/template-helpers';
79
import { TestIoHost } from '../_helpers/io-host';
810

@@ -1352,6 +1354,143 @@ describe('constructs version', () => {
13521354
// cdk.json should not exist since template didn't have one
13531355
expect(await fs.pathExists(path.join(projectDir, 'cdk.json'))).toBeFalsy();
13541356
});
1357+
1358+
describe('package-manager option', () => {
1359+
let spawnSpy: jest.SpyInstance;
1360+
1361+
beforeEach(async () => {
1362+
// Mock child_process.spawn to track which package manager is called
1363+
spawnSpy = jest.spyOn(child_process, 'spawn').mockImplementation(() => ({
1364+
stdout: { on: jest.fn() },
1365+
}) as unknown as child_process.ChildProcess);
1366+
});
1367+
1368+
afterEach(() => {
1369+
spawnSpy.mockRestore();
1370+
});
1371+
1372+
test.each([
1373+
{ language: 'typescript', packageManager: 'npm' },
1374+
{ language: 'typescript', packageManager: 'yarn' },
1375+
{ language: 'typescript', packageManager: 'pnpm' },
1376+
{ language: 'typescript', packageManager: 'bun' },
1377+
{ language: 'javascript', packageManager: 'npm' },
1378+
{ language: 'javascript', packageManager: 'yarn' },
1379+
{ language: 'javascript', packageManager: 'pnpm' },
1380+
{ language: 'javascript', packageManager: 'bun' },
1381+
])('uses $packageManager for $language project', async ({ language, packageManager }) => {
1382+
await withTempDir(async (workDir) => {
1383+
await cliInit({
1384+
ioHelper,
1385+
type: 'app',
1386+
language,
1387+
packageManager: packageManager as JsPackageManager,
1388+
workDir,
1389+
});
1390+
1391+
const installCalls = spawnSpy.mock.calls.filter(
1392+
([cmd, args]) => cmd === packageManager && args.includes('install'),
1393+
);
1394+
expect(installCalls.length).toBeGreaterThan(0);
1395+
});
1396+
});
1397+
1398+
cliTest('uses npm as default when package manager not specified', async (workDir) => {
1399+
await cliInit({
1400+
ioHelper,
1401+
type: 'app',
1402+
language: 'typescript',
1403+
workDir,
1404+
});
1405+
1406+
const installCalls = spawnSpy.mock.calls.filter(
1407+
([cmd, args]) => cmd === 'npm' && args.includes('install'),
1408+
);
1409+
expect(installCalls.length).toBeGreaterThan(0);
1410+
});
1411+
1412+
cliTest('ignores package manager option for non-JavaScript languages', async (workDir) => {
1413+
await cliInit({
1414+
ioHelper,
1415+
type: 'app',
1416+
language: 'python',
1417+
packageManager: 'yarn',
1418+
canUseNetwork: false,
1419+
generateOnly: true,
1420+
workDir,
1421+
});
1422+
1423+
expect(await fs.pathExists(path.join(workDir, 'requirements.txt'))).toBeTruthy();
1424+
});
1425+
});
1426+
1427+
describe('validate CLI init options', () => {
1428+
const cdkBin = path.join(__dirname, '..', '..', 'bin', 'cdk');
1429+
const commonEnv = { ...process.env, CDK_DISABLE_VERSION_CHECK: '1', CI: 'true', FORCE_COLOR: '0' };
1430+
1431+
test.each([
1432+
'python',
1433+
'java',
1434+
'go',
1435+
'csharp',
1436+
'fsharp',
1437+
])('warns when package-manager option is specified for non-JS language=%s', async (language) => {
1438+
await withTempDir(async (workDir) => {
1439+
const output = child_process.execSync(
1440+
`node ${cdkBin} init app --language ${language} --package-manager npm --generate-only`,
1441+
{
1442+
cwd: workDir,
1443+
env: commonEnv,
1444+
encoding: 'utf-8',
1445+
},
1446+
);
1447+
1448+
expect(output).toContain('--package-manager option is only applicable for JavaScript and TypeScript projects');
1449+
expect(output).toContain(`Applying project template app for ${language}`);
1450+
});
1451+
});
1452+
1453+
test.each([
1454+
'python',
1455+
'java',
1456+
'go',
1457+
'csharp',
1458+
'fsharp',
1459+
])('does not warn when package-manager option is omitted for non-JS language=%s', async (language) => {
1460+
await withTempDir(async (workDir) => {
1461+
const output = child_process.execSync(
1462+
`node ${cdkBin} init app --language ${language} --generate-only`,
1463+
{
1464+
cwd: workDir,
1465+
env: commonEnv,
1466+
encoding: 'utf-8',
1467+
},
1468+
);
1469+
1470+
expect(output).not.toContain('--package-manager option is only applicable for JavaScript and TypeScript projects');
1471+
expect(output).toContain(`Applying project template app for ${language}`);
1472+
});
1473+
});
1474+
1475+
test.each([
1476+
'typescript',
1477+
'javascript',
1478+
])('does not warn when package-manager option is specified for language=%s', async (language) => {
1479+
await withTempDir(async (workDir) => {
1480+
const output = child_process.execSync(
1481+
`node ${cdkBin} init app --language ${language} --generate-only`,
1482+
{
1483+
cwd: workDir,
1484+
env: commonEnv,
1485+
encoding: 'utf-8',
1486+
},
1487+
);
1488+
1489+
expect(output).not.toContain('--package-manager option is only applicable for JavaScript and TypeScript projects');
1490+
expect(output).toContain(`Applying project template app for ${language}`);
1491+
});
1492+
});
1493+
});
13551494
});
13561495

13571496
test('when no version number is present (e.g., local development), the v2 templates are chosen by default', async () => {

0 commit comments

Comments
 (0)