UNPKG

@berun/runner-webpack

Version:

Webpack runner for building React web applications

252 lines (225 loc) 7.95 kB
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 } ]) }