create-volt 0.21.0 → 0.23.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,39 @@ 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.23.0] - 2026-06-29
8
+
9
+ ### Added
10
+ - Plugin context now includes **`requireAuth`** and **`sessionFromReq`** (when the
11
+ auth add-on is on) so third-party add-ons can gate routes by login. Purely
12
+ additive — no change to defaults or security posture.
13
+
14
+ ### Note
15
+ - New companion package **`volt-addon-editor`** (separate npm package): a
16
+ standing, role-gated RTEPro WYSIWYG editor that writes markdown pages. Mounts
17
+ only if `ADMIN_PATH` is set (**fail-closed**), behind magic-link auth + an
18
+ `ADMIN_EMAILS` allowlist; the secret path is obscurity *on top of* auth, never
19
+ instead. The AI key stays server-side via a key-injecting proxy. The core stays
20
+ no-standing-admin by default — install only where you want it
21
+ (`npx create-volt add editor`). See `/docs/editor`.
22
+
23
+ ## [0.22.0] - 2026-06-29
24
+
25
+ ### Added
26
+ - **Third-party add-ons (plugins) — the WordPress-plugin equivalent.** Any
27
+ `VOLT_ADDONS` entry that is not built-in is loaded from a local
28
+ `.volt/addons/<name>/index.js` or an installed npm package
29
+ `volt-addon-<name>`, and wired via a single `register(ctx)` export
30
+ (ctx = app, express, io, store, mailer, env, log). Install functionality as
31
+ small, owned packages instead of dashboard plugins.
32
+ - `create-volt add <name>` — install `volt-addon-<name>` and enable it.
33
+ - `create-volt create-addon <name>` — scaffold a publishable add-on package.
34
+ - New `/docs/plugins`.
35
+
36
+ ### Fixed
37
+ - `availableAddons()` tolerates add-on directories without `meta.json` (so
38
+ local third-party add-ons do not break the wizard).
39
+
7
40
  ## [0.21.0] - 2026-06-29
8
41
 
9
42
  ### Added
@@ -301,6 +334,8 @@ All notable changes to `create-volt` are documented here. The format follows
301
334
  watching and full-page hot reload. Supports `--skip-install` and `--force`,
302
335
  and auto-detects npm / pnpm / yarn / bun for the install step.
303
336
 
337
+ [0.23.0]: https://github.com/MIR-2025/volt/releases/tag/v0.23.0
338
+ [0.22.0]: https://github.com/MIR-2025/volt/releases/tag/v0.22.0
304
339
  [0.21.0]: https://github.com/MIR-2025/volt/releases/tag/v0.21.0
305
340
  [0.20.0]: https://github.com/MIR-2025/volt/releases/tag/v0.20.0
306
341
  [0.19.0]: https://github.com/MIR-2025/volt/releases/tag/v0.19.0
package/index.js CHANGED
@@ -94,9 +94,93 @@ const force = flags.has("--force");
94
94
  const dryRun = flags.has("--dry-run");
95
95
  const noGit = flags.has("--no-git");
96
96
 
