gutterpress 0.9.0-alpha.1 → 0.9.0-alpha.2
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/dist/api/index.js +1 -1
- package/dist/{audit-k16v3n98.js → audit-nhn2pjz3.js} +3 -3
- package/dist/{build-8tqr4gbb.js → build-san7fv2z.js} +3 -3
- package/dist/{cli-rkqxbnkq.js → cli-e5zhb0xs.js} +1 -1
- package/dist/{cli-yp0d2hp8.js → cli-k4bd06sd.js} +377 -78
- package/dist/{cli-v2dze8ja.js → cli-najycadg.js} +1 -1
- package/dist/cli.js +15 -15
- package/dist/{doctor-57czxy3q.js → doctor-hrk0kxxz.js} +2 -2
- package/dist/{index-m3pa4wyf.js → index-bynn850m.js} +379 -80
- package/dist/index.js +1 -1
- package/dist/lib/markdown/assemble.d.ts +4 -6
- package/dist/lib/markdown/chapter-id.d.ts +1 -1
- package/dist/lib/markdown/index.d.ts +1 -1
- package/dist/{lint-4sq0dvf6.js → lint-96j9hrj4.js} +3 -3
- package/dist/{new-f2nmyr5b.js → new-8p38wavc.js} +3 -3
- package/dist/{pagedjs-interface-80g3xa8p.js → pagedjs-interface-qxvzgwd7.js} +92 -33
- package/dist/{plugin-xskq3dtx.js → plugin-ees6nhkc.js} +3 -3
- package/dist/{preflight-epcffags.js → preflight-1q6c2edh.js} +3 -3
- package/dist/preview/file-watcher.d.ts +39 -17
- package/dist/preview/http-server.d.ts +5 -2
- package/dist/preview/server-context.d.ts +2 -0
- package/dist/preview-shell-6dqexx1m.js +581 -0
- package/dist/{preview-xf5dktz2.js → preview-y5a2zen1.js} +3 -3
- package/dist/{publish-97vdwq31.js → publish-rm9yb3wh.js} +3 -3
- package/dist/render.js +10 -19
- package/dist/{repair-cz4z4hkg.js → repair-zgq7q2g6.js} +3 -3
- package/dist/server.d.ts +2 -0
- package/dist/{validate-swzejnd8.js → validate-nr0xa6sa.js} +3 -3
- package/package.json +1 -1
- package/dist/preview-shell-rghtfmc8.js +0 -155
|
@@ -20,13 +20,13 @@ import {
|
|
|
20
20
|
stampCreator,
|
|
21
21
|
stripAnnotations,
|
|
22
22
|
warn
|
|
23
|
-
} from "./cli-
|
|
23
|
+
} from "./cli-najycadg.js";
|
|
24
24
|
import {
|
|
25
25
|
BuildError,
|
|
26
26
|
DEBOUNCE,
|
|
27
27
|
UsageError,
|
|
28
28
|
resolvePort
|
|
29
|
-
} from "./cli-
|
|
29
|
+
} from "./cli-e5zhb0xs.js";
|
|
30
30
|
import {
|
|
31
31
|
gitDirFor,
|
|
32
32
|
gitScopeFor,
|
|
@@ -4566,17 +4566,6 @@ async function assembleBookHtml(opts) {
|
|
|
4566
4566
|
throw new Error("No markdown files to render");
|
|
4567
4567
|
}
|
|
4568
4568
|
const md = createMarkdownRenderer(opts.plugins);
|
|
4569
|
-
let sourceChapter = null;
|
|
4570
|
-
if (opts.wrapChapters) {
|
|
4571
|
-
const renderToken = md.renderer.renderToken.bind(md.renderer);
|
|
4572
|
-
md.renderer.renderToken = (tokens, idx, options) => {
|
|
4573
|
-
const token = tokens[idx];
|
|
4574
|
-
if (sourceChapter !== null && token.level === 0 && token.map !== null && token.type.endsWith("_open")) {
|
|
4575
|
-
token.attrSet("data-chapter-src", sourceChapter);
|
|
4576
|
-
}
|
|
4577
|
-
return renderToken(tokens, idx, options);
|
|
4578
|
-
};
|
|
4579
|
-
}
|
|
4580
4569
|
let bodyContent = "";
|
|
4581
4570
|
const imageRefs = new Set;
|
|
4582
4571
|
for (const file of files) {
|
|
@@ -4589,20 +4578,22 @@ async function assembleBookHtml(opts) {
|
|
|
4589
4578
|
throw new Error(`Failed to read file ${file}: ${errorMsg}`);
|
|
4590
4579
|
}
|
|
4591
4580
|
const env = {};
|
|
4592
|
-
|
|
4593
|
-
let rendered;
|
|
4594
|
-
try {
|
|
4595
|
-
rendered = md.render(content, env);
|
|
4596
|
-
} finally {
|
|
4597
|
-
sourceChapter = null;
|
|
4598
|
-
}
|
|
4581
|
+
const rendered = md.render(content, env);
|
|
4599
4582
|
if (env.layoutWarnings && env.layoutWarnings.length > 0) {
|
|
4600
4583
|
opts.onChapterWarnings?.(chapterId, env.layoutWarnings);
|
|
4601
4584
|
}
|
|
4602
4585
|
for (const ref of env.imageRefs ?? [])
|
|
4603
4586
|
imageRefs.add(ref);
|
|
4604
|
-
|
|
4587
|
+
if (opts.wrapChapters) {
|
|
4588
|
+
const safe = chapterId.replace(/&/g, "&").replace(/"/g, """);
|
|
4589
|
+
bodyContent += `<div class="gutterpress-chapter" data-chapter-src="${safe}">
|
|
4590
|
+
${rendered}
|
|
4591
|
+
</div>
|
|
4592
|
+
`;
|
|
4593
|
+
} else {
|
|
4594
|
+
bodyContent += rendered + `
|
|
4605
4595
|
`;
|
|
4596
|
+
}
|
|
4606
4597
|
}
|
|
4607
4598
|
if (opts.onImageRefs) {
|
|
4608
4599
|
for (const ref of collectHtmlImageRefs(bodyContent))
|
|
@@ -6023,13 +6014,13 @@ var favicon_default = "./favicon-wkbm9cjn.ico";
|
|
|
6023
6014
|
var manifest_schema_default = "./manifest.schema-z61rzw44.json";
|
|
6024
6015
|
|
|
6025
6016
|
// src/assets/preview/scripts/pagedjs-interface.js
|
|
6026
|
-
var pagedjs_interface_default = "./pagedjs-interface-
|
|
6017
|
+
var pagedjs_interface_default = "./pagedjs-interface-qxvzgwd7.js";
|
|
6027
6018
|
|
|
6028
6019
|
// src/assets/preview/scripts/pagedjs-bridge.js
|
|
6029
6020
|
var pagedjs_bridge_default = "./pagedjs-bridge-vn4hk9fx.js";
|
|
6030
6021
|
|
|
6031
6022
|
// src/assets/preview/scripts/preview-shell.js
|
|
6032
|
-
var preview_shell_default = "./preview-shell-
|
|
6023
|
+
var preview_shell_default = "./preview-shell-6dqexx1m.js";
|
|
6033
6024
|
|
|
6034
6025
|
// src/assets/vendor/paged.polyfill.js
|
|
6035
6026
|
var paged_polyfill_default = "./paged.polyfill-n95pbxfn.js";
|
|
@@ -7032,12 +7023,17 @@ async function renderPreviewBook(inputPath, config, opts) {
|
|
|
7032
7023
|
}
|
|
7033
7024
|
});
|
|
7034
7025
|
}
|
|
7035
|
-
function injectPreviewScripts(html) {
|
|
7026
|
+
function injectPreviewScripts(html, pageIsolateChapters) {
|
|
7036
7027
|
const iface = `<script src="/preview/scripts/pagedjs-interface.js"></script>
|
|
7037
7028
|
` + `<script src="/preview/scripts/pagedjs-bridge.js"></script>
|
|
7038
7029
|
`;
|
|
7039
|
-
|
|
7030
|
+
let output = html.replace(pagedjsPolyfillTagRegex(), iface + BREAK_INSIDE_HANDLER + `
|
|
7040
7031
|
<script src="/vendor/paged.polyfill.js"></script>`);
|
|
7032
|
+
if (pageIsolateChapters && /<\/head>/i.test(output)) {
|
|
7033
|
+
output = output.replace(/<\/head>/i, `<style>.gutterpress-chapter{break-before:page}</style>
|
|
7034
|
+
</head>`);
|
|
7035
|
+
}
|
|
7036
|
+
return output;
|
|
7041
7037
|
}
|
|
7042
7038
|
async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
|
|
7043
7039
|
if (!inputPath) {
|
|
@@ -7046,9 +7042,10 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
|
|
|
7046
7042
|
return;
|
|
7047
7043
|
}
|
|
7048
7044
|
const nextAssets = new Map;
|
|
7045
|
+
const incremental = incrementalPreviewEnabled();
|
|
7049
7046
|
const html = await renderPreviewBook(inputPath, config, {
|
|
7050
7047
|
files: config.source?.files ?? null,
|
|
7051
|
-
wrapChapters:
|
|
7048
|
+
wrapChapters: incremental,
|
|
7052
7049
|
onCssAssets: (copies) => {
|
|
7053
7050
|
for (const copy of copies)
|
|
7054
7051
|
nextAssets.set(copy.to, copy.from);
|
|
@@ -7057,7 +7054,39 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
|
|
|
7057
7054
|
cssAssets.clear();
|
|
7058
7055
|
for (const [to, from] of nextAssets)
|
|
7059
7056
|
cssAssets.set(to, from);
|
|
7060
|
-
await fsp4.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html), "utf-8");
|
|
7057
|
+
await fsp4.writeFile(path10.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, incremental), "utf-8");
|
|
7058
|
+
}
|
|
7059
|
+
async function renderChapterPreviewHtml(inputPath, file, config) {
|
|
7060
|
+
const html = await renderPreviewBook(inputPath, config, {
|
|
7061
|
+
files: [canonicalChapterId(file)],
|
|
7062
|
+
wrapChapters: true
|
|
7063
|
+
});
|
|
7064
|
+
return injectPreviewScripts(html, true);
|
|
7065
|
+
}
|
|
7066
|
+
function describeChanges(changes, inputResolved) {
|
|
7067
|
+
const files = [];
|
|
7068
|
+
for (const [changedPath, event] of changes) {
|
|
7069
|
+
const relative2 = path10.relative(inputResolved, path10.resolve(changedPath));
|
|
7070
|
+
if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path10.sep}`) || path10.isAbsolute(relative2))
|
|
7071
|
+
continue;
|
|
7072
|
+
files.push({
|
|
7073
|
+
relativePath: canonicalChapterId(relative2),
|
|
7074
|
+
ext: path10.extname(changedPath).toLowerCase(),
|
|
7075
|
+
event
|
|
7076
|
+
});
|
|
7077
|
+
}
|
|
7078
|
+
return files;
|
|
7079
|
+
}
|
|
7080
|
+
function decideBroadcast(files, changeCount, incremental) {
|
|
7081
|
+
const only = files.length === 1 ? files[0] : null;
|
|
7082
|
+
if (incremental && changeCount === 1 && only?.ext === ".md" && only.event !== "unlink" && only.event !== "unlinkDir") {
|
|
7083
|
+
return {
|
|
7084
|
+
kind: "chapter-splice",
|
|
7085
|
+
chapterId: canonicalChapterId(only.relativePath),
|
|
7086
|
+
relativePath: only.relativePath
|
|
7087
|
+
};
|
|
7088
|
+
}
|
|
7089
|
+
return { kind: "full-reload" };
|
|
7061
7090
|
}
|
|
7062
7091
|
function isDotPathUnderRoot(candidatePath, root) {
|
|
7063
7092
|
const normalizedRoot = root.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
@@ -7176,21 +7205,43 @@ function createFileWatcher(state) {
|
|
|
7176
7205
|
let closed = false;
|
|
7177
7206
|
const pendingChanges = new Map;
|
|
7178
7207
|
const suppressInitialExternalAdds = new Set;
|
|
7208
|
+
const settledWrites = new Map;
|
|
7209
|
+
let immediatePending = false;
|
|
7179
7210
|
function scheduleRebuild() {
|
|
7180
7211
|
if (closed)
|
|
7181
7212
|
return;
|
|
7182
7213
|
if (state.rebuildTimer)
|
|
7183
7214
|
clearTimeout(state.rebuildTimer);
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7215
|
+
const timer = setTimeout(() => {
|
|
7216
|
+
if (state.rebuildTimer === timer)
|
|
7217
|
+
state.rebuildTimer = null;
|
|
7218
|
+
runRebuild();
|
|
7219
|
+
}, DEBOUNCE.FILE_WATCH);
|
|
7220
|
+
state.rebuildTimer = timer;
|
|
7221
|
+
}
|
|
7222
|
+
async function recordChange(event, filePath2) {
|
|
7187
7223
|
if (closed)
|
|
7188
7224
|
return;
|
|
7189
|
-
|
|
7190
|
-
|
|
7225
|
+
const target = path10.resolve(filePath2);
|
|
7226
|
+
const settled = settledWrites.get(target);
|
|
7227
|
+
if (settled && event !== "unlink" && Date.now() <= settled.expiresAt) {
|
|
7228
|
+
const diskContent = await fsp4.readFile(target, "utf8").catch(() => null);
|
|
7229
|
+
if (closed)
|
|
7230
|
+
return;
|
|
7231
|
+
if (settledWrites.get(target) === settled && diskContent === settled.content) {
|
|
7232
|
+
debug(`Suppressed settled-write watcher echo: ${target}`);
|
|
7233
|
+
return;
|
|
7234
|
+
}
|
|
7235
|
+
}
|
|
7236
|
+
if (settledWrites.get(target) === settled)
|
|
7237
|
+
settledWrites.delete(target);
|
|
7238
|
+
debug(`File ${event}: ${target}`);
|
|
7239
|
+
pendingChanges.set(target, event);
|
|
7191
7240
|
scheduleRebuild();
|
|
7192
7241
|
}
|
|
7193
|
-
watcher.on("all",
|
|
7242
|
+
watcher.on("all", (event, filePath2) => {
|
|
7243
|
+
recordChange(event, filePath2);
|
|
7244
|
+
});
|
|
7194
7245
|
externalWatcher.on("all", (event, filePath2) => {
|
|
7195
7246
|
const target = path10.resolve(filePath2);
|
|
7196
7247
|
if (!desiredExternals.has(target))
|
|
@@ -7199,6 +7250,43 @@ function createFileWatcher(state) {
|
|
|
7199
7250
|
return;
|
|
7200
7251
|
recordChange(event, target);
|
|
7201
7252
|
});
|
|
7253
|
+
function isWatchedSource(target) {
|
|
7254
|
+
const rel = path10.relative(inputResolved, target);
|
|
7255
|
+
const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path10.sep}`) && !path10.isAbsolute(rel);
|
|
7256
|
+
if (inProject)
|
|
7257
|
+
return !isIgnoredWatchPath(target, inputResolved);
|
|
7258
|
+
return desiredExternals.has(target);
|
|
7259
|
+
}
|
|
7260
|
+
function acceptSettledWrite(target, writtenContent) {
|
|
7261
|
+
const settled = { content: writtenContent, expiresAt: Date.now() + 2000 };
|
|
7262
|
+
settledWrites.set(target, settled);
|
|
7263
|
+
const expiry = setTimeout(() => {
|
|
7264
|
+
if (settledWrites.get(target) === settled)
|
|
7265
|
+
settledWrites.delete(target);
|
|
7266
|
+
}, 2000);
|
|
7267
|
+
expiry.unref?.();
|
|
7268
|
+
pendingChanges.set(target, "change");
|
|
7269
|
+
immediatePending = true;
|
|
7270
|
+
if (state.rebuildTimer) {
|
|
7271
|
+
clearTimeout(state.rebuildTimer);
|
|
7272
|
+
state.rebuildTimer = null;
|
|
7273
|
+
}
|
|
7274
|
+
if (!state.isRebuilding)
|
|
7275
|
+
runRebuild();
|
|
7276
|
+
}
|
|
7277
|
+
state.notifySettledWrite = (filePath2, writtenContent) => {
|
|
7278
|
+
if (closed)
|
|
7279
|
+
return;
|
|
7280
|
+
const target = path10.resolve(filePath2);
|
|
7281
|
+
if (isWatchedSource(target)) {
|
|
7282
|
+
acceptSettledWrite(target, writtenContent);
|
|
7283
|
+
return;
|
|
7284
|
+
}
|
|
7285
|
+
syncQueue.then(() => {
|
|
7286
|
+
if (!closed && isWatchedSource(target))
|
|
7287
|
+
acceptSettledWrite(target, writtenContent);
|
|
7288
|
+
});
|
|
7289
|
+
};
|
|
7202
7290
|
let watchedExternalRoots = new Set;
|
|
7203
7291
|
let syncQueue = Promise.resolve();
|
|
7204
7292
|
function syncExternalWatches() {
|
|
@@ -7250,10 +7338,15 @@ function createFileWatcher(state) {
|
|
|
7250
7338
|
async function runRebuild() {
|
|
7251
7339
|
if (closed || state.isRebuilding)
|
|
7252
7340
|
return;
|
|
7341
|
+
if (state.rebuildTimer) {
|
|
7342
|
+
clearTimeout(state.rebuildTimer);
|
|
7343
|
+
state.rebuildTimer = null;
|
|
7344
|
+
}
|
|
7253
7345
|
const changes = [...pendingChanges.entries()];
|
|
7254
7346
|
pendingChanges.clear();
|
|
7255
7347
|
if (changes.length === 0)
|
|
7256
7348
|
return;
|
|
7349
|
+
immediatePending = false;
|
|
7257
7350
|
state.isRebuilding = true;
|
|
7258
7351
|
let resolveInFlight;
|
|
7259
7352
|
const inFlight = new Promise((resolve9) => {
|
|
@@ -7273,8 +7366,14 @@ function createFileWatcher(state) {
|
|
|
7273
7366
|
await generateAndWriteHtml(inputResolved, state.tempDir, updatedConfig, state.cssAssets);
|
|
7274
7367
|
if (closed)
|
|
7275
7368
|
return;
|
|
7276
|
-
|
|
7277
|
-
|
|
7369
|
+
const decision = decideBroadcast(describeChanges(changes, inputResolved), changes.length, incrementalPreviewEnabled());
|
|
7370
|
+
if (decision.kind === "chapter-splice") {
|
|
7371
|
+
state.previewServer?.broadcastContentUpdate(decision.chapterId);
|
|
7372
|
+
info(`Chapter updated: ${decision.relativePath}`);
|
|
7373
|
+
} else {
|
|
7374
|
+
state.previewServer?.broadcastReload();
|
|
7375
|
+
info(changes.length > 1 ? `Preview updated (${changes.length} files changed — full reload)` : "Preview updated");
|
|
7376
|
+
}
|
|
7278
7377
|
} catch (err) {
|
|
7279
7378
|
error("Failed to regenerate preview:", err);
|
|
7280
7379
|
} finally {
|
|
@@ -7282,13 +7381,19 @@ function createFileWatcher(state) {
|
|
|
7282
7381
|
resolveInFlight();
|
|
7283
7382
|
if (state.rebuildPromise === inFlight)
|
|
7284
7383
|
state.rebuildPromise = null;
|
|
7285
|
-
if (!closed && pendingChanges.size > 0)
|
|
7286
|
-
|
|
7384
|
+
if (!closed && pendingChanges.size > 0) {
|
|
7385
|
+
if (immediatePending)
|
|
7386
|
+
runRebuild();
|
|
7387
|
+
else
|
|
7388
|
+
scheduleRebuild();
|
|
7389
|
+
}
|
|
7287
7390
|
}
|
|
7288
7391
|
}
|
|
7289
7392
|
const closeBookWatcher = watcher.close.bind(watcher);
|
|
7290
7393
|
watcher.close = async () => {
|
|
7291
7394
|
closed = true;
|
|
7395
|
+
state.notifySettledWrite = null;
|
|
7396
|
+
settledWrites.clear();
|
|
7292
7397
|
await syncQueue.catch(() => {});
|
|
7293
7398
|
await state.rebuildPromise?.catch(() => {});
|
|
7294
7399
|
await externalWatcher.close();
|
|
@@ -7441,6 +7546,7 @@ function createServerState(inputPath, tempDir, config, options) {
|
|
|
7441
7546
|
rebuildTimer: null,
|
|
7442
7547
|
isRebuilding: false,
|
|
7443
7548
|
rebuildPromise: null,
|
|
7549
|
+
notifySettledWrite: null,
|
|
7444
7550
|
previewServer: null,
|
|
7445
7551
|
isShuttingDown: false,
|
|
7446
7552
|
tempDir,
|
|
@@ -7453,11 +7559,13 @@ function createServerState(inputPath, tempDir, config, options) {
|
|
|
7453
7559
|
// src/preview/http-server.ts
|
|
7454
7560
|
import http2 from "node:http";
|
|
7455
7561
|
import net from "node:net";
|
|
7562
|
+
import { randomUUID } from "node:crypto";
|
|
7456
7563
|
import { readFile as readFile20, stat as stat4 } from "node:fs/promises";
|
|
7457
7564
|
import path12 from "path";
|
|
7458
7565
|
import { WebSocket, WebSocketServer } from "ws";
|
|
7459
7566
|
var HMR_PATH = "/__gutterpress-hmr";
|
|
7460
|
-
|
|
7567
|
+
function hmrClientSnippet(initialRevision, instanceId) {
|
|
7568
|
+
return `
|
|
7461
7569
|
<script>
|
|
7462
7570
|
(function () {
|
|
7463
7571
|
// When loaded by the preview SHELL (iframe double-buffer), the shell loads us
|
|
@@ -7467,6 +7575,10 @@ var HMR_CLIENT_SNIPPET = `
|
|
|
7467
7575
|
// for scroll-anchor and reload.
|
|
7468
7576
|
if (/[?&]gutterpressshell=1/.test(location.search)) return;
|
|
7469
7577
|
var ANCHOR_KEY = 'gutterpress-scroll-anchor';
|
|
7578
|
+
var appliedRevision = ${initialRevision};
|
|
7579
|
+
var appliedInstance = ${JSON.stringify(instanceId)};
|
|
7580
|
+
var readyToAcknowledge = false;
|
|
7581
|
+
var reloadRequested = false;
|
|
7470
7582
|
|
|
7471
7583
|
// Find the element nearest the top of the viewport that carries a source
|
|
7472
7584
|
// line (markdown-it-source-map emits data-source-line on block elements).
|
|
@@ -7517,7 +7629,13 @@ var HMR_CLIENT_SNIPPET = `
|
|
|
7517
7629
|
}
|
|
7518
7630
|
}
|
|
7519
7631
|
if (el) {
|
|
7520
|
-
window.scrollBy(
|
|
7632
|
+
window.scrollBy({
|
|
7633
|
+
top: el.getBoundingClientRect().top - a.offset,
|
|
7634
|
+
behavior: 'instant'
|
|
7635
|
+
});
|
|
7636
|
+
if (window.previewAPI && typeof window.previewAPI.refresh === 'function') {
|
|
7637
|
+
window.previewAPI.refresh();
|
|
7638
|
+
}
|
|
7521
7639
|
return;
|
|
7522
7640
|
}
|
|
7523
7641
|
if (tries++ < 240) setTimeout(attempt, 25); // wait for pagination
|
|
@@ -7525,44 +7643,100 @@ var HMR_CLIENT_SNIPPET = `
|
|
|
7525
7643
|
}
|
|
7526
7644
|
var restored = false;
|
|
7527
7645
|
function restoreOnce() { if (restored) return; restored = true; restoreAnchor(); }
|
|
7646
|
+
function finishInitialRender() {
|
|
7647
|
+
restoreOnce();
|
|
7648
|
+
readyToAcknowledge = true;
|
|
7649
|
+
acknowledge();
|
|
7650
|
+
}
|
|
7528
7651
|
// CRITICAL ordering: when the Paged.js polyfill is present it re-paginates on
|
|
7529
|
-
// load and its PagedConfig.after
|
|
7652
|
+
// load and its PagedConfig.after scrolls to the document start THEN fires
|
|
7530
7653
|
// 'renderingComplete'. So in engine mode we MUST wait for that event —
|
|
7531
|
-
// restoring earlier would be wiped by
|
|
7654
|
+
// restoring earlier would be wiped by that initial scroll. In static mode (no
|
|
7532
7655
|
// engine) the content is final immediately, so restore right after load.
|
|
7533
|
-
var hasEngine = !!document.querySelector('script[src*="paged.polyfill"]
|
|
7534
|
-
window.addEventListener('renderingComplete',
|
|
7656
|
+
var hasEngine = !!document.querySelector('script[src*="paged.polyfill"]');
|
|
7657
|
+
window.addEventListener('renderingComplete', finishInitialRender);
|
|
7535
7658
|
if (!hasEngine) {
|
|
7536
7659
|
if (document.readyState === 'loading') {
|
|
7537
|
-
document.addEventListener('DOMContentLoaded', function () { setTimeout(
|
|
7660
|
+
document.addEventListener('DOMContentLoaded', function () { setTimeout(finishInitialRender, 50); });
|
|
7538
7661
|
} else {
|
|
7539
|
-
setTimeout(
|
|
7662
|
+
setTimeout(finishInitialRender, 50);
|
|
7540
7663
|
}
|
|
7541
7664
|
}
|
|
7542
|
-
|
|
7665
|
+
var ws = null;
|
|
7666
|
+
var reconnectTimer = null;
|
|
7667
|
+
var reconnectDelay = 250;
|
|
7668
|
+
var stopped = false;
|
|
7543
7669
|
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7670
|
+
function acknowledge() {
|
|
7671
|
+
if (!readyToAcknowledge || !ws || ws.readyState !== 1) return;
|
|
7672
|
+
try {
|
|
7673
|
+
ws.send(JSON.stringify({
|
|
7674
|
+
type: 'reload-applied',
|
|
7675
|
+
instance: appliedInstance,
|
|
7676
|
+
revision: appliedRevision
|
|
7677
|
+
}));
|
|
7678
|
+
} catch (_) {}
|
|
7679
|
+
}
|
|
7680
|
+
|
|
7681
|
+
function connect() {
|
|
7682
|
+
if (stopped) return;
|
|
7683
|
+
ws = new WebSocket(location.origin.replace(/^http/, 'ws') + '${HMR_PATH}');
|
|
7684
|
+
ws.onopen = function () {
|
|
7685
|
+
reconnectDelay = 250;
|
|
7686
|
+
acknowledge();
|
|
7687
|
+
};
|
|
7688
|
+
ws.onmessage = function (e) {
|
|
7689
|
+
var msg;
|
|
7690
|
+
try { msg = JSON.parse(e.data); } catch (_) { return; }
|
|
7691
|
+
if (
|
|
7692
|
+
msg.type !== 'reload-state' &&
|
|
7693
|
+
msg.type !== 'full-reload' &&
|
|
7694
|
+
msg.type !== 'content-update'
|
|
7695
|
+
) return;
|
|
7696
|
+
var instance = typeof msg.instance === 'string' ? msg.instance : null;
|
|
7697
|
+
var revision = Number(msg.revision);
|
|
7698
|
+
if (!instance || !Number.isSafeInteger(revision) || revision < 0) return;
|
|
7699
|
+
if (instance === appliedInstance && revision <= appliedRevision) {
|
|
7700
|
+
acknowledge();
|
|
7701
|
+
return;
|
|
7702
|
+
}
|
|
7703
|
+
if (reloadRequested) return;
|
|
7704
|
+
reloadRequested = true;
|
|
7549
7705
|
try { var a = captureAnchor(); if (a) sessionStorage.setItem(ANCHOR_KEY, JSON.stringify(a)); } catch (_) {}
|
|
7550
7706
|
location.reload();
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7707
|
+
};
|
|
7708
|
+
ws.onclose = function () {
|
|
7709
|
+
ws = null;
|
|
7710
|
+
if (stopped || reconnectTimer !== null) return;
|
|
7711
|
+
reconnectTimer = setTimeout(function () {
|
|
7712
|
+
reconnectTimer = null;
|
|
7713
|
+
connect();
|
|
7714
|
+
}, reconnectDelay);
|
|
7715
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 5000);
|
|
7716
|
+
};
|
|
7717
|
+
ws.onerror = function () { try { ws.close(); } catch (_) {} };
|
|
7718
|
+
}
|
|
7719
|
+
|
|
7720
|
+
window.addEventListener('beforeunload', function () {
|
|
7721
|
+
stopped = true;
|
|
7722
|
+
if (reconnectTimer !== null) clearTimeout(reconnectTimer);
|
|
7723
|
+
});
|
|
7724
|
+
connect();
|
|
7554
7725
|
})();
|
|
7555
7726
|
</script>
|
|
7556
7727
|
`;
|
|
7557
|
-
|
|
7728
|
+
}
|
|
7729
|
+
function shellHtml(initialRevision, instanceId) {
|
|
7730
|
+
return `<!doctype html>
|
|
7558
7731
|
<html lang="en"><head><meta charset="utf-8"><title>gutterpress preview</title>
|
|
7559
7732
|
<style>html,body{margin:0;height:100%;background:#fff;overflow:hidden}
|
|
7560
7733
|
iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block}</style>
|
|
7561
7734
|
</head><body>
|
|
7562
7735
|
<iframe id="gutterpress-active" src="/book.html?gutterpressshell=1" title="preview"></iframe>
|
|
7563
|
-
<script>window.__GUTTERPRESS_HMR=${JSON.stringify(HMR_PATH)};</script>
|
|
7736
|
+
<script>window.__GUTTERPRESS_HMR=${JSON.stringify(HMR_PATH)};window.__GUTTERPRESS_INSTANCE=${JSON.stringify(instanceId)};window.__GUTTERPRESS_REVISION=${initialRevision};</script>
|
|
7564
7737
|
<script src="/preview/scripts/preview-shell.js"></script>
|
|
7565
7738
|
</body></html>`;
|
|
7739
|
+
}
|
|
7566
7740
|
function probePort(port, host) {
|
|
7567
7741
|
return new Promise((resolve9) => {
|
|
7568
7742
|
const srv = net.createServer();
|
|
@@ -7604,13 +7778,14 @@ async function findAvailablePort(startPort, host = "127.0.0.1") {
|
|
|
7604
7778
|
}
|
|
7605
7779
|
throw new BuildError(`Could not find an available port on ${host} from ${firstPort} to ${port - 1}; all are already in use. ` + "Stop another preview server or pass --port with an available port.", undefined, { cause: lastBusyError });
|
|
7606
7780
|
}
|
|
7607
|
-
function injectHmrClient(html) {
|
|
7781
|
+
function injectHmrClient(html, revision, instanceId) {
|
|
7782
|
+
const snippet = hmrClientSnippet(revision, instanceId);
|
|
7608
7783
|
const closingBody = html.lastIndexOf("</body>");
|
|
7609
7784
|
if (closingBody === -1)
|
|
7610
|
-
return html +
|
|
7611
|
-
return html.slice(0, closingBody) +
|
|
7785
|
+
return html + snippet;
|
|
7786
|
+
return html.slice(0, closingBody) + snippet + html.slice(closingBody);
|
|
7612
7787
|
}
|
|
7613
|
-
async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders = {}) {
|
|
7788
|
+
async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders = {}, hmrRevision = 0, hmrInstance = "") {
|
|
7614
7789
|
let filePath2 = absPath;
|
|
7615
7790
|
try {
|
|
7616
7791
|
const s = await stat4(filePath2);
|
|
@@ -7631,7 +7806,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
|
|
|
7631
7806
|
}
|
|
7632
7807
|
const isHtml = filePath2.endsWith(".html") || filePath2.endsWith(".htm");
|
|
7633
7808
|
if (isHtml) {
|
|
7634
|
-
const withHmr = injectHmrClient(data.toString("utf-8"));
|
|
7809
|
+
const withHmr = injectHmrClient(data.toString("utf-8"), hmrRevision, hmrInstance);
|
|
7635
7810
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
7636
7811
|
res.end(withHmr);
|
|
7637
7812
|
return;
|
|
@@ -7650,11 +7825,88 @@ function matchesEmbedded(urlPathname) {
|
|
|
7650
7825
|
return EMBEDDED_PREFIXES.some((p) => urlPathname.startsWith(p));
|
|
7651
7826
|
}
|
|
7652
7827
|
async function createPreviewServer(state, port) {
|
|
7653
|
-
const
|
|
7828
|
+
const ACK_RETRY_MS = 2000;
|
|
7829
|
+
const MAX_ACK_RETRIES = 95;
|
|
7830
|
+
const instanceId = randomUUID();
|
|
7831
|
+
let reloadRevision = 0;
|
|
7832
|
+
const clients = new Map;
|
|
7833
|
+
function sendRevision(ws, type, file) {
|
|
7834
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
7835
|
+
return;
|
|
7836
|
+
try {
|
|
7837
|
+
ws.send(JSON.stringify({
|
|
7838
|
+
type,
|
|
7839
|
+
instance: instanceId,
|
|
7840
|
+
revision: reloadRevision,
|
|
7841
|
+
...file ? { file } : {}
|
|
7842
|
+
}));
|
|
7843
|
+
} catch {}
|
|
7844
|
+
}
|
|
7845
|
+
function waitForAcknowledgement(ws) {
|
|
7846
|
+
const client = clients.get(ws);
|
|
7847
|
+
if (!client)
|
|
7848
|
+
return;
|
|
7849
|
+
if (client.acknowledgedRevision >= reloadRevision) {
|
|
7850
|
+
if (client.retryTimer !== null)
|
|
7851
|
+
clearTimeout(client.retryTimer);
|
|
7852
|
+
client.retryTimer = null;
|
|
7853
|
+
client.retryAttempts = 0;
|
|
7854
|
+
return;
|
|
7855
|
+
}
|
|
7856
|
+
if (client.retryTimer !== null)
|
|
7857
|
+
return;
|
|
7858
|
+
client.retryTimer = setTimeout(() => {
|
|
7859
|
+
const current = clients.get(ws);
|
|
7860
|
+
if (!current)
|
|
7861
|
+
return;
|
|
7862
|
+
current.retryTimer = null;
|
|
7863
|
+
if (current.retryAttempts >= MAX_ACK_RETRIES) {
|
|
7864
|
+
ws.terminate();
|
|
7865
|
+
return;
|
|
7866
|
+
}
|
|
7867
|
+
current.retryAttempts++;
|
|
7868
|
+
sendRevision(ws, "reload-state");
|
|
7869
|
+
waitForAcknowledgement(ws);
|
|
7870
|
+
}, ACK_RETRY_MS);
|
|
7871
|
+
client.retryTimer.unref?.();
|
|
7872
|
+
}
|
|
7873
|
+
function removeClient(ws) {
|
|
7874
|
+
const client = clients.get(ws);
|
|
7875
|
+
if (client && client.retryTimer !== null)
|
|
7876
|
+
clearTimeout(client.retryTimer);
|
|
7877
|
+
clients.delete(ws);
|
|
7878
|
+
}
|
|
7654
7879
|
const wss = new WebSocketServer({ noServer: true });
|
|
7655
7880
|
wss.on("connection", (ws) => {
|
|
7656
|
-
clients.
|
|
7657
|
-
ws.on("
|
|
7881
|
+
clients.set(ws, { acknowledgedRevision: -1, retryAttempts: 0, retryTimer: null });
|
|
7882
|
+
ws.on("message", (raw) => {
|
|
7883
|
+
let message;
|
|
7884
|
+
try {
|
|
7885
|
+
message = JSON.parse(raw.toString());
|
|
7886
|
+
} catch {
|
|
7887
|
+
return;
|
|
7888
|
+
}
|
|
7889
|
+
if (!message || typeof message !== "object")
|
|
7890
|
+
return;
|
|
7891
|
+
const payload = message;
|
|
7892
|
+
if (payload.type !== "reload-applied")
|
|
7893
|
+
return;
|
|
7894
|
+
const revision = Number(payload.revision);
|
|
7895
|
+
if (payload.instance !== instanceId || !Number.isSafeInteger(revision) || revision < 0 || revision > reloadRevision)
|
|
7896
|
+
return;
|
|
7897
|
+
const client = clients.get(ws);
|
|
7898
|
+
if (!client)
|
|
7899
|
+
return;
|
|
7900
|
+
client.acknowledgedRevision = Math.max(client.acknowledgedRevision, revision);
|
|
7901
|
+
waitForAcknowledgement(ws);
|
|
7902
|
+
});
|
|
7903
|
+
ws.on("close", () => removeClient(ws));
|
|
7904
|
+
ws.on("error", () => {
|
|
7905
|
+
removeClient(ws);
|
|
7906
|
+
ws.terminate();
|
|
7907
|
+
});
|
|
7908
|
+
sendRevision(ws, "reload-state");
|
|
7909
|
+
waitForAcknowledgement(ws);
|
|
7658
7910
|
});
|
|
7659
7911
|
const server = http2.createServer(async (req, res) => {
|
|
7660
7912
|
const url = new URL(req.url, "http://127.0.0.1");
|
|
@@ -7677,6 +7929,34 @@ async function createPreviewServer(state, port) {
|
|
|
7677
7929
|
res.end("Not Found");
|
|
7678
7930
|
return;
|
|
7679
7931
|
}
|
|
7932
|
+
if (url.pathname === "/__chapter" && req.method === "GET") {
|
|
7933
|
+
const rawFile = url.searchParams.get("file");
|
|
7934
|
+
const file = rawFile ? canonicalChapterId(rawFile) : "";
|
|
7935
|
+
const revision = Number(url.searchParams.get("revision"));
|
|
7936
|
+
const configuredFiles = state.config.source?.files;
|
|
7937
|
+
if (!file || !state.currentInputPath || !incrementalPreviewEnabled() || !Number.isSafeInteger(revision) || revision !== reloadRevision || path12.extname(file).toLowerCase() !== ".md" || hasDotSegment(file) || !resolveWithinRoot(file, state.currentInputPath) || Array.isArray(configuredFiles) && configuredFiles.length > 0 && !configuredFiles.some((configured) => canonicalChapterId(configured) === file)) {
|
|
7938
|
+
res.writeHead(400);
|
|
7939
|
+
res.end("Bad Request");
|
|
7940
|
+
return;
|
|
7941
|
+
}
|
|
7942
|
+
try {
|
|
7943
|
+
const html = await renderChapterPreviewHtml(state.currentInputPath, file, state.config);
|
|
7944
|
+
if (revision !== reloadRevision) {
|
|
7945
|
+
res.writeHead(409);
|
|
7946
|
+
res.end("Superseded");
|
|
7947
|
+
return;
|
|
7948
|
+
}
|
|
7949
|
+
res.writeHead(200, {
|
|
7950
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7951
|
+
"Cache-Control": "no-store"
|
|
7952
|
+
});
|
|
7953
|
+
res.end(html);
|
|
7954
|
+
} catch (error2) {
|
|
7955
|
+
res.writeHead(500);
|
|
7956
|
+
res.end(error2 instanceof Error ? error2.message : String(error2));
|
|
7957
|
+
}
|
|
7958
|
+
return;
|
|
7959
|
+
}
|
|
7680
7960
|
if (matchesEmbedded(url.pathname)) {
|
|
7681
7961
|
if (req.headers["if-none-match"] === EMBEDDED_ETAG) {
|
|
7682
7962
|
res.writeHead(304, { ETag: EMBEDDED_ETAG, "Cache-Control": EMBEDDED_CACHE_CONTROL });
|
|
@@ -7695,7 +7975,7 @@ async function createPreviewServer(state, port) {
|
|
|
7695
7975
|
}
|
|
7696
7976
|
if (url.pathname === "/" && incrementalPreviewEnabled()) {
|
|
7697
7977
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
7698
|
-
res.end(
|
|
7978
|
+
res.end(shellHtml(reloadRevision, instanceId));
|
|
7699
7979
|
return;
|
|
7700
7980
|
}
|
|
7701
7981
|
const pathname = url.pathname === "/" ? "/book.html" : url.pathname;
|
|
@@ -7722,7 +8002,7 @@ async function createPreviewServer(state, port) {
|
|
|
7722
8002
|
res.end("Not Found");
|
|
7723
8003
|
return;
|
|
7724
8004
|
}
|
|
7725
|
-
await serveStatic(absPath, res).catch((err) => {
|
|
8005
|
+
await serveStatic(absPath, res, "no-store", {}, reloadRevision, instanceId).catch((err) => {
|
|
7726
8006
|
if (!res.headersSent) {
|
|
7727
8007
|
res.writeHead(500);
|
|
7728
8008
|
res.end(`Internal Server Error: ${err.message}`);
|
|
@@ -7749,14 +8029,21 @@ async function createPreviewServer(state, port) {
|
|
|
7749
8029
|
openPath(serverUrl).catch(() => {});
|
|
7750
8030
|
}
|
|
7751
8031
|
let stopped = false;
|
|
7752
|
-
const
|
|
8032
|
+
const broadcastUpdate = (type, file) => {
|
|
7753
8033
|
if (stopped)
|
|
7754
8034
|
return;
|
|
7755
|
-
|
|
7756
|
-
for (const ws of clients) {
|
|
7757
|
-
|
|
7758
|
-
|
|
8035
|
+
reloadRevision++;
|
|
8036
|
+
for (const [ws] of clients) {
|
|
8037
|
+
sendRevision(ws, type, file);
|
|
8038
|
+
waitForAcknowledgement(ws);
|
|
8039
|
+
}
|
|
8040
|
+
};
|
|
8041
|
+
const updateInFlight = () => {
|
|
8042
|
+
for (const client of clients.values()) {
|
|
8043
|
+
if (client.acknowledgedRevision < reloadRevision)
|
|
8044
|
+
return true;
|
|
7759
8045
|
}
|
|
8046
|
+
return false;
|
|
7760
8047
|
};
|
|
7761
8048
|
return {
|
|
7762
8049
|
port: boundPort,
|
|
@@ -7764,8 +8051,11 @@ async function createPreviewServer(state, port) {
|
|
|
7764
8051
|
if (stopped)
|
|
7765
8052
|
return;
|
|
7766
8053
|
stopped = true;
|
|
7767
|
-
for (const ws of clients)
|
|
8054
|
+
for (const [ws, client] of clients) {
|
|
8055
|
+
if (client.retryTimer !== null)
|
|
8056
|
+
clearTimeout(client.retryTimer);
|
|
7768
8057
|
ws.terminate();
|
|
8058
|
+
}
|
|
7769
8059
|
clients.clear();
|
|
7770
8060
|
wss.close();
|
|
7771
8061
|
await new Promise((resolve9) => {
|
|
@@ -7776,7 +8066,13 @@ async function createPreviewServer(state, port) {
|
|
|
7776
8066
|
});
|
|
7777
8067
|
},
|
|
7778
8068
|
broadcastReload() {
|
|
7779
|
-
|
|
8069
|
+
broadcastUpdate("full-reload");
|
|
8070
|
+
},
|
|
8071
|
+
broadcastContentUpdate(file) {
|
|
8072
|
+
if (updateInFlight())
|
|
8073
|
+
broadcastUpdate("full-reload");
|
|
8074
|
+
else
|
|
8075
|
+
broadcastUpdate("content-update", canonicalChapterId(file));
|
|
7780
8076
|
}
|
|
7781
8077
|
};
|
|
7782
8078
|
}
|
|
@@ -7829,7 +8125,10 @@ async function startPreviewServer(options) {
|
|
|
7829
8125
|
}
|
|
7830
8126
|
await shutdownServer(state);
|
|
7831
8127
|
},
|
|
7832
|
-
restart: restartPreview2
|
|
8128
|
+
restart: restartPreview2,
|
|
8129
|
+
notifySettledWrite: (filePath2, writtenContent) => {
|
|
8130
|
+
state.notifySettledWrite?.(filePath2, writtenContent);
|
|
8131
|
+
}
|
|
7833
8132
|
};
|
|
7834
8133
|
}
|
|
7835
8134
|
|
|
@@ -7840,7 +8139,7 @@ import path14 from "node:path";
|
|
|
7840
8139
|
|
|
7841
8140
|
// src/lib/manifest-doc.ts
|
|
7842
8141
|
import { existsSync as existsSync10 } from "node:fs";
|
|
7843
|
-
import { randomUUID } from "node:crypto";
|
|
8142
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7844
8143
|
import { mkdir as mkdir6, open, readFile as readFile21, rename, rm as rm2 } from "node:fs/promises";
|
|
7845
8144
|
import path13 from "node:path";
|
|
7846
8145
|
import { parseDocument, isSeq, YAMLSeq } from "yaml";
|
|
@@ -7862,7 +8161,7 @@ async function loadManifestDoc(projectDir) {
|
|
|
7862
8161
|
async function writeManifestDoc(file, doc) {
|
|
7863
8162
|
const dir = path13.dirname(file);
|
|
7864
8163
|
await mkdir6(dir, { recursive: true });
|
|
7865
|
-
const temporary = path13.join(dir, `.${path13.basename(file)}.tmp-${process.pid}-${
|
|
8164
|
+
const temporary = path13.join(dir, `.${path13.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
|
|
7866
8165
|
try {
|
|
7867
8166
|
const handle = await open(temporary, "wx", 438);
|
|
7868
8167
|
try {
|
|
@@ -8136,7 +8435,7 @@ import path16 from "node:path";
|
|
|
8136
8435
|
import { isSeq as isSeq3, isMap as isMap2, isScalar as isScalar2, YAMLMap, Scalar } from "yaml";
|
|
8137
8436
|
|
|
8138
8437
|
// src/lib/npm-plugin-installer.ts
|
|
8139
|
-
import { createHash as createHash4, randomUUID as
|
|
8438
|
+
import { createHash as createHash4, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
8140
8439
|
import { createReadStream as createReadStream2 } from "node:fs";
|
|
8141
8440
|
import {
|
|
8142
8441
|
lstat as lstat2,
|
|
@@ -8812,7 +9111,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
|
|
|
8812
9111
|
}
|
|
8813
9112
|
try {
|
|
8814
9113
|
await lstat2(finalRoot);
|
|
8815
|
-
backupRoot = `${finalRoot}.backup-${
|
|
9114
|
+
backupRoot = `${finalRoot}.backup-${randomUUID3()}`;
|
|
8816
9115
|
await rename2(finalRoot, backupRoot);
|
|
8817
9116
|
} catch (error2) {
|
|
8818
9117
|
if (error2.code !== "ENOENT")
|