import fs from 'fs';
import path from 'path';
import { parseName, readTemplate, abortIfGeneratedPathExists, fail } from '../utils.js';
import { yellow, cyan, green, gray } from '../colors.js';
/**
* Checks if test generation is enabled via CLI flag or avenx config.
* @param {object} cli
* @param {boolean} [withTestFlag]
* @param {boolean} [noTestFlag]
* @returns {boolean}
*/
function shouldGenerateTest(cli, withTestFlag, noTestFlag) {
if (noTestFlag) return false;
if (withTestFlag) return true;
if (cli?.config?.generate && typeof cli.config.generate.withTests === 'boolean') {
return cli.config.generate.withTests;
}
return false;
}
/**
* Automatically adds import and registration for a component in src/main.app.js.
* @param {object} cli
* @param {string} className
* @param {string} folderName
*/
export function registerInMainApp(cli, className, folderName) {
const mainPath = path.join(cli.baseDir, cli.config.srcDir, 'main.app.js');
if (!fs.existsSync(mainPath)) return;
const content = fs.readFileSync(mainPath, 'utf-8');
const importStatement = `import ${className} from './components/${folderName}/${folderName}.component.js';`;
const registerStatement = `app.register('${className}', ${className});`;
const lines = content.split('\n');
let lastImportIndex = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('import ')) lastImportIndex = i;
}
if (lastImportIndex !== -1) {
lines.splice(lastImportIndex + 1, 0, importStatement);
} else {
lines.unshift(importStatement);
}
let lastRegisterIndex = -1;
let appInstanceIndex = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('app.register(')) lastRegisterIndex = i;
if (lines[i].includes('new AvenxApp')) appInstanceIndex = i;
}
if (lastRegisterIndex !== -1) {
lines.splice(lastRegisterIndex + 1, 0, registerStatement);
} else if (appInstanceIndex !== -1) {
lines.splice(appInstanceIndex + 1, 0, '', registerStatement);
} else {
lines.push('', registerStatement);
}
const hasMount = lines.some((line) => line.includes('app.mount('));
if (!hasMount) {
lines.push(`\napp.mount('${className}');`);
} else {
lines.push(`// app.mount('${className}'); // Uncomment to mount this component`);
}
fs.writeFileSync(mainPath, lines.join('\n'));
console.log(green(`✅ Component '${className}' registered in ${cli.config.srcDir}/main.app.js`));
}
/**
* Generates a new Bridge class and template file.
* @param {object} cli
* @param {string} name
* @param {boolean|object} [dryRun]
* @param {boolean} [force]
* @param {string|null} [templateName]
*/
export function generateBridge(cli, name, dryRun = false, force = false, templateName = null) {
if (typeof dryRun === 'object' && dryRun !== null) {
force = dryRun.force || false;
templateName = dryRun.templateName || null;
dryRun = dryRun.dryRun || false;
}
if (!name) {
fail('Please provide a bridge name (e.g., avenx g bridge auth)');
return;
}
const { folderFileName: lowerName } = parseName(name);
// A bridge is identified by its module, so its name is simply the file name
// in camelCase — the same identifier you would import it under.
const bridgeName = lowerName
.split(/[-_.]/)
.filter(Boolean)
.map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
.join('');
const globalDir = path.join(cli.baseDir, cli.config.srcDir, 'global');
const bridgePath = path.join(globalDir, `${lowerName}.bridge.js`);
if (!force && abortIfGeneratedPathExists(cli.baseDir, 'Bridge', lowerName, [bridgePath])) {
return;
}
if (force && fs.existsSync(bridgePath)) {
console.warn(yellow(`⚠️ Force enabled: overwriting existing Bridge '${lowerName}'.`));
}
if (dryRun) {
console.log(
cyan(`🧪 [Dry Run] Bridge '${bridgeName}' would be created at ${cli.config.srcDir}/global/${lowerName}.bridge.js`),
);
console.log(cyan('🧪 [Dry Run] No files were written.'));
return;
}
if (!fs.existsSync(globalDir)) {
fs.mkdirSync(globalDir, { recursive: true });
}
const template = readTemplate(cli.baseDir, cli.config, cli.frameworkDir, 'bridge', 'bridge.js.template', templateName);
fs.writeFileSync(
bridgePath,
template.replace(/{{ name }}/g, bridgeName).replace(/{{ file }}/g, lowerName),
);
console.log(green(`✅ Bridge '${bridgeName}' generated at ${cli.config.srcDir}/global/${lowerName}.bridge.js`));
console.log(
gray(`ℹ️ Import it where you need it: import ${bridgeName} from '<path>/global/${lowerName}.bridge.js';`),
);
}
/**
* Generates a new Guard class and template file.
* @param {object} cli
* @param {string} name
* @param {boolean|object} [dryRun]
* @param {boolean} [force]
* @param {string|null} [templateName]
*/
export function generateGuard(cli, name, dryRun = false, force = false, templateName = null) {
if (typeof dryRun === 'object' && dryRun !== null) {
force = dryRun.force || false;
templateName = dryRun.templateName || null;
dryRun = dryRun.dryRun || false;
}
if (!name) {
fail('Please provide a guard name (e.g., avenx g guard auth)');
return;
}
const { capitalizedName: baseName, folderFileName: lowerName } = parseName(name);
const capitalizedName = baseName + 'Guard';
const guardDir = path.join(cli.baseDir, cli.config.srcDir, 'guards');
const guardPath = path.join(guardDir, `${lowerName}.guard.js`);
if (!force && abortIfGeneratedPathExists(cli.baseDir, 'Guard', lowerName, [guardPath])) {
return;
}
if (force && fs.existsSync(guardPath)) {
console.warn(yellow(`⚠️ Force enabled: overwriting existing Guard '${lowerName}'.`));
}
if (dryRun) {
console.log(
cyan(`🧪 [Dry Run] Guard '${capitalizedName}' would be created at ${cli.config.srcDir}/guards/${lowerName}.guard.js`),
);
console.log(cyan('🧪 [Dry Run] No files were written.'));
return;
}
if (!fs.existsSync(guardDir)) {
fs.mkdirSync(guardDir, { recursive: true });
}
const template = readTemplate(cli.baseDir, cli.config, cli.frameworkDir, 'guard', 'guard.js.template', templateName);
fs.writeFileSync(guardPath, template.replace(/{{ name }}/g, capitalizedName));
console.log(green(`✅ Guard '${capitalizedName}' generated at ${cli.config.srcDir}/guards/${lowerName}.guard.js`));
console.log(gray(`ℹ️ It can be used in your route configurations.`));
}
/**
* Generates a new Page class and template files.
* @param {object} cli
* @param {string} name
* @param {boolean|object} [dryRun]
* @param {boolean} [force]
* @param {string|null} [templateName]
*/
export function generatePage(cli, name, dryRun = false, force = false, templateName = null) {
if (typeof dryRun === 'object' && dryRun !== null) {
force = dryRun.force || false;
templateName = dryRun.templateName || null;
dryRun = dryRun.dryRun || false;
}
if (!name) {
fail('Please provide a page name (e.g., avenx g page home)');
return;
}
const { capitalizedName, folderFileName: lowerName } = parseName(name);
const pageDir = path.join(cli.baseDir, cli.config.srcDir, 'pages');
const jsPath = path.join(pageDir, `${lowerName}.page.js`);
const cssPath = path.join(pageDir, `${lowerName}.page.css`);
if (!force && abortIfGeneratedPathExists(cli.baseDir, 'Page', lowerName, [jsPath, cssPath])) {
return;
}
if (force && (fs.existsSync(jsPath) || fs.existsSync(cssPath))) {
console.warn(yellow(`⚠️ Force enabled: overwriting existing Page '${lowerName}'.`));
}
if (dryRun) {
console.log(cyan(`🧪 [Dry Run] Page '${capitalizedName}' would be created at:`));
console.log(` ${cli.config.srcDir}/pages/${lowerName}.page.js`);
console.log(` ${cli.config.srcDir}/pages/${lowerName}.page.css`);
console.log(cyan('🧪 [Dry Run] No files were written.'));
return;
}
if (!fs.existsSync(pageDir)) {
fs.mkdirSync(pageDir, { recursive: true });
}
const jsTemplate = readTemplate(cli.baseDir, cli.config, cli.frameworkDir, 'page', 'page.js.template', templateName);
const cssTemplate = readTemplate(cli.baseDir, cli.config, cli.frameworkDir, 'page', 'page.css.template', templateName);
fs.writeFileSync(jsPath, jsTemplate.replace(/{{ name }}/g, capitalizedName));
fs.writeFileSync(cssPath, cssTemplate);
console.log(green(`✅ Page '${capitalizedName}' generated at ${cli.config.srcDir}/pages/${lowerName}.page.js`));
console.log(gray(`ℹ️ It will be automatically registered and routed if you update src/main.app.js.`));
}
/**
* Generates a new component folder and template files, and registers it in main.app.js.
* @param {object} cli
* @param {string} name
* @param {boolean|object} [dryRun]
* @param {boolean} [force]
* @param {string|null} [templateName]
* @param {boolean} [withTest]
* @param {boolean} [noTest]
*/
export function generateComponent(
cli,
name,
dryRun = false,
force = false,
templateName = null,
withTest = false,
noTest = false,
) {
if (typeof dryRun === 'object' && dryRun !== null) {
force = dryRun.force || false;
templateName = dryRun.templateName || null;
withTest = dryRun.withTest || false;
noTest = dryRun.noTest || false;
dryRun = dryRun.dryRun || false;
} else if (typeof force === 'boolean' && typeof templateName === 'boolean') {
// Backward compatibility if called as (cli, name, dryRun, withTest, noTest)
noTest = templateName;
withTest = force;
force = false;
templateName = null;
}
if (!name) {
fail('Please provide a component name (e.g., avenx g my-component)');
return;
}
const { capitalizedName, folderFileName: lowerName } = parseName(name);
const compDir = path.join(cli.baseDir, cli.config.srcDir, 'components', lowerName);
const willGenerateTest = shouldGenerateTest(cli, withTest, noTest);
const jsPath = path.join(compDir, `${lowerName}.component.js`);
const cssPath = path.join(compDir, `${lowerName}.component.css`);
const testPath = path.join(compDir, `${lowerName}.component.test.js`);
const pathsToCheck = [compDir];
if (!force && abortIfGeneratedPathExists(cli.baseDir, 'Component', lowerName, pathsToCheck)) {
return;
}
if (force && fs.existsSync(compDir)) {
console.warn(yellow(`⚠️ Force enabled: overwriting existing Component '${lowerName}'.`));
}
if (dryRun) {
console.log(cyan(`🧪 [Dry Run] Component '${lowerName}' would be created at:`));
console.log(` ${cli.config.srcDir}/components/${lowerName}/${lowerName}.component.js`);
console.log(` ${cli.config.srcDir}/components/${lowerName}/${lowerName}.component.css`);
if (willGenerateTest) {
console.log(` ${cli.config.srcDir}/components/${lowerName}/${lowerName}.component.test.js`);
}
console.log(cyan(`🧪 [Dry Run] ${cli.config.srcDir}/main.app.js would be updated with:`));
console.log(` import ${capitalizedName} from './components/${lowerName}/${lowerName}.component.js';`);
console.log(` app.register('${capitalizedName}', ${capitalizedName});`);
console.log(cyan('🧪 [Dry Run] No files were written.'));
return;
}
fs.mkdirSync(compDir, { recursive: true });
const jsTemplate = readTemplate(cli.baseDir, cli.config, cli.frameworkDir, 'component', 'component.js.template', templateName);
const cssTemplate = readTemplate(cli.baseDir, cli.config, cli.frameworkDir, 'component', 'component.css.template', templateName);
fs.writeFileSync(
jsPath,
jsTemplate.replace(/{{ name }}/g, capitalizedName),
);
fs.writeFileSync(cssPath, cssTemplate);
if (willGenerateTest) {
const testTemplate = readTemplate(cli.baseDir, cli.config, cli.frameworkDir, 'component', 'component.test.js.template', templateName);
fs.writeFileSync(
testPath,
testTemplate
.replace(/{{PascalCaseName}}/g, capitalizedName)
.replace(/{{name}}/g, lowerName),
);
}
console.log(green(`✅ Component '${lowerName}' generated at ${cli.config.srcDir}/components/${lowerName}/`));
registerInMainApp(cli, capitalizedName, lowerName);
}