@liustack/pptfast 0.9.0 → 0.11.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/README.md +2 -1
- package/README.zh-CN.md +2 -1
- package/dist/browser.js +111 -111
- package/dist/{chunk-6JJGXB5S.js → chunk-ONO7V2SF.js} +11927 -10980
- package/dist/chunk-ONO7V2SF.js.map +1 -0
- package/dist/{chunk-RE53OS3K.js → chunk-WCBFCHOZ.js} +24 -21
- package/dist/chunk-WCBFCHOZ.js.map +1 -0
- package/dist/cli.js +409 -33
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +90 -22
- package/dist/index.js +2 -2
- package/dist/{pixel-audit-ZB4RSPHG.js → pixel-audit-GA6B64W7.js} +2 -2
- package/dist/{pixel-audit-ZB4RSPHG.js.map → pixel-audit-GA6B64W7.js.map} +1 -1
- package/dist/validate.d.ts +152 -45
- package/dist/validate.js +58 -35
- package/package.json +2 -1
- package/dist/chunk-6JJGXB5S.js.map +0 -1
- package/dist/chunk-RE53OS3K.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
resolveSpecThemeId,
|
|
11
11
|
specJsonSchema,
|
|
12
12
|
validateSpec
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-WCBFCHOZ.js";
|
|
14
14
|
import {
|
|
15
15
|
installNodePlatform
|
|
16
16
|
} from "./chunk-HFRNKYZ6.js";
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
sniffImageFormat,
|
|
36
36
|
styleJsonSchema,
|
|
37
37
|
validateIr
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-ONO7V2SF.js";
|
|
39
39
|
import {
|
|
40
40
|
getPlatform
|
|
41
41
|
} from "./chunk-L524YK63.js";
|
|
@@ -573,7 +573,10 @@ var JS = `
|
|
|
573
573
|
// skills/pptfast/SKILL.md's phase-6 revision-request handling). Zero
|
|
574
574
|
// network/storage \u2014 an in-memory Blob + a synthetic <a download> click,
|
|
575
575
|
// the standard browser-only download pattern.
|
|
576
|
-
|
|
576
|
+
// pptfast serve (src/cli/serve.ts) depends on this function's
|
|
577
|
+
// callable-and-synchronous contract and return type \u2014 think before
|
|
578
|
+
// changing.
|
|
579
|
+
function buildExportBlob() {
|
|
577
580
|
var requests = []
|
|
578
581
|
Object.keys(annotations).forEach(function (key) {
|
|
579
582
|
var i = parseInt(key, 10)
|
|
@@ -584,7 +587,16 @@ var JS = `
|
|
|
584
587
|
})
|
|
585
588
|
var deckTitle = document.getElementById('pf-title').textContent
|
|
586
589
|
var payload = { version: '1', deck: deckTitle, requests: requests }
|
|
587
|
-
|
|
590
|
+
return new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
|
|
591
|
+
}
|
|
592
|
+
// The one sanctioned cross-module seam this file exposes (see this
|
|
593
|
+
// module's own doc comment) \u2014 a plain function reference, not data, so
|
|
594
|
+
// the caller always gets this build's live annotation state, never a
|
|
595
|
+
// stale snapshot.
|
|
596
|
+
window.__pptfastBuildExportBlob = buildExportBlob
|
|
597
|
+
|
|
598
|
+
exportBtn.addEventListener('click', function () {
|
|
599
|
+
var blob = buildExportBlob()
|
|
588
600
|
var url = URL.createObjectURL(blob)
|
|
589
601
|
var a = document.createElement('a')
|
|
590
602
|
a.href = url
|
|
@@ -695,10 +707,11 @@ async function loadDeckTarget(arg, cwd, projectHit, userHit) {
|
|
|
695
707
|
const target = await resolveDeckTarget(arg, resolveDecksDirSource(projectHit, userHit), cwd);
|
|
696
708
|
if (await isDeckDirectory(target)) {
|
|
697
709
|
const { ir, deckDir } = await readDeckDir(target);
|
|
698
|
-
return { raw: ir, baseDir: deckDir, isDir: true };
|
|
710
|
+
return { raw: ir, baseDir: deckDir, isDir: true, resolvedTarget: deckDir };
|
|
699
711
|
}
|
|
700
712
|
const raw = await loadIrFile(target);
|
|
701
|
-
|
|
713
|
+
const resolvedFile = resolve5(target);
|
|
714
|
+
return { raw, baseDir: dirname2(resolvedFile), isDir: false, resolvedTarget: resolvedFile };
|
|
702
715
|
}
|
|
703
716
|
async function runRender(irPath, opts) {
|
|
704
717
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -799,7 +812,10 @@ async function runSpecValidate(specPath) {
|
|
|
799
812
|
}
|
|
800
813
|
const spec2 = v.spec;
|
|
801
814
|
const axes = resolveNarrative(spec2.narrative);
|
|
802
|
-
|
|
815
|
+
const ok = `OK \u2014 ${spec2.pages.length} pages, narrative ${axes.strategy}/${axes.pacing}/${axes.audience}, theme "${resolveSpecThemeId(spec2)}"`;
|
|
816
|
+
const aliasNote = normalizedNote(v.normalized);
|
|
817
|
+
return aliasNote ? `${ok}
|
|
818
|
+
${aliasNote}` : ok;
|
|
803
819
|
}
|
|
804
820
|
function runSchema(mode) {
|
|
805
821
|
const schema = mode === "style" ? styleJsonSchema() : mode === "spec" ? specJsonSchema() : irJsonSchema();
|
|
@@ -850,50 +866,61 @@ async function runInit(cwd = process.cwd()) {
|
|
|
850
866
|
}
|
|
851
867
|
return `wrote ${target} \u2014 themes: \`pptfast themes\`, style schema: \`pptfast schema --style\``;
|
|
852
868
|
}
|
|
853
|
-
async function
|
|
869
|
+
async function renderDeckSlides(target, opts = {}) {
|
|
854
870
|
const cwd = opts.cwd ?? process.cwd();
|
|
855
871
|
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
856
|
-
const { raw, baseDir } = await loadDeckTarget(
|
|
872
|
+
const { raw, baseDir, isDir, resolvedTarget } = await loadDeckTarget(target, cwd, projectHit, userHit);
|
|
857
873
|
await applyDeckConfig(raw, { cwd, projectHit, userHit });
|
|
858
874
|
const v = validateIr(raw);
|
|
859
875
|
if (!v.ok) throw new PptfastError(`invalid IR:
|
|
860
876
|
${formatIssues(v.errors)}`);
|
|
861
877
|
await resolveLocalAssets(v.ir, baseDir);
|
|
862
|
-
await mkdir2(outDir, { recursive: true });
|
|
863
878
|
const ir = v.ir;
|
|
864
|
-
const svgs =
|
|
879
|
+
const svgs = ir.slides.map((_, i) => renderSlideSvg(ir, i));
|
|
880
|
+
return { ir, svgs, resolvedTarget, isDir, normalized: v.normalized };
|
|
881
|
+
}
|
|
882
|
+
function buildDeckAuditAndHtml(ir, svgs) {
|
|
883
|
+
const hasPlaceholder = ir.slides.some((slide) => slide.placeholder);
|
|
884
|
+
const auditReport = hasPlaceholder ? void 0 : auditDeck(ir);
|
|
885
|
+
const findings = auditReport?.findings ?? [];
|
|
886
|
+
const html = buildPreviewHtml({
|
|
887
|
+
title: ir.filename,
|
|
888
|
+
slides: ir.slides.map((slide, i) => ({
|
|
889
|
+
index: i,
|
|
890
|
+
id: slide.id,
|
|
891
|
+
type: slide.type,
|
|
892
|
+
svg: svgs[i],
|
|
893
|
+
placeholder: slide.placeholder
|
|
894
|
+
})),
|
|
895
|
+
findings: findings.map((f) => ({ page: f.page, slideId: f.slideId, code: f.code, message: f.message })),
|
|
896
|
+
auditNote: hasPlaceholder ? "audit overlay skipped \u2014 deck has unfilled placeholder pages; fill every page and re-run `pptfast preview --html` to see audit findings" : void 0,
|
|
897
|
+
checks: auditReport?.checks
|
|
898
|
+
});
|
|
899
|
+
return { html, findings, checks: auditReport?.checks };
|
|
900
|
+
}
|
|
901
|
+
async function buildDeckPreview(target, opts = {}) {
|
|
902
|
+
const rendered = await renderDeckSlides(target, opts);
|
|
903
|
+
const { html, findings, checks } = buildDeckAuditAndHtml(rendered.ir, rendered.svgs);
|
|
904
|
+
return { ...rendered, html, findings, checks };
|
|
905
|
+
}
|
|
906
|
+
async function runPreview(irPath, outDir, opts = {}) {
|
|
907
|
+
const { ir, svgs, normalized } = await renderDeckSlides(irPath, { cwd: opts.cwd });
|
|
908
|
+
await mkdir2(outDir, { recursive: true });
|
|
865
909
|
for (let i = 0; i < ir.slides.length; i++) {
|
|
866
|
-
const svg = renderSlideSvg(ir, i);
|
|
867
|
-
svgs.push(svg);
|
|
868
910
|
const name = `${String(i + 1).padStart(3, "0")}-${ir.slides[i].type}.svg`;
|
|
869
|
-
await writeFile2(join4(outDir, name),
|
|
911
|
+
await writeFile2(join4(outDir, name), svgs[i]);
|
|
870
912
|
}
|
|
871
913
|
const ok = `wrote ${ir.slides.length} SVG files to ${outDir}`;
|
|
872
914
|
const notes = [];
|
|
873
|
-
const aliasNote = normalizedNote(
|
|
915
|
+
const aliasNote = normalizedNote(normalized);
|
|
874
916
|
if (aliasNote) notes.push(aliasNote);
|
|
875
917
|
if (opts.htmlOut) {
|
|
918
|
+
const { html, findings } = buildDeckAuditAndHtml(ir, svgs);
|
|
876
919
|
const htmlPath = join4(outDir, "preview.html");
|
|
877
|
-
const hasPlaceholder = ir.slides.some((slide) => slide.placeholder);
|
|
878
|
-
const auditReport = hasPlaceholder ? void 0 : auditDeck(ir);
|
|
879
|
-
const auditFindings = auditReport?.findings ?? [];
|
|
880
|
-
const html = buildPreviewHtml({
|
|
881
|
-
title: ir.filename,
|
|
882
|
-
slides: ir.slides.map((slide, i) => ({
|
|
883
|
-
index: i,
|
|
884
|
-
id: slide.id,
|
|
885
|
-
type: slide.type,
|
|
886
|
-
svg: svgs[i],
|
|
887
|
-
placeholder: slide.placeholder
|
|
888
|
-
})),
|
|
889
|
-
findings: auditFindings.map((f) => ({ page: f.page, slideId: f.slideId, code: f.code, message: f.message })),
|
|
890
|
-
auditNote: hasPlaceholder ? "audit overlay skipped \u2014 deck has unfilled placeholder pages; fill every page and re-run `pptfast preview --html` to see audit findings" : void 0,
|
|
891
|
-
checks: auditReport?.checks
|
|
892
|
-
});
|
|
893
920
|
await writeFile2(htmlPath, html);
|
|
894
921
|
notes.push(`note: wrote self-contained preview to ${htmlPath}`);
|
|
895
|
-
if (
|
|
896
|
-
notes.push(`note: audit found ${
|
|
922
|
+
if (findings.length > 0) {
|
|
923
|
+
notes.push(`note: audit found ${findings.length} finding${findings.length === 1 ? "" : "s"} \u2014 see preview.html`);
|
|
897
924
|
}
|
|
898
925
|
}
|
|
899
926
|
return notes.length > 0 ? `${ok}
|
|
@@ -1040,6 +1067,341 @@ ${detail}`);
|
|
|
1040
1067
|
return `wrote ${outPath} (migrated IR v3 \u2192 v4)`;
|
|
1041
1068
|
}
|
|
1042
1069
|
|
|
1070
|
+
// src/cli/serve.ts
|
|
1071
|
+
import { spawn } from "child_process";
|
|
1072
|
+
import { randomUUID } from "crypto";
|
|
1073
|
+
import { watch } from "fs";
|
|
1074
|
+
import { rename, unlink, writeFile as writeFile3 } from "fs/promises";
|
|
1075
|
+
import { createServer } from "http";
|
|
1076
|
+
import { platform as osPlatform } from "os";
|
|
1077
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1078
|
+
var DEFAULT_PORT = 4400;
|
|
1079
|
+
var DEBOUNCE_MS = 200;
|
|
1080
|
+
var HEARTBEAT_MS = 3e4;
|
|
1081
|
+
var REVISION_REQUEST_FILENAME = "revision-request.json";
|
|
1082
|
+
var MAX_REVISION_REQUEST_BYTES = 1024 * 1024;
|
|
1083
|
+
function watchRoots(resolvedTarget, isDir) {
|
|
1084
|
+
if (!isDir) return [resolvedTarget];
|
|
1085
|
+
return [join5(resolvedTarget, SPEC_FILENAME), join5(resolvedTarget, PAGES_DIRNAME), join5(resolvedTarget, ASSETS_DIRNAME)];
|
|
1086
|
+
}
|
|
1087
|
+
var SERVE_CLIENT_SCRIPT_ID = "pptfast-serve-client";
|
|
1088
|
+
var SERVE_CLIENT_JS = `
|
|
1089
|
+
(function () {
|
|
1090
|
+
// Each of the two jobs below is independently wrapped (own function, own
|
|
1091
|
+
// try/catch at its call site at the bottom) so a failure in one \u2014 an
|
|
1092
|
+
// EventSource construction that throws in some unusual embedding, a
|
|
1093
|
+
// future buildPreviewHtml markup change that drops #pf-export-btn \u2014 can
|
|
1094
|
+
// never take the other down with it; a single un-isolated top-level
|
|
1095
|
+
// throw would otherwise abort the rest of this IIFE.
|
|
1096
|
+
|
|
1097
|
+
function setUpLiveReload() {
|
|
1098
|
+
var es = new EventSource('/events')
|
|
1099
|
+
es.addEventListener('reload', function () { location.reload() })
|
|
1100
|
+
|
|
1101
|
+
var banner = document.createElement('div')
|
|
1102
|
+
banner.id = 'pptfast-serve-error-banner'
|
|
1103
|
+
banner.setAttribute('role', 'alert')
|
|
1104
|
+
banner.style.cssText =
|
|
1105
|
+
'display:none;position:fixed;top:0;left:0;right:0;z-index:2147483647;' +
|
|
1106
|
+
'background:#dc2626;color:#fff;font:13px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;' +
|
|
1107
|
+
'padding:8px 16px;text-align:center'
|
|
1108
|
+
document.body.appendChild(banner)
|
|
1109
|
+
|
|
1110
|
+
function showBanner(message) {
|
|
1111
|
+
banner.textContent = 'pptfast serve: ' + message
|
|
1112
|
+
banner.style.display = 'block'
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
es.addEventListener('error', function (e) {
|
|
1116
|
+
if (!e || typeof e.data !== 'string') return // a real connection hiccup, not the server's own rebuild-failed frame
|
|
1117
|
+
var message = 'rebuild failed'
|
|
1118
|
+
try {
|
|
1119
|
+
var parsed = JSON.parse(e.data)
|
|
1120
|
+
if (parsed && typeof parsed.message === 'string') message = parsed.message
|
|
1121
|
+
} catch (err) {}
|
|
1122
|
+
showBanner(message)
|
|
1123
|
+
})
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function setUpRevisionRequestSubmit() {
|
|
1127
|
+
var originalBtn = document.getElementById('pf-export-btn')
|
|
1128
|
+
if (!originalBtn) return
|
|
1129
|
+
|
|
1130
|
+
var submitBtn = originalBtn.cloneNode(true)
|
|
1131
|
+
submitBtn.textContent = 'Submit revision request'
|
|
1132
|
+
originalBtn.replaceWith(submitBtn)
|
|
1133
|
+
|
|
1134
|
+
var status = document.createElement('span')
|
|
1135
|
+
status.id = 'pptfast-serve-submit-status'
|
|
1136
|
+
status.setAttribute('aria-live', 'polite')
|
|
1137
|
+
status.style.cssText = 'margin-left:8px;font-size:12px'
|
|
1138
|
+
|
|
1139
|
+
var downloadLink = document.createElement('a')
|
|
1140
|
+
downloadLink.id = 'pptfast-serve-download-fallback'
|
|
1141
|
+
downloadLink.href = '#'
|
|
1142
|
+
downloadLink.textContent = 'download a copy instead'
|
|
1143
|
+
downloadLink.style.cssText = 'margin-left:8px;font-size:12px;color:#2563eb'
|
|
1144
|
+
|
|
1145
|
+
submitBtn.insertAdjacentElement('afterend', status)
|
|
1146
|
+
status.insertAdjacentElement('afterend', downloadLink)
|
|
1147
|
+
|
|
1148
|
+
submitBtn.addEventListener('click', function () {
|
|
1149
|
+
if (typeof window.__pptfastBuildExportBlob !== 'function') {
|
|
1150
|
+
status.style.color = '#dc2626'
|
|
1151
|
+
status.textContent = 'failed to submit revision request (export function unavailable \u2014 try reloading the page)'
|
|
1152
|
+
return
|
|
1153
|
+
}
|
|
1154
|
+
status.style.color = ''
|
|
1155
|
+
status.textContent = 'submitting\u2026'
|
|
1156
|
+
// The extra Promise.resolve().then(...) wrapper (rather than calling
|
|
1157
|
+
// window.__pptfastBuildExportBlob() directly and chaining off its
|
|
1158
|
+
// result) means a synchronous throw from that call lands in the same
|
|
1159
|
+
// .catch below as an async rejection or a network failure \u2014 every
|
|
1160
|
+
// failure mode this chain can hit surfaces as the same inline
|
|
1161
|
+
// status-line feedback, none of them silent.
|
|
1162
|
+
Promise.resolve()
|
|
1163
|
+
.then(function () {
|
|
1164
|
+
return window.__pptfastBuildExportBlob()
|
|
1165
|
+
})
|
|
1166
|
+
.then(function (blob) {
|
|
1167
|
+
return blob.text()
|
|
1168
|
+
})
|
|
1169
|
+
.then(function (text) {
|
|
1170
|
+
return fetch('/revision-request', {
|
|
1171
|
+
method: 'POST',
|
|
1172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1173
|
+
body: text,
|
|
1174
|
+
})
|
|
1175
|
+
})
|
|
1176
|
+
.then(function (res) {
|
|
1177
|
+
if (!res.ok) throw new Error('server responded ' + res.status)
|
|
1178
|
+
status.style.color = '#16a34a'
|
|
1179
|
+
status.textContent = 'Revision request saved to the deck directory'
|
|
1180
|
+
})
|
|
1181
|
+
.catch(function (err) {
|
|
1182
|
+
status.style.color = '#dc2626'
|
|
1183
|
+
status.textContent = 'failed to submit revision request' + (err && err.message ? ' (' + err.message + ')' : '')
|
|
1184
|
+
})
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
downloadLink.addEventListener('click', function (e) {
|
|
1188
|
+
e.preventDefault()
|
|
1189
|
+
originalBtn.click()
|
|
1190
|
+
})
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
try {
|
|
1194
|
+
setUpLiveReload()
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
console.error('pptfast serve: failed to set up live reload', e)
|
|
1197
|
+
}
|
|
1198
|
+
try {
|
|
1199
|
+
setUpRevisionRequestSubmit()
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
console.error('pptfast serve: failed to set up revision-request submit', e)
|
|
1202
|
+
}
|
|
1203
|
+
})()
|
|
1204
|
+
`.trim();
|
|
1205
|
+
function buildServeClientScriptTag() {
|
|
1206
|
+
return `<script id="${SERVE_CLIENT_SCRIPT_ID}">${SERVE_CLIENT_JS}</script>`;
|
|
1207
|
+
}
|
|
1208
|
+
function injectServeClient(html) {
|
|
1209
|
+
if (html.includes(SERVE_CLIENT_SCRIPT_ID)) return html;
|
|
1210
|
+
return html.replace("</body>", `${buildServeClientScriptTag()}
|
|
1211
|
+
</body>`);
|
|
1212
|
+
}
|
|
1213
|
+
async function atomicWriteFile(targetPath, data) {
|
|
1214
|
+
const tmpPath = `${targetPath}.${randomUUID()}.tmp`;
|
|
1215
|
+
await writeFile3(tmpPath, data, "utf8");
|
|
1216
|
+
try {
|
|
1217
|
+
await rename(tmpPath, targetPath);
|
|
1218
|
+
} catch (e) {
|
|
1219
|
+
await unlink(tmpPath).catch(() => {
|
|
1220
|
+
});
|
|
1221
|
+
throw e;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
async function createServeServer(options) {
|
|
1225
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1226
|
+
const requestedPort = options.port ?? DEFAULT_PORT;
|
|
1227
|
+
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
|
|
1228
|
+
throw new PptfastError(`invalid port ${requestedPort} \u2014 expected an integer between 0 and 65535`);
|
|
1229
|
+
}
|
|
1230
|
+
const initial = await buildDeckPreview(options.target, { cwd });
|
|
1231
|
+
let cachedHtml = injectServeClient(initial.html);
|
|
1232
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
1233
|
+
const revisionRequestPath = initial.isDir ? join5(initial.resolvedTarget, REVISION_REQUEST_FILENAME) : join5(dirname3(initial.resolvedTarget), REVISION_REQUEST_FILENAME);
|
|
1234
|
+
function writeToAll(chunk) {
|
|
1235
|
+
for (const res of sseClients) {
|
|
1236
|
+
try {
|
|
1237
|
+
res.write(chunk);
|
|
1238
|
+
} catch {
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
function broadcast(event, data) {
|
|
1243
|
+
writeToAll(`event: ${event}
|
|
1244
|
+
data: ${JSON.stringify(data)}
|
|
1245
|
+
|
|
1246
|
+
`);
|
|
1247
|
+
}
|
|
1248
|
+
async function rebuild() {
|
|
1249
|
+
try {
|
|
1250
|
+
const result = await buildDeckPreview(options.target, { cwd });
|
|
1251
|
+
cachedHtml = injectServeClient(result.html);
|
|
1252
|
+
broadcast("reload", {});
|
|
1253
|
+
} catch (e) {
|
|
1254
|
+
broadcast("error", { message: e instanceof Error ? e.message : String(e) });
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
async function handleRevisionRequestPost(req, res) {
|
|
1258
|
+
const chunks = [];
|
|
1259
|
+
let total = 0;
|
|
1260
|
+
try {
|
|
1261
|
+
for await (const chunk of req) {
|
|
1262
|
+
total += chunk.length;
|
|
1263
|
+
if (total > MAX_REVISION_REQUEST_BYTES) {
|
|
1264
|
+
res.writeHead(413, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1265
|
+
res.end(`revision request body exceeds the ${MAX_REVISION_REQUEST_BYTES}-byte limit`);
|
|
1266
|
+
req.destroy();
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
chunks.push(chunk);
|
|
1270
|
+
}
|
|
1271
|
+
} catch {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
1275
|
+
try {
|
|
1276
|
+
JSON.parse(body);
|
|
1277
|
+
} catch {
|
|
1278
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1279
|
+
res.end("invalid JSON body");
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
await atomicWriteFile(revisionRequestPath, body);
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1286
|
+
res.end(`failed to write ${REVISION_REQUEST_FILENAME}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
1290
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1291
|
+
}
|
|
1292
|
+
const server = createServer((req, res) => {
|
|
1293
|
+
const pathname = (req.url ?? "/").split("?")[0];
|
|
1294
|
+
if (req.method === "GET" && pathname === "/") {
|
|
1295
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1296
|
+
res.end(cachedHtml);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
if (req.method === "GET" && pathname === "/events") {
|
|
1300
|
+
res.writeHead(200, {
|
|
1301
|
+
"Content-Type": "text/event-stream",
|
|
1302
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1303
|
+
Connection: "keep-alive"
|
|
1304
|
+
});
|
|
1305
|
+
res.write("retry: 2000\n\n");
|
|
1306
|
+
sseClients.add(res);
|
|
1307
|
+
res.on("close", () => sseClients.delete(res));
|
|
1308
|
+
res.on("error", () => sseClients.delete(res));
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (pathname === "/revision-request") {
|
|
1312
|
+
if (req.method !== "POST") {
|
|
1313
|
+
res.writeHead(405, { "Content-Type": "text/plain; charset=utf-8", Allow: "POST" });
|
|
1314
|
+
res.end("method not allowed \u2014 only POST is accepted on /revision-request");
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
void handleRevisionRequestPost(req, res);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1321
|
+
res.end("not found");
|
|
1322
|
+
});
|
|
1323
|
+
const heartbeat = setInterval(() => writeToAll(": heartbeat\n\n"), HEARTBEAT_MS);
|
|
1324
|
+
let debounceTimer;
|
|
1325
|
+
function scheduleRebuild() {
|
|
1326
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1327
|
+
debounceTimer = setTimeout(() => {
|
|
1328
|
+
debounceTimer = void 0;
|
|
1329
|
+
void rebuild();
|
|
1330
|
+
}, DEBOUNCE_MS);
|
|
1331
|
+
}
|
|
1332
|
+
const watchers = [];
|
|
1333
|
+
for (const path of watchRoots(initial.resolvedTarget, initial.isDir)) {
|
|
1334
|
+
try {
|
|
1335
|
+
watchers.push(watch(path, () => scheduleRebuild()));
|
|
1336
|
+
} catch (e) {
|
|
1337
|
+
if (e.code !== "ENOENT") throw e;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
function teardownWatchersAndTimers() {
|
|
1341
|
+
clearInterval(heartbeat);
|
|
1342
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1343
|
+
for (const w of watchers) w.close();
|
|
1344
|
+
}
|
|
1345
|
+
try {
|
|
1346
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
1347
|
+
const onError = (err) => {
|
|
1348
|
+
server.removeListener("listening", onListening);
|
|
1349
|
+
rejectListen(err);
|
|
1350
|
+
};
|
|
1351
|
+
const onListening = () => {
|
|
1352
|
+
server.removeListener("error", onError);
|
|
1353
|
+
resolveListen();
|
|
1354
|
+
};
|
|
1355
|
+
server.once("error", onError);
|
|
1356
|
+
server.once("listening", onListening);
|
|
1357
|
+
server.listen(requestedPort, "127.0.0.1");
|
|
1358
|
+
});
|
|
1359
|
+
} catch (e) {
|
|
1360
|
+
teardownWatchersAndTimers();
|
|
1361
|
+
if (e.code === "EADDRINUSE") {
|
|
1362
|
+
throw new PptfastError(`port ${requestedPort} is already in use \u2014 pick a different one with --port`);
|
|
1363
|
+
}
|
|
1364
|
+
throw e;
|
|
1365
|
+
}
|
|
1366
|
+
const address = server.address();
|
|
1367
|
+
const actualPort = typeof address === "object" && address !== null ? address.port : requestedPort;
|
|
1368
|
+
let closed = false;
|
|
1369
|
+
async function close() {
|
|
1370
|
+
if (closed) return;
|
|
1371
|
+
closed = true;
|
|
1372
|
+
teardownWatchersAndTimers();
|
|
1373
|
+
for (const res of sseClients) res.end();
|
|
1374
|
+
sseClients.clear();
|
|
1375
|
+
if (typeof server.closeIdleConnections === "function") server.closeIdleConnections();
|
|
1376
|
+
if (typeof server.closeAllConnections === "function") server.closeAllConnections();
|
|
1377
|
+
await new Promise((resolveClose, rejectClose) => {
|
|
1378
|
+
server.close((err) => err ? rejectClose(err) : resolveClose());
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
return { server, rebuild, close, url: `http://127.0.0.1:${actualPort}`, port: actualPort };
|
|
1382
|
+
}
|
|
1383
|
+
function openBrowser(url) {
|
|
1384
|
+
const command = osPlatform() === "darwin" ? "open" : "xdg-open";
|
|
1385
|
+
try {
|
|
1386
|
+
const child = spawn(command, [url], { stdio: "ignore", detached: true });
|
|
1387
|
+
child.on("error", () => {
|
|
1388
|
+
});
|
|
1389
|
+
child.unref();
|
|
1390
|
+
} catch {
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
async function runServe(target, opts = {}) {
|
|
1394
|
+
const handle = await createServeServer({ target, port: opts.port, cwd: opts.cwd });
|
|
1395
|
+
console.log(`pptfast serve: ${handle.url} (Ctrl+C to stop)`);
|
|
1396
|
+
if (opts.open !== false) openBrowser(handle.url);
|
|
1397
|
+
process.on("SIGINT", () => {
|
|
1398
|
+
void handle.close().then(
|
|
1399
|
+
() => process.exit(0),
|
|
1400
|
+
() => process.exit(1)
|
|
1401
|
+
);
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1043
1405
|
// src/cli/update.ts
|
|
1044
1406
|
import { execFile } from "child_process";
|
|
1045
1407
|
var PACKAGE_NAME = "@liustack/pptfast";
|
|
@@ -1211,6 +1573,20 @@ program.command("preview").description("Render each slide to an SVG file for vis
|
|
|
1211
1573
|
fail(e);
|
|
1212
1574
|
}
|
|
1213
1575
|
});
|
|
1576
|
+
program.command("serve").description("Serve a live-reloading HTML preview of an IR JSON file, deck project directory, or bare deck name over HTTP").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptfast/decks").option("--port <number>", `port to listen on (default ${DEFAULT_PORT})`).option("--no-open", "do not open the URL in a browser after starting").action(async (target, opts) => {
|
|
1577
|
+
try {
|
|
1578
|
+
let port;
|
|
1579
|
+
if (opts.port !== void 0) {
|
|
1580
|
+
port = Number(opts.port);
|
|
1581
|
+
if (!Number.isInteger(port)) {
|
|
1582
|
+
fail(new Error(`invalid --port value "${opts.port}" \u2014 expected an integer`));
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
await runServe(target, { port, open: opts.open });
|
|
1586
|
+
} catch (e) {
|
|
1587
|
+
fail(e);
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1214
1590
|
program.command("check-update").description("Check npm for a newer pptfast release").action(async () => {
|
|
1215
1591
|
const info = await checkForUpdate({ currentVersion: VERSION });
|
|
1216
1592
|
if (!info.checked) fail(new Error(`update check failed: ${info.error}`));
|