1 |
|
2 |
|
3 |
|
4 | import path from 'path'
|
5 | import autoprefixer from 'autoprefixer'
|
6 | import { loadConfig } from 'browserslist'
|
7 | import CopyWebpackPlugin from 'copy-webpack-plugin'
|
8 | import FaviconsWebpackPlugin from 'favicons-webpack-plugin'
|
9 | import HtmlWebpackPlugin from 'html-webpack-plugin'
|
10 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'
|
11 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
|
12 | import TerserPlugin from 'terser-webpack-plugin'
|
13 | import webpack, { RuleSetRule, RuleSetUseItem } from 'webpack'
|
14 | import UnusedFilesWebpackPlugin from '@4c/unused-files-webpack-plugin'
|
15 | import builtinPlugins from './plugins'
|
16 | import statsConfig, { StatsOptions } from './stats'
|
17 | import type { FaviconWebpackPlugionOptions } from 'favicons-webpack-plugin/src/options'
|
18 |
|
19 | export type { FaviconWebpackPlugionOptions, HtmlWebpackPlugin }
|
20 |
|
21 | export type Env = 'production' | 'test' | 'development'
|
22 |
|
23 | export type LoaderResolver<T extends {}> = (options?: T) => RuleSetUseItem
|
24 |
|
25 | type Rule = RuleSetRule
|
26 |
|
27 | export type RuleFactory<T extends {} = {}> = (options?: T) => Rule
|
28 |
|
29 | export type ContextualRuleFactory<T = {}> = RuleFactory<T> & {
|
30 | internal: RuleFactory<T>
|
31 | external: RuleFactory<T>
|
32 | }
|
33 |
|
34 | export 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 |
|
44 | export type AstroturfRuleFactory = RuleFactory<AstroTurfOptions> & {
|
45 | sass: RuleFactory<AstroTurfOptions>
|
46 | less: RuleFactory<AstroTurfOptions>
|
47 | }
|
48 |
|
49 | type PluginInstance = any
|
50 | type PluginFactory = (...args: any) => PluginInstance
|
51 |
|
52 | type BuiltinPlugins = typeof builtinPlugins
|
53 |
|
54 | type StatAtoms = {
|
55 | none: StatsOptions
|
56 | minimal: StatsOptions
|
57 | }
|
58 |
|
59 | export 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 |
|
69 | export 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 |
|
101 | type JsRule = RuleFactory<any> & {
|
102 | inlineCss: RuleFactory<any>
|
103 | }
|
104 |
|
105 | export 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 |
|
121 | export 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 |
|
134 | export 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 |
|
151 | const VENDOR_MODULE_REGEX = /node_modules/
|
152 | const DEFAULT_BROWSERS = ['> 1%', 'Firefox ESR', 'not ie < 9']
|
153 |
|
154 | function 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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
344 |
|
345 | const rules: any = {}
|
346 |
|
347 | |
348 |
|
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 |
|
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 |
|
383 |
|
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 |
|
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 |
|
411 |
|
412 |
|
413 |
|
414 | rules.files = () => ({
|
415 |
|
416 |
|
417 |
|
418 |
|
419 | exclude: [/\.jsx?$/, /\.html$/, /\.json$/],
|
420 | type: 'asset/resource',
|
421 | generator: {
|
422 | filename: `${assetRelativeRoot}[name]-[hash].[ext]`,
|
423 | },
|
424 | })
|
425 |
|
426 | |
427 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
550 |
|
551 | const plugins: PluginAtoms = {
|
552 | ...builtinPlugins,
|
553 | |
554 |
|
555 |
|
556 |
|
557 |
|
558 |
|
559 | define: (defines = {}) =>
|
560 | new webpack.DefinePlugin({
|
561 |
|
562 | 'process.env.NODE_ENV': JSON.stringify(env),
|
563 | ...defines,
|
564 | }),
|
565 | |
566 |
|
567 |
|
568 |
|
569 | |
570 |
|
571 |
|
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 |
|
587 |
|
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 |
|
599 |
|
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 |
|
647 | const {
|
648 | makeExternalOnly,
|
649 | makeInternalOnly,
|
650 | makeExtractLoaders,
|
651 | stats,
|
652 | loaders,
|
653 | rules,
|
654 | plugins,
|
655 | } = createAtoms()
|
656 |
|
657 | export {
|
658 | makeExternalOnly,
|
659 | makeInternalOnly,
|
660 | makeExtractLoaders,
|
661 | loaders,
|
662 | rules,
|
663 | plugins,
|
664 | stats,
|
665 | createAtoms,
|
666 | }
|