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 +29 -0
- package/addons/pages/files/lib/pages.js +84 -0
- package/addons/pages/meta.json +8 -0
- package/package.json +1 -1
- package/templates/default/public/volt.js +1 -0
- package/templates/default/server.js +6 -2
- package/templates/guestbook/public/volt.js +1 -0
- package/templates/starter/public/volt.js +1 -0
- package/templates/starter/server.js +6 -2
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) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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
|
@@ -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
|
|