create-volt 0.39.1 → 0.41.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 +22 -0
- package/addons/pages/files/lib/pages.js +16 -7
- package/package.json +1 -1
- package/templates/blog/ecosystem.config.cjs +5 -0
- package/templates/blog/package.json +5 -1
- package/templates/blog/server.js +52 -27
- package/templates/default/ecosystem.config.cjs +5 -0
- package/templates/default/package.json +5 -1
- package/templates/default/server.js +52 -27
- package/templates/docs/ecosystem.config.cjs +5 -0
- package/templates/docs/package.json +5 -1
- package/templates/docs/server.js +52 -27
- package/templates/guestbook/ecosystem.config.cjs +5 -0
- package/templates/guestbook/package.json +5 -1
- package/templates/starter/ecosystem.config.cjs +5 -0
- package/templates/starter/package.json +5 -1
- package/templates/starter/server.js +53 -28
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ 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.41.0] - 2026-06-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **PM2 support.** Scaffolds ship `ecosystem.config.cjs` + scripts: `npm run pm2`
|
|
11
|
+
(start under pm2 — fetched via npx if not installed, or uses your global pm2),
|
|
12
|
+
`pm2:restart` (clean reload, no port clash), `pm2:logs`, `pm2:stop`.
|
|
13
|
+
- **`npm run dev` on an already-running app reloads it instead of crashing.** A
|
|
14
|
+
second start detects the in-use port, pings the running instance's new
|
|
15
|
+
`/__volt/reload` route to refresh browsers, prints a note, and exits 0 — no
|
|
16
|
+
more `EADDRINUSE` stack trace.
|
|
17
|
+
|
|
18
|
+
## [0.40.0] - 2026-06-30
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Themed front page.** `pages/index.md` now takes over `/` (rendered in your
|
|
22
|
+
theme), so a content site's home matches the rest of the site instead of
|
|
23
|
+
showing the demo `views/index.html`. With no `pages/index.md`, `/` stays the
|
|
24
|
+
app's index.html. (Answers: "I chose a theme but the home page was unchanged" —
|
|
25
|
+
the theme styles pages/posts; the home needs to *be* a page.)
|
|
26
|
+
|
|
7
27
|
## [0.39.1] - 2026-06-30
|
|
8
28
|
|
|
9
29
|
### Fixed
|
|
@@ -528,6 +548,8 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
528
548
|
watching and full-page hot reload. Supports `--skip-install` and `--force`,
|
|
529
549
|
and auto-detects npm / pnpm / yarn / bun for the install step.
|
|
530
550
|
|
|
551
|
+
[0.41.0]: https://github.com/MIR-2025/volt/releases/tag/v0.41.0
|
|
552
|
+
[0.40.0]: https://github.com/MIR-2025/volt/releases/tag/v0.40.0
|
|
531
553
|
[0.39.1]: https://github.com/MIR-2025/volt/releases/tag/v0.39.1
|
|
532
554
|
[0.39.0]: https://github.com/MIR-2025/volt/releases/tag/v0.39.0
|
|
533
555
|
[0.38.0]: https://github.com/MIR-2025/volt/releases/tag/v0.38.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/package.json
CHANGED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// PM2 process config. Start under pm2 with `npm run pm2` (pm2 is fetched via npx
|
|
2
|
+
// if you don't have it installed); reload cleanly with `npm run pm2:restart` (no
|
|
3
|
+
// port clash), tail logs with `npm run pm2:logs`, remove with `npm run pm2:stop`.
|
|
4
|
+
const { name } = require("./package.json");
|
|
5
|
+
module.exports = { apps: [{ name, script: "server.js" }] };
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
"main": "server.js",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"start": "node server.js",
|
|
10
|
-
"dev": "node server.js"
|
|
10
|
+
"dev": "node server.js",
|
|
11
|
+
"pm2": "npx --yes pm2 start ecosystem.config.cjs",
|
|
12
|
+
"pm2:restart": "npx --yes pm2 restart ecosystem.config.cjs",
|
|
13
|
+
"pm2:stop": "npx --yes pm2 delete ecosystem.config.cjs",
|
|
14
|
+
"pm2:logs": "npx --yes pm2 logs"
|
|
11
15
|
},
|
|
12
16
|
"dependencies": {
|
|
13
17
|
"express": "^4.22.2",
|
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,14 @@ 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
|
-
//
|
|
187
|
+
// Reload connected browsers on demand — used when a second `npm run dev` finds
|
|
188
|
+
// the app already running (see the EADDRINUSE handler below) instead of crashing.
|
|
189
|
+
app.get("/__volt/reload", (_req, res) => {
|
|
190
|
+
io.emit("volt:reload", { file: "__manual__" });
|
|
191
|
+
res.json({ ok: true });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// third-party add-ons â register(ctx). When auth is on, requireAuth/sessionFromReq
|
|
184
195
|
// are provided so add-ons can gate routes by login.
|
|
185
196
|
let requireAuth = null;
|
|
186
197
|
let sessionFromReq = null;
|
|
@@ -196,7 +207,7 @@ async function startApp() {
|
|
|
196
207
|
if (typeof register === "function") {
|
|
197
208
|
await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
|
|
198
209
|
} else {
|
|
199
|
-
console.warn(`[volt] add-on "${name}" not found or missing a register() export
|
|
210
|
+
console.warn(`[volt] add-on "${name}" not found or missing a register() export â skipped`);
|
|
200
211
|
}
|
|
201
212
|
}
|
|
202
213
|
|
|
@@ -204,7 +215,7 @@ async function startApp() {
|
|
|
204
215
|
const onChange = (file) => {
|
|
205
216
|
clearTimeout(timer);
|
|
206
217
|
timer = setTimeout(() => {
|
|
207
|
-
console.log(`[volt] change: ${file ?? "?"}
|
|
218
|
+
console.log(`[volt] change: ${file ?? "?"} â reload`);
|
|
208
219
|
io.emit("volt:reload", { file });
|
|
209
220
|
}, 80);
|
|
210
221
|
};
|
|
@@ -233,7 +244,21 @@ async function startApp() {
|
|
|
233
244
|
}
|
|
234
245
|
|
|
235
246
|
const on = [...enabled];
|
|
236
|
-
|
|
247
|
+
// If the port's taken, the app is likely already running — reload it (tell the
|
|
248
|
+
// running instance to refresh browsers) and exit, instead of an EADDRINUSE crash.
|
|
249
|
+
server.on("error", async (e) => {
|
|
250
|
+
if (e.code === "EADDRINUSE") {
|
|
251
|
+
try {
|
|
252
|
+
await fetch(`http://localhost:${PORT}/__volt/reload`);
|
|
253
|
+
} catch {
|
|
254
|
+
/* old instance without the reload route, or not ours */
|
|
255
|
+
}
|
|
256
|
+
console.log(`\n[volt] already running at http://localhost:${PORT} — reloaded it. (Stop that process, or use pm2, to restart.)`);
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
throw e;
|
|
260
|
+
});
|
|
261
|
+
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
237
262
|
}
|
|
238
263
|
|
|
239
264
|
// Packages an .env's selections need, beyond what package.json already has.
|
|
@@ -257,7 +282,7 @@ function neededPackages(env) {
|
|
|
257
282
|
function ensureDriverInstalled(driver) {
|
|
258
283
|
const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
|
|
259
284
|
if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
|
|
260
|
-
console.log(`[volt] installing ${pkg} for the connection test
|
|
285
|
+
console.log(`[volt] installing ${pkg} for the connection testâ¦`);
|
|
261
286
|
spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
262
287
|
}
|
|
263
288
|
|
|
@@ -380,15 +405,15 @@ function startSetup() {
|
|
|
380
405
|
server.closeIdleConnections?.();
|
|
381
406
|
};
|
|
382
407
|
if (added.length) {
|
|
383
|
-
console.log(`[volt] installing ${added.join(", ")}
|
|
408
|
+
console.log(`[volt] installing ${added.join(", ")}â¦`);
|
|
384
409
|
const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
385
410
|
npm.on("error", () => handoff());
|
|
386
411
|
npm.on("close", () => {
|
|
387
|
-
console.log("[volt] saved .env
|
|
412
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
388
413
|
handoff();
|
|
389
414
|
});
|
|
390
415
|
} else {
|
|
391
|
-
console.log("[volt] saved .env
|
|
416
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
392
417
|
handoff();
|
|
393
418
|
}
|
|
394
419
|
});
|
|
@@ -406,18 +431,18 @@ function startSetup() {
|
|
|
406
431
|
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
432
|
server.listen(PORT, "127.0.0.1", () => {
|
|
408
433
|
const url = `http://localhost:${PORT}`;
|
|
409
|
-
console.log(`\n
|
|
434
|
+
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
410
435
|
console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
|
|
411
436
|
const ssh = process.env.SSH_CONNECTION;
|
|
412
437
|
if (ssh) {
|
|
413
438
|
const host = ssh.split(" ")[2];
|
|
414
439
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
415
|
-
console.log(" Remote box
|
|
440
|
+
console.log(" Remote box â the server is up here; bridge it from your LOCAL machine:");
|
|
416
441
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
417
|
-
console.log(`
|
|
442
|
+
console.log(` â¦then open ${url} on your machine (the tunnel points it here).`);
|
|
418
443
|
}
|
|
419
444
|
console.log("");
|
|
420
|
-
if (openBrowser(url)) console.log(" (opening your browser
|
|
445
|
+
if (openBrowser(url)) console.log(" (opening your browserâ¦)\n");
|
|
421
446
|
});
|
|
422
447
|
}
|
|
423
448
|
|
|
@@ -425,8 +450,8 @@ function readEnvFileLines() {
|
|
|
425
450
|
return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
|
|
426
451
|
}
|
|
427
452
|
|
|
428
|
-
// --- Studio: an ephemeral, localhost-only data browser (
|
|
429
|
-
// Not a route in the running app
|
|
453
|
+
// --- Studio: an ephemeral, localhost-only data browser (ÃÂ la Prisma Studio).
|
|
454
|
+
// Not a route in the running app â it only exists while you run `--studio`, on
|
|
430
455
|
// loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
|
|
431
456
|
const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
|
|
432
457
|
async function startStudio() {
|
|
@@ -439,7 +464,7 @@ async function startStudio() {
|
|
|
439
464
|
try {
|
|
440
465
|
store = await (await addonMod("db")).createStore();
|
|
441
466
|
} catch (e) {
|
|
442
|
-
console.error("Studio: couldn't connect the store
|
|
467
|
+
console.error("Studio: couldn't connect the store â " + e.message);
|
|
443
468
|
process.exit(1);
|
|
444
469
|
}
|
|
445
470
|
const PORT = configPort();
|
|
@@ -497,15 +522,15 @@ async function startStudio() {
|
|
|
497
522
|
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
523
|
server.listen(PORT, "127.0.0.1", () => {
|
|
499
524
|
const url = `http://localhost:${PORT}`;
|
|
500
|
-
console.log(`\n
|
|
501
|
-
console.log(" Browse your data. localhost-only, disposable
|
|
525
|
+
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
526
|
+
console.log(" Browse your data. localhost-only, disposable â Ctrl-C when done.");
|
|
502
527
|
const ssh = process.env.SSH_CONNECTION;
|
|
503
528
|
if (ssh) {
|
|
504
529
|
const host = ssh.split(" ")[2];
|
|
505
530
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
506
|
-
console.log(" Remote box
|
|
531
|
+
console.log(" Remote box â from your LOCAL machine:");
|
|
507
532
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
508
|
-
console.log(`
|
|
533
|
+
console.log(` â¦then open ${url}.`);
|
|
509
534
|
}
|
|
510
535
|
console.log("");
|
|
511
536
|
openBrowser(url);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// PM2 process config. Start under pm2 with `npm run pm2` (pm2 is fetched via npx
|
|
2
|
+
// if you don't have it installed); reload cleanly with `npm run pm2:restart` (no
|
|
3
|
+
// port clash), tail logs with `npm run pm2:logs`, remove with `npm run pm2:stop`.
|
|
4
|
+
const { name } = require("./package.json");
|
|
5
|
+
module.exports = { apps: [{ name, script: "server.js" }] };
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
"main": "server.js",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"start": "node server.js",
|
|
10
|
-
"dev": "node server.js"
|
|
10
|
+
"dev": "node server.js",
|
|
11
|
+
"pm2": "npx --yes pm2 start ecosystem.config.cjs",
|
|
12
|
+
"pm2:restart": "npx --yes pm2 restart ecosystem.config.cjs",
|
|
13
|
+
"pm2:stop": "npx --yes pm2 delete ecosystem.config.cjs",
|
|
14
|
+
"pm2:logs": "npx --yes pm2 logs"
|
|
11
15
|
},
|
|
12
16
|
"dependencies": {
|
|
13
17
|
"express": "^4.22.2",
|
|
@@ -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,14 @@ 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
|
-
//
|
|
187
|
+
// Reload connected browsers on demand — used when a second `npm run dev` finds
|
|
188
|
+
// the app already running (see the EADDRINUSE handler below) instead of crashing.
|
|
189
|
+
app.get("/__volt/reload", (_req, res) => {
|
|
190
|
+
io.emit("volt:reload", { file: "__manual__" });
|
|
191
|
+
res.json({ ok: true });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// third-party add-ons â register(ctx). When auth is on, requireAuth/sessionFromReq
|
|
184
195
|
// are provided so add-ons can gate routes by login.
|
|
185
196
|
let requireAuth = null;
|
|
186
197
|
let sessionFromReq = null;
|
|
@@ -196,7 +207,7 @@ async function startApp() {
|
|
|
196
207
|
if (typeof register === "function") {
|
|
197
208
|
await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
|
|
198
209
|
} else {
|
|
199
|
-
console.warn(`[volt] add-on "${name}" not found or missing a register() export
|
|
210
|
+
console.warn(`[volt] add-on "${name}" not found or missing a register() export â skipped`);
|
|
200
211
|
}
|
|
201
212
|
}
|
|
202
213
|
|
|
@@ -204,7 +215,7 @@ async function startApp() {
|
|
|
204
215
|
const onChange = (file) => {
|
|
205
216
|
clearTimeout(timer);
|
|
206
217
|
timer = setTimeout(() => {
|
|
207
|
-
console.log(`[volt] change: ${file ?? "?"}
|
|
218
|
+
console.log(`[volt] change: ${file ?? "?"} â reload`);
|
|
208
219
|
io.emit("volt:reload", { file });
|
|
209
220
|
}, 80);
|
|
210
221
|
};
|
|
@@ -233,7 +244,21 @@ async function startApp() {
|
|
|
233
244
|
}
|
|
234
245
|
|
|
235
246
|
const on = [...enabled];
|
|
236
|
-
|
|
247
|
+
// If the port's taken, the app is likely already running — reload it (tell the
|
|
248
|
+
// running instance to refresh browsers) and exit, instead of an EADDRINUSE crash.
|
|
249
|
+
server.on("error", async (e) => {
|
|
250
|
+
if (e.code === "EADDRINUSE") {
|
|
251
|
+
try {
|
|
252
|
+
await fetch(`http://localhost:${PORT}/__volt/reload`);
|
|
253
|
+
} catch {
|
|
254
|
+
/* old instance without the reload route, or not ours */
|
|
255
|
+
}
|
|
256
|
+
console.log(`\n[volt] already running at http://localhost:${PORT} — reloaded it. (Stop that process, or use pm2, to restart.)`);
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
throw e;
|
|
260
|
+
});
|
|
261
|
+
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
237
262
|
}
|
|
238
263
|
|
|
239
264
|
// Packages an .env's selections need, beyond what package.json already has.
|
|
@@ -257,7 +282,7 @@ function neededPackages(env) {
|
|
|
257
282
|
function ensureDriverInstalled(driver) {
|
|
258
283
|
const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
|
|
259
284
|
if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
|
|
260
|
-
console.log(`[volt] installing ${pkg} for the connection test
|
|
285
|
+
console.log(`[volt] installing ${pkg} for the connection testâ¦`);
|
|
261
286
|
spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
262
287
|
}
|
|
263
288
|
|
|
@@ -380,15 +405,15 @@ function startSetup() {
|
|
|
380
405
|
server.closeIdleConnections?.();
|
|
381
406
|
};
|
|
382
407
|
if (added.length) {
|
|
383
|
-
console.log(`[volt] installing ${added.join(", ")}
|
|
408
|
+
console.log(`[volt] installing ${added.join(", ")}â¦`);
|
|
384
409
|
const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
385
410
|
npm.on("error", () => handoff());
|
|
386
411
|
npm.on("close", () => {
|
|
387
|
-
console.log("[volt] saved .env
|
|
412
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
388
413
|
handoff();
|
|
389
414
|
});
|
|
390
415
|
} else {
|
|
391
|
-
console.log("[volt] saved .env
|
|
416
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
392
417
|
handoff();
|
|
393
418
|
}
|
|
394
419
|
});
|
|
@@ -406,18 +431,18 @@ function startSetup() {
|
|
|
406
431
|
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
432
|
server.listen(PORT, "127.0.0.1", () => {
|
|
408
433
|
const url = `http://localhost:${PORT}`;
|
|
409
|
-
console.log(`\n
|
|
434
|
+
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
410
435
|
console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
|
|
411
436
|
const ssh = process.env.SSH_CONNECTION;
|
|
412
437
|
if (ssh) {
|
|
413
438
|
const host = ssh.split(" ")[2];
|
|
414
439
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
415
|
-
console.log(" Remote box
|
|
440
|
+
console.log(" Remote box â the server is up here; bridge it from your LOCAL machine:");
|
|
416
441
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
417
|
-
console.log(`
|
|
442
|
+
console.log(` â¦then open ${url} on your machine (the tunnel points it here).`);
|
|
418
443
|
}
|
|
419
444
|
console.log("");
|
|
420
|
-
if (openBrowser(url)) console.log(" (opening your browser
|
|
445
|
+
if (openBrowser(url)) console.log(" (opening your browserâ¦)\n");
|
|
421
446
|
});
|
|
422
447
|
}
|
|
423
448
|
|
|
@@ -425,8 +450,8 @@ function readEnvFileLines() {
|
|
|
425
450
|
return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
|
|
426
451
|
}
|
|
427
452
|
|
|
428
|
-
// --- Studio: an ephemeral, localhost-only data browser (
|
|
429
|
-
// Not a route in the running app
|
|
453
|
+
// --- Studio: an ephemeral, localhost-only data browser (ÃÂ la Prisma Studio).
|
|
454
|
+
// Not a route in the running app â it only exists while you run `--studio`, on
|
|
430
455
|
// loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
|
|
431
456
|
const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
|
|
432
457
|
async function startStudio() {
|
|
@@ -439,7 +464,7 @@ async function startStudio() {
|
|
|
439
464
|
try {
|
|
440
465
|
store = await (await addonMod("db")).createStore();
|
|
441
466
|
} catch (e) {
|
|
442
|
-
console.error("Studio: couldn't connect the store
|
|
467
|
+
console.error("Studio: couldn't connect the store â " + e.message);
|
|
443
468
|
process.exit(1);
|
|
444
469
|
}
|
|
445
470
|
const PORT = configPort();
|
|
@@ -497,15 +522,15 @@ async function startStudio() {
|
|
|
497
522
|
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
523
|
server.listen(PORT, "127.0.0.1", () => {
|
|
499
524
|
const url = `http://localhost:${PORT}`;
|
|
500
|
-
console.log(`\n
|
|
501
|
-
console.log(" Browse your data. localhost-only, disposable
|
|
525
|
+
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
526
|
+
console.log(" Browse your data. localhost-only, disposable â Ctrl-C when done.");
|
|
502
527
|
const ssh = process.env.SSH_CONNECTION;
|
|
503
528
|
if (ssh) {
|
|
504
529
|
const host = ssh.split(" ")[2];
|
|
505
530
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
506
|
-
console.log(" Remote box
|
|
531
|
+
console.log(" Remote box â from your LOCAL machine:");
|
|
507
532
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
508
|
-
console.log(`
|
|
533
|
+
console.log(` â¦then open ${url}.`);
|
|
509
534
|
}
|
|
510
535
|
console.log("");
|
|
511
536
|
openBrowser(url);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// PM2 process config. Start under pm2 with `npm run pm2` (pm2 is fetched via npx
|
|
2
|
+
// if you don't have it installed); reload cleanly with `npm run pm2:restart` (no
|
|
3
|
+
// port clash), tail logs with `npm run pm2:logs`, remove with `npm run pm2:stop`.
|
|
4
|
+
const { name } = require("./package.json");
|
|
5
|
+
module.exports = { apps: [{ name, script: "server.js" }] };
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
"main": "server.js",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"start": "node server.js",
|
|
10
|
-
"dev": "node server.js"
|
|
10
|
+
"dev": "node server.js",
|
|
11
|
+
"pm2": "npx --yes pm2 start ecosystem.config.cjs",
|
|
12
|
+
"pm2:restart": "npx --yes pm2 restart ecosystem.config.cjs",
|
|
13
|
+
"pm2:stop": "npx --yes pm2 delete ecosystem.config.cjs",
|
|
14
|
+
"pm2:logs": "npx --yes pm2 logs"
|
|
11
15
|
},
|
|
12
16
|
"dependencies": {
|
|
13
17
|
"express": "^4.22.2",
|
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,14 @@ 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
|
-
//
|
|
187
|
+
// Reload connected browsers on demand — used when a second `npm run dev` finds
|
|
188
|
+
// the app already running (see the EADDRINUSE handler below) instead of crashing.
|
|
189
|
+
app.get("/__volt/reload", (_req, res) => {
|
|
190
|
+
io.emit("volt:reload", { file: "__manual__" });
|
|
191
|
+
res.json({ ok: true });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// third-party add-ons â register(ctx). When auth is on, requireAuth/sessionFromReq
|
|
184
195
|
// are provided so add-ons can gate routes by login.
|
|
185
196
|
let requireAuth = null;
|
|
186
197
|
let sessionFromReq = null;
|
|
@@ -196,7 +207,7 @@ async function startApp() {
|
|
|
196
207
|
if (typeof register === "function") {
|
|
197
208
|
await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
|
|
198
209
|
} else {
|
|
199
|
-
console.warn(`[volt] add-on "${name}" not found or missing a register() export
|
|
210
|
+
console.warn(`[volt] add-on "${name}" not found or missing a register() export â skipped`);
|
|
200
211
|
}
|
|
201
212
|
}
|
|
202
213
|
|
|
@@ -204,7 +215,7 @@ async function startApp() {
|
|
|
204
215
|
const onChange = (file) => {
|
|
205
216
|
clearTimeout(timer);
|
|
206
217
|
timer = setTimeout(() => {
|
|
207
|
-
console.log(`[volt] change: ${file ?? "?"}
|
|
218
|
+
console.log(`[volt] change: ${file ?? "?"} â reload`);
|
|
208
219
|
io.emit("volt:reload", { file });
|
|
209
220
|
}, 80);
|
|
210
221
|
};
|
|
@@ -233,7 +244,21 @@ async function startApp() {
|
|
|
233
244
|
}
|
|
234
245
|
|
|
235
246
|
const on = [...enabled];
|
|
236
|
-
|
|
247
|
+
// If the port's taken, the app is likely already running — reload it (tell the
|
|
248
|
+
// running instance to refresh browsers) and exit, instead of an EADDRINUSE crash.
|
|
249
|
+
server.on("error", async (e) => {
|
|
250
|
+
if (e.code === "EADDRINUSE") {
|
|
251
|
+
try {
|
|
252
|
+
await fetch(`http://localhost:${PORT}/__volt/reload`);
|
|
253
|
+
} catch {
|
|
254
|
+
/* old instance without the reload route, or not ours */
|
|
255
|
+
}
|
|
256
|
+
console.log(`\n[volt] already running at http://localhost:${PORT} — reloaded it. (Stop that process, or use pm2, to restart.)`);
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
throw e;
|
|
260
|
+
});
|
|
261
|
+
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
237
262
|
}
|
|
238
263
|
|
|
239
264
|
// Packages an .env's selections need, beyond what package.json already has.
|
|
@@ -257,7 +282,7 @@ function neededPackages(env) {
|
|
|
257
282
|
function ensureDriverInstalled(driver) {
|
|
258
283
|
const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
|
|
259
284
|
if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
|
|
260
|
-
console.log(`[volt] installing ${pkg} for the connection test
|
|
285
|
+
console.log(`[volt] installing ${pkg} for the connection testâ¦`);
|
|
261
286
|
spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
262
287
|
}
|
|
263
288
|
|
|
@@ -380,15 +405,15 @@ function startSetup() {
|
|
|
380
405
|
server.closeIdleConnections?.();
|
|
381
406
|
};
|
|
382
407
|
if (added.length) {
|
|
383
|
-
console.log(`[volt] installing ${added.join(", ")}
|
|
408
|
+
console.log(`[volt] installing ${added.join(", ")}â¦`);
|
|
384
409
|
const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
385
410
|
npm.on("error", () => handoff());
|
|
386
411
|
npm.on("close", () => {
|
|
387
|
-
console.log("[volt] saved .env
|
|
412
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
388
413
|
handoff();
|
|
389
414
|
});
|
|
390
415
|
} else {
|
|
391
|
-
console.log("[volt] saved .env
|
|
416
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
392
417
|
handoff();
|
|
393
418
|
}
|
|
394
419
|
});
|
|
@@ -406,18 +431,18 @@ function startSetup() {
|
|
|
406
431
|
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
432
|
server.listen(PORT, "127.0.0.1", () => {
|
|
408
433
|
const url = `http://localhost:${PORT}`;
|
|
409
|
-
console.log(`\n
|
|
434
|
+
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
410
435
|
console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
|
|
411
436
|
const ssh = process.env.SSH_CONNECTION;
|
|
412
437
|
if (ssh) {
|
|
413
438
|
const host = ssh.split(" ")[2];
|
|
414
439
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
415
|
-
console.log(" Remote box
|
|
440
|
+
console.log(" Remote box â the server is up here; bridge it from your LOCAL machine:");
|
|
416
441
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
417
|
-
console.log(`
|
|
442
|
+
console.log(` â¦then open ${url} on your machine (the tunnel points it here).`);
|
|
418
443
|
}
|
|
419
444
|
console.log("");
|
|
420
|
-
if (openBrowser(url)) console.log(" (opening your browser
|
|
445
|
+
if (openBrowser(url)) console.log(" (opening your browserâ¦)\n");
|
|
421
446
|
});
|
|
422
447
|
}
|
|
423
448
|
|
|
@@ -425,8 +450,8 @@ function readEnvFileLines() {
|
|
|
425
450
|
return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
|
|
426
451
|
}
|
|
427
452
|
|
|
428
|
-
// --- Studio: an ephemeral, localhost-only data browser (
|
|
429
|
-
// Not a route in the running app
|
|
453
|
+
// --- Studio: an ephemeral, localhost-only data browser (ÃÂ la Prisma Studio).
|
|
454
|
+
// Not a route in the running app â it only exists while you run `--studio`, on
|
|
430
455
|
// loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
|
|
431
456
|
const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
|
|
432
457
|
async function startStudio() {
|
|
@@ -439,7 +464,7 @@ async function startStudio() {
|
|
|
439
464
|
try {
|
|
440
465
|
store = await (await addonMod("db")).createStore();
|
|
441
466
|
} catch (e) {
|
|
442
|
-
console.error("Studio: couldn't connect the store
|
|
467
|
+
console.error("Studio: couldn't connect the store â " + e.message);
|
|
443
468
|
process.exit(1);
|
|
444
469
|
}
|
|
445
470
|
const PORT = configPort();
|
|
@@ -497,15 +522,15 @@ async function startStudio() {
|
|
|
497
522
|
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
523
|
server.listen(PORT, "127.0.0.1", () => {
|
|
499
524
|
const url = `http://localhost:${PORT}`;
|
|
500
|
-
console.log(`\n
|
|
501
|
-
console.log(" Browse your data. localhost-only, disposable
|
|
525
|
+
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
526
|
+
console.log(" Browse your data. localhost-only, disposable â Ctrl-C when done.");
|
|
502
527
|
const ssh = process.env.SSH_CONNECTION;
|
|
503
528
|
if (ssh) {
|
|
504
529
|
const host = ssh.split(" ")[2];
|
|
505
530
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
506
|
-
console.log(" Remote box
|
|
531
|
+
console.log(" Remote box â from your LOCAL machine:");
|
|
507
532
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
508
|
-
console.log(`
|
|
533
|
+
console.log(` â¦then open ${url}.`);
|
|
509
534
|
}
|
|
510
535
|
console.log("");
|
|
511
536
|
openBrowser(url);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// PM2 process config. Start under pm2 with `npm run pm2` (pm2 is fetched via npx
|
|
2
|
+
// if you don't have it installed); reload cleanly with `npm run pm2:restart` (no
|
|
3
|
+
// port clash), tail logs with `npm run pm2:logs`, remove with `npm run pm2:stop`.
|
|
4
|
+
const { name } = require("./package.json");
|
|
5
|
+
module.exports = { apps: [{ name, script: "server.js" }] };
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
"main": "server.js",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"start": "node server.js",
|
|
10
|
-
"dev": "node server.js"
|
|
10
|
+
"dev": "node server.js",
|
|
11
|
+
"pm2": "npx --yes pm2 start ecosystem.config.cjs",
|
|
12
|
+
"pm2:restart": "npx --yes pm2 restart ecosystem.config.cjs",
|
|
13
|
+
"pm2:stop": "npx --yes pm2 delete ecosystem.config.cjs",
|
|
14
|
+
"pm2:logs": "npx --yes pm2 logs"
|
|
11
15
|
},
|
|
12
16
|
"dependencies": {
|
|
13
17
|
"express": "^4.22.2",
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// PM2 process config. Start under pm2 with `npm run pm2` (pm2 is fetched via npx
|
|
2
|
+
// if you don't have it installed); reload cleanly with `npm run pm2:restart` (no
|
|
3
|
+
// port clash), tail logs with `npm run pm2:logs`, remove with `npm run pm2:stop`.
|
|
4
|
+
const { name } = require("./package.json");
|
|
5
|
+
module.exports = { apps: [{ name, script: "server.js" }] };
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
"main": "server.js",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"start": "node server.js",
|
|
10
|
-
"dev": "node server.js"
|
|
10
|
+
"dev": "node server.js",
|
|
11
|
+
"pm2": "npx --yes pm2 start ecosystem.config.cjs",
|
|
12
|
+
"pm2:restart": "npx --yes pm2 restart ecosystem.config.cjs",
|
|
13
|
+
"pm2:stop": "npx --yes pm2 delete ecosystem.config.cjs",
|
|
14
|
+
"pm2:logs": "npx --yes pm2 logs"
|
|
11
15
|
},
|
|
12
16
|
"dependencies": {
|
|
13
17
|
"express": "^4.22.2",
|
|
@@ -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,14 @@ 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
|
-
//
|
|
213
|
+
// Reload connected browsers on demand — used when a second `npm run dev` finds
|
|
214
|
+
// the app already running (see the EADDRINUSE handler below) instead of crashing.
|
|
215
|
+
app.get("/__volt/reload", (_req, res) => {
|
|
216
|
+
io.emit("volt:reload", { file: "__manual__" });
|
|
217
|
+
res.json({ ok: true });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// third-party add-ons â register(ctx). When auth is on, requireAuth/sessionFromReq
|
|
210
221
|
// are provided so add-ons can gate routes by login.
|
|
211
222
|
let requireAuth = null;
|
|
212
223
|
let sessionFromReq = null;
|
|
@@ -222,7 +233,7 @@ async function startApp() {
|
|
|
222
233
|
if (typeof register === "function") {
|
|
223
234
|
await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
|
|
224
235
|
} else {
|
|
225
|
-
console.warn(`[volt] add-on "${name}" not found or missing a register() export
|
|
236
|
+
console.warn(`[volt] add-on "${name}" not found or missing a register() export â skipped`);
|
|
226
237
|
}
|
|
227
238
|
}
|
|
228
239
|
|
|
@@ -230,7 +241,7 @@ async function startApp() {
|
|
|
230
241
|
const onChange = (file) => {
|
|
231
242
|
clearTimeout(timer);
|
|
232
243
|
timer = setTimeout(() => {
|
|
233
|
-
console.log(`[volt] change: ${file ?? "?"}
|
|
244
|
+
console.log(`[volt] change: ${file ?? "?"} â reload`);
|
|
234
245
|
io.emit("volt:reload", { file });
|
|
235
246
|
}, 80);
|
|
236
247
|
};
|
|
@@ -259,7 +270,21 @@ async function startApp() {
|
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
const on = [...enabled];
|
|
262
|
-
|
|
273
|
+
// If the port's taken, the app is likely already running — reload it (tell the
|
|
274
|
+
// running instance to refresh browsers) and exit, instead of an EADDRINUSE crash.
|
|
275
|
+
server.on("error", async (e) => {
|
|
276
|
+
if (e.code === "EADDRINUSE") {
|
|
277
|
+
try {
|
|
278
|
+
await fetch(`http://localhost:${PORT}/__volt/reload`);
|
|
279
|
+
} catch {
|
|
280
|
+
/* old instance without the reload route, or not ours */
|
|
281
|
+
}
|
|
282
|
+
console.log(`\n[volt] already running at http://localhost:${PORT} — reloaded it. (Stop that process, or use pm2, to restart.)`);
|
|
283
|
+
process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
throw e;
|
|
286
|
+
});
|
|
287
|
+
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
263
288
|
}
|
|
264
289
|
|
|
265
290
|
// Packages an .env's selections need, beyond what package.json already has.
|
|
@@ -283,7 +308,7 @@ function neededPackages(env) {
|
|
|
283
308
|
function ensureDriverInstalled(driver) {
|
|
284
309
|
const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
|
|
285
310
|
if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
|
|
286
|
-
console.log(`[volt] installing ${pkg} for the connection test
|
|
311
|
+
console.log(`[volt] installing ${pkg} for the connection testâ¦`);
|
|
287
312
|
spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
288
313
|
}
|
|
289
314
|
|
|
@@ -406,15 +431,15 @@ function startSetup() {
|
|
|
406
431
|
server.closeIdleConnections?.();
|
|
407
432
|
};
|
|
408
433
|
if (added.length) {
|
|
409
|
-
console.log(`[volt] installing ${added.join(", ")}
|
|
434
|
+
console.log(`[volt] installing ${added.join(", ")}â¦`);
|
|
410
435
|
const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
411
436
|
npm.on("error", () => handoff());
|
|
412
437
|
npm.on("close", () => {
|
|
413
|
-
console.log("[volt] saved .env
|
|
438
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
414
439
|
handoff();
|
|
415
440
|
});
|
|
416
441
|
} else {
|
|
417
|
-
console.log("[volt] saved .env
|
|
442
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
418
443
|
handoff();
|
|
419
444
|
}
|
|
420
445
|
});
|
|
@@ -432,18 +457,18 @@ function startSetup() {
|
|
|
432
457
|
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
458
|
server.listen(PORT, "127.0.0.1", () => {
|
|
434
459
|
const url = `http://localhost:${PORT}`;
|
|
435
|
-
console.log(`\n
|
|
460
|
+
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
436
461
|
console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
|
|
437
462
|
const ssh = process.env.SSH_CONNECTION;
|
|
438
463
|
if (ssh) {
|
|
439
464
|
const host = ssh.split(" ")[2];
|
|
440
465
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
441
|
-
console.log(" Remote box
|
|
466
|
+
console.log(" Remote box â the server is up here; bridge it from your LOCAL machine:");
|
|
442
467
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
443
|
-
console.log(`
|
|
468
|
+
console.log(` â¦then open ${url} on your machine (the tunnel points it here).`);
|
|
444
469
|
}
|
|
445
470
|
console.log("");
|
|
446
|
-
if (openBrowser(url)) console.log(" (opening your browser
|
|
471
|
+
if (openBrowser(url)) console.log(" (opening your browserâ¦)\n");
|
|
447
472
|
});
|
|
448
473
|
}
|
|
449
474
|
|
|
@@ -451,8 +476,8 @@ function readEnvFileLines() {
|
|
|
451
476
|
return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
|
|
452
477
|
}
|
|
453
478
|
|
|
454
|
-
// --- Studio: an ephemeral, localhost-only data browser (
|
|
455
|
-
// Not a route in the running app
|
|
479
|
+
// --- Studio: an ephemeral, localhost-only data browser (ÃÂ la Prisma Studio).
|
|
480
|
+
// Not a route in the running app â it only exists while you run `--studio`, on
|
|
456
481
|
// loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
|
|
457
482
|
const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
|
|
458
483
|
async function startStudio() {
|
|
@@ -465,7 +490,7 @@ async function startStudio() {
|
|
|
465
490
|
try {
|
|
466
491
|
store = await (await addonMod("db")).createStore();
|
|
467
492
|
} catch (e) {
|
|
468
|
-
console.error("Studio: couldn't connect the store
|
|
493
|
+
console.error("Studio: couldn't connect the store â " + e.message);
|
|
469
494
|
process.exit(1);
|
|
470
495
|
}
|
|
471
496
|
const PORT = configPort();
|
|
@@ -523,15 +548,15 @@ async function startStudio() {
|
|
|
523
548
|
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
549
|
server.listen(PORT, "127.0.0.1", () => {
|
|
525
550
|
const url = `http://localhost:${PORT}`;
|
|
526
|
-
console.log(`\n
|
|
527
|
-
console.log(" Browse your data. localhost-only, disposable
|
|
551
|
+
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
552
|
+
console.log(" Browse your data. localhost-only, disposable â Ctrl-C when done.");
|
|
528
553
|
const ssh = process.env.SSH_CONNECTION;
|
|
529
554
|
if (ssh) {
|
|
530
555
|
const host = ssh.split(" ")[2];
|
|
531
556
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
532
|
-
console.log(" Remote box
|
|
557
|
+
console.log(" Remote box â from your LOCAL machine:");
|
|
533
558
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
534
|
-
console.log(`
|
|
559
|
+
console.log(` â¦then open ${url}.`);
|
|
535
560
|
}
|
|
536
561
|
console.log("");
|
|
537
562
|
openBrowser(url);
|