@supernovaio/cli
Version:
Supernova.io Command Line Interface
166 lines (164 loc) • 7.34 kB
JavaScript
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e8f5272f-9787-5175-951f-23267b3cd5e9")}catch(e){}}();
import { action } from "@oclif/core/ux";
import * as fs from "node:fs/promises";
import path from "node:path";
import { spawnAndWait } from "./spawn-and-wait.js";
export async function fileExists(p) {
try {
await fs.access(p);
return true;
}
catch {
return false;
}
}
const FRAMEWORK_CONFIGS = {
angular: {
mainFileExt: ".ts",
appEntryCandidates: ["src/app.component.ts", "src/App.component.ts"],
generateAppContent: (templateId) => {
const componentClassName = `${templateId}Component`;
const selectorName = camelToKebab(templateId);
return `import { Component } from "@angular/core";\nimport { ${componentClassName} } from "./components/${templateId}/${templateId}";\nimport { SnPrototypeComponent } from "../supernova/helpers/sn-prototype.component";\n\n@Component({\n selector: "app-root",\n standalone: true,\n imports: [SnPrototypeComponent, ${componentClassName}],\n template: \`<sn-prototype><${selectorName}></${selectorName}></sn-prototype>\`,\n})\nexport class AppComponent {}\n`;
},
},
react: {
mainFileExt: ".tsx",
appEntryCandidates: ["src/App.tsx", "src/app.tsx"],
generateAppContent: (templateId) => `import { ${templateId} } from "./components/${templateId}/${templateId}"\n\nexport default function App() {\n return <${templateId} />\n}`,
},
};
const FRAMEWORK_DETECTION_ORDER = ["angular", "react"];
async function detectFrameworkAndEntryPath() {
for (const framework of FRAMEWORK_DETECTION_ORDER) {
const config = FRAMEWORK_CONFIGS[framework];
for (const candidate of config.appEntryCandidates) {
const resolved = path.resolve(candidate);
if (await fileExists(resolved))
return { config, appEntryPath: resolved };
}
}
const config = FRAMEWORK_CONFIGS.react;
return { config, appEntryPath: path.resolve(config.appEntryCandidates[0]) };
}
function camelToKebab(str) {
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
}
export async function validateTemplates(templates, logger, debug) {
action.start(`🔍 Validating ${Object.keys(templates).length} template(s)`);
const { config } = await detectFrameworkAndEntryPath();
for (const [templateKey, template] of Object.entries(templates)) {
action.start(`Validating template '${templateKey}'`);
if (!template.files || template.files.length === 0) {
action.stop("no files specified");
logger.error(`Template '${templateKey}': No files specified`);
}
const expectedMainFile = `supernova/templates/${templateKey}/${templateKey}${config.mainFileExt}`;
if (!template.files.includes(expectedMainFile)) {
action.stop("main file missing");
logger.error(`Template '${templateKey}': Expected main file '${expectedMainFile}' not found in files array`);
}
for (const filePath of template.files) {
if (!(await fileExists(path.resolve(filePath)))) {
action.stop("files missing");
logger.error(`Template '${templateKey}': File does not exist: ${filePath}`);
}
}
try {
await testTemplateBuild(templateKey, template.files, logger, debug);
action.stop("build successful");
}
catch (error) {
action.stop("build failed");
logger.error(`Template '${templateKey}': Build test failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
action.stop("validation complete");
}
async function testTemplateBuild(templateKey, files, logger, debug) {
const { config, appEntryPath } = await detectFrameworkAndEntryPath();
const componentsDir = path.resolve("src/components");
const toDestPath = (filePath) => path.join(componentsDir, filePath.replace(/^supernova\/(patterns|templates)\//, ""));
let originalAppContent = "";
let copiedFiles = [];
let failed = false;
try {
await checkNoDirsExist(files.map(f => path.dirname(toDestPath(f))));
copiedFiles = await copyFiles(files, toDestPath);
originalAppContent = await patchAppEntry(appEntryPath, templateKey, config);
await runBuild(logger);
}
catch (error) {
failed = true;
throw error;
}
finally {
const keepFilesForDebugging = debug && failed;
if (!keepFilesForDebugging) {
await cleanup(copiedFiles, appEntryPath, originalAppContent);
}
}
}
async function checkNoDirsExist(dirs) {
for (const dir of new Set(dirs)) {
if (await fileExists(dir)) {
throw new Error(`Component directory already exists: ${path.relative(process.cwd(), dir)}. Please remove or rename this directory before testing the template.`);
}
}
}
async function copyFiles(files, toDestPath) {
const copiedFiles = [];
for (const filePath of files) {
const destPath = toDestPath(filePath);
await fs.mkdir(path.dirname(destPath), { recursive: true });
await fs.copyFile(path.resolve(filePath), destPath);
copiedFiles.push(destPath);
if (destPath.endsWith(".tsx") || destPath.endsWith(".ts")) {
await updateImportsInFile(destPath);
}
}
return copiedFiles;
}
async function patchAppEntry(appEntryPath, templateKey, config) {
if (!(await fileExists(appEntryPath)))
return "";
const originalContent = await fs.readFile(appEntryPath, "utf8");
await fs.writeFile(appEntryPath, config.generateAppContent(templateKey));
return originalContent;
}
async function runBuild(logger) {
try {
await spawnAndWait("npm", ["run", "build"], { cwd: process.cwd() });
}
catch (error) {
logger.log(`Build failed with error: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
async function cleanup(copiedFiles, appTsxPath, originalAppContent) {
for (const filePath of copiedFiles) {
if (await fileExists(filePath)) {
await fs.rm(filePath, { force: true });
}
}
const sortedDirs = [...new Set(copiedFiles.map(f => path.dirname(f)))].sort((a, b) => b.length - a.length);
for (const dir of sortedDirs) {
try {
await fs.rm(dir, { recursive: true, force: true });
}
catch {
}
}
if (originalAppContent && (await fileExists(appTsxPath))) {
await fs.writeFile(appTsxPath, originalAppContent);
}
}
async function updateImportsInFile(filePath) {
const content = await fs.readFile(filePath, "utf8");
const updatedContent = content.replaceAll(/from\s+["']\.\.\/\.\.\/(patterns|templates)\/([^/"']+\/[^"']+)["']/g, 'from "../$2"');
if (content !== updatedContent) {
await fs.writeFile(filePath, updatedContent);
}
}
//# sourceMappingURL=validate-templates.js.map
//# debugId=e8f5272f-9787-5175-951f-23267b3cd5e9