create-volt 0.14.0 → 0.15.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@ All notable changes to `create-volt` are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.15.1] - 2026-06-29
8
+
9
+ ### Fixed
10
+ - `volt.js` no longer touches `window` at import time — the hot-reload client is
11
+ guarded with `typeof window`, so the library is safe to import in Node (SSR,
12
+ tests), not just the browser.
13
+
14
+ ### Changed
15
+ - The `pages` add-on imports `express`/`marked` lazily (only when mounted), so
16
+ its pure helpers load without those packages present.
17
+
18
+ ### Added (repo tooling — not shipped in scaffolded apps)
19
+ - A `node --test` unit suite (reactive core, memory store, pages helpers), a
20
+ `smoke` script (scaffold → install → boot → hit endpoints), a CI workflow, and
21
+ a smoke-test **gate** on the dependency auto-updater: a version bump is
22
+ committed only if unit tests + smoke pass on the bumped versions.
23
+
24
+ ## [0.15.0] - 2026-06-28
25
+
26
+ ### Added
27
+ - **`pages` add-on** — markdown pages, no database and no admin. Drop `.md`
28
+ files in `pages/` and each is served as HTML at `/<slug>`; front-matter
29
+ `title:` sets the page title. Author them in your editor or with AI. Pulls in
30
+ `marked` (added on enable, tracked by the dependency auto-updater); the
31
+ `pages/` directory is auto-created with a sample on first run. Mounted last,
32
+ so your own app routes always win.
33
+
7
34
  ## [0.14.0] - 2026-06-28
8
35
 
9
36
  ### Changed
@@ -203,6 +230,8 @@ All notable changes to `create-volt` are documented here. The format follows
203
230
  watching and full-page hot reload. Supports `--skip-install` and `--force`,
204
231
  and auto-detects npm / pnpm / yarn / bun for the install step.
205
232
 
233
+ [0.15.1]: https://github.com/MIR-2025/volt/releases/tag/v0.15.1
234
+ [0.15.0]: https://github.com/MIR-2025/volt/releases/tag/v0.15.0
206
235
  [0.14.0]: https://github.com/MIR-2025/volt/releases/tag/v0.14.0
207
236
  [0.13.0]: https://github.com/MIR-2025/volt/releases/tag/v0.13.0
208
237
  [0.12.0]: https://github.com/MIR-2025/volt/releases/tag/v0.12.0
