lazyslides 0.3.0 → 0.3.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/index.js CHANGED
@@ -112,6 +112,14 @@ export default function lazyslides(eleventyConfig, options = {}) {
112
112
  d2Cache.set(source, `<div class="d2-error">${safeMsg}</div>`);
113
113
  }
114
114
  }
115
+
116
+ // Terminate the D2 worker thread so the Node process can exit cleanly.
117
+ // The D2 class spawns a Worker for WASM but never exposes a cleanup method.
118
+ if (d2Instance?.worker) {
119
+ await d2Instance.worker.terminate();
120
+ d2Instance = null;
121
+ d2Available = null;
122
+ }
115
123
  });
116
124
 
117
125
  // ---------------------------------------------------------------
package/lib/export-pdf.js CHANGED
@@ -1,4 +1,4 @@
1
- import { execSync, spawn } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import http from "node:http";
@@ -6,14 +6,18 @@ import http from "node:http";
6
6
  const PORT = 4100;
7
7
  const BASE_URL = `http://127.0.0.1:${PORT}`;
8
8
  const OUTPUT_DIR = "_pdfs";
9
- const TIMEOUT_SECONDS = 30;
9
+ const SERVER_TIMEOUT_SECONDS = 60;
10
+ const BUILD_IDLE_TIMEOUT_MS = 15000;
11
+ const BUILD_HARD_TIMEOUT_MS = 180000;
10
12
 
11
13
  let serverProcess = null;
12
14
 
13
15
  function cleanup() {
14
16
  if (serverProcess) {
15
17
  console.log("\n Stopping server...");
16
- serverProcess.kill();
18
+ try {
19
+ serverProcess.kill("SIGTERM");
20
+ } catch {}
17
21
  serverProcess = null;
18
22
  }
19
23
  }
@@ -29,9 +33,9 @@ function waitForServer() {
29
33
  });
30
34
  req.on("error", () => {
31
35
  elapsed++;
32
- if (elapsed >= TIMEOUT_SECONDS) {
36
+ if (elapsed >= SERVER_TIMEOUT_SECONDS) {
33
37
  clearInterval(interval);
34
- reject(new Error(`Server failed to start within ${TIMEOUT_SECONDS}s`));
38
+ reject(new Error(`Server failed to start within ${SERVER_TIMEOUT_SECONDS}s`));
35
39
  }
36
40
  });
37
41
  req.end();
@@ -39,29 +43,107 @@ function waitForServer() {
39
43
  });
40
44
  }
41
45
 
