create-volt 0.39.0 → 0.40.0
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 +21 -0
- package/addons/pages/files/lib/pages.js +16 -7
- package/index.js +6 -2
- package/package.json +2 -1
- package/templates/blog/server.js +31 -27
- package/templates/default/server.js +31 -27
- package/templates/docs/server.js +31 -27
- package/templates/starter/server.js +32 -28
- package/themes/classic/index.js +28 -0
- package/themes/classic/meta.json +3 -0
- package/themes/midnight/index.js +27 -0
- package/themes/midnight/meta.json +3 -0
- package/themes/paper/index.js +26 -0
- package/themes/paper/meta.json +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ 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.40.0] - 2026-06-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Themed front page.** `pages/index.md` now takes over `/` (rendered in your
|
|
11
|
+
theme), so a content site's home matches the rest of the site instead of
|
|
12
|
+
showing the demo `views/index.html`. With no `pages/index.md`, `/` stays the
|
|
13
|
+
app's index.html. (Answers: "I chose a theme but the home page was unchanged" —
|
|
14
|
+
the theme styles pages/posts; the home needs to *be* a page.)
|
|
15
|
+
|
|
16
|
+
## [0.39.1] - 2026-06-30
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Scaffolding was broken in 0.37.0–0.39.0.** The bundled `themes/` dir is
|
|
20
|
+
copied into new apps, but it was never added to the package `files`, so it was
|
|
21
|
+
missing from the npm tarball and `npm create volt` crashed with
|
|
22
|
+
`ENOENT … create-volt/themes`. Added `themes` to `files`, and guarded the
|
|
23
|
+
bundled-dir copy (`addons`/`themes`) so a missing dir is skipped, never fatal.
|
|
24
|
+
Verified by scaffolding from the packed tarball.
|
|
25
|
+
|
|
7
26
|
## [0.39.0] - 2026-06-29
|
|
8
27
|
|
|
9
28
|
### Fixed
|
|
@@ -518,6 +537,8 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
518
537
|
watching and full-page hot reload. Supports `--skip-install` and `--force`,
|
|
519
538
|
and auto-detects npm / pnpm / yarn / bun for the install step.
|
|
520
539
|
|
|
540
|
+
[0.40.0]: https://github.com/MIR-2025/volt/releases/tag/v0.40.0
|
|
541
|
+
[0.39.1]: https://github.com/MIR-2025/volt/releases/tag/v0.39.1
|
|
521
542
|
[0.39.0]: https://github.com/MIR-2025/volt/releases/tag/v0.39.0
|
|
522
543
|
[0.38.0]: https://github.com/MIR-2025/volt/releases/tag/v0.38.0
|
|
523
544
|
[0.37.0]: https://github.com/MIR-2025/volt/releases/tag/v0.37.0
|
|
@@ -176,20 +176,29 @@ export async function pagesRouter({ dir }) {
|
|
|
176
176
|
const { marked } = await import("marked");
|
|
177
177
|
ensure(dir);
|
|
178
178
|
const getTheme = themeResolver(dir);
|
|
179
|
+
// render one markdown file into the theme. `format: html` pages (e.g. from the
|
|
180
|
+
// WYSIWYG editor) are served verbatim; everything else is rendered with marked.
|
|
181
|
+
const renderFile = async (file, fallbackTitle, res) => {
|
|
182
|
+
const { meta, body } = parseFrontMatter(fs.readFileSync(file, "utf8"));
|
|
183
|
+
const content = meta.format === "html" ? body : marked.parse(body);
|
|
184
|
+
const m = { ...meta, title: meta.title || fallbackTitle };
|
|
185
|
+
const { layout } = await getTheme();
|
|
186
|
+
res.type("html").send(injectHot(layout({ title: m.title, head: metaHead(m), content, meta: m })));
|
|
187
|
+
};
|
|
179
188
|
const r = express.Router();
|
|
180
189
|
r.get("/_theme.css", async (_req, res) => res.type("css").send((await getTheme()).css));
|
|
190
|
+
// themed home: pages/index.md takes over "/" (the site's front page) when present
|
|
191
|
+
r.get("/", async (_req, res, next) => {
|
|
192
|
+
const file = path.join(dir, "index.md");
|
|
193
|
+
if (!fs.existsSync(file)) return next();
|
|
194
|
+
await renderFile(file, "Home", res);
|
|
195
|
+
});
|
|
181
196
|
r.get("/:slug", async (req, res, next) => {
|
|
182
197
|
const slug = req.params.slug;
|
|
183
198
|
if (!isSafeSlug(slug)) return next(); // safe slug only — no traversal
|
|
184
199
|
const file = path.join(dir, slug + ".md");
|
|
185
200
|
if (!fs.existsSync(file)) return next();
|
|
186
|
-
|
|
187
|
-
// `format: html` pages (e.g. from the WYSIWYG editor) are served verbatim to
|
|
188
|
-
// preserve complex layouts; everything else is markdown rendered with marked.
|
|
189
|
-
const content = meta.format === "html" ? body : marked.parse(body);
|
|
190
|
-
const m = { ...meta, title: meta.title || slug };
|
|
191
|
-
const { layout } = await getTheme();
|
|
192
|
-
res.type("html").send(injectHot(layout({ title: m.title, head: metaHead(m), content, meta: m })));
|
|
201
|
+
await renderFile(file, slug, res);
|
|
193
202
|
});
|
|
194
203
|
return r;
|
|
195
204
|
}
|
package/index.js
CHANGED
|
@@ -465,8 +465,12 @@ if (fs.existsSync(shippedDockerignore)) {
|
|
|
465
465
|
// Bundle the add-on sources so the app's setup wizard can enable them later
|
|
466
466
|
// (only for templates that ship the wizard, i.e. have a setup/ dir).
|
|
467
467
|
if (fs.existsSync(path.join(targetDir, "setup"))) {
|
|
468
|
-
|
|
469
|
-
|
|
468
|
+
// copy each bundled dir into .volt/ — guard existsSync so a missing dir (e.g.
|
|
469
|
+
// an incomplete install) is skipped rather than crashing the scaffold.
|
|
470
|
+
for (const name of ["addons", "themes"]) {
|
|
471
|
+
const src = path.join(__dirname, name);
|
|
472
|
+
if (fs.existsSync(src)) fs.cpSync(src, path.join(targetDir, ".volt", name), { recursive: true });
|
|
473
|
+
}
|
|
470
474
|
}
|
|
471
475
|
|
|
472
476
|
// --- stamp the project name into package.json ---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-volt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
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": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"lib",
|
|
12
12
|
"templates",
|
|
13
13
|
"addons",
|
|
14
|
+
"themes",
|
|
14
15
|
"CHANGELOG.md"
|
|
15
16
|
],
|
|
16
17
|
"engines": {
|
package/templates/blog/server.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// server.js
|
|
1
|
+
// server.js â dev server with a built-in first-run setup wizard.
|
|
2
2
|
//
|
|
3
3
|
// First run (no .env) or `node server.js --edit` (-e) opens a disposable, local
|
|
4
4
|
// config page: tick add-ons, fill settings, Apply. Apply writes .env (a
|
|
5
5
|
// VOLT_ADDONS list + settings) and adds any needed packages to package.json,
|
|
6
|
-
// runs npm install, then starts the app
|
|
6
|
+
// runs npm install, then starts the app â which wires whatever .env enables.
|
|
7
7
|
// Add-on code is bundled under .volt/addons; nothing is copied into your code.
|
|
8
8
|
//
|
|
9
9
|
// No build step, no env-file flag: .env is auto-loaded below.
|
|
@@ -24,7 +24,7 @@ const THEMES_DIR = path.join(__dirname, ".volt", "themes"); // bundled themes th
|
|
|
24
24
|
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
25
25
|
const CONFIG_DEFAULT_PORT = 5050; // the --edit/--studio config UI's default port (its own, so it never clashes with a running app)
|
|
26
26
|
|
|
27
|
-
// `--port <n>` (or --port=<n>) overrides the listen port for this run
|
|
27
|
+
// `--port <n>` (or --port=<n>) overrides the listen port for this run â lets
|
|
28
28
|
// --edit/--studio dodge a port the running app already holds, and runs the app
|
|
29
29
|
// itself on a one-off port. Explicit flag wins over PORT in .env.
|
|
30
30
|
function cliPort() {
|
|
@@ -104,7 +104,7 @@ const imp = (rel) => import(pathToFileURL(path.join(__dirname, rel)).href);
|
|
|
104
104
|
const addonMod = (n) => imp(path.join(".volt", "addons", n, "files", "lib", LIB_FILE[n]));
|
|
105
105
|
|
|
106
106
|
// Built-in add-ons are wired explicitly below; everything else in VOLT_ADDONS is
|
|
107
|
-
// a third-party add-on
|
|
107
|
+
// a third-party add-on â a local .volt/addons/<name>/index.js or an installed
|
|
108
108
|
// npm package "volt-addon-<name>" exporting register(ctx). See /docs/plugins.
|
|
109
109
|
const BUILTINS = new Set(Object.keys(LIB_FILE));
|
|
110
110
|
async function loadAddon(name) {
|
|
@@ -128,7 +128,7 @@ function openBrowser(url) {
|
|
|
128
128
|
const args = plat === "win32" ? ["/c", "start", "", url] : [url];
|
|
129
129
|
try {
|
|
130
130
|
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
131
|
-
child.on("error", () => {}); // launcher missing
|
|
131
|
+
child.on("error", () => {}); // launcher missing â emits async, don't crash
|
|
132
132
|
child.unref();
|
|
133
133
|
return true;
|
|
134
134
|
} catch {
|
|
@@ -163,7 +163,11 @@ async function startApp() {
|
|
|
163
163
|
if (fs.existsSync(pub)) app.use(express.static(pub));
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
app.get("/", (_req, res) =>
|
|
166
|
+
app.get("/", (_req, res, next) => {
|
|
167
|
+
// a themed home page (pages/index.md) takes over "/" — else the app's index.html
|
|
168
|
+
if (enabled.has("pages") && fs.existsSync(path.join(__dirname, "pages", "index.md"))) return next();
|
|
169
|
+
res.sendFile(path.join(__dirname, "views", "index.html"));
|
|
170
|
+
});
|
|
167
171
|
|
|
168
172
|
// media uploads (POST /api/media, auth-gated; local files served at /media)
|
|
169
173
|
if (enabled.has("media") && store) {
|
|
@@ -171,8 +175,8 @@ async function startApp() {
|
|
|
171
175
|
app.use(await (await addonMod("media")).mediaRouter({ requireAuth, dir: path.join(__dirname, "media") }));
|
|
172
176
|
}
|
|
173
177
|
|
|
174
|
-
// markdown pages (/<slug>
|
|
175
|
-
// blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml)
|
|
178
|
+
// markdown pages (/<slug> â pages/<slug>.md) â mounted last, so app routes win
|
|
179
|
+
// blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) â before pages so /blog wins; renders in the same theme.
|
|
176
180
|
if (enabled.has("posts")) app.use(await (await addonMod("posts")).postsRouter({ dir: path.join(__dirname, "posts"), themeDir: path.join(__dirname, "pages") }));
|
|
177
181
|
if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
|
|
178
182
|
|
|
@@ -180,7 +184,7 @@ async function startApp() {
|
|
|
180
184
|
const io = new SocketServer(server);
|
|
181
185
|
if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
|
|
182
186
|
|
|
183
|
-
// third-party add-ons
|
|
187
|
+
// third-party add-ons â register(ctx). When auth is on, requireAuth/sessionFromReq
|
|
184
188
|
// are provided so add-ons can gate routes by login.
|
|
185
189
|
let requireAuth = null;
|
|
186
190
|
let sessionFromReq = null;
|
|
@@ -196,7 +200,7 @@ async function startApp() {
|
|
|
196
200
|
if (typeof register === "function") {
|
|
197
201
|
await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
|
|
198
202
|
} else {
|
|
199
|
-
console.warn(`[volt] add-on "${name}" not found or missing a register() export
|
|
203
|
+
console.warn(`[volt] add-on "${name}" not found or missing a register() export â skipped`);
|
|
200
204
|
}
|
|
201
205
|
}
|
|
202
206
|
|
|
@@ -204,7 +208,7 @@ async function startApp() {
|
|
|
204
208
|
const onChange = (file) => {
|
|
205
209
|
clearTimeout(timer);
|
|
206
210
|
timer = setTimeout(() => {
|
|
207
|
-
console.log(`[volt] change: ${file ?? "?"}
|
|
211
|
+
console.log(`[volt] change: ${file ?? "?"} â reload`);
|
|
208
212
|
io.emit("volt:reload", { file });
|
|
209
213
|
}, 80);
|
|
210
214
|
};
|
|
@@ -233,7 +237,7 @@ async function startApp() {
|
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
const on = [...enabled];
|
|
236
|
-
server.listen(PORT, () => console.log(
|
|
240
|
+
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
// Packages an .env's selections need, beyond what package.json already has.
|
|
@@ -257,7 +261,7 @@ function neededPackages(env) {
|
|
|
257
261
|
function ensureDriverInstalled(driver) {
|
|
258
262
|
const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
|
|
259
263
|
if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
|
|
260
|
-
console.log(`[volt] installing ${pkg} for the connection test
|
|
264
|
+
console.log(`[volt] installing ${pkg} for the connection testâ¦`);
|
|
261
265
|
spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
262
266
|
}
|
|
263
267
|
|
|
@@ -380,15 +384,15 @@ function startSetup() {
|
|
|
380
384
|
server.closeIdleConnections?.();
|
|
381
385
|
};
|
|
382
386
|
if (added.length) {
|
|
383
|
-
console.log(`[volt] installing ${added.join(", ")}
|
|
387
|
+
console.log(`[volt] installing ${added.join(", ")}â¦`);
|
|
384
388
|
const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
385
389
|
npm.on("error", () => handoff());
|
|
386
390
|
npm.on("close", () => {
|
|
387
|
-
console.log("[volt] saved .env
|
|
391
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
388
392
|
handoff();
|
|
389
393
|
});
|
|
390
394
|
} else {
|
|
391
|
-
console.log("[volt] saved .env
|
|
395
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
392
396
|
handoff();
|
|
393
397
|
}
|
|
394
398
|
});
|
|
@@ -406,18 +410,18 @@ function startSetup() {
|
|
|
406
410
|
server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
|
|
407
411
|
server.listen(PORT, "127.0.0.1", () => {
|
|
408
412
|
const url = `http://localhost:${PORT}`;
|
|
409
|
-
console.log(`\n
|
|
413
|
+
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
410
414
|
console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
|
|
411
415
|
const ssh = process.env.SSH_CONNECTION;
|
|
412
416
|
if (ssh) {
|
|
413
417
|
const host = ssh.split(" ")[2];
|
|
414
418
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
415
|
-
console.log(" Remote box
|
|
419
|
+
console.log(" Remote box â the server is up here; bridge it from your LOCAL machine:");
|
|
416
420
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
417
|
-
console.log(`
|
|
421
|
+
console.log(` â¦then open ${url} on your machine (the tunnel points it here).`);
|
|
418
422
|
}
|
|
419
423
|
console.log("");
|
|
420
|
-
if (openBrowser(url)) console.log(" (opening your browser
|
|
424
|
+
if (openBrowser(url)) console.log(" (opening your browserâ¦)\n");
|
|
421
425
|
});
|
|
422
426
|
}
|
|
423
427
|
|
|
@@ -425,8 +429,8 @@ function readEnvFileLines() {
|
|
|
425
429
|
return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
|
|
426
430
|
}
|
|
427
431
|
|
|
428
|
-
// --- Studio: an ephemeral, localhost-only data browser (
|
|
429
|
-
// Not a route in the running app
|
|
432
|
+
// --- Studio: an ephemeral, localhost-only data browser (ÃÂ la Prisma Studio).
|
|
433
|
+
// Not a route in the running app â it only exists while you run `--studio`, on
|
|
430
434
|
// loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
|
|
431
435
|
const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
|
|
432
436
|
async function startStudio() {
|
|
@@ -439,7 +443,7 @@ async function startStudio() {
|
|
|
439
443
|
try {
|
|
440
444
|
store = await (await addonMod("db")).createStore();
|
|
441
445
|
} catch (e) {
|
|
442
|
-
console.error("Studio: couldn't connect the store
|
|
446
|
+
console.error("Studio: couldn't connect the store â " + e.message);
|
|
443
447
|
process.exit(1);
|
|
444
448
|
}
|
|
445
449
|
const PORT = configPort();
|
|
@@ -497,15 +501,15 @@ async function startStudio() {
|
|
|
497
501
|
server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
|
|
498
502
|
server.listen(PORT, "127.0.0.1", () => {
|
|
499
503
|
const url = `http://localhost:${PORT}`;
|
|
500
|
-
console.log(`\n
|
|
501
|
-
console.log(" Browse your data. localhost-only, disposable
|
|
504
|
+
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
505
|
+
console.log(" Browse your data. localhost-only, disposable â Ctrl-C when done.");
|
|
502
506
|
const ssh = process.env.SSH_CONNECTION;
|
|
503
507
|
if (ssh) {
|
|
504
508
|
const host = ssh.split(" ")[2];
|
|
505
509
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
506
|
-
console.log(" Remote box
|
|
510
|
+
console.log(" Remote box â from your LOCAL machine:");
|
|
507
511
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
508
|
-
console.log(`
|
|
512
|
+
console.log(` â¦then open ${url}.`);
|
|
509
513
|
}
|
|
510
514
|
console.log("");
|
|
511
515
|
openBrowser(url);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// server.js
|
|
1
|
+
// server.js â dev server with a built-in first-run setup wizard.
|
|
2
2
|
//
|
|
3
3
|
// First run (no .env) or `node server.js --edit` (-e) opens a disposable, local
|
|
4
4
|
// config page: tick add-ons, fill settings, Apply. Apply writes .env (a
|
|
5
5
|
// VOLT_ADDONS list + settings) and adds any needed packages to package.json,
|
|
6
|
-
// runs npm install, then starts the app
|
|
6
|
+
// runs npm install, then starts the app â which wires whatever .env enables.
|
|
7
7
|
// Add-on code is bundled under .volt/addons; nothing is copied into your code.
|
|
8
8
|
//
|
|
9
9
|
// No build step, no env-file flag: .env is auto-loaded below.
|
|
@@ -24,7 +24,7 @@ const THEMES_DIR = path.join(__dirname, ".volt", "themes"); // bundled themes th
|
|
|
24
24
|
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
25
25
|
const CONFIG_DEFAULT_PORT = 5050; // the --edit/--studio config UI's default port (its own, so it never clashes with a running app)
|
|
26
26
|
|
|
27
|
-
// `--port <n>` (or --port=<n>) overrides the listen port for this run
|
|
27
|
+
// `--port <n>` (or --port=<n>) overrides the listen port for this run â lets
|
|
28
28
|
// --edit/--studio dodge a port the running app already holds, and runs the app
|
|
29
29
|
// itself on a one-off port. Explicit flag wins over PORT in .env.
|
|
30
30
|
function cliPort() {
|
|
@@ -104,7 +104,7 @@ const imp = (rel) => import(pathToFileURL(path.join(__dirname, rel)).href);
|
|
|
104
104
|
const addonMod = (n) => imp(path.join(".volt", "addons", n, "files", "lib", LIB_FILE[n]));
|
|
105
105
|
|
|
106
106
|
// Built-in add-ons are wired explicitly below; everything else in VOLT_ADDONS is
|
|
107
|
-
// a third-party add-on
|
|
107
|
+
// a third-party add-on â a local .volt/addons/<name>/index.js or an installed
|
|
108
108
|
// npm package "volt-addon-<name>" exporting register(ctx). See /docs/plugins.
|
|
109
109
|
const BUILTINS = new Set(Object.keys(LIB_FILE));
|
|
110
110
|
async function loadAddon(name) {
|
|
@@ -128,7 +128,7 @@ function openBrowser(url) {
|
|
|
128
128
|
const args = plat === "win32" ? ["/c", "start", "", url] : [url];
|
|
129
129
|
try {
|
|
130
130
|
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
131
|
-
child.on("error", () => {}); // launcher missing
|
|
131
|
+
child.on("error", () => {}); // launcher missing â emits async, don't crash
|
|
132
132
|
child.unref();
|
|
133
133
|
return true;
|
|
134
134
|
} catch {
|
|
@@ -163,7 +163,11 @@ async function startApp() {
|
|
|
163
163
|
if (fs.existsSync(pub)) app.use(express.static(pub));
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
app.get("/", (_req, res) =>
|
|
166
|
+
app.get("/", (_req, res, next) => {
|
|
167
|
+
// a themed home page (pages/index.md) takes over "/" — else the app's index.html
|
|
168
|
+
if (enabled.has("pages") && fs.existsSync(path.join(__dirname, "pages", "index.md"))) return next();
|
|
169
|
+
res.sendFile(path.join(__dirname, "views", "index.html"));
|
|
170
|
+
});
|
|
167
171
|
|
|
168
172
|
// media uploads (POST /api/media, auth-gated; local files served at /media)
|
|
169
173
|
if (enabled.has("media") && store) {
|
|
@@ -171,8 +175,8 @@ async function startApp() {
|
|
|
171
175
|
app.use(await (await addonMod("media")).mediaRouter({ requireAuth, dir: path.join(__dirname, "media") }));
|
|
172
176
|
}
|
|
173
177
|
|
|
174
|
-
// markdown pages (/<slug>
|
|
175
|
-
// blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml)
|
|
178
|
+
// markdown pages (/<slug> â pages/<slug>.md) â mounted last, so app routes win
|
|
179
|
+
// blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) â before pages so /blog wins; renders in the same theme.
|
|
176
180
|
if (enabled.has("posts")) app.use(await (await addonMod("posts")).postsRouter({ dir: path.join(__dirname, "posts"), themeDir: path.join(__dirname, "pages") }));
|
|
177
181
|
if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
|
|
178
182
|
|
|
@@ -180,7 +184,7 @@ async function startApp() {
|
|
|
180
184
|
const io = new SocketServer(server);
|
|
181
185
|
if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
|
|
182
186
|
|
|
183
|
-
// third-party add-ons
|
|
187
|
+
// third-party add-ons â register(ctx). When auth is on, requireAuth/sessionFromReq
|
|
184
188
|
// are provided so add-ons can gate routes by login.
|
|
185
189
|
let requireAuth = null;
|
|
186
190
|
let sessionFromReq = null;
|
|
@@ -196,7 +200,7 @@ async function startApp() {
|
|
|
196
200
|
if (typeof register === "function") {
|
|
197
201
|
await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
|
|
198
202
|
} else {
|
|
199
|
-
console.warn(`[volt] add-on "${name}" not found or missing a register() export
|
|
203
|
+
console.warn(`[volt] add-on "${name}" not found or missing a register() export â skipped`);
|
|
200
204
|
}
|
|
201
205
|
}
|
|
202
206
|
|
|
@@ -204,7 +208,7 @@ async function startApp() {
|
|
|
204
208
|
const onChange = (file) => {
|
|
205
209
|
clearTimeout(timer);
|
|
206
210
|
timer = setTimeout(() => {
|
|
207
|
-
console.log(`[volt] change: ${file ?? "?"}
|
|
211
|
+
console.log(`[volt] change: ${file ?? "?"} â reload`);
|
|
208
212
|
io.emit("volt:reload", { file });
|
|
209
213
|
}, 80);
|
|
210
214
|
};
|
|
@@ -233,7 +237,7 @@ async function startApp() {
|
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
const on = [...enabled];
|
|
236
|
-
server.listen(PORT, () => console.log(
|
|
240
|
+
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
// Packages an .env's selections need, beyond what package.json already has.
|
|
@@ -257,7 +261,7 @@ function neededPackages(env) {
|
|
|
257
261
|
function ensureDriverInstalled(driver) {
|
|
258
262
|
const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
|
|
259
263
|
if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
|
|
260
|
-
console.log(`[volt] installing ${pkg} for the connection test
|
|
264
|
+
console.log(`[volt] installing ${pkg} for the connection testâ¦`);
|
|
261
265
|
spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
262
266
|
}
|
|
263
267
|
|
|
@@ -380,15 +384,15 @@ function startSetup() {
|
|
|
380
384
|
server.closeIdleConnections?.();
|
|
381
385
|
};
|
|
382
386
|
if (added.length) {
|
|
383
|
-
console.log(`[volt] installing ${added.join(", ")}
|
|
387
|
+
console.log(`[volt] installing ${added.join(", ")}â¦`);
|
|
384
388
|
const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
385
389
|
npm.on("error", () => handoff());
|
|
386
390
|
npm.on("close", () => {
|
|
387
|
-
console.log("[volt] saved .env
|
|
391
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
388
392
|
handoff();
|
|
389
393
|
});
|
|
390
394
|
} else {
|
|
391
|
-
console.log("[volt] saved .env
|
|
395
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
392
396
|
handoff();
|
|
393
397
|
}
|
|
394
398
|
});
|
|
@@ -406,18 +410,18 @@ function startSetup() {
|
|
|
406
410
|
server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
|
|
407
411
|
server.listen(PORT, "127.0.0.1", () => {
|
|
408
412
|
const url = `http://localhost:${PORT}`;
|
|
409
|
-
console.log(`\n
|
|
413
|
+
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
410
414
|
console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
|
|
411
415
|
const ssh = process.env.SSH_CONNECTION;
|
|
412
416
|
if (ssh) {
|
|
413
417
|
const host = ssh.split(" ")[2];
|
|
414
418
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
415
|
-
console.log(" Remote box
|
|
419
|
+
console.log(" Remote box â the server is up here; bridge it from your LOCAL machine:");
|
|
416
420
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
417
|
-
console.log(`
|
|
421
|
+
console.log(` â¦then open ${url} on your machine (the tunnel points it here).`);
|
|
418
422
|
}
|
|
419
423
|
console.log("");
|
|
420
|
-
if (openBrowser(url)) console.log(" (opening your browser
|
|
424
|
+
if (openBrowser(url)) console.log(" (opening your browserâ¦)\n");
|
|
421
425
|
});
|
|
422
426
|
}
|
|
423
427
|
|
|
@@ -425,8 +429,8 @@ function readEnvFileLines() {
|
|
|
425
429
|
return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
|
|
426
430
|
}
|
|
427
431
|
|
|
428
|
-
// --- Studio: an ephemeral, localhost-only data browser (
|
|
429
|
-
// Not a route in the running app
|
|
432
|
+
// --- Studio: an ephemeral, localhost-only data browser (ÃÂ la Prisma Studio).
|
|
433
|
+
// Not a route in the running app â it only exists while you run `--studio`, on
|
|
430
434
|
// loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
|
|
431
435
|
const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
|
|
432
436
|
async function startStudio() {
|
|
@@ -439,7 +443,7 @@ async function startStudio() {
|
|
|
439
443
|
try {
|
|
440
444
|
store = await (await addonMod("db")).createStore();
|
|
441
445
|
} catch (e) {
|
|
442
|
-
console.error("Studio: couldn't connect the store
|
|
446
|
+
console.error("Studio: couldn't connect the store â " + e.message);
|
|
443
447
|
process.exit(1);
|
|
444
448
|
}
|
|
445
449
|
const PORT = configPort();
|
|
@@ -497,15 +501,15 @@ async function startStudio() {
|
|
|
497
501
|
server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
|
|
498
502
|
server.listen(PORT, "127.0.0.1", () => {
|
|
499
503
|
const url = `http://localhost:${PORT}`;
|
|
500
|
-
console.log(`\n
|
|
501
|
-
console.log(" Browse your data. localhost-only, disposable
|
|
504
|
+
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
505
|
+
console.log(" Browse your data. localhost-only, disposable â Ctrl-C when done.");
|
|
502
506
|
const ssh = process.env.SSH_CONNECTION;
|
|
503
507
|
if (ssh) {
|
|
504
508
|
const host = ssh.split(" ")[2];
|
|
505
509
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
506
|
-
console.log(" Remote box
|
|
510
|
+
console.log(" Remote box â from your LOCAL machine:");
|
|
507
511
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
508
|
-
console.log(`
|
|
512
|
+
console.log(` â¦then open ${url}.`);
|
|
509
513
|
}
|
|
510
514
|
console.log("");
|
|
511
515
|
openBrowser(url);
|
package/templates/docs/server.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// server.js
|
|
1
|
+
// server.js â dev server with a built-in first-run setup wizard.
|
|
2
2
|
//
|
|
3
3
|
// First run (no .env) or `node server.js --edit` (-e) opens a disposable, local
|
|
4
4
|
// config page: tick add-ons, fill settings, Apply. Apply writes .env (a
|
|
5
5
|
// VOLT_ADDONS list + settings) and adds any needed packages to package.json,
|
|
6
|
-
// runs npm install, then starts the app
|
|
6
|
+
// runs npm install, then starts the app â which wires whatever .env enables.
|
|
7
7
|
// Add-on code is bundled under .volt/addons; nothing is copied into your code.
|
|
8
8
|
//
|
|
9
9
|
// No build step, no env-file flag: .env is auto-loaded below.
|
|
@@ -24,7 +24,7 @@ const THEMES_DIR = path.join(__dirname, ".volt", "themes"); // bundled themes th
|
|
|
24
24
|
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
25
25
|
const CONFIG_DEFAULT_PORT = 5050; // the --edit/--studio config UI's default port (its own, so it never clashes with a running app)
|
|
26
26
|
|
|
27
|
-
// `--port <n>` (or --port=<n>) overrides the listen port for this run
|
|
27
|
+
// `--port <n>` (or --port=<n>) overrides the listen port for this run â lets
|
|
28
28
|
// --edit/--studio dodge a port the running app already holds, and runs the app
|
|
29
29
|
// itself on a one-off port. Explicit flag wins over PORT in .env.
|
|
30
30
|
function cliPort() {
|
|
@@ -104,7 +104,7 @@ const imp = (rel) => import(pathToFileURL(path.join(__dirname, rel)).href);
|
|
|
104
104
|
const addonMod = (n) => imp(path.join(".volt", "addons", n, "files", "lib", LIB_FILE[n]));
|
|
105
105
|
|
|
106
106
|
// Built-in add-ons are wired explicitly below; everything else in VOLT_ADDONS is
|
|
107
|
-
// a third-party add-on
|
|
107
|
+
// a third-party add-on â a local .volt/addons/<name>/index.js or an installed
|
|
108
108
|
// npm package "volt-addon-<name>" exporting register(ctx). See /docs/plugins.
|
|
109
109
|
const BUILTINS = new Set(Object.keys(LIB_FILE));
|
|
110
110
|
async function loadAddon(name) {
|
|
@@ -128,7 +128,7 @@ function openBrowser(url) {
|
|
|
128
128
|
const args = plat === "win32" ? ["/c", "start", "", url] : [url];
|
|
129
129
|
try {
|
|
130
130
|
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
131
|
-
child.on("error", () => {}); // launcher missing
|
|
131
|
+
child.on("error", () => {}); // launcher missing â emits async, don't crash
|
|
132
132
|
child.unref();
|
|
133
133
|
return true;
|
|
134
134
|
} catch {
|
|
@@ -163,7 +163,11 @@ async function startApp() {
|
|
|
163
163
|
if (fs.existsSync(pub)) app.use(express.static(pub));
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
app.get("/", (_req, res) =>
|
|
166
|
+
app.get("/", (_req, res, next) => {
|
|
167
|
+
// a themed home page (pages/index.md) takes over "/" — else the app's index.html
|
|
168
|
+
if (enabled.has("pages") && fs.existsSync(path.join(__dirname, "pages", "index.md"))) return next();
|
|
169
|
+
res.sendFile(path.join(__dirname, "views", "index.html"));
|
|
170
|
+
});
|
|
167
171
|
|
|
168
172
|
// media uploads (POST /api/media, auth-gated; local files served at /media)
|
|
169
173
|
if (enabled.has("media") && store) {
|
|
@@ -171,8 +175,8 @@ async function startApp() {
|
|
|
171
175
|
app.use(await (await addonMod("media")).mediaRouter({ requireAuth, dir: path.join(__dirname, "media") }));
|
|
172
176
|
}
|
|
173
177
|
|
|
174
|
-
// markdown pages (/<slug>
|
|
175
|
-
// blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml)
|
|
178
|
+
// markdown pages (/<slug> â pages/<slug>.md) â mounted last, so app routes win
|
|
179
|
+
// blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) â before pages so /blog wins; renders in the same theme.
|
|
176
180
|
if (enabled.has("posts")) app.use(await (await addonMod("posts")).postsRouter({ dir: path.join(__dirname, "posts"), themeDir: path.join(__dirname, "pages") }));
|
|
177
181
|
if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
|
|
178
182
|
|
|
@@ -180,7 +184,7 @@ async function startApp() {
|
|
|
180
184
|
const io = new SocketServer(server);
|
|
181
185
|
if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
|
|
182
186
|
|
|
183
|
-
// third-party add-ons
|
|
187
|
+
// third-party add-ons â register(ctx). When auth is on, requireAuth/sessionFromReq
|
|
184
188
|
// are provided so add-ons can gate routes by login.
|
|
185
189
|
let requireAuth = null;
|
|
186
190
|
let sessionFromReq = null;
|
|
@@ -196,7 +200,7 @@ async function startApp() {
|
|
|
196
200
|
if (typeof register === "function") {
|
|
197
201
|
await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
|
|
198
202
|
} else {
|
|
199
|
-
console.warn(`[volt] add-on "${name}" not found or missing a register() export
|
|
203
|
+
console.warn(`[volt] add-on "${name}" not found or missing a register() export â skipped`);
|
|
200
204
|
}
|
|
201
205
|
}
|
|
202
206
|
|
|
@@ -204,7 +208,7 @@ async function startApp() {
|
|
|
204
208
|
const onChange = (file) => {
|
|
205
209
|
clearTimeout(timer);
|
|
206
210
|
timer = setTimeout(() => {
|
|
207
|
-
console.log(`[volt] change: ${file ?? "?"}
|
|
211
|
+
console.log(`[volt] change: ${file ?? "?"} â reload`);
|
|
208
212
|
io.emit("volt:reload", { file });
|
|
209
213
|
}, 80);
|
|
210
214
|
};
|
|
@@ -233,7 +237,7 @@ async function startApp() {
|
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
const on = [...enabled];
|
|
236
|
-
server.listen(PORT, () => console.log(
|
|
240
|
+
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
// Packages an .env's selections need, beyond what package.json already has.
|
|
@@ -257,7 +261,7 @@ function neededPackages(env) {
|
|
|
257
261
|
function ensureDriverInstalled(driver) {
|
|
258
262
|
const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
|
|
259
263
|
if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
|
|
260
|
-
console.log(`[volt] installing ${pkg} for the connection test
|
|
264
|
+
console.log(`[volt] installing ${pkg} for the connection testâ¦`);
|
|
261
265
|
spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
262
266
|
}
|
|
263
267
|
|
|
@@ -380,15 +384,15 @@ function startSetup() {
|
|
|
380
384
|
server.closeIdleConnections?.();
|
|
381
385
|
};
|
|
382
386
|
if (added.length) {
|
|
383
|
-
console.log(`[volt] installing ${added.join(", ")}
|
|
387
|
+
console.log(`[volt] installing ${added.join(", ")}â¦`);
|
|
384
388
|
const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
385
389
|
npm.on("error", () => handoff());
|
|
386
390
|
npm.on("close", () => {
|
|
387
|
-
console.log("[volt] saved .env
|
|
391
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
388
392
|
handoff();
|
|
389
393
|
});
|
|
390
394
|
} else {
|
|
391
|
-
console.log("[volt] saved .env
|
|
395
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
392
396
|
handoff();
|
|
393
397
|
}
|
|
394
398
|
});
|
|
@@ -406,18 +410,18 @@ function startSetup() {
|
|
|
406
410
|
server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
|
|
407
411
|
server.listen(PORT, "127.0.0.1", () => {
|
|
408
412
|
const url = `http://localhost:${PORT}`;
|
|
409
|
-
console.log(`\n
|
|
413
|
+
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
410
414
|
console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
|
|
411
415
|
const ssh = process.env.SSH_CONNECTION;
|
|
412
416
|
if (ssh) {
|
|
413
417
|
const host = ssh.split(" ")[2];
|
|
414
418
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
415
|
-
console.log(" Remote box
|
|
419
|
+
console.log(" Remote box â the server is up here; bridge it from your LOCAL machine:");
|
|
416
420
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
417
|
-
console.log(`
|
|
421
|
+
console.log(` â¦then open ${url} on your machine (the tunnel points it here).`);
|
|
418
422
|
}
|
|
419
423
|
console.log("");
|
|
420
|
-
if (openBrowser(url)) console.log(" (opening your browser
|
|
424
|
+
if (openBrowser(url)) console.log(" (opening your browserâ¦)\n");
|
|
421
425
|
});
|
|
422
426
|
}
|
|
423
427
|
|
|
@@ -425,8 +429,8 @@ function readEnvFileLines() {
|
|
|
425
429
|
return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
|
|
426
430
|
}
|
|
427
431
|
|
|
428
|
-
// --- Studio: an ephemeral, localhost-only data browser (
|
|
429
|
-
// Not a route in the running app
|
|
432
|
+
// --- Studio: an ephemeral, localhost-only data browser (ÃÂ la Prisma Studio).
|
|
433
|
+
// Not a route in the running app â it only exists while you run `--studio`, on
|
|
430
434
|
// loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
|
|
431
435
|
const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
|
|
432
436
|
async function startStudio() {
|
|
@@ -439,7 +443,7 @@ async function startStudio() {
|
|
|
439
443
|
try {
|
|
440
444
|
store = await (await addonMod("db")).createStore();
|
|
441
445
|
} catch (e) {
|
|
442
|
-
console.error("Studio: couldn't connect the store
|
|
446
|
+
console.error("Studio: couldn't connect the store â " + e.message);
|
|
443
447
|
process.exit(1);
|
|
444
448
|
}
|
|
445
449
|
const PORT = configPort();
|
|
@@ -497,15 +501,15 @@ async function startStudio() {
|
|
|
497
501
|
server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
|
|
498
502
|
server.listen(PORT, "127.0.0.1", () => {
|
|
499
503
|
const url = `http://localhost:${PORT}`;
|
|
500
|
-
console.log(`\n
|
|
501
|
-
console.log(" Browse your data. localhost-only, disposable
|
|
504
|
+
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
505
|
+
console.log(" Browse your data. localhost-only, disposable â Ctrl-C when done.");
|
|
502
506
|
const ssh = process.env.SSH_CONNECTION;
|
|
503
507
|
if (ssh) {
|
|
504
508
|
const host = ssh.split(" ")[2];
|
|
505
509
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
506
|
-
console.log(" Remote box
|
|
510
|
+
console.log(" Remote box â from your LOCAL machine:");
|
|
507
511
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
508
|
-
console.log(`
|
|
512
|
+
console.log(` â¦then open ${url}.`);
|
|
509
513
|
}
|
|
510
514
|
console.log("");
|
|
511
515
|
openBrowser(url);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// server.js
|
|
1
|
+
// server.js â dev server with a built-in first-run setup wizard.
|
|
2
2
|
//
|
|
3
3
|
// First run (no .env) or `node server.js --edit` (-e) opens a disposable, local
|
|
4
4
|
// config page: tick add-ons, fill settings, Apply. Apply writes .env (a
|
|
5
5
|
// VOLT_ADDONS list + settings) and adds any needed packages to package.json,
|
|
6
|
-
// runs npm install, then starts the app
|
|
6
|
+
// runs npm install, then starts the app â which wires whatever .env enables.
|
|
7
7
|
// Add-on code is bundled under .volt/addons; nothing is copied into your code.
|
|
8
8
|
//
|
|
9
9
|
// No build step, no env-file flag: .env is auto-loaded below.
|
|
@@ -25,7 +25,7 @@ const THEMES_DIR = path.join(__dirname, ".volt", "themes"); // bundled themes th
|
|
|
25
25
|
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
26
26
|
const CONFIG_DEFAULT_PORT = 5050; // the --edit/--studio config UI's default port (its own, so it never clashes with a running app)
|
|
27
27
|
|
|
28
|
-
// `--port <n>` (or --port=<n>) overrides the listen port for this run
|
|
28
|
+
// `--port <n>` (or --port=<n>) overrides the listen port for this run â lets
|
|
29
29
|
// --edit/--studio dodge a port the running app already holds, and runs the app
|
|
30
30
|
// itself on a one-off port. Explicit flag wins over PORT in .env.
|
|
31
31
|
function cliPort() {
|
|
@@ -105,7 +105,7 @@ const imp = (rel) => import(pathToFileURL(path.join(__dirname, rel)).href);
|
|
|
105
105
|
const addonMod = (n) => imp(path.join(".volt", "addons", n, "files", "lib", LIB_FILE[n]));
|
|
106
106
|
|
|
107
107
|
// Built-in add-ons are wired explicitly below; everything else in VOLT_ADDONS is
|
|
108
|
-
// a third-party add-on
|
|
108
|
+
// a third-party add-on â a local .volt/addons/<name>/index.js or an installed
|
|
109
109
|
// npm package "volt-addon-<name>" exporting register(ctx). See /docs/plugins.
|
|
110
110
|
const BUILTINS = new Set(Object.keys(LIB_FILE));
|
|
111
111
|
async function loadAddon(name) {
|
|
@@ -129,7 +129,7 @@ function openBrowser(url) {
|
|
|
129
129
|
const args = plat === "win32" ? ["/c", "start", "", url] : [url];
|
|
130
130
|
try {
|
|
131
131
|
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
132
|
-
child.on("error", () => {}); // launcher missing
|
|
132
|
+
child.on("error", () => {}); // launcher missing â emits async, don't crash
|
|
133
133
|
child.unref();
|
|
134
134
|
return true;
|
|
135
135
|
} catch {
|
|
@@ -157,7 +157,7 @@ async function startApp() {
|
|
|
157
157
|
if (enabled.has("mailer")) mailer = await (await addonMod("mailer")).createMailer();
|
|
158
158
|
if (enabled.has("auth") && store && mailer) app.use((await addonMod("auth")).authRouter({ store, mailer }));
|
|
159
159
|
|
|
160
|
-
// notes
|
|
160
|
+
// notes â a per-user CRUD example (auth-gated, owner-scoped, db-backed)
|
|
161
161
|
if (enabled.has("db") && enabled.has("auth") && store) {
|
|
162
162
|
const guard = (await addonMod("auth")).requireAuth(store);
|
|
163
163
|
const notes = store.collection("notes");
|
|
@@ -189,7 +189,11 @@ async function startApp() {
|
|
|
189
189
|
if (fs.existsSync(pub)) app.use(express.static(pub));
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
app.get("/", (_req, res) =>
|
|
192
|
+
app.get("/", (_req, res, next) => {
|
|
193
|
+
// a themed home page (pages/index.md) takes over "/" — else the app's index.html
|
|
194
|
+
if (enabled.has("pages") && fs.existsSync(path.join(__dirname, "pages", "index.md"))) return next();
|
|
195
|
+
res.sendFile(path.join(__dirname, "views", "index.html"));
|
|
196
|
+
});
|
|
193
197
|
|
|
194
198
|
// media uploads (POST /api/media, auth-gated; local files served at /media)
|
|
195
199
|
if (enabled.has("media") && store) {
|
|
@@ -197,8 +201,8 @@ async function startApp() {
|
|
|
197
201
|
app.use(await (await addonMod("media")).mediaRouter({ requireAuth, dir: path.join(__dirname, "media") }));
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
// markdown pages (/<slug>
|
|
201
|
-
// blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml)
|
|
204
|
+
// markdown pages (/<slug> â pages/<slug>.md) â mounted last, so app routes win
|
|
205
|
+
// blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) â before pages so /blog wins; renders in the same theme.
|
|
202
206
|
if (enabled.has("posts")) app.use(await (await addonMod("posts")).postsRouter({ dir: path.join(__dirname, "posts"), themeDir: path.join(__dirname, "pages") }));
|
|
203
207
|
if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
|
|
204
208
|
|
|
@@ -206,7 +210,7 @@ async function startApp() {
|
|
|
206
210
|
const io = new SocketServer(server);
|
|
207
211
|
if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
|
|
208
212
|
|
|
209
|
-
// third-party add-ons
|
|
213
|
+
// third-party add-ons â register(ctx). When auth is on, requireAuth/sessionFromReq
|
|
210
214
|
// are provided so add-ons can gate routes by login.
|
|
211
215
|
let requireAuth = null;
|
|
212
216
|
let sessionFromReq = null;
|
|
@@ -222,7 +226,7 @@ async function startApp() {
|
|
|
222
226
|
if (typeof register === "function") {
|
|
223
227
|
await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
|
|
224
228
|
} else {
|
|
225
|
-
console.warn(`[volt] add-on "${name}" not found or missing a register() export
|
|
229
|
+
console.warn(`[volt] add-on "${name}" not found or missing a register() export â skipped`);
|
|
226
230
|
}
|
|
227
231
|
}
|
|
228
232
|
|
|
@@ -230,7 +234,7 @@ async function startApp() {
|
|
|
230
234
|
const onChange = (file) => {
|
|
231
235
|
clearTimeout(timer);
|
|
232
236
|
timer = setTimeout(() => {
|
|
233
|
-
console.log(`[volt] change: ${file ?? "?"}
|
|
237
|
+
console.log(`[volt] change: ${file ?? "?"} â reload`);
|
|
234
238
|
io.emit("volt:reload", { file });
|
|
235
239
|
}, 80);
|
|
236
240
|
};
|
|
@@ -259,7 +263,7 @@ async function startApp() {
|
|
|
259
263
|
}
|
|
260
264
|
|
|
261
265
|
const on = [...enabled];
|
|
262
|
-
server.listen(PORT, () => console.log(
|
|
266
|
+
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
// Packages an .env's selections need, beyond what package.json already has.
|
|
@@ -283,7 +287,7 @@ function neededPackages(env) {
|
|
|
283
287
|
function ensureDriverInstalled(driver) {
|
|
284
288
|
const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
|
|
285
289
|
if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
|
|
286
|
-
console.log(`[volt] installing ${pkg} for the connection test
|
|
290
|
+
console.log(`[volt] installing ${pkg} for the connection testâ¦`);
|
|
287
291
|
spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
288
292
|
}
|
|
289
293
|
|
|
@@ -406,15 +410,15 @@ function startSetup() {
|
|
|
406
410
|
server.closeIdleConnections?.();
|
|
407
411
|
};
|
|
408
412
|
if (added.length) {
|
|
409
|
-
console.log(`[volt] installing ${added.join(", ")}
|
|
413
|
+
console.log(`[volt] installing ${added.join(", ")}â¦`);
|
|
410
414
|
const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
411
415
|
npm.on("error", () => handoff());
|
|
412
416
|
npm.on("close", () => {
|
|
413
|
-
console.log("[volt] saved .env
|
|
417
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
414
418
|
handoff();
|
|
415
419
|
});
|
|
416
420
|
} else {
|
|
417
|
-
console.log("[volt] saved .env
|
|
421
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
418
422
|
handoff();
|
|
419
423
|
}
|
|
420
424
|
});
|
|
@@ -432,18 +436,18 @@ function startSetup() {
|
|
|
432
436
|
server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
|
|
433
437
|
server.listen(PORT, "127.0.0.1", () => {
|
|
434
438
|
const url = `http://localhost:${PORT}`;
|
|
435
|
-
console.log(`\n
|
|
439
|
+
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
436
440
|
console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
|
|
437
441
|
const ssh = process.env.SSH_CONNECTION;
|
|
438
442
|
if (ssh) {
|
|
439
443
|
const host = ssh.split(" ")[2];
|
|
440
444
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
441
|
-
console.log(" Remote box
|
|
445
|
+
console.log(" Remote box â the server is up here; bridge it from your LOCAL machine:");
|
|
442
446
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
443
|
-
console.log(`
|
|
447
|
+
console.log(` â¦then open ${url} on your machine (the tunnel points it here).`);
|
|
444
448
|
}
|
|
445
449
|
console.log("");
|
|
446
|
-
if (openBrowser(url)) console.log(" (opening your browser
|
|
450
|
+
if (openBrowser(url)) console.log(" (opening your browserâ¦)\n");
|
|
447
451
|
});
|
|
448
452
|
}
|
|
449
453
|
|
|
@@ -451,8 +455,8 @@ function readEnvFileLines() {
|
|
|
451
455
|
return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
|
|
452
456
|
}
|
|
453
457
|
|
|
454
|
-
// --- Studio: an ephemeral, localhost-only data browser (
|
|
455
|
-
// Not a route in the running app
|
|
458
|
+
// --- Studio: an ephemeral, localhost-only data browser (ÃÂ la Prisma Studio).
|
|
459
|
+
// Not a route in the running app â it only exists while you run `--studio`, on
|
|
456
460
|
// loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
|
|
457
461
|
const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
|
|
458
462
|
async function startStudio() {
|
|
@@ -465,7 +469,7 @@ async function startStudio() {
|
|
|
465
469
|
try {
|
|
466
470
|
store = await (await addonMod("db")).createStore();
|
|
467
471
|
} catch (e) {
|
|
468
|
-
console.error("Studio: couldn't connect the store
|
|
472
|
+
console.error("Studio: couldn't connect the store â " + e.message);
|
|
469
473
|
process.exit(1);
|
|
470
474
|
}
|
|
471
475
|
const PORT = configPort();
|
|
@@ -523,15 +527,15 @@ async function startStudio() {
|
|
|
523
527
|
server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
|
|
524
528
|
server.listen(PORT, "127.0.0.1", () => {
|
|
525
529
|
const url = `http://localhost:${PORT}`;
|
|
526
|
-
console.log(`\n
|
|
527
|
-
console.log(" Browse your data. localhost-only, disposable
|
|
530
|
+
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
531
|
+
console.log(" Browse your data. localhost-only, disposable â Ctrl-C when done.");
|
|
528
532
|
const ssh = process.env.SSH_CONNECTION;
|
|
529
533
|
if (ssh) {
|
|
530
534
|
const host = ssh.split(" ")[2];
|
|
531
535
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
532
|
-
console.log(" Remote box
|
|
536
|
+
console.log(" Remote box â from your LOCAL machine:");
|
|
533
537
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
534
|
-
console.log(`
|
|
538
|
+
console.log(` â¦then open ${url}.`);
|
|
535
539
|
}
|
|
536
540
|
console.log("");
|
|
537
541
|
openBrowser(url);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// volt-theme-classic — a structured site theme: top nav bar + card content.
|
|
2
|
+
const NAME = process.env.SITE_NAME || "Home";
|
|
3
|
+
|
|
4
|
+
export const css = `:root{--ink:#1f2329;--bg:#f4f5f7;--card:#fff;--accent:#2557d6;--muted:#5b6573;--line:#e2e5ea}
|
|
5
|
+
*{box-sizing:border-box}
|
|
6
|
+
body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.7 system-ui,sans-serif}
|
|
7
|
+
header.site{background:var(--card);border-bottom:1px solid var(--line)}
|
|
8
|
+
header.site .bar{display:flex;align-items:center;gap:1rem;max-width:840px;margin:0 auto;padding:.9rem 1.2rem}
|
|
9
|
+
header.site a.brand{font-weight:800;color:var(--ink);text-decoration:none;font-size:1.15rem}
|
|
10
|
+
header.site nav{margin-left:auto}
|
|
11
|
+
header.site nav a{color:var(--muted);text-decoration:none;margin-left:1rem}
|
|
12
|
+
header.site nav a:hover{color:var(--accent)}
|
|
13
|
+
main .card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:2rem;max-width:840px;margin:2rem auto}
|
|
14
|
+
h1,h2,h3{line-height:1.25}
|
|
15
|
+
a{color:var(--accent)}
|
|
16
|
+
pre{background:#0b0d11;color:#cfe3ff;padding:1rem;border-radius:8px;overflow:auto}
|
|
17
|
+
:not(pre)>code{background:#eef1f5;padding:.1em .35em;border-radius:5px}
|
|
18
|
+
img{max-width:100%}
|
|
19
|
+
footer.site{text-align:center;color:var(--muted);font-size:.9rem;padding:2rem 1rem}`;
|
|
20
|
+
|
|
21
|
+
export function layout({ title, head, content }) {
|
|
22
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
23
|
+
<title>${title}</title>${head}<link rel="stylesheet" href="/_theme.css" /></head><body>
|
|
24
|
+
<header class="site"><div class="bar"><a class="brand" href="/">${NAME}</a><nav><a href="/">Home</a></nav></div></header>
|
|
25
|
+
<main><div class="card">${content}</div></main>
|
|
26
|
+
<footer class="site">${NAME} — built with Volt</footer>
|
|
27
|
+
</body></html>`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// volt-theme-midnight — a dark, modern, sans-serif theme.
|
|
2
|
+
const NAME = process.env.SITE_NAME || "Home";
|
|
3
|
+
|
|
4
|
+
export const css = `:root{--ink:#e6e8ee;--bg:#0e1116;--accent:#7c9cff;--muted:#9aa4b2;--line:#222831}
|
|
5
|
+
*{box-sizing:border-box}
|
|
6
|
+
body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.7 system-ui,-apple-system,sans-serif}
|
|
7
|
+
.wrap{max-width:760px;margin:0 auto;padding:0 1.2rem}
|
|
8
|
+
header.site{position:sticky;top:0;background:rgba(14,17,22,.85);backdrop-filter:blur(6px);border-bottom:1px solid var(--line);padding:.9rem 0}
|
|
9
|
+
header.site a{color:var(--accent);font-weight:800;text-decoration:none;letter-spacing:-.02em}
|
|
10
|
+
main{padding:2rem 0 3rem}
|
|
11
|
+
h1,h2,h3{line-height:1.2;letter-spacing:-.01em}
|
|
12
|
+
a{color:var(--accent)}
|
|
13
|
+
pre{background:#0a0d12;border:1px solid var(--line);padding:1rem;border-radius:10px;overflow:auto;color:#cfe3ff}
|
|
14
|
+
:not(pre)>code{background:rgba(124,156,255,.15);padding:.1em .35em;border-radius:5px;color:var(--accent)}
|
|
15
|
+
blockquote{border-left:3px solid var(--accent);margin:1.2rem 0;padding:.2rem 1rem;color:var(--muted)}
|
|
16
|
+
img{max-width:100%;border-radius:10px}
|
|
17
|
+
table{border-collapse:collapse;width:100%}td,th{border:1px solid var(--line);padding:.5rem .7rem}
|
|
18
|
+
footer.site{border-top:1px solid var(--line);margin-top:3rem;padding:1.5rem 0;color:var(--muted);font-size:.9rem}`;
|
|
19
|
+
|
|
20
|
+
export function layout({ title, head, content }) {
|
|
21
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
22
|
+
<title>${title}</title>${head}<link rel="stylesheet" href="/_theme.css" /></head><body>
|
|
23
|
+
<header class="site"><div class="wrap"><a href="/">${NAME}</a></div></header>
|
|
24
|
+
<main><div class="wrap">${content}</div></main>
|
|
25
|
+
<footer class="site"><div class="wrap">${NAME} — built with Volt</div></footer>
|
|
26
|
+
</body></html>`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// volt-theme-paper — a light, serif, reading-focused theme.
|
|
2
|
+
const NAME = process.env.SITE_NAME || "Home";
|
|
3
|
+
|
|
4
|
+
export const css = `:root{--ink:#222;--bg:#fbfaf7;--accent:#9a3b2e;--muted:#6b6b6b}
|
|
5
|
+
*{box-sizing:border-box}
|
|
6
|
+
body{margin:0;background:var(--bg);color:var(--ink);font:18px/1.75 Georgia,"Times New Roman",serif}
|
|
7
|
+
.wrap{max-width:680px;margin:0 auto;padding:0 1.2rem}
|
|
8
|
+
header.site{padding:2rem 0 1rem;border-bottom:1px solid #e7e3da}
|
|
9
|
+
header.site a{color:var(--ink);text-decoration:none;font-weight:700;font-size:1.3rem}
|
|
10
|
+
main{padding:2rem 0 3rem}
|
|
11
|
+
h1,h2,h3{font-weight:700;line-height:1.2}
|
|
12
|
+
a{color:var(--accent)}
|
|
13
|
+
pre{background:#f1ede4;padding:1rem;border-radius:6px;overflow:auto;font:14px/1.5 ui-monospace,monospace}
|
|
14
|
+
:not(pre)>code{background:#f1ede4;padding:.1em .35em;border-radius:4px;font-size:.9em}
|
|
15
|
+
blockquote{border-left:3px solid var(--accent);margin:1.5rem 0;padding:.2rem 1.2rem;color:var(--muted);font-style:italic}
|
|
16
|
+
img{max-width:100%;border-radius:6px}
|
|
17
|
+
footer.site{border-top:1px solid #e7e3da;margin-top:3rem;padding:1.5rem 0;color:var(--muted);font-size:.9rem}`;
|
|
18
|
+
|
|
19
|
+
export function layout({ title, head, content }) {
|
|
20
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
21
|
+
<title>${title}</title>${head}<link rel="stylesheet" href="/_theme.css" /></head><body>
|
|
22
|
+
<header class="site"><div class="wrap"><a href="/">${NAME}</a></div></header>
|
|
23
|
+
<main><div class="wrap">${content}</div></main>
|
|
24
|
+
<footer class="site"><div class="wrap">${NAME} — built with Volt</div></footer>
|
|
25
|
+
</body></html>`;
|
|
26
|
+
}
|