Skip to content

Commit f893542

Browse files
author
Patrik Sletmo
committed
feat: add hook for customizing injected runtime tags
Enable other plugins to tap into the tag injection mechanism in order to customize the functionality of lazy loaded chunks. Closes #40.
1 parent a33f22f commit f893542

File tree

13 files changed

+157
-116
lines changed

13 files changed

+157
-116
lines changed

package-lock.json

+3-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"dependencies": {
4545
"loader-utils": "^2.0.0",
4646
"schema-utils": "^3.0.0",
47+
"tapable": "^2.2.0",
4748
"webpack-sources": "^1.1.0"
4849
},
4950
"devDependencies": {

src/index.js

+113-67
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable class-methods-use-this */
22

33
import { validate } from 'schema-utils';
4+
import { SyncWaterfallHook } from 'tapable';
45

56
import schema from './plugin-options.json';
67
import { MODULE_TYPE, compareModulesByIdentifier } from './utils';
@@ -28,6 +29,8 @@ const cssModuleCache = new WeakMap();
2829
*/
2930
const cssDependencyCache = new WeakMap();
3031

32+
const compilerHookMap = new WeakMap();
33+
3134
class MiniCssExtractPlugin {
3235
static getCssModule(webpack) {
3336
/**
@@ -300,6 +303,20 @@ class MiniCssExtractPlugin {
300303
return CssDependency;
301304
}
302305

306+
static getCompilerHooks(compiler) {
307+
/**
308+
* Prevent creation of multiple compiler hook maps to allow other integrations to get the current mapping.
309+
*/
310+
let hooks = compilerHookMap.get(compiler);
311+
if (!hooks) {
312+
hooks = {
313+
customize: new SyncWaterfallHook(['attributes']),
314+
};
315+
compilerHookMap.set(compiler, hooks);
316+
}
317+
return hooks;
318+
}
319+
303320
constructor(options = {}) {
304321
validate(schema, options, {
305322
name: 'Mini CSS Extract Plugin',
@@ -690,6 +707,42 @@ class MiniCssExtractPlugin {
690707
}
691708
);
692709

710+
const attributes = {
711+
href: `${mainTemplate.requireFn}.p + href`,
712+
rel: JSON.stringify('stylesheet'),
713+
onload: 'onLinkComplete',
714+
onerror: 'onLinkComplete',
715+
};
716+
717+
// Some attributes cannot be assigned through setAttribute, so we maintain
718+
// a list of attributes that can safely be assigned through dot notation
719+
const safeAttrs = ['href', 'rel', 'type', 'onload', 'onerror'];
720+
721+
// TODO: Test with webpack 4 as well
722+
if (this.runtimeOptions.linkType) {
723+
attributes.type = JSON.stringify(this.runtimeOptions.linkType);
724+
}
725+
726+
if (crossOriginLoading) {
727+
attributes.crossOrigin = `(linkTag.href.indexOf(window.location.origin + '/') !== 0)
728+
? ${JSON.stringify(crossOriginLoading)}
729+
: undefined`;
730+
}
731+
732+
// Append static attributes
733+
if (this.runtimeOptions.attributes) {
734+
Object.entries(this.runtimeOptions.attributes).forEach(
735+
([key, value]) => {
736+
attributes[key] = JSON.stringify(value);
737+
}
738+
);
739+
}
740+
741+
// Append dynamic attributes
742+
MiniCssExtractPlugin.getCompilerHooks(compiler).customize.call(
743+
attributes
744+
);
745+
693746
return Template.asString([
694747
source,
695748
'',
@@ -701,7 +754,7 @@ class MiniCssExtractPlugin {
701754
'promises.push(installedCssChunks[chunkId] = new Promise(function(resolve, reject) {',
702755
Template.indent([
703756
`var href = ${linkHrefPath};`,
704-
`var fullhref = ${mainTemplate.requireFn}.p + href;`,
757+
`var fullhref = ${attributes.href};`,
705758
'var existingLinkTags = document.getElementsByTagName("link");',
706759
'for(var i = 0; i < existingLinkTags.length; i++) {',
707760
Template.indent([
@@ -719,25 +772,6 @@ class MiniCssExtractPlugin {
719772
]),
720773
'}',
721774
'var linkTag = document.createElement("link");',
722-
this.runtimeOptions.attributes
723-
? Template.asString(
724-
Object.entries(this.runtimeOptions.attributes).map(
725-
(entry) => {
726-
const [key, value] = entry;
727-
728-
return `linkTag.setAttribute(${JSON.stringify(
729-
key
730-
)}, ${JSON.stringify(value)});`;
731-
}
732-
)
733-
)
734-
: '',
735-
'linkTag.rel = "stylesheet";',
736-
this.runtimeOptions.linkType
737-
? `linkTag.type = ${JSON.stringify(
738-
this.runtimeOptions.linkType
739-
)};`
740-
: '',
741775
'var onLinkComplete = function (event) {',
742776
Template.indent([
743777
'// avoid mem leaks.',
@@ -759,19 +793,17 @@ class MiniCssExtractPlugin {
759793
'}',
760794
]),
761795
'};',
762-
'linkTag.onerror = linkTag.onload = onLinkComplete;',
763-
'linkTag.href = fullhref;',
764-
crossOriginLoading
765-
? Template.asString([
766-
`if (linkTag.href.indexOf(window.location.origin + '/') !== 0) {`,
767-
Template.indent(
768-
`linkTag.crossOrigin = ${JSON.stringify(
769-
crossOriginLoading
770-
)};`
771-
),
772-
'}',
773-
])
774-
: '',
796+
Template.asString(
797+
Object.entries(attributes).map(([key, value]) => {
798+
if (safeAttrs.includes(key)) {
799+
return `linkTag.${key} = ${value};`;
800+
}
801+
802+
return `linkTag.setAttribute(${JSON.stringify(
803+
key
804+
)}, ${value});`;
805+
})
806+
),
775807
typeof this.runtimeOptions.insert !== 'undefined'
776808
? typeof this.runtimeOptions.insert === 'function'
777809
? `(${this.runtimeOptions.insert.toString()})(linkTag)`
@@ -847,30 +879,46 @@ class MiniCssExtractPlugin {
847879
return null;
848880
}
849881

882+
const attributes = {
883+
href: `${RuntimeGlobals.publicPath} + ${RuntimeGlobals.require}.miniCssF(chunkId)`,
884+
rel: JSON.stringify('stylesheet'),
885+
onload: 'onLinkComplete',
886+
onerror: 'onLinkComplete',
887+
};
888+
889+
// Some attributes cannot be assigned through setAttribute, so we maintain
890+
// a list of attributes that can safely be assigned through dot notation
891+
const safeAttrs = ['href', 'rel', 'type', 'onload', 'onerror'];
892+
893+
if (this.runtimeOptions.linkType) {
894+
attributes.type = JSON.stringify(this.runtimeOptions.linkType);
895+
}
896+
897+
if (crossOriginLoading) {
898+
attributes.crossOrigin = `(linkTag.href.indexOf(window.location.origin + '/') !== 0)
899+
? ${JSON.stringify(crossOriginLoading)}
900+
: undefined`;
901+
}
902+
903+
// Append static attributes
904+
if (this.runtimeOptions.attributes) {
905+
Object.entries(this.runtimeOptions.attributes).forEach(
906+
([key, value]) => {
907+
attributes[key] = JSON.stringify(value);
908+
}
909+
);
910+
}
911+
912+
// Append dynamic attributes
913+
MiniCssExtractPlugin.getCompilerHooks(compiler).customize.call(
914+
attributes
915+
);
916+
850917
return Template.asString([
851918
`var createStylesheet = ${runtimeTemplate.basicFunction(
852919
'chunkId, fullhref, resolve, reject',
853920
[
854921
'var linkTag = document.createElement("link");',
855-
this.runtimeOptions.attributes
856-
? Template.asString(
857-
Object.entries(this.runtimeOptions.attributes).map(
858-
(entry) => {
859-
const [key, value] = entry;
860-
861-
return `linkTag.setAttribute(${JSON.stringify(
862-
key
863-
)}, ${JSON.stringify(value)});`;
864-
}
865-
)
866-
)
867-
: '',
868-
'linkTag.rel = "stylesheet";',
869-
this.runtimeOptions.linkType
870-
? `linkTag.type = ${JSON.stringify(
871-
this.runtimeOptions.linkType
872-
)};`
873-
: '',
874922
`var onLinkComplete = ${runtimeTemplate.basicFunction(
875923
'event',
876924
[
@@ -892,19 +940,17 @@ class MiniCssExtractPlugin {
892940
'}',
893941
]
894942
)}`,
895-
'linkTag.onerror = linkTag.onload = onLinkComplete;',
896-
'linkTag.href = fullhref;',
897-
crossOriginLoading
898-
? Template.asString([
899-
`if (linkTag.href.indexOf(window.location.origin + '/') !== 0) {`,
900-
Template.indent(
901-
`linkTag.crossOrigin = ${JSON.stringify(
902-
crossOriginLoading
903-
)};`
904-
),
905-
'}',
906-
])
907-
: '',
943+
Template.asString(
944+
Object.entries(attributes).map(([key, value]) => {
945+
if (safeAttrs.includes(key)) {
946+
return `linkTag.${key} = ${value};`;
947+
}
948+
949+
return `linkTag.setAttribute(${JSON.stringify(
950+
key
951+
)}, ${value});`;
952+
})
953+
),
908954
typeof this.runtimeOptions.insert !== 'undefined'
909955
? typeof this.runtimeOptions.insert === 'function'
910956
? `(${this.runtimeOptions.insert.toString()})(linkTag)`
@@ -945,7 +991,7 @@ class MiniCssExtractPlugin {
945991
'resolve, reject',
946992
[
947993
`var href = ${RuntimeGlobals.require}.miniCssF(chunkId);`,
948-
`var fullhref = ${RuntimeGlobals.publicPath} + href;`,
994+
`var fullhref = ${attributes.href};`,
949995
'if(findStylesheet(href, fullhref)) return resolve();',
950996
'createStylesheet(chunkId, fullhref, resolve, reject);',
951997
]
@@ -1016,7 +1062,7 @@ class MiniCssExtractPlugin {
10161062
'chunkId',
10171063
[
10181064
`var href = ${RuntimeGlobals.require}.miniCssF(chunkId);`,
1019-
`var fullhref = ${RuntimeGlobals.publicPath} + href;`,
1065+
`var fullhref = ${attributes.href};`,
10201066
'var oldTag = findStylesheet(href, fullhref);',
10211067
'if(!oldTag) return;',
10221068
`promises.push(new Promise(${runtimeTemplate.basicFunction(

test/__snapshots__/attributes-option.test.js.snap.webpack5

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ exports[`attributes option should work with attributes option: DOM 1`] = `
44
"<!DOCTYPE html><html><head>
55
<title>style-loader test</title>
66
<style id=\\"existing-style\\">.existing { color: yellow }</style>
7-
<link id=\\"target\\" data-target=\\"example\\" rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
7+
<link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\" id=\\"target\\" data-target=\\"example\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
88
<body>
99
<h1>Body</h1>
1010
<div class=\\"target\\"></div>
@@ -22,7 +22,7 @@ exports[`attributes option should work without attributes option: DOM 1`] = `
2222
"<!DOCTYPE html><html><head>
2323
<title>style-loader test</title>
2424
<style id=\\"existing-style\\">.existing { color: yellow }</style>
25-
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
25+
<link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
2626
<body>
2727
<h1>Body</h1>
2828
<div class=\\"target\\"></div>

test/__snapshots__/insert-option.test.js.snap.webpack5

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
exports[`insert option should work when insert option is function: DOM 1`] = `
44
"<!DOCTYPE html><html><head>
55
<title>style-loader test</title>
6-
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\"><style id=\\"existing-style\\">.existing { color: yellow }</style>
6+
<link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\"><style id=\\"existing-style\\">.existing { color: yellow }</style>
77
<script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
88
<body>
99
<h1>Body</h1>
@@ -21,7 +21,7 @@ exports[`insert option should work when insert option is function: warnings 1`]
2121
exports[`insert option should work when insert option is string: DOM 1`] = `
2222
"<!DOCTYPE html><html><head>
2323
<title>style-loader test</title>
24-
<style id=\\"existing-style\\">.existing { color: yellow }</style><link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\">
24+
<style id=\\"existing-style\\">.existing { color: yellow }</style><link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\">
2525
<script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
2626
<body>
2727
<h1>Body</h1>
@@ -40,7 +40,7 @@ exports[`insert option should work without insert option: DOM 1`] = `
4040
"<!DOCTYPE html><html><head>
4141
<title>style-loader test</title>
4242
<style id=\\"existing-style\\">.existing { color: yellow }</style>
43-
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
43+
<link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
4444
<body>
4545
<h1>Body</h1>
4646
<div class=\\"target\\"></div>

test/__snapshots__/linkTag-option.test.js.snap.webpack5

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ exports[`linkType option should work when linkType option is "false": DOM 1`] =
44
"<!DOCTYPE html><html><head>
55
<title>style-loader test</title>
66
<style id=\\"existing-style\\">.existing { color: yellow }</style>
7-
<link rel=\\"stylesheet\\" href=\\"simple.css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
7+
<link href=\\"simple.css\\" rel=\\"stylesheet\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
88
<body>
99
<h1>Body</h1>
1010
<div class=\\"target\\"></div>
@@ -22,7 +22,7 @@ exports[`linkType option should work when linkType option is "text/css": DOM 1`]
2222
"<!DOCTYPE html><html><head>
2323
<title>style-loader test</title>
2424
<style id=\\"existing-style\\">.existing { color: yellow }</style>
25-
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
25+
<link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
2626
<body>
2727
<h1>Body</h1>
2828
<div class=\\"target\\"></div>
@@ -40,7 +40,7 @@ exports[`linkType option should work without linkType option: DOM 1`] = `
4040
"<!DOCTYPE html><html><head>
4141
<title>style-loader test</title>
4242
<style id=\\"existing-style\\">.existing { color: yellow }</style>
43-
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
43+
<link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
4444
<body>
4545
<h1>Body</h1>
4646
<div class=\\"target\\"></div>

0 commit comments

Comments
 (0)