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 +35 -0
- package/index.js +86 -2
- package/package.json +1 -1
- package/templates/default/server.js +40 -3
- package/templates/starter/server.js +40 -3
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
|
-
//
|
|
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,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 (
|
|
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,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);
|