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 +18 -0
- package/index.js +86 -2
- package/package.json +1 -1
- package/templates/default/server.js +32 -3
- package/templates/starter/server.js +32 -3
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
|
-
//
|
|
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
|
-
|
|
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
|
@@ -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 (
|
|
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]
|
|
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 (
|
|
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]
|
|
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);
|