flecks/packages/web/build/web.webpack.config.js
2024-01-19 03:38:16 -06:00

248 lines
7.3 KiB
JavaScript

const {join} = require('path');
const {
defaultConfig,
regexFromExtensions,
webpack,
} = require('@flecks/core/server');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {htmlTagObjectToString} = require('html-webpack-plugin/lib/html-tags');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const runtime = require('./runtime');
const WaitForManifestPlugin = require('./wait-for-manifest');
const {
FLECKS_CORE_ROOT = process.cwd(),
} = process.env;
module.exports = async (env, argv, flecks) => {
const {
appMountId,
base,
devHost,
devPort,
devStats,
meta,
icon,
port,
title,
} = flecks.get('@flecks/web');
const isProduction = 'production' === argv.mode;
const plugins = [
// Environment.
new webpack.EnvironmentPlugin({
FLECKS_CORE_BUILD_TARGET: 'web',
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
process: 'process/browser',
}),
];
if (isProduction) {
plugins.push(
// Inline the main entrypoint (nice for FCP).
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/^assets\/index(\.[^.]*)?\.js$/]),
);
}
// DLL
const dll = flecks.get('@flecks/web.dll');
if (!isProduction && dll.length > 0) {
const manifest = join(FLECKS_CORE_ROOT, 'node_modules', '.cache', '@flecks', 'web', 'vendor');
plugins.push(new WaitForManifestPlugin(join(manifest, 'manifest.json')));
plugins.push(
new webpack.DllReferencePlugin({
manifest: join(manifest, 'manifest.json'),
}),
);
plugins.push(
new AddAssetHtmlPlugin({
filepath: join(manifest, 'web-vendor.js'),
outputPath: '/assets',
publicPath: '/assets',
}),
);
}
const entry = {};
const entries = [
['index', {
entry: '@flecks/web/server/build/entry',
}],
];
if (!isProduction) {
entries.push(['tests', {
entry: '@flecks/web/server/build/tests',
title: 'Testbed',
}]);
}
await Promise.all(
entries
.map(async ([name, mainsConfig]) => {
const {entry: entryPoint, ...htmlTemplateConfig} = mainsConfig;
// @todo source maps working?
entry[name] = [entryPoint];
plugins.push(new HtmlWebpackPlugin({
appMountId: flecks.interpolate(appMountId),
base: flecks.interpolate(base),
chunks: [name],
filename: `${name}.html`,
inject: false,
lang: 'en',
meta,
template: await flecks.resolveBuildConfig('template.ejs', name),
templateParameters: (compilation, assets, assetTags, options) => {
function createTag(tagName, attributes, content) {
const tag = HtmlWebpackPlugin.createHtmlTagObject(
tagName,
attributes,
content,
{plugin: '@flecks/web/server'},
);
tag.toString = () => htmlTagObjectToString(tag, false);
return tag;
}
if (icon) {
assetTags.headTags.push('link', {rel: 'icon', href: icon});
}
if ('index' === name) {
const styleChunks = Array.from(compilation.chunks)
.filter((chunk) => chunk.idNameHints.has('flecks-compiled'));
for (let i = 0; i < styleChunks.length; ++i) {
const styleChunk = styleChunks[i];
const styleChunkFiles = Array.from(styleChunk.files)
.filter((file) => file.match(/\.css$/));
const styleAssets = styleChunkFiles.map((filename) => compilation.assets[filename]);
for (let j = 0; j < styleAssets.length; ++j) {
const asset = styleAssets[j];
if (asset) {
assetTags.headTags = assetTags.headTags
.filter(({attributes}) => attributes?.href !== styleChunkFiles[j]);
assetTags.headTags.unshift(
createTag(
...isProduction
? [
'style',
{'data-href': `/${styleChunkFiles[j]}`},
asset.source(),
]
: [
'link',
{
href: `/${styleChunkFiles[j]}`,
rel: 'stylesheet',
type: 'text/css',
},
],
),
);
}
}
}
}
return {
compilation,
webpackConfig: compilation.options,
htmlWebpackPlugin: {
tags: assetTags,
files: assets,
options,
},
};
},
title: flecks.interpolate(title),
...htmlTemplateConfig,
}));
}),
);
// @todo dynamic extensions
const styleExtensionsRegex = regexFromExtensions(
['.css', '.sass', '.scss'].map((ext) => [ext, `.module${ext}`]).flat(),
);
const config = defaultConfig(flecks, {
devServer: {
compress: false,
devMiddleware: {
stats: {
...devStats,
warningsFilter: [
/Failed to parse source map/,
],
},
},
historyApiFallback: {
disableDotRule: true,
},
hot: false,
host: devHost,
port: devPort || (port + 1),
},
devtool: 'source-map',
entry,
module: {
rules: [
// HTML.
{
test: /\.html$/,
use: ['html-loader'],
},
// Fold in existing source maps.
{
enforce: 'pre',
test: styleExtensionsRegex,
use: ['source-map-loader'],
},
],
},
optimization: {
minimize: isProduction,
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
applicationStyles: {
chunks: 'all',
type: 'css/mini-extract',
enforce: true,
name: 'applicationStyles',
priority: 100,
test: 'index',
},
},
chunks: 'all',
},
},
output: {
chunkFilename: isProduction ? 'assets/[name].[contenthash:8].js' : 'assets/[name].js',
filename: isProduction ? 'assets/[name].[contenthash:8].js' : 'assets/[name].js',
path: join(FLECKS_CORE_ROOT, 'dist', 'web'),
publicPath: '/',
},
plugins,
resolve: {
// Resolve the generated `url(assets/*)` style paths.
alias: {
assets: '.',
},
fallback: {
buffer: require.resolve('buffer'),
child_process: false,
fs: false,
path: require.resolve('path-browserify'),
process: require.resolve('process/browser'),
stream: require.resolve('stream-browserify'),
zlib: require.resolve('browserify-zlib'),
},
},
stats: {
...flecks.get('@flecks/web.stats'),
warningsFilter: [
/Failed to parse source map/,
],
},
target: 'web',
});
// Build the client runtime.
await runtime(config, env, argv, flecks);
return config;
};