/* eslint-disable max-len */ import { MAIN_SCRIPT_ID } from '../script'; import { Plugin } from '../plugin'; import { ContextStore } from '../common'; import { AppConfigData } from '@gedit/app-config/lib/common'; import { escapeHtml } from '../common/utils'; import { minify } from 'terser'; const babel = require('@babel/core'); export enum OutputType { html, scripts, react, } export interface OutputOptions { type: OutputType, title?: string, backgroundColor?: string; desc?: string; icon?: string; headerInserts?: string[] bodyInserts?: string[] babelTransform?: boolean // 是否添加babel vconsole?: boolean // 是否添加vconsole minify?: boolean // 是否压缩代码,默认 true(生产环境压缩,本地开发可设为 false) config: AppConfigData } const httpUrl = /^http(s)?/; const pathUrl = /^(\/+)[A-Za-z\.]*/; const baleOptions = { presets: [ '@babel/preset-typescript', [ '@babel/preset-env', { 'loose': true, 'targets': { 'chrome': '49', 'ios': '9', }, }, ], ], plugins: [ '@babel/plugin-proposal-object-rest-spread', '@babel/plugin-transform-classes', ], // see https://github.com/babel/babel/issues/8900#issuecomment-431240426 sourceType: 'unambiguous', // cacheDirectory: true }; /** * Terser 深度压缩配置 */ const terserMinifyOptions: import('terser').MinifyOptions = { sourceMap: false, compress: { // 移除无法到达的代码 dead_code: true, // 移除 console 和 debugger(生产环境) drop_console: false, drop_debugger: true, // 内联常量 evaluate: true, // 优化常量表达式 booleans: true, // 优化循环 loops: true, // 优化条件表达式 conditionals: true, // 合并连续语句 sequences: true, // 优化 if return if_return: true, // 移除未引用的变量 unused: true, // 合并连续声明 join_vars: true, // 优化比较运算 comparisons: true, // 内联简单函数 inline: 2, // 移除无用代码 side_effects: true, // 优化属性访问 properties: true, // 优化 switch switches: true, // 折叠常量 collapse_vars: true, // 减少变量声明 reduce_vars: true, // 移除无用的参数名 negate_iife: true, // 优化 typeof typeofs: true, // 顶级作用域优化 toplevel: true, // 传递压缩 passes: 3, }, mangle: { // 混淆变量名 toplevel: true, // 混淆属性名(谨慎使用,可能破坏属性访问) properties: { // 只混淆以 _ 开头的属性名(约定私有属性) regex: /^_/, }, }, format: { // 移除注释 comments: false, // 移除多余空格 beautify: false, }, // 顶级作用域处理 toplevel: true, }; /** * 检测代码是否包含 TypeScript 语法 */ function containsTypeScript(code: string): boolean { // 检测常见的 TypeScript 特有语法 const tsPatterns = [ /:\s*(string|number|boolean|any|void|object|never|unknown|GameObject|JSAPIInterface|Promise)/, // 类型注解 /<\w+>/, // 泛型 /interface\s+\w+/, // 接口定义 /type\s+\w+\s*=/, // 类型别名 /enum\s+\w+/, // 枚举 /import\s+type\s+/, // 类型导入 /as\s+\w+/, // 类型断言 ]; return tsPatterns.some(pattern => pattern.test(code)); } export async function transformBabel(code: string, skipMinify: boolean = false): Promise { const data = await babel.transformAsync(code, { ...baleOptions, filename: 'script.ts', // TypeScript preset 需要文件名来识别语法 }); if (!data?.code) return ''; // 压缩代码 if (skipMinify) return data.code; const result = await minify(data.code, terserMinifyOptions); return result?.code || ''; } async function printHtml(element: string, outputs: string[], opts: OutputOptions): Promise { // console.log(JSON.stringify(opts)); const icon = escapeHtml(opts.icon); // 是否启用压缩(默认启用,本地开发可设为 false) const enableMinify = opts.minify !== false; const desc = escapeHtml(opts.desc); const title = escapeHtml(opts.title); return ` ${title} ${opts.headerInserts ? opts.headerInserts.join('') : '' } ${opts.vconsole ? ` ` : ''}
${ (outputs && outputs.length > 0) ? (await Promise.all(outputs.map(async output => { if (httpUrl.test(output) || pathUrl.test(output)) { return ` `; } else { // 自动检测是否需要 Babel 转换(TypeScript 或启用了 babelTransform) const needsTransform = opts.babelTransform || containsTypeScript(output); if (needsTransform) { // TypeScript 代码需要转换(根据 enableMinify 决定是否压缩) return ` `; } else if (enableMinify) { // 普通 JS 代码进行深度压缩 const result = await minify(output, terserMinifyOptions); return ` `; } else { // 不压缩,直接输出 return ` `; } } }))).join('\n') : ' ' } ${opts.bodyInserts ? opts.bodyInserts.join('') : ''} `; } export function outputPlugin(options: OutputOptions): Plugin { const { type, config } = options; return { async onPrinted(): Promise { const outputs: string[] = []; config.scripts?.forEach(c => { outputs.push(c.src); }); const resolveContent = (id: Symbol) => { const context = this.getScriptContext(id); (context.ref.imports || []).forEach(scriptImport => { const { scriptId } = scriptImport; resolveContent(scriptId); }); outputs.push(context.printer.output); }; resolveContent(MAIN_SCRIPT_ID); if (type === OutputType.html) { this.setValue(ContextStore.output, await printHtml(this.ctx.element, outputs.filter(o => !!o), options)); } else { this.setValue(ContextStore.output, outputs.filter(o => !!o)); } } }; }