@heroiclands/package-build 25.5.3 → 25.5.5
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/CHANGELOG.md +28 -0
- package/README.md +7 -0
- package/bin/content-commands.mjs +32 -8
- package/docs/api.md +57 -16
- package/docs/authoring/links-and-markup.md +51 -3
- package/docs/commands.md +60 -17
- package/docs/configuration.md +3 -2
- package/docs/content-format.md +10 -0
- package/docs/diagnostics.md +17 -0
- package/docs/guides/module-project.md +11 -3
- package/docs/guides/site.md +12 -2
- package/docs/guides/writing-in-invented-scripts.md +5 -2
- package/docs/reference/format-details.md +61 -11
- package/docs/reference/note-types.md +32 -32
- package/e2e.mjs +1 -1
- package/engine/base-compiler.mjs +2 -0
- package/engine/compile-corpus.mjs +6 -2
- package/engine/content-charset.mjs +1 -1
- package/engine/content-render.mjs +14 -2
- package/engine/content-scripts.mjs +44 -6
- package/engine/content-session.mjs +57 -20
- package/engine/content-spans.mjs +3 -4
- package/engine/content-verse.mjs +52 -10
- package/engine/core-report.mjs +27 -18
- package/engine/language-names.mjs +53 -23
- package/engine/markdownlint-run.mjs +1 -1
- package/engine/markup-validation.mjs +2 -0
- package/engine/note-vocabulary.mjs +10 -5
- package/engine/npc-generate.mjs +52 -7
- package/engine/page-breaks.mjs +115 -0
- package/engine/pdf-build.mjs +12 -4
- package/engine/pdf-render.mjs +10 -1
- package/engine/preview-index.mjs +4 -2
- package/engine/site-build.mjs +12 -3
- package/engine/site-preview.mjs +3 -0
- package/engine/site-render.mjs +18 -4
- package/engine/site-root.mjs +10 -1
- package/engine/site-serve-http.mjs +151 -0
- package/engine/site-serve.mjs +196 -126
- package/engine/sql-tables.mjs +379 -214
- package/engine/verse-metre.mjs +50 -5
- package/engine/verse-regions.mjs +16 -0
- package/package.json +3 -3
- package/types/e2e.d.mts +1 -1
- package/types/engine/content-scripts.d.mts +9 -1
- package/types/engine/content-session.d.mts +1 -1
- package/types/engine/content-verse.d.mts +11 -2
- package/types/engine/core-report.d.mts +6 -2
- package/types/engine/language-names.d.mts +18 -6
- package/types/engine/npc-generate.d.mts +3 -0
- package/types/engine/page-breaks.d.mts +30 -0
- package/types/engine/pdf-render.d.mts +2 -0
- package/types/engine/site-build.d.mts +3 -1
- package/types/engine/site-render.d.mts +6 -2
- package/types/engine/site-root.d.mts +3 -1
- package/types/engine/site-serve-http.d.mts +1 -0
- package/types/engine/site-serve.d.mts +12 -6
- package/types/engine/sql-tables.d.mts +37 -8
- package/types/engine/verse-metre.d.mts +2 -1
- package/types/engine/verse-regions.d.mts +8 -0
package/engine/site-serve.mjs
CHANGED
|
@@ -25,21 +25,20 @@
|
|
|
25
25
|
*
|
|
26
26
|
* A page that changes because another note changed — a table that lists the
|
|
27
27
|
* note, a link that carries its name — is written again by the next whole
|
|
28
|
-
* build. A
|
|
29
|
-
*
|
|
30
|
-
* site in place.
|
|
28
|
+
* build. A failed build is reported and leaves the last successful site in
|
|
29
|
+
* place, including its search index.
|
|
31
30
|
*
|
|
32
31
|
* The site is served from the deployed root, so a page is at the address it is
|
|
33
32
|
* deployed at, its base path included. Every HTML page is sent with a script
|
|
34
33
|
* that listens on {@link RELOAD_PATH} for the server's `reload` event; the
|
|
35
34
|
* script is added as the page is sent and is never written to the site. The
|
|
36
|
-
* search index is built
|
|
35
|
+
* search index is built with each whole build before it replaces the last site.
|
|
37
36
|
*
|
|
38
37
|
* @module
|
|
39
38
|
*/
|
|
40
39
|
|
|
41
40
|
import fs from "node:fs";
|
|
42
|
-
import
|
|
41
|
+
import { Worker } from "node:worker_threads";
|
|
43
42
|
import path from "node:path";
|
|
44
43
|
|
|
45
44
|
import { siteBuildFailed } from "./site-build.mjs";
|
|
@@ -47,6 +46,7 @@ import { SITE_THEME_DIR } from "./site-theme.mjs";
|
|
|
47
46
|
import { SITE_STATIC_DIR, buildPackageSite, writeSite, writeThemeFiles } from "./site-render.mjs";
|
|
48
47
|
import { siteBase } from "../content-config.mjs";
|
|
49
48
|
import { SEARCH_DIR, indexSite } from "./site-root.mjs";
|
|
49
|
+
import { readCachedNavigation, siteSettings } from "./site-config.mjs";
|
|
50
50
|
|
|
51
51
|
/** The address a served page listens on for the server's reloads. */
|
|
52
52
|
export const RELOAD_PATH = "/__package-build/reload";
|
|
@@ -54,29 +54,6 @@ export const RELOAD_PATH = "/__package-build/reload";
|
|
|
54
54
|
/** How long a burst of changes is gathered before it is written, in milliseconds. */
|
|
55
55
|
const SETTLE_MS = 120;
|
|
56
56
|
|
|
57
|
-
/** What a file is sent as, by its extension. */
|
|
58
|
-
const CONTENT_TYPES = Object.freeze({
|
|
59
|
-
".html": "text/html; charset=utf-8",
|
|
60
|
-
".css": "text/css; charset=utf-8",
|
|
61
|
-
".js": "text/javascript; charset=utf-8",
|
|
62
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
63
|
-
".json": "application/json",
|
|
64
|
-
".xml": "application/xml",
|
|
65
|
-
".txt": "text/plain; charset=utf-8",
|
|
66
|
-
".svg": "image/svg+xml",
|
|
67
|
-
".png": "image/png",
|
|
68
|
-
".jpg": "image/jpeg",
|
|
69
|
-
".jpeg": "image/jpeg",
|
|
70
|
-
".webp": "image/webp",
|
|
71
|
-
".gif": "image/gif",
|
|
72
|
-
".woff2": "font/woff2",
|
|
73
|
-
".woff": "font/woff",
|
|
74
|
-
".wasm": "application/wasm",
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
/** The script a served page is sent with. */
|
|
78
|
-
const RELOAD_SCRIPT = `<script>new EventSource(${JSON.stringify(RELOAD_PATH)}).addEventListener("reload", () => location.reload());</script>`;
|
|
79
|
-
|
|
80
57
|
/** Whether a note's text holds a query its build prepares ahead of the render. */
|
|
81
58
|
const holdsQuery = (text) => /```\s*sql\b|\{\{[^}]*\bsql\b/.test(text);
|
|
82
59
|
|
|
@@ -91,16 +68,19 @@ const holdsQuery = (text) => /```\s*sql\b|\{\{[^}]*\bsql\b/.test(text);
|
|
|
91
68
|
* directory above `out` by default; `out` itself for a site served at `/`.
|
|
92
69
|
* @param {string} [options.host] - The address to listen on.
|
|
93
70
|
* @param {number} [options.port] - The port; `0` takes any free one.
|
|
94
|
-
* @param {boolean} [options.search] - Build the search index
|
|
71
|
+
* @param {boolean} [options.search] - Build the search index with each whole
|
|
95
72
|
* build, where the site has one.
|
|
96
73
|
* @param {number} [options.year] - The year the footer states.
|
|
97
|
-
* @param {(event: {kind: string, built?: object, files?: string[], findings?: object[], error?: unknown}) => void} [options.report] -
|
|
98
|
-
* Told of each build: `built` for a whole build, `page` for one note's page,
|
|
74
|
+
* @param {(event: {kind: string, built?: object, files?: string[], findings?: object[], error?: unknown, message?: string, fatal?: boolean}) => void} [options.report] -
|
|
75
|
+
* Told of progress (`message`) and each build: `built` for a whole build, `page` for one note's page,
|
|
99
76
|
* `theme` for the theme, `failed` with what the build found, `error` with
|
|
100
77
|
* what it threw.
|
|
101
|
-
* @returns {Promise<{url: string, close: () => Promise<void>, settled: () => Promise<void>}>}
|
|
102
|
-
*
|
|
103
|
-
*
|
|
78
|
+
* @returns {Promise<{url: string, close: () => Promise<void>, settled: () => Promise<void>, done: Promise<void>}>}
|
|
79
|
+
* Resolves as soon as HTTP listens, before the first build. The address of
|
|
80
|
+
* the site's home page, how to stop serving, and a promise for the initial
|
|
81
|
+
* build and every change seen so far to finish. Failed builds are reported
|
|
82
|
+
* through `report` and leave the last successful site in service. `done`
|
|
83
|
+
* resolves on close and rejects if HTTP stops unexpectedly.
|
|
104
84
|
*/
|
|
105
85
|
export async function serveSite({
|
|
106
86
|
config,
|
|
@@ -112,27 +92,147 @@ export async function serveSite({
|
|
|
112
92
|
year,
|
|
113
93
|
report = () => {},
|
|
114
94
|
}) {
|
|
115
|
-
|
|
116
|
-
|
|
95
|
+
// Refuse a site that cannot be described before starting its server.
|
|
96
|
+
siteSettings({ config });
|
|
97
|
+
readCachedNavigation(config);
|
|
98
|
+
root = path.resolve(root);
|
|
99
|
+
out = path.resolve(out);
|
|
100
|
+
let timer = null;
|
|
101
|
+
const watchers = [];
|
|
102
|
+
let closing = false;
|
|
103
|
+
let complete;
|
|
104
|
+
let abort;
|
|
105
|
+
const done = new Promise((resolve, reject) => {
|
|
106
|
+
complete = resolve;
|
|
107
|
+
abort = reject;
|
|
108
|
+
});
|
|
109
|
+
// The caller may attach its lifecycle handler after awaiting startup.
|
|
110
|
+
done.catch(() => {});
|
|
111
|
+
const stopWatching = () => {
|
|
112
|
+
if (timer) clearTimeout(timer);
|
|
113
|
+
timer = null;
|
|
114
|
+
for (const watcher of watchers) watcher.close();
|
|
115
|
+
};
|
|
116
|
+
const worker = new Worker(new URL("./site-serve-http.mjs", import.meta.url), {
|
|
117
|
+
workerData: { root, out, host, port, reloadPath: RELOAD_PATH },
|
|
118
|
+
// The HTTP helper uses only Node builtins. A caller's --input-type
|
|
119
|
+
// or loader flags describe its own entry point, not this file.
|
|
120
|
+
execArgv: [],
|
|
121
|
+
});
|
|
122
|
+
let workerFailure;
|
|
123
|
+
let started = false;
|
|
124
|
+
const requests = new Map();
|
|
125
|
+
worker.on("message", (event) => {
|
|
126
|
+
const waiting = requests.get(event.kind);
|
|
127
|
+
if (!waiting) return;
|
|
128
|
+
requests.delete(event.kind);
|
|
129
|
+
if (event.error) waiting.reject(event.error);
|
|
130
|
+
else waiting.resolve(event);
|
|
131
|
+
});
|
|
132
|
+
const failed = (error) => {
|
|
133
|
+
if (workerFailure) return;
|
|
134
|
+
workerFailure = error;
|
|
135
|
+
stopWatching();
|
|
136
|
+
abort(error);
|
|
137
|
+
for (const waiting of requests.values()) waiting.reject(error);
|
|
138
|
+
requests.clear();
|
|
139
|
+
if (started) report({ kind: "error", error, fatal: true });
|
|
140
|
+
};
|
|
141
|
+
worker.on("error", failed);
|
|
142
|
+
const exited = new Promise((resolve) => {
|
|
143
|
+
worker.once("exit", (code) => {
|
|
144
|
+
if ((started || requests.size) && !workerFailure)
|
|
145
|
+
failed(new Error(`the preview HTTP server stopped (exit ${code})`));
|
|
146
|
+
resolve();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
const receive = (kind) =>
|
|
150
|
+
new Promise((resolve, reject) => {
|
|
151
|
+
if (workerFailure) reject(workerFailure);
|
|
152
|
+
else requests.set(kind, { resolve, reject });
|
|
153
|
+
});
|
|
154
|
+
const listening = await receive("listening");
|
|
155
|
+
started = true;
|
|
117
156
|
let state = null;
|
|
157
|
+
const progress = (message) => report({ kind: "progress", message });
|
|
158
|
+
const reload = () => worker.postMessage({ kind: "reload" });
|
|
118
159
|
|
|
119
|
-
|
|
120
|
-
|
|
160
|
+
// Whole builds and incremental updates publish through the same boundary.
|
|
161
|
+
// HTTP owns the renames, between requests; writers only touch private trees.
|
|
162
|
+
const publish = async (write) => {
|
|
163
|
+
if (workerFailure || closing) return null;
|
|
164
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
165
|
+
const staging = fs.mkdtempSync(path.join(path.dirname(out), ".site-preview-"));
|
|
166
|
+
const staged = path.join(staging, "site");
|
|
167
|
+
const previous = path.join(staging, "previous");
|
|
168
|
+
let installed = false;
|
|
169
|
+
try {
|
|
170
|
+
const result = await write(staged);
|
|
171
|
+
if (!result || closing) return null;
|
|
172
|
+
if (workerFailure) throw workerFailure;
|
|
173
|
+
const ready = receive("installed");
|
|
174
|
+
worker.postMessage({ kind: "install", staged, previous });
|
|
175
|
+
await ready;
|
|
176
|
+
installed = true;
|
|
177
|
+
return result;
|
|
178
|
+
} finally {
|
|
179
|
+
// A failed rollback must retain the only remaining previous tree.
|
|
180
|
+
if (installed || !fs.existsSync(previous))
|
|
181
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
182
|
+
}
|
|
121
183
|
};
|
|
122
184
|
|
|
123
185
|
const buildAll = async () => {
|
|
124
|
-
const result = await
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
186
|
+
const result = await publish(async (staged) => {
|
|
187
|
+
const built = await buildPackageSite({ config, out: staged, year, progress });
|
|
188
|
+
const findings = [...(built.written?.findings ?? []), ...(built.fonts?.findings ?? [])];
|
|
189
|
+
if (
|
|
190
|
+
!built.written ||
|
|
191
|
+
siteBuildFailed(built.built) ||
|
|
192
|
+
built.fonts?.findings.length ||
|
|
193
|
+
findings.some((finding) => finding.severity === "error")
|
|
194
|
+
) {
|
|
195
|
+
report({ kind: "failed", built: built.built, findings });
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
if (search && config.site.search !== false) {
|
|
199
|
+
progress("building the search index");
|
|
200
|
+
const indexed = await indexSite({
|
|
201
|
+
site: staged,
|
|
202
|
+
output: path.join(staged, SEARCH_DIR),
|
|
203
|
+
base: siteBase(config),
|
|
204
|
+
});
|
|
205
|
+
progress(`indexed ${indexed.pages} page(s) for search`);
|
|
206
|
+
}
|
|
207
|
+
return { ...built, findings };
|
|
208
|
+
});
|
|
209
|
+
if (!result) return false;
|
|
128
210
|
state = result;
|
|
129
|
-
report({ kind: "built", built: result.built, findings: result.
|
|
211
|
+
report({ kind: "built", built: result.built, findings: result.findings });
|
|
130
212
|
return true;
|
|
131
213
|
};
|
|
132
214
|
|
|
133
|
-
const
|
|
215
|
+
const redraw = async ({ pages, only, theme = false }) => {
|
|
216
|
+
return publish((staged) => {
|
|
217
|
+
// Keep unchanged pages, static files and the current search index.
|
|
218
|
+
fs.cpSync(out, staged, { recursive: true, mode: fs.constants.COPYFILE_FICLONE });
|
|
219
|
+
if (theme) writeThemeFiles(staged);
|
|
220
|
+
const written = writeSite({ site: state.site, pages, out: staged, only });
|
|
221
|
+
if (written.findings.some((finding) => finding.severity === "error")) {
|
|
222
|
+
report({ kind: "failed", findings: written.findings });
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
...written,
|
|
227
|
+
files: written.files.map((file) => path.join(out, path.relative(staged, file))),
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const redrawNotes = async (files) => {
|
|
134
233
|
if (!state) return false;
|
|
135
234
|
const changed = new Set();
|
|
235
|
+
let pages = state.pages;
|
|
136
236
|
for (const file of files) {
|
|
137
237
|
const text = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
138
238
|
if (!file.endsWith(".md") || !text || holdsQuery(text)) return false;
|
|
@@ -140,41 +240,33 @@ export async function serveSite({
|
|
|
140
240
|
if (!entry) return false;
|
|
141
241
|
if (entry.findings.length) {
|
|
142
242
|
report({ kind: "failed", findings: entry.findings });
|
|
143
|
-
return
|
|
243
|
+
return null;
|
|
144
244
|
}
|
|
145
245
|
const page = state.pageOf(entry);
|
|
146
|
-
|
|
246
|
+
pages = pages.map((old) => (old.url === page.url ? page : old));
|
|
147
247
|
changed.add(page.url);
|
|
148
248
|
}
|
|
149
|
-
const written =
|
|
249
|
+
const written = await redraw({ pages, only: changed });
|
|
250
|
+
if (!written) return null;
|
|
251
|
+
state.pages = pages;
|
|
150
252
|
report({ kind: "page", files: written.files, findings: written.findings });
|
|
151
253
|
return true;
|
|
152
254
|
};
|
|
153
255
|
|
|
154
|
-
const redrawTheme = () => {
|
|
256
|
+
const redrawTheme = async () => {
|
|
155
257
|
if (!state) return false;
|
|
156
|
-
|
|
157
|
-
const written = writeSite({
|
|
158
|
-
site: state.site,
|
|
258
|
+
const written = await redraw({
|
|
159
259
|
pages: state.pages,
|
|
160
|
-
out,
|
|
161
260
|
only: new Set(state.pages.map((page) => page.url)),
|
|
261
|
+
theme: true,
|
|
162
262
|
});
|
|
263
|
+
if (!written) return false;
|
|
163
264
|
report({ kind: "theme", files: written.files, findings: written.findings });
|
|
164
265
|
return true;
|
|
165
266
|
};
|
|
166
267
|
|
|
167
|
-
const indexForSearch = async () => {
|
|
168
|
-
if (!search || config.site.search === false || !state) return;
|
|
169
|
-
await indexSite({ site: out, output: path.join(out, SEARCH_DIR), base: siteBase(config) });
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
await buildAll();
|
|
173
|
-
await indexForSearch();
|
|
174
|
-
|
|
175
268
|
/** @type {{content: Set<string>, theme: boolean}} */
|
|
176
269
|
let pending = { content: new Set(), theme: false };
|
|
177
|
-
let timer = null;
|
|
178
270
|
let work = Promise.resolve();
|
|
179
271
|
const flush = () => {
|
|
180
272
|
timer = null;
|
|
@@ -182,93 +274,71 @@ export async function serveSite({
|
|
|
182
274
|
pending = { content: new Set(), theme: false };
|
|
183
275
|
work = work.then(async () => {
|
|
184
276
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (done) reload();
|
|
277
|
+
if (workerFailure || closing) return;
|
|
278
|
+
let changed = batch.content.size ? await redrawNotes([...batch.content]) : true;
|
|
279
|
+
if (changed === false) changed = await buildAll();
|
|
280
|
+
else if (changed && batch.theme) changed = await redrawTheme();
|
|
281
|
+
if (changed) reload();
|
|
191
282
|
} catch (error) {
|
|
192
|
-
report({ kind: "error", error });
|
|
283
|
+
if (error !== workerFailure) report({ kind: "error", error });
|
|
193
284
|
}
|
|
194
285
|
});
|
|
195
286
|
};
|
|
196
287
|
const queue = (kind, file) => {
|
|
288
|
+
if (workerFailure || closing) return;
|
|
197
289
|
if (kind === "theme") pending.theme = true;
|
|
198
290
|
else pending.content.add(file);
|
|
199
291
|
if (!timer) timer = setTimeout(flush, SETTLE_MS);
|
|
200
292
|
};
|
|
201
293
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
.filter((dir) => fs.existsSync(dir))
|
|
208
|
-
.map((dir) => fs.watch(dir, { recursive: true }, () => queue("theme"))),
|
|
209
|
-
];
|
|
210
|
-
|
|
211
|
-
const server = http.createServer((req, res) => {
|
|
212
|
-
const address = decodeURIComponent(
|
|
213
|
-
new URL(req.url ?? "/", "http://serve.invalid").pathname,
|
|
294
|
+
try {
|
|
295
|
+
watchers.push(
|
|
296
|
+
fs.watch(config.paths.content, { recursive: true }, (_event, name) => {
|
|
297
|
+
if (name) queue("content", path.join(config.paths.content, String(name)));
|
|
298
|
+
}),
|
|
214
299
|
);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
"cache-control": "no-cache",
|
|
219
|
-
connection: "keep-alive",
|
|
220
|
-
});
|
|
221
|
-
res.write(": connected\n\n");
|
|
222
|
-
listeners.add(res);
|
|
223
|
-
req.on("close", () => listeners.delete(res));
|
|
224
|
-
return;
|
|
300
|
+
for (const dir of [SITE_THEME_DIR, SITE_STATIC_DIR]) {
|
|
301
|
+
if (fs.existsSync(dir))
|
|
302
|
+
watchers.push(fs.watch(dir, { recursive: true }, () => queue("theme")));
|
|
225
303
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const type = CONTENT_TYPES[path.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
243
|
-
let body = fs.readFileSync(file);
|
|
244
|
-
if (type.startsWith("text/html")) {
|
|
245
|
-
const html = body.toString("utf8");
|
|
246
|
-
const at = html.lastIndexOf("</body>");
|
|
247
|
-
body = Buffer.from(
|
|
248
|
-
at < 0 ? html + RELOAD_SCRIPT : html.slice(0, at) + RELOAD_SCRIPT + html.slice(at),
|
|
249
|
-
);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
stopWatching();
|
|
306
|
+
started = false;
|
|
307
|
+
worker.postMessage({ kind: "close" });
|
|
308
|
+
await exited;
|
|
309
|
+
abort(error);
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Return the listening address before beginning the first long build.
|
|
314
|
+
work = new Promise((resolve) => setImmediate(resolve)).then(async () => {
|
|
315
|
+
try {
|
|
316
|
+
if (await buildAll()) reload();
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (error !== workerFailure) report({ kind: "error", error });
|
|
250
319
|
}
|
|
251
|
-
res.writeHead(status, { "content-type": type, "cache-control": "no-store" });
|
|
252
|
-
res.end(body);
|
|
253
320
|
});
|
|
254
|
-
await new Promise((resolve) => server.listen(port, host, resolve));
|
|
255
|
-
const listening = /** @type {import("node:net").AddressInfo} */ (server.address());
|
|
256
|
-
const base = new URL(config.homepage).pathname;
|
|
257
321
|
|
|
258
322
|
return {
|
|
259
|
-
url: `http://${host}:${listening.port}${
|
|
323
|
+
url: `http://${host}:${listening.port}${new URL(config.homepage).pathname}`,
|
|
324
|
+
done,
|
|
260
325
|
settled: async () => {
|
|
261
|
-
|
|
326
|
+
for (;;) {
|
|
327
|
+
const current = work;
|
|
328
|
+
await current;
|
|
329
|
+
if (workerFailure) throw workerFailure;
|
|
330
|
+
if (!timer && !pending.content.size && !pending.theme && current === work) return;
|
|
262
331
|
await new Promise((resolve) => setTimeout(resolve, SETTLE_MS));
|
|
263
332
|
}
|
|
264
|
-
await work;
|
|
265
333
|
},
|
|
266
334
|
close: async () => {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
for (const res of listeners) res.end();
|
|
335
|
+
closing = true;
|
|
336
|
+
stopWatching();
|
|
270
337
|
await work;
|
|
271
|
-
|
|
338
|
+
started = false;
|
|
339
|
+
worker.postMessage({ kind: "close" });
|
|
340
|
+
await exited;
|
|
341
|
+
complete();
|
|
272
342
|
},
|
|
273
343
|
};
|
|
274
344
|
}
|