@yanhe-su/cli
Version:
CLI tool for DAO Style projects - providing project scaffolding, template generation and dependency management
233 lines (232 loc) • 7.92 kB
JavaScript
import * as path from 'path';
import ejs from 'ejs';
import fs from 'fs-extra';
export function createTemplateData(data) {
return {
...data,
helpers: {
raw: (options) => options.fn(),
capitalize: (str) => str.charAt(0).toUpperCase() + str.slice(1),
formatDate: (date) => date.toLocaleDateString(),
},
};
}
// 根据文件扩展名判断是否需要模板渲染
function shouldRenderTemplate(filePath) {
const ext = path.extname(filePath);
return (ext === '.ejs' ||
ext === '.html' ||
ext === '.json' ||
ext === '.js' ||
ext === '.ts');
}
// 新增函数:处理 package.json
function processPackageJson(content, data) {
try {
const pkg = JSON.parse(content);
// 合并来自 data 的配置
if (data.scripts) {
pkg.scripts = { ...pkg.scripts, ...data.scripts };
}
if (data.dependencies) {
pkg.dependencies = { ...pkg.dependencies, ...data.dependencies };
}
if (data.devDependencies) {
pkg.devDependencies = { ...pkg.devDependencies, ...data.devDependencies };
}
if (data.gitHooks) {
pkg.gitHooks = { ...pkg.gitHooks, ...data.gitHooks };
}
if (data['lint-staged']) {
pkg['lint-staged'] = { ...pkg['lint-staged'], ...data['lint-staged'] };
}
if (data.config) {
pkg.config = { ...pkg.config, ...data.config };
}
// 对依赖进行排序
if (pkg.dependencies) {
pkg.dependencies = Object.keys(pkg.dependencies)
.sort()
.reduce((acc, key) => {
const value = pkg.dependencies?.[key];
if (value)
acc[key] = value;
return acc;
}, {});
}
if (pkg.devDependencies) {
pkg.devDependencies = Object.keys(pkg.devDependencies)
.sort()
.reduce((acc, key) => {
const value = pkg.devDependencies?.[key];
if (value)
acc[key] = value;
return acc;
}, {});
}
// 对 scripts 进行排序
if (pkg.scripts) {
pkg.scripts = Object.keys(pkg.scripts)
.sort()
.reduce((acc, key) => {
const value = pkg.scripts?.[key];
if (value)
acc[key] = value;
return acc;
}, {});
}
// 对配置类字段进行排序
if (pkg.config) {
pkg.config = Object.keys(pkg.config)
.sort()
.reduce((acc, key) => {
const value = pkg.config?.[key];
if (value !== undefined)
acc[key] = value;
return acc;
}, {});
}
if (pkg.gitHooks) {
pkg.gitHooks = Object.keys(pkg.gitHooks)
.sort()
.reduce((acc, key) => {
const value = pkg.gitHooks?.[key];
if (value)
acc[key] = value;
return acc;
}, {});
}
if (pkg['lint-staged']) {
pkg['lint-staged'] = Object.keys(pkg['lint-staged'])
.sort()
.reduce((acc, key) => {
const value = pkg['lint-staged']?.[key];
if (value)
acc[key] = value;
return acc;
}, {});
}
// 对 keywords 进行排序
if (pkg.keywords) {
pkg.keywords.sort();
}
// 对其他可能的配置字段进行排序
const configFields = [
'peerDependencies',
'optionalDependencies',
'resolutions',
'overrides',
];
for (const field of configFields) {
if (pkg[field]) {
pkg[field] = Object.keys(pkg[field])
.sort()
.reduce((acc, key) => {
acc[key] = pkg[field][key];
return acc;
}, {});
}
}
return JSON.stringify(pkg, null, 2);
}
catch (error) {
console.error('Error processing package.json:', error);
return content;
}
}
export async function renderTemplate(content, data, templateDir, filePath) {
if (!shouldRenderTemplate(filePath)) {
return content;
}
try {
const rendered = await ejs.render(content, createTemplateData(data), {
filename: filePath,
async: true,
});
// 如果是 package.json 文件,进行特殊处理
if (path.basename(filePath) === 'package.json') {
return processPackageJson(rendered, data);
}
return rendered;
}
catch (error) {
console.error(`Error rendering template ${filePath}:`, error);
throw error;
}
}
// 判断是否是二进制文件或系统文件
function shouldSkipFile(filename) {
const skipFiles = [
'.DS_Store',
'Thumbs.db',
'.git',
'.svn',
'.hg',
'.idea',
'.vscode',
'node_modules',
];
return (skipFiles.includes(filename) ||
/^\._/.test(filename) || // 跳过以 ._ 开头的文件
/^\./.test(filename)); // 跳过所有隐藏文件
}
// 处理文件名中的模板变量
function processFileName(fileName, data) {
let processedName = fileName;
// 处理 _file.ext 形式的文件名
if (processedName.startsWith('_')) {
processedName = `.${processedName.slice(1)}`;
}
try {
// 使用 ejs 处理文件名中的模板变量
return ejs.render(processedName.replace(/_([^_]+)_/g, '<%= $1 %>'), data);
}
catch (error) {
console.error(`Error processing filename ${fileName}:`, error);
return processedName;
}
}
export async function renderDirectory(sourcePath, targetPath, data) {
try {
await fs.ensureDir(targetPath);
const files = await fs.readdir(sourcePath);
for (const file of files) {
// 跳过系统文件和二进制文件
if (shouldSkipFile(file)) {
continue;
}
const sourceFilePath = path.join(sourcePath, file);
// 处理文件名中的模板变量
const processedFileName = processFileName(file, data);
const targetFilePath = path.join(targetPath, processedFileName);
const stats = await fs.stat(sourceFilePath);
if (stats.isDirectory()) {
// 处理目录名中的模板变量
const processedDirName = processFileName(file, data);
const targetDirPath = path.join(targetPath, processedDirName);
await renderDirectory(sourceFilePath, targetDirPath, data);
}
else {
try {
const content = await fs.readFile(sourceFilePath, 'utf-8');
const rendered = await renderTemplate(content, data, path.dirname(sourceFilePath), sourceFilePath);
await fs.writeFile(targetFilePath, rendered);
}
catch (error) {
// 如果文件无法作为文本读取,直接复制
if (error instanceof Error &&
error.message.includes('Invalid or incomplete UTF-8')) {
await fs.copy(sourceFilePath, targetFilePath);
}
else {
throw error;
}
}
}
}
}
catch (error) {
console.error('Error in renderDirectory:', error);
throw error;
}
}