Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CliHelpers, type CliConfig } from '@aws-cdk/user-input-gen';
import * as cdk_from_cfn from 'cdk-from-cfn';
import { StackActivityProgress } from '../commands/deploy';
import { availableInitLanguages } from '../commands/init';
import { JS_PACKAGE_MANAGERS } from '../commands/init/package-manager';
import { getLanguageAlias } from '../commands/language';

export const YARGS_HELPERS = new CliHelpers('./util/yargs-helpers');
Expand Down Expand Up @@ -407,6 +408,7 @@ export async function makeConfig(): Promise<CliConfig> {
'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.' },
'from-path': { type: 'string', desc: 'Path to a local custom template directory or multi-template repository', requiresArg: true, conflicts: ['lib-version'] },
'template-path': { type: 'string', desc: 'Path to a specific template within a multi-template repository', requiresArg: true },
'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 },
},
implies: { 'template-path': 'from-path' },
},
Expand Down
10 changes: 10 additions & 0 deletions packages/aws-cdk/lib/cli/cli-type-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,16 @@
"type": "string",
"desc": "Path to a specific template within a multi-template repository",
"requiresArg": true
},
"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": [
"npm",
"yarn",
"pnpm",
"bun"
]
}
},
"implies": {
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
libVersion: args.libVersion,
fromPath: args['from-path'],
templatePath: args['template-path'],
packageManager: args['package-manager'],
});
}
case 'migrate':
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/cli/convert-to-user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ export function convertYargsToUserInput(args: any): UserInput {
libVersion: args.libVersion,
fromPath: args.fromPath,
templatePath: args.templatePath,
packageManager: args.packageManager,
TEMPLATE: args.TEMPLATE,
};
break;
Expand Down Expand Up @@ -493,6 +494,7 @@ export function convertConfigToUserInput(config: any): UserInput {
libVersion: config.init?.libVersion,
fromPath: config.init?.fromPath,
templatePath: config.init?.templatePath,
packageManager: config.init?.packageManager,
};
const migrateOptions = {
stackName: config.migrate?.stackName,
Expand Down
6 changes: 6 additions & 0 deletions packages/aws-cdk/lib/cli/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,12 @@ export function parseCommandLineArguments(args: Array<string>): any {
type: 'string',
desc: 'Path to a specific template within a multi-template repository',
requiresArg: true,
})
.option('package-manager', {
default: undefined,
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: ['npm', 'yarn', 'pnpm', 'bun'],
}),
)
.command('migrate', 'Migrate existing AWS resources into a CDK app', (yargs: Argv) =>
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/cli/user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,13 @@ export interface InitOptions {
*/
readonly templatePath?: string;

/**
* The package manager to use to install dependencies. Only applicable for TypeScript and JavaScript projects. Defaults to npm in TypeScript and JavaScript projects.
*
* @default - undefined
*/
readonly packageManager?: string;

/**
* Positional argument for init
*/
Expand Down
36 changes: 28 additions & 8 deletions packages/aws-cdk/lib/commands/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { versionNumber } from '../../cli/version';
import { cdkHomeDir, formatErrorMessage, rangeFromSemver } from '../../util';
import type { LanguageInfo } from '../language';
import { getLanguageAlias, getLanguageExtensions, SUPPORTED_LANGUAGES } from '../language';
import type { JsPackageManager } from './package-manager';

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

/**
* The package manager to use for installing dependencies. Only applicable for TypeScript and JavaScript projects.
* @default - If specified language is 'typescript' or 'javascript', 'npm' is selected. Otherwise, no package manager is used.
*/
readonly packageManager?: JsPackageManager;

readonly ioHelper: IoHelper;
}

/**
* Initialize a CDK package in the current directory
*/
export async function cliInit(options: CliInitOptions) {
await ensureValidCliInitOptions(options, options.ioHelper);

const ioHelper = options.ioHelper;
const canUseNetwork = options.canUseNetwork ?? true;
const generateOnly = options.generateOnly ?? false;
Expand Down Expand Up @@ -116,9 +125,19 @@ export async function cliInit(options: CliInitOptions) {
options.stackName,
options.migrate,
options.libVersion,
options.packageManager,
);
}

/**
* Validate CLI init options and handle invalid or incompatible option combinations
*/
async function ensureValidCliInitOptions(options: CliInitOptions, ioHelper: IoHelper) {
if (options.packageManager && !['javascript', 'typescript'].includes(options.language ?? '')) {
await ioHelper.defaults.warn(`--package-manager option is only applicable for JavaScript and TypeScript projects. Ignoring the provided value: ${options.packageManager}`);
}
}

/**
* Load a local custom template from file system path
* @param fromPath - Path to the local template directory or multi-template repository
Expand Down Expand Up @@ -653,6 +672,7 @@ async function initializeProject(
stackName?: string,
migrate?: boolean,
cdkVersion?: string,
packageManager?: JsPackageManager,
) {
// Step 1: Ensure target directory is empty
await assertIsEmptyDirectory(workDir);
Expand All @@ -675,7 +695,7 @@ async function initializeProject(
await initializeGitRepository(ioHelper, workDir);

// Step 4: Post-install steps
await postInstall(ioHelper, language, canUseNetwork, workDir);
await postInstall(ioHelper, language, canUseNetwork, workDir, packageManager);
}

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

async function postInstall(ioHelper: IoHelper, language: string, canUseNetwork: boolean, workDir: string) {
async function postInstall(ioHelper: IoHelper, language: string, canUseNetwork: boolean, workDir: string, packageManager?: JsPackageManager) {
switch (language) {
case 'javascript':
return postInstallJavascript(ioHelper, canUseNetwork, workDir);
return postInstallJavascript(ioHelper, canUseNetwork, workDir, packageManager);
case 'typescript':
return postInstallTypescript(ioHelper, canUseNetwork, workDir);
return postInstallTypescript(ioHelper, canUseNetwork, workDir, packageManager);
case 'java':
return postInstallJava(ioHelper, canUseNetwork, workDir);
case 'python':
Expand All @@ -747,12 +767,12 @@ async function postInstall(ioHelper: IoHelper, language: string, canUseNetwork:
}
}

async function postInstallJavascript(ioHelper: IoHelper, canUseNetwork: boolean, cwd: string) {
return postInstallTypescript(ioHelper, canUseNetwork, cwd);
async function postInstallJavascript(ioHelper: IoHelper, canUseNetwork: boolean, cwd: string, packageManager?: JsPackageManager) {
return postInstallTypescript(ioHelper, canUseNetwork, cwd, packageManager);
}

async function postInstallTypescript(ioHelper: IoHelper, canUseNetwork: boolean, cwd: string) {
const command = 'npm';
async function postInstallTypescript(ioHelper: IoHelper, canUseNetwork: boolean, cwd: string, packageManager?: JsPackageManager) {
const command = packageManager ?? 'npm';

if (!canUseNetwork) {
await ioHelper.defaults.warn(`Please run '${command} install'!`);
Expand Down
3 changes: 3 additions & 0 deletions packages/aws-cdk/lib/commands/init/package-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const JS_PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'bun'] as const;

export type JsPackageManager = (typeof JS_PACKAGE_MANAGERS)[number];
139 changes: 139 additions & 0 deletions packages/aws-cdk/test/commands/init.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as child_process from 'child_process';
import * as os from 'os';
import * as path from 'path';
import * as cxapi from '@aws-cdk/cx-api';
import * as fs from 'fs-extra';
import { availableInitLanguages, availableInitTemplates, cliInit, currentlyRecommendedAwsCdkLibFlags, expandPlaceholders, printAvailableTemplates } from '../../lib/commands/init';
import type { JsPackageManager } from '../../lib/commands/init/package-manager';
import { createSingleLanguageTemplate, createMultiLanguageTemplate, createMultiTemplateRepository } from '../_fixtures/init-templates/template-helpers';
import { TestIoHost } from '../_helpers/io-host';

Expand Down Expand Up @@ -1352,6 +1354,143 @@ describe('constructs version', () => {
// cdk.json should not exist since template didn't have one
expect(await fs.pathExists(path.join(projectDir, 'cdk.json'))).toBeFalsy();
});

describe('package-manager option', () => {
let spawnSpy: jest.SpyInstance;

beforeEach(async () => {
// Mock child_process.spawn to track which package manager is called
spawnSpy = jest.spyOn(child_process, 'spawn').mockImplementation(() => ({
stdout: { on: jest.fn() },
}) as unknown as child_process.ChildProcess);
});

afterEach(() => {
spawnSpy.mockRestore();
});

test.each([
{ language: 'typescript', packageManager: 'npm' },
{ language: 'typescript', packageManager: 'yarn' },
{ language: 'typescript', packageManager: 'pnpm' },
{ language: 'typescript', packageManager: 'bun' },
{ language: 'javascript', packageManager: 'npm' },
{ language: 'javascript', packageManager: 'yarn' },
{ language: 'javascript', packageManager: 'pnpm' },
{ language: 'javascript', packageManager: 'bun' },
])('uses $packageManager for $language project', async ({ language, packageManager }) => {
await withTempDir(async (workDir) => {
await cliInit({
ioHelper,
type: 'app',
language,
packageManager: packageManager as JsPackageManager,
workDir,
});

const installCalls = spawnSpy.mock.calls.filter(
([cmd, args]) => cmd === packageManager && args.includes('install'),
);
expect(installCalls.length).toBeGreaterThan(0);
});
});

cliTest('uses npm as default when package manager not specified', async (workDir) => {
await cliInit({
ioHelper,
type: 'app',
language: 'typescript',
workDir,
});

const installCalls = spawnSpy.mock.calls.filter(
([cmd, args]) => cmd === 'npm' && args.includes('install'),
);
expect(installCalls.length).toBeGreaterThan(0);
});

cliTest('ignores package manager option for non-JavaScript languages', async (workDir) => {
await cliInit({
ioHelper,
type: 'app',
language: 'python',
packageManager: 'yarn',
canUseNetwork: false,
generateOnly: true,
workDir,
});

expect(await fs.pathExists(path.join(workDir, 'requirements.txt'))).toBeTruthy();
});
});

describe('validate CLI init options', () => {
const cdkBin = path.join(__dirname, '..', '..', 'bin', 'cdk');
const commonEnv = { ...process.env, CDK_DISABLE_VERSION_CHECK: '1', CI: 'true', FORCE_COLOR: '0' };

test.each([
'python',
'java',
'go',
'csharp',
'fsharp',
])('warns when package-manager option is specified for non-JS language=%s', async (language) => {
await withTempDir(async (workDir) => {
const output = child_process.execSync(
`node ${cdkBin} init app --language ${language} --package-manager npm --generate-only`,
{
cwd: workDir,
env: commonEnv,
encoding: 'utf-8',
},
);

expect(output).toContain('--package-manager option is only applicable for JavaScript and TypeScript projects');
expect(output).toContain(`Applying project template app for ${language}`);
});
});

test.each([
'python',
'java',
'go',
'csharp',
'fsharp',
])('does not warn when package-manager option is omitted for non-JS language=%s', async (language) => {
await withTempDir(async (workDir) => {
const output = child_process.execSync(
`node ${cdkBin} init app --language ${language} --generate-only`,
{
cwd: workDir,
env: commonEnv,
encoding: 'utf-8',
},
);

expect(output).not.toContain('--package-manager option is only applicable for JavaScript and TypeScript projects');
expect(output).toContain(`Applying project template app for ${language}`);
});
});

test.each([
'typescript',
'javascript',
])('does not warn when package-manager option is specified for language=%s', async (language) => {
await withTempDir(async (workDir) => {
const output = child_process.execSync(
`node ${cdkBin} init app --language ${language} --generate-only`,
{
cwd: workDir,
env: commonEnv,
encoding: 'utf-8',
},
);

expect(output).not.toContain('--package-manager option is only applicable for JavaScript and TypeScript projects');
expect(output).toContain(`Applying project template app for ${language}`);
});
});
});
});

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