UNPKG

7.67 kBPlain TextView Raw
1import * as chalk from 'chalk'
2import * as fs from 'fs'
3import * as HtmlWebpackPlugin from 'html-webpack-plugin'
4import * as CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'
5import * as InterpolateHtmlPlugin from 'react-dev-utils/InterpolateHtmlPlugin'
6import * as WatchMissingNodeModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin'
7import * as ModuleNotFoundPlugin from 'react-dev-utils/ModuleNotFoundPlugin'
8import { DefinePlugin, HotModuleReplacementPlugin, IgnorePlugin } from 'webpack'
9import * as ManifestPlugin from 'webpack-manifest-plugin'
10import * as ProgressBarPlugin from 'progress-bar-webpack-plugin'
11import * as forkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
12
13// PROD ONLY
14import * as WorkboxWebpackPlugin from 'workbox-webpack-plugin'
15
16import * as deepmerge from 'deepmerge'
17import Berun from '@berun/berun'
18
19/**
20 * Generates an `index.html` file with the <script> injected.
21 */
22export const pluginHtml = (
23 berun: Berun,
24 options: { html?: any; title?: string; templateContext?: any } = {}
25) => {
26 const ISPRODUCTION = process.env.NODE_ENV === 'production'
27
28 const htmlPluginArgs = (deepmerge as any)(
29 {
30 inject: true,
31 template: fs.existsSync(berun.options.paths.appHtml)
32 ? berun.options.paths.appHtml
33 : null
34 },
35 (ISPRODUCTION &&
36 ({
37 minify: {
38 removeComments: true,
39 collapseWhitespace: true,
40 removeRedundantAttributes: true,
41 useShortDoctype: true,
42 removeEmptyAttributes: true,
43 removeStyleLinkTypeAttributes: true,
44 keepClosingSlash: true,
45 minifyJS: true,
46 minifyCSS: true,
47 minifyURLs: true
48 }
49 } as any)) ||
50 {},
51 options.html || {}
52 ) as any
53
54 if (!htmlPluginArgs.template) {
55 delete htmlPluginArgs.template
56 htmlPluginArgs.templateContent = `<!DOCTYPE html>
57 <html>
58 <head>
59 <meta charset='utf-8'>
60 <title>${options.title || 'BeRun App'}</title>
61 <style>*{box-sizing:border-box}body{margin:0;font-family:system-ui,sans-serif}</style>
62 </head>
63 <body>
64 <div id="root"></div>
65 </body>
66 </html>`
67 }
68
69 berun.webpack
70 .plugin('html')
71 .use(HtmlWebpackPlugin, [htmlPluginArgs])
72 .end()
73}
74
75/**
76 * Makes some environment variables available in index.html.
77 * The public URL is available as %PUBLIC_URL% in index.html, e.g.:
78 * <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
79 * In development, this will be an empty string.} berun
80 */
81export const pluginInterpolateHtml = (berun: Berun, _) => {
82 berun.webpack
83 .plugin('interpolate-html')
84 .use(InterpolateHtmlPlugin, [HtmlWebpackPlugin, berun.options.env.raw])
85}
86
87export const pluginProgressBar = (
88 berun: Berun,
89 opt: { name?: string; color?: string } = {}
90) => {
91 const { name = 'berun', color = 'green' } = opt
92
93 const options = {
94 width: '24',
95 complete: '█',
96 incomplete: chalk.gray('░'),
97 format: [
98 chalk[color](`[${name}] :bar`),
99 chalk[color](':percent'),
100 chalk.gray(':elapseds :msg')
101 ].join(' '),
102 summary: false,
103 customSummary: () => {
104 /** noop */
105 }
106 }
107
108 berun.webpack.plugin('progress-bar').use(ProgressBarPlugin, [options])
109}
110
111/**
112 * This gives some necessary context to module not found errors, such as
113 * the requesting resource.
114 */
115export const pluginModuleNotFound = (berun: Berun, _) => {
116 berun.webpack
117 .plugin('modulenotfound')
118 .use(ModuleNotFoundPlugin, [berun.options.paths.appPath])
119}
120
121/**
122 * <akes some environment variables available to the JS code, for example:
123 * if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`.
124 */
125export const pluginEnv = (berun: Berun, options) => {
126 const processEnv = Object.assign(
127 berun.options.env.stringified['process.env'],
128 options || {}
129 )
130
131 berun.webpack.plugin('env').use(DefinePlugin, [{ 'process.env': processEnv }])
132}
133
134/**
135 * <akes some environment variables available to the JS code, for example:
136 * if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`.
137 */
138export const pluginPackageInfo = (berun: Berun, options) => {
139 const packageJson = require(berun.options.paths.appPackageJson)
140
141 const PACKAGE = {
142 APP_PATH: JSON.stringify(berun.options.paths.appPath),
143 WORKSPACE: JSON.stringify(berun.options.paths.workspace),
144 PUBLIC_URL: JSON.stringify(berun.options.paths.publicUrl),
145 REMOTE_ORIGIN_URL: JSON.stringify(berun.options.paths.remoteOriginUrl),
146 TITLE: JSON.stringify(packageJson.name || 'BeRun App'),
147 VERSION: JSON.stringify(packageJson.version),
148 DIRECTORIES: JSON.stringify(packageJson.directories || {})
149 }
150
151 const processEnv = Object.assign(
152 PACKAGE,
153 berun.options.env.stringified['process.env'],
154 options || {}
155 )
156
157 berun.webpack.plugin('env').use(DefinePlugin, [
158 {
159 'process.env': processEnv
160 }
161 ])
162}
163
164/**
165 * This is necessary to emit hot updates (currently CSS only)
166 */
167export const pluginHot = (berun: Berun, _) => {
168 berun.webpack.plugin('hot').use(HotModuleReplacementPlugin)
169}
170
171/**
172 * Watcher doesn't work well if you mistype casing in a path so we use
173 * a plugin that prints an error when you attempt to do this.
174 */
175export const pluginCaseSensitivePaths = (berun: Berun, _) => {
176 berun.webpack.plugin('case-sensitive-paths').use(CaseSensitivePathsPlugin)
177}
178
179/**
180 * If you require a missing module and then `npm install` it, you still have
181 * to restart the development server for Webpack to discover it. This plugin
182 * makes the discovery automatic so you don't have to restart.
183 */
184export const pluginWatchMissingNodeModules = (berun: Berun, _) => {
185 berun.webpack
186 .plugin('watch-missing-node-modules')
187 .use(WatchMissingNodeModulesPlugin, [berun.options.paths.appNodeModules])
188}
189
190/**
191 * Moment.js is an extremely popular library that bundles large locale files
192 * by default due to how Webpack interprets its code. This is a practical
193 * solution that requires the user to opt into importing specific locales.
194 */
195export const pluginMoment = (berun: Berun, _) => {
196 berun.webpack.plugin('moment').use(IgnorePlugin, [/^\.\/locale$/, /moment$/])
197}
198
199/**
200 * Generate a manifest file which contains a mapping of all asset filenames
201 * to their corresponding output file so that tools can pick it up without
202 * having to parse `index.html`.
203 */
204export const pluginManifest = (berun: Berun, _) => {
205 berun.webpack.plugin('manifest').use(ManifestPlugin, [
206 {
207 fileName: 'asset-manifest.json',
208 publicPath: berun.options.paths.publicPath
209 }
210 ])
211}
212
213/**
214 * Generate a service worker script that will precache, and keep up to date,
215 * the HTML & assets that are part of the Webpack build.
216 */
217export const pluginWorkbox = (berun: Berun, _) => {
218 berun.webpack.plugin('workbox').use(WorkboxWebpackPlugin.GenerateSW, [
219 {
220 clientsClaim: true,
221 exclude: [/\.map$/, /asset-manifest\.json$/],
222 importWorkboxFrom: 'cdn',
223 navigateFallback: `${berun.options.paths.publicUrl}/index.html`,
224 navigateFallbackBlacklist: [
225 // Exclude URLs starting with /_, as they're likely an API call
226 new RegExp('^/_'),
227 // Exclude URLs containing a dot, as they're likely a resource in
228 // public/ and not a SPA route
229 new RegExp('/[^/]+\\.[^/]+$')
230 ]
231 }
232 ])
233}
234
235/**
236 * Typescript type checking
237 */
238export const pluginForkTsChecker = (berun: Berun, _) => {
239 berun.webpack
240 .plugin('fork-ts-checker')
241 .use(forkTsCheckerWebpackPlugin as any, [
242 {
243 async: false,
244 tsconfig: berun.options.paths.appTSConfig,
245 eslint: false
246 }
247 ])
248}