Files
001code-html--cocos/scratch-gui/webpack.config.js
2026-06-16 16:27:02 +08:00

746 lines
30 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
]
})
])
})) : []
);