42
- function runDecktape(name) {
43
- const url = `${BASE_URL}/presentations/${name}/?pdf`;
44
- const output = path.join(OUTPUT_DIR, `${name}.pdf`);
46
+ /**
47
+ * Run `pnpm run build`, but tolerate the eleventy child not exiting cleanly
48
+ * (known issue: d2 WASM / chokidar handles can keep the event loop alive after
49
+ * "Wrote N files"). We detect idle stdout and force-kill.
50
+ */
51
+ function runBuild(cwd) {
52
+ return new Promise((resolve, reject) => {
53
+ // Pipe stdio so we can observe activity and reset the idle timer.
54
+ const child = spawn("pnpm", ["run", "build"], {
55
+ cwd,
56
+ stdio: ["ignore", "pipe", "pipe"],
57
+ });
58
+
59
+ let idleTimer = null;
60
+ let hardTimer = null;
61
+ let resolved = false;
62
+
63
+ const done = (err) => {
64
+ if (resolved) return;
65
+ resolved = true;
66
+ clearTimeout(idleTimer);
67
+ clearTimeout(hardTimer);
68
+ try { child.kill("SIGKILL"); } catch {}
69
+ err ? reject(err) : resolve();
70
+ };
71
+
72
+ const bumpIdle = () => {
73
+ clearTimeout(idleTimer);
74
+ idleTimer = setTimeout(() => {
75
+ console.log(`\n Build child idle for ${BUILD_IDLE_TIMEOUT_MS / 1000}s — assuming done and force-exiting.`);
76
+ done();
77
+ }, BUILD_IDLE_TIMEOUT_MS);
78
+ };
79
+
80
+ child.stdout.on("data", (chunk) => {
81
+ process.stdout.write(chunk);
82
+ bumpIdle();
83
+ });
84
+ child.stderr.on("data", (chunk) => {
85
+ process.stderr.write(chunk);
86
+ bumpIdle();
87
+ });
88
+
89
+ bumpIdle();
90
+
91
+ hardTimer = setTimeout(() => {
92
+ done(new Error(`Build exceeded hard timeout ${BUILD_HARD_TIMEOUT_MS / 1000}s`));
93
+ }, BUILD_HARD_TIMEOUT_MS);
94
+
95
+ child.on("exit", (code) => {
96
+ if (code === 0 || code === null) done();
97
+ else done(new Error(`Build failed with exit code ${code}`));
98
+ });
99
+ child.on("error", (err) => done(err));
100
+ });
101
+ }
45
102
 
46
- console.log(` → Exporting: ${name}`);
47
- try {
48
- execSync(
49
- `npx decktape reveal --size 1920x1080 --pause 1000 --load-pause 2000 "${url}" "${output}"`,
50
- { stdio: ["ignore", "pipe", "pipe"] }
103
+ function runDecktape(name, cwd) {
104
+ return new Promise((resolve) => {
105
+ const url = `${BASE_URL}/presentations/${name}/?pdf`;
106
+ const output = path.join(OUTPUT_DIR, `${name}.pdf`);
107
+
108
+ console.log(` → Exporting: ${name}`);
109
+ console.log(` URL: ${url}`);
110
+ console.log(` Output: ${output}\n`);
111
+
112
+ const child = spawn(
113
+ "npx",
114
+ [
115
+ "decktape",
116
+ "reveal",
117
+ "--size", "1920x1080",
118
+ "--pause", "1000",
119
+ "--load-pause", "2000",
120
+ url,
121
+ output,
122
+ ],
123
+ { cwd, stdio: ["ignore", "inherit", "inherit"] }
51
124
  );
52
- console.log(` \u2713 ${output}`);
53
- return true;
54
- } catch {
55
- console.log(` \u2717 Failed: ${name}`);
56
- return false;
57
- }
125
+
126
+ child.on("exit", (code) => {
127
+ if (code === 0) {
128
+ console.log(`\n \u2713 ${output}`);
129
+ resolve(true);
130
+ } else {
131
+ console.log(`\n \u2717 Failed: ${name} (decktape exit code ${code})`);
132
+ resolve(false);
133
+ }
134
+ });
135
+ child.on("error", (err) => {
136
+ console.log(`\n \u2717 Failed: ${name} (${err.message})`);
137
+ resolve(false);
138
+ });
139
+ });
58
140
  }
59
141
 
60
- function getAvailablePresentations(cwd) {
142
+ function getAvailablePresentations(cwd, { includeHidden = false } = {}) {
61
143
  const dir = path.join(cwd, "presentations");
62
144
  return fs
63
145
  .readdirSync(dir, { withFileTypes: true })
64
- .filter((d) => d.isDirectory() && d.name !== "_template")
146
+ .filter((d) => d.isDirectory() && (includeHidden || !d.name.startsWith("_")))
65
147
  .map((d) => d.name);
66
148
  }
67
149
 
@@ -75,9 +157,10 @@ export async function run(opts = {}) {
75
157
  const cwd = opts.cwd || process.cwd();
76
158
  const requestedName = opts.name;
77
159
  const available = getAvailablePresentations(cwd);
160
+ const allForLookup = getAvailablePresentations(cwd, { includeHidden: true });
78
161
 
79
162
  if (requestedName) {
80
- if (!available.includes(requestedName)) {
163
+ if (!allForLookup.includes(requestedName)) {
81
164
  console.error(`Presentation not found: ${requestedName}`);
82
165
  console.error(" Available presentations:");
83
166
  for (const name of available) {
@@ -89,16 +172,25 @@ export async function run(opts = {}) {
89
172
 
90
173
  const presentations = requestedName ? [requestedName] : available;
91
174
 
92
- // Build the site
175
+ // Build the site (tolerate lingering child handles)
93
176
  console.log("Building site...");
94
- execSync("pnpm run build", { stdio: "inherit", cwd });
177
+ try {
178
+ await runBuild(cwd);
179
+ } catch (err) {
180
+ console.error(`Build failed: ${err.message}`);
181
+ process.exit(1);
182
+ }
95
183
 
96
184
  // Start server
97
185
  console.log(`\nStarting server on port ${PORT}...`);
98
186
  serverProcess = spawn("npx", ["eleventy", "--serve", "--port", String(PORT)], {
99
- stdio: "ignore",
187
+ stdio: ["ignore", "pipe", "pipe"],
100
188
  cwd,
101
189
  });
190
+ serverProcess.stdout.on("data", () => {}); // drain
191
+ serverProcess.stderr.on("data", (chunk) => {
192
+ process.stderr.write(`[server] ${chunk}`);
193
+ });
102
194
 
103
195
  process.on("exit", cleanup);
104
196
  process.on("SIGINT", () => { cleanup(); process.exit(1); });
@@ -106,7 +198,13 @@ export async function run(opts = {}) {
106
198
 
107
199
  // Wait for server
108
200
  console.log("Waiting for server...");
109
- await waitForServer();
201
+ try {
202
+ await waitForServer();
203
+ } catch (err) {
204
+ console.error(`\n${err.message}`);
205
+ cleanup();
206
+ process.exit(1);
207
+ }
110
208
  console.log("Server ready");
111
209
 
112
210
  // Create output directory
@@ -118,7 +216,7 @@ export async function run(opts = {}) {
118
216
  console.log(`\nExporting ${presentations.length} presentation(s) to PDF...\n`);
119
217
 
120
218
  for (const name of presentations) {
121
- if (runDecktape(name)) {
219
+ if (await runDecktape(name, cwd)) {
122
220
  success++;
123
221
  } else {
124
222
  failure++;
@@ -131,7 +229,6 @@ export async function run(opts = {}) {
131
229
  // Summary
132
230
  console.log("\u2501".repeat(30));
133
231
  console.log(`Exported: ${success} Failed: ${failure}`);
134
- if (failure > 0) {
135
- process.exit(1);
136
- }
232
+ // Force exit to ensure all child processes (server, npx wrappers) are cleaned up.
233
+ process.exit(failure > 0 ? 1 : 0);
137
234
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazyslides",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Slide decks with native agentic AI integration",
5
5
  "license": "MIT",
6
6
  "author": "Chris Tietz",
@@ -50,6 +50,7 @@
50
50
  "@11ty/eleventy": "^3.0.0"
51
51
  },
52
52
  "dependencies": {
53
+ "decktape": "^3.14.0",
53
54
  "js-yaml": "^4.1.0"
54
55
  },
55
56
  "optionalDependencies": {
@@ -59,7 +60,6 @@
59
60
  "@11ty/eleventy": "^3.0.0",
60
61
  "@tailwindcss/cli": "4.1.18",
61
62
  "concurrently": "^9.0.0",
62
- "decktape": "^3.14.0",
63
63
  "tailwindcss": "4.1.18",
64
64
  "vitest": "^4.1.2"
65
65
  },