create-volt 0.31.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 +24 -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/public/favicon.webp +0 -0
- package/templates/default/public/logo.webp +0 -0
- package/templates/default/server.js +4 -0
- package/templates/default/setup/index.html +2 -1
- package/templates/default/setup/studio.html +2 -1
- package/templates/default/views/index.html +4 -5
- 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/public/app.js +1 -1
- package/templates/starter/public/favicon.webp +0 -0
- package/templates/starter/public/logo.webp +0 -0
- package/templates/starter/server.js +4 -0
- package/templates/starter/setup/index.html +2 -1
- package/templates/starter/setup/studio.html +2 -1
- package/templates/starter/views/index.html +2 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// setup.js — first-run / --edit wizard, built with Volt. Tick add-ons + fill
|
|
2
|
+
// settings → writes .env (a VOLT_ADDONS list + settings), adds any needed
|
|
3
|
+
// packages, installs, and starts the app. Add-on code is bundled; enabling is
|
|
4
|
+
// just config.
|
|
5
|
+
import { signal, computed, html, mount } from "/volt.js";
|
|
6
|
+
|
|
7
|
+
const { available, current, defaultPort } = await (await fetch("/setup/state")).json();
|
|
8
|
+
const depsOf = Object.fromEntries(available.map((a) => [a.name, a.dependsOn || []]));
|
|
9
|
+
const order = available.map((a) => a.name);
|
|
10
|
+
const enabledNow = new Set(String(current.VOLT_ADDONS || "").split(",").map((s) => s.trim()).filter(Boolean));
|
|
11
|
+
|
|
12
|
+
const state = signal({
|
|
13
|
+
addons: Object.fromEntries(available.map((a) => [a.name, enabledNow.has(a.name)])),
|
|
14
|
+
dbDriver: current.DB_DRIVER || "memory",
|
|
15
|
+
mongoUri: current.MONGODB_URI || "",
|
|
16
|
+
mongoDb: current.MONGODB_DATABASE || "",
|
|
17
|
+
dbUrl: current.DATABASE_URL || "",
|
|
18
|
+
smtpUrl: current.SMTP_URL || "",
|
|
19
|
+
mailFrom: current.MAIL_FROM || "",
|
|
20
|
+
mediaDriver: current.MEDIA_DRIVER || "local",
|
|
21
|
+
s3Endpoint: current.S3_ENDPOINT || "",
|
|
22
|
+
s3Region: current.S3_REGION || "",
|
|
23
|
+
s3Bucket: current.S3_BUCKET || "",
|
|
24
|
+
s3Key: current.S3_KEY || "",
|
|
25
|
+
s3Secret: current.S3_SECRET || "",
|
|
26
|
+
s3PublicBase: current.S3_PUBLIC_BASE || "",
|
|
27
|
+
port: current.PORT || String(defaultPort),
|
|
28
|
+
});
|
|
29
|
+
const set = (patch) => state({ ...state(), ...patch });
|
|
30
|
+
const toggle = (n) => state({ ...state(), addons: { ...state().addons, [n]: !state().addons[n] } });
|
|
31
|
+
const status = signal("");
|
|
32
|
+
|
|
33
|
+
// selected add-ons, dependencies expanded, in display order
|
|
34
|
+
function effective(s) {
|
|
35
|
+
const want = new Set();
|
|
36
|
+
const visit = (n) => {
|
|
37
|
+
if (want.has(n)) return;
|
|
38
|
+
want.add(n);
|
|
39
|
+
(depsOf[n] || []).forEach(visit);
|
|
40
|
+
};
|
|
41
|
+
for (const n of order) if (s.addons[n]) visit(n);
|
|
42
|
+
return order.filter((n) => want.has(n));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const clean = (v) => String(v).replace(/[\r\n]/g, "").trim(); // one value per line; no injection
|
|
46
|
+
function genEnv(s) {
|
|
47
|
+
const eff = effective(s);
|
|
48
|
+
const out = [`VOLT_ADDONS=${eff.join(",")}`, `PORT=${clean(s.port)}`];
|
|
49
|
+
if (eff.includes("db")) {
|
|
50
|
+
out.push(`DB_DRIVER=${clean(s.dbDriver)}`);
|
|
51
|
+
if (s.dbDriver === "mongodb") {
|
|
52
|
+
out.push(`MONGODB_URI=${clean(s.mongoUri)}`);
|
|
53
|
+
if (s.mongoDb) out.push(`MONGODB_DATABASE=${clean(s.mongoDb)}`);
|
|
54
|
+
} else if (s.dbDriver === "mysql" || s.dbDriver === "postgres") {
|
|
55
|
+
out.push(`DATABASE_URL=${clean(s.dbUrl)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (eff.includes("mailer")) {
|
|
59
|
+
if (s.smtpUrl) out.push(`SMTP_URL=${clean(s.smtpUrl)}`);
|
|
60
|
+
else out.push("# SMTP_URL= # unset → emails print to the console");
|
|
61
|
+
if (s.mailFrom) out.push(`MAIL_FROM=${clean(s.mailFrom)}`);
|
|
62
|
+
}
|
|
63
|
+
if (eff.includes("media")) {
|
|
64
|
+
out.push(`MEDIA_DRIVER=${clean(s.mediaDriver)}`);
|
|
65
|
+
if (s.mediaDriver === "s3") {
|
|
66
|
+
out.push(`S3_ENDPOINT=${clean(s.s3Endpoint)}`);
|
|
67
|
+
out.push(`S3_REGION=${clean(s.s3Region)}`);
|
|
68
|
+
out.push(`S3_BUCKET=${clean(s.s3Bucket)}`);
|
|
69
|
+
out.push(`S3_KEY=${clean(s.s3Key)}`);
|
|
70
|
+
out.push(`S3_SECRET=${clean(s.s3Secret)}`);
|
|
71
|
+
if (s.s3PublicBase) out.push(`S3_PUBLIC_BASE=${clean(s.s3PublicBase)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out.join("\n") + "\n";
|
|
75
|
+
}
|
|
76
|
+
const env = computed(() => genEnv(state()));
|
|
77
|
+
const eff = computed(() => effective(state()));
|
|
78
|
+
// Memoized, primitive-valued derivations: a conditional section keyed on these
|
|
79
|
+
// only re-renders when the *discriminant* changes — not on every keystroke in a
|
|
80
|
+
// field it contains (which would recreate the input and drop focus).
|
|
81
|
+
const dbDriver = computed(() => state().dbDriver);
|
|
82
|
+
const mediaDriver = computed(() => state().mediaDriver);
|
|
83
|
+
const hasDb = computed(() => eff().includes("db"));
|
|
84
|
+
const hasMailer = computed(() => eff().includes("mailer"));
|
|
85
|
+
const hasMedia = computed(() => eff().includes("media"));
|
|
86
|
+
|
|
87
|
+
async function testDb() {
|
|
88
|
+
const s = state();
|
|
89
|
+
const e = { DB_DRIVER: s.dbDriver };
|
|
90
|
+
if (s.dbDriver === "mongodb") {
|
|
91
|
+
e.MONGODB_URI = s.mongoUri;
|
|
92
|
+
e.MONGODB_DATABASE = s.mongoDb;
|
|
93
|
+
} else if (s.dbDriver === "mysql" || s.dbDriver === "postgres") {
|
|
94
|
+
e.DATABASE_URL = s.dbUrl;
|
|
95
|
+
}
|
|
96
|
+
status("Testing connection…");
|
|
97
|
+
try {
|
|
98
|
+
const r = await (await fetch("/setup/test-db", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ env: e }) })).json();
|
|
99
|
+
status(r.ok ? `✓ Connected (${r.driver}).` : `✗ ${r.error}`);
|
|
100
|
+
} catch {
|
|
101
|
+
status("Network error testing connection.");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function apply() {
|
|
106
|
+
status("Saving…");
|
|
107
|
+
let d;
|
|
108
|
+
try {
|
|
109
|
+
d = await (await fetch("/setup/apply", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ addons: eff(), env: env() }) })).json();
|
|
110
|
+
} catch {
|
|
111
|
+
return status("Network error.");
|
|
112
|
+
}
|
|
113
|
+
if (!d.ok) return status("Error: " + d.error);
|
|
114
|
+
status(d.installing?.length ? `Installing ${d.installing.join(", ")}, then starting…` : "Starting the app…");
|
|
115
|
+
const target = `http://localhost:${d.port}/`;
|
|
116
|
+
const tries = d.installing?.length ? 90 : 20; // npm install can take a while
|
|
117
|
+
const go = async (n) => {
|
|
118
|
+
try {
|
|
119
|
+
await fetch(target, { mode: "no-cors" });
|
|
120
|
+
location.href = target;
|
|
121
|
+
} catch {
|
|
122
|
+
if (n > 0) setTimeout(() => go(n - 1), 500);
|
|
123
|
+
else location.href = target;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
setTimeout(() => go(tries), 600);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- views ---
|
|
130
|
+
const field = (label, key, placeholder = "") =>
|
|
131
|
+
html`<div class="mb-2">
|
|
132
|
+
<label class="form-label small mb-1">${label}</label>
|
|
133
|
+
<input class="form-control" placeholder=${placeholder} value=${() => state()[key]} oninput=${(e) => set({ [key]: e.target.value })} />
|
|
134
|
+
</div>`;
|
|
135
|
+
|
|
136
|
+
const addonRow = (a) =>
|
|
137
|
+
html`<div class="form-check mb-2">
|
|
138
|
+
<input class="form-check-input" type="checkbox" id=${"x-" + a.name} checked=${() => state().addons[a.name]} onchange=${() => toggle(a.name)} />
|
|
139
|
+
<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>` : ""}
|
|
141
|
+
<div class="small text-muted">${a.description}</div>
|
|
142
|
+
</label>
|
|
143
|
+
</div>`;
|
|
144
|
+
|
|
145
|
+
const dbSettings = () =>
|
|
146
|
+
html`<div class="mb-2">
|
|
147
|
+
<label class="form-label small mb-1">Database (DB_DRIVER)</label>
|
|
148
|
+
<select class="form-select" value=${() => dbDriver()} onchange=${(e) => set({ dbDriver: e.target.value })}>
|
|
149
|
+
<option value="memory">memory (no setup)</option>
|
|
150
|
+
<option value="mongodb">mongodb</option>
|
|
151
|
+
<option value="mysql">mysql</option>
|
|
152
|
+
<option value="postgres">postgres</option>
|
|
153
|
+
</select>
|
|
154
|
+
</div>
|
|
155
|
+
${() =>
|
|
156
|
+
dbDriver() === "mongodb"
|
|
157
|
+
? html`${field("MONGODB_URI", "mongoUri", "mongodb://user:pass@host:27017/db")}${field("MONGODB_DATABASE", "mongoDb", "db")}`
|
|
158
|
+
: dbDriver() === "mysql" || dbDriver() === "postgres"
|
|
159
|
+
? field("DATABASE_URL", "dbUrl", dbDriver() + "://user:pass@host/db")
|
|
160
|
+
: null}
|
|
161
|
+
${() => (dbDriver() !== "memory" ? html`<button class="btn btn-sm btn-outline-secondary mb-2" onclick=${testDb}>Test connection</button>` : null)}`;
|
|
162
|
+
|
|
163
|
+
const mediaSettings = () =>
|
|
164
|
+
html`<div class="mb-2">
|
|
165
|
+
<label class="form-label small mb-1">Media storage (MEDIA_DRIVER)</label>
|
|
166
|
+
<select class="form-select" value=${() => mediaDriver()} onchange=${(e) => set({ mediaDriver: e.target.value })}>
|
|
167
|
+
<option value="local">local (disk)</option>
|
|
168
|
+
<option value="s3">s3 — AWS S3 / DigitalOcean Spaces</option>
|
|
169
|
+
</select>
|
|
170
|
+
</div>
|
|
171
|
+
${() =>
|
|
172
|
+
mediaDriver() === "s3"
|
|
173
|
+
? html`${field("S3_ENDPOINT", "s3Endpoint", "https://nyc3.digitaloceanspaces.com")}${field("S3_REGION", "s3Region", "us-east-1")}${field("S3_BUCKET", "s3Bucket", "my-space")}${field("S3_KEY", "s3Key", "access key")}${field("S3_SECRET", "s3Secret", "secret key")}${field("S3_PUBLIC_BASE (optional CDN base)", "s3PublicBase", "https://cdn.example.com")}`
|
|
174
|
+
: null}`;
|
|
175
|
+
|
|
176
|
+
mount(
|
|
177
|
+
"#app",
|
|
178
|
+
available.length
|
|
179
|
+
? html`<div class="card-x p-4 mb-3">
|
|
180
|
+
<h2 class="h6 mb-3">Features</h2>
|
|
181
|
+
${available.map(addonRow)}
|
|
182
|
+
<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p>
|
|
183
|
+
</div>`
|
|
184
|
+
: null,
|
|
185
|
+
html`<div class="card-x p-4 mb-3">
|
|
186
|
+
<h2 class="h6 mb-3">Settings</h2>
|
|
187
|
+
${field("PORT", "port", String(defaultPort))}
|
|
188
|
+
${() => (hasDb() ? dbSettings() : null)}
|
|
189
|
+
${() => (hasMailer() ? html`${field("SMTP_URL (optional)", "smtpUrl", "smtp://user:pass@smtp.host:587")}${field("MAIL_FROM", "mailFrom", "App <no-reply@you.com>")}` : null)}
|
|
190
|
+
${() => (hasMedia() ? mediaSettings() : null)}
|
|
191
|
+
</div>`,
|
|
192
|
+
html`<div class="card-x p-4 mb-3">
|
|
193
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
194
|
+
<h2 class="h6 mb-0">.env</h2>
|
|
195
|
+
<button class="btn btn-primary btn-sm" onclick=${apply}>Apply & start →</button>
|
|
196
|
+
</div>
|
|
197
|
+
<pre class="mb-0" style="background:#0b0d11;border:1px solid #232a36;border-radius:10px;padding:12px;color:#cfe3ff;white-space:pre-wrap">${env}</pre>
|
|
198
|
+
</div>`,
|
|
199
|
+
() => (status() ? html`<p class="small accent">${status}</p>` : null),
|
|
200
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
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>Volt Studio</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
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<main class="container py-5" style="max-width: 820px;">
|
|
17
|
+
<header class="mb-4">
|
|
18
|
+
<h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt Studio</span> <small class="text-muted">data browser</small></h1>
|
|
19
|
+
<p class="text-muted mb-0">Browse the database in your <code>.env</code>. Disposable & localhost-only — close the terminal when done.</p>
|
|
20
|
+
</header>
|
|
21
|
+
<div id="app"></div>
|
|
22
|
+
</main>
|
|
23
|
+
<script type="module">
|
|
24
|
+
import { mount } from "/volt.js";
|
|
25
|
+
import { dbAdminPanel } from "/db-admin-ui.js";
|
|
26
|
+
mount("#app", dbAdminPanel());
|
|
27
|
+
</script>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
<title>My Volt Blog</title>
|
|
7
|
+
<meta name="description" content="A blog built with Volt — markdown posts, a theme, and real SEO." />
|
|
8
|
+
<link rel="icon" href="/favicon.webp" />
|
|
9
|
+
<!-- the same stylesheet the posts/pages use, so the home matches the theme -->
|
|
10
|
+
<link rel="stylesheet" href="/_theme.css" />
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<header class="site"><div class="wrap"><a class="brand" href="/"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> My Volt Blog</a>
|
|
14
|
+
<nav><a href="/blog">Blog</a><a href="/about">About</a><a href="/feed.xml">RSS</a></nav></div></header>
|
|
15
|
+
<main><div class="wrap">
|
|
16
|
+
<h1>A blog built with Volt</h1>
|
|
17
|
+
<p>Markdown posts in <code>posts/</code>, pages in <code>pages/</code>, one theme in
|
|
18
|
+
<code>pages/_theme.js</code>. No build step, no database, no admin to attack.</p>
|
|
19
|
+
<p><a href="/blog">Read the blog →</a></p>
|
|
20
|
+
<p style="opacity:.7;font-size:.95rem">Edit any <code>.md</code> file and refresh. Add posts, change the theme,
|
|
21
|
+
or run <code>npm run dev -- --edit</code> to add features (auth, a database, the WYSIWYG editor).</p>
|
|
22
|
+
</div></main>
|
|
23
|
+
<footer class="site"><div class="wrap">My Volt Blog — built with Volt</div></footer>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
@@ -225,6 +225,8 @@ function startSetup() {
|
|
|
225
225
|
const assets = {
|
|
226
226
|
"/setup.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "setup", "setup.js"))],
|
|
227
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"))],
|
|
228
230
|
};
|
|
229
231
|
const indexHtml = fs.readFileSync(path.join(__dirname, "setup", "index.html"));
|
|
230
232
|
|
|
@@ -381,6 +383,8 @@ async function startStudio() {
|
|
|
381
383
|
const visible = (n) => n && !HIDDEN_COLLECTIONS.has(n);
|
|
382
384
|
const assets = {
|
|
383
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"))],
|
|
384
388
|
"/db-admin-ui.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(ADDONS_DIR, "db", "files", "public", "db-admin-ui.js"))],
|
|
385
389
|
};
|
|
386
390
|
const studioHtml = fs.readFileSync(path.join(__dirname, "setup", "studio.html"));
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
6
7
|
<title>Set up your Volt app</title>
|
|
7
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
9
|
<style>
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
<body>
|
|
18
19
|
<main class="container py-5" style="max-width: 640px;">
|
|
19
20
|
<header class="mb-4">
|
|
20
|
-
<h1 class="h3"><span class="accent"
|
|
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>
|
|
21
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>
|
|
22
23
|
</header>
|
|
23
24
|
<div id="app"></div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
6
7
|
<title>Volt Studio</title>
|
|
7
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
9
|
<style>
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
<body>
|
|
15
16
|
<main class="container py-5" style="max-width: 820px;">
|
|
16
17
|
<header class="mb-4">
|
|
17
|
-
<h1 class="h3"><span class="accent"
|
|
18
|
+
<h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt Studio</span> <small class="text-muted">data browser</small></h1>
|
|
18
19
|
<p class="text-muted mb-0">Browse the database in your <code>.env</code>. Disposable & localhost-only — close the terminal when done.</p>
|
|
19
20
|
</header>
|
|
20
21
|
<div id="app"></div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
6
7
|
<title>Volt — signals, no build, hot reload</title>
|
|
7
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
9
|
<style>
|
|
@@ -14,12 +15,13 @@
|
|
|
14
15
|
.form-control:focus { background: #0f1115; color: #e7e9ee; border-color: #ffd24a; box-shadow: none; }
|
|
15
16
|
.list-group-item { background: #0f1115; color: #e7e9ee; border-color: #232a36; }
|
|
16
17
|
a { color: #ffd24a; }
|
|
18
|
+
.text-muted { color: #9aa4b2 !important; } /* readable muted on the dark bg */
|
|
17
19
|
</style>
|
|
18
20
|
</head>
|
|
19
21
|
<body>
|
|
20
22
|
<main class="container py-5" style="max-width: 720px;">
|
|
21
23
|
<header class="text-center mb-5">
|
|
22
|
-
<h1 class="brand display-5"><span class="accent"
|
|
24
|
+
<h1 class="brand display-5"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt</span></h1>
|
|
23
25
|
<p class="text-muted mb-0">
|
|
24
26
|
Fine-grained signals · no JSX · no virtual DOM · live hot reload.
|
|
25
27
|
Edit <code>public/app.js</code> and save — this page reloads itself.
|
|
@@ -29,10 +31,7 @@
|
|
|
29
31
|
<div id="app"></div>
|
|
30
32
|
|
|
31
33
|
<footer class="text-center text-muted mt-5">
|
|
32
|
-
<small
|
|
33
|
-
<div class="mt-2">
|
|
34
|
-
<small><a href="https://github.com/MIR-2025/volt#readme" target="_blank" rel="noopener">📖 How to build a Volt app →</a></small>
|
|
35
|
-
</div>
|
|
34
|
+
<small><a href="https://github.com/MIR-2025/volt#readme" target="_blank" rel="noopener">📖 How to build a Volt app →</a></small>
|
|
36
35
|
</footer>
|
|
37
36
|
</main>
|
|
38
37
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Volt app — production container. Runs on Render, Fly.io, Railway, DO App
|
|
2
|
+
# Platform, and any container host. They handle the server, DNS, and TLS; you
|
|
3
|
+
# just set config as env vars.
|
|
4
|
+
#
|
|
5
|
+
# Configure via the platform's env vars (NOT a committed .env):
|
|
6
|
+
# VOLT_ADDONS=db,auth,... DB_DRIVER=... MONGODB_URI / DATABASE_URL
|
|
7
|
+
# MEDIA_DRIVER=s3 S3_ENDPOINT/S3_REGION/S3_BUCKET/S3_KEY/S3_SECRET etc.
|
|
8
|
+
#
|
|
9
|
+
# Tip: run the local wizard first (`npm run dev`) so the add-on packages are
|
|
10
|
+
# saved into package.json; commit package.json, then deploy and set the same
|
|
11
|
+
# config as env vars here. NODE_ENV=production makes the app boot straight up
|
|
12
|
+
# (no setup wizard).
|
|
13
|
+
FROM node:22-alpine
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
ENV NODE_ENV=production
|
|
16
|
+
COPY package*.json ./
|
|
17
|
+
RUN npm install --omit=dev
|
|
18
|
+
COPY . .
|
|
19
|
+
EXPOSE 8080
|
|
20
|
+
CMD ["node", "server.js"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
web: node server.js
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Volt docs
|
|
2
|
+
|
|
3
|
+
A documentation site built with [Volt](https://voltjs.com) — markdown pages in a sidebar layout. No build step, no database.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm install
|
|
7
|
+
npm run dev # → http://localhost:26629 (redirects to /getting-started)
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Where things live
|
|
11
|
+
|
|
12
|
+
| Path | What |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `pages/*.md` | Each becomes a doc page at `/<slug>` |
|
|
15
|
+
| `pages/_theme.js` | The sidebar layout + CSS. Add pages to the `NAV` list here |
|
|
16
|
+
| `views/index.html` | Redirects `/` to the first page |
|
|
17
|
+
| `.env` | `VOLT_ADDONS=pages`, `SITE_NAME` |
|
|
18
|
+
|
|
19
|
+
## Add a page
|
|
20
|
+
|
|
21
|
+
1. Create `pages/my-topic.md` with front-matter (`title`, `description`).
|
|
22
|
+
2. Add `["/my-topic", "My topic"]` to `NAV` in `pages/_theme.js`.
|
|
23
|
+
|
|
24
|
+
Run `npm run dev -- --edit` to add features (auth, a database, the WYSIWYG editor), or set `THEME=<name>` to use a published `volt-theme-*`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Fly.io — run `fly launch` (it uses the Dockerfile), then set config:
|
|
2
|
+
# fly secrets set VOLT_ADDONS=db,auth DB_DRIVER=mongodb MONGODB_URI=...
|
|
3
|
+
app = "volt-app"
|
|
4
|
+
|
|
5
|
+
[build]
|
|
6
|
+
|
|
7
|
+
[http_service]
|
|
8
|
+
internal_port = 8080
|
|
9
|
+
force_https = true
|
|
10
|
+
auto_stop_machines = true
|
|
11
|
+
auto_start_machines = true
|
|
12
|
+
|
|
13
|
+
[env]
|
|
14
|
+
PORT = "8080"
|
|
15
|
+
NODE_ENV = "production"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "volt-docs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "A Volt app — no-build, signals-based UI with Socket.io hot reload.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "server.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "node server.js",
|
|
10
|
+
"dev": "node server.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"express": "^4.22.2",
|
|
14
|
+
"socket.io": "^4.8.3",
|
|
15
|
+
"marked": "^18.0.5"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// _theme.js — the docs theme: a fixed sidebar + a content column. Add a page in
|
|
2
|
+
// pages/ and link it in NAV below. `css` is served at /_theme.css.
|
|
3
|
+
const NAME = process.env.SITE_NAME || "Docs";
|
|
4
|
+
const NAV = [
|
|
5
|
+
["/getting-started", "Getting started"],
|
|
6
|
+
["/configuration", "Configuration"],
|
|
7
|
+
["/deployment", "Deployment"],
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export const css = `:root{--ink:#1f2329;--bg:#fff;--accent:#2557d6;--muted:#5b6573;--line:#e7eaef;--side:#f7f8fa}
|
|
11
|
+
*{box-sizing:border-box}
|
|
12
|
+
body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.7 system-ui,-apple-system,sans-serif;display:flex;min-height:100vh}
|
|
13
|
+
aside{width:240px;flex:0 0 240px;background:var(--side);border-right:1px solid var(--line);padding:1.5rem}
|
|
14
|
+
aside .brand{display:flex;align-items:center;gap:.4rem;font-weight:800;color:var(--ink);text-decoration:none;margin-bottom:1rem}
|
|
15
|
+
aside nav a{display:block;color:var(--muted);text-decoration:none;padding:.3rem 0}
|
|
16
|
+
aside nav a:hover{color:var(--accent)}
|
|
17
|
+
main{flex:1;min-width:0;padding:2.5rem;max-width:780px}
|
|
18
|
+
h1,h2,h3{line-height:1.25}
|
|
19
|
+
a{color:var(--accent)}
|
|
20
|
+
pre{background:#0b0d11;color:#cfe3ff;padding:1rem;border-radius:8px;overflow:auto}
|
|
21
|
+
:not(pre)>code{background:#eef1f5;padding:.1em .35em;border-radius:5px}
|
|
22
|
+
img{max-width:100%}
|
|
23
|
+
@media(max-width:700px){body{flex-direction:column}aside{width:auto;flex:none;border-right:none;border-bottom:1px solid var(--line)}}`;
|
|
24
|
+
|
|
25
|
+
export function layout({ title, head, content }) {
|
|
26
|
+
const nav = NAV.map(([href, label]) => `<a href="${href}">${label}</a>`).join("");
|
|
27
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
28
|
+
<title>${title}</title>${head}<link rel="icon" href="/favicon.webp" /><link rel="stylesheet" href="/_theme.css" /></head><body>
|
|
29
|
+
<aside><a class="brand" href="/getting-started"><img src="/logo.webp" alt="" style="height:1.1em" /> ${NAME}</a><nav>${nav}</nav></aside>
|
|
30
|
+
<main>${content}</main>
|
|
31
|
+
</body></html>`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Configuration
|
|
3
|
+
description: Everything is environment variables.
|
|
4
|
+
---
|
|
5
|
+
# Configuration
|
|
6
|
+
|
|
7
|
+
Volt is configured entirely with environment variables (no config-in-database):
|
|
8
|
+
|
|
9
|
+
- `SITE_NAME` — shown in the sidebar.
|
|
10
|
+
- `VOLT_ADDONS` — which features are on (here: `pages`).
|
|
11
|
+
- `THEME` — a published `volt-theme-*`, or use the local `pages/_theme.js`.
|
|
12
|
+
|
|
13
|
+
In dev these live in `.env`; in production set them as platform env vars.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Deployment
|
|
3
|
+
description: Ship it anywhere Node runs.
|
|
4
|
+
---
|
|
5
|
+
# Deployment
|
|
6
|
+
|
|
7
|
+
This template ships a `Dockerfile`, `render.yaml`, `fly.toml`, and a `Procfile`.
|
|
8
|
+
Set `NODE_ENV=production` (and your env vars) and it boots straight into the app
|
|
9
|
+
— no wizard, no `.env` needed.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Getting started
|
|
3
|
+
description: Your first five minutes with this docs site.
|
|
4
|
+
---
|
|
5
|
+
# Getting started
|
|
6
|
+
|
|
7
|
+
Every page here is a markdown file in `pages/`, served at `/<slug>` inside the
|
|
8
|
+
docs theme (`pages/_theme.js`). To add a page, drop a `.md` file and link it in
|
|
9
|
+
the sidebar (`NAV` in the theme).
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npm run dev # start
|
|
13
|
+
npm run dev -- --edit # wizard: add auth, a database, the editor…
|
|
14
|
+
```
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// volt-ssr.js — server-side rendering for Volt. Renders the same html`` markup,
|
|
2
|
+
// h() elements, and signal values to an HTML string in Node (no DOM), so a Volt
|
|
3
|
+
// app can be fully server-rendered for SEO and hydrate interactive islands with
|
|
4
|
+
// volt.js on the client.
|
|
5
|
+
//
|
|
6
|
+
// import { html, h, raw, renderToString } from "./volt-ssr.js";
|
|
7
|
+
// renderToString(html`<p>${name}</p>`) // → "<p>Ada</p>" (name is escaped)
|
|
8
|
+
//
|
|
9
|
+
// Authoring matches the client: html`` interpolations render as escaped text;
|
|
10
|
+
// nest html``/h() nodes for structure; use raw() for trusted pre-built HTML.
|
|
11
|
+
|
|
12
|
+
const VOID = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
|
|
13
|
+
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
14
|
+
|
|
15
|
+
// trusted, pre-rendered HTML — emitted verbatim. Use only for content you control.
|
|
16
|
+
export const raw = (s) => ({ __raw: String(s) });
|
|
17
|
+
|
|
18
|
+
// tagged-template markup; the literal chunks are trusted, ${values} are escaped.
|
|
19
|
+
export const html = (strings, ...values) => ({ __tpl: true, strings, values });
|
|
20
|
+
|
|
21
|
+
const isNode = (x) => x && typeof x === "object" && (x.__tpl || x.__raw || x.__el);
|
|
22
|
+
|
|
23
|
+
// hyperscript element: h(tag, props?, ...children)
|
|
24
|
+
export function h(tag, props, ...children) {
|
|
25
|
+
if (props === undefined || props === null || isNode(props) || Array.isArray(props) || typeof props !== "object") {
|
|
26
|
+
if (props !== undefined && props !== null) children.unshift(props);
|
|
27
|
+
props = {};
|
|
28
|
+
}
|
|
29
|
+
return { __el: true, tag, props, children };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const read = (v) => (typeof v === "function" ? v() : v); // resolve signals/thunks (once)
|
|
33
|
+
|
|
34
|
+
function attrs(props) {
|
|
35
|
+
let out = "";
|
|
36
|
+
for (const [k, rawVal] of Object.entries(props)) {
|
|
37
|
+
if (k === "children" || k.startsWith("on")) continue; // event handlers don't SSR
|
|
38
|
+
const v = read(rawVal);
|
|
39
|
+
if (v == null || v === false) continue;
|
|
40
|
+
const name = k === "className" ? "class" : k;
|
|
41
|
+
out += v === true ? ` ${name}` : ` ${name}="${esc(v)}"`;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function renderToString(node) {
|
|
47
|
+
const v = read(node);
|
|
48
|
+
if (v == null || v === false || v === true) return "";
|
|
49
|
+
if (typeof v === "string" || typeof v === "number") return esc(v);
|
|
50
|
+
if (v.__raw != null) return v.__raw;
|
|
51
|
+
if (Array.isArray(v)) return v.map(renderToString).join("");
|
|
52
|
+
if (v.__tpl) {
|
|
53
|
+
let out = v.strings[0];
|
|
54
|
+
for (let i = 0; i < v.values.length; i++) out += renderToString(v.values[i]) + v.strings[i + 1];
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
if (v.__el) {
|
|
58
|
+
if (typeof v.tag === "function") return renderToString(v.tag({ ...v.props, children: v.children }));
|
|
59
|
+
const open = `<${v.tag}${attrs(v.props)}>`;
|
|
60
|
+
return VOID.has(v.tag) ? open : `${open}${v.children.map(renderToString).join("")}</${v.tag}>`;
|
|
61
|
+
}
|
|
62
|
+
return esc(String(v));
|
|
63
|
+
}
|