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 = ` -