UNPKG

16.4 kBPlain TextView Raw
1/* eslint-disable prefer-const */
2/* eslint-disable @typescript-eslint/no-shadow */
3/* eslint-disable @typescript-eslint/ban-types */
4import path from 'path'
5import autoprefixer from 'autoprefixer'
6import { loadConfig } from 'browserslist'
7import CopyWebpackPlugin from 'copy-webpack-plugin'
8import FaviconsWebpackPlugin from 'favicons-webpack-plugin'
9import HtmlWebpackPlugin from 'html-webpack-plugin'
10import MiniCssExtractPlugin from 'mini-css-extract-plugin'
11import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
12import TerserPlugin from 'terser-webpack-plugin'
13import webpack, { RuleSetRule, RuleSetUseItem } from 'webpack'
14import UnusedFilesWebpackPlugin from '@4c/unused-files-webpack-plugin'
15import builtinPlugins from './plugins'
16import statsConfig, { StatsOptions } from './stats'
17import type { FaviconWebpackPlugionOptions } from 'favicons-webpack-plugin/src/options'
18
19export type { FaviconWebpackPlugionOptions, HtmlWebpackPlugin }
20
21export type Env = 'production' | 'test' | 'development'
22
23export type LoaderResolver<T extends {}> = (options?: T) => RuleSetUseItem
24
25type Rule = RuleSetRule
26
27export type RuleFactory<T extends {} = {}> = (options?: T) => Rule
28
29export type ContextualRuleFactory<T = {}> = RuleFactory<T> & {
30 internal: RuleFactory<T>
31 external: RuleFactory<T>
32}
33
34export interface AstroTurfOptions {
35 getFileName?(path: string, opts: AstroTurfOptions, id: string): string
36 allowGlobal?: boolean
37 extension?: string
38 tagName?: string
39 styleTag?: string
40 useAltLoader?: boolean
41 enableCssProp?: boolean
42}
43
44export type AstroturfRuleFactory = RuleFactory<AstroTurfOptions> & {
45 sass: RuleFactory<AstroTurfOptions>
46 less: RuleFactory<AstroTurfOptions>
47}
48
49type PluginInstance = any
50type PluginFactory = (...args: any) => PluginInstance
51
52type BuiltinPlugins = typeof builtinPlugins
53
54type StatAtoms = {
55 none: StatsOptions
56 minimal: StatsOptions
57}
58
59export type WebpackAtomsOptions = {
60 babelConfig?: {}
61 browsers?: string[]
62 vendorRegex?: RegExp
63 env?: Env | null
64 assetRelativeRoot?: string
65 disableMiniExtractInDev?: boolean
66 ignoreBrowserslistConfig?: boolean
67}
68
69export type LoaderAtoms = {
70 json: LoaderResolver<any>
71 yaml: LoaderResolver<any>
72 null: LoaderResolver<any>
73 raw: LoaderResolver<any>
74
75 style: LoaderResolver<any>
76 css: LoaderResolver<any>
77 miniCssExtract: LoaderResolver<
78 {
79 disable?: boolean
80 fallback?: RuleSetUseItem
81 } & MiniCssExtractPlugin.PluginOptions
82 >
83 astroturf: LoaderResolver<any>
84 postcss: LoaderResolver<{
85 browsers?: string[]
86 postcssOptions?:
87 | Record<string, any>
88 | ((...args: any[]) => Record<string, any>)
89 }>
90 less: LoaderResolver<any>
91 sass: LoaderResolver<any>
92
93 file: LoaderResolver<any>
94 url: LoaderResolver<any>
95 js: LoaderResolver<any>
96
97 imports: LoaderResolver<any>
98 exports: LoaderResolver<any>
99}
100
101type JsRule = RuleFactory<any> & {
102 inlineCss: RuleFactory<any>
103}
104
105export type RuleAtoms = {
106 js: JsRule
107 yaml: RuleFactory<any>
108 fonts: RuleFactory<any>
109 images: RuleFactory<any>
110 audioVideo: RuleFactory<any>
111 files: RuleFactory<any>
112
113 css: ContextualRuleFactory
114 postcss: ContextualRuleFactory
115 less: ContextualRuleFactory
116 sass: ContextualRuleFactory
117
118 astroturf: AstroturfRuleFactory
119}
120
121export type PluginAtoms = BuiltinPlugins & {
122 define: PluginFactory
123 extractCss: PluginFactory
124 html: PluginFactory
125 loaderOptions: PluginFactory
126 moment: PluginFactory
127 minifyJs: PluginFactory
128 minifyCss: PluginFactory
129 unusedFiles: PluginFactory
130 favicons: PluginFactory
131 copy: PluginFactory
132}
133
134export type WebpackAtoms = {
135 loaders: LoaderAtoms
136
137 rules: RuleAtoms
138
139 plugins: PluginAtoms
140
141 stats: StatAtoms
142
143 makeExternalOnly: (original: RuleFactory<any>) => RuleFactory<any>
144 makeInternalOnly: (original: RuleFactory<any>) => RuleFactory<any>
145 makeExtractLoaders: (
146 options: { extract?: boolean },
147 config: { fallback: RuleSetUseItem; use: RuleSetUseItem[] },
148 ) => RuleSetUseItem[]
149}
150
151const VENDOR_MODULE_REGEX = /node_modules/
152const DEFAULT_BROWSERS = ['> 1%', 'Firefox ESR', 'not ie < 9']
153
154function createAtoms(options: WebpackAtomsOptions = {}): WebpackAtoms {
155 let {
156 babelConfig = {},
157 assetRelativeRoot = '',
158 env = process.env.NODE_ENV,
159 vendorRegex = VENDOR_MODULE_REGEX,
160 disableMiniExtractInDev = true,
161 ignoreBrowserslistConfig = false,
162 browsers: supportedBrowsers,
163 } = options
164
165 const hasBrowsersListConfig = !!loadConfig({ path: path.resolve('.') })
166
167 if (ignoreBrowserslistConfig || !hasBrowsersListConfig) {
168 supportedBrowsers = supportedBrowsers || DEFAULT_BROWSERS
169 }
170
171 const makeExternalOnly = (original: RuleFactory<any>) => (
172 options = {},
173 ): Rule => {
174 const rule = original(options)
175 rule.include = vendorRegex
176 return rule
177 }
178
179 const makeInternalOnly = (original: RuleFactory<any>) => (
180 options = {},
181 ): Rule => {
182 const rule = original(options)
183 rule.exclude = vendorRegex
184 return rule
185 }
186
187 const makeContextual = <T>(
188 rule: RuleFactory<T>,
189 ): ContextualRuleFactory<T> => {
190 return Object.assign(rule, {
191 external: makeExternalOnly(rule),
192 internal: makeInternalOnly(rule),
193 })
194 }
195
196 const makeExtractLoaders = (
197 { extract }: { extract?: boolean } = {},
198 config: { fallback: RuleSetUseItem; use: RuleSetUseItem[] },
199 ): RuleSetUseItem[] => [
200 // eslint-disable-next-line @typescript-eslint/no-use-before-define
201 loaders.miniCssExtract({
202 fallback: config.fallback,
203 disable: extract == undefined ? extract : !extract,
204 }),
205 ...config.use,
206 ]
207
208 const PRODUCTION = env === 'production'
209
210 /**
211 * Loaders
212 */
213 const loaders: LoaderAtoms = {
214 json: () => ({
215 loader: require.resolve('json-loader'),
216 }),
217
218 yaml: () => ({
219 loader: require.resolve('yaml-loader'),
220 }),
221
222 null: () => ({
223 loader: require.resolve('null-loader'),
224 }),
225
226 raw: () => ({
227 loader: require.resolve('raw-loader'),
228 }),
229
230 style: () => ({
231 loader: require.resolve('style-loader'),
232 }),
233
234 miniCssExtract: (opts = {}) => {
235 const {
236 disable = !PRODUCTION && disableMiniExtractInDev,
237 fallback,
238 ...options
239 } = opts!
240
241 return disable
242 ? fallback || loaders.style()
243 : { loader: MiniCssExtractPlugin.loader, options }
244 },
245
246 css: (options = {}) => ({
247 loader: require.resolve('css-loader'),
248 options: {
249 sourceMap: !PRODUCTION,
250 ...options,
251 modules: options.modules
252 ? {
253 // https://github.com/webpack-contrib/css-loader/issues/406
254 localIdentName: '[name]--[local]--[hash:base64:5]',
255 exportLocalsConvention: 'dashes',
256 ...options.modules,
257 }
258 : false,
259 },
260 }),
261
262 astroturf: (options) => ({
263 options: { extension: '.module.css', ...options },
264 loader: require.resolve('astroturf/loader'),
265 }),
266
267 postcss: (options = {}) => {
268 const { postcssOptions, browsers = supportedBrowsers, ...rest } = options
269 const loader = require.resolve('postcss-loader')
270
271 return {
272 loader,
273 options: {
274 ...rest,
275 postcssOptions: (...args) => {
276 const postcssOpts =
277 typeof postcssOptions === `function`
278 ? postcssOptions(...args)
279 : postcssOptions
280
281 const plugins = postcssOpts?.plugins ?? []
282
283 return {
284 ...postcssOpts,
285 plugins: [
286 ...plugins,
287 // overrideBrowserslist is only set when browsers is explicit
288 autoprefixer({
289 overrideBrowserslist: browsers,
290 flexbox: `no-2009`,
291 }),
292 ],
293 }
294 },
295 },
296 }
297 },
298
299 less: (options = {}) => ({
300 options,
301 loader: require.resolve('less-loader'),
302 }),
303
304 sass: (options = {}) => ({
305 options,
306 loader: require.resolve('sass-loader'),
307 }),
308
309 file: (options = {}) => ({
310 loader: require.resolve('file-loader'),
311 options: {
312 name: `${assetRelativeRoot}[name]-[hash].[ext]`,
313 ...options,
314 },
315 }),
316
317 url: (options = {}) => ({
318 loader: require.resolve('url-loader'),
319 options: {
320 limit: 10000,
321 name: `${assetRelativeRoot}[name]-[hash].[ext]`,
322 ...options,
323 },
324 }),
325
326 js: (options = babelConfig) => ({
327 options,
328 loader: require.resolve('babel-loader'),
329 }),
330
331 imports: (options = {}) => ({
332 options,
333 loader: require.resolve('imports-loader'),
334 }),
335
336 exports: (options = {}) => ({
337 options,
338 loader: require.resolve('exports-loader'),
339 }),
340 }
341
342 /**
343 * Rules
344 */
345 const rules: any = {}
346
347 /**
348 * Javascript loader via babel, excludes node_modules
349 */
350 {
351 const js = (options = {}) => ({
352 test: /\.(j|t)sx?$/,
353 exclude: vendorRegex,
354 use: [loaders.js(options)],
355 })
356
357 rules.js = js
358 }
359
360 rules.yaml = () => ({
361 test: /\.ya?ml/,
362 use: [loaders.json(), loaders.yaml()],
363 })
364
365 /**
366 * Font loader
367 */
368 rules.fonts = () => ({
369 type: 'asset',
370 test: /\.(eot|otf|ttf|woff(2)?)(\?.*)?$/,
371 parser: {
372 dataUrlCondition: {
373 maxSize: 10000,
374 }
375 },
376 generator: {
377 filename: `${assetRelativeRoot}[name]-[hash].[ext]`,
378 },
379 })
380
381 /**
382 * Loads image assets, inlines images via a data URI if they are below
383 * the size threshold
384 */
385 rules.images = () => ({
386 type: 'asset',
387 test: /\.(ico|svg|jpg|jpeg|png|gif|webp)(\?.*)?$/,
388 parser: {
389 dataUrlCondition: {
390 maxSize: 10000,
391 }
392 },
393 generator: {
394 filename: `${assetRelativeRoot}[name]-[hash].[ext]`,
395 },
396 })
397
398 /**
399 * Loads audio or video assets
400 */
401 rules.audioVideo = () => ({
402 type: 'asset/resource',
403 test: /\.(mp4|webm|wav|mp3|m4a|aac|oga|flac)$/,
404 generator: {
405 filename: `${assetRelativeRoot}[name]-[hash].[ext]`,
406 },
407 })
408
409 /**
410 * A catch-all rule for everything that isn't js, json, or html.
411 * Should only be used in the context of a webpack `oneOf` rule as a fallback
412 * (see rules.assets())
413 */
414 rules.files = () => ({
415 // Exclude `js` files to keep "css" loader working as it injects
416 // it's runtime that would otherwise processed through "file" loader.
417 // Also exclude `html` and `json` extensions so they get processed
418 // by webpacks internal loaders.
419 exclude: [/\.jsx?$/, /\.html$/, /\.json$/],
420 type: 'asset/resource',
421 generator: {
422 filename: `${assetRelativeRoot}[name]-[hash].[ext]`,
423 },
424 })
425
426 /**
427 * Astroturf loader.
428 */
429 {
430 const astroturf = (options = {}) => ({
431 test: /\.(j|t)sx?$/,
432 use: [loaders.astroturf(options)],
433 })
434
435 Object.assign(astroturf, {
436 sass: (opts) => astroturf({ extension: '.module.scss', ...opts }),
437 less: (opts) => astroturf({ extension: '.module.less', ...opts }),
438 })
439
440 rules.astroturf = astroturf as AstroturfRuleFactory
441 }
442 /**
443 * CSS style loader.
444 */
445 {
446 const css = ({ browsers, extract, ...options }: any = {}) => ({
447 test: /\.css$/,
448 use: makeExtractLoaders(
449 { extract },
450 {
451 fallback: loaders.style(),
452 use: [
453 loaders.css({ ...options, importLoaders: 1 }),
454 loaders.postcss({ browsers }),
455 ],
456 },
457 ),
458 })
459
460 rules.css = makeContextual(({ modules = true, ...opts }: any = {}) => ({
461 oneOf: [
462 { ...css({ ...opts, modules }), test: /\.module\.css$/ },
463 css(opts),
464 ],
465 }))
466 }
467
468 /**
469 * PostCSS loader.
470 */
471 {
472 const postcss = ({ modules, extract, ...opts }: any = {}) => ({
473 test: /\.css$/,
474 use: makeExtractLoaders(
475 { extract },
476 {
477 fallback: loaders.style(),
478 use: [
479 loaders.css({ importLoaders: 1, modules }),
480 loaders.postcss(opts),
481 ],
482 },
483 ),
484 })
485
486 rules.postcss = makeContextual(({ modules = true, ...opts }: any = {}) => ({
487 oneOf: [
488 { ...postcss({ ...opts, modules }), test: /\.module\.css$/ },
489 postcss(opts),
490 ],
491 }))
492 }
493
494 /**
495 * Less style loader.
496 */
497 {
498 const less = ({ modules, browsers, extract, ...options }: any = {}) => ({
499 test: /\.less$/,
500 use: makeExtractLoaders(
501 { extract },
502 {
503 fallback: loaders.style(),
504 use: [
505 loaders.css({ importLoaders: 2, modules }),
506 loaders.postcss({ browsers }),
507 loaders.less(options),
508 ],
509 },
510 ),
511 })
512
513 rules.less = makeContextual(({ modules = true, ...opts }: any = {}) => ({
514 oneOf: [
515 { ...less({ ...opts, modules }), test: /\.module\.less$/ },
516 less(opts),
517 ],
518 }))
519 }
520
521 /**
522 * SASS style loader, excludes node_modules.
523 */
524 {
525 const sass = ({ browsers, modules, extract, ...options }: any = {}) => ({
526 test: /\.s(a|c)ss$/,
527 use: makeExtractLoaders(
528 { extract },
529 {
530 fallback: loaders.style(),
531 use: [
532 loaders.css({ importLoaders: 2, modules }),
533 loaders.postcss({ browsers }),
534 loaders.sass(options),
535 ],
536 },
537 ),
538 })
539
540 rules.sass = makeContextual(({ modules = true, ...opts }: any = {}) => ({
541 oneOf: [
542 { ...sass({ ...opts, modules }), test: /\.module\.s(a|c)ss$/ },
543 sass(opts),
544 ],
545 }))
546 }
547
548 /**
549 * Plugins
550 */
551 const plugins: PluginAtoms = {
552 ...builtinPlugins,
553 /**
554 * https://webpack.js.org/plugins/define-plugin/
555 *
556 * Replace tokens in code with static values. Defaults to setting NODE_ENV
557 * which is used by React and other libraries to toggle development mode.
558 */
559 define: (defines = {}) =>
560 new webpack.DefinePlugin({
561 // eslint-disable-next-line @typescript-eslint/naming-convention
562 'process.env.NODE_ENV': JSON.stringify(env),
563 ...defines,
564 }),
565 /**
566 * Minify javascript code without regard for IE8. Attempts
567 * to parallelize the work to save time. Generally only add in Production
568 */
569 /**
570 * Minify javascript code without regard for IE8. Attempts
571 * to parallelize the work to save time. Generally only add in Production
572 */
573 minifyJs: ({ terserOptions, ...options }: any = {}) =>
574 new TerserPlugin({
575 parallel: true,
576 exclude: /\.min\.js/,
577 terserOptions: {
578 ecma: 8,
579 ie8: false,
580 ...terserOptions,
581 },
582 ...options,
583 }),
584
585 /**
586 * Extracts css requires into a single file;
587 * includes some reasonable defaults
588 */
589 extractCss: (options) =>
590 new MiniCssExtractPlugin({
591 filename: '[name]-[contenthash].css',
592 ...options,
593 }),
594
595 minifyCss: (options = {}) => new CssMinimizerPlugin(options),
596
597 /**
598 * Generates an html file that includes the output bundles.
599 * Sepecify a `title` option to set the page title.
600 */
601 html: (options?: HtmlWebpackPlugin.Options | undefined) =>
602 new HtmlWebpackPlugin({
603 inject: true,
604 template: path.join(__dirname, '../assets/index.html'),
605 ...options,
606 }),
607
608 moment: () =>
609 new webpack.IgnorePlugin({
610 contextRegExp: /^\.\/locale$/,
611 resourceRegExp: /moment$/,
612 }),
613
614 copy: (...args) => new CopyWebpackPlugin(...args),
615 unusedFiles: (...args) => new UnusedFilesWebpackPlugin(...args),
616 favicons: (args: string | FaviconWebpackPlugionOptions) =>
617 new FaviconsWebpackPlugin(args),
618 }
619
620 const stats: StatAtoms = {
621 none: statsConfig,
622 minimal: {
623 ...statsConfig,
624 errors: true,
625 errorDetails: true,
626 assets: true,
627 chunks: true,
628 colors: true,
629 performance: true,
630 timings: true,
631 warnings: true,
632 },
633 }
634
635 return {
636 loaders,
637 rules: rules as RuleAtoms,
638 plugins: plugins as PluginAtoms,
639 stats,
640
641 makeExternalOnly,
642 makeInternalOnly,
643 makeExtractLoaders,
644 }
645}
646
647const {
648 makeExternalOnly,
649 makeInternalOnly,
650 makeExtractLoaders,
651 stats,
652 loaders,
653 rules,
654 plugins,
655} = createAtoms()
656
657export {
658 makeExternalOnly,
659 makeInternalOnly,
660 makeExtractLoaders,
661 loaders,
662 rules,
663 plugins,
664 stats,
665 createAtoms,
666}