Skip to content

Commit

Permalink
feat: add hook for customizing injected runtime tags
Browse files Browse the repository at this point in the history
    Enable other plugins to tap into the tag injection mechanism in order to
    customize the functionality of lazy loaded chunks.

    Closes #40.
  • Loading branch information
Patrik Sletmo committed Apr 12, 2021
1 parent a33f22f commit f893542
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 116 deletions.
7 changes: 3 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -44,6 +44,7 @@
"dependencies": {
"loader-utils": "^2.0.0",
"schema-utils": "^3.0.0",
"tapable": "^2.2.0",
"webpack-sources": "^1.1.0"
},
"devDependencies": {
Expand Down
180 changes: 113 additions & 67 deletions src/index.js
@@ -1,6 +1,7 @@
/* eslint-disable class-methods-use-this */

import { validate } from 'schema-utils';
import { SyncWaterfallHook } from 'tapable';

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

const compilerHookMap = new WeakMap();

class MiniCssExtractPlugin {
static getCssModule(webpack) {
/**
Expand Down Expand Up @@ -300,6 +303,20 @@ class MiniCssExtractPlugin {
return CssDependency;
}

static getCompilerHooks(compiler) {
/**
* Prevent creation of multiple compiler hook maps to allow other integrations to get the current mapping.
*/
let hooks = compilerHookMap.get(compiler);
if (!hooks) {
hooks = {
customize: new SyncWaterfallHook(['attributes']),
};
compilerHookMap.set(compiler, hooks);
}
return hooks;
}

constructor(options = {}) {
validate(schema, options, {
name: 'Mini CSS Extract Plugin',
Expand Down Expand Up @@ -690,6 +707,42 @@ class MiniCssExtractPlugin {
}
);

const attributes = {
href: `${mainTemplate.requireFn}.p + href`,
rel: JSON.stringify('stylesheet'),
onload: 'onLinkComplete',
onerror: 'onLinkComplete',
};

// Some attributes cannot be assigned through setAttribute, so we maintain
// a list of attributes that can safely be assigned through dot notation
const safeAttrs = ['href', 'rel', 'type', 'onload', 'onerror'];

// TODO: Test with webpack 4 as well
if (this.runtimeOptions.linkType) {
attributes.type = JSON.stringify(this.runtimeOptions.linkType);
}

if (crossOriginLoading) {
attributes.crossOrigin = `(linkTag.href.indexOf(window.location.origin + '/') !== 0)
? ${JSON.stringify(crossOriginLoading)}
: undefined`;
}

// Append static attributes
if (this.runtimeOptions.attributes) {
Object.entries(this.runtimeOptions.attributes).forEach(
([key, value]) => {
attributes[key] = JSON.stringify(value);
}
);
}

// Append dynamic attributes
MiniCssExtractPlugin.getCompilerHooks(compiler).customize.call(
attributes
);

return Template.asString([
source,
'',
Expand All @@ -701,7 +754,7 @@ class MiniCssExtractPlugin {
'promises.push(installedCssChunks[chunkId] = new Promise(function(resolve, reject) {',
Template.indent([
`var href = ${linkHrefPath};`,
`var fullhref = ${mainTemplate.requireFn}.p + href;`,
`var fullhref = ${attributes.href};`,
'var existingLinkTags = document.getElementsByTagName("link");',
'for(var i = 0; i < existingLinkTags.length; i++) {',
Template.indent([
Expand All @@ -719,25 +772,6 @@ class MiniCssExtractPlugin {
]),
'}',
'var linkTag = document.createElement("link");',
this.runtimeOptions.attributes
? Template.asString(
Object.entries(this.runtimeOptions.attributes).map(
(entry) => {
const [key, value] = entry;

return `linkTag.setAttribute(${JSON.stringify(
key
)}, ${JSON.stringify(value)});`;
}
)
)
: '',
'linkTag.rel = "stylesheet";',
this.runtimeOptions.linkType
? `linkTag.type = ${JSON.stringify(
this.runtimeOptions.linkType
)};`
: '',
'var onLinkComplete = function (event) {',
Template.indent([
'// avoid mem leaks.',
Expand All @@ -759,19 +793,17 @@ class MiniCssExtractPlugin {
'}',
]),
'};',
'linkTag.onerror = linkTag.onload = onLinkComplete;',
'linkTag.href = fullhref;',
crossOriginLoading
? Template.asString([
`if (linkTag.href.indexOf(window.location.origin + '/') !== 0) {`,
Template.indent(
`linkTag.crossOrigin = ${JSON.stringify(
crossOriginLoading
)};`
),
'}',
])
: '',
Template.asString(
Object.entries(attributes).map(([key, value]) => {
if (safeAttrs.includes(key)) {
return `linkTag.${key} = ${value};`;
}

return `linkTag.setAttribute(${JSON.stringify(
key
)}, ${value});`;
})
),
typeof this.runtimeOptions.insert !== 'undefined'
? typeof this.runtimeOptions.insert === 'function'
? `(${this.runtimeOptions.insert.toString()})(linkTag)`
Expand Down Expand Up @@ -847,30 +879,46 @@ class MiniCssExtractPlugin {
return null;
}

const attributes = {
href: `${RuntimeGlobals.publicPath} + ${RuntimeGlobals.require}.miniCssF(chunkId)`,
rel: JSON.stringify('stylesheet'),
onload: 'onLinkComplete',
onerror: 'onLinkComplete',
};

// Some attributes cannot be assigned through setAttribute, so we maintain
// a list of attributes that can safely be assigned through dot notation
const safeAttrs = ['href', 'rel', 'type', 'onload', 'onerror'];

if (this.runtimeOptions.linkType) {
attributes.type = JSON.stringify(this.runtimeOptions.linkType);
}

if (crossOriginLoading) {
attributes.crossOrigin = `(linkTag.href.indexOf(window.location.origin + '/') !== 0)
? ${JSON.stringify(crossOriginLoading)}
: undefined`;
}

// Append static attributes
if (this.runtimeOptions.attributes) {
Object.entries(this.runtimeOptions.attributes).forEach(
([key, value]) => {
attributes[key] = JSON.stringify(value);
}
);
}

// Append dynamic attributes
MiniCssExtractPlugin.getCompilerHooks(compiler).customize.call(
attributes
);

return Template.asString([
`var createStylesheet = ${runtimeTemplate.basicFunction(
'chunkId, fullhref, resolve, reject',
[
'var linkTag = document.createElement("link");',
this.runtimeOptions.attributes
? Template.asString(
Object.entries(this.runtimeOptions.attributes).map(
(entry) => {
const [key, value] = entry;
return `linkTag.setAttribute(${JSON.stringify(
key
)}, ${JSON.stringify(value)});`;
}
)
)
: '',
'linkTag.rel = "stylesheet";',
this.runtimeOptions.linkType
? `linkTag.type = ${JSON.stringify(
this.runtimeOptions.linkType
)};`
: '',
`var onLinkComplete = ${runtimeTemplate.basicFunction(
'event',
[
Expand All @@ -892,19 +940,17 @@ class MiniCssExtractPlugin {
'}',
]
)}`,
'linkTag.onerror = linkTag.onload = onLinkComplete;',
'linkTag.href = fullhref;',
crossOriginLoading
? Template.asString([
`if (linkTag.href.indexOf(window.location.origin + '/') !== 0) {`,
Template.indent(
`linkTag.crossOrigin = ${JSON.stringify(
crossOriginLoading
)};`
),
'}',
])
: '',
Template.asString(
Object.entries(attributes).map(([key, value]) => {
if (safeAttrs.includes(key)) {
return `linkTag.${key} = ${value};`;
}
return `linkTag.setAttribute(${JSON.stringify(
key
)}, ${value});`;
})
),
typeof this.runtimeOptions.insert !== 'undefined'
? typeof this.runtimeOptions.insert === 'function'
? `(${this.runtimeOptions.insert.toString()})(linkTag)`
Expand Down Expand Up @@ -945,7 +991,7 @@ class MiniCssExtractPlugin {
'resolve, reject',
[
`var href = ${RuntimeGlobals.require}.miniCssF(chunkId);`,
`var fullhref = ${RuntimeGlobals.publicPath} + href;`,
`var fullhref = ${attributes.href};`,
'if(findStylesheet(href, fullhref)) return resolve();',
'createStylesheet(chunkId, fullhref, resolve, reject);',
]
Expand Down Expand Up @@ -1016,7 +1062,7 @@ class MiniCssExtractPlugin {
'chunkId',
[
`var href = ${RuntimeGlobals.require}.miniCssF(chunkId);`,
`var fullhref = ${RuntimeGlobals.publicPath} + href;`,
`var fullhref = ${attributes.href};`,
'var oldTag = findStylesheet(href, fullhref);',
'if(!oldTag) return;',
`promises.push(new Promise(${runtimeTemplate.basicFunction(
Expand Down
4 changes: 2 additions & 2 deletions test/__snapshots__/attributes-option.test.js.snap.webpack5
Expand Up @@ -4,7 +4,7 @@ exports[`attributes option should work with attributes option: DOM 1`] = `
"<!DOCTYPE html><html><head>
<title>style-loader test</title>
<style id=\\"existing-style\\">.existing { color: yellow }</style>
<link id=\\"target\\" data-target=\\"example\\" rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\" id=\\"target\\" data-target=\\"example\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<body>
<h1>Body</h1>
<div class=\\"target\\"></div>
Expand All @@ -22,7 +22,7 @@ exports[`attributes option should work without attributes option: DOM 1`] = `
"<!DOCTYPE html><html><head>
<title>style-loader test</title>
<style id=\\"existing-style\\">.existing { color: yellow }</style>
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<body>
<h1>Body</h1>
<div class=\\"target\\"></div>
Expand Down
6 changes: 3 additions & 3 deletions test/__snapshots__/insert-option.test.js.snap.webpack5
Expand Up @@ -3,7 +3,7 @@
exports[`insert option should work when insert option is function: DOM 1`] = `
"<!DOCTYPE html><html><head>
<title>style-loader test</title>
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\"><style id=\\"existing-style\\">.existing { color: yellow }</style>
<link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\"><style id=\\"existing-style\\">.existing { color: yellow }</style>
<script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<body>
<h1>Body</h1>
Expand All @@ -21,7 +21,7 @@ exports[`insert option should work when insert option is function: warnings 1`]
exports[`insert option should work when insert option is string: DOM 1`] = `
"<!DOCTYPE html><html><head>
<title>style-loader test</title>
<style id=\\"existing-style\\">.existing { color: yellow }</style><link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\">
<style id=\\"existing-style\\">.existing { color: yellow }</style><link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\">
<script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<body>
<h1>Body</h1>
Expand All @@ -40,7 +40,7 @@ exports[`insert option should work without insert option: DOM 1`] = `
"<!DOCTYPE html><html><head>
<title>style-loader test</title>
<style id=\\"existing-style\\">.existing { color: yellow }</style>
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<body>
<h1>Body</h1>
<div class=\\"target\\"></div>
Expand Down
6 changes: 3 additions & 3 deletions test/__snapshots__/linkTag-option.test.js.snap.webpack5
Expand Up @@ -4,7 +4,7 @@ exports[`linkType option should work when linkType option is "false": DOM 1`] =
"<!DOCTYPE html><html><head>
<title>style-loader test</title>
<style id=\\"existing-style\\">.existing { color: yellow }</style>
<link rel=\\"stylesheet\\" href=\\"simple.css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<link href=\\"simple.css\\" rel=\\"stylesheet\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<body>
<h1>Body</h1>
<div class=\\"target\\"></div>
Expand All @@ -22,7 +22,7 @@ exports[`linkType option should work when linkType option is "text/css": DOM 1`]
"<!DOCTYPE html><html><head>
<title>style-loader test</title>
<style id=\\"existing-style\\">.existing { color: yellow }</style>
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<body>
<h1>Body</h1>
<div class=\\"target\\"></div>
Expand All @@ -40,7 +40,7 @@ exports[`linkType option should work without linkType option: DOM 1`] = `
"<!DOCTYPE html><html><head>
<title>style-loader test</title>
<style id=\\"existing-style\\">.existing { color: yellow }</style>
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"simple.css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<link href=\\"simple.css\\" rel=\\"stylesheet\\" type=\\"text/css\\"><script charset=\\"utf-8\\" src=\\"simple.bundle.js\\"></script></head>
<body>
<h1>Body</h1>
<div class=\\"target\\"></div>
Expand Down

0 comments on commit f893542

Please sign in to comment.