Skip to content

Instantly share code, notes, and snippets.

@Luiz-Monad
Created February 23, 2021 06:22
Show Gist options
  • Save Luiz-Monad/c64dddb988b4a85a8a6736ae9aa1f5ab to your computer and use it in GitHub Desktop.
Save Luiz-Monad/c64dddb988b4a85a8a6736ae9aa1f5ab to your computer and use it in GitHub Desktop.
webpack from hell
// Template for webpack.config.js in Fable projects
// Find latest version in https://github.com/fable-compiler/webpack-config-template
// In most cases, you'll only need to edit the CONFIG object (after dependencies)
// See below if you need better fine-tuning of Webpack options
// Dependencies. Also required: core-js, fable-loader, fable-compiler, @babel/core, @babel/preset-env, babel-loader
const webpack = require('webpack');
//const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const util = require('./webpack.util');
const path = require('path');
const localResolve = util.resolve(__dirname);
const configFn = paramFile => {
const options = require(paramFile);
const resolve = util.resolve(options.dirName);
const baseConfig = {
dirName: options.dirName,
fsharpDefine: options.fsharpDefine,
fsharpEntry: resolve(options.fsharpEntry),
indexHtmlTemplate: [
'raw-loader',
localResolve('webpack.loader.node.js'),
resolve(path.join(options.outputDir, '../ssr/ssr.js'))
].join('!'),
srcDir: resolve('.'),
nodeModDir: resolve('node_modules'),
outputDir: resolve(options.outputDir),
assetsDir: resolve(options.assetsDir),
devServerHost: options.devServerHost,
devServerPort: options.devServerPort,
// When using webpack-dev-server, you may need to redirect some calls
// to a external API server. See https://webpack.js.org/configuration/dev-server/#devserver-proxy
devServerProxy: {
// redirect requests that start with /api/* to the server on port port
'/api/*': {
target: 'http://localhost:' + options.devServerProxy,
changeOrigin: true
},
// redirect websocket requests that start with /socket/* to the server on the port 8085
'/socket/*': {
target: 'http://localhost:' + options.devServerProxy,
ws: true
}
},
// Use babel-preset-env to generate JS compatible with most-used browsers.
// More info at https://babeljs.io/docs/en/next/babel-preset-env.html
babel: {
presets: [
['@babel/preset-env', {
modules: false,
// This adds polyfills when needed. Requires core-js dependency.
// See https://babeljs.io/docs/en/babel-preset-env#usebuiltins
// Note that you still need to add custom polyfills if necessary (e.g. whatwg-fetch)
useBuiltIns: 'usage',
corejs: 3,
}]
],
}
};
// If we're running the webpack-dev-server, assume we're in development mode
const isProduction = (process.argv.mode === 'production');
const mode = (isProduction ? 'production' : 'development');
console.log('Bundling for ' + mode + '...');
// The HtmlWebpackPlugin allows us to use a template for the index.html page
// and automatically injects <script> or <link> tags for generated bundles.
const commonPlugins = [
new HtmlWebpackPlugin({
filename: 'index.html',
template: baseConfig.indexHtmlTemplate,
inlineSource: '^.+\.(css)$',
}),
//new BundleAnalyzerPlugin(),
];
const thisConfig = {
// In development, split the JavaScript files in order to
// have a faster HMR support.
entry: {
app: [
baseConfig.fsharpEntry
]
},
// Add a hash to the output file name in production
// to prevent browser caching if code changes
output: {
path: baseConfig.outputDir,
pathinfo: !isProduction,
chunkFilename: isProduction ? '[name].[chunkhash].js' : '[name].js',
filename: isProduction ? '[name].[chunkhash].js' : '[name].js',
},
mode: mode,
devtool: isProduction ? 'source-map' : 'eval-source-map', //'inline-cheap-source-map',
context: baseConfig.srcDir,
optimization: {
minimize: isProduction,
nodeEnv: mode,
splitChunks: {
chunks: 'all',
name(module, chunks, cacheGroupKey) {
const allChunksNames = chunks.map((item) => item.name).join('~');
return `${cacheGroupKey}-${allChunksNames}`;
},
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
fable: {
test: /[\\/].fable[\\/]/,
priority: -20,
enforce: true
},
default: {
test: /[\\/]src[\\/]/,
priority: -20,
enforce: true
},
},
},
},
cache: {
type: 'filesystem'
},
target: 'web',
// Besides the HtmlPlugin, we use the following plugins:
// PRODUCTION
// DEVELOPMENT
// - HotModuleReplacementPlugin: Enables hot reloading when code changes without refreshing
plugins: isProduction
? commonPlugins.concat([
])
: commonPlugins.concat([
new webpack.HotModuleReplacementPlugin(),
]),
resolve: {
// See https://github.com/fable-compiler/Fable/issues/1490
symlinks: false,
modules: [
baseConfig.srcDir,
baseConfig.nodeModDir,
baseConfig.assetsDir,
baseConfig.outputDir,
]
},
// Configuration for webpack-dev-server
devServer: {
publicPath: '/',
contentBase: baseConfig.assetsDir,
host: baseConfig.devServerHost,
port: baseConfig.devServerPort,
proxy: baseConfig.devServerProxy,
hot: true,
inline: true,
},
// - cache-loader: caches transformed files.
// - fable-loader: transforms F# into JS.
// - babel-loader: transforms JS to old syntax (compatible with old browsers).
// - file-loader: moves files referenced in the code (fonts, images) into output folder.
// - css-loader: parses css, transforms imports to JS requires.
// - html-loader: parses html, transforms srcs/hrefs to JS requires.
// - extract-loader: requires resources as dependencies (from css/html loaders).
// - raw-loader: transforms previous text resource into a JS module.
// - val-loader: runs external commands to generate assets.
module: {
rules: [
{
test: /\.fs(x|proj)?$/,
use: [{
loader: 'fable-loader',
options: {
babel: baseConfig.babel,
define: baseConfig.fsharpDefine.concat(
isProduction ? [] : ["DEBUG"]),
extra: { optimizeWatch: !isProduction }
}
}]
},
{
test: /\.js$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: baseConfig.babel,
}]
},
{
type: 'javascript/auto',
test: /\.json$/,
use: [{
loader: 'file-loader',
}]
},
{
test: /\.css$/,
use: [{
loader: 'file-loader',
}, {
loader: 'extract-loader',
}, {
loader: 'css-loader',
}]
},
{
test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?.*)?$/,
use: [{
loader: 'file-loader',
}]
},
]
},
};
return thisConfig;
};
module.exports = env => configFn(env.param);
// Config for Node SSR.
const webpack = require('webpack');
const baseConfigFn = require('./webpack.config');
const util = require('./webpack.util');
const node_path = require('path');
const configFn = baseConfig => {
const isProduction = (baseConfig.mode == 'production');
console.log('Bundling for server-side-rendering...');
// fable-loader extra defines.
const loader = util.findLoader(baseConfig.module, 'fable-loader');
const fable = Object.assign({}, loader,
{
options: Object.assign({}, loader.options, {
define: loader.options.define.concat([
"SSR"
]),
})
}
);
return ({
entry: {
ssr: baseConfig.entry.app,
},
output: {
path: node_path.join(baseConfig.output.path, '../ssr/'),
pathinfo: baseConfig.output.pathinfo,
chunkFilename: '[name].js',
filename: '[name].js',
libraryTarget: 'umd',
},
mode: baseConfig.mode,
context: baseConfig.context,
optimization: {
nodeEnv: baseConfig.mode,
minimize: isProduction,
removeAvailableModules: false,
removeEmptyChunks: false,
mergeDuplicateChunks: false,
splitChunks: isProduction ? false : baseConfig.optimization.splitChunks,
},
cache: {
type: 'filesystem'
},
target: 'node',
plugins: isProduction ? [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
}),
] : [],
resolve: {
symlinks: baseConfig.symlinks,
modules: baseConfig.resolve.modules,
alias: {
process: "process/browser",
crypto: "crypto-browserify",
}
},
module: util.replaceLoader(baseConfig.module, {
'fable-loader': fable
}),
});
};
module.exports = env => configFn(baseConfigFn(env));
const path = require('path');
const fs = require('fs');
function processResult(loaderContext, result) {
if (
typeof result !== "string" &&
result instanceof Buffer === false
) {
loaderContext.callback(
new Error(
`The returned code of module "${loaderContext.resource}" is neither a string nor an instance of Buffer`
)
);
return;
}
const dir = path.dirname(loaderContext.resourcePath);
const emitFile = files => {
if (!files || !files[0]) {
loaderContext.cacheable(true);
loaderContext.callback(
null,
result
);
return;
}
const file = files[0];
const fullpath = path.join(dir, files[0]);
fs.readFile(fullpath, (err, content) => {
if (err) {
reject(new Error(`Couldn\'t find the "${file}" file.`));
return;
}
//ignore js files, they are build deps only.
if (!(/\.js$/.test(file))) {
loaderContext.emitFile(file, content);
}
loaderContext.addDependency(fullpath);
emitFile(files.splice(1));
});
};
fs.readdir(dir, function(err, items) {
if (err) {
reject(new Error(`Couldn\'t list "${dir}" directory.`));
return;
}
emitFile(items);
});
}
const loader = function (content, map, meta) {
const callback = this.async();
const exports = require(this.resourcePath);
console.log(exports);
let func = exports && exports.default ? exports.default : exports;
if (typeof func !== "function") { func = exports[Object.keys(exports)[0]] }
if (typeof func !== "function") {
callback(
new Error(
`Module "${this.resource}" does not export a function as default`
)
);
return;
}
let result;
try {
result = func();
} catch (error) {
callback(new Error(`Module "${this.resource}" throw error: ${error}`));
return;
}
if (result && typeof result.then === "function") {
result
.then((res) => processResult(this, res))
.catch((error) => {
callback(new Error(`Module "${this.resource}" throw error: ${error}`));
});
return;
}
// No return necessary because processResult calls this.callback()
processResult(this, result);
}
module.exports = loader;
module.exports = {
dirName: __dirname,
fsharpDefine: ['PROJ_IS_CLIENT'],
fsharpEntry: './src/Client/Client.fsproj',
outputDir: './src/Client/out',
assetsDir: './src/Client/static',
devServerHost: 'localhost',
devServerPort: 8086,
devServerProxy: 8085,
};
const path = require('path');
function resolve(dirname) {
return function (filePath) {
return path.isAbsolute(filePath) ? filePath : path.join(dirname, filePath);
};
}
function findLoader(modl, loader) {
return modl.rules.map(function(m) {
return m.use && m.use.length && m.use[m.use.length - 1].loader === loader
? m.use[m.use.length - 1] : null
}).filter(function (m) { return !!m })[0];
}
function replaceLoader(modl, loaders) {
return Object.keys(loaders).reduce(function(prev, cur) {
return Object.assign({}, prev, {
rules: prev.rules.map(function(m) {
return m.use && m.use.length && m.use[m.use.length - 1].loader === cur
? Object.assign({}, m, { use: loaders[cur] }) : m
}).filter(function (m) { return !!m.use })
})
}, modl);
}
module.exports = { resolve, findLoader, replaceLoader };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment