forked from craftamap/esbuild-plugin-html
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
245 lines (245 loc) · 13.3 KB
/
index.js
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.htmlPlugin = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const jsdom_1 = require("jsdom");
const lodash_template_1 = __importDefault(require("lodash.template"));
const defaultHtmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
</body>
</html>
`;
const REGEXES = {
DIR_REGEX: '(?<dir>\\S+\\/?)',
HASH_REGEX: '(?<hash>[A-Z2-7]{8})',
NAME_REGEX: '(?<name>[^\\s\\/]+)',
};
// This function joins a path, and in case of windows, it converts backward slashes ('\') forward slashes ('/').
function posixJoin(...paths) {
const joined = path_1.default.join(...paths);
if (path_1.default.sep === '/') {
return joined;
}
return joined.split(path_1.default.sep).join(path_1.default.posix.sep);
}
function escapeRegExp(text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}
const htmlPlugin = (configuration = { files: [], }) => {
configuration.files = configuration.files.map((htmlFileConfiguration) => {
return Object.assign({}, { findRelatedOutputFiles: false, findRelatedCssFiles: true }, htmlFileConfiguration); // Set default values
});
let logInfo = false;
function collectEntrypoints(htmlFileConfiguration, metafile) {
const entryPoints = Object.entries((metafile === null || metafile === void 0 ? void 0 : metafile.outputs) || {}).filter(([, value]) => {
if (!value.entryPoint) {
return false;
}
return htmlFileConfiguration.entryPoints.includes(value.entryPoint);
}).map(outputData => {
// Flatten the output, instead of returning an array, let's return an object that contains the path of the output file as path
return { path: outputData[0], ...outputData[1] };
});
return entryPoints;
}
function findNameRelatedOutputFiles(entrypoint, metafile, entryNames) {
var _a, _b;
const pathOfMatchedOutput = path_1.default.parse(entrypoint.path);
// Search for all files that are "related" to the output (.css and map files, for example files, as assets are dealt with otherwise).
if (entryNames) {
// If entryNames is set, the related output files are more difficult to find, as the filename can also contain a hash.
// The hash could also be part of the path, which could make it even more difficult
// We therefore try to extract the dir, name and hash from the "main"-output, and try to find all files with
// the same [name] and [dir].
// This should always include the "main"-output, as well as all relatedOutputs
const joinedPathOfMatch = posixJoin(pathOfMatchedOutput.dir, pathOfMatchedOutput.name);
const findVariablesRegexString = escapeRegExp(entryNames)
.replace('\\[hash\\]', REGEXES.HASH_REGEX)
.replace('\\[name\\]', REGEXES.NAME_REGEX)
.replace('\\[dir\\]', REGEXES.DIR_REGEX);
const findVariablesRegex = new RegExp(findVariablesRegexString);
const match = findVariablesRegex.exec(joinedPathOfMatch);
const name = (_a = match === null || match === void 0 ? void 0 : match.groups) === null || _a === void 0 ? void 0 : _a['name'];
const dir = (_b = match === null || match === void 0 ? void 0 : match.groups) === null || _b === void 0 ? void 0 : _b['dir'];
return Object.entries((metafile === null || metafile === void 0 ? void 0 : metafile.outputs) || {}).filter(([pathOfCurrentOutput,]) => {
if (entryNames) {
// if a entryName is set, we need to parse the output filename, get the name and dir,
// and find files that match the same criteria
const findFilesWithSameVariablesRegexString = escapeRegExp(entryNames.replace('[name]', name !== null && name !== void 0 ? name : '').replace('[dir]', dir !== null && dir !== void 0 ? dir : ''))
.replace('\\[hash\\]', REGEXES.HASH_REGEX);
const findFilesWithSameVariablesRegex = new RegExp(findFilesWithSameVariablesRegexString);
return findFilesWithSameVariablesRegex.test(pathOfCurrentOutput);
}
}).map(outputData => {
// Flatten the output, instead of returning an array, let's return an object that contains the path of the output file as path
return { path: outputData[0], ...outputData[1] };
});
}
else {
// If entryNames is not set, the related files are always next to the "main" output, and have the same filename, but the extension differs
return Object.entries((metafile === null || metafile === void 0 ? void 0 : metafile.outputs) || {}).filter(([key,]) => {
return path_1.default.parse(key).name === pathOfMatchedOutput.name && path_1.default.parse(key).dir === pathOfMatchedOutput.dir;
}).map(outputData => {
// Flatten the output, instead of returning an array, let's return an object that contains the path of the output file as path
return { path: outputData[0], ...outputData[1] };
});
}
}
async function renderTemplate({ htmlTemplate, define }) {
const template = (htmlTemplate && fs_1.default.existsSync(htmlTemplate)
? await fs_1.default.promises.readFile(htmlTemplate)
: htmlTemplate || '').toString();
const compiledTemplateFn = (0, lodash_template_1.default)(template || defaultHtmlTemplate);
return compiledTemplateFn({ define });
}
// use the same joinWithPublicPath function as esbuild:
// https://github.com/evanw/esbuild/blob/a1ff9d144cdb8d50ea2fa79a1d11f43d5bd5e2d8/internal/bundler/bundler.go#L533
function joinWithPublicPath(publicPath, relPath) {
relPath = path_1.default.normalize(relPath);
if (!publicPath) {
publicPath = '.';
}
let slash = '/';
if (publicPath.endsWith('/')) {
slash = '';
}
return `${publicPath}${slash}${relPath}`;
}
function injectFiles(dom, assets, outDir, publicPath, htmlFileConfiguration) {
const document = dom.window.document;
for (const script of (htmlFileConfiguration === null || htmlFileConfiguration === void 0 ? void 0 : htmlFileConfiguration.extraScripts) || []) {
const scriptTag = document.createElement('script');
if (typeof script === 'string') {
scriptTag.setAttribute('src', script);
}
else {
scriptTag.setAttribute('src', script.src);
Object.entries(script.attrs || {}).forEach(([key, value]) => {
scriptTag.setAttribute(key, value);
});
}
document.body.append(scriptTag);
}
for (const outputFile of assets) {
const filepath = outputFile.path;
let targetPath;
if (publicPath) {
targetPath = joinWithPublicPath(publicPath, path_1.default.relative(outDir, filepath));
}
else {
const htmlFileDirectory = posixJoin(outDir, htmlFileConfiguration.filename);
targetPath = path_1.default.relative(path_1.default.dirname(htmlFileDirectory), filepath);
}
const ext = path_1.default.parse(filepath).ext;
if (ext === '.js') {
const scriptTag = document.createElement('script');
scriptTag.setAttribute('src', targetPath);
if (htmlFileConfiguration.scriptLoading === 'module') {
// If module, add type="module"
scriptTag.setAttribute('type', 'module');
}
else if (!htmlFileConfiguration.scriptLoading || htmlFileConfiguration.scriptLoading === 'defer') {
// if scriptLoading is unset, or defer, use defer
scriptTag.setAttribute('defer', '');
}
document.body.append(scriptTag);
}
else if (ext === '.css') {
const linkTag = document.createElement('link');
linkTag.setAttribute('rel', 'stylesheet');
linkTag.setAttribute('href', targetPath);
document.head.appendChild(linkTag);
}
else {
logInfo && console.log(`Warning: found file ${targetPath}, but it was neither .js nor .css`);
}
}
}
return {
name: 'esbuild-html-plugin',
setup(build) {
build.onStart(() => {
if (!build.initialOptions.metafile) {
throw new Error('metafile is not enabled');
}
if (!build.initialOptions.outdir) {
throw new Error('outdir must be set');
}
});
build.onEnd(async (result) => {
const startTime = Date.now();
if (build.initialOptions.logLevel == 'debug' || build.initialOptions.logLevel == 'info') {
logInfo = true;
}
logInfo && console.log();
for (const htmlFileConfiguration of configuration.files) {
// First, search for outputs with the configured entryPoints
const collectedEntrypoints = collectEntrypoints(htmlFileConfiguration, result.metafile);
// All output files relevant for this html file
let collectedOutputFiles = [];
for (const entrypoint of collectedEntrypoints) {
if (!entrypoint) {
throw new Error(`Found no match for ${htmlFileConfiguration.entryPoints}`);
}
const relatedOutputFiles = new Map();
relatedOutputFiles.set(entrypoint.path, entrypoint);
if (htmlFileConfiguration.findRelatedCssFiles) {
if (entrypoint === null || entrypoint === void 0 ? void 0 : entrypoint.cssBundle) {
relatedOutputFiles.set(entrypoint.cssBundle, { path: entrypoint === null || entrypoint === void 0 ? void 0 : entrypoint.cssBundle });
}
}
if (htmlFileConfiguration.findRelatedOutputFiles) {
findNameRelatedOutputFiles(entrypoint, result.metafile, build.initialOptions.entryNames).forEach((item) => {
relatedOutputFiles.set(item.path, item);
});
}
collectedOutputFiles = [...collectedOutputFiles, ...relatedOutputFiles.values()];
}
// Note: we can safely disable this rule here, as we already asserted this in setup.onStart
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const outdir = build.initialOptions.outdir;
const publicPath = build.initialOptions.publicPath;
const templatingResult = await renderTemplate(htmlFileConfiguration);
// Next, we insert the found files into the htmlTemplate - if no htmlTemplate was specified, we default to a basic one.
const dom = new jsdom_1.JSDOM(templatingResult);
const document = dom.window.document;
if (htmlFileConfiguration.title) {
// If a title was given, we pass the title as well
document.title = htmlFileConfiguration.title;
}
if (htmlFileConfiguration.favicon) {
// Injects a favicon if present
await fs_1.default.promises.copyFile(htmlFileConfiguration.favicon, `${outdir}/favicon.ico`);
const linkTag = document.createElement('link');
linkTag.setAttribute('rel', 'icon');
let faviconPublicPath = '/favicon.ico';
if (publicPath) {
faviconPublicPath = joinWithPublicPath(publicPath, 'favicon.ico');
}
linkTag.setAttribute('href', faviconPublicPath);
document.head.appendChild(linkTag);
}
injectFiles(dom, collectedOutputFiles, outdir, publicPath, htmlFileConfiguration);
const out = posixJoin(outdir, htmlFileConfiguration.filename);
await fs_1.default.promises.mkdir(path_1.default.dirname(out), {
recursive: true,
});
await fs_1.default.promises.writeFile(out, dom.serialize());
const stat = await fs_1.default.promises.stat(out);
logInfo && console.log(` ${out} - ${stat.size}`);
}
logInfo && console.log(` HTML Plugin Done in ${Date.now() - startTime}ms`);
});
}
};
};
exports.htmlPlugin = htmlPlugin;