Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bc5eca1
feat(cli): auto-detect language for single-language custom templates
rohang9000 Aug 28, 2025
21c505f
Merge branch 'main' into fix/auto-detect-single-language-template
iankhou Sep 9, 2025
0bf2c03
chore(deps): upgrade dependencies (#839)
aws-cdk-automation Sep 10, 2025
0d194f9
chore(deps): upgrade dependencies (#841)
aws-cdk-automation Sep 11, 2025
6975eae
chore: enable trusted publishing for releases to npm and pypi (#842)
iliapolo Sep 11, 2025
9766049
chore: npm trusted publishing for jsii packages is ignored (#843)
iliapolo Sep 11, 2025
9def72c
chore(deps): upgrade dependencies (#844)
aws-cdk-automation Sep 11, 2025
2be5f7a
chore: bump node (and npm) version in publish jobs to allow for trust…
iliapolo Sep 11, 2025
14c188a
fix(cli): stack filter positional arg is not being respected (#846)
otaviomacedo Sep 12, 2025
1c4d691
chore(deps): upgrade dependencies (#847)
aws-cdk-automation Sep 15, 2025
30be191
chore: add close-stale-issues workflow (#849)
pahud Sep 16, 2025
2a428d7
chore: fix stale-issue-cleanup version (#851)
mrgrain Sep 18, 2025
0eaefff
fix(cloud-assembly-schema): `unconfiguredBehavesLike` contains info f…
rix0rrr Sep 18, 2025
17bd208
chore(cli): remove deprecated AWS SDK v3 utilities in favor of Smithy…
mrgrain Sep 18, 2025
30b99b4
chore: revert aws-actions/stale-issue-cleanup back to v6 (#853)
mrgrain Sep 18, 2025
421cff6
chore: fix codeowner team name (#856)
mrgrain Sep 19, 2025
f5c0f15
refactor(cli): property bag for refreshStacks function (#855)
dgandhi62 Sep 19, 2025
e6e8dbb
chore(deps): upgrade dependencies (#857)
aws-cdk-automation Sep 22, 2025
1808f42
fix(cli-integ): sporadic failures due to external causes (#897)
iliapolo Sep 29, 2025
96f6f0b
feat(toolkit-lib): add forceDeployment option to BootstrapOptions (#898)
mrgrain Sep 30, 2025
b5cb197
chore(cli): add cdk flags to readme command list (#899)
kaizencc Oct 6, 2025
c091f72
feat(cli): allow users to enable all feature flags that do not impact…
vivian12300 Oct 6, 2025
70b3dea
refactor(toolkit-lib): standardize confirmation requests to use Confi…
mrgrain Oct 6, 2025
325a749
chore(cli-lib-alpha): stop releasing deprecated package (#905)
mrgrain Oct 6, 2025
3406ff7
feat(integ-runner): use toolkit-lib as default engine (#906)
mrgrain Oct 6, 2025
07895ef
address my comments and increase test coverage for init.ts (97.81%)
iankhou Oct 7, 2025
7801fb2
Merge branch 'main' into fix/auto-detect-single-language-template
iankhou Oct 7, 2025
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
124 changes: 85 additions & 39 deletions packages/aws-cdk/lib/commands/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ import type { IoHelper } from '../../api-private';
import { cliRootDir } from '../../cli/root-dir';
import { versionNumber } from '../../cli/version';
import { cdkHomeDir, formatErrorMessage, rangeFromSemver } from '../../util';
import { getLanguageAlias } from '../language';
import type { LanguageInfo } from '../language';
import { getLanguageAlias, getLanguageExtensions, SUPPORTED_LANGUAGES } from '../language';

/* eslint-disable @typescript-eslint/no-var-requires */ // Packages don't have @types module
// eslint-disable-next-line @typescript-eslint/no-require-imports
const camelCase = require('camelcase');
// eslint-disable-next-line @typescript-eslint/no-require-imports
const decamelize = require('decamelize');

const SUPPORTED_LANGUAGE_NAMES = SUPPORTED_LANGUAGES.map((l: LanguageInfo) => l.name);

export interface CliInitOptions {
/**
* Template name to initialize
Expand Down Expand Up @@ -85,7 +88,7 @@ export async function cliInit(options: CliInitOptions) {
const generateOnly = options.generateOnly ?? false;
const workDir = options.workDir ?? process.cwd();

// Show available templates if no type and no language provided (main branch logic)
// Show available templates only if no fromPath, type, or language provided
if (!options.fromPath && !options.type && !options.language) {
await printAvailableTemplates(ioHelper);
return;
Expand Down Expand Up @@ -209,52 +212,47 @@ async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, reque
* @returns Promise resolving to array of potential template directory names
*/
async function findPotentialTemplates(repositoryPath: string): Promise<string[]> {
try {
const entries = await fs.readdir(repositoryPath, { withFileTypes: true });
const potentialTemplates: string[] = [];
const entries = await fs.readdir(repositoryPath, { withFileTypes: true });

for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('.')) {
const templateValidationPromises = entries
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
.map(async (entry) => {
try {
const templatePath = path.join(repositoryPath, entry.name);
const languages = await getLanguageDirectories(templatePath);
if (languages.length > 0) {
potentialTemplates.push(entry.name);
}
const { languages } = await getLanguageDirectories(templatePath);
return languages.length > 0 ? entry.name : null;
} catch (error: any) {
// If we can't read a specific template directory, skip it but don't fail the entire operation
return null;
}
}
});

return potentialTemplates;
} catch (error: any) {
return [];
}
/* eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism */ // Limited to directory entries
const validationResults = await Promise.all(templateValidationPromises);
return validationResults.filter((templateName): templateName is string => templateName !== null);
}

/**
* Get valid CDK language directories from a template path
* @param templatePath - Path to the template directory
* @returns Promise resolving to array of supported language names
*/
async function getLanguageDirectories(templatePath: string): Promise<string[]> {
const cdkSupportedLanguages = ['typescript', 'javascript', 'python', 'java', 'csharp', 'fsharp', 'go'];
const languageExtensions: Record<string, string[]> = {
typescript: ['.ts', '.js'],
javascript: ['.js'],
python: ['.py'],
java: ['.java'],
csharp: ['.cs'],
fsharp: ['.fs'],
go: ['.go'],
};

/**
* Get valid CDK language directories from a template path
* @param templatePath - Path to the template directory
* @returns Promise resolving to array of supported language names and directory entries
* @throws ToolkitError if directory cannot be read or validated
*/
async function getLanguageDirectories(templatePath: string): Promise<{ languages: string[]; entries: fs.Dirent[] }> {
try {
const entries = await fs.readdir(templatePath, { withFileTypes: true });

const languageValidationPromises = entries
.filter(directoryEntry => directoryEntry.isDirectory() && cdkSupportedLanguages.includes(directoryEntry.name))
.filter(directoryEntry => directoryEntry.isDirectory() && SUPPORTED_LANGUAGE_NAMES.includes(directoryEntry.name))
.map(async (directoryEntry) => {
const languageDirectoryPath = path.join(templatePath, directoryEntry.name);
try {
const hasValidLanguageFiles = await hasLanguageFiles(languageDirectoryPath, languageExtensions[directoryEntry.name]);
const hasValidLanguageFiles = await hasLanguageFiles(languageDirectoryPath, getLanguageExtensions(directoryEntry.name));
return hasValidLanguageFiles ? directoryEntry.name : null;
} catch (error: any) {
throw new ToolkitError(`Cannot read language directory '${directoryEntry.name}': ${error.message}`);
Expand All @@ -263,7 +261,10 @@ async function getLanguageDirectories(templatePath: string): Promise<string[]> {

/* eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism */ // Limited to supported CDK languages (7 max)
const validationResults = await Promise.all(languageValidationPromises);
return validationResults.filter((languageName): languageName is string => languageName !== null);
return {
languages: validationResults.filter((languageName): languageName is string => languageName !== null),
entries,
};
} catch (error: any) {
throw new ToolkitError(`Cannot read template directory '${templatePath}': ${error.message}`);
}
Expand Down Expand Up @@ -337,10 +338,31 @@ export class InitTemplate {
throw new ToolkitError(`Template path does not exist: ${basePath}`);
}

const languages = await getLanguageDirectories(basePath);
let templateSourcePath = basePath;
let { languages, entries } = await getLanguageDirectories(basePath);

if (languages.length === 0) {
const languageDirs = entries.filter(entry =>
entry.isDirectory() &&
SUPPORTED_LANGUAGE_NAMES.includes(entry.name),
);

if (languageDirs.length === 1) {
// Validate that the language directory contains appropriate files
const langDir = languageDirs[0].name;
templateSourcePath = path.join(basePath, langDir);
const hasValidFiles = await hasLanguageFiles(templateSourcePath, getLanguageExtensions(langDir));

if (!hasValidFiles) {
// If we found a language directory but it doesn't contain valid files, we should inform the user
throw new ToolkitError(`Found '${langDir}' directory but it doesn't contain the expected language files. Ensure the template contains ${langDir} source files.`);
}
}
}

const name = path.basename(basePath);

return new InitTemplate(basePath, name, languages, null, TemplateType.CUSTOM);
return new InitTemplate(templateSourcePath, name, languages, null, TemplateType.CUSTOM);
}

public readonly description?: string;
Expand Down Expand Up @@ -401,7 +423,13 @@ export class InitTemplate {
projectInfo.versions['aws-cdk-lib'] = libVersion;
}

const sourceDirectory = path.join(this.basePath, language);
let sourceDirectory = path.join(this.basePath, language);

// For auto-detected single language templates, use basePath directly
if (this.templateType === TemplateType.CUSTOM && this.languages.length === 1 &&
path.basename(this.basePath) === language) {
sourceDirectory = this.basePath;
}

if (this.templateType === TemplateType.CUSTOM) {
// For custom templates, copy files without processing placeholders
Expand Down Expand Up @@ -653,18 +681,36 @@ async function initializeProject(
await ioHelper.defaults.info('✅ All done!');
}

/**
* Validate that a directory exists and is empty (ignoring hidden files)
* @param workDir - Directory path to validate
* @throws ToolkitError if directory doesn't exist or is not empty
*/
async function assertIsEmptyDirectory(workDir: string) {
try {
const stats = await fs.stat(workDir);
if (!stats.isDirectory()) {
throw new ToolkitError(`Path exists but is not a directory: ${workDir}`);
}

const files = await fs.readdir(workDir);
if (files.filter((f) => !f.startsWith('.')).length !== 0) {
throw new ToolkitError('`cdk init` cannot be run in a non-empty directory!');
const visibleFiles = files.filter(f => !f.startsWith('.'));

if (visibleFiles.length > 0) {
throw new ToolkitError(
'`cdk init` cannot be run in a non-empty directory!\n' +
`Found ${visibleFiles.length} visible files in ${workDir}:\n` +
visibleFiles.map(f => ` - ${f}`).join('\n'),
);
}
} catch (e: any) {
if (e.code === 'ENOENT') {
throw new ToolkitError(`Directory does not exist: ${workDir}. Please create the directory first.`);
} else {
throw e;
throw new ToolkitError(
`Directory does not exist: ${workDir}\n` +
'Please create the directory first using: mkdir -p ' + workDir,
);
}
throw new ToolkitError(`Failed to validate directory ${workDir}: ${e.message}`);
}
}

Expand Down
33 changes: 25 additions & 8 deletions packages/aws-cdk/lib/commands/language.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
export const SUPPORTED_LANGUAGES: { name: string; alias: string }[] = [
{ name: 'csharp', alias: 'cs' },
{ name: 'fsharp', alias: 'fs' },
{ name: 'go', alias: 'go' },
{ name: 'java', alias: 'java' },
{ name: 'javascript', alias: 'js' },
{ name: 'python', alias: 'py' },
{ name: 'typescript', alias: 'ts' },
export interface LanguageInfo {
name: string;
alias: string;
extensions: string[];
}

export const SUPPORTED_LANGUAGES: LanguageInfo[] = [
{ name: 'csharp', alias: 'cs', extensions: ['.cs'] },
{ name: 'fsharp', alias: 'fs', extensions: ['.fs'] },
{ name: 'go', alias: 'go', extensions: ['.go'] },
{ name: 'java', alias: 'java', extensions: ['.java'] },
{ name: 'javascript', alias: 'js', extensions: ['.js'] },
{ name: 'python', alias: 'py', extensions: ['.py'] },
{ name: 'typescript', alias: 'ts', extensions: ['.ts', '.js'] },
];

/**
Expand All @@ -29,3 +35,14 @@ export function getLanguageAlias(language: string): string | undefined {
export function getLanguageFromAlias(alias: string): string | undefined {
return SUPPORTED_LANGUAGES.find((l) => l.alias === alias || l.name === alias)?.name;
}

/**
* get the file extensions for a given language name or alias
*
* @example
* getLanguageExtensions('typescript') // returns ['.ts', '.js']
* getLanguageExtensions('python') // returns ['.py']
*/
export function getLanguageExtensions(language: string): string[] {
return SUPPORTED_LANGUAGES.find((l) => l.name === language || l.alias === language)?.extensions ?? [];
}
Loading
Loading