import * as webpack from "webpack"; import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; import * as merge from "webpack-merge"; import * as UglifyJsPlugin from "uglifyjs-webpack-plugin"; import * as CopyWebpackPlugin from "copy-webpack-plugin"; import DllLinkPlugin = require("dll-link-webpack-plugin"); import * as path from "path"; import * as FriendlyErrorsPlugin from "friendly-errors-webpack-plugin"; import * as ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; import MapPlugin from "./MapPlugin"; import HtmlPlugin, { HtmlExternalOptions } from "./HtmlPlugin"; import { localIP } from "./utils"; export interface CSSPath { include: string | string[]; exclude?: string | string[]; } export interface WebpackOptions { /** * 项目根目录 */ rootPath: string; /** * tsx?包含的文件目录 */ tsInclude?: string | string[]; /** * jsx?包含的文件目录 */ jsInclude?: string | string[]; /** * 项目的 entry,等价于 webpack 的 entry */ entry: webpack.Entry; /** * 和上面的 entry 一样,只是存放的是第三方库 */ dllEntry: webpack.Entry; /** * 打包的文件输出路径 */ outputPath: string; /** * 相当于 webpack 的 public path,只有在打包的时候才会用到。可传一个对象,区分 uat 和 online */ cdnPath: | string | { uat: string; online: string; }; /** * 配置 css modules 路径 */ cssModulePath?: CSSPath; /** * 配置全局 css 路径 */ cssGlobalPath?: CSSPath; /** * 项目是否要生成 html 文件 */ html?: boolean; /** * 自定义 html 参数 */ htmlOption?: HtmlExternalOptions; /** * 自定义的 babel 插件 */ babelPlugins?: any[]; /** * webpack 的 externals */ externals?: { [key: string]: boolean | string; }; /** * 其他需要运行的plugins */ plugins?: any[]; /** * 禁用Common Chunks Plugin */ disableCommonChunks?: boolean; /** * 打包出来代码的version,理论上来讲可以不用传,为了应对一些js文件 modify 的缓存bug */ version?: string; /** * 静态文件复制路径,from 为绝对路径,to 为相对 outputPath 的路径 * see https://github.com/webpack-contrib/copy-webpack-plugin#pattern-properties */ copyPath?: { from: string; to?: string; }[]; } const MODULE_PATH = path.resolve(__dirname, "../node_modules"); function isParentDir(parentPath: string, childPath: string) { return !path.relative(parentPath, childPath).startsWith(`..${path.sep}`); } function genStyleLoaders(isProd: boolean, modulePath?: CSSPath, globalPath?: CSSPath) { const moduleName = isProd ? "[hash:base64:5]" : "[folder]--[local]--[hash:base64:5]"; const cssModuleLoader = { loader: "css-loader", options: { modules: true, localIdentName: moduleName, importLoaders: 1 } }; const postCSSLoader = { loader: "postcss-loader", options: { plugins: function() { return [require("autoprefixer")]; } } }; const cssModulesUse = ["style-loader", cssModuleLoader, postCSSLoader]; const cssGlobalUse = ["style-loader", "css-loader", postCSSLoader]; const styleLoaders: any[] = []; const nodeModulesPath = path.resolve("./node_modules"); if (modulePath) { const { include, exclude } = modulePath; styleLoaders.push( { test: /\.css$/, include, exclude, use: cssModulesUse }, { test: /\.scss$/, include, exclude, use: cssModulesUse.concat("sass-loader") } ); } if (globalPath) { const { include, exclude } = globalPath; styleLoaders.push( { test: /\.css$/, include, exclude, use: cssGlobalUse }, { test: /\.scss$/, include, exclude, use: cssGlobalUse.concat("sass-loader") } ); } if (styleLoaders.length === 0) { // 如果没有配置 css 路径,默认对非 node_modules 进行 css modules 处理 styleLoaders.push( { test: /\.css$/, exclude: nodeModulesPath, use: cssModulesUse }, { test: /\.scss$/, exclude: nodeModulesPath, use: cssModulesUse.concat("sass-loader") } ); } // 默认会处理 node_modules 里面的 css 文件 let needProcessNodeModulesGlobalCSS = true; if (globalPath !== undefined && globalPath.include !== undefined) { const paths = !Array.isArray(globalPath.include) ? [globalPath.include] : globalPath.include; needProcessNodeModulesGlobalCSS = paths.find(path => path === nodeModulesPath || isParentDir(path, nodeModulesPath)) === undefined; } if (needProcessNodeModulesGlobalCSS) { styleLoaders.push({ test: /\.css$/, include: nodeModulesPath, use: cssGlobalUse }); } return styleLoaders; } function genDllConfig(options: WebpackOptions, isProd: boolean): any { const env = isProd ? "production" : "development"; const version = typeof options.version === "string" ? `.${options.version}` : ""; const filename = isProd ? `js/[name]${version}.[chunkhash:8].dll.js` : "[name].dll.js"; const library = "[name]_lib"; return { entry: options.dllEntry, output: { filename, path: options.outputPath, publicPath: isProd ? options.cdnPath : "", library }, externals: options.externals || {}, module: { rules: [ { test: /\.css/, use: ["style-loader", "css-loader"] } ] }, plugins: [ new webpack.DllPlugin({ path: "[name]-manifest.json", name: library, context: options.outputPath }), new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify(env) }), // remove moment locale, except en and zh new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en-gb|zh-cn/) ].concat( isProd ? [ new UglifyJsPlugin(), new webpack.HashedModuleIdsPlugin(), new webpack.optimize.ModuleConcatenationPlugin() ] : [] ) }; } function genConfig( options: WebpackOptions, name: string, isProd: boolean, devPort?: number ): webpack.Configuration & { entry: webpack.Entry; } { let jsLoaders: any[] = [ { loader: "babel-loader", options: { presets: [ [ "env", { modules: false } ], "react", "stage-1" ], plugins: [ ...(options.babelPlugins ? options.babelPlugins : []), "lodash", [ "import", [ { libraryName: "antd", libraryDirectory: "es" } ] ], "transform-decorators-legacy" ], comments: false } } ]; if (!isProd) { jsLoaders = ["react-hot-loader/webpack", "cache-loader", ...jsLoaders]; } const tsLoaders = [ ...jsLoaders, { loader: "ts-loader", options: { transpileOnly: true } } ]; const plugins: any[] = [ new DllLinkPlugin({ config: genDllConfig(options, isProd), appendVersion: isProd, assetsMode: true, htmlMode: true }), // remove moment locale, except en and zh // see https://github.com/moment/moment/issues/2416#issuecomment-344840418 new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en-gb|zh-cn/), new ForkTsCheckerWebpackPlugin() ]; const { cssModulePath, cssGlobalPath } = options; if (options.html) { const { htmlOption, entry, dllEntry } = options; const htmlConfig: any = { entry, dllEntry, isProd }; if (htmlOption) { const { title, template, entryTemplates, preload, disableMinify, customTplData } = htmlOption; if (title) { htmlConfig.title = title; } if (template) { htmlConfig.template = template; } if (entryTemplates) { htmlConfig.entryTemplates = entryTemplates; } if (preload) { htmlConfig.preload = preload; } if (disableMinify) { htmlConfig.disableMinify = disableMinify; } htmlConfig.customTplData = customTplData; } plugins.push(new HtmlPlugin(htmlConfig)); } else { plugins.push( new MapPlugin({ name: "map", dllEntry: options.dllEntry, isProd, devPort }) ); } const polyfillNames = ["babel-polyfill", "core-js"]; const polyfillName = Object.keys(options.dllEntry).reduce(function(pValue, cValue) { if (pValue === "") { const entry = options.dllEntry[cValue]; if (typeof entry === "string" && polyfillNames.includes(entry)) { return entry; } else if (Array.isArray(entry)) { const result = entry.find(item => polyfillNames.includes(item)); if (typeof result !== "undefined") { return result; } } } return pValue; }, ""); let entryPrefix: string[] = []; if (polyfillName !== "") { entryPrefix.push(polyfillName); } if (!isProd) { entryPrefix.push( "react-hot-loader/patch", `webpack-hot-middleware/client?path=http://${localIP}:${devPort}/__webpack_hmr&overlay=false` ); } if (entryPrefix.length > 0) { Object.keys(options.entry).forEach(function(name) { options.entry[name] = entryPrefix.concat(options.entry[name]); }); } return { entry: options.entry, resolve: { extensions: [".webpack.js", ".js", ".jsx", ".tsx", ".ts"], modules: [options.rootPath, "node_modules", MODULE_PATH] }, resolveLoader: { modules: ["node_modules", MODULE_PATH] }, module: { rules: [ { test: /\.tsx?$/, include: options.tsInclude || options.rootPath, use: tsLoaders }, { test: /\.jsx?$/, include: options.jsInclude || options.rootPath, use: jsLoaders }, { test: /.(woff(2)?|eot|ttf|svg)(\?[a-z0-9=.]+)?$/, use: [ { loader: "url-loader", options: { limit: 8192, name: isProd ? "fonts/[name].[hash].[ext]" : "fonts/[name].[ext]" } } ] }, { test: /.(png|jpg)(\?[a-z0-9=.]+)?$/, use: [ { loader: "url-loader", options: { limit: 8192, name: isProd ? "images/[name].[hash].[ext]" : "images/[name].[ext]" } } ] }, ...genStyleLoaders(isProd, cssModulePath, cssGlobalPath) ] }, plugins: plugins.concat(Array.isArray(options.plugins) ? options.plugins : []), externals: options.externals }; } export function genDevConfig(options: WebpackOptions, name: string, devPort: number) { const baseConfig = genConfig(options, name, false, devPort); return merge(baseConfig, { output: { publicPath: `http://${localIP}:${devPort}/`, filename: "[name].js", chunkFilename: "[name].chunk.js", path: options.outputPath }, devtool: "cheap-module-eval-source-map", plugins: [ new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }), new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin(), new FriendlyErrorsPlugin() ] }); } export function genProdConfig( options: WebpackOptions, name: string, isOnline: boolean, analyse?: boolean ) { let { cdnPath, disableCommonChunks } = options; if (typeof cdnPath === "object") { if (isOnline) { cdnPath = cdnPath.online; } else { cdnPath = cdnPath.uat; } } options.cdnPath = cdnPath; const baseConfig = genConfig(options, name, true); const plugins = [ new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }), new UglifyJsPlugin({ cache: true, parallel: true }), // remove moment locale, except en and zh // see https://github.com/moment/moment/issues/2416#issuecomment-344840418 new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en-gb|zh-cn/), new webpack.HashedModuleIdsPlugin() ]; if (disableCommonChunks !== true) { plugins.push( new webpack.optimize.CommonsChunkPlugin({ name: "common", minChunks: 2 }) ); plugins.push( new webpack.optimize.CommonsChunkPlugin({ name: "manifest" }) ); } if (analyse) { plugins.push( new BundleAnalyzerPlugin({ analyzerMode: "static" }) ); } else { // exclude ModuleConcatenationPlugin in analyse mode // see https://github.com/webpack-contrib/webpack-bundle-analyzer#i-cant-see-all-the-dependencies-in-a-chunk plugins.push(new webpack.optimize.ModuleConcatenationPlugin()); } if (Array.isArray(options.copyPath) && options.cdnPath.length > 0) { plugins.push(new CopyWebpackPlugin(options.copyPath)); } const version = typeof options.version === "string" ? `.${options.version}` : ""; return merge(baseConfig, { output: { path: options.outputPath, publicPath: cdnPath, filename: `js/[name]${version}.[chunkhash:8].js`, chunkFilename: `js/[name]${version}.[chunkhash:8].chunk.js` }, devtool: false, plugins }); }