create-volt 0.32.0 → 0.33.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 +15 -0
- package/addons/posts/files/lib/posts.js +4 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/templates/blog/Dockerfile +20 -0
- package/templates/blog/Procfile +1 -0
- package/templates/blog/README.md +38 -0
- package/templates/blog/dockerignore +6 -0
- package/templates/blog/env +3 -0
- package/templates/blog/fly.toml +15 -0
- package/templates/blog/gitignore +5 -0
- package/templates/blog/package.json +17 -0
- package/templates/blog/pages/_theme.js +35 -0
- package/templates/blog/pages/about.md +8 -0
- package/templates/blog/posts/2026-06-10-themes.md +11 -0
- package/templates/blog/posts/2026-06-20-markdown-and-seo.md +14 -0
- package/templates/blog/posts/2026-06-28-hello-volt.md +17 -0
- package/templates/blog/public/favicon.webp +0 -0
- package/templates/blog/public/logo.webp +0 -0
- package/templates/blog/public/volt-ssr.js +63 -0
- package/templates/blog/public/volt.js +273 -0
- package/templates/blog/render.yaml +15 -0
- package/templates/blog/server.js +464 -0
- package/templates/blog/setup/index.html +28 -0
- package/templates/blog/setup/setup.js +200 -0
- package/templates/blog/setup/studio.html +29 -0
- package/templates/blog/views/index.html +25 -0
- package/templates/default/views/index.html +2 -4
- package/templates/docs/Dockerfile +20 -0
- package/templates/docs/Procfile +1 -0
- package/templates/docs/README.md +24 -0
- package/templates/docs/dockerignore +6 -0
- package/templates/docs/env +2 -0
- package/templates/docs/fly.toml +15 -0
- package/templates/docs/gitignore +5 -0
- package/templates/docs/package.json +17 -0
- package/templates/docs/pages/_theme.js +32 -0
- package/templates/docs/pages/configuration.md +13 -0
- package/templates/docs/pages/deployment.md +9 -0
- package/templates/docs/pages/getting-started.md +14 -0
- package/templates/docs/public/favicon.webp +0 -0
- package/templates/docs/public/logo.webp +0 -0
- package/templates/docs/public/volt-ssr.js +63 -0
- package/templates/docs/public/volt.js +273 -0
- package/templates/docs/render.yaml +15 -0
- package/templates/docs/server.js +464 -0
- package/templates/docs/setup/index.html +28 -0
- package/templates/docs/setup/setup.js +200 -0
- package/templates/docs/setup/studio.html +29 -0
- package/templates/docs/views/index.html +15 -0
- package/templates/starter/views/index.html +1 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
// server.js â dev server with a built-in first-run setup wizard.
|
|
2
|
+
//
|
|
3
|
+
// First run (no .env) or `node server.js --edit` (-e) opens a disposable, local
|
|
4
|
+
// config page: tick add-ons, fill settings, Apply. Apply writes .env (a
|
|
5
|
+
// VOLT_ADDONS list + settings) and adds any needed packages to package.json,
|
|
6
|
+
// runs npm install, then starts the app â which wires whatever .env enables.
|
|
7
|
+
// Add-on code is bundled under .volt/addons; nothing is copied into your code.
|
|
8
|
+
//
|
|
9
|
+
// No build step, no env-file flag: .env is auto-loaded below.
|
|
10
|
+
|
|
11
|
+
import http from "node:http";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
16
|
+
import express from "express";
|
|
17
|
+
import { Server as SocketServer } from "socket.io";
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const ENV_PATH = path.join(__dirname, ".env");
|
|
21
|
+
const PKG_PATH = path.join(__dirname, "package.json");
|
|
22
|
+
const ADDONS_DIR = path.join(__dirname, ".volt", "addons"); // bundled add-on sources
|
|
23
|
+
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
24
|
+
const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1", marked: "^18.0.5", busboy: "^1.6.0", "@aws-sdk/client-s3": "^3.1075.0" };
|
|
25
|
+
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
|
+
|
|
27
|
+
// --- tiny .env loader (no dependency); never overrides an existing env var ---
|
|
28
|
+
function readEnvFile() {
|
|
29
|
+
const out = {};
|
|
30
|
+
if (!fs.existsSync(ENV_PATH)) return out;
|
|
31
|
+
for (const line of fs.readFileSync(ENV_PATH, "utf8").split("\n")) {
|
|
32
|
+
const m = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*?)\s*$/);
|
|
33
|
+
if (m) out[m[1]] = m[2];
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function loadEnv() {
|
|
38
|
+
for (const [k, v] of Object.entries(readEnvFile())) if (!(k in process.env)) process.env[k] = v;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Add-ons available to enable (bundled under .volt/addons by create-volt).
|
|
42
|
+
function availableAddons() {
|
|
43
|
+
if (!fs.existsSync(ADDONS_DIR)) return [];
|
|
44
|
+
return fs
|
|
45
|
+
.readdirSync(ADDONS_DIR, { withFileTypes: true })
|
|
46
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(ADDONS_DIR, e.name, "meta.json"))) // skip local 3rd-party add-ons (no meta)
|
|
47
|
+
.map((e) => {
|
|
48
|
+
const m = JSON.parse(fs.readFileSync(path.join(ADDONS_DIR, e.name, "meta.json"), "utf8"));
|
|
49
|
+
return { name: e.name, description: m.description, dependsOn: m.dependsOn || [] };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Which add-ons does VOLT_ADDONS turn on (dependencies expanded)?
|
|
54
|
+
function enabledFrom(env) {
|
|
55
|
+
const metas = Object.fromEntries(availableAddons().map((a) => [a.name, a]));
|
|
56
|
+
const out = new Set();
|
|
57
|
+
const visit = (n) => {
|
|
58
|
+
if (out.has(n)) return; // include third-party names too, not just bundled ones
|
|
59
|
+
out.add(n);
|
|
60
|
+
for (const d of metas[n]?.dependsOn || []) visit(d);
|
|
61
|
+
};
|
|
62
|
+
for (const n of String(env.VOLT_ADDONS || "").split(",").map((s) => s.trim()).filter(Boolean)) visit(n);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const imp = (rel) => import(pathToFileURL(path.join(__dirname, rel)).href);
|
|
67
|
+
const addonMod = (n) => imp(path.join(".volt", "addons", n, "files", "lib", LIB_FILE[n]));
|
|
68
|
+
|
|
69
|
+
// Built-in add-ons are wired explicitly below; everything else in VOLT_ADDONS is
|
|
70
|
+
// a third-party add-on â a local .volt/addons/<name>/index.js or an installed
|
|
71
|
+
// npm package "volt-addon-<name>" exporting register(ctx). See /docs/plugins.
|
|
72
|
+
const BUILTINS = new Set(Object.keys(LIB_FILE));
|
|
73
|
+
async function loadAddon(name) {
|
|
74
|
+
const local = path.join(__dirname, ".volt", "addons", name, "index.js");
|
|
75
|
+
if (fs.existsSync(local)) return imp(path.join(".volt", "addons", name, "index.js"));
|
|
76
|
+
for (const id of [`volt-addon-${name}`, name]) {
|
|
77
|
+
try {
|
|
78
|
+
return await import(id);
|
|
79
|
+
} catch {
|
|
80
|
+
/* try next */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function openBrowser(url) {
|
|
87
|
+
if (process.env.VOLT_NO_OPEN || process.argv.includes("--no-open")) return false;
|
|
88
|
+
const plat = process.platform;
|
|
89
|
+
if (plat === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
|
|
90
|
+
const cmd = plat === "darwin" ? "open" : plat === "win32" ? "cmd" : "xdg-open";
|
|
91
|
+
const args = plat === "win32" ? ["/c", "start", "", url] : [url];
|
|
92
|
+
try {
|
|
93
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
94
|
+
child.on("error", () => {}); // launcher missing â emits async, don't crash
|
|
95
|
+
child.unref();
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- the actual app: wires whichever add-ons .env enables ---
|
|
103
|
+
async function startApp() {
|
|
104
|
+
const PORT = Number(process.env.PORT) || DEFAULT_PORT;
|
|
105
|
+
const enabled = enabledFrom(process.env);
|
|
106
|
+
const app = express();
|
|
107
|
+
app.disable("x-powered-by");
|
|
108
|
+
app.use((_req, res, next) => {
|
|
109
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
110
|
+
res.setHeader("X-Frame-Options", "SAMEORIGIN");
|
|
111
|
+
res.setHeader("Referrer-Policy", "same-origin");
|
|
112
|
+
next();
|
|
113
|
+
});
|
|
114
|
+
app.use(express.static(path.join(__dirname, "public")));
|
|
115
|
+
|
|
116
|
+
let store = null;
|
|
117
|
+
let mailer = null;
|
|
118
|
+
if (enabled.has("db")) store = await (await addonMod("db")).createStore();
|
|
119
|
+
if (enabled.has("mailer")) mailer = await (await addonMod("mailer")).createMailer();
|
|
120
|
+
if (enabled.has("auth") && store && mailer) app.use((await addonMod("auth")).authRouter({ store, mailer }));
|
|
121
|
+
|
|
122
|
+
// expose which add-ons are on, and serve each enabled add-on's frontend assets
|
|
123
|
+
app.get("/__volt/addons", (_req, res) => res.json([...enabled]));
|
|
124
|
+
for (const n of enabled) {
|
|
125
|
+
const pub = path.join(ADDONS_DIR, n, "files", "public");
|
|
126
|
+
if (fs.existsSync(pub)) app.use(express.static(pub));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
app.get("/", (_req, res) => res.sendFile(path.join(__dirname, "views", "index.html")));
|
|
130
|
+
|
|
131
|
+
// media uploads (POST /api/media, auth-gated; local files served at /media)
|
|
132
|
+
if (enabled.has("media") && store) {
|
|
133
|
+
const requireAuth = (await addonMod("auth")).requireAuth(store);
|
|
134
|
+
app.use(await (await addonMod("media")).mediaRouter({ requireAuth, dir: path.join(__dirname, "media") }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// markdown pages (/<slug> â pages/<slug>.md) â mounted last, so app routes win
|
|
138
|
+
// blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) — before pages so /blog wins; renders in the same theme.
|
|
139
|
+
if (enabled.has("posts")) app.use(await (await addonMod("posts")).postsRouter({ dir: path.join(__dirname, "posts"), themeDir: path.join(__dirname, "pages") }));
|
|
140
|
+
if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
|
|
141
|
+
|
|
142
|
+
const server = http.createServer(app);
|
|
143
|
+
const io = new SocketServer(server);
|
|
144
|
+
if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
|
|
145
|
+
|
|
146
|
+
// third-party add-ons â register(ctx). When auth is on, requireAuth/sessionFromReq
|
|
147
|
+
// are provided so add-ons can gate routes by login.
|
|
148
|
+
let requireAuth = null;
|
|
149
|
+
let sessionFromReq = null;
|
|
150
|
+
if (enabled.has("auth") && store) {
|
|
151
|
+
const a = await addonMod("auth");
|
|
152
|
+
requireAuth = a.requireAuth(store);
|
|
153
|
+
sessionFromReq = (req) => a.sessionFromReq(store, req);
|
|
154
|
+
}
|
|
155
|
+
for (const name of enabled) {
|
|
156
|
+
if (BUILTINS.has(name)) continue;
|
|
157
|
+
const mod = await loadAddon(name);
|
|
158
|
+
const register = mod && (mod.register || mod.default);
|
|
159
|
+
if (typeof register === "function") {
|
|
160
|
+
await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
|
|
161
|
+
} else {
|
|
162
|
+
console.warn(`[volt] add-on "${name}" not found or missing a register() export â skipped`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let timer = null;
|
|
167
|
+
const onChange = (file) => {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
timer = setTimeout(() => {
|
|
170
|
+
console.log(`[volt] change: ${file ?? "?"} â reload`);
|
|
171
|
+
io.emit("volt:reload");
|
|
172
|
+
}, 80);
|
|
173
|
+
};
|
|
174
|
+
const watchRecursive = (dir) => {
|
|
175
|
+
try {
|
|
176
|
+
fs.watch(dir, { recursive: true }, (_e, f) => onChange(f));
|
|
177
|
+
return;
|
|
178
|
+
} catch {
|
|
179
|
+
/* per-dir fallback */
|
|
180
|
+
}
|
|
181
|
+
const w = (d) => {
|
|
182
|
+
try {
|
|
183
|
+
fs.watch(d, (_e, f) => onChange(f));
|
|
184
|
+
} catch {
|
|
185
|
+
/* ignore */
|
|
186
|
+
}
|
|
187
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) if (e.isDirectory()) w(path.join(d, e.name));
|
|
188
|
+
};
|
|
189
|
+
w(dir);
|
|
190
|
+
};
|
|
191
|
+
for (const d of ["views", "public"]) watchRecursive(path.join(__dirname, d));
|
|
192
|
+
|
|
193
|
+
const on = [...enabled];
|
|
194
|
+
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Packages an .env's selections need, beyond what package.json already has.
|
|
198
|
+
function neededPackages(env) {
|
|
199
|
+
const pkg = JSON.parse(fs.readFileSync(PKG_PATH, "utf8"));
|
|
200
|
+
const deps = pkg.dependencies || {};
|
|
201
|
+
const want = [];
|
|
202
|
+
const driver = (env.match(/^\s*DB_DRIVER\s*=\s*(\w+)/m) || [])[1];
|
|
203
|
+
if (driver === "mongodb") want.push("mongodb");
|
|
204
|
+
if (driver === "mysql") want.push("mysql2");
|
|
205
|
+
if (driver === "postgres") want.push("pg");
|
|
206
|
+
if (/^\s*SMTP_URL\s*=\s*\S/m.test(env)) want.push("nodemailer");
|
|
207
|
+
if (/^\s*VOLT_ADDONS\s*=.*\b(pages|posts)\b/m.test(env)) want.push("marked");
|
|
208
|
+
if (/^\s*VOLT_ADDONS\s*=.*\bmedia\b/m.test(env)) want.push("busboy");
|
|
209
|
+
if (/^\s*MEDIA_DRIVER\s*=\s*s3\b/m.test(env)) want.push("@aws-sdk/client-s3");
|
|
210
|
+
return want.filter((p) => !deps[p]);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Install a DB driver's package on demand (pinned), so the wizard's "Test
|
|
214
|
+
// connection" works before Apply has installed it.
|
|
215
|
+
function ensureDriverInstalled(driver) {
|
|
216
|
+
const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
|
|
217
|
+
if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
|
|
218
|
+
console.log(`[volt] installing ${pkg} for the connection testâ¦`);
|
|
219
|
+
spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- the disposable setup wizard (localhost only) ---
|
|
223
|
+
function startSetup() {
|
|
224
|
+
const PORT = Number(process.env.PORT) || Number(readEnvFile().PORT) || DEFAULT_PORT;
|
|
225
|
+
const assets = {
|
|
226
|
+
"/setup.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "setup", "setup.js"))],
|
|
227
|
+
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
228
|
+
"/logo.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "logo.webp"))],
|
|
229
|
+
"/favicon.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "favicon.webp"))],
|
|
230
|
+
};
|
|
231
|
+
const indexHtml = fs.readFileSync(path.join(__dirname, "setup", "index.html"));
|
|
232
|
+
|
|
233
|
+
const server = http.createServer((req, res) => {
|
|
234
|
+
const u = new URL(req.url, "http://localhost");
|
|
235
|
+
const p = u.pathname;
|
|
236
|
+
if (req.method === "GET" && p === "/") {
|
|
237
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
238
|
+
return res.end(indexHtml);
|
|
239
|
+
}
|
|
240
|
+
if (req.method === "GET" && assets[p]) {
|
|
241
|
+
res.setHeader("Content-Type", assets[p][0]);
|
|
242
|
+
return res.end(assets[p][1]);
|
|
243
|
+
}
|
|
244
|
+
if (req.method === "GET" && p === "/setup/state") {
|
|
245
|
+
res.setHeader("Content-Type", "application/json");
|
|
246
|
+
return res.end(JSON.stringify({ available: availableAddons(), current: readEnvFile(), defaultPort: DEFAULT_PORT }));
|
|
247
|
+
}
|
|
248
|
+
if (req.method === "POST" && p === "/setup/test-db") {
|
|
249
|
+
let body = "";
|
|
250
|
+
req.on("data", (c) => (body += c));
|
|
251
|
+
req.on("end", async () => {
|
|
252
|
+
const keys = ["DB_DRIVER", "MONGODB_URI", "MONGODB_DATABASE", "DATABASE_URL"];
|
|
253
|
+
const saved = Object.fromEntries(keys.map((k) => [k, process.env[k]]));
|
|
254
|
+
try {
|
|
255
|
+
const { env = {} } = JSON.parse(body);
|
|
256
|
+
for (const k of keys) {
|
|
257
|
+
if (env[k]) process.env[k] = env[k];
|
|
258
|
+
else delete process.env[k];
|
|
259
|
+
}
|
|
260
|
+
ensureDriverInstalled(process.env.DB_DRIVER); // install the driver first, so the test can connect
|
|
261
|
+
const store = await (await addonMod("db")).createStore();
|
|
262
|
+
await store.collection("__voltcheck").all();
|
|
263
|
+
res.setHeader("Content-Type", "application/json");
|
|
264
|
+
res.end(JSON.stringify({ ok: true, driver: store.name }));
|
|
265
|
+
} catch (e) {
|
|
266
|
+
res.setHeader("Content-Type", "application/json");
|
|
267
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
268
|
+
} finally {
|
|
269
|
+
for (const k of keys) {
|
|
270
|
+
if (saved[k] == null) delete process.env[k];
|
|
271
|
+
else process.env[k] = saved[k];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (req.method === "POST" && p === "/setup/apply") {
|
|
278
|
+
let body = "";
|
|
279
|
+
req.on("data", (c) => (body += c));
|
|
280
|
+
req.on("end", () => {
|
|
281
|
+
try {
|
|
282
|
+
const { env } = JSON.parse(body);
|
|
283
|
+
if (typeof env !== "string") throw new Error("missing env");
|
|
284
|
+
|
|
285
|
+
// 1) write .env, preserving any custom keys the form doesn't manage
|
|
286
|
+
let finalEnv = env;
|
|
287
|
+
if (fs.existsSync(ENV_PATH)) {
|
|
288
|
+
const managed = new Set([...env.matchAll(/^\s*([A-Za-z0-9_]+)\s*=/gm)].map((m) => m[1]));
|
|
289
|
+
const extra = readEnvFileLines().filter((line) => {
|
|
290
|
+
const m = line.match(/^\s*([A-Za-z0-9_]+)\s*=/);
|
|
291
|
+
return m && !managed.has(m[1]);
|
|
292
|
+
});
|
|
293
|
+
if (extra.length) finalEnv = env.replace(/\n*$/, "\n") + extra.join("\n") + "\n";
|
|
294
|
+
}
|
|
295
|
+
fs.writeFileSync(ENV_PATH, finalEnv);
|
|
296
|
+
|
|
297
|
+
// 2) declare any needed packages in package.json
|
|
298
|
+
const added = neededPackages(env);
|
|
299
|
+
if (added.length) {
|
|
300
|
+
const pkg = JSON.parse(fs.readFileSync(PKG_PATH, "utf8"));
|
|
301
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
302
|
+
for (const name of added) pkg.dependencies[name] = PKG_VERSIONS[name] || "latest";
|
|
303
|
+
fs.writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + "\n");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const envPort = Number((env.match(/^\s*PORT\s*=\s*(\d+)/m) || [])[1]);
|
|
307
|
+
const newPort = process.env.PORT ? Number(process.env.PORT) : envPort || DEFAULT_PORT;
|
|
308
|
+
res.setHeader("Content-Type", "application/json");
|
|
309
|
+
res.end(JSON.stringify({ ok: true, port: newPort, installing: added }));
|
|
310
|
+
|
|
311
|
+
// 3) install (if needed), then hand off to the app
|
|
312
|
+
res.on("finish", () => {
|
|
313
|
+
const handoff = () => {
|
|
314
|
+
server.close(() => {
|
|
315
|
+
loadEnv();
|
|
316
|
+
startApp();
|
|
317
|
+
});
|
|
318
|
+
server.closeIdleConnections?.();
|
|
319
|
+
};
|
|
320
|
+
if (added.length) {
|
|
321
|
+
console.log(`[volt] installing ${added.join(", ")}â¦`);
|
|
322
|
+
const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
323
|
+
npm.on("error", () => handoff());
|
|
324
|
+
npm.on("close", () => {
|
|
325
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
326
|
+
handoff();
|
|
327
|
+
});
|
|
328
|
+
} else {
|
|
329
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
330
|
+
handoff();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
} catch (e) {
|
|
334
|
+
res.statusCode = 400;
|
|
335
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
res.statusCode = 404;
|
|
341
|
+
res.end("not found");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
server.listen(PORT, "127.0.0.1", () => {
|
|
345
|
+
const url = `http://localhost:${PORT}`;
|
|
346
|
+
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
347
|
+
console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
|
|
348
|
+
const ssh = process.env.SSH_CONNECTION;
|
|
349
|
+
if (ssh) {
|
|
350
|
+
const host = ssh.split(" ")[2];
|
|
351
|
+
const user = process.env.USER || process.env.USERNAME || "you";
|
|
352
|
+
console.log(" Remote box â the server is up here; bridge it from your LOCAL machine:");
|
|
353
|
+
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
354
|
+
console.log(` â¦then open ${url} on your machine (the tunnel points it here).`);
|
|
355
|
+
}
|
|
356
|
+
console.log("");
|
|
357
|
+
if (openBrowser(url)) console.log(" (opening your browserâ¦)\n");
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function readEnvFileLines() {
|
|
362
|
+
return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --- Studio: an ephemeral, localhost-only data browser (Ã la Prisma Studio).
|
|
366
|
+
// Not a route in the running app â it only exists while you run `--studio`, on
|
|
367
|
+
// loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
|
|
368
|
+
const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
|
|
369
|
+
async function startStudio() {
|
|
370
|
+
loadEnv();
|
|
371
|
+
if (!enabledFrom(process.env).has("db")) {
|
|
372
|
+
console.error("Studio needs the db add-on. Enable it: npm run dev -- --edit");
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
let store;
|
|
376
|
+
try {
|
|
377
|
+
store = await (await addonMod("db")).createStore();
|
|
378
|
+
} catch (e) {
|
|
379
|
+
console.error("Studio: couldn't connect the store â " + e.message);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
const PORT = Number(process.env.PORT) || Number(readEnvFile().PORT) || DEFAULT_PORT;
|
|
383
|
+
const visible = (n) => n && !HIDDEN_COLLECTIONS.has(n);
|
|
384
|
+
const assets = {
|
|
385
|
+
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
386
|
+
"/logo.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "logo.webp"))],
|
|
387
|
+
"/favicon.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "favicon.webp"))],
|
|
388
|
+
"/db-admin-ui.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(ADDONS_DIR, "db", "files", "public", "db-admin-ui.js"))],
|
|
389
|
+
};
|
|
390
|
+
const studioHtml = fs.readFileSync(path.join(__dirname, "setup", "studio.html"));
|
|
391
|
+
const json = (res, code, obj) => {
|
|
392
|
+
res.statusCode = code;
|
|
393
|
+
res.setHeader("Content-Type", "application/json");
|
|
394
|
+
res.end(JSON.stringify(obj));
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const server = http.createServer(async (req, res) => {
|
|
398
|
+
const u = new URL(req.url, "http://localhost");
|
|
399
|
+
const p = u.pathname;
|
|
400
|
+
try {
|
|
401
|
+
if (req.method === "GET" && p === "/") {
|
|
402
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
403
|
+
return res.end(studioHtml);
|
|
404
|
+
}
|
|
405
|
+
if (req.method === "GET" && assets[p]) {
|
|
406
|
+
res.setHeader("Content-Type", assets[p][0]);
|
|
407
|
+
return res.end(assets[p][1]);
|
|
408
|
+
}
|
|
409
|
+
if (req.method === "GET" && p === "/admin/db/collections") {
|
|
410
|
+
const all = (await store.collections()) || [];
|
|
411
|
+
return json(res, 200, { driver: store.name, collections: all.filter(visible) });
|
|
412
|
+
}
|
|
413
|
+
if (req.method === "GET" && p === "/admin/db/collection") {
|
|
414
|
+
const name = u.searchParams.get("name") || "";
|
|
415
|
+
if (!visible(name)) return json(res, 403, { ok: false, error: "hidden" });
|
|
416
|
+
const docs = (await store.collection(name).all()).slice(0, 500);
|
|
417
|
+
return json(res, 200, { ok: true, name, docs });
|
|
418
|
+
}
|
|
419
|
+
if (req.method === "DELETE" && p === "/admin/db/doc") {
|
|
420
|
+
const name = u.searchParams.get("name") || "";
|
|
421
|
+
const id = u.searchParams.get("id") || "";
|
|
422
|
+
if (!visible(name)) return json(res, 403, { ok: false, error: "hidden" });
|
|
423
|
+
if (!id) return json(res, 400, { ok: false, error: "missing id" });
|
|
424
|
+
await store.collection(name).delete(id);
|
|
425
|
+
return json(res, 200, { ok: true });
|
|
426
|
+
}
|
|
427
|
+
res.statusCode = 404;
|
|
428
|
+
res.end("not found");
|
|
429
|
+
} catch (e) {
|
|
430
|
+
json(res, 500, { ok: false, error: e.message });
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
server.listen(PORT, "127.0.0.1", () => {
|
|
435
|
+
const url = `http://localhost:${PORT}`;
|
|
436
|
+
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
437
|
+
console.log(" Browse your data. localhost-only, disposable â Ctrl-C when done.");
|
|
438
|
+
const ssh = process.env.SSH_CONNECTION;
|
|
439
|
+
if (ssh) {
|
|
440
|
+
const host = ssh.split(" ")[2];
|
|
441
|
+
const user = process.env.USER || process.env.USERNAME || "you";
|
|
442
|
+
console.log(" Remote box â from your LOCAL machine:");
|
|
443
|
+
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
444
|
+
console.log(` â¦then open ${url}.`);
|
|
445
|
+
}
|
|
446
|
+
console.log("");
|
|
447
|
+
openBrowser(url);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// --- gate: studio / setup (first run, --edit) / the app ---
|
|
452
|
+
const editMode = process.argv.includes("--edit") || process.argv.includes("-e");
|
|
453
|
+
// In production / on a PaaS there's no interactive wizard: config comes from the
|
|
454
|
+
// platform's env vars (a Dockerfile sets NODE_ENV=production). Only fall back to
|
|
455
|
+
// the first-run wizard when nothing is configured and we're not in production.
|
|
456
|
+
const configured = fs.existsSync(ENV_PATH) || process.env.VOLT_ADDONS != null || process.env.NODE_ENV === "production";
|
|
457
|
+
if (process.argv.includes("--studio")) {
|
|
458
|
+
startStudio();
|
|
459
|
+
} else if (editMode || !configured) {
|
|
460
|
+
startSetup();
|
|
461
|
+
} else {
|
|
462
|
+
loadEnv();
|
|
463
|
+
startApp();
|
|
464
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
7
|
+
<title>Set up your Volt app</title>
|
|
8
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
9
|
+
<style>
|
|
10
|
+
body { background: #0f1115; color: #e7e9ee; }
|
|
11
|
+
.accent { color: #ffd24a; }
|
|
12
|
+
.card-x { background: #161a22; border: 1px solid #232a36; border-radius: 14px; }
|
|
13
|
+
.form-control, .form-select { background: #0f1115; color: #e7e9ee; border-color: #232a36; }
|
|
14
|
+
.form-control:focus, .form-select:focus { background: #0f1115; color: #e7e9ee; border-color: #ffd24a; box-shadow: none; }
|
|
15
|
+
code { color: #ffd24a; }
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<main class="container py-5" style="max-width: 640px;">
|
|
20
|
+
<header class="mb-4">
|
|
21
|
+
<h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Set up your Volt app</span></h1>
|
|
22
|
+
<p class="text-muted mb-0">Fill these in and the app starts. This page is disposable — it disappears once you click Apply. Re-open it anytime with <code>npm run dev -- --edit</code>.</p>
|
|
23
|
+
</header>
|
|
24
|
+
<div id="app"></div>
|
|
25
|
+
</main>
|
|
26
|
+
<script type="module" src="/setup.js"></script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|