97
- // The `add` command was replaced by `config` catch old muscle memory.
97
+ // Scaffolded files for `create-addon` (a publishable third-party add-on).
98
+ const ADDON_INDEX = [
99
+ 'import path from "node:path";',
100
+ 'import { fileURLToPath } from "node:url";',
101
+ "",
102
+ "const __dirname = path.dirname(fileURLToPath(import.meta.url));",
103
+ "",
104
+ "// A Volt add-on. register(ctx) runs once at startup.",
105
+ "// ctx = { app, io, store, mailer, env, log, express }",
106
+ "// - app: the Express app (add routes)",
107
+ "// - io: Socket.io server (if the realtime add-on is on)",
108
+ "// - store: the database store (collection(name).{put,get,all,find,delete}) (if db is on)",
109
+ "// - mailer: send mail (if the mailer add-on is on)",
110
+ "// - express: the host's Express — use express.static / express.Router without installing it",
111
+ "export function register({ app, express, store, log }) {",
112
+ ' // serve this add-on\'s frontend assets (public/) at /__NAME__',
113
+ ' app.use("/__NAME__", express.static(path.join(__dirname, "public")));',
114
+ "",
115
+ " // a tiny example API",
116
+ ' app.get("/api/__NAME__/ping", (_req, res) => res.json({ ok: true, addon: "__NAME__" }));',
117
+ "",
118
+ ' log("ready");',
119
+ "}",
120
+ "",
121
+ ].join("\n");
122
+
123
+ const ADDON_README = [
124
+ "# volt-addon-__NAME__",
125
+ "",
126
+ "A [Volt](https://voltjs.com) add-on.",
127
+ "",
128
+ "## Install (inside a Volt app)",
129
+ "",
130
+ "```",
131
+ "npx create-volt add __NAME__",
132
+ "```",
133
+ "",
134
+ "That installs this package and adds `__NAME__` to `VOLT_ADDONS` in `.env`.",
135
+ "",
136
+ "## Develop",
137
+ "",
138
+ "Edit `index.js` — implement `register(ctx)` (see the context it receives). Then:",
139
+ "",
140
+ "```",
141
+ "npm publish",
142
+ "```",
143
+ "",
144
+ ].join("\n");
145
+
146
+ // --- `add` subcommand: install a third-party add-on and enable it ---
98
147
  if (positionals[0] === "add") {
99
- die(`${cyan("add")} was replaced by ${cyan("create-volt config")} — run that to add integrations.`);
148
+ const name = positionals[1];
149
+ if (!name) die(`Usage: ${cyan("create-volt add <name>")} — installs ${cyan("volt-addon-<name>")} and enables it (run inside a Volt app).`);
150
+ const cwd = process.cwd();
151
+ if (!fs.existsSync(path.join(cwd, "server.js"))) die(`Run ${cyan("create-volt add")} from inside a Volt app (no ${cyan("server.js")} here).`);
152
+ const pkg = /^(@|volt-addon-)/.test(name) ? name : `volt-addon-${name}`;
153
+ const short = pkg.replace(/^@[^/]+\//, "").replace(/^volt-addon-/, "");
154
+ console.log(dim(`Installing ${pkg}…`));
155
+ const res = spawnSync("npm", ["install", pkg], { cwd, stdio: "inherit", shell: process.platform === "win32" });
156
+ if (res.status) die(`npm install ${pkg} failed.`);
157
+ const envPath = path.join(cwd, ".env");
158
+ let env = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
159
+ const m = env.match(/^\s*VOLT_ADDONS\s*=(.*)$/m);
160
+ const list = (m ? m[1] : "").split(",").map((s) => s.trim()).filter(Boolean);
161
+ if (!list.includes(short)) list.push(short);
162
+ const line = `VOLT_ADDONS=${list.join(",")}`;
163
+ env = m ? env.replace(/^\s*VOLT_ADDONS\s*=.*$/m, line) : env.replace(/\n*$/, env ? "\n" : "") + line + "\n";
164
+ fs.writeFileSync(envPath, env);
165
+ console.log(`${cyan("✓ added")} ${short} — restart with ${cyan("npm run dev")}`);
166
+ process.exit(0);
167
+ }
168
+
169
+ // --- `create-addon` subcommand: scaffold a publishable third-party add-on ---
170
+ if (positionals[0] === "create-addon") {
171
+ const name = positionals[1];
172
+ if (!name || !/^[a-z0-9][a-z0-9-]*$/.test(name)) die(`Usage: ${cyan("create-volt create-addon <name>")} — name: lowercase letters, numbers, hyphens.`);
173
+ const dir = path.resolve(`volt-addon-${name}`);
174
+ if (fs.existsSync(dir) && !flags.has("--force")) die(`${cyan(dir)} already exists (use ${cyan("--force")}).`);
175
+ fs.mkdirSync(path.join(dir, "public"), { recursive: true });
176
+ const pkgJson = { name: `volt-addon-${name}`, version: "0.1.0", description: `A Volt add-on: ${name}`, type: "module", main: "index.js", keywords: ["volt", "volt-addon"], files: ["index.js", "public"], license: "MIT" };
177
+ fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify(pkgJson, null, 2) + "\n");
178
+ fs.writeFileSync(path.join(dir, "index.js"), ADDON_INDEX.replace(/__NAME__/g, name));
179
+ fs.writeFileSync(path.join(dir, "README.md"), ADDON_README.replace(/__NAME__/g, name));
180
+ console.log(`${cyan("✓ created")} ${path.relative(process.cwd(), dir) || dir} — a Volt add-on.`);
181
+ console.log(dim(` edit index.js (register), then publish: cd volt-addon-${name} && npm publish`));
182
+ console.log(dim(` users install with: npx create-volt add ${name}`));
183
+ process.exit(0);
100
184
  }
