create-volt 0.15.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 +18 -0
- package/addons/pages/files/lib/pages.js +11 -6
- package/package.json +1 -1
- package/templates/default/public/volt.js +1 -0
- package/templates/default/server.js +1 -1
- package/templates/guestbook/public/volt.js +1 -0
- package/templates/starter/public/volt.js +1 -0
- package/templates/starter/server.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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
|
+
|
|
7
24
|
## [0.15.0] - 2026-06-28
|
|
8
25
|
|
|
9
26
|
### Added
|
|
@@ -213,6 +230,7 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
213
230
|
watching and full-page hot reload. Supports `--skip-install` and `--force`,
|
|
214
231
|
and auto-detects npm / pnpm / yarn / bun for the install step.
|
|
215
232
|
|
|
233
|
+
[0.15.1]: https://github.com/MIR-2025/volt/releases/tag/v0.15.1
|
|
216
234
|
[0.15.0]: https://github.com/MIR-2025/volt/releases/tag/v0.15.0
|
|
217
235
|
[0.14.0]: https://github.com/MIR-2025/volt/releases/tag/v0.14.0
|
|
218
236
|
[0.13.0]: https://github.com/MIR-2025/volt/releases/tag/v0.13.0
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Pages are code-owned files (trusted), so their markdown HTML is served as-is.
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// express + marked are imported lazily in pagesRouter() so this module's pure
|
|
7
|
+
// helpers (parseFrontMatter, isSafeSlug) load without those deps installed.
|
|
8
8
|
|
|
9
9
|
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
10
10
|
|
|
@@ -36,7 +36,7 @@ function ensure(dir) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const FM = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/;
|
|
39
|
-
function
|
|
39
|
+
export function parseFrontMatter(src) {
|
|
40
40
|
const m = src.match(FM);
|
|
41
41
|
if (!m) return { meta: {}, body: src };
|
|
42
42
|
const meta = {};
|
|
@@ -47,6 +47,9 @@ function parse(src) {
|
|
|
47
47
|
return { meta, body: src.slice(m[0].length) };
|
|
48
48
|
}
|
|
49
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
|
+
|
|
50
53
|
function shell(title, inner) {
|
|
51
54
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8" />
|
|
52
55
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
@@ -64,15 +67,17 @@ table { border-collapse: collapse } td, th { border: 1px solid #ccc; padding: .4
|
|
|
64
67
|
</style></head><body>${inner}</body></html>`;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
export function pagesRouter({ dir }) {
|
|
70
|
+
export async function pagesRouter({ dir }) {
|
|
71
|
+
const express = (await import("express")).default;
|
|
72
|
+
const { marked } = await import("marked");
|
|
68
73
|
ensure(dir);
|
|
69
74
|
const r = express.Router();
|
|
70
75
|
r.get("/:slug", (req, res, next) => {
|
|
71
76
|
const slug = req.params.slug;
|
|
72
|
-
if (
|
|
77
|
+
if (!isSafeSlug(slug)) return next(); // safe slug only — no traversal
|
|
73
78
|
const file = path.join(dir, slug + ".md");
|
|
74
79
|
if (!fs.existsSync(file)) return next();
|
|
75
|
-
const { meta, body } =
|
|
80
|
+
const { meta, body } = parseFrontMatter(fs.readFileSync(file, "utf8"));
|
|
76
81
|
res.type("html").send(shell(meta.title || slug, marked.parse(body)));
|
|
77
82
|
});
|
|
78
83
|
return r;
|
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();
|
|
@@ -112,7 +112,7 @@ async function startApp() {
|
|
|
112
112
|
app.get("/", (_req, res) => res.sendFile(path.join(__dirname, "views", "index.html")));
|
|
113
113
|
|
|
114
114
|
// markdown pages (/<slug> ← pages/<slug>.md) — mounted last, so app routes win
|
|
115
|
-
if (enabled.has("pages")) app.use((await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
|
|
115
|
+
if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
|
|
116
116
|
|
|
117
117
|
const server = http.createServer(app);
|
|
118
118
|
const io = new SocketServer(server);
|
|
@@ -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();
|
|
@@ -138,7 +138,7 @@ async function startApp() {
|
|
|
138
138
|
app.get("/", (_req, res) => res.sendFile(path.join(__dirname, "views", "index.html")));
|
|
139
139
|
|
|
140
140
|
// markdown pages (/<slug> ← pages/<slug>.md) — mounted last, so app routes win
|
|
141
|
-
if (enabled.has("pages")) app.use((await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
|
|
141
|
+
if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
|
|
142
142
|
|
|
143
143
|
const server = http.createServer(app);
|
|
144
144
|
const io = new SocketServer(server);
|