const defaultsDeep = require('lodash.defaultsdeep'); const path = require('path'); const webpack = require('webpack'); // Plugins const CopyWebpackPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CompressionPlugin = require("compression-webpack-plugin"); // const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') // PostCss const autoprefixer = require('autoprefixer'); const postcssVars = require('postcss-simple-vars'); const postcssImport = require('postcss-import'); const TerserPlugin = require('terser-webpack-plugin'); const STATIC_PATH = process.env.STATIC_PATH || '/static'; const { APP_NAME } = require('./src/lib/brand'); const root = process.env.ROOT || ''; if (root.length > 0 && !root.endsWith('/')) { throw new Error('If ROOT is defined, it must have a trailing slash.'); } const version = "2025071401"; const htmlWebpackPluginCommon = { root: root, meta: JSON.parse(process.env.EXTRA_META || '{}'), APP_NAME, version: version, // 使用时间戳作为版本号 minify: process.env.NODE_ENV === 'production' ? { collapseWhitespace: true, removeComments: true, removeRedundantAttributes: false, // 这会保留 type="text" 等属性 removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true, useShortDoctype: true } : false }; // When this changes, the path for all JS files will change, bypassing any HTTP caches const CACHE_EPOCH = 'gleba'; const fs = require('fs'); const base = { mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', devtool: process.env.SOURCEMAP || (process.env.NODE_ENV === 'production' ? false : 'cheap-module-source-map'), devServer: { contentBase: path.resolve(__dirname, 'build'), host: '0.0.0.0', disableHostCheck: true, compress: true, // https: { // key: fs.readFileSync('server.key'), // 自签名证书的密钥路径 // cert: fs.readFileSync('server.cert') // 自签名证书的路径 // }, port: process.env.PORT || 8601, // allows ROUTING_STYLE=wildcard to work properly historyApiFallback: { rewrites: [ { from: /^\/$/, to: '/login.html' }, // 根路径重定向到 login.html // {from: /^\/\d+\/fullscreen\/?$/, to: '/fullscreen.html'}, // {from: /^\/\d+\/editor\/?$/, to: '/editor.html'}, // {from: /^\/\d+\/embed\/?$/, to: '/embed.html'}, { from: /^\/editor\/?$/, to: '/editor.html' }, // {from: /^\/addons\/?$/, to: '/addons.html'}, { from: /^\/login\/?$/, to: '/login.html' }, // {from: /^\/forgotpass\/?$/, to: '/forgotpass.html'}, { from: /^\/signup\/?$/, to: '/signup.html' }, // {from: /^\/onegame\/?$/, to: '/onegame.html'}, { from: /^\/welcome\/?$/, to: '/welcome.html' }, { from: /^\/competition\/?$/, to: '/competition.html' }, { from: /^\/competition_sum\/?$/, to: '/competition_sum.html' }, { from: /^\/competition_list\/?$/, to: '/competition_list.html' }, { from: /^\/programming-learning\/?$/, to: '/programming-learning.html' }, { from: /^\/leaderboard\/?$/, to: '/leaderboard.html' }, // {from: /^\/001oj\/?$/, to: '/001oj.html'}, // {from: /^\/aipic\/?$/, to: '/aipic.html'}, { from: /^\/matchsum\/?$/, to: '/matchsum.html' }, { from: /^\/third-part-login\/?$/, to: '/third-part-login.html' }, { from: /^\/competition-groups\/?$/, to: '/competition-groups.html' }, { from: /^\/sichuan-chongqing-competition\/?$/, to: '/sichuan-chongqing-competition.html' }, { from: /^\/aboutus\/?$/, to: '/aboutus.html' }, { from: /^\/home\/?$/, to: '/home.html' }, { from: /^\/home-laoshan\/?$/, to: '/competition-scssn-laoshan.html' }, //崂山赛事定制 { from: /^\/aicoding-groups-laoshan\/?$/, to: '/competition-groups-laoshan.html' }, { from: /^\/home-linyi\/?$/, to: '/competition-scssn-linyi.html' }, { from: /^\/aicoding-groups-linyi\/?$/, to: '/competition-groups-linyi.html' }, { from: /^\/home-linqing\/?$/, to: '/competition-linqing.html' }, { from: /^\/aicoding-groups-linqing\/?$/, to: '/competition-groups-linqing.html' }, { from: /^\/home-chengyang\/?$/, to: '/competition-chengyang.html' }, { from: /^\/aicoding-groups-chengyang\/?$/, to: '/competition-groups-chengyang.html' }, { from: /^\/home-longyan\/?$/, to: '/competition-longyan.html' }, { from: /^\/aicoding-groups-longyan\/?$/, to: '/competition-groups-longyan.html' }, ] }, headers: { 'Access-Control-Allow-Origin': '*' }, proxy: (() => { const remote = process.env.UNITY_CDN_REMOTE || 'https://oss.eanic.cn/001_code_cocos_res_20260616'; const proxyPrefix = process.env.UNITY_CDN_DEV_PROXY || '/cdn-unity'; let origin; let pathname; try { const u = new URL(remote); origin = u.origin; pathname = u.pathname.replace(/\/$/, ''); } catch (e) { return {}; } return { [proxyPrefix]: { target: origin, pathRewrite: {[proxyPrefix]: pathname}, changeOrigin: true, secure: true, logLevel: 'warn' } }; })(), // 自定义请求头 before: function (app, server) { app.get('*.wasm.br', (req, res, next) => { res.setHeader('Content-Type', 'application/wasm'); next(); }); app.get('*.asm.js.br', (req, res, next) => { res.setHeader('Content-Type', 'application/javascript'); next(); }); app.get('*.br', (req, res, next) => { res.set('Content-Encoding', 'br'); // 为 Brotli 文件添加 Content-Encoding 头 next(); }); } }, output: { library: 'GUI', filename: process.env.NODE_ENV === 'production' ? `js/${CACHE_EPOCH}/[name].[contenthash].js` : 'js/[name].js', chunkFilename: process.env.NODE_ENV === 'production' ? `js/${CACHE_EPOCH}/[name].[contenthash].js` : 'js/[name].js', publicPath: root }, resolve: { symlinks: false, alias: { 'text-encoding$': path.resolve(__dirname, 'src/lib/tw-text-encoder'), 'scratch-render-fonts$': path.resolve(__dirname, 'src/lib/tw-scratch-render-fonts') } }, module: { rules: [{ test: /\.jsx?$/, loader: 'babel-loader', include: [ path.resolve(__dirname, 'src'), /node_modules[\\/]scratch-[^\\/]+[\\/]src/, /node_modules[\\/]pify/, /node_modules[\\/]@vernier[\\/]godirect/ ], options: { // Explicitly disable babelrc so we don't catch various config // in much lower dependencies. babelrc: false, plugins: [ ['react-intl', { messagesDir: './translations/messages/' }]], presets: ['@babel/preset-env', '@babel/preset-react'] } }, { test: /\.css$/, use: [{ loader: 'style-loader' }, { loader: 'css-loader', options: { modules: true, importLoaders: 1, localIdentName: '[name]_[local]_[hash:base64:5]', camelCase: true } }, { loader: 'postcss-loader', options: { ident: 'postcss', plugins: function () { return [ postcssImport, postcssVars, autoprefixer ]; } } }] }, { test: /\.hex$/, use: [{ loader: 'url-loader', options: { limit: 16 * 1024 } }] }] }, plugins: [ new CopyWebpackPlugin({ patterns: [ { from: 'node_modules/scratch-blocks/media', to: 'static/blocks-media/default' }, { from: 'node_modules/scratch-blocks/media', to: 'static/blocks-media/high-contrast' }, { from: 'src/lib/themes/blocks/high-contrast-media/blocks-media', to: 'static/blocks-media/high-contrast', force: true }, { from: 'static/css', to: 'css' }, { from: 'static/js', to: 'js' } ] }) ] }; if (!process.env.CI) { base.plugins.push(new webpack.ProgressPlugin()); } module.exports = [ // to run editor examples defaultsDeep({}, base, { entry: { 'editor': ['./src/playground/checkStatus.js', './src/playground/editor.jsx', './src/playground/auth.js'], // 'player': './src/playground/player.jsx', // 'fullscreen': './src/playground/fullscreen.jsx', // 'embed': './src/playground/embed.jsx', // 'addon-settings': './src/playground/addon-settings.jsx', // 'credits': './src/playground/credits/credits.jsx', 'login': './src/playground/login.js', // 新增的 login.js 入口 'signup': '/src/playground/signup.js', 'matchsum': ['./src/playground/checkStatus.js', '/src/playground/matchsum.js', './src/playground/auth.js'], 'app-target': './src/playground/app-target.js', // 确保全局入口文件被包含 'welcome': ['./src/playground/checkStatus.js', './src/playground/welcome.js', './src/playground/auth.js'], 'programming-learning': ['./src/playground/checkStatus.js', './src/playground/programming-learning.js', './src/playground/auth.js'], 'competition_sum': ['./src/playground/checkcompetitionStatus.js', './src/playground/competition_sum.js', './src/playground/auth.js'], 'competition': ['./src/playground/competition.js'], 'competition_list': ['./src/playground/competition_list.js'], // 'onegame': ['./src/playground/import-check.js', './src/playground/onegame.js'] 'third-part-login': ['./src/playground/third-part-login.js'], 'leaderboard': './src/playground/leaderboard.js' }, output: { path: path.resolve(__dirname, 'build') }, module: { rules: base.module.rules.concat([ { test: /\.(svg|png|wav|mp3|gif|jpg|woff2)$/, loader: 'url-loader', options: { limit: 2048, outputPath: 'static/assets/', esModule: false } } ]) }, optimization: { // 启用tree-shaking和代码去重 usedExports: true, sideEffects: false, minimize: true, minimizer: [ new TerserPlugin({ terserOptions: { parse: { ecma: 8 }, compress: { ecma: 5, warnings: false, comparisons: false, inline: 2, drop_console: process.env.NODE_ENV === 'production', drop_debugger: true, pure_funcs: ['console.log'] }, output: { ecma: 5, comments: false, ascii_only: true }, mangle: true }, parallel: true, cache: true }) ], splitChunks: { chunks: 'all', minChunks: 2, minSize: 50000, maxInitialRequests: 5 } // splitChunks: { // chunks: 'all', // minSize: 30000, // 提高最小chunk体积(减少小chunks) // maxSize: 500000, // 最大chunk体积 // minChunks: 2, // 至少被2个入口引用才抽离 // maxAsyncRequests: 30, // maxInitialRequests: 3, // 降低初始请求数(减少chunks) // automaticNameDelimiter: '~', // cacheGroups: { // // 只抽离真正被多个入口使用的库 // vendors: { // test: /[\\/]node_modules[\\/]/, // name: 'vendors', // priority: 10, // reuseExistingChunk: true, // enforce: true, // minChunks: 3 // 至少3个入口使用才抽离vendors // }, // // React和Redux等核心库 // react: { // test: /[\\/]node_modules[\\/](react|react-dom|react-redux|redux)[\\/]/, // name: 'react-vendors', // priority: 20, // reuseExistingChunk: true, // minChunks: 2 // }, // // Scratch相关库 // scratch: { // test: /[\\/]node_modules[\\/](scratch-vm|scratch-render)[\\/]/, // name: 'scratch-vendors', // priority: 19, // reuseExistingChunk: true, // minChunks: 2 // }, // // 通用工具库 // common: { // minChunks: 2, // priority: 5, // reuseExistingChunk: true // } // } // } }, plugins: base.plugins.concat([ new webpack.DefinePlugin({ 'process.env.NODE_ENV': `"${process.env.NODE_ENV}"`, 'process.env.DEBUG': Boolean(process.env.DEBUG), 'process.env.ENABLE_SERVICE_WORKER': JSON.stringify(process.env.ENABLE_SERVICE_WORKER || ''), 'process.env.ROOT': JSON.stringify(root), 'process.env.ROUTING_STYLE': JSON.stringify(process.env.ROUTING_STYLE || 'filehash') }), new HtmlWebpackPlugin({ chunks: ['editor'], template: 'src/playground/index.ejs', filename: 'editor.html', title: `${APP_NAME}`, isEditor: true, ...htmlWebpackPluginCommon }), // new HtmlWebpackPlugin({ // chunks: ['player'], // template: 'src/playground/index.ejs', // filename: 'index.html', // title: `${APP_NAME} - Run Scratch projects faster`, // ...htmlWebpackPluginCommon // }), // new HtmlWebpackPlugin({ // chunks: ['fullscreen'], // template: 'src/playground/index.ejs', // filename: 'fullscreen.html', // title: `${APP_NAME}`, // ...htmlWebpackPluginCommon // }), // new HtmlWebpackPlugin({ // chunks: ['embed'], // template: 'src/playground/embed.ejs', // filename: 'embed.html', // title: `Embedded Project - ${APP_NAME}`, // ...htmlWebpackPluginCommon // }), // new HtmlWebpackPlugin({ // chunks: ['addon-settings'], // template: 'src/playground/simple.ejs', // filename: 'addons.html', // title: `Addon Settings - ${APP_NAME}`, // ...htmlWebpackPluginCommon // }), // new HtmlWebpackPlugin({ // chunks: ['credits'], // template: 'src/playground/simple.ejs', // filename: 'credits.html', // title: `${APP_NAME} Credits`, // ...htmlWebpackPluginCommon // }), new HtmlWebpackPlugin({ chunks: ['signup'], template: 'src/playground/signup-new.ejs', // 注册页面 filename: 'signup.html', title: `signup - ${APP_NAME}`, minify: { collapseWhitespace: true, removeComments: true, removeRedundantAttributes: false, // 这会保留 type="text" removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true, useShortDoctype: true }, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['forgotpass'], template: 'src/playground/forgotpass.ejs', // 忘记密码页面 filename: 'forgotpass.html', title: `forgotpass - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new CopyWebpackPlugin({ patterns: [ { from: 'static', to: '', globOptions: { ignore: process.env.UNITY_CDN_PURE ? ['**/unity/**'] : [] } } ] }), new HtmlWebpackPlugin({ chunks: ['login'], template: 'src/playground/login-new.ejs', // 新创建的 EJS 文件 filename: 'login.html', // 生成的登录页面文件 title: `Login - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['login'], template: 'src/playground/login-new.ejs', // 新创建的 EJS 文件 filename: 'index.html', // 生成的登录页面文件 title: `Login - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition-scssn-laoshan'], template: 'src/playground/aicoding-laoshan.ejs', filename: 'competition-scssn-laoshan.html', // 崂山主页 title: `比赛入口 - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition-groups-laoshan'], template: 'src/playground/aicoding-groups-laoshan.ejs', filename: 'aicoding-groups-laoshan.html', title: `aicoding-groups-laoshan - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition-scssn-linyi'], template: 'src/playground/aicoding-linyi.ejs', filename: 'competition-scssn-linyi.html', // 临沂主页 title: `比赛入口 - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition-groups-linyi'], template: 'src/playground/aicoding-groups-linyi.ejs', filename: 'aicoding-groups-linyi.html', title: `aicoding-groups-linyi - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition-linqing'], template: 'src/playground/aicoding-groups-linqing.ejs', filename: 'aicoding-groups-linqing.html', title: `aicoding-groups-linqing - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition-linqing'], template: 'src/playground/aicoding-linqing.ejs', filename: 'competition-linqing.html', // 临清主页 title: `比赛入口 - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition-linqing'], template: 'src/playground/aicoding-groups-chengyang.ejs', filename: 'aicoding-groups-chengyang.html', title: `aicoding-groups-chengyang - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition-chengyang'], template: 'src/playground/aicoding-chengyang.ejs', filename: 'competition-chengyang.html', // 城阳区主页 title: `比赛入口 - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition-linqing'], template: 'src/playground/aicoding-groups-longyan.ejs', filename: 'aicoding-groups-longyan.html', title: `aicoding-groups-longyan - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition-longyan'], template: 'src/playground/aicoding-longyan.ejs', filename: 'competition-longyan.html', // 龙岩区主页 title: `比赛入口 - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition'], template: 'src/playground/competition.ejs', filename: 'competition.html', title: `competition - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition_sum'], template: 'src/playground/competition_sum.ejs', filename: 'competition_sum.html', title: `competition_sum - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition_list'], template: 'src/playground/competition_list.ejs', filename: 'competition_list.html', title: `competition_list - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['competition-groups'], template: 'src/playground/competition-groups.ejs', filename: 'competition-groups.html', title: `competition-groups - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['sichuan-chongqing-competition'], template: 'src/playground/sichuan-chongqing-competition.ejs', filename: 'sichuan-chongqing-competition.html', title: `sichuan-chongqing-competition - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['leaderboard'], template: 'src/playground/leaderboard.ejs', filename: 'leaderboard.html', title: `leaderboard - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['third-part-login'], template: 'src/playground/third-part-login.ejs', filename: 'third-part-login.html', title: `third-part-login - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['import-check', 'welcome'], template: 'src/playground/welcome.ejs', filename: 'welcome.html', title: `Welcome - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['import-check', 'programming-learning'], template: 'src/playground/programming-learning.ejs', filename: 'programming-learning.html', title: `programming-learning - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['import-check', 'matchsum'], template: 'src/playground/matchsum.ejs', filename: 'matchsum.html', title: `matchsum - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['import-check', 'aboutus'], template: 'src/playground/aboutus.ejs', filename: 'aboutus.html', title: `aboutus - ${APP_NAME}`, ...htmlWebpackPluginCommon }), new HtmlWebpackPlugin({ chunks: ['import-check', 'home'], template: 'src/playground/home.ejs', filename: 'home.html', title: `home - ${APP_NAME}`, ...htmlWebpackPluginCommon }), // new HtmlWebpackPlugin({ // chunks: ['import-check', 'onegame'], // 确保 import-first 在 welcome 之前引入 // template: 'src/playground/onegame.ejs', // filename: 'onegame.html', // title: `onegame - ${APP_NAME}`, // ...htmlWebpackPluginCommon // }), // new HtmlWebpackPlugin({ // chunks: ['001oj'], // template: 'src/playground/001oj.ejs', // filename: '001oj.html', // title: `001oj - ${APP_NAME}`, // pageTitle: '001OJ - 在线评测系统', // ogTitle: '001OJ', // content: '这是动态生成的内容', // footerText: '欢迎您', // ...htmlWebpackPluginCommon // }), // new HtmlWebpackPlugin({ // chunks: ['aipic'], // template: 'src/playground/aipic.ejs', // filename: 'aipic.html', // title: `aipic - ${APP_NAME}`, // pageTitle: 'aipic - 在线创作', // ogTitle: 'aipic', // content: '这是动态生成的内容', // footerText: '欢迎您', // ...htmlWebpackPluginCommon // }), new CopyWebpackPlugin({ patterns: [ { from: 'extensions/**', to: 'static', context: 'src/examples' } ] }), // Brotli压缩(比gzip更高效) new CompressionPlugin({ algorithm: 'brotliCompress', test: /\.(js|css|html|svg|json)$/, threshold: 8192, // 只压缩>8KB的文件 minRatio: 0.8, filename: '[path][base].br' }), // 可选:gzip作为后备 new CompressionPlugin({ filename: '[path][base].gz', algorithm: 'gzip', test: /\.(js|css|html|svg|json)$/, threshold: 8192, minRatio: 0.8, compressionOptions: { level: 9 } }), // new BundleAnalyzerPlugin({ // analyzerMode: 'server', // 启动本地服务 // openAnalyzer: true // 自动打开浏览器 // }) ]) }) ].concat( process.env.NODE_ENV === 'production' || process.env.BUILD_MODE === 'dist' ? ( // export as library defaultsDeep({}, base, { target: 'web', entry: { 'scratch-gui': './src/index.js' }, output: { libraryTarget: 'umd', filename: 'js/[name].js', chunkFilename: 'js/[name].js', path: path.resolve('dist'), publicPath: `${STATIC_PATH}/` }, externals: { 'react': 'react', 'react-dom': 'react-dom' }, module: { rules: base.module.rules.concat([ { test: /\.(svg|png|wav|mp3|gif|jpg|woff2)$/, loader: 'url-loader', options: { limit: 2048, outputPath: 'static/assets/', publicPath: `${STATIC_PATH}/assets/`, esModule: false } } ]) }, plugins: base.plugins.concat([ new CopyWebpackPlugin({ patterns: [ { from: 'extension-worker.{js,js.map}', context: 'node_modules/scratch-vm/dist/web', noErrorOnMissing: true } ] }), // Include library JSON files for scratch-desktop to use for downloading new CopyWebpackPlugin({ patterns: [ { from: 'src/lib/libraries/*.json', to: 'libraries', flatten: true } ] }) ]) })) : [] );