diff --git a/lib/RuntimeGlobals.js b/lib/RuntimeGlobals.js index 1455ff988..52f1bdb24 100644 --- a/lib/RuntimeGlobals.js +++ b/lib/RuntimeGlobals.js @@ -105,6 +105,16 @@ exports.preloadChunk = "__webpack_require__.G"; */ exports.preloadChunkHandlers = "__webpack_require__.H"; +/** + * a flag when a module/chunk/tree has css modules + */ +exports.hasPreloadUrl = "has preload url"; + +/** + * the url preload function + */ +exports.preloadUrl = "__webpack_require__.B"; + /** * the exported property define getters function */ diff --git a/lib/WebpackOptionsApply.js b/lib/WebpackOptionsApply.js index ef6acf43f..ccc128cf8 100644 --- a/lib/WebpackOptionsApply.js +++ b/lib/WebpackOptionsApply.js @@ -12,6 +12,7 @@ const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin"); const JsonModulesPlugin = require("./json/JsonModulesPlugin"); const ChunkPrefetchPreloadPlugin = require("./prefetch/ChunkPrefetchPreloadPlugin"); +const URLPreloadPlugin = require("./prefetch/URLPreloadPlugin"); const EntryOptionPlugin = require("./EntryOptionPlugin"); const RecordIdsPlugin = require("./RecordIdsPlugin"); @@ -180,6 +181,7 @@ class WebpackOptionsApply extends OptionsApply { } new ChunkPrefetchPreloadPlugin().apply(compiler); + new URLPreloadPlugin().apply(compiler); if (typeof options.output.chunkFormat === "string") { switch (options.output.chunkFormat) { diff --git a/lib/asset/AssetModulesPlugin.js b/lib/asset/AssetModulesPlugin.js index f77fc8288..55004fdf0 100644 --- a/lib/asset/AssetModulesPlugin.js +++ b/lib/asset/AssetModulesPlugin.js @@ -72,7 +72,7 @@ const getAssetSourceGenerator = memoize(() => ); const type = ASSET_MODULE_TYPE; -const plugin = "AssetModulesPlugin"; +const PLUGIN_NAME = "AssetModulesPlugin"; class AssetModulesPlugin { /** @@ -82,11 +82,11 @@ class AssetModulesPlugin { */ apply(compiler) { compiler.hooks.compilation.tap( - plugin, + PLUGIN_NAME, (compilation, { normalModuleFactory }) => { normalModuleFactory.hooks.createParser .for(ASSET_MODULE_TYPE) - .tap(plugin, parserOptions => { + .tap(PLUGIN_NAME, parserOptions => { validateParserOptions(parserOptions); parserOptions = cleverMerge( compiler.options.module.parser.asset, @@ -107,21 +107,21 @@ class AssetModulesPlugin { }); normalModuleFactory.hooks.createParser .for(ASSET_MODULE_TYPE_INLINE) - .tap(plugin, parserOptions => { + .tap(PLUGIN_NAME, parserOptions => { const AssetParser = getAssetParser(); return new AssetParser(true); }); normalModuleFactory.hooks.createParser .for(ASSET_MODULE_TYPE_RESOURCE) - .tap(plugin, parserOptions => { + .tap(PLUGIN_NAME, parserOptions => { const AssetParser = getAssetParser(); return new AssetParser(false); }); normalModuleFactory.hooks.createParser .for(ASSET_MODULE_TYPE_SOURCE) - .tap(plugin, parserOptions => { + .tap(PLUGIN_NAME, parserOptions => { const AssetSourceParser = getAssetSourceParser(); return new AssetSourceParser(); @@ -134,7 +134,7 @@ class AssetModulesPlugin { ]) { normalModuleFactory.hooks.createGenerator .for(type) - .tap(plugin, generatorOptions => { + .tap(PLUGIN_NAME, generatorOptions => { validateGeneratorOptions[type](generatorOptions); let dataUrl = undefined; @@ -171,13 +171,13 @@ class AssetModulesPlugin { } normalModuleFactory.hooks.createGenerator .for(ASSET_MODULE_TYPE_SOURCE) - .tap(plugin, () => { + .tap(PLUGIN_NAME, () => { const AssetSourceGenerator = getAssetSourceGenerator(); return new AssetSourceGenerator(); }); - compilation.hooks.renderManifest.tap(plugin, (result, options) => { + compilation.hooks.renderManifest.tap(PLUGIN_NAME, (result, options) => { const { chunkGraph } = compilation; const { chunk, codeGenerationResults } = options; @@ -217,9 +217,8 @@ class AssetModulesPlugin { return result; }); - compilation.hooks.prepareModuleExecution.tap( - "AssetModulesPlugin", + PLUGIN_NAME, (options, context) => { const { codeGenerationResult } = options; const source = codeGenerationResult.sources.get(ASSET_MODULE_TYPE); diff --git a/lib/css/CssLoadingRuntimeModule.js b/lib/css/CssLoadingRuntimeModule.js index 0b1a852c0..0adc50e91 100644 --- a/lib/css/CssLoadingRuntimeModule.js +++ b/lib/css/CssLoadingRuntimeModule.js @@ -55,7 +55,7 @@ class CssLoadingRuntimeModule extends RuntimeModule { * @param {ReadOnlyRuntimeRequirements} runtimeRequirements runtime requirements */ constructor(runtimeRequirements) { - super("css loading", 10); + super("css loading", RuntimeModule.STAGE_ATTACH); this._runtimeRequirements = runtimeRequirements; } diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index eedd4fdb3..fec9de5b3 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -5,6 +5,7 @@ "use strict"; +const InitFragment = require("../InitFragment"); const RuntimeGlobals = require("../RuntimeGlobals"); const RawDataUrlModule = require("../asset/RawDataUrlModule"); const { @@ -30,6 +31,13 @@ const ModuleDependency = require("./ModuleDependency"); /** @typedef {import("../util/Hash")} Hash */ /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */ +/** + * @typedef {object} PreloadOptions + * @property {number=} preloadOrder + * @property {string=} preloadAs + * @property {("low" | "high" | "auto")=} fetchPriority + */ + const getIgnoredRawDataUrlModule = memoize(() => { return new RawDataUrlModule("data:,", `ignored-asset`, `(ignored asset)`); }); @@ -40,12 +48,14 @@ class URLDependency extends ModuleDependency { * @param {Range} range range of the arguments of new URL( |> ... <| ) * @param {Range} outerRange range of the full |> new URL(...) <| * @param {boolean=} relative use relative urls instead of absolute with base uri + * @param {PreloadOptions=} groupOptions use relative urls instead of absolute with base uri */ - constructor(request, range, outerRange, relative) { + constructor(request, range, outerRange, relative, groupOptions) { super(request); this.range = range; this.outerRange = outerRange; this.relative = relative || false; + this.groupOptions = groupOptions; /** @type {Set | boolean | undefined} */ this.usedByExports = undefined; } @@ -85,6 +95,7 @@ class URLDependency extends ModuleDependency { const { write } = context; write(this.outerRange); write(this.relative); + write(this.groupOptions); write(this.usedByExports); super.serialize(context); } @@ -96,6 +107,7 @@ class URLDependency extends ModuleDependency { const { read } = context; this.outerRange = read(); this.relative = read(); + this.groupOptions = read(); this.usedByExports = read(); super.deserialize(context); } @@ -116,7 +128,8 @@ URLDependency.Template = class URLDependencyTemplate extends ( moduleGraph, runtimeRequirements, runtimeTemplate, - runtime + runtime, + initFragments } = templateContext; const dep = /** @type {URLDependency} */ (dependency); const connection = moduleGraph.getConnection(dep); @@ -132,6 +145,10 @@ URLDependency.Template = class URLDependencyTemplate extends ( runtimeRequirements.add(RuntimeGlobals.require); + const module = moduleGraph.getModule(dep); + const request = dep.request; + const weak = false; + if (dep.relative) { runtimeRequirements.add(RuntimeGlobals.relativeUrl); source.replace( @@ -141,10 +158,10 @@ URLDependency.Template = class URLDependencyTemplate extends ( RuntimeGlobals.relativeUrl }(${runtimeTemplate.moduleRaw({ chunkGraph, - module: moduleGraph.getModule(dep), - request: dep.request, + module, + request, runtimeRequirements, - weak: false + weak })})` ); } else { @@ -155,13 +172,45 @@ URLDependency.Template = class URLDependencyTemplate extends ( dep.range[1] - 1, `/* asset import */ ${runtimeTemplate.moduleRaw({ chunkGraph, - module: moduleGraph.getModule(dep), - request: dep.request, + module, + request, runtimeRequirements, - weak: false + weak })}, ${RuntimeGlobals.baseURI}` ); } + + if (dep.groupOptions && dep.groupOptions.preloadOrder !== undefined) { + runtimeRequirements.add(RuntimeGlobals.hasPreloadUrl); + + const moduleId = runtimeTemplate.moduleId({ + module, + chunkGraph, + request, + weak + }); + const needRelativeParam = + dep.relative || runtimeRequirements.has(RuntimeGlobals.relativeUrl); + const preloadAs = dep.groupOptions.preloadAs; + const fetchPriority = dep.groupOptions.fetchPriority; + + initFragments.push( + new InitFragment( + `${RuntimeGlobals.preloadUrl}(${moduleId}, ${JSON.stringify( + preloadAs + )}${ + fetchPriority + ? `, ${JSON.stringify(fetchPriority)}` + : needRelativeParam + ? ", undefined" + : "" + }${needRelativeParam ? `, ${dep.relative}` : ""});\n`, + InitFragment.STAGE_CONSTANTS, + dep.groupOptions.preloadOrder * -1, + `__webpack_url_preload__(${moduleId})` + ) + ); + } } }; diff --git a/lib/dependencies/URLPlugin.js b/lib/dependencies/URLPlugin.js index 7ae95ea74..ed71081f0 100644 --- a/lib/dependencies/URLPlugin.js +++ b/lib/dependencies/URLPlugin.js @@ -6,10 +6,12 @@ "use strict"; const { pathToFileURL } = require("url"); +const CommentCompilationWarning = require("../CommentCompilationWarning"); const { JAVASCRIPT_MODULE_TYPE_AUTO, JAVASCRIPT_MODULE_TYPE_ESM } = require("../ModuleTypeConstants"); +const UnsupportedFeatureWarning = require("../UnsupportedFeatureWarning"); const BasicEvaluatedExpression = require("../javascript/BasicEvaluatedExpression"); const { approve } = require("../javascript/JavascriptParserHelpers"); const InnerGraph = require("../optimize/InnerGraph"); @@ -23,6 +25,7 @@ const URLDependency = require("./URLDependency"); /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */ /** @typedef {import("../javascript/JavascriptParser")} Parser */ /** @typedef {import("../javascript/JavascriptParser").Range} Range */ +/** @typedef {import("./URLDependency").PreloadOptions} PreloadOptions */ const PLUGIN_NAME = "URLPlugin"; @@ -105,6 +108,94 @@ class URLPlugin { if (!request) return; + /** @type {PreloadOptions} */ + const preloadOptions = {}; + const { urlPreload, urlPreloadAs, urlFetchPriority } = + parserOptions; + if (urlPreload !== undefined && urlPreload !== false) + preloadOptions.preloadOrder = + urlPreload === true ? 0 : urlPreload; + if (urlPreloadAs !== undefined && urlPreloadAs !== false) + preloadOptions.preloadAs = urlPreloadAs; + if (urlFetchPriority !== undefined && urlFetchPriority !== false) + preloadOptions.fetchPriority = urlFetchPriority; + + const { options: importOptions, errors: commentErrors } = + parser.parseCommentOptions(/** @type {Range} */ (expr.range)); + + if (commentErrors) { + for (const e of commentErrors) { + const { comment } = e; + parser.state.module.addWarning( + new CommentCompilationWarning( + `Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`, + comment.loc + ) + ); + } + } + + if (importOptions) { + if (importOptions.webpackPreload !== undefined) { + if (importOptions.webpackPreload === true) { + preloadOptions.preloadOrder = 0; + } else if (typeof importOptions.webpackPreload === "number") { + preloadOptions.preloadOrder = importOptions.webpackPreload; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreload\` expected true or a number, but received: ${importOptions.webpackPreload}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + if (importOptions.webpackPreloadAs !== undefined) { + if (typeof importOptions.webpackPreloadAs === "string") { + preloadOptions.preloadAs = importOptions.webpackPreloadAs; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreloadAs\` expected string, but received: ${importOptions.webpackPreloadAs}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + if (importOptions.webpackFetchPriority !== undefined) { + if ( + typeof importOptions.webpackFetchPriority === "string" && + ["high", "low", "auto"].includes( + importOptions.webpackFetchPriority + ) + ) { + preloadOptions.fetchPriority = + importOptions.webpackFetchPriority; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackFetchPriority\` expected true or "low", "high" or "auto", but received: ${importOptions.webpackFetchPriority}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + } + + if ( + preloadOptions.preloadOrder !== undefined && + preloadOptions.preloadAs === undefined + ) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreload\` for \`new URL(...)\` expected \`webpackPreloadAs\` comment.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + + // TODO - `type` and `media` support + const [arg1, arg2] = expr.arguments; const dep = new URLDependency( request, @@ -113,7 +204,8 @@ class URLPlugin { /** @type {Range} */ (arg2.range)[1] ], /** @type {Range} */ (expr.range), - relative + relative, + preloadOptions ); dep.loc = /** @type {DependencyLocation} */ (expr.loc); parser.state.current.addDependency(dep); diff --git a/lib/index.js b/lib/index.js index 9764b4fa8..d44723f60 100644 --- a/lib/index.js +++ b/lib/index.js @@ -464,6 +464,9 @@ module.exports = mergeExports(fn, { prefetch: { get ChunkPrefetchPreloadPlugin() { return require("./prefetch/ChunkPrefetchPreloadPlugin"); + }, + get URLPreloadPlugin() { + return require("./prefetch/URLPreloadPlugin"); } }, diff --git a/lib/prefetch/URLPreloadPlugin.js b/lib/prefetch/URLPreloadPlugin.js new file mode 100644 index 000000000..4c701dcfd --- /dev/null +++ b/lib/prefetch/URLPreloadPlugin.js @@ -0,0 +1,35 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const RuntimeGlobals = require("../RuntimeGlobals"); +const URLPreloadRuntimeModule = require("./URLPreloadRuntimeModule"); + +/** @typedef {import("../Chunk")} Chunk */ +/** @typedef {import("../ChunkGroup").RawChunkGroupOptions} RawChunkGroupOptions */ +/** @typedef {import("../Compiler")} Compiler */ + +const PLUGIN_NAME = "URLPreloadPlugin"; + +class URLPreloadPlugin { + /** + * @param {Compiler} compiler the compiler + * @returns {void} + */ + apply(compiler) { + compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.hasPreloadUrl) + .tap(PLUGIN_NAME, (chunk, set) => { + set.add(RuntimeGlobals.hasOwnProperty); + compilation.addRuntimeModule(chunk, new URLPreloadRuntimeModule(set)); + return true; + }); + }); + } +} + +module.exports = URLPreloadPlugin; diff --git a/lib/prefetch/URLPreloadRuntimeModule.js b/lib/prefetch/URLPreloadRuntimeModule.js new file mode 100644 index 000000000..65595cf52 --- /dev/null +++ b/lib/prefetch/URLPreloadRuntimeModule.js @@ -0,0 +1,127 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php +*/ + +"use strict"; + +const { SyncWaterfallHook } = require("tapable"); +const Compilation = require("../Compilation"); +const RuntimeGlobals = require("../RuntimeGlobals"); +const RuntimeModule = require("../RuntimeModule"); +const Template = require("../Template"); + +/** @typedef {import("../Module").ReadOnlyRuntimeRequirements} ReadOnlyRuntimeRequirements */ +/** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */ + +/** + * @typedef {object} URLPreloadRuntimeModulePluginHooks + * @property {SyncWaterfallHook<[string]>} linkPreload + */ + +/** @type {WeakMap} */ +const compilationHooksMap = new WeakMap(); + +class URLPreloadRuntimeModule extends RuntimeModule { + /** + * @param {Compilation} compilation the compilation + * @returns {URLPreloadRuntimeModulePluginHooks} hooks + */ + static getCompilationHooks(compilation) { + if (!(compilation instanceof Compilation)) { + throw new TypeError( + "The 'compilation' argument must be an instance of Compilation" + ); + } + let hooks = compilationHooksMap.get(compilation); + if (hooks === undefined) { + hooks = { + linkPreload: new SyncWaterfallHook(["source"]) + }; + compilationHooksMap.set(compilation, hooks); + } + return hooks; + } + + /** + * @param {ReadOnlyRuntimeRequirements} runtimeRequirements runtime requirements + */ + constructor(runtimeRequirements) { + super("asset preloading", RuntimeModule.STAGE_ATTACH); + + this.runtimeRequirements = runtimeRequirements; + } + + /** + * @returns {string | null} runtime code + */ + generate() { + const compilation = /** @type {Compilation} */ (this.compilation); + const { + runtimeTemplate, + outputOptions: { crossOriginLoading } + } = compilation; + const { linkPreload } = + URLPreloadRuntimeModule.getCompilationHooks(compilation); + const hasBaseURI = this.runtimeRequirements.has(RuntimeGlobals.baseURI); + const hasRelativeUrl = this.runtimeRequirements.has( + RuntimeGlobals.relativeUrl + ); + + let urlConstructor; + + // TODO improve me + if (hasBaseURI && !hasRelativeUrl) { + urlConstructor = "URL"; + } else if (!hasBaseURI && hasRelativeUrl) { + urlConstructor = RuntimeGlobals.relativeUrl; + } else { + urlConstructor = `(relative ? ${RuntimeGlobals.relativeUrl} : URL)`; + } + + return Template.asString([ + `${RuntimeGlobals.preloadUrl} = ${runtimeTemplate.basicFunction( + `moduleId, as, fetchPriority${hasRelativeUrl ? ", relative" : ""}`, + [ + `if((!${RuntimeGlobals.hasOwnProperty}(__webpack_module_cache__, moduleId))) {`, + Template.indent([ + linkPreload.call( + Template.asString([ + "var link = document.createElement('link');", + `if (${RuntimeGlobals.scriptNonce}) {`, + Template.indent( + `link.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});` + ), + "}", + "if(fetchPriority) {", + Template.indent( + 'link.setAttribute("fetchpriority", fetchPriority);' + ), + "}", + 'link.rel = "preload";', + "link.as = as;", + `link.href = new ${urlConstructor}(${RuntimeGlobals.require}(moduleId), ${RuntimeGlobals.baseURI});`, + crossOriginLoading + ? crossOriginLoading === "use-credentials" + ? 'link.crossOrigin = "use-credentials";' + : Template.asString([ + "if (link.href.indexOf(window.location.origin + '/') !== 0) {", + Template.indent( + `link.crossOrigin = ${JSON.stringify( + crossOriginLoading + )};` + ), + "}" + ]) + : "" + ]) + ), + "document.head.appendChild(link);" + ]), + "}" + ] + )};` + ]); + } +} + +module.exports = URLPreloadRuntimeModule; diff --git a/types.d.ts b/types.d.ts index b2a4f1c54..af344edac 100644 --- a/types.d.ts +++ b/types.d.ts @@ -14275,6 +14275,10 @@ declare interface TrustedTypes { policyName?: string; } declare const UNDEFINED_MARKER: unique symbol; +declare class URLPreloadPlugin { + constructor(); + apply(compiler: Compiler): void; +} /** * `URL` class is a global reference for `require('url').URL` @@ -15070,6 +15074,8 @@ declare namespace exports { export let prefetchChunkHandlers: "__webpack_require__.F"; export let preloadChunk: "__webpack_require__.G"; export let preloadChunkHandlers: "__webpack_require__.H"; + export let hasPreloadUrl: "has preload url"; + export let preloadUrl: "__webpack_require__.B"; export let definePropertyGetters: "__webpack_require__.d"; export let makeNamespaceObject: "__webpack_require__.r"; export let createFakeNamespaceObject: "__webpack_require__.t"; @@ -15231,7 +15237,7 @@ declare namespace exports { export { GetChunkFilenameRuntimeModule, LoadScriptRuntimeModule }; } export namespace prefetch { - export { ChunkPrefetchPreloadPlugin }; + export { ChunkPrefetchPreloadPlugin, URLPreloadPlugin }; } export namespace web { export {