@berun/runner-webpack
Version:
Webpack runner for building React web applications
252 lines (225 loc) • 7.95 kB
text/typescript
import * as fs from 'fs'
import * as chalk from 'chalk'
import * as CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'
import * as InterpolateHtmlPlugin from 'react-dev-utils/InterpolateHtmlPlugin'
import * as WatchMissingNodeModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin'
import * as ModuleNotFoundPlugin from 'react-dev-utils/ModuleNotFoundPlugin'
import { DefinePlugin, HotModuleReplacementPlugin, IgnorePlugin } from 'webpack'
import * as ManifestPlugin from 'webpack-manifest-plugin'
import * as ProgressBarPlugin from 'progress-bar-webpack-plugin'
import * as forkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
// PROD ONLY
import * as WorkboxWebpackPlugin from 'workbox-webpack-plugin'
import * as deepmerge from 'deepmerge'
import Berun from '@berun/berun'
const HtmlWebpackPlugin = require('html-webpack-plugin')
/**
* Generates an `index.html` file with the <script> injected.
*/
export const pluginHtml = (
berun: Berun,
options: { html?: any; title?: string; templateContext?: any } = {}
) => {
const ISPRODUCTION = process.env.NODE_ENV === 'production'
const htmlPluginArgs = (deepmerge as any)(
{
inject: true,
template: fs.existsSync(berun.options.paths.appHtml)
? berun.options.paths.appHtml
: null
},
(ISPRODUCTION &&
({
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
}
} as any)) ||
{},
options.html || {}
) as any
if (!htmlPluginArgs.template) {
delete htmlPluginArgs.template
htmlPluginArgs.templateContent = `<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>${options.title || 'BeRun App'}</title>
<style>*{box-sizing:border-box}body{margin:0;font-family:system-ui,sans-serif}</style>
</head>
<body>
<div id="root"></div>
</body>
</html>`
}
berun.webpack.plugin('html').use(HtmlWebpackPlugin, [htmlPluginArgs]).end()
}
/**
* Makes some environment variables available in index.html.
* The public URL is available as %PUBLIC_URL% in index.html, e.g.:
* <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
* In development, this will be an empty string.} berun
*/
export const pluginInterpolateHtml = (berun: Berun, _) => {
berun.webpack
.plugin('interpolate-html')
.use(InterpolateHtmlPlugin, [HtmlWebpackPlugin, berun.options.env])
}
export const pluginProgressBar = (
berun: Berun,
opt: { name?: string; color?: string } = {}
) => {
const { name = 'berun', color = 'green' } = opt
const options = {
width: '24',
complete: '█',
incomplete: chalk.gray('░'),
format: [
chalk[color](`[${name}] :bar`),
chalk[color](':percent'),
chalk.gray(':elapseds :msg')
].join(' '),
summary: false,
customSummary: () => {
/** noop */
}
}
berun.webpack.plugin('progress-bar').use(ProgressBarPlugin, [options])
}
/**
* This gives some necessary context to module not found errors, such as
* the requesting resource.
*/
export const pluginModuleNotFound = (berun: Berun, _) => {
berun.webpack
.plugin('modulenotfound')
.use(ModuleNotFoundPlugin, [berun.options.paths.appPath])
}
/**
* <akes some environment variables available to the JS code, for example:
* if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`.
*/
export const pluginEnv = (berun: Berun, options) => {
const raw = berun.options.env
const stringified = Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key])
return env
}, {})
const processEnv = Object.assign(stringified, options || {})
berun.webpack.plugin('env').use(DefinePlugin, [{ 'process.env': processEnv }])
}
/**
* <akes some environment variables available to the JS code, for example:
* if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`.
*/
export const pluginPackageInfo = (berun: Berun, options) => {
const packageJson = require(berun.options.paths.appPackageJson)
const PACKAGE = {
APP_PATH: JSON.stringify(berun.options.paths.appPath),
WORKSPACE: JSON.stringify(berun.options.paths.workspace),
META_WORKSPACE: JSON.stringify(berun.options.paths.metaWorkspace),
PUBLIC_URL: JSON.stringify(berun.options.paths.publicUrl),
REMOTE_ORIGIN_URL: JSON.stringify(berun.options.paths.remoteOriginUrl),
TITLE: JSON.stringify(packageJson.name || 'BeRun App'),
VERSION: JSON.stringify(packageJson.version),
DIRECTORIES: JSON.stringify(packageJson.directories || {})
}
const raw = berun.options.env
const stringified = Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key])
return env
}, {})
const processEnv = Object.assign(PACKAGE, stringified, options || {})
berun.webpack.plugin('env').use(DefinePlugin, [
{
'process.env': processEnv
}
])
}
/**
* This is necessary to emit hot updates (currently CSS only)
*/
export const pluginHot = (berun: Berun, _) => {
berun.webpack.plugin('hot').use(HotModuleReplacementPlugin)
}
/**
* Watcher doesn't work well if you mistype casing in a path so we use
* a plugin that prints an error when you attempt to do this.
*/
export const pluginCaseSensitivePaths = (berun: Berun, _) => {
berun.webpack.plugin('case-sensitive-paths').use(CaseSensitivePathsPlugin)
}
/**
* If you require a missing module and then `npm install` it, you still have
* to restart the development server for Webpack to discover it. This plugin
* makes the discovery automatic so you don't have to restart.
*/
export const pluginWatchMissingNodeModules = (berun: Berun, _) => {
berun.webpack
.plugin('watch-missing-node-modules')
.use(WatchMissingNodeModulesPlugin, [berun.options.paths.appNodeModules])
}
/**
* Moment.js is an extremely popular library that bundles large locale files
* by default due to how Webpack interprets its code. This is a practical
* solution that requires the user to opt into importing specific locales.
*/
export const pluginMoment = (berun: Berun, _) => {
berun.webpack.plugin('moment').use(IgnorePlugin, [/^\.\/locale$/, /moment$/])
}
/**
* Generate a manifest file which contains a mapping of all asset filenames
* to their corresponding output file so that tools can pick it up without
* having to parse `index.html`.
*/
export const pluginManifest = (berun: Berun, _) => {
berun.webpack.plugin('manifest').use(ManifestPlugin, [
{
fileName: 'asset-manifest.json',
publicPath: berun.options.paths.publicPath
}
])
}
/**
* Generate a service worker script that will precache, and keep up to date,
* the HTML & assets that are part of the Webpack build.
*/
export const pluginWorkbox = (berun: Berun, _) => {
berun.webpack.plugin('workbox').use(WorkboxWebpackPlugin.GenerateSW, [
{
clientsClaim: true,
exclude: [/\.map$/, /asset-manifest\.json$/],
importWorkboxFrom: 'cdn',
navigateFallback: `${berun.options.paths.publicUrl}/index.html`,
navigateFallbackBlacklist: [
// Exclude URLs starting with /_, as they're likely an API call
new RegExp('^/_'),
// Exclude URLs containing a dot, as they're likely a resource in
// public/ and not a SPA route
new RegExp('/[^/]+\\.[^/]+$')
]
}
])
}
/**
* Typescript type checking
*/
export const pluginForkTsChecker = (berun: Berun, _) => {
berun.webpack
.plugin('fork-ts-checker')
.use(forkTsCheckerWebpackPlugin as any, [
{
async: false,
tsconfig: berun.options.paths.appTSConfig,
eslint: false
}
])
}