101
185
 
102
186
  // --- `update` subcommand: refresh public/volt.js in the current app to the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-volt",
3
- "version": "0.21.0",
3
+ "version": "0.23.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": {
@@ -43,7 +43,7 @@ function availableAddons() {
43
43
  if (!fs.existsSync(ADDONS_DIR)) return [];
44
44
  return fs
45
45
  .readdirSync(ADDONS_DIR, { withFileTypes: true })
46
- .filter((e) => e.isDirectory())
46
+ .filter((e) => e.isDirectory() && fs.existsSync(path.join(ADDONS_DIR, e.name, "meta.json"))) // skip local 3rd-party add-ons (no meta)
47
47
  .map((e) => {
48
48
  const m = JSON.parse(fs.readFileSync(path.join(ADDONS_DIR, e.name, "meta.json"), "utf8"));
49
49
  return { name: e.name, description: m.description, dependsOn: m.dependsOn || [] };
@@ -55,9 +55,9 @@ function enabledFrom(env) {
55
55
  const metas = Object.fromEntries(availableAddons().map((a) => [a.name, a]));
56
56
  const out = new Set();
57
57
  const visit = (n) => {
58
- if (!metas[n] || out.has(n)) return;
58
+ if (out.has(n)) return; // include third-party names too, not just bundled ones
59
59
  out.add(n);
60
- for (const d of metas[n].dependsOn) visit(d);
60
+ for (const d of metas[n]?.dependsOn || []) visit(d);
61
61
  };
62
62
  for (const n of String(env.VOLT_ADDONS || "").split(",").map((s) => s.trim()).filter(Boolean)) visit(n);
63
63
  return out;
@@ -66,6 +66,23 @@ function enabledFrom(env) {
66
66
  const imp = (rel) => import(pathToFileURL(path.join(__dirname, rel)).href);
67
67
  const addonMod = (n) => imp(path.join(".volt", "addons", n, "files", "lib", LIB_FILE[n]));
68
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
+
69
86
  function openBrowser(url) {
70
87
  if (process.env.VOLT_NO_OPEN || process.argv.includes("--no-open")) return false;
71
88
  const plat = process.platform;
@@ -124,6 +141,26 @@ async function startApp() {
124
141
  const io = new SocketServer(server);
125
142
  if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
126
143
 
144
+ // third-party add-ons — register(ctx). When auth is on, requireAuth/sessionFromReq
145
+ // are provided so add-ons can gate routes by login.
146
+ let requireAuth = null;
147
+ let sessionFromReq = null;
148
+ if (enabled.has("auth") && store) {
149
+ const a = await addonMod("auth");
150
+ requireAuth = a.requireAuth(store);
151
+ sessionFromReq = (req) => a.sessionFromReq(store, req);
152
+ }
153
+ for (const name of enabled) {
154
+ if (BUILTINS.has(name)) continue;
155
+ const mod = await loadAddon(name);
156
+ const register = mod && (mod.register || mod.default);
157
+ if (typeof register === "function") {
158
+ await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
159
+ } else {
160
+ console.warn(`[volt] add-on "${name}" not found or missing a register() export — skipped`);
161
+ }
162
+ }
163
+
127
164
  let timer = null;
128
165
  const onChange = (file) => {
129
166
  clearTimeout(timer);
@@ -44,7 +44,7 @@ function availableAddons() {
44
44
  if (!fs.existsSync(ADDONS_DIR)) return [];
45
45
  return fs
46
46
  .readdirSync(ADDONS_DIR, { withFileTypes: true })
47
- .filter((e) => e.isDirectory())
47
+ .filter((e) => e.isDirectory() && fs.existsSync(path.join(ADDONS_DIR, e.name, "meta.json"))) // skip local 3rd-party add-ons (no meta)
48
48
  .map((e) => {
49
49
  const m = JSON.parse(fs.readFileSync(path.join(ADDONS_DIR, e.name, "meta.json"), "utf8"));
50
50
  return { name: e.name, description: m.description, dependsOn: m.dependsOn || [] };
@@ -56,9 +56,9 @@ function enabledFrom(env) {
56
56
  const metas = Object.fromEntries(availableAddons().map((a) => [a.name, a]));
57
57
  const out = new Set();
58
58
  const visit = (n) => {
59
- if (!metas[n] || out.has(n)) return;
59
+ if (out.has(n)) return; // include third-party names too, not just bundled ones
60
60
  out.add(n);
61
- for (const d of metas[n].dependsOn) visit(d);
61
+ for (const d of metas[n]?.dependsOn || []) visit(d);
62
62
  };
63
63
  for (const n of String(env.VOLT_ADDONS || "").split(",").map((s) => s.trim()).filter(Boolean)) visit(n);
64
64
  return out;
@@ -67,6 +67,23 @@ function enabledFrom(env) {
67
67
  const imp = (rel) => import(pathToFileURL(path.join(__dirname, rel)).href);
68
68
  const addonMod = (n) => imp(path.join(".volt", "addons", n, "files", "lib", LIB_FILE[n]));
69
69
 
70
+ // Built-in add-ons are wired explicitly below; everything else in VOLT_ADDONS is
71
+ // a third-party add-on — a local .volt/addons/<name>/index.js or an installed
72
+ // npm package "volt-addon-<name>" exporting register(ctx). See /docs/plugins.
73
+ const BUILTINS = new Set(Object.keys(LIB_FILE));
74
+ async function loadAddon(name) {
75
+ const local = path.join(__dirname, ".volt", "addons", name, "index.js");
76
+ if (fs.existsSync(local)) return imp(path.join(".volt", "addons", name, "index.js"));
77
+ for (const id of [`volt-addon-${name}`, name]) {
78
+ try {
79
+ return await import(id);
80
+ } catch {
81
+ /* try next */
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+
70
87
  function openBrowser(url) {
71
88
  if (process.env.VOLT_NO_OPEN || process.argv.includes("--no-open")) return false;
72
89
  const plat = process.platform;
@@ -150,6 +167,26 @@ async function startApp() {
150
167
  const io = new SocketServer(server);
151
168
  if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
152
169
 
170
+ // third-party add-ons — register(ctx). When auth is on, requireAuth/sessionFromReq
171
+ // are provided so add-ons can gate routes by login.
172
+ let requireAuth = null;
173
+ let sessionFromReq = null;
174
+ if (enabled.has("auth") && store) {
175
+ const a = await addonMod("auth");
176
+ requireAuth = a.requireAuth(store);
177
+ sessionFromReq = (req) => a.sessionFromReq(store, req);
178
+ }
179
+ for (const name of enabled) {
180
+ if (BUILTINS.has(name)) continue;
181
+ const mod = await loadAddon(name);
182
+ const register = mod && (mod.register || mod.default);
183
+ if (typeof register === "function") {
184
+ await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
185
+ } else {
186
+ console.warn(`[volt] add-on "${name}" not found or missing a register() export — skipped`);
187
+ }
188
+ }
189
+
153
190
  let timer = null;
154
191
  const onChange = (file) => {
155
192
  clearTimeout(timer);