UNPKG

18.6 kBJavaScriptView Raw
1import webpack from 'webpack';
2import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
3import SystemBellWebpackPlugin from 'system-bell-webpack-plugin';
4import ExtractTextPlugin from 'extract-text-webpack-plugin';
5import ManifestPlugin from 'webpack-manifest-plugin';
6import SWPrecacheWebpackPlugin from 'sw-precache-webpack-plugin';
7import autoprefixer from 'autoprefixer';
8import { dirname, resolve, join, extname } from 'path';
9import { existsSync } from 'fs';
10import eslintFormatter from 'react-dev-utils/eslintFormatter';
11import assert from 'assert';
12import deprecate from 'deprecate';
13import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
14import CopyWebpackPlugin from 'copy-webpack-plugin';
15import HTMLWebpackPlugin from 'html-webpack-plugin';
16import ProgressPlugin from 'progress-bar-webpack-plugin';
17import { sync as resolveSync } from 'resolve';
18import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
19import HardSourceWebpackPlugin from 'hard-source-webpack-plugin';
20import uglifyJSConfig from './defaultConfigs/uglifyJS';
21import babelConfig from './defaultConfigs/babel';
22import defaultBrowsers from './defaultConfigs/browsers';
23import stringifyObject from './stringifyObject';
24import normalizeTheme from './normalizeTheme';
25import { applyWebpackConfig } from './applyWebpackConfig';
26import readRc from './readRc';
27import { stripLastSlash } from './utils';
28import { getPkgPath, shouldTransform } from './es5ImcompatibleVersions';
29
30const { TsConfigPathsPlugin } = require('awesome-typescript-loader'); // eslint-disable-line
31const debug = require('debug')('af-webpack:getConfig');
32
33if (process.env.DISABLE_TSLINT) {
34 deprecate('DISABLE_TSLINT is deprecated, use TSLINT=none instead');
35}
36if (process.env.DISABLE_ESLINT) {
37 deprecate('DISABLE_ESLINT is deprecated, use ESLINT=none instead');
38}
39if (process.env.NO_COMPRESS) {
40 deprecate('NO_COMPRESS is deprecated, use COMPRESS=none instead');
41}
42
43export default function getConfig(opts = {}) {
44 assert(opts.cwd, 'opts.cwd must be specified');
45
46 const isDev = process.env.NODE_ENV === 'development';
47 const theme = normalizeTheme(opts.theme);
48 const postcssOptions = {
49 // Necessary for external CSS imports to work
50 // https://github.com/facebookincubator/create-react-app/issues/2677
51 ident: 'postcss',
52 plugins: () => [
53 require('postcss-flexbugs-fixes'), // eslint-disable-line
54 autoprefixer({
55 browsers: opts.browserslist || defaultBrowsers,
56 flexbox: 'no-2009',
57 }),
58 ...(opts.extraPostCSSPlugins ? opts.extraPostCSSPlugins : []),
59 ],
60 };
61 const cssModulesConfig = {
62 modules: true,
63 localIdentName: isDev
64 ? '[name]__[local]___[hash:base64:5]'
65 : '[local]___[hash:base64:5]',
66 };
67 const lessOptions = {
68 modifyVars: theme,
69 ...(opts.lessLoaderOptions || {}),
70 };
71 const cssOptions = {
72 importLoaders: 1,
73 ...(isDev
74 ? {}
75 : {
76 minimize: !(
77 process.env.CSS_COMPRESS === 'none' ||
78 process.env.COMPRESS === 'none' ||
79 process.env.NO_COMPRESS
80 )
81 ? {
82 // ref: https://github.com/umijs/umi/issues/164
83 minifyFontValues: false,
84 }
85 : false,
86 sourceMap: !opts.disableCSSSourceMap,
87 }),
88 ...(opts.cssLoaderOptions || {}),
89 };
90
91 function getCSSLoader(opts = {}) {
92 const { cssModules, less, sass, sassOptions } = opts;
93
94 let hasSassLoader = true;
95 try {
96 require.resolve('sass-loader');
97 } catch (e) {
98 hasSassLoader = false;
99 }
100
101 return [
102 require.resolve('style-loader'),
103 {
104 loader: require.resolve('css-loader'),
105 options: {
106 ...cssOptions,
107 ...(cssModules ? cssModulesConfig : {}),
108 },
109 },
110 {
111 loader: require.resolve('postcss-loader'),
112 options: postcssOptions,
113 },
114 ...(less
115 ? [
116 {
117 loader: require.resolve('less-loader'),
118 options: lessOptions,
119 },
120 ]
121 : []),
122 ...(sass && hasSassLoader
123 ? [
124 {
125 loader: require.resolve('sass-loader'),
126 options: sassOptions,
127 },
128 ]
129 : []),
130 ];
131 }
132
133 function exclude(filePath) {
134 if (/node_modules/.test(filePath)) {
135 return true;
136 }
137 if (opts.cssModulesWithAffix) {
138 if (/\.module\.(css|less|sass|scss)$/.test(filePath)) return true;
139 }
140 if (opts.cssModulesExcludes) {
141 for (const exclude of opts.cssModulesExcludes) {
142 if (filePath.indexOf(exclude) > -1) return true;
143 }
144 }
145 }
146
147 const cssRules = [
148 ...(opts.cssModulesExcludes
149 ? opts.cssModulesExcludes.map(file => {
150 return {
151 test(filePath) {
152 return filePath.indexOf(file) > -1;
153 },
154 use: getCSSLoader({
155 less: extname(file).toLowerCase() === '.less',
156 sass:
157 extname(file).toLowerCase() === '.sass' ||
158 extname(file).toLowerCase() === '.scss',
159 sassOptions: opts.sass,
160 }),
161 };
162 })
163 : []),
164 ...(opts.cssModulesWithAffix
165 ? [
166 {
167 test: /\.module\.css$/,
168 use: getCSSLoader({
169 cssModules: true,
170 }),
171 },
172 {
173 test: /\.module\.less$/,
174 use: getCSSLoader({
175 cssModules: true,
176 less: true,
177 }),
178 },
179 {
180 test: /\.module\.(sass|scss)$/,
181 use: getCSSLoader({
182 cssModules: true,
183 sass: true,
184 sassOptions: opts.sass,
185 }),
186 },
187 ]
188 : []),
189 {
190 test: /\.css$/,
191 exclude,
192 use: getCSSLoader({
193 cssModules: !opts.disableCSSModules,
194 }),
195 },
196 {
197 test: /\.css$/,
198 include: /node_modules/,
199 use: getCSSLoader(),
200 },
201 {
202 test: /\.less$/,
203 exclude,
204 use: getCSSLoader({
205 cssModules: !opts.disableCSSModules,
206 less: true,
207 }),
208 },
209 {
210 test: /\.less$/,
211 include: /node_modules/,
212 use: getCSSLoader({
213 less: true,
214 }),
215 },
216 {
217 test: /\.(sass|scss)$/,
218 exclude,
219 use: getCSSLoader({
220 cssModules: !opts.disableCSSModules,
221 sass: true,
222 sassOptions: opts.sass,
223 }),
224 },
225 {
226 test: /\.(sass|scss)$/,
227 include: /node_modules/,
228 use: getCSSLoader({
229 sass: true,
230 sassOptions: opts.sass,
231 }),
232 },
233 ];
234
235 // 生成环境下用 ExtractTextPlugin 提取出来
236 if (!isDev) {
237 cssRules.forEach(rule => {
238 rule.use = ExtractTextPlugin.extract({
239 use: rule.use.slice(1),
240 });
241 });
242 }
243
244 // TODO: 根据 opts.hash 自动处理这里的 filename
245 const commonsPlugins = (opts.commons || []).map(common => {
246 return new webpack.optimize.CommonsChunkPlugin(common);
247 });
248
249 // Declare outputPath here for reuse
250 const outputPath = opts.outputPath
251 ? resolve(opts.cwd, opts.outputPath)
252 : resolve(opts.cwd, 'dist');
253
254 // Copy files in public to outputPath
255 const copyPlugins = opts.copy ? [new CopyWebpackPlugin(opts.copy)] : [];
256 if (existsSync(resolve(opts.cwd, 'public'))) {
257 copyPlugins.push(
258 new CopyWebpackPlugin(
259 [
260 {
261 from: resolve(opts.cwd, 'public'),
262 to: outputPath,
263 toType: 'dir',
264 },
265 ],
266 { ignore: ['**/.*'] },
267 ),
268 );
269 }
270
271 // js 和 css 采用不同的 hash 算法
272 const jsHash = !isDev && opts.hash ? '.[chunkhash:8]' : '';
273 const cssHash = !isDev && opts.hash ? '.[contenthash:8]' : '';
274
275 const babelOptions = {
276 ...(opts.babel || babelConfig),
277 cacheDirectory: process.env.BABEL_CACHE !== 'none',
278 babelrc: !!process.env.BABELRC,
279 };
280 babelOptions.plugins = [
281 ...(babelOptions.plugins || []),
282 ...(opts.disableDynamicImport
283 ? [require.resolve('babel-plugin-dynamic-import-node-sync')]
284 : []),
285 ];
286 const babelUse = [
287 {
288 loader: require('path').join(__dirname, 'debugLoader.js'), // eslint-disable-line
289 },
290 {
291 loader: require.resolve('babel-loader'),
292 options: babelOptions,
293 },
294 ];
295 const babelOptionsDeps = {
296 presets: [
297 [
298 require.resolve('babel-preset-umi'),
299 {
300 disableTransform: true,
301 },
302 ],
303 ],
304 cacheDirectory: process.env.BABEL_CACHE !== 'none',
305 babelrc: !!process.env.BABELRC,
306 };
307 const babelUseDeps = [
308 {
309 loader: require('path').join(__dirname, 'debugLoader.js'), // eslint-disable-line
310 },
311 {
312 loader: require.resolve('babel-loader'),
313 options: babelOptionsDeps,
314 },
315 ];
316
317 const eslintOptions = {
318 formatter: eslintFormatter,
319 baseConfig: {
320 extends: [require.resolve('eslint-config-umi')],
321 },
322 ignore: false,
323 eslintPath: require.resolve('eslint'),
324 useEslintrc: false,
325 };
326
327 // 用用户的 eslint
328 try {
329 const { dependencies, devDependencies } = require(resolve('package.json')); // eslint-disable-line
330 if (dependencies.eslint || devDependencies.eslint) {
331 const eslintPath = resolveSync('eslint', {
332 basedir: opts.cwd,
333 });
334 eslintOptions.eslintPath = eslintPath;
335 debug(`use user's eslint bin: ${eslintPath}`);
336 }
337 } catch (e) {
338 // do nothing
339 }
340
341 // 读用户的 eslintrc
342 const userEslintRulePath = resolve(opts.cwd, '.eslintrc');
343 if (existsSync(userEslintRulePath)) {
344 try {
345 const userRc = readRc(userEslintRulePath);
346 debug(`userRc: ${JSON.stringify(userRc)}`);
347 if (userRc.extends) {
348 debug(`use user's .eslintrc: ${userEslintRulePath}`);
349 eslintOptions.useEslintrc = true;
350 eslintOptions.baseConfig = false;
351 eslintOptions.ignore = true;
352 } else {
353 debug(`extend with user's .eslintrc: ${userEslintRulePath}`);
354 eslintOptions.baseConfig = {
355 ...eslintOptions.baseConfig,
356 ...userRc,
357 };
358 }
359 } catch (e) {
360 debug(e);
361 }
362 }
363
364 const extraBabelIncludes = opts.extraBabelIncludes || [];
365 if (opts.es5ImcompatibleVersions) {
366 extraBabelIncludes.push(a => {
367 if (a.indexOf('node_modules') === -1) return false;
368 const pkgPath = getPkgPath(a);
369 return shouldTransform(pkgPath);
370 });
371 }
372
373 const config = {
374 bail: !isDev,
375 devtool: opts.devtool || undefined,
376 entry: opts.entry || null,
377 output: {
378 path: outputPath,
379 // Add /* filename */ comments to generated require()s in the output.
380 pathinfo: isDev,
381 filename: `[name]${jsHash}.js`,
382 publicPath: opts.publicPath || undefined,
383 chunkFilename: `[name]${jsHash}.async.js`,
384 },
385 resolve: {
386 modules: [
387 'node_modules',
388 resolve(__dirname, '../node_modules'),
389 ...(opts.extraResolveModules || []),
390 ],
391 extensions: [
392 ...(opts.extraResolveExtensions || []),
393 '.web.js',
394 '.web.jsx',
395 '.web.ts',
396 '.web.tsx',
397 '.js',
398 '.json',
399 '.jsx',
400 '.ts',
401 '.tsx',
402 ],
403 alias: {
404 ...opts.alias,
405 },
406 plugins:
407 process.env.TS_CONFIG_PATHS_PLUGIN &&
408 process.env.TS_CONFIG_PATHS_PLUGIN !== 'none'
409 ? [new TsConfigPathsPlugin()]
410 : [],
411 },
412 module: {
413 rules: [
414 ...(process.env.DISABLE_TSLINT || process.env.TSLINT === 'none'
415 ? []
416 : [
417 {
418 test: /\.tsx?$/,
419 include: opts.cwd,
420 exclude: /node_modules/,
421 enforce: 'pre',
422 use: [
423 {
424 options: {
425 emitErrors: true,
426 // formatter: eslintFormatter,
427 },
428 loader: require.resolve('tslint-loader'),
429 },
430 ],
431 },
432 ]),
433 ...(process.env.DISABLE_ESLINT || process.env.ESLINT === 'none'
434 ? []
435 : [
436 {
437 test: /\.(js|jsx)$/,
438 include: opts.cwd,
439 exclude: /node_modules/,
440 enforce: 'pre',
441 use: [
442 {
443 options: eslintOptions,
444 loader: require.resolve('eslint-loader'),
445 },
446 ],
447 },
448 ]),
449 {
450 exclude: [
451 /\.(html|ejs)$/,
452 /\.json$/,
453 /\.(js|jsx|ts|tsx)$/,
454 /\.(css|less|scss|sass)$/,
455 ...(opts.urlLoaderExcludes || []),
456 ],
457 loader: require.resolve('url-loader'),
458 options: {
459 limit: 10000,
460 name: 'static/[name].[hash:8].[ext]',
461 },
462 },
463 {
464 test: /\.js$/,
465 include: opts.cwd,
466 exclude: /node_modules/,
467 use: babelUse,
468 },
469 {
470 test: /\.jsx$/,
471 include: opts.cwd,
472 use: babelUse,
473 },
474 {
475 test: /\.(ts|tsx)$/,
476 include: opts.cwd,
477 exclude: /node_modules/,
478 use: [
479 ...babelUse,
480 {
481 loader: require.resolve('awesome-typescript-loader'),
482 options: {
483 configFileName:
484 opts.tsConfigFile || join(opts.cwd, 'tsconfig.json'),
485 transpileOnly: true,
486 ...(opts.typescript || {}),
487 },
488 },
489 ],
490 },
491 ...extraBabelIncludes.map(include => {
492 return {
493 test: /\.(js|jsx)$/,
494 include:
495 typeof include === 'string' ? join(opts.cwd, include) : include,
496 use: babelUseDeps,
497 };
498 }),
499 {
500 test: /\.html$/,
501 loader: require.resolve('file-loader'),
502 options: {
503 name: '[name].[ext]',
504 },
505 },
506 ...cssRules,
507 ],
508 },
509 plugins: [
510 ...(isDev
511 ? [
512 new webpack.HotModuleReplacementPlugin(),
513 // Disable this plugin since it causes 100% cpu when have lost deps
514 // new WatchMissingNodeModulesPlugin(join(opts.cwd, 'node_modules')),
515 new SystemBellWebpackPlugin(),
516 ...(process.env.HARD_SOURCE && process.env.HARD_SOURCE !== 'none'
517 ? [new HardSourceWebpackPlugin()]
518 : []),
519 ].concat(
520 opts.devtool
521 ? []
522 : [
523 new webpack.SourceMapDevToolPlugin({
524 columns: false,
525 moduleFilenameTemplate: info => {
526 if (
527 /\/koi-pkgs\/packages/.test(
528 info.absoluteResourcePath,
529 ) ||
530 /packages\/koi-core/.test(info.absoluteResourcePath) ||
531 /webpack\/bootstrap/.test(info.absoluteResourcePath) ||
532 /\/node_modules\//.test(info.absoluteResourcePath)
533 ) {
534 return `internal:///${info.absoluteResourcePath}`;
535 }
536 return resolve(info.absoluteResourcePath).replace(
537 /\\/g,
538 '/',
539 );
540 },
541 }),
542 ],
543 )
544 : [
545 ...(process.env.__FROM_TEST
546 ? []
547 : [new webpack.HashedModuleIdsPlugin()]),
548 new webpack.optimize.ModuleConcatenationPlugin(),
549 new ExtractTextPlugin({
550 filename: `[name]${cssHash}.css`,
551 allChunks: true,
552 }),
553 ...(opts.serviceworker
554 ? [
555 new SWPrecacheWebpackPlugin({
556 filename: 'service-worker.js',
557 minify: !(
558 process.env.NO_COMPRESS || process.env.COMPRESS === 'none'
559 ),
560 staticFileGlobsIgnorePatterns: [
561 /\.map$/,
562 /asset-manifest\.json$/,
563 ],
564 ...opts.serviceworker,
565 }),
566 ]
567 : []),
568 ...(opts.manifest
569 ? [
570 new ManifestPlugin({
571 fileName: 'manifest.json',
572 ...opts.manifest,
573 }),
574 ]
575 : []),
576 ]),
577 ...(isDev || (process.env.NO_COMPRESS || process.env.COMPRESS === 'none')
578 ? []
579 : [
580 new webpack.optimize.UglifyJsPlugin({
581 ...uglifyJSConfig,
582 ...(opts.devtool ? { sourceMap: true } : {}),
583 }),
584 ]),
585 new webpack.DefinePlugin({
586 'process.env.NODE_ENV': JSON.stringify(
587 // eslint-disable-line
588 isDev ? 'development' : 'production',
589 ), // eslint-disable-line
590 'process.env.HMR': process.env.HMR,
591 // 给 socket server 用
592 ...(process.env.SOCKET_SERVER
593 ? {
594 'process.env.SOCKET_SERVER': JSON.stringify(
595 process.env.SOCKET_SERVER,
596 ),
597 }
598 : {}),
599 ...stringifyObject(opts.define || {}),
600 }),
601 ...(opts.html ? [new HTMLWebpackPlugin(opts.html)] : []),
602 new CaseSensitivePathsPlugin(),
603 new webpack.LoaderOptionsPlugin({
604 options: {
605 context: __dirname,
606 },
607 }),
608 new ProgressPlugin(),
609 ...(process.env.TS_TYPECHECK ? [new ForkTsCheckerWebpackPlugin()] : []),
610 ...(opts.ignoreMomentLocale
611 ? [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)]
612 : []),
613 ...commonsPlugins,
614 ...copyPlugins,
615 ...(process.env.ANALYZE
616 ? [
617 new BundleAnalyzerPlugin({
618 analyzerMode: 'server',
619 analyzerPort: process.env.ANALYZE_PORT || 8888,
620 openAnalyzer: true,
621 }),
622 ]
623 : []),
624 ],
625 externals: opts.externals,
626 node: {
627 dgram: 'empty',
628 fs: 'empty',
629 net: 'empty',
630 tls: 'empty',
631 child_process: 'empty',
632 },
633 performance: isDev
634 ? {
635 hints: false,
636 }
637 : {},
638 };
639
640 if (process.env.PUBLIC_PATH) {
641 config.output.publicPath = `${stripLastSlash(process.env.PUBLIC_PATH)}/`;
642 }
643
644 return applyWebpackConfig(opts.cwd, config);
645}