UNPKG

browser-plugin-creator

Version:

A modern scaffolding tool for creating browser extensions with ease

322 lines (295 loc) 10.2 kB
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); } }