UNPKG

builder-webpack-core

Version:
723 lines (642 loc) 23.4 kB
'use strict'; const osenv = require('osenv'); const fs = require('fs'); const path = require('path'); const glob = require("glob"); const webpack = require('webpack'); const HappyPack = require('happypack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin'); const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); const ReplaceBundleStringPlugin = require('replace-bundle-webpack-plugin'); const HtmlStringReplace = require('html-string-replace-webpack-plugin'); const StringReplaceWebpackPlugin = require('string-replace-webpack-plugin'); const SriPlugin = require('webpack-subresource-integrity'); const OfflineWebpackPlugin = require('offline-webpack-plugin'); const PurgecssPlugin = require('purgecss-webpack-plugin'); const {deepCopy, listDir, merge, isEmpty} = require('./util'); const Config = require('./config'); // 当前运行的时候的根目录 let projectRoot = Config.getPath('feflow.json'); if (!projectRoot) { projectRoot = Config.getPath('feflow.js'); } const PATHS = { src: path.join(projectRoot, 'src') }; module.exports = class BuilderCore { static setHappyPack(jsLoader, useTreeShaking) { let presets = ['env', 'stage-0', 'react']; if (useTreeShaking) { presets = [[ "env", { "modules": false, }], 'stage-0', 'react']; } let options = Object.assign({ cacheDirectory: true, plugins: [ 'transform-decorators-legacy', [ "import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" } ] ], presets: presets }, jsLoader); return new HappyPack({ loaders: [ { loader: 'babel-loader', options } ] }); } static setTsHappyPack() { return new HappyPack({ id: 'ts', loaders: [ { loader: 'ts-loader', options: { happyPackMode: true } } ] }); } /** * 设置打包后的输出 output 内容 * @param useHash 是否开启JS资源hash * @param pathPrefix JS的前缀, 不传入则为空 * @param publicPath * @returns {{filename: string, path: string, publicPath: *}} * @private */ static setOutput(useHash, pathPrefix, publicPath, outDir) { let filename = ''; let hash = ''; if (pathPrefix) { filename = pathPrefix + '/'; } if (useHash) { hash = '_[chunkhash:8]'; } outDir = outDir || 'public' return { filename: `${filename}[name]${hash}.js?_bid=152`, path: path.join(projectRoot, `${outDir}/`), publicPath: publicPath, crossOriginLoading: 'anonymous' }; } /** * 设置图片解析规则 * @param useHash 是否开启图片资源hash * @param pathPrefix 图片的前缀,不传入则为空 * @returns {{test: RegExp, use: {loader: string, options: {name: string}}}} * @private */ static setImgRule(useHash, pathPrefix, akbid) { let filename = ''; let hash = ''; if (pathPrefix) { filename = pathPrefix + '/'; } if (useHash) { hash = '_[hash:8]'; } return { test: /\.(png|svg|jpg|gif|blob)$/, use: [{ loader: 'inline-file-loader', options: { name: akbid ? `${filename}img/[name]${hash}.[ext]?_bid=${akbid}` : `${filename}img/[name]${hash}.[ext]` } }] }; } /** * 设置富媒体解析规则 * @param useHash 是否开启资源hash * @param pathPrefix 前缀,不传入则为空 * @returns {{test: RegExp, use: {loader: string, options: {name: string}}}} * @private */ static setMediaRule(useHash, pathPrefix) { let filename = ''; let hash = ''; if (pathPrefix) { filename = pathPrefix + '/'; } if (useHash) { hash = '_[hash:8]'; } return { test: /\.(mp3|mp4|mov)$/, use: [{ loader: 'file-loader', options: { name: `${filename}img/[name]${hash}.[ext]` } }] }; } /** * 设置字体解析规则 */ static setFontRule() { return { test: /\.(woff|woff2|eot|ttf|otf)$/, use: { loader: 'file-loader' } }; } /** * 设置 Html 文件解析规则 * 支持 Fis3 的 ?inline 语法糖 * * @returns {{test: RegExp, use: Array}} * @private */ static setHtmlRule() { const htmlRuleArray = []; htmlRuleArray.push({ loader: 'html-loader', options: { // 支持 html `${}` 语法 interpolate: 1, attrs: [':src'] } }); // Fis3 inline 语法糖支持 htmlRuleArray.push({ loader: 'replace-text-loader', options: { rules: [ { // inline script, 匹配所有的<script src="package?__inline"></script> 需要inline的标签 // 并且替换为 // <script>${require('raw-loader!babel-loader!../../node_modules/@tencent/report // - whitelist')}</script> 语法 pattern: /<script.*?src="(.*?)\?__inline".*?>.*?<\/script>/gmi, replacement: (source) => { // 找到需要 inline 的包 const result = /<script.*?src="(.*?)\?__inline"/gmi.exec(source); const pkg = result && result[1]; return "<script>${require('raw-loader!babel-loader!" + pkg + "')}</script>"; } }, { // inline html, 匹配<!--inline[/assets/inline/meta.html]-->语法 pattern: /<!--inline\[.*?\]-->/gmi, replacement: (source) => { // 找到需要 inline 的包 const result = /<!--inline\[(.*?)\]-->/gmi.exec(source); let path = result && result[1]; if (path && path[0] === '/') { path = '../..' + path; } return "${require('raw-loader!" + path + "')}"; } } ] } }); return {test: /index\.html$/, use: htmlRuleArray} } /** * 设置CSS解析规则 */ static setCssRule() { return { test: /\.css$/, use: ['style-loader', 'css-loader'] } } /** * 设置Scss文件解析规则 * * @param minimize 是否压缩Css * @param usePx2rem 是否使用px2rem loader * @param remUnit rem单位,默认75 * @param remPrecision rem精度, 默认8 * @returns {{test: RegExp, use: *}} * @private */ static setScssRule(minimize, usePx2rem, remUnit, remPrecision) { const cssRuleArray = []; // 加载Css loader, 判断是否开启压缩 const cssLoaderRule = { loader: "css-loader", options: { alias: this.setAlias() } }; if (minimize) { cssLoaderRule.options = { minimize: true }; } cssRuleArray.push(cssLoaderRule); // 如果开启px2rem,则加载px2rem-loader if (usePx2rem) { cssRuleArray.push({ loader: "px2rem-loader", options: { remUnit: remUnit || 75, remPrecision: remPrecision || 8 } }); } // css 前缀,兼容低版本浏览器 cssRuleArray.push({loader: "autoprefixer-loader" + "?"+ JSON.stringify({browsers:["last 2 version", "> 1%", "iOS 7"]})}); // 雪碧图loader cssRuleArray.push({loader: "sprites-loader"}); // 加载解析scss的sass-loader cssRuleArray.push({ loader: "sass-loader", options: { includePaths: [path.join(projectRoot, "./src")] } }); // 支持scss里面的绝对路径写法,比如 @import "/assets/css/mixin"; cssRuleArray.push(StringReplaceWebpackPlugin.replace({ replacements: [ { pattern: /@import\s*"(\/.*?)";/gmi, replacement: function (match) { const result = /@import\s*"(\/.*?)";/gmi.exec(match); let path = result && result[1]; if (path && path[0] === '/') { path = path.substring(1); } return `@import "${path}";`; } } ] })); return { test: /\.scss$/, use: ['css-hot-loader'].concat(ExtractTextPlugin.extract({ fallback: 'style-loader', use: cssRuleArray })) }; } /** * 设置Less文件解析规则 * * @param minimize 是否压缩Css * @param usePx2rem 是否使用px2rem loader * @param remUnit rem单位,默认75 * @param remPrecision rem精度, 默认8 * @returns {{test: RegExp, use: *}} * @private */ static setLessRule(minimize, usePx2rem, remUnit, remPrecision, lessLoaderOptions) { const cssRuleArray = []; // 加载Css loader, 判断是否开启压缩 const cssLoaderRule = { loader: "css-loader", options: { alias: this.setAlias() } }; if (minimize) { cssLoaderRule.options = { minimize: true }; } cssRuleArray.push(cssLoaderRule); // 如果开启px2rem,则加载px2rem-loader if (usePx2rem) { cssRuleArray.push({ loader: "px2rem-loader", options: { remUnit: remUnit || 75, remPrecision: remPrecision || 8 } }); } // css 前缀,兼容低版本浏览器 cssRuleArray.push({ loader: "autoprefixer-loader" + "?"+ JSON.stringify({browsers:["last 2 version", "> 1%", "iOS 7"]}) }); // 雪碧图loader cssRuleArray.push({ loader: "sprites-loader" }); // 加载解析less的less-loader cssRuleArray.push({ loader: "less-loader", options: lessLoaderOptions || { includePaths: [path.join(projectRoot, "./src")] } }); return { test: /\.less$/, use: ['css-hot-loader'].concat(ExtractTextPlugin.extract({ fallback: "style-loader", use: cssRuleArray })) }; } /** * 设置Js文件解析规则, 此处使用happypack,多实例构建 * * @returns {{test: RegExp, loader: string}} * @private */ static setJsRule() { return {test: /\.js$/, loader: 'happypack/loader', exclude: path.join(projectRoot, 'node_modules')}; } /** * 设置TS文件解析规则, 此处使用happypack,多实例构建 * * @returns {{test: RegExp, loader: string}} * @private */ static setTsRule() { return {test: /\.ts(x?)$/, loader: 'happypack/loader?id=ts', exclude: path.join(projectRoot, 'node_modules')}; } /** * 打包构建后替换的内容, 此处替换html文件 * @param regex 正则 * @param replaceWith 替换成xx字符串 * @returns {HtmlStringReplace} * @private */ static setReplaceHtmlPlugin(regex, replaceWith) { return new HtmlStringReplace({ enable: true, patterns: [ { match: regex, replacement: () => { return replaceWith; } } ] }) } /** * 打包构建后替换的内容, 此处替换css和js文件 * @param regex 正则 * @param replaceWith 替换成xx字符串 * @returns {ReplaceBundleStringPlugin} * @private */ static setReplaceBundlePlugin(regex, replaceWith) { return new ReplaceBundleStringPlugin([ { partten: regex, replacement: () => { return replaceWith; } } ]); } /** * 设置提取Css资源的插件 * @param useHash 是否开启图片资源hash * @param pathPrefix CSS的前缀,不传入则为空 * @private */ static setExtractTextPlugin(useHash, pathPrefix) { let filename = ''; let hash = ''; if (pathPrefix) { filename = pathPrefix + '/'; } if (useHash) { hash = '_[contenthash:8]'; } return new ExtractTextPlugin(`${filename}[name]${hash}.css`); } /** * 不把React, react-dom打到公共包里 * * @private */ static setExternalPlugin(externals) { const newExternals = externals || [ { module: 'react', entry: '//11.url.cn/now/lib/15.1.0/react-with-addons.min.js?_bid=3123', global: 'React' }, { module: 'react-dom', entry: '//11.url.cn/now/lib/15.1.0/react-dom.min.js?_bid=3123', global: 'ReactDOM' } ]; return new HtmlWebpackExternalsPlugin({externals: newExternals}); } /** * 多页面打包 * @param entries glob的entry路径 * @param minifyHtml 是否压缩html * @param inject 是否自动注入打包出来的js和css * @param inlineCSS 是否inline打包出来的css * @param assetsPrefix Css, Js, img的路径前缀 * @param htmlPrefix Html的路径前缀 * @returns {{newEntry: {}, htmlWebpackPlugins: Array}} * @private */ static setMultiplePage(entries, minifyHtml, inject, inlineCSS, assetsPrefix, htmlPrefix) { const newEntry = {}; const htmlWebpackPlugins = []; Object .keys(entries) .map((index) => { const entry = entries[index]; const entryFile = `${entry}/init.js`; // 支持 init.js 文件不是必须存在的场景,纯html const isEntryFileExists = fs.existsSync(entryFile); const match = entry.match(/\/pages\/(.*)/); const pageName = match && match[1]; let filename = ''; if (htmlPrefix) { filename = htmlPrefix + '/'; } if (isEntryFileExists) { newEntry[pageName] = entryFile; } htmlWebpackPlugins.push(new HtmlWebpackPlugin({ inlineSource: inlineCSS ? '\\.css$' : undefined, template: path.join(projectRoot, `src/pages/${pageName}/index.html`), filename: `${filename}${pageName}.html`, chunks: [pageName], assetsPrefix: `${assetsPrefix}/`, inject: inject && isEntryFileExists, minify: minifyHtml ? { html5: true, collapseWhitespace: true, preserveLineBreaks: false, minifyCSS: true, minifyJS: true, removeComments: false } : false })); }); return {newEntry, htmlWebpackPlugins}; } /** * 给生成的 js bundle文件增加 crossorigin="anonymous" 跨域头 * @returns {SriPlugin} * @private */ static setSriPlugin() { return new SriPlugin({ hashFuncNames: ['sha256', 'sha384'] }); } /** CSS Treeshaking 功能 */ static setCssTreeShaking() { return new PurgecssPlugin({ paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }), }); } /** * 离线包打包逻辑 * @param assetsPrefix CSS、Img、JS等打包路径前缀 * @param htmlPrefix Html打包路径前缀 * @param cdnUrl CDN路径 * @param serverUrl now.qq.com路径 * @param domain 域名 * @param cdn cdn路径 * @param product cdn对应的产品 * @private */ static setOffline(assetsPrefix, htmlPrefix, cdnUrl, serverUrl, domain, cdn, product, outDir) { outDir = outDir || 'public' return new OfflineWebpackPlugin({ path: path.join(projectRoot, `./${outDir}/offline`), filename: 'offline.zip', pathMapper: (assetPath) => { if (assetPath.indexOf(htmlPrefix) !== -1) { // 所有资源都改成 now.qq.com 的域名, 防止跨域问题 assetPath = assetPath.replace(htmlPrefix, ''); } else if (assetPath.indexOf(assetsPrefix) !== -1) { assetPath = assetPath.replace(assetsPrefix, ''); } return path.join(serverUrl.replace('//', ''), assetPath); }, beforeAddBuffer(source, assetPath) { if (assetPath.indexOf(htmlPrefix) !== -1) { // 基础包域名修改, 防止跨域问题 source = source.replace(/\/\/11\.url\.cn\/now\/([^"'\)\`]+)(["'\`\)])/g, `//${domain}/$1$2`); // 业务资源的域名修改, 防止跨域问题 const regex = new RegExp(`//${cdn}/${product}/([^"'\)\`]+)(["'\`\)])`, 'g'); source = source.replace(regex, `//${domain}/$1$2`); // 去掉 crossorigin="annoymous" source = source.replace(/crossorigin=\"anonymous\"/g, ''); // 去掉 业务 js 文件的 ?_bid=152 source = source.replace(/\?_bid=152/g, ''); // 去掉业务 js 上的 integrity 属性 source = source.replace(/(<script.*)integrity=".*?"/, '$1'); // 注入离线包的版本号. pack const inject = { version: Date.now() }; source = source.replace(/(<script)/, '<script>var pack = ' + JSON.stringify(inject) + '</script>$1'); } else { const regex = new RegExp(`//${cdn}/${product}/([^"'\)\`]+)(["'\`\)])`, 'g'); source = source.replace(regex, `//${domain}/$1$2`); } return source; } }) } /** * Code split, 提取出公共js文件,避免每个页面重复打包 */ static setCommonsChunkPlugin() { return new webpack.optimize.CommonsChunkPlugin({ name: 'common' }); } /** * 设置NODE_ENV,否则 线上会报 warning. * https://stackoverflow.com/questions/30030031 */ static setDefinePlugin(env) { return new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(env) }); } /** * webpack进程默认不能关掉,通过插件的方式在生产环境构建时抛出异常,避免发布上线 */ static setDoneErrorPlugin() { return function() { this.plugin('done', (stats) => { if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') == -1) { console.log(stats.compilation.errors); process.exit(1); } }); } } /** * 设置dev server,WDS配置 * @param port * @returns {{contentBase: string, inline: boolean, historyApiFallback: boolean, disableHostCheck: boolean, port: *}} * @private */ static setDevServer(port) { return { contentBase: path.join(projectRoot, './src'), inline: true, historyApiFallback: false, disableHostCheck: true, port: port }; } /** * 设置引用的Alias路径匹配规则,以src下面的目录为根目录 * 支持import TitleBar from "/modules/components/titlebar" 绝对路径语法 * @param alias 用户自定义的alias * * @private */ static setAlias(alias) { const aliasObject = {}; listDir(path.join(projectRoot, './src'), 1).forEach((dir) => { const {name, dirPath} = dir; aliasObject['/' + name] = dirPath; aliasObject[name] = dirPath; }); if (isEmpty(alias)) { return aliasObject; } return merge(alias, aliasObject); } /** * 设置Loader的path查找的目录 * 好处是:把构建器放在 ./feflow 目录下,多个项目可以公用一个构建器,便于构建器的增量更新和统一升级 */ static setResolveLoaderPath(packageName, runtime) { const jbRuntime = runtime || 'runtime-now-6'; const resolveLoaderPath = path.join(osenv.home(), './.feflow/node_modules'); const globalStyleResolveLoaderPath = path.join(osenv.home(), `./.feflow/node_modules/${ packageName }/node_modules`); // Loader在捷豹平台的查找路径 const jbLoaderPath = `/data/frontend/install/AlloyDist/${jbRuntime}/node_modules`; return { modules: [ resolveLoaderPath, globalStyleResolveLoaderPath, jbLoaderPath ] }; } }