create-volt 0.34.0 → 0.36.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 +27 -0
- package/package.json +1 -1
- package/templates/blog/server.js +29 -4
- package/templates/blog/setup/setup.js +30 -2
- package/templates/default/server.js +29 -4
- package/templates/default/setup/setup.js +26 -2
- package/templates/docs/server.js +29 -4
- package/templates/docs/setup/setup.js +30 -2
- package/templates/starter/server.js +29 -4
- package/templates/starter/setup/setup.js +26 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ 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.36.0] - 2026-06-29
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`--port <n>` and `CONFIG_PORT`.** `--port` overrides the listen port for any
|
|
11
|
+
run; `CONFIG_PORT` in `.env` gives the `--edit`/`--studio` config UI its own
|
|
12
|
+
port, so it never collides with a running app. An in-use config port now prints
|
|
13
|
+
a clear hint instead of a raw `EADDRINUSE` stack.
|
|
14
|
+
- **Hot reload watches `pages/` and `posts/`.** Editing markdown content now
|
|
15
|
+
reloads the browser (content is read per request, so the edit shows). Theme
|
|
16
|
+
files (`_theme.js`) still need a restart — ES modules cache.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- The `blog` + `docs` templates had drifted from `default`s `server.js`/wizard and
|
|
20
|
+
silently missed recent fixes (including the hot-reload watcher). Re-synced, with
|
|
21
|
+
a test that fails if they drift again.
|
|
22
|
+
|
|
23
|
+
## [0.35.0] - 2026-06-29
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **Setup wizard: dependency add-ons now show as checked.** Enabling `auth` pulls
|
|
27
|
+
in `db` + `mailer` (its dependencies), which were added to `VOLT_ADDONS` but
|
|
28
|
+
whose checkboxes stayed *unchecked* — so the generated `.env` looked like it had
|
|
29
|
+
add-ons you never picked. Pulled-in dependencies now render **checked + disabled**
|
|
30
|
+
with a "required by <add-on>" note, so the checkboxes always match `VOLT_ADDONS`.
|
|
31
|
+
|
|
7
32
|
## [0.34.0] - 2026-06-29
|
|
8
33
|
|
|
9
34
|
### Added
|
|
@@ -454,6 +479,8 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
454
479
|
watching and full-page hot reload. Supports `--skip-install` and `--force`,
|
|
455
480
|
and auto-detects npm / pnpm / yarn / bun for the install step.
|
|
456
481
|
|
|
482
|
+
[0.36.0]: https://github.com/MIR-2025/volt/releases/tag/v0.36.0
|
|
483
|
+
[0.35.0]: https://github.com/MIR-2025/volt/releases/tag/v0.35.0
|
|
457
484
|
[0.34.0]: https://github.com/MIR-2025/volt/releases/tag/v0.34.0
|
|
458
485
|
[0.33.0]: https://github.com/MIR-2025/volt/releases/tag/v0.33.0
|
|
459
486
|
[0.32.0]: https://github.com/MIR-2025/volt/releases/tag/v0.32.0
|
package/package.json
CHANGED
package/templates/blog/server.js
CHANGED
|
@@ -21,6 +21,24 @@ const ENV_PATH = path.join(__dirname, ".env");
|
|
|
21
21
|
const PKG_PATH = path.join(__dirname, "package.json");
|
|
22
22
|
const ADDONS_DIR = path.join(__dirname, ".volt", "addons"); // bundled add-on sources
|
|
23
23
|
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
24
|
+
|
|
25
|
+
// `--port <n>` (or --port=<n>) overrides the listen port for this run — lets
|
|
26
|
+
// --edit/--studio dodge a port the running app already holds, and runs the app
|
|
27
|
+
// itself on a one-off port. Explicit flag wins over PORT in .env.
|
|
28
|
+
function cliPort() {
|
|
29
|
+
const i = process.argv.indexOf("--port");
|
|
30
|
+
const raw = i > -1 ? process.argv[i + 1] : (process.argv.find((a) => a.startsWith("--port=")) || "").split("=")[1];
|
|
31
|
+
const n = Number(raw);
|
|
32
|
+
return Number.isInteger(n) && n >= 1 && n <= 65535 ? n : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Port for the disposable config UI (--edit / --studio): --port wins, then
|
|
36
|
+
// CONFIG_PORT in .env (run it on its own port so it never clashes with the app),
|
|
37
|
+
// then the app's PORT, then the date-port.
|
|
38
|
+
function configPort() {
|
|
39
|
+
const env = readEnvFile(); // --edit runs before loadEnv(), so read the file too
|
|
40
|
+
return cliPort() || Number(process.env.CONFIG_PORT) || Number(env.CONFIG_PORT) || Number(process.env.PORT) || Number(env.PORT) || DEFAULT_PORT;
|
|
41
|
+
}
|
|
24
42
|
const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1", marked: "^18.0.5", busboy: "^1.6.0", "@aws-sdk/client-s3": "^3.1075.0" };
|
|
25
43
|
const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js", pages: "pages.js", posts: "posts.js", media: "media.js" };
|
|
26
44
|
|
|
@@ -101,7 +119,7 @@ function openBrowser(url) {
|
|
|
101
119
|
|
|
102
120
|
// --- the actual app: wires whichever add-ons .env enables ---
|
|
103
121
|
async function startApp() {
|
|
104
|
-
const PORT = Number(process.env.PORT) || DEFAULT_PORT;
|
|
122
|
+
const PORT = cliPort() || Number(process.env.PORT) || DEFAULT_PORT;
|
|
105
123
|
const enabled = enabledFrom(process.env);
|
|
106
124
|
const app = express();
|
|
107
125
|
app.disable("x-powered-by");
|
|
@@ -188,7 +206,12 @@ async function startApp() {
|
|
|
188
206
|
};
|
|
189
207
|
w(dir);
|
|
190
208
|
};
|
|
191
|
-
|
|
209
|
+
// watch content dirs too (pages/posts markdown is read per request, so a
|
|
210
|
+
// browser reload shows the edit); skip dirs that don't exist.
|
|
211
|
+
for (const d of ["views", "public", "pages", "posts"]) {
|
|
212
|
+
const full = path.join(__dirname, d);
|
|
213
|
+
if (fs.existsSync(full)) watchRecursive(full);
|
|
214
|
+
}
|
|
192
215
|
|
|
193
216
|
const on = [...enabled];
|
|
194
217
|
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
@@ -221,7 +244,7 @@ function ensureDriverInstalled(driver) {
|
|
|
221
244
|
|
|
222
245
|
// --- the disposable setup wizard (localhost only) ---
|
|
223
246
|
function startSetup() {
|
|
224
|
-
const PORT =
|
|
247
|
+
const PORT = configPort();
|
|
225
248
|
const assets = {
|
|
226
249
|
"/setup.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "setup", "setup.js"))],
|
|
227
250
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
@@ -341,6 +364,7 @@ function startSetup() {
|
|
|
341
364
|
res.end("not found");
|
|
342
365
|
});
|
|
343
366
|
|
|
367
|
+
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; });
|
|
344
368
|
server.listen(PORT, "127.0.0.1", () => {
|
|
345
369
|
const url = `http://localhost:${PORT}`;
|
|
346
370
|
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
@@ -379,7 +403,7 @@ async function startStudio() {
|
|
|
379
403
|
console.error("Studio: couldn't connect the store â " + e.message);
|
|
380
404
|
process.exit(1);
|
|
381
405
|
}
|
|
382
|
-
const PORT =
|
|
406
|
+
const PORT = configPort();
|
|
383
407
|
const visible = (n) => n && !HIDDEN_COLLECTIONS.has(n);
|
|
384
408
|
const assets = {
|
|
385
409
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
@@ -431,6 +455,7 @@ async function startStudio() {
|
|
|
431
455
|
}
|
|
432
456
|
});
|
|
433
457
|
|
|
458
|
+
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; });
|
|
434
459
|
server.listen(PORT, "127.0.0.1", () => {
|
|
435
460
|
const url = `http://localhost:${PORT}`;
|
|
436
461
|
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
@@ -25,6 +25,9 @@ const state = signal({
|
|
|
25
25
|
s3Secret: current.S3_SECRET || "",
|
|
26
26
|
s3PublicBase: current.S3_PUBLIC_BASE || "",
|
|
27
27
|
port: current.PORT || String(defaultPort),
|
|
28
|
+
// detect the admin's timezone from their browser (the wizard runs here), so
|
|
29
|
+
// dates render in their zone — not the server's (usually UTC on a host).
|
|
30
|
+
tz: current.SITE_TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || "",
|
|
28
31
|
});
|
|
29
32
|
const set = (patch) => state({ ...state(), ...patch });
|
|
30
33
|
const toggle = (n) => state({ ...state(), addons: { ...state().addons, [n]: !state().addons[n] } });
|
|
@@ -42,10 +45,28 @@ function effective(s) {
|
|
|
42
45
|
return order.filter((n) => want.has(n));
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
// which *enabled* add-ons pull in `name` as a (transitive) dependency
|
|
49
|
+
function requiredBy(s, name) {
|
|
50
|
+
const causes = [];
|
|
51
|
+
for (const n of order) {
|
|
52
|
+
if (n === name || !s.addons[n]) continue;
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const visit = (x) => {
|
|
55
|
+
if (seen.has(x)) return;
|
|
56
|
+
seen.add(x);
|
|
57
|
+
(depsOf[x] || []).forEach(visit);
|
|
58
|
+
};
|
|
59
|
+
(depsOf[n] || []).forEach(visit);
|
|
60
|
+
if (seen.has(name)) causes.push(n);
|
|
61
|
+
}
|
|
62
|
+
return causes;
|
|
63
|
+
}
|
|
64
|
+
|
|
45
65
|
const clean = (v) => String(v).replace(/[\r\n]/g, "").trim(); // one value per line; no injection
|
|
46
66
|
function genEnv(s) {
|
|
47
67
|
const eff = effective(s);
|
|
48
68
|
const out = [`VOLT_ADDONS=${eff.join(",")}`, `PORT=${clean(s.port)}`];
|
|
69
|
+
if (s.tz) out.push(`SITE_TZ=${clean(s.tz)}`); // admin's timezone, for date display
|
|
49
70
|
if (eff.includes("db")) {
|
|
50
71
|
out.push(`DB_DRIVER=${clean(s.dbDriver)}`);
|
|
51
72
|
if (s.dbDriver === "mongodb") {
|
|
@@ -133,11 +154,18 @@ const field = (label, key, placeholder = "") =>
|
|
|
133
154
|
<input class="form-control" placeholder=${placeholder} value=${() => state()[key]} oninput=${(e) => set({ [key]: e.target.value })} />
|
|
134
155
|
</div>`;
|
|
135
156
|
|
|
157
|
+
// A dependency pulled in by another enabled add-on shows as checked + disabled
|
|
158
|
+
// (you can't turn it off while something needs it), with a "required by" note —
|
|
159
|
+
// so the .env's VOLT_ADDONS always matches what the boxes show.
|
|
136
160
|
const addonRow = (a) =>
|
|
137
161
|
html`<div class="form-check mb-2">
|
|
138
|
-
<input class="form-check-input" type="checkbox" id=${"x-" + a.name}
|
|
162
|
+
<input class="form-check-input" type="checkbox" id=${"x-" + a.name}
|
|
163
|
+
checked=${() => eff().includes(a.name)}
|
|
164
|
+
disabled=${() => !state().addons[a.name] && eff().includes(a.name)}
|
|
165
|
+
onchange=${() => toggle(a.name)} />
|
|
139
166
|
<label class="form-check-label" for=${"x-" + a.name}>
|
|
140
|
-
<span class="accent">${a.name}</span>${a.dependsOn?.length ? html` <span class="text-muted small">(needs ${a.dependsOn.join(", ")})</span>` : ""}
|
|
167
|
+
<span class="accent">${a.name}</span>${a.dependsOn?.length ? html` <span class="text-muted small">(needs ${a.dependsOn.join(", ")})</span>` : ""}${() =>
|
|
168
|
+
!state().addons[a.name] && eff().includes(a.name) ? html` <span class="text-muted small">· required by ${requiredBy(state(), a.name).join(", ")}</span>` : ""}
|
|
141
169
|
<div class="small text-muted">${a.description}</div>
|
|
142
170
|
</label>
|
|
143
171
|
</div>`;
|
|
@@ -21,6 +21,24 @@ const ENV_PATH = path.join(__dirname, ".env");
|
|
|
21
21
|
const PKG_PATH = path.join(__dirname, "package.json");
|
|
22
22
|
const ADDONS_DIR = path.join(__dirname, ".volt", "addons"); // bundled add-on sources
|
|
23
23
|
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
24
|
+
|
|
25
|
+
// `--port <n>` (or --port=<n>) overrides the listen port for this run — lets
|
|
26
|
+
// --edit/--studio dodge a port the running app already holds, and runs the app
|
|
27
|
+
// itself on a one-off port. Explicit flag wins over PORT in .env.
|
|
28
|
+
function cliPort() {
|
|
29
|
+
const i = process.argv.indexOf("--port");
|
|
30
|
+
const raw = i > -1 ? process.argv[i + 1] : (process.argv.find((a) => a.startsWith("--port=")) || "").split("=")[1];
|
|
31
|
+
const n = Number(raw);
|
|
32
|
+
return Number.isInteger(n) && n >= 1 && n <= 65535 ? n : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Port for the disposable config UI (--edit / --studio): --port wins, then
|
|
36
|
+
// CONFIG_PORT in .env (run it on its own port so it never clashes with the app),
|
|
37
|
+
// then the app's PORT, then the date-port.
|
|
38
|
+
function configPort() {
|
|
39
|
+
const env = readEnvFile(); // --edit runs before loadEnv(), so read the file too
|
|
40
|
+
return cliPort() || Number(process.env.CONFIG_PORT) || Number(env.CONFIG_PORT) || Number(process.env.PORT) || Number(env.PORT) || DEFAULT_PORT;
|
|
41
|
+
}
|
|
24
42
|
const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1", marked: "^18.0.5", busboy: "^1.6.0", "@aws-sdk/client-s3": "^3.1075.0" };
|
|
25
43
|
const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js", pages: "pages.js", posts: "posts.js", media: "media.js" };
|
|
26
44
|
|
|
@@ -101,7 +119,7 @@ function openBrowser(url) {
|
|
|
101
119
|
|
|
102
120
|
// --- the actual app: wires whichever add-ons .env enables ---
|
|
103
121
|
async function startApp() {
|
|
104
|
-
const PORT = Number(process.env.PORT) || DEFAULT_PORT;
|
|
122
|
+
const PORT = cliPort() || Number(process.env.PORT) || DEFAULT_PORT;
|
|
105
123
|
const enabled = enabledFrom(process.env);
|
|
106
124
|
const app = express();
|
|
107
125
|
app.disable("x-powered-by");
|
|
@@ -188,7 +206,12 @@ async function startApp() {
|
|
|
188
206
|
};
|
|
189
207
|
w(dir);
|
|
190
208
|
};
|
|
191
|
-
|
|
209
|
+
// watch content dirs too (pages/posts markdown is read per request, so a
|
|
210
|
+
// browser reload shows the edit); skip dirs that don't exist.
|
|
211
|
+
for (const d of ["views", "public", "pages", "posts"]) {
|
|
212
|
+
const full = path.join(__dirname, d);
|
|
213
|
+
if (fs.existsSync(full)) watchRecursive(full);
|
|
214
|
+
}
|
|
192
215
|
|
|
193
216
|
const on = [...enabled];
|
|
194
217
|
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
@@ -221,7 +244,7 @@ function ensureDriverInstalled(driver) {
|
|
|
221
244
|
|
|
222
245
|
// --- the disposable setup wizard (localhost only) ---
|
|
223
246
|
function startSetup() {
|
|
224
|
-
const PORT =
|
|
247
|
+
const PORT = configPort();
|
|
225
248
|
const assets = {
|
|
226
249
|
"/setup.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "setup", "setup.js"))],
|
|
227
250
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
@@ -341,6 +364,7 @@ function startSetup() {
|
|
|
341
364
|
res.end("not found");
|
|
342
365
|
});
|
|
343
366
|
|
|
367
|
+
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; });
|
|
344
368
|
server.listen(PORT, "127.0.0.1", () => {
|
|
345
369
|
const url = `http://localhost:${PORT}`;
|
|
346
370
|
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
@@ -379,7 +403,7 @@ async function startStudio() {
|
|
|
379
403
|
console.error("Studio: couldn't connect the store â " + e.message);
|
|
380
404
|
process.exit(1);
|
|
381
405
|
}
|
|
382
|
-
const PORT =
|
|
406
|
+
const PORT = configPort();
|
|
383
407
|
const visible = (n) => n && !HIDDEN_COLLECTIONS.has(n);
|
|
384
408
|
const assets = {
|
|
385
409
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
@@ -431,6 +455,7 @@ async function startStudio() {
|
|
|
431
455
|
}
|
|
432
456
|
});
|
|
433
457
|
|
|
458
|
+
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; });
|
|
434
459
|
server.listen(PORT, "127.0.0.1", () => {
|
|
435
460
|
const url = `http://localhost:${PORT}`;
|
|
436
461
|
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
@@ -45,6 +45,23 @@ function effective(s) {
|
|
|
45
45
|
return order.filter((n) => want.has(n));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// which *enabled* add-ons pull in `name` as a (transitive) dependency
|
|
49
|
+
function requiredBy(s, name) {
|
|
50
|
+
const causes = [];
|
|
51
|
+
for (const n of order) {
|
|
52
|
+
if (n === name || !s.addons[n]) continue;
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const visit = (x) => {
|
|
55
|
+
if (seen.has(x)) return;
|
|
56
|
+
seen.add(x);
|
|
57
|
+
(depsOf[x] || []).forEach(visit);
|
|
58
|
+
};
|
|
59
|
+
(depsOf[n] || []).forEach(visit);
|
|
60
|
+
if (seen.has(name)) causes.push(n);
|
|
61
|
+
}
|
|
62
|
+
return causes;
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
const clean = (v) => String(v).replace(/[\r\n]/g, "").trim(); // one value per line; no injection
|
|
49
66
|
function genEnv(s) {
|
|
50
67
|
const eff = effective(s);
|
|
@@ -137,11 +154,18 @@ const field = (label, key, placeholder = "") =>
|
|
|
137
154
|
<input class="form-control" placeholder=${placeholder} value=${() => state()[key]} oninput=${(e) => set({ [key]: e.target.value })} />
|
|
138
155
|
</div>`;
|
|
139
156
|
|
|
157
|
+
// A dependency pulled in by another enabled add-on shows as checked + disabled
|
|
158
|
+
// (you can't turn it off while something needs it), with a "required by" note —
|
|
159
|
+
// so the .env's VOLT_ADDONS always matches what the boxes show.
|
|
140
160
|
const addonRow = (a) =>
|
|
141
161
|
html`<div class="form-check mb-2">
|
|
142
|
-
<input class="form-check-input" type="checkbox" id=${"x-" + a.name}
|
|
162
|
+
<input class="form-check-input" type="checkbox" id=${"x-" + a.name}
|
|
163
|
+
checked=${() => eff().includes(a.name)}
|
|
164
|
+
disabled=${() => !state().addons[a.name] && eff().includes(a.name)}
|
|
165
|
+
onchange=${() => toggle(a.name)} />
|
|
143
166
|
<label class="form-check-label" for=${"x-" + a.name}>
|
|
144
|
-
<span class="accent">${a.name}</span>${a.dependsOn?.length ? html` <span class="text-muted small">(needs ${a.dependsOn.join(", ")})</span>` : ""}
|
|
167
|
+
<span class="accent">${a.name}</span>${a.dependsOn?.length ? html` <span class="text-muted small">(needs ${a.dependsOn.join(", ")})</span>` : ""}${() =>
|
|
168
|
+
!state().addons[a.name] && eff().includes(a.name) ? html` <span class="text-muted small">· required by ${requiredBy(state(), a.name).join(", ")}</span>` : ""}
|
|
145
169
|
<div class="small text-muted">${a.description}</div>
|
|
146
170
|
</label>
|
|
147
171
|
</div>`;
|
package/templates/docs/server.js
CHANGED
|
@@ -21,6 +21,24 @@ const ENV_PATH = path.join(__dirname, ".env");
|
|
|
21
21
|
const PKG_PATH = path.join(__dirname, "package.json");
|
|
22
22
|
const ADDONS_DIR = path.join(__dirname, ".volt", "addons"); // bundled add-on sources
|
|
23
23
|
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
24
|
+
|
|
25
|
+
// `--port <n>` (or --port=<n>) overrides the listen port for this run — lets
|
|
26
|
+
// --edit/--studio dodge a port the running app already holds, and runs the app
|
|
27
|
+
// itself on a one-off port. Explicit flag wins over PORT in .env.
|
|
28
|
+
function cliPort() {
|
|
29
|
+
const i = process.argv.indexOf("--port");
|
|
30
|
+
const raw = i > -1 ? process.argv[i + 1] : (process.argv.find((a) => a.startsWith("--port=")) || "").split("=")[1];
|
|
31
|
+
const n = Number(raw);
|
|
32
|
+
return Number.isInteger(n) && n >= 1 && n <= 65535 ? n : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Port for the disposable config UI (--edit / --studio): --port wins, then
|
|
36
|
+
// CONFIG_PORT in .env (run it on its own port so it never clashes with the app),
|
|
37
|
+
// then the app's PORT, then the date-port.
|
|
38
|
+
function configPort() {
|
|
39
|
+
const env = readEnvFile(); // --edit runs before loadEnv(), so read the file too
|
|
40
|
+
return cliPort() || Number(process.env.CONFIG_PORT) || Number(env.CONFIG_PORT) || Number(process.env.PORT) || Number(env.PORT) || DEFAULT_PORT;
|
|
41
|
+
}
|
|
24
42
|
const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1", marked: "^18.0.5", busboy: "^1.6.0", "@aws-sdk/client-s3": "^3.1075.0" };
|
|
25
43
|
const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js", pages: "pages.js", posts: "posts.js", media: "media.js" };
|
|
26
44
|
|
|
@@ -101,7 +119,7 @@ function openBrowser(url) {
|
|
|
101
119
|
|
|
102
120
|
// --- the actual app: wires whichever add-ons .env enables ---
|
|
103
121
|
async function startApp() {
|
|
104
|
-
const PORT = Number(process.env.PORT) || DEFAULT_PORT;
|
|
122
|
+
const PORT = cliPort() || Number(process.env.PORT) || DEFAULT_PORT;
|
|
105
123
|
const enabled = enabledFrom(process.env);
|
|
106
124
|
const app = express();
|
|
107
125
|
app.disable("x-powered-by");
|
|
@@ -188,7 +206,12 @@ async function startApp() {
|
|
|
188
206
|
};
|
|
189
207
|
w(dir);
|
|
190
208
|
};
|
|
191
|
-
|
|
209
|
+
// watch content dirs too (pages/posts markdown is read per request, so a
|
|
210
|
+
// browser reload shows the edit); skip dirs that don't exist.
|
|
211
|
+
for (const d of ["views", "public", "pages", "posts"]) {
|
|
212
|
+
const full = path.join(__dirname, d);
|
|
213
|
+
if (fs.existsSync(full)) watchRecursive(full);
|
|
214
|
+
}
|
|
192
215
|
|
|
193
216
|
const on = [...enabled];
|
|
194
217
|
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
@@ -221,7 +244,7 @@ function ensureDriverInstalled(driver) {
|
|
|
221
244
|
|
|
222
245
|
// --- the disposable setup wizard (localhost only) ---
|
|
223
246
|
function startSetup() {
|
|
224
|
-
const PORT =
|
|
247
|
+
const PORT = configPort();
|
|
225
248
|
const assets = {
|
|
226
249
|
"/setup.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "setup", "setup.js"))],
|
|
227
250
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
@@ -341,6 +364,7 @@ function startSetup() {
|
|
|
341
364
|
res.end("not found");
|
|
342
365
|
});
|
|
343
366
|
|
|
367
|
+
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; });
|
|
344
368
|
server.listen(PORT, "127.0.0.1", () => {
|
|
345
369
|
const url = `http://localhost:${PORT}`;
|
|
346
370
|
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
@@ -379,7 +403,7 @@ async function startStudio() {
|
|
|
379
403
|
console.error("Studio: couldn't connect the store â " + e.message);
|
|
380
404
|
process.exit(1);
|
|
381
405
|
}
|
|
382
|
-
const PORT =
|
|
406
|
+
const PORT = configPort();
|
|
383
407
|
const visible = (n) => n && !HIDDEN_COLLECTIONS.has(n);
|
|
384
408
|
const assets = {
|
|
385
409
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
@@ -431,6 +455,7 @@ async function startStudio() {
|
|
|
431
455
|
}
|
|
432
456
|
});
|
|
433
457
|
|
|
458
|
+
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; });
|
|
434
459
|
server.listen(PORT, "127.0.0.1", () => {
|
|
435
460
|
const url = `http://localhost:${PORT}`;
|
|
436
461
|
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
@@ -25,6 +25,9 @@ const state = signal({
|
|
|
25
25
|
s3Secret: current.S3_SECRET || "",
|
|
26
26
|
s3PublicBase: current.S3_PUBLIC_BASE || "",
|
|
27
27
|
port: current.PORT || String(defaultPort),
|
|
28
|
+
// detect the admin's timezone from their browser (the wizard runs here), so
|
|
29
|
+
// dates render in their zone — not the server's (usually UTC on a host).
|
|
30
|
+
tz: current.SITE_TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || "",
|
|
28
31
|
});
|
|
29
32
|
const set = (patch) => state({ ...state(), ...patch });
|
|
30
33
|
const toggle = (n) => state({ ...state(), addons: { ...state().addons, [n]: !state().addons[n] } });
|
|
@@ -42,10 +45,28 @@ function effective(s) {
|
|
|
42
45
|
return order.filter((n) => want.has(n));
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
// which *enabled* add-ons pull in `name` as a (transitive) dependency
|
|
49
|
+
function requiredBy(s, name) {
|
|
50
|
+
const causes = [];
|
|
51
|
+
for (const n of order) {
|
|
52
|
+
if (n === name || !s.addons[n]) continue;
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const visit = (x) => {
|
|
55
|
+
if (seen.has(x)) return;
|
|
56
|
+
seen.add(x);
|
|
57
|
+
(depsOf[x] || []).forEach(visit);
|
|
58
|
+
};
|
|
59
|
+
(depsOf[n] || []).forEach(visit);
|
|
60
|
+
if (seen.has(name)) causes.push(n);
|
|
61
|
+
}
|
|
62
|
+
return causes;
|
|
63
|
+
}
|
|
64
|
+
|
|
45
65
|
const clean = (v) => String(v).replace(/[\r\n]/g, "").trim(); // one value per line; no injection
|
|
46
66
|
function genEnv(s) {
|
|
47
67
|
const eff = effective(s);
|
|
48
68
|
const out = [`VOLT_ADDONS=${eff.join(",")}`, `PORT=${clean(s.port)}`];
|
|
69
|
+
if (s.tz) out.push(`SITE_TZ=${clean(s.tz)}`); // admin's timezone, for date display
|
|
49
70
|
if (eff.includes("db")) {
|
|
50
71
|
out.push(`DB_DRIVER=${clean(s.dbDriver)}`);
|
|
51
72
|
if (s.dbDriver === "mongodb") {
|
|
@@ -133,11 +154,18 @@ const field = (label, key, placeholder = "") =>
|
|
|
133
154
|
<input class="form-control" placeholder=${placeholder} value=${() => state()[key]} oninput=${(e) => set({ [key]: e.target.value })} />
|
|
134
155
|
</div>`;
|
|
135
156
|
|
|
157
|
+
// A dependency pulled in by another enabled add-on shows as checked + disabled
|
|
158
|
+
// (you can't turn it off while something needs it), with a "required by" note —
|
|
159
|
+
// so the .env's VOLT_ADDONS always matches what the boxes show.
|
|
136
160
|
const addonRow = (a) =>
|
|
137
161
|
html`<div class="form-check mb-2">
|
|
138
|
-
<input class="form-check-input" type="checkbox" id=${"x-" + a.name}
|
|
162
|
+
<input class="form-check-input" type="checkbox" id=${"x-" + a.name}
|
|
163
|
+
checked=${() => eff().includes(a.name)}
|
|
164
|
+
disabled=${() => !state().addons[a.name] && eff().includes(a.name)}
|
|
165
|
+
onchange=${() => toggle(a.name)} />
|
|
139
166
|
<label class="form-check-label" for=${"x-" + a.name}>
|
|
140
|
-
<span class="accent">${a.name}</span>${a.dependsOn?.length ? html` <span class="text-muted small">(needs ${a.dependsOn.join(", ")})</span>` : ""}
|
|
167
|
+
<span class="accent">${a.name}</span>${a.dependsOn?.length ? html` <span class="text-muted small">(needs ${a.dependsOn.join(", ")})</span>` : ""}${() =>
|
|
168
|
+
!state().addons[a.name] && eff().includes(a.name) ? html` <span class="text-muted small">· required by ${requiredBy(state(), a.name).join(", ")}</span>` : ""}
|
|
141
169
|
<div class="small text-muted">${a.description}</div>
|
|
142
170
|
</label>
|
|
143
171
|
</div>`;
|
|
@@ -22,6 +22,24 @@ const ENV_PATH = path.join(__dirname, ".env");
|
|
|
22
22
|
const PKG_PATH = path.join(__dirname, "package.json");
|
|
23
23
|
const ADDONS_DIR = path.join(__dirname, ".volt", "addons"); // bundled add-on sources
|
|
24
24
|
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
25
|
+
|
|
26
|
+
// `--port <n>` (or --port=<n>) overrides the listen port for this run — lets
|
|
27
|
+
// --edit/--studio dodge a port the running app already holds, and runs the app
|
|
28
|
+
// itself on a one-off port. Explicit flag wins over PORT in .env.
|
|
29
|
+
function cliPort() {
|
|
30
|
+
const i = process.argv.indexOf("--port");
|
|
31
|
+
const raw = i > -1 ? process.argv[i + 1] : (process.argv.find((a) => a.startsWith("--port=")) || "").split("=")[1];
|
|
32
|
+
const n = Number(raw);
|
|
33
|
+
return Number.isInteger(n) && n >= 1 && n <= 65535 ? n : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Port for the disposable config UI (--edit / --studio): --port wins, then
|
|
37
|
+
// CONFIG_PORT in .env (run it on its own port so it never clashes with the app),
|
|
38
|
+
// then the app's PORT, then the date-port.
|
|
39
|
+
function configPort() {
|
|
40
|
+
const env = readEnvFile(); // --edit runs before loadEnv(), so read the file too
|
|
41
|
+
return cliPort() || Number(process.env.CONFIG_PORT) || Number(env.CONFIG_PORT) || Number(process.env.PORT) || Number(env.PORT) || DEFAULT_PORT;
|
|
42
|
+
}
|
|
25
43
|
const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1", marked: "^18.0.5", busboy: "^1.6.0", "@aws-sdk/client-s3": "^3.1075.0" };
|
|
26
44
|
const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js", pages: "pages.js", posts: "posts.js", media: "media.js" };
|
|
27
45
|
|
|
@@ -102,7 +120,7 @@ function openBrowser(url) {
|
|
|
102
120
|
|
|
103
121
|
// --- the actual app: wires whichever add-ons .env enables ---
|
|
104
122
|
async function startApp() {
|
|
105
|
-
const PORT = Number(process.env.PORT) || DEFAULT_PORT;
|
|
123
|
+
const PORT = cliPort() || Number(process.env.PORT) || DEFAULT_PORT;
|
|
106
124
|
const enabled = enabledFrom(process.env);
|
|
107
125
|
const app = express();
|
|
108
126
|
app.disable("x-powered-by");
|
|
@@ -214,7 +232,12 @@ async function startApp() {
|
|
|
214
232
|
};
|
|
215
233
|
w(dir);
|
|
216
234
|
};
|
|
217
|
-
|
|
235
|
+
// watch content dirs too (pages/posts markdown is read per request, so a
|
|
236
|
+
// browser reload shows the edit); skip dirs that don't exist.
|
|
237
|
+
for (const d of ["views", "public", "pages", "posts"]) {
|
|
238
|
+
const full = path.join(__dirname, d);
|
|
239
|
+
if (fs.existsSync(full)) watchRecursive(full);
|
|
240
|
+
}
|
|
218
241
|
|
|
219
242
|
const on = [...enabled];
|
|
220
243
|
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
@@ -247,7 +270,7 @@ function ensureDriverInstalled(driver) {
|
|
|
247
270
|
|
|
248
271
|
// --- the disposable setup wizard (localhost only) ---
|
|
249
272
|
function startSetup() {
|
|
250
|
-
const PORT =
|
|
273
|
+
const PORT = configPort();
|
|
251
274
|
const assets = {
|
|
252
275
|
"/setup.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "setup", "setup.js"))],
|
|
253
276
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
@@ -367,6 +390,7 @@ function startSetup() {
|
|
|
367
390
|
res.end("not found");
|
|
368
391
|
});
|
|
369
392
|
|
|
393
|
+
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; });
|
|
370
394
|
server.listen(PORT, "127.0.0.1", () => {
|
|
371
395
|
const url = `http://localhost:${PORT}`;
|
|
372
396
|
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
@@ -405,7 +429,7 @@ async function startStudio() {
|
|
|
405
429
|
console.error("Studio: couldn't connect the store â " + e.message);
|
|
406
430
|
process.exit(1);
|
|
407
431
|
}
|
|
408
|
-
const PORT =
|
|
432
|
+
const PORT = configPort();
|
|
409
433
|
const visible = (n) => n && !HIDDEN_COLLECTIONS.has(n);
|
|
410
434
|
const assets = {
|
|
411
435
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
@@ -457,6 +481,7 @@ async function startStudio() {
|
|
|
457
481
|
}
|
|
458
482
|
});
|
|
459
483
|
|
|
484
|
+
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; });
|
|
460
485
|
server.listen(PORT, "127.0.0.1", () => {
|
|
461
486
|
const url = `http://localhost:${PORT}`;
|
|
462
487
|
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
@@ -45,6 +45,23 @@ function effective(s) {
|
|
|
45
45
|
return order.filter((n) => want.has(n));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// which *enabled* add-ons pull in `name` as a (transitive) dependency
|
|
49
|
+
function requiredBy(s, name) {
|
|
50
|
+
const causes = [];
|
|
51
|
+
for (const n of order) {
|
|
52
|
+
if (n === name || !s.addons[n]) continue;
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const visit = (x) => {
|
|
55
|
+
if (seen.has(x)) return;
|
|
56
|
+
seen.add(x);
|
|
57
|
+
(depsOf[x] || []).forEach(visit);
|
|
58
|
+
};
|
|
59
|
+
(depsOf[n] || []).forEach(visit);
|
|
60
|
+
if (seen.has(name)) causes.push(n);
|
|
61
|
+
}
|
|
62
|
+
return causes;
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
const clean = (v) => String(v).replace(/[\r\n]/g, "").trim(); // one value per line; no injection
|
|
49
66
|
function genEnv(s) {
|
|
50
67
|
const eff = effective(s);
|
|
@@ -137,11 +154,18 @@ const field = (label, key, placeholder = "") =>
|
|
|
137
154
|
<input class="form-control" placeholder=${placeholder} value=${() => state()[key]} oninput=${(e) => set({ [key]: e.target.value })} />
|
|
138
155
|
</div>`;
|
|
139
156
|
|
|
157
|
+
// A dependency pulled in by another enabled add-on shows as checked + disabled
|
|
158
|
+
// (you can't turn it off while something needs it), with a "required by" note —
|
|
159
|
+
// so the .env's VOLT_ADDONS always matches what the boxes show.
|
|
140
160
|
const addonRow = (a) =>
|
|
141
161
|
html`<div class="form-check mb-2">
|
|
142
|
-
<input class="form-check-input" type="checkbox" id=${"x-" + a.name}
|
|
162
|
+
<input class="form-check-input" type="checkbox" id=${"x-" + a.name}
|
|
163
|
+
checked=${() => eff().includes(a.name)}
|
|
164
|
+
disabled=${() => !state().addons[a.name] && eff().includes(a.name)}
|
|
165
|
+
onchange=${() => toggle(a.name)} />
|
|
143
166
|
<label class="form-check-label" for=${"x-" + a.name}>
|
|
144
|
-
<span class="accent">${a.name}</span>${a.dependsOn?.length ? html` <span class="text-muted small">(needs ${a.dependsOn.join(", ")})</span>` : ""}
|
|
167
|
+
<span class="accent">${a.name}</span>${a.dependsOn?.length ? html` <span class="text-muted small">(needs ${a.dependsOn.join(", ")})</span>` : ""}${() =>
|
|
168
|
+
!state().addons[a.name] && eff().includes(a.name) ? html` <span class="text-muted small">· required by ${requiredBy(state(), a.name).join(", ")}</span>` : ""}
|
|
145
169
|
<div class="small text-muted">${a.description}</div>
|
|
146
170
|
</label>
|
|
147
171
|
</div>`;
|