-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathindex.ts
179 lines (148 loc) · 5.3 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import assert from "assert";
import { Compiler, compilation } from "webpack";
import Compilation = compilation.Compilation;
import HtmlWebpackPlugin, { Hooks, HtmlTagObject } from "html-webpack-plugin";
import { Source } from "webpack-sources";
import { createHash } from "crypto";
// FIXME: HtmlWebpackPlugin typing definitions seem to be broken
interface HtmlWebpackPluginConstructor {
getHooks(compilation: Compilation): Hooks;
}
type RuntimeTag = [HtmlTagObject, RuntimeUri];
interface RuntimeUri {
file: string;
url: string;
}
interface Options {
csp?: boolean;
}
// Webpack plugin to automatically inline runtime chunks
class HtmlWebpackInlineRuntimePlugin {
NAME = "HtmlWebpackInlineRuntimePlugin";
options: Options;
constructor(options?: Options) {
this.options = options || {};
}
apply(compiler: Compiler) {
compiler.hooks.compilation.tap(this.NAME, (compilation: Compilation) => {
const runtimeChunkOption = getRuntimeChunkOption(compiler);
findHtmlWebpackPlugin(compiler)
.getHooks(compilation)
.alterAssetTags.tapPromise(this.NAME, async (data) => {
// Do nothing if there is no runtimeChunk option set
if (!runtimeChunkOption) {
return data;
}
// Gather all files and uris of runtime chunks
const runtimeUris = getRuntimeUris(compilation);
// Apply inlining to tags that refer to runtime chunks
const hashes = data.assetTags.scripts
.reduce(runtimeTags(runtimeUris), [])
.reduce(inlineContent(compilation, this.options), []);
if (this.options.csp) {
// Add the inline csp hashes to the meta tags
data.assetTags.meta.push(cspMetaTag(hashes));
}
return data;
});
});
}
}
// Find a current instance of the HtmlWebpackPlugin in the webpack compiler
function findHtmlWebpackPlugin(compiler: Compiler) {
if (compiler.options.plugins) {
const plugin = compiler.options.plugins
.filter((plugin) => plugin.constructor.name === HtmlWebpackPlugin.name)
.pop();
if (plugin) {
return (plugin.constructor as unknown) as HtmlWebpackPluginConstructor;
} else {
assert.fail("Unable to find `HtmlWebpackPlugin` instance.");
}
}
return assert.fail("Unable to find compiler plugins.");
}
// Return the runtimeChunk option object if there is one set.
//
// While the options can be strings "single" or "multiple" in the webpack
// config, they are shortcuts for defining an object in the shape of
// RuntimeChunkOption.
function getRuntimeChunkOption(compiler: Compiler) {
return (
compiler.options.optimization && compiler.options.optimization.runtimeChunk
);
}
// Find the publicPath setting from the current Compilation
function getPublicPath(compilation: Compilation) {
let path = compilation.outputOptions && compilation.outputOptions.publicPath;
// Append a trailing slash if one does not exist
if (path && path.length && path.substr(-1, 1) !== "/") {
path += "/";
}
return path || "";
}
// Find all the runtimeChunks from the current compilation's entries
function getRuntimeUris(compilation: Compilation) {
const publicPath = getPublicPath(compilation);
return Array.from(compilation.entrypoints.values())
.reduce((acc, entry) => {
const files = entry.runtimeChunk.files.values();
acc.push(...files);
return acc;
}, [])
.filter((filename: string) => filename.length > 0)
.map((file: string) => ({ file, url: publicPath + file }));
}
// Find the asset tags that match a RuntimeUri
function runtimeTags(runtimeUris: RuntimeUri[]) {
return function (acc: RuntimeTag[], tag: HtmlTagObject) {
if (tag.attributes.hasOwnProperty("src")) {
let match = runtimeUris.find((uri) => uri.url === tag.attributes.src);
// We only want to match tags that are related to a runtime asset
if (match) {
acc.push([tag, match]);
}
}
return acc;
};
}
// Find the right file in all of the webpack assets
function getAssetContent(compilation: Compilation, file: string) {
return Object.entries<Source>(compilation.assets)
.filter(([asset, _]) => asset === file)
.map(([_, content]) => content.source() as string)
.pop();
}
// Replace remote asset source with inline runtime asset content
function inlineContent(compilation: Compilation, options: Options) {
return function (hashes: string[], [tag, runtime]: RuntimeTag) {
const content = getAssetContent(compilation, runtime.file);
if (content) {
tag.innerHTML = content;
delete tag.attributes.src;
if (options.csp) {
// Add the content hash to the meta tags to allow for CSP policies
hashes.push(cspHash(content));
}
}
return hashes;
};
}
// Hash input and format it as a CSP hash value
function cspHash(input: string) {
let hash = createHash("sha256").update(input).digest("base64");
return `'sha256-${hash}'`;
}
// Create a CSP meta tag to allow inline script source
function cspMetaTag(hashes: string[]): HtmlTagObject {
return {
tagName: "meta",
voidTag: true,
attributes: {
"http-equiv": "Content-Security-Policy",
content: `script-src 'self' ${hashes.join(" ")}`,
},
};
}
// Export the plugin as the module so that node.js can use it
export = HtmlWebpackInlineRuntimePlugin;