html-bundle 6.3.0 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +5 -5
- package/README.md +34 -3
- package/dist/bundle.mjs +69 -18
- package/dist/hmr-client.d.ts +40 -0
- package/dist/hmr-client.js +446 -0
- package/dist/utils.d.mts +14 -2
- package/dist/utils.mjs +56 -132
- package/package.json +4 -2
- package/src/bundle.mts +67 -18
- package/src/hmr-client.ts +577 -0
- package/src/utils.mts +79 -144
- package/tests/bundle.test.mjs +62 -3
- package/tests/hmr-client.test.mjs +245 -0
- package/tests/hmr-server.test.mjs +175 -0
- package/tsconfig.json +1 -1
package/dist/utils.mjs
CHANGED
|
@@ -6,8 +6,19 @@ import express from "express";
|
|
|
6
6
|
import postcssrc from "postcss-load-config";
|
|
7
7
|
import cssnano from "cssnano";
|
|
8
8
|
import { parse, parseFragment, serialize } from "parse5";
|
|
9
|
-
import { createScript, getTagName, findElement
|
|
9
|
+
import { createScript, getTagName, findElement } from "@web/parse5-utils";
|
|
10
10
|
export const bundleConfig = await getBundleConfig();
|
|
11
|
+
// The HMR client runtime is authored in src/hmr-client.ts and compiled by tsc to
|
|
12
|
+
// dist/hmr-client.js alongside this module (a real file, so its code is never
|
|
13
|
+
// mangled by template-literal escaping). Read it once and substitute the per-page
|
|
14
|
+
// tokens on demand in buildHMRClient().
|
|
15
|
+
let hmrClientTemplate = "";
|
|
16
|
+
try {
|
|
17
|
+
hmrClientTemplate = await readFile(new URL("./hmr-client.js", import.meta.url), "utf-8");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Only needed when --hmr is active; addHMRCode tolerates an empty template.
|
|
21
|
+
}
|
|
11
22
|
export function fileCopy(file) {
|
|
12
23
|
return copyFile(file, getBuildPath(file));
|
|
13
24
|
}
|
|
@@ -19,24 +30,29 @@ export function createDir(file) {
|
|
|
19
30
|
export function getBuildPath(file) {
|
|
20
31
|
return file.replace(`${bundleConfig.src}/`, `${bundleConfig.build}/`);
|
|
21
32
|
}
|
|
22
|
-
const CONNECTIONS =
|
|
33
|
+
const CONNECTIONS = new Set(); // In order to send the HMR information
|
|
23
34
|
export let serverSentEvents;
|
|
24
35
|
export async function createDefaultServer(isSecure) {
|
|
25
36
|
const router = express.Router();
|
|
26
37
|
const app = express();
|
|
27
38
|
app.use(router);
|
|
28
39
|
app.use(express.static(path.join(process.cwd(), bundleConfig.build)));
|
|
29
|
-
router.get("/hmr", (
|
|
40
|
+
router.get("/hmr", (req, reply) => {
|
|
30
41
|
reply.setHeader("Content-Type", "text/event-stream");
|
|
31
42
|
reply.setHeader("Cache-Control", "no-cache");
|
|
32
43
|
!isSecure && reply.setHeader("Connection", "keep-alive");
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
reply.flushHeaders();
|
|
45
|
+
CONNECTIONS.add(reply);
|
|
46
|
+
req.on("close", () => {
|
|
47
|
+
CONNECTIONS.delete(reply);
|
|
48
|
+
});
|
|
49
|
+
serverSentEvents = (event) => {
|
|
38
50
|
CONNECTIONS.forEach((rep) => {
|
|
39
|
-
rep.
|
|
51
|
+
if (rep.destroyed || rep.writableEnded) {
|
|
52
|
+
CONNECTIONS.delete(rep);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
rep.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
40
56
|
});
|
|
41
57
|
};
|
|
42
58
|
});
|
|
@@ -63,7 +79,15 @@ export async function getPostCSSConfig() {
|
|
|
63
79
|
try {
|
|
64
80
|
return await postcssrc({});
|
|
65
81
|
}
|
|
66
|
-
catch {
|
|
82
|
+
catch (err) {
|
|
83
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
84
|
+
// postcssrc throws "No PostCSS Config found" when the project is zero-config;
|
|
85
|
+
// that is expected, so stay silent. Any other failure (e.g. a broken config
|
|
86
|
+
// or a TypeScript config that cannot be loaded) would otherwise silently
|
|
87
|
+
// degrade the build to cssnano-only, so surface it.
|
|
88
|
+
if (!/No PostCSS Config found/i.test(message)) {
|
|
89
|
+
console.error(`\u26A0\uFE0F Could not load your PostCSS config \u2013 falling back to cssnano only. ${message}`);
|
|
90
|
+
}
|
|
67
91
|
return { plugins: [cssnano], options: {}, file: "" };
|
|
68
92
|
}
|
|
69
93
|
}
|
|
@@ -96,140 +120,40 @@ export function addHMRCode(html, file, ast) {
|
|
|
96
120
|
if (!htmlIdMap.has(file)) {
|
|
97
121
|
htmlIdMap.set(file, randomText());
|
|
98
122
|
}
|
|
99
|
-
const
|
|
123
|
+
const id = htmlIdMap.get(file);
|
|
124
|
+
const script = createScript({ type: "module", "data-hmr-client": id }, buildHMRClient(file, id, bundleConfig.src));
|
|
100
125
|
let DOM;
|
|
101
126
|
if (html.includes("<!DOCTYPE html>") || html.includes("<html")) {
|
|
102
127
|
DOM = ast || parse(html);
|
|
103
128
|
const headNode = findElement(DOM, (e) => getTagName(e) === "head");
|
|
104
|
-
|
|
129
|
+
// Inject first in <head> so the client runs before the page's own scripts.
|
|
130
|
+
prependChild(headNode, script);
|
|
105
131
|
}
|
|
106
132
|
else {
|
|
107
133
|
DOM = ast || parseFragment(html);
|
|
108
|
-
|
|
134
|
+
prependChild(DOM, script);
|
|
109
135
|
}
|
|
110
136
|
//@ts-ignore
|
|
111
|
-
DOM.childNodes.forEach((node) => node.attrs?.push({ name: "data-hmr", value:
|
|
137
|
+
DOM.childNodes.forEach((node) => node.attrs?.push({ name: "data-hmr", value: id }));
|
|
112
138
|
return serialize(DOM);
|
|
113
139
|
}
|
|
114
140
|
function randomText() {
|
|
115
141
|
return Math.random().toString(32).slice(2);
|
|
116
142
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const file = "${file}";
|
|
134
|
-
|
|
135
|
-
if (file === dataObj.file && "html" in dataObj) {
|
|
136
|
-
let newHTML;
|
|
137
|
-
try {
|
|
138
|
-
newHTML = html\`\${dataObj.html}\`
|
|
139
|
-
} catch {
|
|
140
|
-
setShouldSetReactivity(false);
|
|
141
|
-
newHTML = html\`\${dataObj.html}\`
|
|
142
|
-
setShouldSetReactivity(true);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (dataObj.html.startsWith('<!DOCTYPE html>') || dataObj.html.startsWith('<html')) {
|
|
146
|
-
document.head.remove(); // Don't try to diff the head – just re-run the scripts
|
|
147
|
-
|
|
148
|
-
// Restore Scroll
|
|
149
|
-
window.addEventListener("afterRouting", () => {
|
|
150
|
-
window.scrollTo(0, window.lastScroll);
|
|
151
|
-
delete window.lastScroll;
|
|
152
|
-
}, { once: true })
|
|
153
|
-
|
|
154
|
-
render(newHTML, document.documentElement, false);
|
|
155
|
-
} else {
|
|
156
|
-
const hmrID = "${id}";
|
|
157
|
-
const hmrElems = Array.from(newHTML.childNodes);
|
|
158
|
-
const hmrWheres = Array.from($$(\`[data-hmr="\${hmrID}"]\`))
|
|
159
|
-
// render new elements for old elements. Then, remove rest old elements and add add new elements after the last old one
|
|
160
|
-
hmrWheres.forEach((where, index) => {
|
|
161
|
-
if (index < hmrElems.length) {
|
|
162
|
-
render(hmrElems[index], where, false);
|
|
163
|
-
} else {
|
|
164
|
-
where.remove();
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
for (let rest = hmrWheres.length; rest < hmrElems.length; rest++) {
|
|
168
|
-
if (hmrWheres.length) {
|
|
169
|
-
const template = document.createElement('template');
|
|
170
|
-
hmrElems[hmrWheres.length - 1].after(template);
|
|
171
|
-
render(hmrElems[rest], template, false);
|
|
172
|
-
template.remove();
|
|
173
|
-
} else {
|
|
174
|
-
render(hmrElems[rest], false, false)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
$$('link[rel="stylesheet"][href]').forEach(link => {
|
|
180
|
-
link.setAttribute("href", link.getAttribute("href") + "?v=" + String(Math.random().toFixed(4)).slice(2));
|
|
181
|
-
})
|
|
182
|
-
if (dataObj.html.includes("<script")) updateElem("script");
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (dataObj.file === \`${src}/index.html\`) {
|
|
186
|
-
dispatchEvent(new Event("popstate"));
|
|
187
|
-
}
|
|
188
|
-
} else if (dataObj.file.endsWith(".css")) {
|
|
189
|
-
const now = performance.now();
|
|
190
|
-
if (!window.lastCalled.has(dataObj.file) || now - window.lastCalled.get(dataObj.file) > 100) {
|
|
191
|
-
$$('link[rel="stylesheet"][href]').forEach(link => {
|
|
192
|
-
link.setAttribute("href", link.getAttribute("href") + "?v=" + String(Math.random().toFixed(4)).slice(2));
|
|
193
|
-
})
|
|
194
|
-
window.lastCalled.set(dataObj.file, now)
|
|
195
|
-
}
|
|
196
|
-
} else if (dataObj.file.endsWith(".js")) {
|
|
197
|
-
const now = performance.now();
|
|
198
|
-
if (!window.lastCalled.has(dataObj.file) || now - window.lastCalled.get(dataObj.file) > 100) {
|
|
199
|
-
$$('link[rel="stylesheet"][href]').forEach(link => {
|
|
200
|
-
link.setAttribute("href", link.getAttribute("href") + "?v=" + String(Math.random().toFixed(4)).slice(2));
|
|
201
|
-
})
|
|
202
|
-
updateElem("script");
|
|
203
|
-
window.lastCalled.set(dataObj.file, now)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
function updateElem(type) {
|
|
209
|
-
const hmrId = "${id}";
|
|
210
|
-
const noSrcFile = dataObj.file.replace(\`${src}/\`, '');
|
|
211
|
-
const attr = type === "script" ? "src" : "href";
|
|
212
|
-
const elem = $(\`[data-hmr="\${hmrId}"] \${type}[\${attr}^="\${noSrcFile}"]\`); // could be $(\`\${type}[data-hmr="\${hmrId}"][\${attr}^="\${noSrcFile}"]\`) ?
|
|
213
|
-
|
|
214
|
-
if (elem) {
|
|
215
|
-
updateOne(type, attr, elem)
|
|
216
|
-
} else {
|
|
217
|
-
for(const e of $$(\`[data-hmr="\${hmrId}"] \${type}\`)) {
|
|
218
|
-
updateOne(type, attr, e);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function updateOne(type, attr, elem) {
|
|
224
|
-
const clone = document.createElement(type);
|
|
225
|
-
for (const key of elem.getAttributeNames()) {
|
|
226
|
-
clone.setAttribute(key, elem.getAttribute(key));
|
|
227
|
-
}
|
|
228
|
-
const attrVal = elem.getAttribute(attr);
|
|
229
|
-
if (attrVal) clone.setAttribute(attr, attrVal + "?v=" + String(Math.random().toFixed(4)).slice(2));
|
|
230
|
-
render(clone, elem, false);
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
`;
|
|
143
|
+
// Produce the per-page HMR client by substituting tokens into the shared runtime
|
|
144
|
+
// template (src/hmr-client.ts). The runtime is injected as an inline module so
|
|
145
|
+
// esbuild bundles hydro-js for it, but it coordinates through a single global hub
|
|
146
|
+
// so every composed page shares one EventSource and patches its own region.
|
|
147
|
+
function buildHMRClient(file, id, src) {
|
|
148
|
+
return hmrClientTemplate
|
|
149
|
+
.replaceAll("__HMR_FILE__", file)
|
|
150
|
+
.replaceAll("__HMR_ID__", id)
|
|
151
|
+
.replaceAll("__HMR_SRC__", src);
|
|
152
|
+
}
|
|
153
|
+
function prependChild(parent, node) {
|
|
154
|
+
// Insert as the first child so the HMR client runs before the page's own
|
|
155
|
+
// scripts — required for window.htmlBundleHMR.dispose()/data to be usable on
|
|
156
|
+
// initial load.
|
|
157
|
+
node.parentNode = parent;
|
|
158
|
+
parent.childNodes.unshift(node);
|
|
235
159
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "html-bundle",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"description": "A very simple bundler for HTML SFC",
|
|
5
5
|
"bin": "./dist/bundle.mjs",
|
|
6
6
|
"main": "./dist/bundle.mjs",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"types": "./dist/bundle.d.mts",
|
|
13
13
|
"scripts": {
|
|
14
|
-
"start": "tsc",
|
|
14
|
+
"start": "tsc && node -e \"require('fs').chmodSync('dist/bundle.mjs', 0o755)\"",
|
|
15
15
|
"test": "npm run start && node --test tests/*.test.mjs",
|
|
16
16
|
"update": "npx npm-check-updates -u && npx typesync && npm i && npm outdated"
|
|
17
17
|
},
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"@types/html-minifier-terser": "^7.0.2",
|
|
33
33
|
"@types/parse5": "^7.0.0",
|
|
34
34
|
"@types/postcss-load-config": "^3.0.1",
|
|
35
|
+
"happy-dom": "^20.10.6",
|
|
35
36
|
"typescript": "^6.0.3"
|
|
36
37
|
},
|
|
37
38
|
"repository": {
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"glob": "^13.0.6",
|
|
51
52
|
"html-minifier-terser": "^7.2.0",
|
|
52
53
|
"hydro-js": "^1.9.0",
|
|
54
|
+
"jiti": "^2.7.0",
|
|
53
55
|
"parse5": "^8.0.1",
|
|
54
56
|
"postcss": "^8.5.16",
|
|
55
57
|
"postcss-load-config": "^6.0.1"
|
package/src/bundle.mts
CHANGED
|
@@ -122,6 +122,13 @@ async function build(files: string[], firstRun = true) {
|
|
|
122
122
|
console.log(`⌛ Waiting for file changes ...`);
|
|
123
123
|
|
|
124
124
|
const chokidarOptions = { awaitWriteFinish: false };
|
|
125
|
+
let rebuildQueue = Promise.resolve();
|
|
126
|
+
const enqueueRebuild = (file: string) => {
|
|
127
|
+
rebuildQueue = rebuildQueue
|
|
128
|
+
.catch(() => undefined)
|
|
129
|
+
.then(() => rebuild(file));
|
|
130
|
+
return rebuildQueue;
|
|
131
|
+
};
|
|
125
132
|
if (postcssFile) {
|
|
126
133
|
const postCSSWatcher = watch(postcssFile, chokidarOptions);
|
|
127
134
|
const tailwindCSSWatcher = watch(
|
|
@@ -156,7 +163,8 @@ async function build(files: string[], firstRun = true) {
|
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
try {
|
|
159
|
-
|
|
166
|
+
files.push(file);
|
|
167
|
+
await enqueueRebuild(file);
|
|
160
168
|
} catch {}
|
|
161
169
|
|
|
162
170
|
console.log(`⚡ added ${file} to the build`);
|
|
@@ -167,7 +175,7 @@ async function build(files: string[], firstRun = true) {
|
|
|
167
175
|
}
|
|
168
176
|
file = String.raw`${file}`.replace(/\\/g, "/");
|
|
169
177
|
|
|
170
|
-
await
|
|
178
|
+
await enqueueRebuild(file);
|
|
171
179
|
|
|
172
180
|
console.log(`⚡ modified ${file} on the build`);
|
|
173
181
|
});
|
|
@@ -177,6 +185,8 @@ async function build(files: string[], firstRun = true) {
|
|
|
177
185
|
}
|
|
178
186
|
file = String.raw`${file}`.replace(/\\/g, "/");
|
|
179
187
|
|
|
188
|
+
const fileIndex = files.indexOf(file);
|
|
189
|
+
if (fileIndex !== -1) files.splice(fileIndex, 1);
|
|
180
190
|
inlineFiles.delete(file);
|
|
181
191
|
const buildFile = getBuildPath(file)
|
|
182
192
|
.replace(".ts", ".js")
|
|
@@ -189,45 +199,82 @@ async function build(files: string[], firstRun = true) {
|
|
|
189
199
|
if (!stats.length) await rm(bfDir);
|
|
190
200
|
} catch {}
|
|
191
201
|
|
|
202
|
+
serverSentEvents?.({ type: "full-reload", file });
|
|
203
|
+
|
|
192
204
|
console.log(`⚡ deleted ${file} from the build`);
|
|
193
205
|
});
|
|
194
206
|
|
|
195
207
|
async function rebuild(file: string) {
|
|
196
208
|
// Rebuild all CSS because a change in any file might need to trigger PostCSS zu rebuild(e.g. Tailwind CSS)
|
|
197
209
|
await rebuildCSS(files.filter((file) => file.endsWith(".css")));
|
|
210
|
+
const htmlFiles = files.filter((f) => f.endsWith(".html"));
|
|
198
211
|
|
|
199
|
-
let html;
|
|
200
212
|
if (file.endsWith(".html")) {
|
|
213
|
+
const previousHtml = builtHTMLCache.get(file);
|
|
201
214
|
// To refill the inlineFiles needed to build JS
|
|
202
|
-
for (const htmlFile of
|
|
215
|
+
for (const htmlFile of htmlFiles) {
|
|
203
216
|
await writeInlineScripts(htmlFile);
|
|
204
217
|
}
|
|
205
218
|
await minifyCode();
|
|
206
|
-
for (const
|
|
207
|
-
if (INLINE_BUNDLE_FILE.test(
|
|
208
|
-
inlineFiles.delete(
|
|
209
|
-
await rm(
|
|
219
|
+
for (const inline of inlineFiles) {
|
|
220
|
+
if (INLINE_BUNDLE_FILE.test(inline)) {
|
|
221
|
+
inlineFiles.delete(inline);
|
|
222
|
+
await rm(inline);
|
|
210
223
|
}
|
|
211
224
|
}
|
|
212
|
-
html = await minifyHTML(file, getBuildPath(file));
|
|
225
|
+
const html = await minifyHTML(file, getBuildPath(file));
|
|
226
|
+
serverSentEvents?.({ type: "html", file, html, previousHtml });
|
|
213
227
|
} else if (/\.(jsx?|tsx?)$/.test(file)) {
|
|
228
|
+
// A module change alters the inlined output of whichever page(s) import
|
|
229
|
+
// it. Rebuild every page, then emit only the pages whose HTML actually
|
|
230
|
+
// changed; the client diff re-runs just the scripts that differ, so
|
|
231
|
+
// unrelated pages keep their state.
|
|
214
232
|
inlineFiles.add(file);
|
|
233
|
+
for (const htmlFile of htmlFiles) {
|
|
234
|
+
await writeInlineScripts(htmlFile);
|
|
235
|
+
}
|
|
215
236
|
await minifyCode();
|
|
216
|
-
|
|
237
|
+
for (const inline of inlineFiles) {
|
|
238
|
+
if (INLINE_BUNDLE_FILE.test(inline)) {
|
|
239
|
+
inlineFiles.delete(inline);
|
|
240
|
+
await rm(inline);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
let didEmit = false;
|
|
244
|
+
for (const htmlFile of htmlFiles) {
|
|
245
|
+
const previousHtml = builtHTMLCache.get(htmlFile);
|
|
246
|
+
const html = await minifyHTML(htmlFile, getBuildPath(htmlFile));
|
|
247
|
+
if (html !== previousHtml) {
|
|
248
|
+
didEmit = true;
|
|
249
|
+
serverSentEvents?.({
|
|
250
|
+
type: "html",
|
|
251
|
+
file: htmlFile,
|
|
252
|
+
html,
|
|
253
|
+
previousHtml,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!didEmit) {
|
|
258
|
+
serverSentEvents?.({ type: "full-reload", file });
|
|
259
|
+
}
|
|
260
|
+
} else if (file.endsWith(".css")) {
|
|
261
|
+
serverSentEvents?.({ type: "css", file });
|
|
262
|
+
} else {
|
|
217
263
|
if (handlerFile) {
|
|
218
|
-
|
|
264
|
+
try {
|
|
265
|
+
const { stdout } = await execFilePromise("node", [
|
|
266
|
+
handlerFile,
|
|
267
|
+
file,
|
|
268
|
+
]);
|
|
219
269
|
console.log("📋 Logging Handler: ", String(stdout));
|
|
220
|
-
})
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.error(err);
|
|
272
|
+
}
|
|
221
273
|
} else {
|
|
222
274
|
await fileCopy(file);
|
|
223
275
|
}
|
|
224
|
-
|
|
225
|
-
execFilePromise("node", [handlerFile, file]).then(({ stdout }) => {
|
|
226
|
-
console.log("📋 Logging Handler: ", String(stdout));
|
|
227
|
-
});
|
|
276
|
+
serverSentEvents?.({ type: "asset", file });
|
|
228
277
|
}
|
|
229
|
-
|
|
230
|
-
serverSentEvents?.({ file, html });
|
|
231
278
|
}
|
|
232
279
|
}
|
|
233
280
|
}
|
|
@@ -297,6 +344,7 @@ async function minifyCode(): Promise<unknown> {
|
|
|
297
344
|
}
|
|
298
345
|
|
|
299
346
|
const htmlFilesCache = new Map();
|
|
347
|
+
const builtHTMLCache = new Map<string, string>();
|
|
300
348
|
async function writeInlineScripts(file: string) {
|
|
301
349
|
let fileText = await readFile(file, { encoding: "utf-8" });
|
|
302
350
|
|
|
@@ -430,6 +478,7 @@ async function minifyHTML(file: string, buildFile: string) {
|
|
|
430
478
|
}
|
|
431
479
|
|
|
432
480
|
await writeFile(buildFile, fileText);
|
|
481
|
+
builtHTMLCache.set(file, fileText);
|
|
433
482
|
return fileText;
|
|
434
483
|
}
|
|
435
484
|
|