Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for link preload/prefetch #1043

Merged
merged 9 commits into from Apr 16, 2024
178 changes: 176 additions & 2 deletions src/index.js
Expand Up @@ -5,6 +5,11 @@ const path = require("path");
const { validate } = require("schema-utils");
const { SyncWaterfallHook } = require("tapable");

// @ts-ignore
const JsonpChunkLoadingRuntimeModule = require("webpack/lib/web/JsonpChunkLoadingRuntimeModule");
// @ts-ignore
const compileBooleanMatcher = require("webpack/lib/util/compileBooleanMatcher");

const schema = require("./plugin-options.json");
const {
trueFn,
Expand Down Expand Up @@ -841,6 +846,27 @@ class MiniCssExtractPlugin {
return obj;
};

/**
* @param {Chunk} chunk chunk
* @param {ChunkGraph} chunkGraph chunk graph
* @returns {boolean} true, when the chunk has css
*/
function chunkHasCss(chunk, chunkGraph) {
// this function replace:
// const chunkHasCss = require("webpack/lib/css/CssModulesPlugin").chunkHasCss;
return (
!!chunkGraph.getChunkModulesIterableBySourceType(chunk, "css") ||
!!chunkGraph.getChunkModulesIterableBySourceType(
chunk,
"css-import"
) ||
!!chunkGraph.getChunkModulesIterableBySourceType(
chunk,
"css/mini-extract"
)
);
}

class CssLoadingRuntimeModule extends RuntimeModule {
/**
* @param {Set<string>} runtimeRequirements
Expand All @@ -854,22 +880,38 @@ class MiniCssExtractPlugin {
}

generate() {
const { chunk, runtimeRequirements } = this;
const { chunkGraph, chunk, runtimeRequirements } = this;
const {
runtimeTemplate,
outputOptions: { crossOriginLoading },
outputOptions: { chunkLoadingGlobal, crossOriginLoading },
} = /** @type {Compilation} */ (this.compilation);
const chunkMap = getCssChunkObject(
/** @type {Chunk} */ (chunk),
/** @type {Compilation} */ (this.compilation)
);
const { globalObject } = runtimeTemplate;
const { linkPreload, linkPrefetch } =
JsonpChunkLoadingRuntimeModule.getCompilationHooks(compilation);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put these hooks here, i.e. just copy/paste, because this plugin can work not only with Jsonp, also with ESM modules

const conditionMap = /** @type {ChunkGraph} */ (
chunkGraph
).getChunkConditionMap(/** @type {Chunk} */ (chunk), chunkHasCss);
const hasCssMatcher = compileBooleanMatcher(conditionMap);

const withLoading =
runtimeRequirements.has(RuntimeGlobals.ensureChunkHandlers) &&
Object.keys(chunkMap).length > 0;
const withHmr = runtimeRequirements.has(
RuntimeGlobals.hmrDownloadUpdateHandlers
);
const withPrefetch = runtimeRequirements.has(
RuntimeGlobals.prefetchChunkHandlers
);
const withPreload = runtimeRequirements.has(
RuntimeGlobals.preloadChunkHandlers
);
const chunkLoadingGlobalExpr = `${globalObject}[${JSON.stringify(
chunkLoadingGlobal
)}]`;

if (!withLoading && !withHmr) {
return "";
Expand Down Expand Up @@ -1010,6 +1052,23 @@ class MiniCssExtractPlugin {
),
"};",
"",
`var webpackJsonpCallback = ${runtimeTemplate.basicFunction(
"parentChunkLoadingFunction, data",
[
runtimeTemplate.destructureArray(["chunkIds"], "data"),
"for(var i=0;i < chunkIds.length; i++) {",
Template.indent([
"var chunkId = chunkIds[i];",
"installedCssChunks[chunkId] = 0;",
]),
"}",
]
)}`,
"",
`var chunkLoadingGlobal = ${chunkLoadingGlobalExpr} = ${chunkLoadingGlobalExpr} || [];`,
"chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));",
"chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));",
"",
`${
RuntimeGlobals.ensureChunkHandlers
}.miniCss = ${runtimeTemplate.basicFunction(
Expand Down Expand Up @@ -1088,6 +1147,115 @@ class MiniCssExtractPlugin {
)}`,
])
: "// no hmr",
"",
withPrefetch && hasCssMatcher !== false
? `${
RuntimeGlobals.prefetchChunkHandlers
}.miniCss = ${runtimeTemplate.basicFunction("chunkId", [
`if((!${
RuntimeGlobals.hasOwnProperty
}(installedCssChunks, chunkId) || installedCssChunks[chunkId] === undefined) && ${
hasCssMatcher === true ? "true" : hasCssMatcher("chunkId")
}) {`,
Template.indent([
`var getLinkElements = function (rel, as) {`,
Template.indent([
`var links = document.getElementsByTagName("link");`,
`var loadedLinks = [];`,
`for (var i = 0; i < links.length; i++) {`,
Template.indent([
`if (`,
Template.indent([
`links[i].getAttribute("rel") === rel &&`,
`links[i].getAttribute("as") === as`,
]),
`) {`,
Template.indent([
`loadedLinks.push(links[i].getAttribute("href"));`,
]),
`}`,
]),
`}`,
`return loadedLinks;`,
]),
`};`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove getLinkElements from here too

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be another investigation

"",
`var loadedPreloadLinkElements = getLinkElements("preload", "style");`,
`var chunkIdHref = ${RuntimeGlobals.publicPath} + ${RuntimeGlobals.require}.miniCssF(chunkId);`,
`if(!loadedPreloadLinkElements.includes(chunkIdHref)) {`,
Template.indent([
"installedCssChunks[chunkId] = null;",
linkPrefetch.call(
Template.asString([
"var link = document.createElement('link');",
crossOriginLoading
? `link.crossOrigin = ${JSON.stringify(
crossOriginLoading
)};`
: "",
`if (${RuntimeGlobals.scriptNonce}) {`,
Template.indent(
`link.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});`
),
"}",
'link.rel = "prefetch";',
'link.as = "style";',
`link.href = ${RuntimeGlobals.publicPath} + ${RuntimeGlobals.require}.miniCssF(chunkId);`,
]),
chunk
),
"document.head.appendChild(link);",
]),
`}`,
]),
"}",
])};`
: "// no prefetching",
"",
withPreload && hasCssMatcher !== false
? `${
RuntimeGlobals.preloadChunkHandlers
}.miniCss = ${runtimeTemplate.basicFunction("chunkId", [
`if((!${
RuntimeGlobals.hasOwnProperty
}(installedCssChunks, chunkId) || installedCssChunks[chunkId] === undefined) && ${
hasCssMatcher === true ? "true" : hasCssMatcher("chunkId")
}) {`,
Template.indent([
"installedCssChunks[chunkId] = null;",
linkPreload.call(
Template.asString([
"var link = document.createElement('link');",
"link.charset = 'utf-8';",
`if (${RuntimeGlobals.scriptNonce}) {`,
Template.indent(
`link.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});`
),
"}",
'link.rel = "preload";',
'link.as = "style";',
`link.href = ${RuntimeGlobals.publicPath} + ${RuntimeGlobals.require}.miniCssF(chunkId);`,
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
)};`
),
"}",
])
: "",
]),
chunk
),
"document.head.appendChild(link);",
]),
"}",
])};`
: "// no preloaded",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you copy this code from webpack?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, with adjustments to CSS.
Except for the getLinkElements function that I added (b785fd8).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to finish - webpack/webpack#18190, i.e. for built-in CSS support, then I will check code here, regaridng - getLinkElements, it is an intresting question, because I think browsers make different things - webpack/webpack#17497, we need to report about to chrome bug tracker, if it is a bug, they need to fix it, if not - I will improve our code late

]);
}
}
Expand Down Expand Up @@ -1149,6 +1317,12 @@ class MiniCssExtractPlugin {
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hmrDownloadUpdateHandlers)
.tap(pluginName, handler);
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.prefetchChunkHandlers)
.tap(pluginName, handler);
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.preloadChunkHandlers)
.tap(pluginName, handler);
});
}

Expand Down
2 changes: 2 additions & 0 deletions test/cases/prefetch-preload-mixed/a.js
@@ -0,0 +1,2 @@
import(/* webpackPrefetch: true, webpackChunkName: "a1" */ "./a1");
import(/* webpackPrefetch: true, webpackChunkName: "a2" */ "./a2");
Empty file.
Empty file.
3 changes: 3 additions & 0 deletions test/cases/prefetch-preload-mixed/b.js
@@ -0,0 +1,3 @@
import(/* webpackPrefetch: true, webpackChunkName: "b1" */ "./b1");
import(/* webpackPreload: true, webpackChunkName: "b2" */ "./b2");
import(/* webpackPrefetch: true, webpackChunkName: "b3" */ "./b3");
Empty file.
Empty file.
Empty file.
2 changes: 2 additions & 0 deletions test/cases/prefetch-preload-mixed/c.js
@@ -0,0 +1,2 @@
import(/* webpackPreload: true, webpackChunkName: "c1" */ "./c1");
import(/* webpackPreload: true, webpackChunkName: "c2" */ "./c2");
Empty file.
Empty file.
11 changes: 11 additions & 0 deletions test/cases/prefetch-preload-mixed/expected/a.js
@@ -0,0 +1,11 @@
(self["webpackChunk"] = self["webpackChunk"] || []).push([[0],[
/* 0 */,
/* 1 */
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {

__webpack_require__.e(/* import() | a1 */ 1).then(__webpack_require__.t.bind(__webpack_require__, 4, 23));
__webpack_require__.e(/* import() | a2 */ 2).then(__webpack_require__.t.bind(__webpack_require__, 5, 23));


/***/ })
]]);
10 changes: 10 additions & 0 deletions test/cases/prefetch-preload-mixed/expected/a1.js
@@ -0,0 +1,10 @@
(self["webpackChunk"] = self["webpackChunk"] || []).push([[1],{

/***/ 4:
/***/ (() => {



/***/ })

}]);
10 changes: 10 additions & 0 deletions test/cases/prefetch-preload-mixed/expected/a2.js
@@ -0,0 +1,10 @@
(self["webpackChunk"] = self["webpackChunk"] || []).push([[2],{

/***/ 5:
/***/ (() => {



/***/ })

}]);
13 changes: 13 additions & 0 deletions test/cases/prefetch-preload-mixed/expected/b.js
@@ -0,0 +1,13 @@
(self["webpackChunk"] = self["webpackChunk"] || []).push([[3],{

/***/ 2:
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {

__webpack_require__.e(/* import() | b1 */ 4).then(__webpack_require__.t.bind(__webpack_require__, 6, 23));
__webpack_require__.e(/* import() | b2 */ 5).then(__webpack_require__.t.bind(__webpack_require__, 7, 23));
__webpack_require__.e(/* import() | b3 */ 6).then(__webpack_require__.t.bind(__webpack_require__, 8, 23));


/***/ })

}]);
10 changes: 10 additions & 0 deletions test/cases/prefetch-preload-mixed/expected/b1.js
@@ -0,0 +1,10 @@
(self["webpackChunk"] = self["webpackChunk"] || []).push([[4],{

/***/ 6:
/***/ (() => {



/***/ })

}]);
10 changes: 10 additions & 0 deletions test/cases/prefetch-preload-mixed/expected/b2.js
@@ -0,0 +1,10 @@
(self["webpackChunk"] = self["webpackChunk"] || []).push([[5],{

/***/ 7:
/***/ (() => {



/***/ })

}]);
10 changes: 10 additions & 0 deletions test/cases/prefetch-preload-mixed/expected/b3.js
@@ -0,0 +1,10 @@
(self["webpackChunk"] = self["webpackChunk"] || []).push([[6],{

/***/ 8:
/***/ (() => {



/***/ })

}]);
12 changes: 12 additions & 0 deletions test/cases/prefetch-preload-mixed/expected/c.js
@@ -0,0 +1,12 @@
(self["webpackChunk"] = self["webpackChunk"] || []).push([[7],{

/***/ 3:
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {

__webpack_require__.e(/* import() | c1 */ 8).then(__webpack_require__.t.bind(__webpack_require__, 9, 23));
__webpack_require__.e(/* import() | c2 */ 9).then(__webpack_require__.t.bind(__webpack_require__, 10, 23));


/***/ })

}]);
10 changes: 10 additions & 0 deletions test/cases/prefetch-preload-mixed/expected/c1.js
@@ -0,0 +1,10 @@
(self["webpackChunk"] = self["webpackChunk"] || []).push([[8],{

/***/ 9:
/***/ (() => {



/***/ })

}]);
10 changes: 10 additions & 0 deletions test/cases/prefetch-preload-mixed/expected/c2.js
@@ -0,0 +1,10 @@
(self["webpackChunk"] = self["webpackChunk"] || []).push([[9],{

/***/ 10:
/***/ (() => {



/***/ })

}]);