create-volt 0.20.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 +30 -0
- package/index.js +111 -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,34 @@ 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
|
+
|
|
24
|
+
## [0.21.0] - 2026-06-29
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **`import-wp-db`** — import WordPress content by reading its MySQL/MariaDB
|
|
28
|
+
database directly, for when the REST API is disabled but you have DB access
|
|
29
|
+
(on the server or via an SSH tunnel). `--prefix` for non-default table
|
|
30
|
+
prefixes (validated against SQL injection); creds via `WP_DB_URL` to keep them
|
|
31
|
+
out of shell history; `mysql2` loaded lazily. Reuses the WXR converter; unit
|
|
32
|
+
tested with an injected connection. Third migration path alongside
|
|
33
|
+
`import-wp` (REST) and `import-wxr` (file).
|
|
34
|
+
|
|
7
35
|
## [0.20.0] - 2026-06-29
|
|
8
36
|
|
|
9
37
|
### Added
|
|
@@ -290,6 +318,8 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
290
318
|
watching and full-page hot reload. Supports `--skip-install` and `--force`,
|
|
291
319
|
and auto-detects npm / pnpm / yarn / bun for the install step.
|
|
292
320
|
|
|
321
|
+
[0.22.0]: https://github.com/MIR-2025/volt/releases/tag/v0.22.0
|
|
322
|
+
[0.21.0]: https://github.com/MIR-2025/volt/releases/tag/v0.21.0
|
|
293
323
|
[0.20.0]: https://github.com/MIR-2025/volt/releases/tag/v0.20.0
|
|
294
324
|
[0.19.0]: https://github.com/MIR-2025/volt/releases/tag/v0.19.0
|
|
295
325
|
[0.18.0]: https://github.com/MIR-2025/volt/releases/tag/v0.18.0
|
package/index.js
CHANGED
|
@@ -63,6 +63,7 @@ let portArg = null;
|
|
|
63
63
|
let templateArg = null;
|
|
64
64
|
let outArg = null;
|
|
65
65
|
let userArg = null;
|
|
66
|
+
let prefixArg = null;
|
|
66
67
|
for (let i = 0; i < argv.length; i++) {
|
|
67
68
|
const a = argv[i];
|
|
68
69
|
if (a === "--port") portArg = argv[++i];
|
|
@@ -73,6 +74,8 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
73
74
|
else if (a.startsWith("--out=")) outArg = a.slice("--out=".length);
|
|
74
75
|
else if (a === "--user") userArg = argv[++i];
|
|
75
76
|
else if (a.startsWith("--user=")) userArg = a.slice("--user=".length);
|
|
77
|
+
else if (a === "--prefix") prefixArg = argv[++i];
|
|
78
|
+
else if (a.startsWith("--prefix=")) prefixArg = a.slice("--prefix=".length);
|
|
76
79
|
else if (a.startsWith("-")) flags.add(a);
|
|
77
80
|
else positionals.push(a);
|
|
78
81
|
}
|
|
@@ -91,9 +94,93 @@ const force = flags.has("--force");
|
|
|
91
94
|
const dryRun = flags.has("--dry-run");
|
|
92
95
|
const noGit = flags.has("--no-git");
|
|
93
96
|
|
|
94
|
-
//
|
|
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 ---
|
|
95
147
|
if (positionals[0] === "add") {
|
|
96
|
-
|
|
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);
|
|
97
184
|
}
|
|
98
185
|
|
|
99
186
|
// --- `update` subcommand: refresh public/volt.js in the current app to the
|
|
@@ -189,6 +276,28 @@ if (positionals[0] === "import-wp") {
|
|
|
189
276
|
process.exit(0);
|
|
190
277
|
}
|
|
191
278
|
|
|
279
|
+
// --- `import-wp-db` subcommand: read a WordPress MySQL database directly ---
|
|
280
|
+
if (positionals[0] === "import-wp-db") {
|
|
281
|
+
const dbUrl = positionals[1] || process.env.WP_DB_URL || process.env.DATABASE_URL;
|
|
282
|
+
if (!dbUrl) {
|
|
283
|
+
die(
|
|
284
|
+
`Usage: ${cyan("create-volt import-wp-db <mysql://user:pass@host/db>")} [--prefix wp_] [--out pages] [--drafts] [--force]\n` +
|
|
285
|
+
` Tip: set ${cyan("WP_DB_URL")} instead of passing the URL, so credentials stay out of shell history.\n` +
|
|
286
|
+
` WordPress DBs are usually firewalled to localhost — run this on the server or over an SSH tunnel. Requires ${cyan("mysql2")} (npm i mysql2).`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const { runImportFromDB } = await import("./lib/import-wp-db.js");
|
|
290
|
+
console.log(dim(`Reading WordPress database (prefix ${prefixArg || "wp_"})…`));
|
|
291
|
+
let result;
|
|
292
|
+
try {
|
|
293
|
+
result = await runImportFromDB(dbUrl, { prefix: prefixArg || "wp_", drafts: flags.has("--drafts") });
|
|
294
|
+
} catch (e) {
|
|
295
|
+
die(e.message);
|
|
296
|
+
}
|
|
297
|
+
emitImported(result.imported, result.stats, path.resolve(outArg || "pages"));
|
|
298
|
+
process.exit(0);
|
|
299
|
+
}
|
|
300
|
+
|
|
192
301
|
// --- `studio` subcommand: ephemeral, localhost-only data browser (server.js --studio) ---
|
|
193
302
|
if (positionals[0] === "studio") {
|
|
194
303
|
const cwd = process.cwd();
|
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);
|