dev-add
Version:
A CLI tool to scaffold components or utility files with optional templates and automated index exports.
130 lines (128 loc) • 4.42 kB
JavaScript
import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
function parseFileInput(input) {
const match = input.match(/^(.*?)\/\[(.*)\](\.tsx)?$/);
if (match) {
const basePath = match[1];
const items = match[2].split(",").map((s) => s.trim());
const ext = match[3] || "";
return items.map((name) => `${basePath}/${name}${ext}`);
} else {
return [input];
}
}
async function ensureIndexFile(folderPath) {
const indexPath = path.join(folderPath, "index.js");
if (!(await fs.pathExists(indexPath))) {
await fs.writeFile(indexPath, "", "utf8");
}
return indexPath;
}
async function rebuildIndex(indexPath) {
const folder = path.dirname(indexPath);
const files = await fs.readdir(folder);
const componentFiles = files
.filter((f) => f !== "index.js")
.filter((f) => /\.(tsx|ts|js)$/.test(f));
const imports = componentFiles.map((f) => {
const name = path.basename(f, path.extname(f));
return `import ${name} from './${name}';`;
});
const exports = `export { ${componentFiles
.map((f) => path.basename(f, path.extname(f)))
.join(", ")} };`;
const content = `${imports.join("\n")}\n\n${exports}\n`;
await fs.writeFile(indexPath, content, "utf8");
}
function findPackageJson(startDir) {
let dir = startDir;
while (dir !== path.parse(dir).root) {
if (fs.existsSync(path.join(dir, "package.json"))) {
return true;
}
dir = path.dirname(dir);
}
return false;
}
function resolveAliasPath(inputPath) {
const cwd = process.cwd();
const tsconfigPath = path.join(cwd, "tsconfig.json");
const jsconfigPath = path.join(cwd, "jsconfig.json");
let configPath = null;
if (fs.existsSync(tsconfigPath)) {
configPath = tsconfigPath;
} else if (fs.existsSync(jsconfigPath)) {
configPath = jsconfigPath;
}
if (!configPath) {
return inputPath;
}
const config = fs.readJsonSync(configPath);
const paths = config.compilerOptions?.paths || config.paths || {};
for (const alias in paths) {
const cleanAlias = alias.replace("/*", "");
const targetPaths = paths[alias];
if (inputPath.startsWith(`${cleanAlias}/`)) {
const target = targetPaths[0].replace("/*", "");
const resolved = inputPath.replace(`${cleanAlias}/`, target + "/");
return resolved;
}
}
return inputPath;
}
function getTemplateContent(name, filename, options) {
const baseName = path.basename(filename, path.extname(filename));
if (options.named) {
return `export const ${baseName} = () => {\n return null;\n};\n`;
}
if (name === "react-component") {
return `const ${capitalize(baseName)} = () => {\n return <div>${capitalize(
baseName
)}</div>;\n};\n\nexport default ${capitalize(baseName)};\n`;
}
return `// ${filename}`;
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export async function addFiles(inputPath, options = {}) {
const cwd = process.cwd();
if (!findPackageJson(cwd)) {
console.log(
chalk.red(
"✖ Error: package.json not found. Make sure you are inside a Node.js project."
)
);
process.exit(1);
}
const resolvedPath = resolveAliasPath(inputPath);
const files = parseFileInput(
resolvedPath,
options.ext ? `.${options.ext}` : ".tsx"
);
for (const file of files) {
const fullPath = path.resolve(process.cwd(), file);
const folder = path.dirname(fullPath);
const filename = path.basename(fullPath);
const content = getTemplateContent(options.template, filename, options);
if (!(await fs.pathExists(fullPath))) {
await fs.ensureDir(folder);
if (options.dry) {
console.log(chalk.gray(`[dry] Would create: ${file}`));
} else {
await fs.writeFile(fullPath, content, "utf8");
}
console.log(chalk.green(`✔ Created: ${file}`));
} else {
console.log(chalk.yellow(`⚠ Already exists: ${file}`));
}
if (!options.nocomp) {
const indexPath = await ensureIndexFile(folder);
await rebuildIndex(indexPath);
console.log(chalk.cyan(`↪ Updated: ${path.relative(process.cwd(), indexPath)}`));
} else if (options.verbose || 1) {
console.log(chalk.gray(`⏭ Skipped index.js update for ${folder} (due to --nocomp)`));
}
}
}