Skip to content

Commit 4975411

Browse files
committed
fix(cli): carry S1 review fixes into the serve wave
Four quick-adds the S1 review flagged for S2 to bring along: - close(): explicit closeIdleConnections()/closeAllConnections() fallback, typeof-guarded for Node 18.0-18.18 (the methods land in 18.2.0, nodejs/node#42812) — Node's own close() briefly auto-invoked closeIdleConnections internally on some 18.x releases via a mistaken v19-only backport, reverted in 18.20.3 (nodejs/node#52336), so close() alone can't be assumed sufficient on any given patch. - Extend the ENOENT watch-setup comment: a pages/or assets/ directory created after startup is never picked up until pptfast serve restarts. - Two watch tests: a file appearing in assets/ triggers a rebuild, and a deck.spec.json-only edit (no pages/ touch) triggers one too — S1's own coverage only exercised pages/*.json. - One-line comment at runPreview's moved mkdir(outDir) noting the deliberate after-render timing.
1 parent e1c4de5 commit 4975411

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

‎src/cli/commands.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,7 @@ export async function buildDeckPreview(target: string, opts: { cwd?: string } =
782782
*/
783783
export async function runPreview(irPath: string, outDir: string, opts: PreviewOptions = {}): Promise<string> {
784784
const { ir, svgs, normalized } = await renderDeckSlides(irPath, { cwd: opts.cwd })
785+
// After render, not before (S1 review carry) — see this function's own doc comment.
785786
await mkdir(outDir, { recursive: true })
786787
for (let i = 0; i < ir.slides.length; i++) {
787788
const name = `${String(i + 1).padStart(3, "0")}-${ir.slides[i]!.type}.svg`

‎src/cli/serve.test.ts‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ const VALID_IR = {
2424
// out of createServeServer's own initial build.
2525
const INVALID_IR_SHAPE = { version: "4" }
2626

27+
// 1x1 red PNG — the same fixture commands.test.ts uses to exercise a real
28+
// deck-dir `assets/` asset (see its own "assets/ auto-registration reaches
29+
// rendered output" describe block).
30+
const PNG_1PX = Buffer.from(
31+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
32+
"base64",
33+
)
34+
2735
function makeDir(prefix = "pptfast-serve-"): Promise<string> {
2836
return mkdtemp(join(tmpdir(), prefix))
2937
}
@@ -293,6 +301,57 @@ describe("createServeServer — watch + rebuild (deck project directory)", () =>
293301
expect(recovered.body).toContain("recovered draft")
294302
sse.close()
295303
})
304+
305+
// S1 review carry: task S1's own watch-coverage tests only exercised
306+
// pages/*.json — these two round out the other two watch roots
307+
// (`watchRoots`, `./serve.ts`) with dedicated coverage of their own.
308+
309+
it("rebuilds when a file appears inside assets/ (dedicated assets/ watch coverage)", async () => {
310+
const deckDir = await makeDir()
311+
await writeFile(join(deckDir, "deck.spec.json"), JSON.stringify(makeDeckPlan()))
312+
await mkdir(join(deckDir, "pages"))
313+
await writeFile(
314+
join(deckDir, "pages", "p-a.json"),
315+
JSON.stringify({ components: [{ type: "image", asset_id: "logo" }] }),
316+
)
317+
await mkdir(join(deckDir, "assets"))
318+
await writeFile(join(deckDir, "assets", "logo.png"), PNG_1PX)
319+
const handle = await startServe(deckDir)
320+
const initial = await get(handle.port, "/")
321+
expect(initial.body).toContain("data:image/png;base64")
322+
323+
const sse = connectSSE(handle.port)
324+
// A second, unreferenced file — this test's only job is proving the
325+
// assets/ *directory* is watched at all (task S1's own coverage was
326+
// pages/*.json only), not re-proving asset-content resolution
327+
// (commands.test.ts's "assets/ auto-registration reaches rendered
328+
// output" already covers that).
329+
await writeFile(join(deckDir, "assets", "extra.png"), PNG_1PX)
330+
await sse.waitFor("reload")
331+
sse.close()
332+
})
333+
334+
it("rebuilds when only deck.spec.json changes, with no pages/ edit", async () => {
335+
const deckDir = await makeDir()
336+
await writeFile(join(deckDir, "deck.spec.json"), JSON.stringify(makeDeckPlan()))
337+
await mkdir(join(deckDir, "pages"))
338+
await writeFile(
339+
join(deckDir, "pages", "p-a.json"),
340+
JSON.stringify({ components: [{ type: "paragraph", text: "steady content" }] }),
341+
)
342+
const handle = await startServe(deckDir)
343+
const initial = await get(handle.port, "/")
344+
expect(initial.body).toContain("serve-deck")
345+
346+
const sse = connectSSE(handle.port)
347+
const renamedPlan = { ...makeDeckPlan(), filename: "serve-deck-renamed" }
348+
await writeFile(join(deckDir, "deck.spec.json"), JSON.stringify(renamedPlan))
349+
await sse.waitFor("reload")
350+
351+
const revised = await get(handle.port, "/")
352+
expect(revised.body).toContain("serve-deck-renamed")
353+
sse.close()
354+
})
296355
})
297356

298357
describe("createServeServer — rebuild()", () => {

‎src/cli/serve.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,13 @@ export async function createServeServer(options: ServeOptions): Promise<ServeHan
206206
// (nothing filled in, no local images) — nothing to watch there until
207207
// it's created, not a reason to fail serve startup. Anything other
208208
// than "doesn't exist yet" (permissions, ...) is a real problem.
209+
// Consequence (S1 review carry): this watch-setup pass only ever runs
210+
// once, at `createServeServer` call time — a directory that gets
211+
// created *later* in the same session (e.g. the first local image
212+
// asset is added, materializing `assets/` mid-edit) is never picked
213+
// up, since nothing here re-scans for newly-appeared watch roots
214+
// afterward. Changes under such a directory go unnoticed until the
215+
// user restarts `pptfast serve`.
209216
if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e
210217
}
211218
}
@@ -248,6 +255,28 @@ export async function createServeServer(options: ServeOptions): Promise<ServeHan
248255
teardownWatchersAndTimers()
249256
for (const res of sseClients) res.end()
250257
sseClients.clear()
258+
// `res.end()` above finishes each SSE response, but the socket behind a
259+
// `Connection: keep-alive` response (`GET /events`'s own header) is not
260+
// guaranteed to be released the instant the response ends —
261+
// `server.close()`'s callback only fires once every socket the server
262+
// ever accepted has actually closed, so a lingering keep-alive socket
263+
// can otherwise leave it hanging indefinitely (S1 review carry).
264+
// `closeIdleConnections`/`closeAllConnections` ("http: added connection
265+
// closing methods", nodejs/node#42812) exist since Node 18.2.0 — this
266+
// repo's actual floor is 18.0.0 (package.json#engines: ">=18"), hence
267+
// the `typeof` guard rather than a direct call. Calling both
268+
// unconditionally on every Node 18+ patch is deliberate, not redundant
269+
// belt-and-suspenders: Node's own `close()` briefly, accidentally
270+
// auto-invoked `closeIdleConnections()` internally on some Node 18.x
271+
// releases — a v19-only behavior mistakenly backported and later
272+
// reverted (nodejs/node#52336, landed 18.20.3: "closeIdleConnections
273+
// should not be called while server.close in node v18. This behavior is
274+
// for node v19 and above") — so whether `close()` alone is already
275+
// sufficient varies by exact patch and cannot be assumed; calling both
276+
// methods explicitly here is correct on every patch, a harmless no-op
277+
// wherever `close()` already handled it.
278+
if (typeof server.closeIdleConnections === "function") server.closeIdleConnections()
279+
if (typeof server.closeAllConnections === "function") server.closeAllConnections()
251280
await new Promise<void>((resolveClose, rejectClose) => {
252281
server.close((err) => (err ? rejectClose(err) : resolveClose()))
253282
})

0 commit comments

Comments
 (0)