chunk-filename-replace-webpack-plugin
Version:
ChunkFilename for Webpack4, and dynamically imported chunk names (paths)
226 lines (216 loc) • 11.8 kB
JavaScript
const _webpack = require('webpack');
const PluginName = 'chunk-filename-replace-webpack-plugin';
const fileTypeMaps = {
js: {
pluginName: 'JsonpMainTemplatePlugin',
moduleType: 'javascript'
},
css: {
pluginName: 'mini-css-extract-plugin',
moduleType: 'css/mini-extract'
}
};
function insertStr(soure, start, newStr) {
return soure.slice(0, start) + newStr + soure.slice(start);
}
const _webpack2 = _interopRequireDefault(_webpack);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const {Template} = _webpack2.default;
class ChunkFilenameReplaceWebpackPlugin {
constructor(options) {
/**
* interface Type {
* js: string;
* css: string;
* }
* interface Object {
* form: <Type>,
* to: <Type>;
* }
* @params Object or Array<Obj>
*/
this.options = options;
}
apply(compiler) {
compiler.hooks.compilation.tap(PluginName, (compilation) => {
compilation.mainTemplate.hooks.localVars.tap(
PluginName,
(source, chunk, hash) => {
let _source = source;
if (_source && _source.length > 0 && _source.indexOf('jsonpScriptSrc') !== -1) {
const _hrefMaps = this.getLinkHrefPathMaps(compiler, compilation, chunk, hash, 'js');
const originTemplateString = this.getHrefSrcPath(compiler, compilation.mainTemplate, chunk, hash, 'javascript'); // 不需要修改的chunk路径统一放在一个变量里
const jsChunkTemplate = Template.asString([
Template.indent([
'var originTemplate = function (chunkId) { ',
Template.indent([`return ${originTemplateString}`]),
'};',
`return ${compilation.mainTemplate.requireFn}.p + eval((${JSON.stringify(_hrefMaps)})[chunkId]); //${PluginName} insert!`
]),
''
]);
const insertPos = 'function jsonpScriptSrc(chunkId)';
_source = insertStr(_source, _source.indexOf(insertPos) + insertPos.length + 3, jsChunkTemplate);
return _source;
}
}
);
compilation.mainTemplate.hooks.requireEnsure.tap(PluginName, (source, chunk, hash) => {
let _source = source;
const _hrefMaps = this.getLinkHrefPathMaps(compiler, compilation, chunk, hash, 'css');
const originTemplateString = this.getHrefSrcPath(compiler, compilation.mainTemplate, chunk, hash, 'css/mini-extract');
const cssChunkTemplate = Template.asString([
'var originTemplate = function (chunkId) { ',
Template.indent([
Template.indent([
Template.indent([`return ${originTemplateString}`]),
'};'
])
]),
Template.indent([Template.indent([`href = eval((${JSON.stringify(_hrefMaps)})[chunkId]); //${PluginName} insert!`])]),
''
]);
_source = insertStr(_source, _source.indexOf('var fullhref'), cssChunkTemplate);
return _source;
});
});
compiler.hooks.make.tap(PluginName, (compilation) => { // 因为在webpack内部JavascriptModulesPlugin插件的compiler.hooks.compilation时,webpack对js的Chunk处理刚刚进行,而在本插件中的compiler.hooks.compilation并不能访问到js的chunk,所以就不能在compiler.hooks.compilation中修改js的chunk名称,所以在make时统一对css和js的chunk进行修改
compilation.chunkTemplate.hooks.renderManifest.tap(PluginName, (result) => {
const isJsChunk = /^chunk/g; // result成员的identifier^为chunk时为js chunk
const isCssChunk = /^mini-css-extract/g;// result成员的identifier^为mini-css-extract时为css chunk
const jsChunks = [];
const cssChunks = [];
if (result.length) {
result.forEach((i) => {
isJsChunk.test(i.identifier) && jsChunks.push(i);
isCssChunk.test(i.identifier) && cssChunks.push(i);
});
this.replaceChunkName(cssChunks, 'css');
this.replaceChunkName(jsChunks, 'js');
}
});
});
}
replaceChunkName(result, fileType) { // 修改输出文件名称
if (this.isType(this.options) === 'Array') {
this.options.forEach((option) => {
if (!option.from || !option.to) {
throw new Error(`${PluginName}: Both "from" and "to" are required fields!`);
}
const isReg = this.isType(option.from[fileType]) === 'RegExp';
const isString = typeof option.from[fileType] === 'string';
result.forEach((oldChunk) => {
if ((isReg && option.from[fileType].test(oldChunk.pathOptions.chunk.name)) || (isString && option.from[fileType] === oldChunk.pathOptions.chunk.name)) {
oldChunk.filenameTemplate = option.to[fileType];
}
});
});
} else if (this.isType(this.options) === 'Object') {
const isReg = this.isType(this.options.from[fileType]) === 'RegExp';
const isString = typeof this.options.from[fileType] === 'string';
if (!this.options.from || !this.options.to) {
throw new Error(`${PluginName}: Both "from" and "to" are required fields!`);
}
result.forEach((oldChunk) => {
if ((isReg && this.options.from[fileType].test(oldChunk.pathOptions.chunk.name)) || (isString && this.options.from[fileType] === oldChunk.pathOptions.chunk.name)) {
oldChunk.filenameTemplate = this.options.to[fileType];
}
});
}
}
getHrefSrcPath(compiler, mainTemplate, chunk, hash, moduleType, to) {
let chunkFilename;
const isCssModule = moduleType === fileTypeMaps.css.moduleType;
const isJsModule = moduleType === fileTypeMaps.js.moduleType;
const cssPluginInstalled = compiler.options.plugins.find((i) => i.constructor.name === 'MiniCssExtractPlugin');
if (!cssPluginInstalled && isCssModule) {
throw new Error('Make sure the mini-css-extract-plugin is installed !');
}
if (!cssPluginInstalled.options.chunkFilename && isCssModule) {
throw new Error('Make sure the chunkFilename option for the mini-css-extract-plugin is set !');
}
if (isCssModule) {
chunkFilename = cssPluginInstalled.options.chunkFilename;
}
if (isJsModule) {
chunkFilename = mainTemplate.outputOptions.chunkFilename;
}
const chunkMaps = chunk.getChunkMaps();
return mainTemplate.getAssetPath(JSON.stringify(to || chunkFilename), {
hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`,
hashWithLength: (length) => `" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`,
chunk: {
id: '" + chunkId + "',
hash: `" + ${JSON.stringify(chunkMaps.hash)}[chunkId] + "`,
hashWithLength(length) {
const shortChunkHashMap = Object.create(null);
for (const chunkId of Object.keys(chunkMaps.hash)) {
if (typeof chunkMaps.hash[chunkId] === 'string') {
shortChunkHashMap[chunkId] = chunkMaps.hash[chunkId].substring(0, length);
}
}
return `" + ${JSON.stringify(shortChunkHashMap)}[chunkId] + "`;
},
contentHash: {[moduleType]: `" + ${JSON.stringify(chunkMaps.contentHash[moduleType])}[chunkId] + "`},
contentHashWithLength: {
[moduleType]: (length) => {
const shortContentHashMap = {};
const contentHash = chunkMaps.contentHash[moduleType];
for (const chunkId of Object.keys(contentHash)) {
if (typeof contentHash[chunkId] === 'string') {
shortContentHashMap[chunkId] = contentHash[chunkId].substring(0, length);
}
}
return `" + ${JSON.stringify(shortContentHashMap)}[chunkId] + "`;
}
},
name: `" + (${JSON.stringify(chunkMaps.name)}[chunkId]||chunkId) + "`
},
contentHashType: moduleType
});
}
getLinkHrefPathMaps(compiler, compilation, chunk, hash, fileType = 'css') { // 修改在入口文件中动态引入的文件名称
const linkHrefPathMaps = {};
const { mainTemplate } = compilation;
const chunkMaps = chunk.getChunkMaps();
let isReg;
let isString;
const optionArr = this.isType(this.options) === 'Array';
const optionObj = this.isType(this.options) === 'Object';
Object.keys(chunkMaps.name).forEach((oldChunk) => {
if (optionArr) {
this.options.forEach((option) => {
if (!option.from || !option.to) {
throw new Error(`${PluginName}: Both "from" and "to" are required fields!`);
}
isReg = this.isType(option.from[fileType]) === 'RegExp';
isString = typeof option.from[fileType] === 'string';
if ((isReg && option.from[fileType].test(oldChunk)) || (isString && option.from[fileType] === oldChunk)) {
linkHrefPathMaps[oldChunk] = this.getHrefSrcPath(compiler, mainTemplate, chunk, hash, fileTypeMaps[fileType].moduleType, option.to[fileType]);
}
});
} else if (optionObj) {
isReg = this.isType(this.options.from[fileType]) === 'RegExp';
isString = typeof this.options.from[fileType] === 'string';
if (!this.options.from || !this.options.to) {
throw new Error(`${PluginName}: Both "from" and "to" are required fields!`);
}
if ((isReg && this.options.from[fileType].test(oldChunk)) || (isString && this.options.from[fileType] === oldChunk)) {
linkHrefPathMaps[oldChunk] = this.getHrefSrcPath(compiler, mainTemplate, chunk, hash, fileTypeMaps[fileType].moduleType, this.options.to[fileType]);
}
}
});
Object.keys(Object.assign({}, chunkMaps.hash || {}, chunkMaps.name)).forEach((i) => { // 保留不需要修改的chunk路径 2021/12/27:当chunk中存在chunk-xxx时,从chunkMaps.hash里取值
if (!Object.hasOwnProperty.call(linkHrefPathMaps, i)) {
linkHrefPathMaps[i] = 'originTemplate(chunkId)'; // originTemplate是不需要修改的chunk路径变量,对运行时代码优化,减少冗余代码
}
});
return linkHrefPathMaps;
}
isType(obj) {
return Object.prototype.toString.call(obj).replace(/^\[object ((\S+))\]$/g, '$1');
}
}
module.exports = ChunkFilenameReplaceWebpackPlugin;