diff --git a/.markdownlintrc.json b/.markdownlintrc.json
index 1ea11c4..2586735 100644
--- a/.markdownlintrc.json
+++ b/.markdownlintrc.json
@@ -2,6 +2,7 @@
"extends": "markdownlint/style",
"MD001": false,
"MD022": false,
+ "MD024": false,
"MD025": false,
"MD032": false,
"MD034": false,
diff --git a/README.md b/README.md
index 4aa945e..8f66ba1 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Marp-based slides for a CodeHub meetup talk on home networking and Pi-hole.
## Requirements
-- Node.js 20 or newer
+- Node.js 20.19 or newer
- pnpm 11 or newer
## Local development
@@ -16,7 +16,9 @@ pnpm install
pnpm dev
```
-Marp serves the deck at a local URL. Open that URL in your browser to present or review the deck.
+`pnpm dev` builds `dist/index.html` (including Mermaid transformation), serves `dist/`, and rebuilds when `slides.md` changes.
+
+By default it uses port `8080`; if `8080` is busy it automatically uses the next available port and prints the URL.
## Build and export
diff --git a/marp.config.js b/marp.config.js
index 873d776..5d4b89c 100644
--- a/marp.config.js
+++ b/marp.config.js
@@ -1,4 +1,7 @@
module.exports = {
+ // html is required: slides use
tags inside Mermaid diagram labels.
+ // Implication: raw HTML in slides.md will be rendered as-is in the output.
+ // Keep slides.md under source control and avoid user-supplied content.
html: true,
markdown: {
html: true,
diff --git a/package.json b/package.json
index baa09d8..a63b44a 100644
--- a/package.json
+++ b/package.json
@@ -4,10 +4,10 @@
"version": "0.1.0",
"packageManager": "pnpm@11.3.0",
"engines": {
- "node": ">=20.0.0"
+ "node": ">=20.19.0"
},
"scripts": {
- "dev": "pnpm exec marp slides.md --server --allow-local-files",
+ "dev": "node scripts/dev.mjs",
"build": "node scripts/build.mjs",
"serve": "http-server dist -p 8001 -o",
"preview": "pnpm build && pnpm serve",
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index d09e9f2..7fc6418 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,2 +1,2 @@
-allowBuilds:
- '@biomejs/biome': set this to true or false
+onlyBuiltDependencies:
+ - '@biomejs/biome'
diff --git a/scripts/build.mjs b/scripts/build.mjs
index b220c2f..abd3c27 100644
--- a/scripts/build.mjs
+++ b/scripts/build.mjs
@@ -29,12 +29,14 @@ async function main() {
// The deck starts without images. Copy assets only when they exist.
}
- await runMarpBuild();
+ await runMarpBuild();
+
// Inject Mermaid.js into the generated HTML
execFileSync('node', [path.join(__dirname, 'inject-mermaid.mjs')], {
cwd: distDir,
stdio: 'inherit',
- });}
+ });
+}
main().catch((error) => {
console.error(error);
diff --git a/scripts/dev.mjs b/scripts/dev.mjs
new file mode 100644
index 0000000..5814f0c
--- /dev/null
+++ b/scripts/dev.mjs
@@ -0,0 +1,100 @@
+import { execFileSync, spawn } from 'node:child_process';
+import net from 'node:net';
+import { watchFile } from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const rootDir = process.cwd();
+const slidesFile = path.join(rootDir, 'slides.md');
+const buildScript = path.join(__dirname, 'build.mjs');
+
+let isBuilding = false;
+let rebuildPending = false;
+
+function isPortAvailable(port) {
+ return new Promise((resolve) => {
+ const tester = net.createServer();
+
+ tester.once('error', () => resolve(false));
+ tester.once('listening', () => {
+ tester.close(() => resolve(true));
+ });
+
+ tester.listen(port, '0.0.0.0');
+ });
+}
+
+async function findAvailablePort(startPort, maxAttempts = 20) {
+ for (let index = 0; index < maxAttempts; index += 1) {
+ const candidate = startPort + index;
+ if (await isPortAvailable(candidate)) {
+ return candidate;
+ }
+ }
+
+ throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
+}
+
+function buildDeck() {
+ if (isBuilding) {
+ rebuildPending = true;
+ return;
+ }
+
+ isBuilding = true;
+
+ try {
+ execFileSync('node', [buildScript], {
+ cwd: rootDir,
+ stdio: 'inherit',
+ });
+ } finally {
+ isBuilding = false;
+
+ if (rebuildPending) {
+ rebuildPending = false;
+ buildDeck();
+ }
+ }
+}
+
+async function main() {
+ buildDeck();
+
+ const requestedPort = Number(process.env.PORT ?? '8080');
+ const port = await findAvailablePort(requestedPort);
+
+ if (port !== requestedPort) {
+ console.log(`[dev] Port ${requestedPort} is in use, using ${port} instead.`);
+ }
+
+ const server = spawn('pnpm', ['exec', 'http-server', 'dist', '-p', String(port), '-c-1', '-o'], {
+ cwd: rootDir,
+ stdio: 'inherit',
+ });
+
+ watchFile(slidesFile, { interval: 500 }, (current, previous) => {
+ if (current.mtimeMs !== previous.mtimeMs) {
+ console.log('[dev] slides.md changed; rebuilding...');
+ buildDeck();
+ }
+ });
+
+ function shutdown(code = 0) {
+ server.kill('SIGTERM');
+ process.exit(code);
+ }
+
+ process.on('SIGINT', () => shutdown(0));
+ process.on('SIGTERM', () => shutdown(0));
+
+ server.on('exit', (code) => {
+ process.exit(code ?? 0);
+ });
+}
+
+main().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/scripts/inject-mermaid.mjs b/scripts/inject-mermaid.mjs
index a58a8dd..8a33802 100644
--- a/scripts/inject-mermaid.mjs
+++ b/scripts/inject-mermaid.mjs
@@ -27,41 +27,32 @@ for (const codeBlock of codeBlocks) {
}
}
-// Get the updated HTML
-let html = dom.serialize();
-
-// Add Mermaid.js library before closing body tag if not already present
-if (!html.includes("mermaid.min.js")) {
- const mermaidScript = `
-