import { watch } from 'chokidar'; import { globSync } from 'glob'; import HTMLInlineCSSWebpackPluginModule from 'html-inline-css-webpack-plugin'; import HtmlInlineScriptWebpackPlugin from 'html-inline-script-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import _ from 'lodash'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import { exec } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import url from 'node:url'; import RemarkHTML from 'remark-html'; import { Server } from 'socket.io'; import TerserPlugin from 'terser-webpack-plugin'; import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; import unpluginAutoImport from 'unplugin-auto-import/webpack'; import { VueUseComponentsResolver, VueUseDirectiveResolver } from 'unplugin-vue-components/resolvers'; import unpluginVueComponents from 'unplugin-vue-components/webpack'; import { VueLoaderPlugin } from 'vue-loader'; import webpack from 'webpack'; import WebpackObfuscator from 'webpack-obfuscator'; const HTMLInlineCSSWebpackPlugin = (HTMLInlineCSSWebpackPluginModule as any).default || HTMLInlineCSSWebpackPluginModule; const __filename = url.fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); interface Config { port: number; entries: Entry[]; } interface Entry { script: string; html?: string; } function parse_entry(script_file: string) { const html = path.join(path.dirname(script_file), 'index.html'); if (fs.existsSync(html)) { return { script: script_file, html }; } return { script: script_file }; } function common_path(lhs: string, rhs: string) { const lhs_parts = lhs.split(path.sep); const rhs_parts = rhs.split(path.sep); for (let i = 0; i < Math.min(lhs_parts.length, rhs_parts.length); i++) { if (lhs_parts[i] !== rhs_parts[i]) { return lhs_parts.slice(0, i).join(path.sep); } } return lhs_parts.join(path.sep); } function glob_script_files() { const files: string[] = globSync(`src/**/index.{ts,tsx,js,jsx}`).filter( file => process.env.CI !== 'true' || !fs.readFileSync(path.join(__dirname, file)).includes('@no-ci'), ); const results: string[] = []; const handle = (file: string) => { const file_dirname = path.dirname(file); for (const [index, result] of results.entries()) { const result_dirname = path.dirname(result); const common = common_path(result_dirname, file_dirname); if (common === result_dirname) { return; } if (common === file_dirname) { results.splice(index, 1, file); return; } } results.push(file); }; files.forEach(handle); return results; } const config: Config = { port: 6621, entries: glob_script_files().map(parse_entry), }; let io: Server; function watch_it(compiler: webpack.Compiler) { if (compiler.options.watch) { if (!io) { const port = config.port ?? 6621; io = new Server(port, { cors: { origin: '*' } }); console.info(`[Listener] 已启动酒馆监听服务, 正在监听: http://0.0.0.0:${port}`); io.on('connect', socket => { console.info(`[Listener] 成功连接到酒馆网页 '${socket.id}', 初始化推送...`); io.emit('iframe_updated'); socket.on('disconnect', reason => { console.info(`[Listener] 与酒馆网页 '${socket.id}' 断开连接: ${reason}`); }); }); } compiler.hooks.done.tap('updater', () => { console.info('\n[Listener] 检测到完成编译, 推送更新事件...'); io.emit('iframe_updated'); }); } } function dump_schema(compiler: webpack.Compiler) { const execute = () => { exec('pnpm dump', { cwd: __dirname }); }; const execute_debounced = _.debounce(execute, 500, { leading: true, trailing: false }); if (!compiler.options.watch) { execute(); } else { watch('src', { awaitWriteFinish: true, }).on('all', (_event, path) => { if (path.endsWith('schema.ts')) { execute_debounced(); } }); } } function parse_configuration(entry: Entry): (_env: any, argv: any) => webpack.Configuration { const should_obfuscate = fs.readFileSync(path.join(__dirname, entry.script), 'utf-8').includes('@obfuscate'); const script_filepath = path.parse(entry.script); return (_env, argv) => ({ experiments: { outputModule: true, }, devtool: argv.mode === 'production' ? 'hidden-source-map' : 'eval-source-map', watchOptions: { ignored: ['**/dist', '**/node_modules'], }, entry: path.join(__dirname, entry.script), target: 'browserslist', output: { devtoolNamespace: 'tavern_helper_template', devtoolModuleFilenameTemplate: info => { const resource_path = decodeURIComponent(info.resourcePath.replace(/^\.\//, '')); const is_direct = info.allLoaders === ''; const is_vue_script = resource_path.match(/\.vue$/) && info.query.match(/\btype=script\b/) && !info.allLoaders.match(/\bts-loader\b/); return `${is_direct === true ? 'src' : 'webpack'}://${info.namespace}/${resource_path}${is_direct || is_vue_script ? '' : '?' + info.hash}`; }, filename: `${script_filepath.name}.js`, path: path.join(__dirname, 'dist', path.relative(path.join(__dirname, 'src'), script_filepath.dir)), chunkFilename: `${script_filepath.name}.[contenthash].chunk.js`, asyncChunks: true, clean: true, publicPath: '', library: { type: 'module', }, }, module: { rules: [ { test: /\.vue$/, use: 'vue-loader', exclude: /node_modules/, }, { oneOf: [ { test: /\.tsx?$/, loader: 'ts-loader', options: { transpileOnly: true, onlyCompileBundledFiles: true, compilerOptions: { noUnusedLocals: false, noUnusedParameters: false, }, }, resourceQuery: /raw/, type: 'asset/source', exclude: /node_modules/, }, { test: /\.(sa|sc)ss$/, use: ['postcss-loader', 'sass-loader'], resourceQuery: /raw/, type: 'asset/source', exclude: /node_modules/, }, { test: /\.css$/, use: ['postcss-loader'], resourceQuery: /raw/, type: 'asset/source', exclude: /node_modules/, }, { resourceQuery: /raw/, type: 'asset/source', exclude: /node_modules/, }, { test: /\.tsx?$/, loader: 'ts-loader', options: { transpileOnly: true, onlyCompileBundledFiles: true, compilerOptions: { noUnusedLocals: false, noUnusedParameters: false, }, }, resourceQuery: /url/, type: 'asset/inline', exclude: /node_modules/, }, { test: /\.(sa|sc)ss$/, use: ['postcss-loader', 'sass-loader'], resourceQuery: /url/, type: 'asset/inline', exclude: /node_modules/, }, { test: /\.css$/, use: ['postcss-loader'], resourceQuery: /url/, type: 'asset/inline', exclude: /node_modules/, }, { resourceQuery: /url/, type: 'asset/inline', exclude: /node_modules/, }, { test: /\.tsx?$/, loader: 'ts-loader', options: { transpileOnly: true, onlyCompileBundledFiles: true, compilerOptions: { noUnusedLocals: false, noUnusedParameters: false, }, }, exclude: /node_modules/, }, { test: /\.html$/, use: 'html-loader', exclude: /node_modules/, }, { test: /\.md$/, use: [ { loader: 'html-loader', }, { loader: 'remark-loader', options: { remarkOptions: { plugins: [RemarkHTML], }, }, }, ], }, ].concat( entry.html === undefined ? ([ { test: /\.vue\.s(a|c)ss$/, use: [ { loader: 'vue-style-loader', options: { ssrId: true } }, { loader: 'css-loader', options: { url: false } }, 'postcss-loader', 'sass-loader', ], exclude: /node_modules/, }, { test: /\.vue\.css$/, use: [ { loader: 'vue-style-loader', options: { ssrId: true } }, { loader: 'css-loader', options: { url: false } }, 'postcss-loader', ], exclude: /node_modules/, }, { test: /\.s(a|c)ss$/, use: [ 'style-loader', { loader: 'css-loader', options: { url: false } }, 'postcss-loader', 'sass-loader', ], exclude: /node_modules/, }, { test: /\.css$/, use: ['style-loader', { loader: 'css-loader', options: { url: false } }, 'postcss-loader'], exclude: /node_modules/, }, ] as any[]) : ([ { test: /\.s(a|c)ss$/, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { url: false } }, 'postcss-loader', 'sass-loader', ], exclude: /node_modules/, }, { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { url: false } }, 'postcss-loader', ], exclude: /node_modules/, }, ] as any[]), ), }, ], }, resolve: { extensions: ['.ts', '.js', '.tsx', '.jsx', '.css'], plugins: [ new TsconfigPathsPlugin({ extensions: ['.ts', '.js', '.tsx', '.jsx'], configFile: path.join(__dirname, 'tsconfig.json'), }), ], alias: {}, }, plugins: (entry.html === undefined ? [new MiniCssExtractPlugin()] : [ new HtmlWebpackPlugin({ template: path.join(__dirname, entry.html), filename: path.parse(entry.html).base, scriptLoading: 'module', cache: false, }), new HtmlInlineScriptWebpackPlugin(), new MiniCssExtractPlugin(), new HTMLInlineCSSWebpackPlugin({ styleTagFactory({ style }: { style: string }) { return ``; }, }), ] ) .concat( { apply: watch_it }, { apply: dump_schema }, new VueLoaderPlugin(), unpluginAutoImport({ dts: true, dtsMode: 'overwrite', imports: [ 'vue', 'pinia', '@vueuse/core', { from: 'dedent', imports: [['default', 'dedent']] }, { from: 'klona', imports: ['klona'] }, { from: 'vue-final-modal', imports: ['useModal'] }, { from: 'zod', imports: ['z'] }, ], }), unpluginVueComponents({ dts: true, syncMode: 'overwrite', // globs: ['src/panel/component/*.vue'], resolvers: [VueUseComponentsResolver(), VueUseDirectiveResolver()], }), new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }), new webpack.DefinePlugin({ __VUE_OPTIONS_API__: false, __VUE_PROD_DEVTOOLS__: process.env.CI !== 'true', __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, }), ) .concat( should_obfuscate ? [ new WebpackObfuscator({ controlFlowFlattening: true, numbersToExpressions: true, selfDefending: true, simplify: true, splitStrings: true, seed: 1, }), ] : [], ), optimization: { minimize: true, minimizer: [ argv.mode === 'production' ? new TerserPlugin({ terserOptions: { format: { quote_style: 1 }, mangle: { reserved: ['_', 'toastr', 'YAML', '$', 'z'] } }, }) : new TerserPlugin({ extractComments: false, terserOptions: { format: { beautify: true, indent_level: 2 }, compress: false, mangle: false, }, }), ], splitChunks: { chunks: 'async', minSize: 20000, minChunks: 1, maxAsyncRequests: 30, maxInitialRequests: 30, cacheGroups: { vendor: { name: 'vendor', test: /[\\/]node_modules[\\/]/, priority: -10, }, default: { name: 'default', minChunks: 2, priority: -20, reuseExistingChunk: true, }, }, }, }, externals: ({ context, request }, callback) => { if (!context || !request) { return callback(); } if ( request.startsWith('-') || request.startsWith('.') || request.startsWith('/') || request.startsWith('!') || request.startsWith('http') || request.startsWith('@/') || path.isAbsolute(request) || fs.existsSync(path.join(context, request)) || fs.existsSync(request) ) { return callback(); } const builtin = ['vue3-pixi', 'vue-demi']; if (builtin.includes(request)) { return callback(); } if (argv.mode !== 'production' && ['vue', 'pixi'].some(key => request.includes(key))) { return callback(); } if (['react'].some(key => request.includes(key))) { return callback(); } const global = { jquery: '$', lodash: '_', showdown: 'showdown', toastr: 'toastr', vue: 'Vue', 'vue-router': 'VueRouter', yaml: 'YAML', zod: 'z', 'pixi.js': 'PIXI', }; if (request in global) { return callback(null, 'var ' + global[request as keyof typeof global]); } const cdn = { sass: 'https://jspm.dev/sass', }; return callback( null, 'module-import ' + (cdn[request as keyof typeof cdn] ?? `https://testingcf.jsdelivr.net/npm/${request}/+esm`), ); }, }); } export default config.entries.map(parse_configuration);