import chalk from 'chalk'; import fs from 'node:fs'; import path from 'node:path'; import { RootConfigWrite } from '../domain/root-config'; import { CodeDirectoryName, TopLevelDirectoryName, TopLevelFileName, WorkflowsDirectoryName, } from '../domain/file-names'; const alterationHooksFilePath = path.join(TopLevelDirectoryName.Workflows, WorkflowsDirectoryName.AlterationHooks); const applicationAlterationHooksFilePath = path.join( TopLevelDirectoryName.Workflows, WorkflowsDirectoryName.ApplicationAlterationHooks, ); const memberAlterationHooksFilePath = path.join( TopLevelDirectoryName.Workflows, WorkflowsDirectoryName.MemberAlterationHooks, ); const formatHookMismatch = (params: { header: string; hooksDirPath: string; configKey: 'alterationHooks' | 'applicationAlterationHooks' | 'memberAlterationHooks'; missingFiles: string[]; extraFiles: string[]; }): string => { const { header, hooksDirPath, configKey, missingFiles, extraFiles } = params; const hooksDirDisplay = `${hooksDirPath}/`; const yellowDir = chalk.yellow(hooksDirDisplay); const yellowConfigFile = chalk.yellow(TopLevelFileName.RootConfig); const lines: string[] = [header, `Source: ${yellowConfigFile} ↔ ${yellowDir}`, '']; if (missingFiles.length > 0) { lines.push(`Missing files (listed in config but not found in ${yellowDir}):`); for (const key of missingFiles) { const fileName = `${key}.json`; lines.push(` • ${chalk.yellow(fileName)}`); } lines.push(''); } if (extraFiles.length > 0) { lines.push(`Extra files (found in folder but not listed in config):`); for (const file of extraFiles) lines.push(` • ${chalk.yellow(file)}`); lines.push(''); } lines.push( `Tip: ensure every file in ${yellowDir} matches the keys listed under ${chalk.yellow( configKey, )} in ${yellowConfigFile} (case sensitive).`, ); return lines.join('\n'); }; const formatCodeFileMismatch = (params: { missingFiles: string[]; extraFiles: string[] }): string => { const { missingFiles, extraFiles } = params; const codeDir = `${TopLevelDirectoryName.Code}/`; const lines: string[] = [ 'Code file mismatch', `Source: ${chalk.yellow(TopLevelFileName.RootConfig)} ↔ ${chalk.yellow(codeDir)}`, '', ]; if (missingFiles.length > 0) { lines.push(`Missing files (listed in config but not found in ${chalk.yellow(codeDir)}):`); for (const file of missingFiles) lines.push(` • ${chalk.yellow(file)}`); lines.push(''); } if (extraFiles.length > 0) { lines.push(`Extra files (found in folder but not listed in config):`); for (const file of extraFiles) lines.push(` • ${chalk.yellow(file)}`); lines.push(''); } lines.push( `Tip: ensure every file in ${chalk.yellow(codeDir)} matches the entries in ${chalk.yellow( 'codeFileOrder', )} in ${chalk.yellow(TopLevelFileName.RootConfig)} (case sensitive).`, ); return lines.join('\n'); }; export const checkValidProductModuleDirectory = (params: { productModuleConfig: RootConfigWrite }) => { const { productModuleConfig } = params; const isTypeScript = !!productModuleConfig.settings.typescriptProductModuleCode; // Force main.js to be the first file within file structure (JS projects only). if (!isTypeScript && productModuleConfig.codeFileOrder[0] !== 'main.js') { throw new Error( [ 'Invalid code file order', `File: ${chalk.yellow(TopLevelFileName.RootConfig)}`, '', `Tip: the first entry in ${chalk.yellow('codeFileOrder')} must be ${chalk.yellow('"main.js"')}.`, ].join('\n'), ); } // Ensure TypeScript entry point exists. if (isTypeScript) { const entryPoint = path.join(TopLevelDirectoryName.Code, CodeDirectoryName.Src, 'index.ts'); if (!fs.existsSync(entryPoint)) { throw new Error( [ 'TypeScript entry file not found', `File: ${chalk.yellow(entryPoint)}`, '', `Tip: create ${chalk.yellow(entryPoint)} or disable ${chalk.yellow( 'typescriptProductModuleCode', )} in ${chalk.yellow(TopLevelFileName.RootConfig)}.`, ].join('\n'), ); } } if (!fs.existsSync(alterationHooksFilePath)) { // If it doesnt exist, create the folder. // This solves backwards compatibility fs.mkdirSync(alterationHooksFilePath); } const actualAlterationHookFileNames = [...fs.readdirSync(alterationHooksFilePath).filter((fn) => fn !== '.DS_Store')]; const configAlterationHookKeys = (productModuleConfig.alterationHooks || []).map( (alterationHook: any) => alterationHook.key, ); const actualAlterationHookFileNamesThatDontExist = actualAlterationHookFileNames.filter( (filename) => !configAlterationHookKeys.some( (configAlterationHookKey: string) => configAlterationHookKey === filename.replace('.json', ''), ), ); const actualConfigAlterationHookKeysThatDontExist: string[] = configAlterationHookKeys.filter( (configAlterationHookKey: string) => !actualAlterationHookFileNames.some((fileName) => fileName.replace('.json', '') === configAlterationHookKey), ); if (actualAlterationHookFileNamesThatDontExist.length > 0 || actualConfigAlterationHookKeysThatDontExist.length > 0) { throw new Error( formatHookMismatch({ header: 'Alteration hook mismatch', hooksDirPath: alterationHooksFilePath, configKey: 'alterationHooks', missingFiles: actualConfigAlterationHookKeysThatDontExist, extraFiles: actualAlterationHookFileNamesThatDontExist, }), ); } if (!fs.existsSync(applicationAlterationHooksFilePath)) { // If it doesnt exist, create the folder. // This solves backwards compatibility fs.mkdirSync(applicationAlterationHooksFilePath); } const actualApplicationAlterationHookFileNames = [ ...fs.readdirSync(applicationAlterationHooksFilePath).filter((fn) => fn !== '.DS_Store'), ]; const configApplicationAlterationHookKeys = (productModuleConfig.applicationAlterationHooks || []).map( (alterationHook: any) => alterationHook.key, ); const actualApplicationAlterationHookFileNamesThatDontExist = actualApplicationAlterationHookFileNames.filter( (filename) => !configApplicationAlterationHookKeys.some( (configAlterationHookKey: string) => configAlterationHookKey === filename.replace('.json', ''), ), ); const actualConfigApplicationAlterationHookKeysThatDontExist: string[] = configApplicationAlterationHookKeys.filter( (configAlterationHookKey: string) => !actualApplicationAlterationHookFileNames.some( (fileName) => fileName.replace('.json', '') === configAlterationHookKey, ), ); if ( actualApplicationAlterationHookFileNamesThatDontExist.length > 0 || actualConfigApplicationAlterationHookKeysThatDontExist.length > 0 ) { throw new Error( formatHookMismatch({ header: 'Application alteration hook mismatch', hooksDirPath: applicationAlterationHooksFilePath, configKey: 'applicationAlterationHooks', missingFiles: actualConfigApplicationAlterationHookKeysThatDontExist, extraFiles: actualApplicationAlterationHookFileNamesThatDontExist, }), ); } if (!fs.existsSync(memberAlterationHooksFilePath)) { fs.mkdirSync(memberAlterationHooksFilePath); } const actualMemberAlterationHookFileNames = [ ...fs.readdirSync(memberAlterationHooksFilePath).filter((fn) => fn !== '.DS_Store'), ]; const configMemberAlterationHookKeys = (productModuleConfig.memberAlterationHooks || []).map( (alterationHook: any) => alterationHook.key, ); const actualMemberAlterationHookFileNamesThatDontExist = actualMemberAlterationHookFileNames.filter( (filename) => !configMemberAlterationHookKeys.some( (configAlterationHookKey: string) => configAlterationHookKey === filename.replace('.json', ''), ), ); const actualConfigMemberAlterationHookKeysThatDontExist: string[] = configMemberAlterationHookKeys.filter( (configAlterationHookKey: string) => !actualMemberAlterationHookFileNames.some( (fileName) => fileName.replace('.json', '') === configAlterationHookKey, ), ); if ( actualMemberAlterationHookFileNamesThatDontExist.length > 0 || actualConfigMemberAlterationHookKeysThatDontExist.length > 0 ) { throw new Error( formatHookMismatch({ header: 'Member alteration hook mismatch', hooksDirPath: memberAlterationHooksFilePath, configKey: 'memberAlterationHooks', missingFiles: actualConfigMemberAlterationHookKeysThatDontExist, extraFiles: actualMemberAlterationHookFileNamesThatDontExist, }), ); } // Check that all code files listed in config matches files in ./code directory (JS projects only) if (!isTypeScript) { const listedFileNames = [...productModuleConfig.codeFileOrder, CodeDirectoryName.UnitTests]; // Pass with tests or without const actualFileNames = [ ...fs.readdirSync(TopLevelDirectoryName.Code).filter((fn) => fn.slice(-3) === '.js'), CodeDirectoryName.UnitTests, ]; const listedFilesThatDontExist = listedFileNames.filter((fn1) => !actualFileNames.includes(fn1)); const actualFilesThatAreNotListed = actualFileNames.filter((fn1) => !listedFileNames.includes(fn1)); if (listedFilesThatDontExist.length > 0 || actualFilesThatAreNotListed.length > 0) { throw new Error( formatCodeFileMismatch({ missingFiles: listedFilesThatDontExist, extraFiles: actualFilesThatAreNotListed, }), ); } } };