create-volt 0.21.0 → 0.22.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.22.0] - 2026-06-29
8
+
9
+ ### Added
10
+ - **Third-party add-ons (plugins) — the WordPress-plugin equivalent.** Any
11
+ `VOLT_ADDONS` entry that is not built-in is loaded from a local
12
+ `.volt/addons/<name>/index.js` or an installed npm package
13
+ `volt-addon-<name>`, and wired via a single `register(ctx)` export
14
+ (ctx = app, express, io, store, mailer, env, log). Install functionality as
15
+ small, owned packages instead of dashboard plugins.
16
+ - `create-volt add <name>` — install `volt-addon-<name>` and enable it.
17
+ - `create-volt create-addon <name>` — scaffold a publishable add-on package.
18
+ - New `/docs/plugins`.
19
+
20
+ ### Fixed
21
+ - `availableAddons()` tolerates add-on directories without `meta.json` (so
22
+ local third-party add-ons do not break the wizard).
23
+
7
24
  ## [0.21.0] - 2026-06-29
8
25
 
9
26
  ### Added
@@ -301,6 +318,7 @@ All notable changes to `create-volt` are documented here. The format follows
301
318
  watching and full-page hot reload. Supports `--skip-install` and `--force`,
302
319
  and auto-detects npm / pnpm / yarn / bun for the install step.
303
320
 
321
+ [0.22.0]: https://github.com/MIR-2025/volt/releases/tag/v0.22.0
304
322
  [0.21.0]: https://github.com/MIR-2025/volt/releases/tag/v0.21.0
305
323
  [0.20.0]: https://github.com/MIR-2025/volt/releases/tag/v0.20.0
306
324
  [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.22.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,18 @@ 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) gets the app, io, store, mailer, and env
145
+ for (const name of enabled) {
146
+ if (BUILTINS.has(name)) continue;
147
+ const mod = await loadAddon(name);
148
+ const register = mod && (mod.register || mod.default);
149
+ if (typeof register === "function") {
150
+ await register({ app, express, io, store, mailer, env: process.env, log: (...a) => console.log(`[${name}]`, ...a) });
151
+ } else {
152
+ console.warn(`[volt] add-on "${name}" not found or missing a register() export — skipped`);
153
+ }
154
+ }
155
+
127
156
  let timer = null;
128
157
  const onChange = (file) => {
129
158
  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,18 @@ 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) gets the app, io, store, mailer, and env
171
+ for (const name of enabled) {
172
+ if (BUILTINS.has(name)) continue;
173
+ const mod = await loadAddon(name);
174
+ const register = mod && (mod.register || mod.default);
175
+ if (typeof register === "function") {
176
+ await register({ app, express, io, store, mailer, env: process.env, log: (...a) => console.log(`[${name}]`, ...a) });
177
+ } else {
178
+ console.warn(`[volt] add-on "${name}" not found or missing a register() export — skipped`);
179
+ }
180
+ }
181
+
153
182
  let timer = null;
154
183
  const onChange = (file) => {
155
184
  clearTimeout(timer);