@@ -0,0 +1,84 @@
1
+ // pages.js — markdown pages. Drop *.md files in pages/ and each is served as
2
+ // HTML at /<slug>. No database, no admin: author them in your editor or with AI.
3
+ // Pages are code-owned files (trusted), so their markdown HTML is served as-is.
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ // express + marked are imported lazily in pagesRouter() so this module's pure
7
+ // helpers (parseFrontMatter, isSafeSlug) load without those deps installed.
8
+
9
+ const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
10
+
11
+ const SAMPLE = `---
12
+ title: Welcome
13
+ ---
14
+
15
+ # Your Volt pages
16
+
17
+ This page is a markdown file at \`pages/welcome.md\`, served at \`/welcome\`.
18
+
19
+ - Drop more \`.md\` files in \`pages/\` — each becomes a page at \`/<filename>\`.
20
+ - Add front-matter to set the title:
21
+
22
+ \`\`\`
23
+ ---
24
+ title: About us
25
+ ---
26
+ \`\`\`
27
+
28
+ Author them in your editor, or ask an AI to write them. No database, no admin.
29
+ `;
30
+
31
+ function ensure(dir) {
32
+ if (!fs.existsSync(dir)) {
33
+ fs.mkdirSync(dir, { recursive: true });
34
+ fs.writeFileSync(path.join(dir, "welcome.md"), SAMPLE);
35
+ }
36
+ }
37
+
38
+ const FM = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/;
39
+ export function parseFrontMatter(src) {
40
+ const m = src.match(FM);
41
+ if (!m) return { meta: {}, body: src };
42
+ const meta = {};
43
+ for (const line of m[1].split(/\r?\n/)) {
44
+ const i = line.indexOf(":");
45
+ if (i > 0) meta[line.slice(0, i).trim()] = line.slice(i + 1).trim();
46
+ }
47
+ return { meta, body: src.slice(m[0].length) };
48
+ }
49
+
50
+ // slugs are restricted to a safe charset — no dots/slashes → no path traversal
51
+ export const isSafeSlug = (s) => /^[a-z0-9][a-z0-9-]*$/i.test(s);
52
+
53
+ function shell(title, inner) {
54
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" />
55
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
56
+ <title>${esc(title)}</title>
57
+ <style>
58
+ :root { color-scheme: light dark }
59
+ body { max-width: 720px; margin: 2.5rem auto; padding: 0 1.1rem; font: 17px/1.7 system-ui, -apple-system, sans-serif; }
60
+ h1, h2, h3 { line-height: 1.25; margin: 1.6rem 0 .6rem }
61
+ pre { background: #0b0d11; color: #cfe3ff; padding: 1rem; border-radius: 10px; overflow: auto }
62
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em }
63
+ :not(pre) > code { background: rgba(127,127,127,.18); padding: .1em .35em; border-radius: 5px }
64
+ img { max-width: 100% } a { color: #0b67d6 }
65
+ blockquote { border-left: 3px solid #ccc; margin: 1rem 0; padding: .2rem 1rem; opacity: .8 }
66
+ table { border-collapse: collapse } td, th { border: 1px solid #ccc; padding: .4rem .7rem }
67
+ </style></head><body>${inner}</body></html>`;
68
+ }
69
+
70
+ export async function pagesRouter({ dir }) {
71
+ const express = (await import("express")).default;
72
+ const { marked } = await import("marked");
73
+ ensure(dir);
74
+ const r = express.Router();
75
+ r.get("/:slug", (req, res, next) => {
76
+ const slug = req.params.slug;
77
+ if (!isSafeSlug(slug)) return next(); // safe slug only — no traversal
78
+ const file = path.join(dir, slug + ".md");
79
+ if (!fs.existsSync(file)) return next();
80
+ const { meta, body } = parseFrontMatter(fs.readFileSync(file, "utf8"));
81
+ res.type("html").send(shell(meta.title || slug, marked.parse(body)));
82
+ });
83
+ return r;
84
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "description": "Markdown pages: drop .md files in pages/ and each is served as HTML at /<slug>. No database, no admin — author them in your editor or with AI.",
3
+ "dependsOn": [],
4
+ "sentinel": "lib/pages.js",
5
+ "install": ["marked"],
6
+ "optional": {},
7
+ "wiring": "Add markdown files to the pages/ directory (created automatically on first run). pages/about.md is served at /about; front-matter `title:` sets the page title."
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-volt",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "description": "Scaffold a new Volt app — no-build, signals-based UI with Socket.io hot reload.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -254,6 +254,7 @@ function bindNodeHole(comment, value) {
254
254
  // ---------------------------------------------------------------------------
255
255
 
256
256
  (function startHotReload() {
257
+ if (typeof window === "undefined") return; // not a browser (SSR / Node imports / tests)
257
258
  const connect = () => {
258
259
  if (!window.io) return false;
259
260
  const socket = window.io();
@@ -21,8 +21,8 @@ const ENV_PATH = path.join(__dirname, ".env");
21
21
  const PKG_PATH = path.join(__dirname, "package.json");
22
22
  const ADDONS_DIR = path.join(__dirname, ".volt", "addons"); // bundled add-on sources
23
23
  const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
24
- const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1" };
25
- const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js" };
24
+ const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1", marked: "^18.0.5" };
25
+ const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js", pages: "pages.js" };
26
26
 
27
27
  // --- tiny .env loader (no dependency); never overrides an existing env var ---
28
28
  function readEnvFile() {
@@ -111,6 +111,9 @@ async function startApp() {
111
111
 
112
112
  app.get("/", (_req, res) => res.sendFile(path.join(__dirname, "views", "index.html")));
113
113
 
114
+ // markdown pages (/<slug> ← pages/<slug>.md) — mounted last, so app routes win
115
+ if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
116
+
114
117
  const server = http.createServer(app);
115
118
  const io = new SocketServer(server);
116
119
  if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
@@ -156,6 +159,7 @@ function neededPackages(env) {
156
159
  if (driver === "mysql") want.push("mysql2");
157
160
  if (driver === "postgres") want.push("pg");
158
161
  if (/^\s*SMTP_URL\s*=\s*\S/m.test(env)) want.push("nodemailer");
162
+ if (/^\s*VOLT_ADDONS\s*=.*\bpages\b/m.test(env)) want.push("marked");
159
163
  return want.filter((p) => !deps[p]);
160
164
  }
161
165
 
@@ -254,6 +254,7 @@ function bindNodeHole(comment, value) {
254
254
  // ---------------------------------------------------------------------------
255
255
 
256
256
  (function startHotReload() {
257
+ if (typeof window === "undefined") return; // not a browser (SSR / Node imports / tests)
257
258
  const connect = () => {
258
259
  if (!window.io) return false;
259
260
  const socket = window.io();
@@ -254,6 +254,7 @@ function bindNodeHole(comment, value) {
254
254
  // ---------------------------------------------------------------------------
255
255
 
256
256
  (function startHotReload() {
257
+ if (typeof window === "undefined") return; // not a browser (SSR / Node imports / tests)
257
258
  const connect = () => {
258
259
  if (!window.io) return false;
259
260
  const socket = window.io();
@@ -22,8 +22,8 @@ const ENV_PATH = path.join(__dirname, ".env");
22
22
  const PKG_PATH = path.join(__dirname, "package.json");
23
23
  const ADDONS_DIR = path.join(__dirname, ".volt", "addons"); // bundled add-on sources
24
24
  const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
25
- const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1" };
26
- const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js" };
25
+ const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1", marked: "^18.0.5" };
26
+ const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js", pages: "pages.js" };
27
27
 
28
28
  // --- tiny .env loader (no dependency); never overrides an existing env var ---
29
29
  function readEnvFile() {
@@ -137,6 +137,9 @@ async function startApp() {
137
137
 
138
138
  app.get("/", (_req, res) => res.sendFile(path.join(__dirname, "views", "index.html")));
139
139
 
140
+ // markdown pages (/<slug> ← pages/<slug>.md) — mounted last, so app routes win
141
+ if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
142
+
140
143
  const server = http.createServer(app);
141
144
  const io = new SocketServer(server);
142
145
  if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
@@ -182,6 +185,7 @@ function neededPackages(env) {
182
185
  if (driver === "mysql") want.push("mysql2");
183
186
  if (driver === "postgres") want.push("pg");
184
187
  if (/^\s*SMTP_URL\s*=\s*\S/m.test(env)) want.push("nodemailer");
188
+ if (/^\s*VOLT_ADDONS\s*=.*\bpages\b/m.test(env)) want.push("marked");
185
189
  return want.filter((p) => !deps[p]);
186
190
  }
187
191