@@ -8,14 +8,17 @@ import type { IoHelper } from '../../api-private';
88import { cliRootDir } from '../../cli/root-dir' ;
99import { versionNumber } from '../../cli/version' ;
1010import { cdkHomeDir , formatErrorMessage , rangeFromSemver } from '../../util' ;
11- import { getLanguageAlias } from '../language' ;
11+ import type { LanguageInfo } from '../language' ;
12+ import { getLanguageAlias , getLanguageExtensions , SUPPORTED_LANGUAGES } from '../language' ;
1213
1314/* eslint-disable @typescript-eslint/no-var-requires */ // Packages don't have @types module
1415// eslint-disable-next-line @typescript-eslint/no-require-imports
1516const camelCase = require ( 'camelcase' ) ;
1617// eslint-disable-next-line @typescript-eslint/no-require-imports
1718const decamelize = require ( 'decamelize' ) ;
1819
20+ const SUPPORTED_LANGUAGE_NAMES = SUPPORTED_LANGUAGES . map ( ( l : LanguageInfo ) => l . name ) ;
21+
1922export interface CliInitOptions {
2023 /**
2124 * Template name to initialize
@@ -209,52 +212,47 @@ async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, reque
209212 * @returns Promise resolving to array of potential template directory names
210213 */
211214async function findPotentialTemplates ( repositoryPath : string ) : Promise < string [ ] > {
212- try {
213- const entries = await fs . readdir ( repositoryPath , { withFileTypes : true } ) ;
214- const potentialTemplates : string [ ] = [ ] ;
215+ const entries = await fs . readdir ( repositoryPath , { withFileTypes : true } ) ;
215216
216- for ( const entry of entries ) {
217- if ( entry . isDirectory ( ) && ! entry . name . startsWith ( '.' ) ) {
217+ const templateValidationPromises = entries
218+ . filter ( entry => entry . isDirectory ( ) && ! entry . name . startsWith ( '.' ) )
219+ . map ( async ( entry ) => {
220+ try {
218221 const templatePath = path . join ( repositoryPath , entry . name ) ;
219- const languages = await getLanguageDirectories ( templatePath ) ;
220- if ( languages . length > 0 ) {
221- potentialTemplates . push ( entry . name ) ;
222- }
222+ const { languages } = await getLanguageDirectories ( templatePath ) ;
223+ return languages . length > 0 ? entry . name : null ;
224+ } catch ( error : any ) {
225+ // If we can't read a specific template directory, skip it but don't fail the entire operation
226+ return null ;
223227 }
224- }
228+ } ) ;
225229
226- return potentialTemplates ;
227- } catch ( error : any ) {
228- return [ ] ;
229- }
230+ /* eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism */ // Limited to directory entries
231+ const validationResults = await Promise . all ( templateValidationPromises ) ;
232+ return validationResults . filter ( ( templateName ) : templateName is string => templateName !== null ) ;
230233}
231234
232235/**
233236 * Get valid CDK language directories from a template path
234237 * @param templatePath - Path to the template directory
235238 * @returns Promise resolving to array of supported language names
236239 */
237- async function getLanguageDirectories ( templatePath : string ) : Promise < string [ ] > {
238- const cdkSupportedLanguages = [ 'typescript' , 'javascript' , 'python' , 'java' , 'csharp' , 'fsharp' , 'go' ] ;
239- const languageExtensions : Record < string , string [ ] > = {
240- typescript : [ '.ts' , '.js' ] ,
241- javascript : [ '.js' ] ,
242- python : [ '.py' ] ,
243- java : [ '.java' ] ,
244- csharp : [ '.cs' ] ,
245- fsharp : [ '.fs' ] ,
246- go : [ '.go' ] ,
247- } ;
248-
240+ /**
241+ * Get valid CDK language directories from a template path
242+ * @param templatePath - Path to the template directory
243+ * @returns Promise resolving to array of supported language names and directory entries
244+ * @throws ToolkitError if directory cannot be read or validated
245+ */
246+ async function getLanguageDirectories ( templatePath : string ) : Promise < { languages : string [ ] ; entries : fs . Dirent [ ] } > {
249247 try {
250248 const entries = await fs . readdir ( templatePath , { withFileTypes : true } ) ;
251249
252250 const languageValidationPromises = entries
253- . filter ( directoryEntry => directoryEntry . isDirectory ( ) && cdkSupportedLanguages . includes ( directoryEntry . name ) )
251+ . filter ( directoryEntry => directoryEntry . isDirectory ( ) && SUPPORTED_LANGUAGE_NAMES . includes ( directoryEntry . name ) )
254252 . map ( async ( directoryEntry ) => {
255253 const languageDirectoryPath = path . join ( templatePath , directoryEntry . name ) ;
256254 try {
257- const hasValidLanguageFiles = await hasLanguageFiles ( languageDirectoryPath , languageExtensions [ directoryEntry . name ] ) ;
255+ const hasValidLanguageFiles = await hasLanguageFiles ( languageDirectoryPath , getLanguageExtensions ( directoryEntry . name ) ) ;
258256 return hasValidLanguageFiles ? directoryEntry . name : null ;
259257 } catch ( error : any ) {
260258 throw new ToolkitError ( `Cannot read language directory '${ directoryEntry . name } ': ${ error . message } ` ) ;
@@ -263,7 +261,10 @@ async function getLanguageDirectories(templatePath: string): Promise<string[]> {
263261
264262 /* eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism */ // Limited to supported CDK languages (7 max)
265263 const validationResults = await Promise . all ( languageValidationPromises ) ;
266- return validationResults . filter ( ( languageName ) : languageName is string => languageName !== null ) ;
264+ return {
265+ languages : validationResults . filter ( ( languageName ) : languageName is string => languageName !== null ) ,
266+ entries,
267+ } ;
267268 } catch ( error : any ) {
268269 throw new ToolkitError ( `Cannot read template directory '${ templatePath } ': ${ error . message } ` ) ;
269270 }
@@ -299,24 +300,6 @@ async function hasLanguageFiles(directoryPath: string, extensions: string[]): Pr
299300 return false ;
300301}
301302
302- /**
303- * Get file extensions for a specific language
304- * @param language - The programming language
305- * @returns Array of file extensions for the language
306- */
307- function getLanguageExtensions ( language : string ) : string [ ] {
308- const languageExtensions : Record < string , string [ ] > = {
309- typescript : [ '.ts' , '.js' ] ,
310- javascript : [ '.js' ] ,
311- python : [ '.py' ] ,
312- java : [ '.java' ] ,
313- csharp : [ '.cs' ] ,
314- fsharp : [ '.fs' ] ,
315- go : [ '.go' ] ,
316- } ;
317- return languageExtensions [ language ] || [ ] ;
318- }
319-
320303/**
321304 * Returns the name of the Python executable for this OS
322305 * @returns The Python executable name for the current platform
@@ -355,33 +338,31 @@ export class InitTemplate {
355338 throw new ToolkitError ( `Template path does not exist: ${ basePath } ` ) ;
356339 }
357340
358- let actualBasePath = basePath ;
359- let languages = await getLanguageDirectories ( basePath ) ;
341+ let templateSourcePath = basePath ;
342+ let { languages, entries } = await getLanguageDirectories ( basePath ) ;
360343
361- // Auto-detect single language templates
362344 if ( languages . length === 0 ) {
363- const entries = await fs . readdir ( basePath , { withFileTypes : true } ) ;
364345 const languageDirs = entries . filter ( entry =>
365346 entry . isDirectory ( ) &&
366- [ 'typescript' , 'javascript' , 'python' , 'java' , 'csharp' , 'fsharp' , 'go' ] . includes ( entry . name ) ,
347+ SUPPORTED_LANGUAGE_NAMES . includes ( entry . name ) ,
367348 ) ;
368349
369350 if ( languageDirs . length === 1 ) {
370351 // Validate that the language directory contains appropriate files
371352 const langDir = languageDirs [ 0 ] . name ;
372- const langDirPath = path . join ( basePath , langDir ) ;
373- const hasValidFiles = await hasLanguageFiles ( langDirPath , getLanguageExtensions ( langDir ) ) ;
353+ templateSourcePath = path . join ( basePath , langDir ) ;
354+ const hasValidFiles = await hasLanguageFiles ( templateSourcePath , getLanguageExtensions ( langDir ) ) ;
374355
375- if ( hasValidFiles ) {
376- actualBasePath = path . join ( basePath , langDir ) ;
377- languages = [ langDir ] ;
356+ if ( ! hasValidFiles ) {
357+ // If we found a language directory but it doesn't contain valid files, we should inform the user
358+ throw new ToolkitError ( `Found ' ${ langDir } ' directory but it doesn't contain the expected language files. Ensure the template contains ${ langDir } source files.` ) ;
378359 }
379360 }
380361 }
381362
382363 const name = path . basename ( basePath ) ;
383364
384- return new InitTemplate ( actualBasePath , name , languages , null , TemplateType . CUSTOM ) ;
365+ return new InitTemplate ( templateSourcePath , name , languages , null , TemplateType . CUSTOM ) ;
385366 }
386367
387368 public readonly description ?: string ;
@@ -700,18 +681,36 @@ async function initializeProject(
700681 await ioHelper . defaults . info ( '✅ All done!' ) ;
701682}
702683
684+ /**
685+ * Validate that a directory exists and is empty (ignoring hidden files)
686+ * @param workDir - Directory path to validate
687+ * @throws ToolkitError if directory doesn't exist or is not empty
688+ */
703689async function assertIsEmptyDirectory ( workDir : string ) {
704690 try {
691+ const stats = await fs . stat ( workDir ) ;
692+ if ( ! stats . isDirectory ( ) ) {
693+ throw new ToolkitError ( `Path exists but is not a directory: ${ workDir } ` ) ;
694+ }
695+
705696 const files = await fs . readdir ( workDir ) ;
706- if ( files . filter ( ( f ) => ! f . startsWith ( '.' ) ) . length !== 0 ) {
707- throw new ToolkitError ( '`cdk init` cannot be run in a non-empty directory!' ) ;
697+ const visibleFiles = files . filter ( f => ! f . startsWith ( '.' ) ) ;
698+
699+ if ( visibleFiles . length > 0 ) {
700+ throw new ToolkitError (
701+ '`cdk init` cannot be run in a non-empty directory!\n' +
702+ `Found ${ visibleFiles . length } visible files in ${ workDir } :\n` +
703+ visibleFiles . map ( f => ` - ${ f } ` ) . join ( '\n' ) ,
704+ ) ;
708705 }
709706 } catch ( e : any ) {
710707 if ( e . code === 'ENOENT' ) {
711- throw new ToolkitError ( `Directory does not exist: ${ workDir } . Please create the directory first.` ) ;
712- } else {
713- throw e ;
708+ throw new ToolkitError (
709+ `Directory does not exist: ${ workDir } \n` +
710+ 'Please create the directory first using: mkdir -p ' + workDir ,
711+ ) ;
714712 }
713+ throw new ToolkitError ( `Failed to validate directory ${ workDir } : ${ e . message } ` ) ;
715714 }
716715}
717716
0 commit comments