create-volt 0.33.0 → 0.35.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 CHANGED
@@ -4,6 +4,23 @@ 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.35.0] - 2026-06-29
8
+
9
+ ### Fixed
10
+ - **Setup wizard: dependency add-ons now show as checked.** Enabling `auth` pulls
11
+ in `db` + `mailer` (its dependencies), which were added to `VOLT_ADDONS` but
12
+ whose checkboxes stayed *unchecked* — so the generated `.env` looked like it had
13
+ add-ons you never picked. Pulled-in dependencies now render **checked + disabled**
14
+ with a "required by <add-on>" note, so the checkboxes always match `VOLT_ADDONS`.
15
+
16
+ ## [0.34.0] - 2026-06-29
17
+
18
+ ### Added
19
+ - **Timezone detection.** The setup wizard detects the admin's timezone from
20
+ their browser (`Intl`) and writes `SITE_TZ` to `.env`. The `posts` add-on then
21
+ renders full timestamps in `SITE_TZ` rather than the server's zone (usually UTC
22
+ on a host); date-only values render as that calendar day either way.
23
+
7
24
  ## [0.33.0] - 2026-06-29
8
25
 
9
26
  ### Added
@@ -446,6 +463,8 @@ All notable changes to `create-volt` are documented here. The format follows
446
463
  watching and full-page hot reload. Supports `--skip-install` and `--force`,
447
464
  and auto-detects npm / pnpm / yarn / bun for the install step.
448
465
 
466
+ [0.35.0]: https://github.com/MIR-2025/volt/releases/tag/v0.35.0
467
+ [0.34.0]: https://github.com/MIR-2025/volt/releases/tag/v0.34.0
449
468
  [0.33.0]: https://github.com/MIR-2025/volt/releases/tag/v0.33.0
450
469
  [0.32.0]: https://github.com/MIR-2025/volt/releases/tag/v0.32.0
451
470
  [0.31.0]: https://github.com/MIR-2025/volt/releases/tag/v0.31.0
@@ -17,11 +17,15 @@ const tagsOf = (meta) => String(meta.tags || "").split(",").map((s) => s.trim())
17
17
 
18
18
  function fmtDate(d) {
19
19
  if (!d) return "";
20
- // parse YYYY-MM-DD as *local* midnight (new Date("2026-06-28") is UTC → off by a
21
- // day in negative-offset zones); fall back to Date() for full timestamps.
20
+ const opts = { year: "numeric", month: "long", day: "numeric" };
21
+ // A date-only value (YYYY-MM-DD) is a calendar day with no timezone — parse it
22
+ // as local midnight and render that day (new Date("2026-06-28") would be UTC →
23
+ // off by a day in negative-offset zones). A full timestamp is rendered in the
24
+ // admin's timezone (SITE_TZ, detected by the setup wizard), not the server's.
22
25
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(d).trim());
23
- const t = m ? new Date(+m[1], +m[2] - 1, +m[3]) : new Date(d);
24
- return isNaN(t.getTime()) ? esc(d) : t.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
26
+ if (m) return new Date(+m[1], +m[2] - 1, +m[3]).toLocaleDateString("en-US", opts);
27
+ const t = new Date(d);
28
+ return isNaN(t.getTime()) ? esc(d) : t.toLocaleDateString("en-US", process.env.SITE_TZ ? { ...opts, timeZone: process.env.SITE_TZ } : opts);
25
29
  }
26
30
 
27
31
  function excerpt(p) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-volt",
3
- "version": "0.33.0",
3
+ "version": "0.35.0",
4
4
  "description": "Scaffold a new Volt app — no-build, signals-based UI with Socket.io hot reload.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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} checked=${() => state().addons[a.name]} onchange=${() => toggle(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>`;
@@ -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} checked=${() => state().addons[a.name]} onchange=${() => toggle(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>`;