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.
Files changed (30) hide show
  1. package/dist/api/index.js +1 -1
  2. package/dist/{audit-k16v3n98.js → audit-nhn2pjz3.js} +3 -3
  3. package/dist/{build-8tqr4gbb.js → build-san7fv2z.js} +3 -3
  4. package/dist/{cli-rkqxbnkq.js → cli-e5zhb0xs.js} +1 -1
  5. package/dist/{cli-yp0d2hp8.js → cli-k4bd06sd.js} +377 -78
  6. package/dist/{cli-v2dze8ja.js → cli-najycadg.js} +1 -1
  7. package/dist/cli.js +15 -15
  8. package/dist/{doctor-57czxy3q.js → doctor-hrk0kxxz.js} +2 -2
  9. package/dist/{index-m3pa4wyf.js → index-bynn850m.js} +379 -80
  10. package/dist/index.js +1 -1
  11. package/dist/lib/markdown/assemble.d.ts +4 -6
  12. package/dist/lib/markdown/chapter-id.d.ts +1 -1
  13. package/dist/lib/markdown/index.d.ts +1 -1
  14. package/dist/{lint-4sq0dvf6.js → lint-96j9hrj4.js} +3 -3
  15. package/dist/{new-f2nmyr5b.js → new-8p38wavc.js} +3 -3
  16. package/dist/{pagedjs-interface-80g3xa8p.js → pagedjs-interface-qxvzgwd7.js} +92 -33
  17. package/dist/{plugin-xskq3dtx.js → plugin-ees6nhkc.js} +3 -3
  18. package/dist/{preflight-epcffags.js → preflight-1q6c2edh.js} +3 -3
  19. package/dist/preview/file-watcher.d.ts +39 -17
  20. package/dist/preview/http-server.d.ts +5 -2
  21. package/dist/preview/server-context.d.ts +2 -0
  22. package/dist/preview-shell-6dqexx1m.js +581 -0
  23. package/dist/{preview-xf5dktz2.js → preview-y5a2zen1.js} +3 -3
  24. package/dist/{publish-97vdwq31.js → publish-rm9yb3wh.js} +3 -3
  25. package/dist/render.js +10 -19
  26. package/dist/{repair-cz4z4hkg.js → repair-zgq7q2g6.js} +3 -3
  27. package/dist/server.d.ts +2 -0
  28. package/dist/{validate-swzejnd8.js → validate-nr0xa6sa.js} +3 -3
  29. package/package.json +1 -1
  30. 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-v2dze8ja.js";
23
+ } from "./cli-najycadg.js";
24
24
  import {
25
25
  BuildError,
26
26
  DEBOUNCE,
27
27
  UsageError,
28
28
  resolvePort
29
- } from "./cli-rkqxbnkq.js";
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
- sourceChapter = opts.wrapChapters ? chapterId : null;
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
- bodyContent += rendered + `
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-80g3xa8p.js";
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-rghtfmc8.js";
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
- return html.replace(pagedjsPolyfillTagRegex(), iface + BREAK_INSIDE_HANDLER + `
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: true,
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
- state.rebuildTimer = setTimeout(runRebuild, DEBOUNCE.FILE_WATCH);
7185
- }
7186
- function recordChange(event, filePath2) {
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
- debug(`File ${event}: ${filePath2}`);
7190
- pendingChanges.set(path10.resolve(filePath2), event);
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", recordChange);
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
- state.previewServer?.broadcastReload();
7277
- info(changes.length > 1 ? `Preview updated (${changes.length} files changed — full reload)` : "Preview updated");
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
- scheduleRebuild();
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
- var HMR_CLIENT_SNIPPET = `
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(0, el.getBoundingClientRect().top - a.offset);
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 calls scrollTo(0,0) THEN fires
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 the scrollTo(0,0). In static mode (no
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"], script[src*="pagedjs"]');
7534
- window.addEventListener('renderingComplete', restoreOnce);
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(restoreOnce, 50); });
7660
+ document.addEventListener('DOMContentLoaded', function () { setTimeout(finishInitialRender, 50); });
7538
7661
  } else {
7539
- setTimeout(restoreOnce, 50);
7662
+ setTimeout(finishInitialRender, 50);
7540
7663
  }
7541
7664
  }
7542
- setTimeout(restoreOnce, 10000); // safety net if renderingComplete never fires
7665
+ var ws = null;
7666
+ var reconnectTimer = null;
7667
+ var reconnectDelay = 250;
7668
+ var stopped = false;
7543
7669
 
7544
- var ws = new WebSocket(location.origin.replace(/^http/, 'ws') + '${HMR_PATH}');
7545
- ws.onmessage = function (e) {
7546
- var msg;
7547
- try { msg = JSON.parse(e.data); } catch (_) { return; }
7548
- if (msg.type === 'full-reload') {
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
- return;
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
- var SHELL_HTML = `<!doctype html>
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 + HMR_CLIENT_SNIPPET;
7611
- return html.slice(0, closingBody) + HMR_CLIENT_SNIPPET + html.slice(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 clients = new Set;
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.add(ws);
7657
- ws.on("close", () => clients.delete(ws));
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(SHELL_HTML);
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 broadcast = (payload) => {
8032
+ const broadcastUpdate = (type, file) => {
7753
8033
  if (stopped)
7754
8034
  return;
7755
- const message = JSON.stringify(payload);
7756
- for (const ws of clients) {
7757
- if (ws.readyState === WebSocket.OPEN)
7758
- ws.send(message);
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
- broadcast({ type: "full-reload" });
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}-${randomUUID()}`);
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 randomUUID2, timingSafeEqual } from "node:crypto";
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-${randomUUID2()}`;
9114
+ backupRoot = `${finalRoot}.backup-${randomUUID3()}`;
8816
9115
  await rename2(finalRoot, backupRoot);
8817
9116
  } catch (error2) {
8818
9117
  if (error2.code !== "ENOENT")