browser-plugin-creator
Version:
A modern scaffolding tool for creating browser extensions with ease
322 lines (295 loc) • 10.2 kB
JavaScript
import path from 'path';
import fs from 'fs-extra';
import Handlebars from 'handlebars';
export class TemplateRenderer {
constructor(config) {
this.config = config;
this.registerHelpers();
}
registerHelpers() {
// 大写转换助手
Handlebars.registerHelper('uppercase', (str) => str.toUpperCase());
// 小写转换助手
Handlebars.registerHelper('lowercase', (str) => str.toLowerCase());
// 驼峰命名转换
Handlebars.registerHelper('camelCase', (str) => {
return str.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : '');
});
// 帕斯卡命名转换
Handlebars.registerHelper('pascalCase', (str) => {
return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
return index === 0 ? word.toLowerCase() : word.toUpperCase();
}).replace(/\s+/g, '');
});
// kebab-case转换
Handlebars.registerHelper('kebabCase', (str) => {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
});
// 条件判断助手
Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) {
switch (operator) {
case '==':
return (v1 == v2) ? options.fn(this) : options.inverse(this);
case '===':
return (v1 === v2) ? options.fn(this) : options.inverse(this);
case '!=':
return (v1 != v2) ? options.fn(this) : options.inverse(this);
case '!==':
return (v1 !== v2) ? options.fn(this) : options.inverse(this);
case '<':
return (v1 < v2) ? options.fn(this) : options.inverse(this);
case '<=':
return (v1 <= v2) ? options.fn(this) : options.inverse(this);
case '>':
return (v1 > v2) ? options.fn(this) : options.inverse(this);
case '>=':
return (v1 >= v2) ? options.fn(this) : options.inverse(this);
case '&&':
return (v1 && v2) ? options.fn(this) : options.inverse(this);
case '||':
return (v1 || v2) ? options.fn(this) : options.inverse(this);
default:
return options.inverse(this);
}
});
// 数组包含判断
Handlebars.registerHelper('includes', function (array, value, options) {
return array && array.includes(value) ? options.fn(this) : options.inverse(this);
});
// 数组长度
Handlebars.registerHelper('length', function (array) {
return array ? array.length : 0;
});
}
async renderTemplate(templatePath, outputPath, context) {
try {
const content = await fs.readFile(templatePath, 'utf8');
const template = Handlebars.compile(content);
const rendered = template(context);
await fs.ensureDir(path.dirname(outputPath));
await fs.writeFile(outputPath, rendered);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`渲染模板失败: ${templatePath} -> ${errorMessage}`);
}
}
async copyTemplate(templateName, projectPath, options) {
const templatePath = this.config.templatesPath || path.join(__dirname, '../../templates');
const sourcePath = path.join(templatePath, templateName);
if (!await fs.pathExists(sourcePath)) {
throw new Error(`模板不存在: ${templateName}`);
}
const context = {
...options,
year: new Date().getFullYear(),
date: new Date().toISOString().split('T')[0]
};
// 复制模板文件
await this.copyDirectory(sourcePath, projectPath, context);
// 根据选项生成额外文件
await this.generateAdditionalFiles(projectPath, options);
}
async copyDirectory(source, destination, context) {
await fs.ensureDir(destination);
const items = await fs.readdir(source);
for (const item of items) {
const sourcePath = path.join(source, item);
const destPath = path.join(destination, item);
const stat = await fs.stat(sourcePath);
if (stat.isDirectory()) {
await this.copyDirectory(sourcePath, destPath, context);
}
else {
// 处理模板文件
if (item.endsWith('.template')) {
const outputFile = item.replace('.template', '');
const outputPath = path.join(destination, outputFile);
await this.renderTemplate(sourcePath, outputPath, context);
}
else if (this.isTemplateFile(item)) {
await this.renderTemplate(sourcePath, destPath, context);
}
else {
await fs.copy(sourcePath, destPath);
}
}
}
}
isTemplateFile(filename) {
const templateExtensions = ['.html', '.js', '.ts', '.json', '.md', '.css', '.yml', '.yaml'];
return templateExtensions.some(ext => filename.endsWith(ext));
}
async generateAdditionalFiles(projectPath, options) {
// 根据选项生成不同的配置文件
if (options.useTypeScript) {
await this.generateTypeScriptConfig(projectPath, options);
}
if (options.useWebpack) {
await this.generateWebpackConfig(projectPath, options);
}
// 生成.gitignore
await this.generateGitIgnore(projectPath, options);
// 生成README
await this.generateReadme(projectPath, options);
}
async generateTypeScriptConfig(projectPath, options) {
const tsConfig = {
compilerOptions: {
target: 'ES2020',
module: 'ES2020',
moduleResolution: 'node',
strict: true,
esModuleInterop: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
outDir: './dist',
rootDir: './src',
declaration: true,
sourceMap: true
},
include: ['src/**/*'],
exclude: ['node_modules', 'dist']
};
await fs.writeJson(path.join(projectPath, 'tsconfig.json'), tsConfig, { spaces: 2 });
}
async generateWebpackConfig(projectPath, options) {
const webpackConfig = this.getWebpackConfig(options);
await fs.writeFile(path.join(projectPath, 'webpack.config.js'), webpackConfig);
}
getWebpackConfig(options) {
const baseConfig = `const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
entry: {
${this.getEntryPoints(options)}
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
clean: true
},
${options.useTypeScript ? "resolve: { extensions: ['.ts', '.js'] }," : ''}
module: {
rules: [
${options.useTypeScript ? `{
test: /\\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
},` : ''}
{
test: /\\.css$/i,
use: ['style-loader', 'css-loader']
},
{
test: /\\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource'
}
]
},
plugins: [
new CopyPlugin({
patterns: [
{ from: 'manifest.json', to: 'manifest.json' },
{ from: 'icons', to: 'icons', noErrorOnMissing: true }
]
})
],
mode: process.env.NODE_ENV || 'development',
devtool: process.env.NODE_ENV === 'production' ? false : 'source-map'
};`;
return baseConfig;
}
getEntryPoints(options) {
const entries = [];
// 根据模板类型确定入口文件
switch (options.template) {
case 'basic-popup':
entries.push('popup: "./src/popup/popup.js"');
break;
case 'content-script':
entries.push('content: "./src/content.js"', 'popup: "./src/popup/popup.js"');
break;
default:
entries.push('popup: "./src/popup/popup.js"');
}
return entries.join(',\n ');
}
async generateGitIgnore(projectPath, options) {
const gitignoreContent = `# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build outputs
dist/
build/
*.tsbuildinfo
# Runtime data
pids
*.pid
*.seed
# Coverage directory used by tools like istanbul
coverage/
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# dotenv environment variables file
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Editor directories and files
.vscode/
.idea/
*.swp
*.swo
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Extension specific
*.pem
*.key
*.crx
*.xpi
key.pem`;
await fs.writeFile(path.join(projectPath, '.gitignore'), gitignoreContent);
}
async generateReadme(projectPath, options) {
const readmeContent = `# ${options.name}
${options.description}
## 开发
### 安装依赖
\`\`\`bash
${options.packageManager} install
\`\`\`
### 开发模式
\`\`\`bash
${options.packageManager} run dev
\`\`\`
### 构建扩展
\`\`\`bash
${options.packageManager} run build
\`\`\`
### 加载到浏览器
1. 打开 Chrome 浏览器
2. 访问 \`chrome://extensions/\`
3. 开启右上角的"开发者模式"
4. 点击"加载已解压的扩展程序"
5. 选择 \`dist\` 文件夹
## 功能特性
${options.features.map(feature => `- ${feature}`).join('\n')}
## 浏览器支持
${options.browserSupport.map(browser => `- ${browser}`).join('\n')}
## 许可证
MIT License`;
await fs.writeFile(path.join(projectPath, 'README.md'), readmeContent);
}
}