san-cli-service
Version:
定制化的前端工程构建工具
250 lines (239 loc) • 10.8 kB
JavaScript
/**
* Copyright (c) Baidu Inc. All rights reserved.
*
* This source code is licensed under the MIT license.
* See LICENSE file in the project root for license information.
*
* @file 主要配置 html-plugin
* @author ksky521
*/
const path = require('path');
const fs = require('fs');
const minify = require('html-minifier-terser').minify;
const lMerge = require('lodash.merge');
const {terserOptions: defaultTerserOptions, htmlMinifyOptions} = require('../options');
module.exports = {
id: 'built-in:app',
apply(api, options) {
api.chainWebpack(webpackConfig => {
const isProd = api.isProd();
const outputDir = api.resolve(options.outputDir);
const terserOptions = Object.assign(defaultTerserOptions, options.terserOptions);
// 1. 判断 pages
// 2. build 做的事情是判断 serve 对象
const htmlOptions = {
inject: true,
templateParameters: (...args) => {
/* eslint-disable one-var */
let compilation, assets, assetTags, pluginOptions;
/* eslint-enable one-var */
if (args.length === 4) {
// v4 版本
[compilation, assets, assetTags, pluginOptions] = args;
}
else {
// v3 版本
[compilation, assets, pluginOptions] = args;
}
// enhance html-webpack-plugin's built in template params
let stats;
return Object.assign({
// make stats lazy as it is expensive
get webpack() {
return stats || (stats = compilation.getStats().toJson());
},
compilation,
webpackConfig: compilation.options,
htmlWebpackPlugin: {
files: assets,
tags: assetTags,
options: pluginOptions
}
});
}
};
if (isProd) {
// 压缩 html
// 跟 terserOptions 打平
htmlMinifyOptions.minifyJS = terserOptions;
lMerge(htmlOptions, {
minify: htmlMinifyOptions
});
}
// resolve HTML file(s)
const multiPageConfig = options.pages;
const HTMLPlugin = require('html-webpack-plugin');
const SanHtmlPlugin = require('san-cli-webpack/lib/HTMLPlugin');
const htmlPath = api.resolve('public/index.html');
// 默认路径
const defaultHtmlPath = require.resolve('../template/index.html');
const publicCopyIgnore = ['index.html', '.DS_Store'];
let useHtmlPlugin = false;
if (!multiPageConfig) {
// default, single page setup.
htmlOptions.alwaysWriteToDisk = true;
htmlOptions.inject = true;
htmlOptions.template = fs.existsSync(htmlPath) ? htmlPath : defaultHtmlPath;
webpackConfig.plugin('html').use(HTMLPlugin, [htmlOptions]);
useHtmlPlugin = true;
}
else {
// multi-page setup
/** simple
* pages: {
index: {
entry: 'src/entry-point/index/main.js', //entry for the public page
template: 'public/index.html', // source template
filename: 'index.html' // output as dist/*
},
signin: {
entry: 'src/entry-point/signin/main.js',
template: 'public/signin.html',
filename: 'signin.html'
}
}
*/
webpackConfig.entryPoints.clear();
const pages = Object.keys(multiPageConfig);
const normalizePageConfig = c => (typeof c === 'string' ? {entry: c} : c);
pages.forEach(name => {
let pageConfig = normalizePageConfig(multiPageConfig[name]);
let {
title,
entry,
template = `public/${name}.html`,
filename,
// 这里需要跟 mode 里面的 splitChunks 遥相呼应
chunks = [name]
// chunks = ['common', 'vendors', 'css-common', name]
} = pageConfig;
// inject entry
const entries = Array.isArray(entry) ? entry : [entry];
webpackConfig.entry(name).merge(entries.map(e => api.resolve(e)));
if (!filename) {
// 处理 smarty 情况
if (path.extname(template) === '.tpl') {
filename = path.basename(template);
}
else {
filename = `${name}.html`;
}
}
// filename = path.join(options.templateDir, filename);
// resolve page index template
const hasDedicatedTemplate = fs.existsSync(api.resolve(template));
if (hasDedicatedTemplate) {
publicCopyIgnore.push(template);
}
const templatePath = hasDedicatedTemplate
? template
: fs.existsSync(htmlPath)
? htmlPath
: defaultHtmlPath;
// inject html plugin for the page
const pageHtmlOptions = Object.assign(
{
alwaysWriteToDisk: true
},
htmlOptions,
pageConfig,
{
chunks,
entry: name,
template: templatePath,
// add templateDir
filename: ensureRelative(outputDir, filename),
title
}
);
webpackConfig.plugin(`html-${name}`).use(HTMLPlugin, [pageHtmlOptions]);
webpackConfig.plugin(`san-html-${name}`).use(SanHtmlPlugin);
});
useHtmlPlugin = true;
}
if (useHtmlPlugin) {
// 这里插件是依赖 html-webpack-plguin 的,所以不配置 hwp,会报错哦~
// html-webpack-harddisk-plugin
webpackConfig
.plugin('html-webpack-harddisk-plugin')
.use(require('html-webpack-harddisk-plugin'));
}
const copyArgs = [];
// copy static assets in public/
// 这里不属于 cli 的范畴,所以暂时不加了,让打包脚本自己处理吧
// const publicDir = api.resolve('public');
// if (options.copyPublicDir && fs.existsSync(publicDir)) {
// copyArgs.push({
// from: publicDir,
// to: outputDir,
// ignore: publicCopyIgnore
// });
// }
// ------ 这里把 copy 拿到这里来处理是为了合并 ignore
if (options.copy) {
const addCopyOptions = options => {
let {from, to = './', ignore = [], compress = true} = options;
// 默认开启压缩 tpl 和 html 文件,smarty 的专属
const defaultTransformOptions = compress
? {
transform: (content, path) => {
if (/\.(tpl|html|htm)$/.test(path)) {
return minify(content.toString(), {
minifyCSS: true,
minifyJS: terserOptions,
ignoreCustomFragments: [/{%[\s\S]*?%}/, /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/]
});
}
return content;
}
}
: {};
// 排除 templte 的情况
ignore = publicCopyIgnore.concat(typeof ignore === 'string' ? [ignore] : ignore);
if (from && !/[$^*?!]+/.test(from) && fs.existsSync(api.resolve(from))) {
from = api.resolve(from);
// 保证template的相对路径
ignore = ignore.map((f, i) => (i > 1 ? ensureRelative(from, api.resolve(f)) : f));
copyArgs.push(
Object.assign(defaultTransformOptions, options, {
from,
to: path.join(outputDir, to),
globOptions: {
ignore
}
})
);
}
else {
// 正则的,不处理
copyArgs.push(
Object.assign(defaultTransformOptions, options, {
from,
to: path.join(outputDir, to),
globOptions: {
ignore
}
})
);
}
};
if (Array.isArray(options.copy)) {
// 数组就循环吧
options.copy.forEach(addCopyOptions);
}
else {
addCopyOptions(options.copy);
}
}
if (copyArgs.length) {
webpackConfig.plugin('copy-webpack-plugin').use(require('copy-webpack-plugin'), [{patterns: copyArgs}]);
}
});
}
};
function ensureRelative(outputDir, p) {
if (path.isAbsolute(p)) {
return path.relative(outputDir, p);
}
return p;
}