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.
Files changed (51) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/addons/posts/files/lib/posts.js +4 -1
  3. package/index.js +1 -1
  4. package/package.json +1 -1
  5. package/templates/blog/Dockerfile +20 -0
  6. package/templates/blog/Procfile +1 -0
  7. package/templates/blog/README.md +38 -0
  8. package/templates/blog/dockerignore +6 -0
  9. package/templates/blog/env +3 -0
  10. package/templates/blog/fly.toml +15 -0
  11. package/templates/blog/gitignore +5 -0
  12. package/templates/blog/package.json +17 -0
  13. package/templates/blog/pages/_theme.js +35 -0
  14. package/templates/blog/pages/about.md +8 -0
  15. package/templates/blog/posts/2026-06-10-themes.md +11 -0
  16. package/templates/blog/posts/2026-06-20-markdown-and-seo.md +14 -0
  17. package/templates/blog/posts/2026-06-28-hello-volt.md +17 -0
  18. package/templates/blog/public/favicon.webp +0 -0
  19. package/templates/blog/public/logo.webp +0 -0
  20. package/templates/blog/public/volt-ssr.js +63 -0
  21. package/templates/blog/public/volt.js +273 -0
  22. package/templates/blog/render.yaml +15 -0
  23. package/templates/blog/server.js +464 -0
  24. package/templates/blog/setup/index.html +28 -0
  25. package/templates/blog/setup/setup.js +200 -0
  26. package/templates/blog/setup/studio.html +29 -0
  27. package/templates/blog/views/index.html +25 -0
  28. package/templates/default/views/index.html +2 -4
  29. package/templates/docs/Dockerfile +20 -0
  30. package/templates/docs/Procfile +1 -0
  31. package/templates/docs/README.md +24 -0
  32. package/templates/docs/dockerignore +6 -0
  33. package/templates/docs/env +2 -0
  34. package/templates/docs/fly.toml +15 -0
  35. package/templates/docs/gitignore +5 -0
  36. package/templates/docs/package.json +17 -0
  37. package/templates/docs/pages/_theme.js +32 -0
  38. package/templates/docs/pages/configuration.md +13 -0
  39. package/templates/docs/pages/deployment.md +9 -0
  40. package/templates/docs/pages/getting-started.md +14 -0
  41. package/templates/docs/public/favicon.webp +0 -0
  42. package/templates/docs/public/logo.webp +0 -0
  43. package/templates/docs/public/volt-ssr.js +63 -0
  44. package/templates/docs/public/volt.js +273 -0
  45. package/templates/docs/render.yaml +15 -0
  46. package/templates/docs/server.js +464 -0
  47. package/templates/docs/setup/index.html +28 -0
  48. package/templates/docs/setup/setup.js +200 -0
  49. package/templates/docs/setup/studio.html +29 -0
  50. package/templates/docs/views/index.html +15 -0
  51. package/templates/starter/views/index.html +1 -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 &amp; 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,15 @@
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>Docs</title>
7
+ <link rel="icon" href="/favicon.webp" />
8
+ <!-- the docs home redirects to the first page; edit pages/ + the theme's NAV -->
9
+ <meta http-equiv="refresh" content="0; url=/getting-started" />
10
+ <link rel="canonical" href="/getting-started" />
11
+ </head>
12
+ <body>
13
+ <p>Redirecting to <a href="/getting-started">the docs →</a></p>
14
+ </body>
15
+ </html>
@@ -17,6 +17,7 @@
17
17
  .navx .btn-link { color: #9aa4b2; text-decoration: none; }
18
18
  .navx .btn-link.active { color: #ffd24a; }
19
19
  a { color: #ffd24a; }
20
+ .text-muted { color: #9aa4b2 !important; } /* readable muted on the dark bg */
20
21
  </style>
21
22
  </head>
22
23
  <body>