Merge remote-tracking branch 'upstream/main'

This commit is contained in:
2026-08-26 09:31:34 +08:00
parent 9b8c4bfab6
commit 394eb0285d
34137 changed files with 3589424 additions and 0 deletions
@@ -0,0 +1,45 @@
'use strict';
const CHUNK_OPTIONS = ['all', 'async'];
const getPublicPath = require('./common.js').getPublicPath;
const createResourceHint = require('./resource-hints.js').createResourceHint;
const matches = require('./common.js').matches;
const addAsyncChunkResourceHints = (chunks, options) => {
const getRef = generateRef(options);
const hints = [];
chunks
.filter(chunk => !isInitial(chunk))
.reduce(
(files, chunk) => files.concat(chunk.files),
[])
.forEach(file => {
if (optionsMatch(options.preload, file)) {
hints.push(createResourceHint('preload', getRef(file)));
} else if (optionsMatch(options.prefetch, file)) {
hints.push(createResourceHint('prefetch', getRef(file)));
}
});
return hints;
};
const isInitial = chunk =>
chunk.canBeInitial
? chunk.canBeInitial()
: chunk.isInitial
? chunk.isInitial()
: chunk.isInitial;
const optionsMatch = (option, file) => {
return matches(option.chunks, CHUNK_OPTIONS) && matches(file, option.test);
};
const generateRef = options => {
const publicPath = getPublicPath(options);
return publicPath
? file => publicPath + file
: file => file;
};
module.exports = addAsyncChunkResourceHints;
+71
View File
@@ -0,0 +1,71 @@
'use strict';
const debug = require('debug')('ScriptExt');
const separator = '/';
const isScript = (tag) => tag.tagName === 'script';
const isResourceLink = (tag) => tag.tagName === 'link' && tag.attributes && tag.attributes.as === 'script';
const hasScriptName = tag => {
if (isScript(tag)) {
return tag.attributes && tag.attributes.src;
} else if (isResourceLink(tag)) {
return tag.attributes && tag.attributes.href;
} else {
return false;
}
};
const getRawScriptName = tag => {
if (isScript(tag)) {
return (tag.attributes && tag.attributes.src) || '';
} else if (isResourceLink(tag)) {
return (tag.attributes && tag.attributes.href) || '';
} else {
return '';
}
};
const getPublicPath = options => {
const output = options.compilationOptions.output;
if (output) {
const publicPath = output.publicPath;
if (publicPath) {
return publicPath.endsWith(separator) ? publicPath : publicPath + separator;
}
}
};
const getScriptName = (options, tag) => {
let scriptName = getRawScriptName(tag);
const publicPath = getPublicPath(options);
if (publicPath) {
scriptName = scriptName.replace(publicPath, '');
}
if (options.htmlWebpackOptions.hash) {
scriptName = scriptName.split('?', 1)[0];
}
return scriptName;
};
const matches = (toMatch, matchers) => {
return matchers.some((matcher) => {
if (matcher instanceof RegExp) {
return matcher.test(toMatch);
} else {
return toMatch.includes(matcher);
}
});
};
module.exports = {
debug,
getPublicPath,
getRawScriptName,
getScriptName,
hasScriptName,
isResourceLink,
isScript,
matches
};
+116
View File
@@ -0,0 +1,116 @@
'use strict';
const PLUGIN = require('./constants.js').PLUGIN;
const DEFAULT_HASH = {
test: []
};
const DEFAULT_RESOURCE_HINT_HASH = {
test: [],
chunks: 'initial'
};
const DEFAULT_CUSTOM_HASH = {
test: [],
attribute: '',
value: true
};
const DEFAULT_OPTIONS = {
inline: DEFAULT_HASH,
sync: DEFAULT_HASH,
async: DEFAULT_HASH,
defer: DEFAULT_HASH,
module: DEFAULT_HASH,
prefetch: DEFAULT_RESOURCE_HINT_HASH,
preload: DEFAULT_RESOURCE_HINT_HASH,
defaultAttribute: 'sync',
removeInlinedAssets: true,
custom: []
};
const POSSIBLE_VALUES = ['chunks', 'attribute', 'value'];
const normaliseOptions = options => {
if (!options) return DEFAULT_OPTIONS;
validate(options);
const normalised = Object.assign({}, DEFAULT_OPTIONS, options);
// now overwrite values which are not of DEFAULT_HASH form
Object.keys(options).forEach(key => {
const value = options[key];
switch (key) {
case 'inline':
case 'sync':
case 'async':
case 'defer':
case 'module':
normalised[key] = normaliseAttribute(value);
break;
case 'prefetch':
case 'preload':
normalised[key] = normaliseResourceHint(value);
break;
case 'custom':
normalised[key] = normaliseCustomArray(value);
break;
default:
break;
}
});
return normalised;
};
const validate = options => {
const failureTests = []; // TODO!
if (failureTests.some(test => test(options))) error();
};
const error = () => {
throw new Error(`${PLUGIN}: invalid configuration - please see https://github.com/numical/script-ext-html-webpack-plugin#configuration`);
};
const normaliseValue = (defaultProps, value) => {
const normalised = Object.assign({}, defaultProps);
if (value) {
normalised.test = convertToArray(value, () => {
if (typeof value === 'object') {
POSSIBLE_VALUES.forEach(key => copyValue(key, normalised, value));
if (value.test) {
return convertToArray(value.test, error);
} else {
error();
}
}
});
}
return normalised;
};
const normaliseAttribute = normaliseValue.bind(null, DEFAULT_HASH);
const normaliseResourceHint = normaliseValue.bind(null, DEFAULT_RESOURCE_HINT_HASH);
const normaliseCustomAttribute = normaliseValue.bind(null, DEFAULT_CUSTOM_HASH);
const normaliseCustomArray = value => {
const array = Array.isArray(value) ? value : [value];
return array.map(normaliseCustomAttribute);
};
const convertToArray = (value, elseFn) => {
if (typeof value === 'string') {
return [value];
} else if (value instanceof RegExp) {
return [value];
} else if (Array.isArray(value)) {
return value;
} else {
return elseFn();
}
};
const copyValue = (key, to, from) => {
if (Object.prototype.hasOwnProperty.call(from, key)) {
to[key] = from[key];
}
};
module.exports = normaliseOptions;
module.exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
@@ -0,0 +1,9 @@
'use strict';
const PLUGIN = 'ScriptExtHtmlWebpackPlugin';
const EVENT = 'html-webpack-plugin-alter-asset-tags';
module.exports = {
PLUGIN,
EVENT
};
@@ -0,0 +1,46 @@
'use strict';
const CONSTANTS = require('./constants.js');
const common = require('./common.js');
const debug = common.debug;
const getScriptName = common.getScriptName;
const isResourceLink = common.isResourceLink;
const isScript = common.isScript;
const matches = common.matches;
const shouldAdd = options => {
return options.custom.length > 0;
};
const add = (options, tags) => {
const update = updateElement.bind(null, options);
return tags.map(update);
};
const updateElement = (options, tag) => {
return (isScript(tag) || isResourceLink(tag))
? updateScriptElement(options, tag)
: tag;
};
const updateScriptElement = (options, tag) => {
const scriptName = getScriptName(options, tag);
let updated = false;
options.custom.forEach(customOption => {
if (matches(scriptName, customOption.test)) {
tag.attributes = tag.attributes || {};
tag.attributes[customOption.attribute] = customOption.value;
updated = true;
}
});
if (updated) {
debug(`${CONSTANTS.PLUGIN}: updated to: ${JSON.stringify(tag)}`);
}
return tag;
};
module.exports = {
shouldAdd,
add
};
+83
View File
@@ -0,0 +1,83 @@
'use strict';
const CONSTANTS = require('./constants.js');
const SYNC = 'sync';
const ATTRIBUTE_PRIORITIES = [SYNC, 'async', 'defer'];
const common = require('./common.js');
const debug = common.debug;
const isScript = common.isScript;
const matches = common.matches;
const getScriptName = common.getScriptName;
const shouldUpdate = (options) => {
if (ATTRIBUTE_PRIORITIES.indexOf(options.defaultAttribute) < 0) {
throw new Error(`${CONSTANTS.PLUGIN}: invalid default attribute`);
}
return !(options.defaultAttribute === SYNC &&
options.inline.test.length === 0 &&
options.async.test.length === 0 &&
options.defer.test.length === 0 &&
options.module.test.length === 0);
};
const update = (assets, options, tags) => {
const update = updateElement.bind(null, assets, options);
return tags.map(update);
};
const updateElement = (assets, options, tag) => {
return (isScript(tag))
? updateScriptElement(assets, options, tag)
: tag;
};
const updateScriptElement = (assets, options, tag) => {
debug(`${CONSTANTS.EVENT}: processing <script> element: ${JSON.stringify(tag)}`);
return (isInline(options, tag))
? replaceWithInlineElement(assets, options, tag)
: updateSrcElement(options, tag);
};
const isInline = (options, tag) =>
matches(getScriptName(options, tag), options.inline.test);
const replaceWithInlineElement = (assets, options, tag) => {
const scriptName = getScriptName(options, tag);
const asset = assets[scriptName];
if (!asset) throw new Error(`${CONSTANTS.PLUGIN}: no asset with href '${scriptName}'`);
const newTag = {
tagName: 'script',
closeTag: true,
innerHTML: asset.source()
};
debug(`${CONSTANTS.PLUGIN}: replaced by: ${JSON.stringify(newTag)}`);
return newTag;
};
const updateSrcElement = (options, tag) => {
const scriptName = getScriptName(options, tag);
// select new attribute, if any, by priority
let newAttribute;
ATTRIBUTE_PRIORITIES.some(attribute => {
if (matches(scriptName, options[attribute].test)) {
newAttribute = attribute;
return true;
}
});
if (!newAttribute) newAttribute = options.defaultAttribute;
if (newAttribute !== SYNC) {
tag.attributes[newAttribute] = true;
}
// possibly overwrite existing type attribute
if (matches(scriptName, options.module.test)) {
tag.attributes.type = 'module';
}
debug(`${CONSTANTS.PLUGIN}: updated to: ${JSON.stringify(tag)}`);
return tag;
};
module.exports = {
shouldUpdate,
update
};
@@ -0,0 +1,32 @@
'use strict';
const CHUNK_OPTIONS = ['all', 'initial'];
const createResourceHint = require('./resource-hints.js').createResourceHint;
const common = require('./common.js');
const matches = common.matches;
const getScriptName = common.getScriptName;
const getRawScriptName = common.getRawScriptName;
const hasScriptName = common.hasScriptName;
const optionsMatch = (option, scriptName) => {
return matches(option.chunks, CHUNK_OPTIONS) && matches(scriptName, option.test);
};
const addInitialChunkResourceHints = (options, tags) => {
return tags
.filter(hasScriptName)
.reduce((hints, tag) => {
const scriptName = getScriptName(options, tag);
if (optionsMatch(options.preload, scriptName)) {
hints.push(createResourceHint('preload', getRawScriptName(tag)));
} else if (optionsMatch(options.prefetch, scriptName)) {
hints.push(createResourceHint('prefetch', getRawScriptName(tag)));
}
return hints;
},
[]
);
};
module.exports = addInitialChunkResourceHints;
+118
View File
@@ -0,0 +1,118 @@
'use strict';
const htmlWebpackPlugin = require('html-webpack-plugin');
const { EVENT, PLUGIN } = require('./constants.js');
const debug = require('./common.js').debug;
const matches = require('./common.js').matches;
const normaliseOptions = require('./config.js');
const shouldAddResourceHints = require('./resource-hints.js').shouldAddResourceHints;
const addInitialChunkResourceHints = require('./initial-chunk-resource-hints.js');
const addAsyncChunkResourceHints = require('./async-chunk-resource-hints.js');
const elements = require('./elements.js');
const customAttributes = require('./custom-attributes.js');
const debugEvent = msg => debug(`${EVENT}: ${msg}`);
const falsySafeConcat = arrays =>
arrays.reduce(
(combined, array) => array ? combined.concat(array) : combined,
[]
);
const getHtmlWebpackOptions = pluginArgs =>
(pluginArgs && pluginArgs.plugin && pluginArgs.plugin.options)
? pluginArgs.plugin.options
: {};
const getCompilationOptions = compilation =>
(compilation && compilation.options) ? compilation.options : {};
class ScriptExtHtmlWebpackPlugin {
constructor (options) {
this.options = normaliseOptions(options);
}
apply (compiler) {
const compile = this.compilationCallback.bind(this);
const emit = this.emitCallback.bind(this);
if (compiler.hooks) {
compiler.hooks.compilation.tap(PLUGIN, compile);
compiler.hooks.emit.tap(PLUGIN, emit);
} else {
compiler.plugin('compilation', compile);
compiler.plugin('emit', emit);
}
}
compilationCallback (compilation) {
const alterAssetTags = this.alterAssetTagsCallback.bind(this, compilation);
if (compilation.hooks) {
const alterAssetTagGroups = compilation.hooks.htmlWebpackPluginAlterAssetTags || htmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups;
alterAssetTagGroups.tap(PLUGIN, alterAssetTags);
} else {
compilation.plugin(EVENT, alterAssetTags);
}
}
alterAssetTagsCallback (compilation, pluginArgs, callback) {
const options = this.options;
const headTagName = Object.prototype.hasOwnProperty.call(pluginArgs, 'headTags') ? 'headTags' : 'head';
const bodyTagName = Object.prototype.hasOwnProperty.call(pluginArgs, 'bodyTags') ? 'bodyTags' : 'body';
try {
options.htmlWebpackOptions = getHtmlWebpackOptions(pluginArgs);
options.compilationOptions = getCompilationOptions(compilation);
debugEvent('starting');
if (elements.shouldUpdate(options)) {
debugEvent('replacing <head> <script> elements');
pluginArgs[headTagName] = elements.update(compilation.assets, options, pluginArgs[headTagName]);
debugEvent('replacing <body> <script> elements');
pluginArgs[bodyTagName] = elements.update(compilation.assets, options, pluginArgs[bodyTagName]);
}
if (shouldAddResourceHints(options)) {
debugEvent('adding resource hints');
pluginArgs[headTagName] = falsySafeConcat([
pluginArgs[headTagName],
addInitialChunkResourceHints(options, pluginArgs[headTagName]),
addInitialChunkResourceHints(options, pluginArgs[bodyTagName]),
addAsyncChunkResourceHints(compilation.chunks, options)
]);
}
if (customAttributes.shouldAdd(options)) {
debugEvent('adding custom attribues to <head> <script> elements');
pluginArgs[headTagName] = customAttributes.add(options, pluginArgs[headTagName]);
debugEvent('adding custom attributes to <body> <script> elements');
pluginArgs[bodyTagName] = customAttributes.add(options, pluginArgs[bodyTagName]);
}
debugEvent('completed');
if (callback) {
callback(null, pluginArgs);
}
} catch (err) {
if (callback) {
callback(err);
} else {
compilation.errors.push(err);
}
}
}
emitCallback (compilation, callback) {
const options = this.options;
if (options.inline.test.length > 0 && options.removeInlinedAssets) {
debug('emit: deleting assets');
Object.keys(compilation.assets).forEach((assetName) => {
if (matches(assetName, options.inline.test)) {
debug(`emit: deleting asset '${assetName}'`);
delete compilation.assets[assetName];
}
});
}
if (callback) {
callback();
}
}
}
module.exports = ScriptExtHtmlWebpackPlugin;
@@ -0,0 +1,23 @@
'use strict';
const shouldAddResourceHints = options => {
return !(options.prefetch.test.length === 0 &&
options.preload.test.length === 0);
};
const createResourceHint = (rel, href) => {
return {
tagName: 'link',
selfClosingTag: true,
attributes: {
rel: rel,
href: href,
as: 'script'
}
};
};
module.exports = {
shouldAddResourceHints,
createResourceHint
};