Skip to content

Instantly share code, notes, and snippets.

@Luiz-Monad
Last active May 7, 2020 02:16
Show Gist options
  • Save Luiz-Monad/366d43f9a762317ffa824bff364b35be to your computer and use it in GitHub Desktop.
Save Luiz-Monad/366d43f9a762317ffa824bff364b35be to your computer and use it in GitHub Desktop.
webpack server side rendering (all rights reserved)
open Browser.Types
open Browser.Dom
open Node
let private proc = Api.``process``
// Node Application Entry
let mutable runApp = null
match proc.env?SSR, proc.env?MODE with
//:: WDS ::
| _, "development" ->
appMain placeholderId theme
//:: Node ::
| "ssr", _ ->
PageList.pages.Keys |> Seq.iter id //for sidefx
let out = renderHtmlString "" ""
runApp <- box out
//:: Broswer ::
| _ ->
appMain placeholderId theme
//UMD
exportDefault runApp
// 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 HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
const util = require('./webpack.config.util')
const localResolve = util.resolve(__dirname);
module.exports = (options) => {
const resolve = util.resolve(options.dirName);
const baseConfig = {
dirName: options.dirName,
// The tags to include the generated JS and CSS will be automatically injected in the HTML template
// See https://github.com/jantimon/html-webpack-plugin
indexHtmlTemplate: localResolve('./webpack.config.ssr.val.js'),
fsharpDefine: options.fsharpDefine,
fsharpEntry: resolve(options.fsharpEntry),
srcDir: resolve('.'),
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
}
},
// 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.find(v => v.indexOf('webpack-dev-server') !== -1);
console.log('Bundling for ' + (isProduction ? 'production' : 'development') + '...');
// 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 webpack.EnvironmentPlugin({
MODE: (isProduction ? 'production' : 'development')
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: baseConfig.indexHtmlTemplate,
inlineSource: '^.+\.(css)$',
}),
new HtmlWebpackInlineSourcePlugin(),
];
return {
// 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].[hash].js' : '[name].js',
filename: isProduction ? '[name].[hash].js' : '[name].js',
},
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map', //'inline-cheap-source-map',
context: baseConfig.srcDir,
optimization: {
minimize: isProduction,
nodeEnv: isProduction ? 'production' : 'development',
splitChunks: {
chunks: 'async',
cacheGroups: {
app: {
test: /[\\/]src[\\/]app[\\/]/i,
name: "app",
reuseExistingChunk: true,
enforce: true,
},
async: {
test: /[\\/]src[\\/]hydrate[\\/]/i,
name: "hydrate",
reuseExistingChunk: true,
enforce: true,
},
},
},
},
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.assetsDir,
baseConfig.outputDir,
'node_modules',
],
},
// 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: 'cache-loader',
}, {
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',
}]
},
{
test: /\.val\.js$/,
use: [{
loader: 'html-loader',
options: {
minimize: true,
attributes: {
root: baseConfig.assetsDir,
list: [
{
tag: 'link',
attribute: 'href',
type: 'src',
filter: util.filter_extracted_css,
}
],
urlFilter: util.urlFilter_ssr,
},
preprocessor: util.extract_css,
}
}, {
loader: 'val-loader',
options: {
config: options,
}
}]
}
]
},
};
};
// Config for Node SSR.
const webpack = require('webpack');
const util = require('./webpack.config.util');
module.exports = (configBase) => {
console.log('Bundling for server-side-rendering...');
const config = configBase;
return ({
entry: {
ssr: config.entry.app,
},
output: {
path: config.output.path,
pathinfo: config.output.pathinfo,
chunkFilename: '[name].js',
filename: '[name].js',
libraryTarget: 'umd',
},
mode: config.mode,
context: config.context,
optimization: {
nodeEnv: config.mode,
minimize: false,
removeAvailableModules: false,
removeEmptyChunks: false,
mergeDuplicateChunks: false,
splitChunks: false,
},
target: 'node',
plugins: ([
new webpack.EnvironmentPlugin({
SSR: 'ssr',
}),
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
}),
]),
resolve: config.resolve,
module: util.replaceLoader(config.module, {
'val-loader': null,
}),
});
};
const CONFIG_BASE = './webpack.config';
const CONFIG_SSR = './webpack.config.ssr';
const path = require('path');
const webpack = require('webpack');
const MemoryFS = require('memory-fs');
const Module = require('module');
const fs = new MemoryFS();
const outputStats = (logger, err, stats) => {
if (err) {
logger.error(err)
return true;
}
const info = stats.toJson('verbose');
(info.errors || []).forEach(logger.error.bind(logger));
(info.warnings || []).forEach(logger.warn.bind(logger));
logger.log(stats.toString({
colors: true
}));
return false;
};
const parentModule = module;
const exec = (code, loaderContext) => {
const { resource, context } = loaderContext;
const module = new Module(resource, parentModule);
module.paths = Module._nodeModulePaths(context);
module.filename = resource;
module._compile(code, resource);
return module.exports;
}
module.exports = (options, loaderContext) => {
const logger = loaderContext.getLogger('SSR')
return new Promise((resolve, reject) => {
logger.log('SSR: Compiling ...');
const configBase = path.join(__dirname, CONFIG_BASE);
const configSSR = path.join(__dirname, CONFIG_SSR);
loaderContext.addDependency(configBase);
loaderContext.addDependency(configSSR);
const webpackConfBase = require(configBase);
const webpackConf = require(configSSR);
const serverCompiler = webpack(webpackConf(webpackConfBase(options.config)));
serverCompiler.outputFileSystem = fs;
serverCompiler.run((err, stats) => {
if (outputStats(logger, err, stats)) {
reject(new Error('Webpack failed.'));
return;
}
const assets = stats.compilation.assets;
const fileDeps = stats.compilation.fileDependencies;
const ctxDeps = stats.compilation.contextDependencies;
const entry = stats.compilation.entrypoints.keys().next().value;
const asset = Object.keys(assets).reduce((_, a) => a.startsWith(entry) ? a : null);
const content = assets[asset].source();
const extras = Object.keys(assets).filter((a) => !a.startsWith(entry));
let modl;
try {
modl = exec(content, loaderContext);
} catch (err) {
reject(new Error(`Failure compiling "${entry}": ${err}`));
return;
}
let output;
try {
output = modl[Object.keys(modl)[0]]();
} catch (err) {
reject(new Error(`Failure running "${entry}": ${err}`))
return;
}
extras.forEach(e => loaderContext.emitFile(e, assets[e].source()));
logger.log('SSR: Done');
resolve({
code: output,
dependencies: fileDeps,
contextDependencies: ctxDeps,
cacheable: true,
})
});
})
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment