create-volt 0.30.0 → 0.32.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 +21 -0
- package/addons/pages/files/lib/pages.js +2 -2
- package/addons/posts/files/lib/posts.js +168 -0
- package/addons/posts/meta.json +8 -0
- package/package.json +1 -1
- package/templates/default/public/favicon.webp +0 -0
- package/templates/default/public/logo.webp +0 -0
- package/templates/default/server.js +32 -26
- package/templates/default/setup/index.html +2 -1
- package/templates/default/setup/studio.html +2 -1
- package/templates/default/views/index.html +2 -1
- package/templates/starter/public/app.js +1 -1
- package/templates/starter/public/favicon.webp +0 -0
- package/templates/starter/public/logo.webp +0 -0
- package/templates/starter/server.js +33 -27
- package/templates/starter/setup/index.html +2 -1
- package/templates/starter/setup/studio.html +2 -1
- package/templates/starter/views/index.html +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ 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.32.0] - 2026-06-29
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **Brand refresh:** the ⚡ glyph is replaced with the Volt logo across the demo
|
|
11
|
+
app, the setup wizard, and Studio; scaffolded apps now ship `logo.webp` +
|
|
12
|
+
`favicon.webp` and link a favicon (so apps get a real tab icon). README titles
|
|
13
|
+
keep the ⚡.
|
|
14
|
+
|
|
15
|
+
## [0.31.0] - 2026-06-29
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`posts` add-on — a blog (the WordPress content model).** Markdown in `posts/`
|
|
19
|
+
becomes a paginated index at `/blog`, single posts at `/blog/<slug>`, plus
|
|
20
|
+
`/category/<name>`, `/tag/<name>`, and an RSS feed at `/feed.xml`. Posts carry
|
|
21
|
+
date/author/category/tags front-matter (date can also come from a
|
|
22
|
+
`YYYY-MM-DD-` filename prefix); drafts (`draft: true`) are skipped. Renders in
|
|
23
|
+
the site theme and reuses pages' SEO — single posts get an auto Article
|
|
24
|
+
JSON-LD + `og:type=article`. Depends on the `pages` add-on. New `/docs/posts`.
|
|
25
|
+
|
|
7
26
|
## [0.30.0] - 2026-06-29
|
|
8
27
|
|
|
9
28
|
### Fixed
|
|
@@ -413,6 +432,8 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
413
432
|
watching and full-page hot reload. Supports `--skip-install` and `--force`,
|
|
414
433
|
and auto-detects npm / pnpm / yarn / bun for the install step.
|
|
415
434
|
|
|
435
|
+
[0.32.0]: https://github.com/MIR-2025/volt/releases/tag/v0.32.0
|
|
436
|
+
[0.31.0]: https://github.com/MIR-2025/volt/releases/tag/v0.31.0
|
|
416
437
|
[0.30.0]: https://github.com/MIR-2025/volt/releases/tag/v0.30.0
|
|
417
438
|
[0.29.0]: https://github.com/MIR-2025/volt/releases/tag/v0.29.0
|
|
418
439
|
[0.28.0]: https://github.com/MIR-2025/volt/releases/tag/v0.28.0
|
|
@@ -52,7 +52,7 @@ export function parseFrontMatter(src) {
|
|
|
52
52
|
export const isSafeSlug = (s) => /^[a-z0-9][a-z0-9-]*$/i.test(s);
|
|
53
53
|
|
|
54
54
|
// SEO/social head from front-matter: description, image, type, canonical, jsonld.
|
|
55
|
-
function metaHead(meta) {
|
|
55
|
+
export function metaHead(meta) {
|
|
56
56
|
const t = [];
|
|
57
57
|
const title = meta.title || "";
|
|
58
58
|
const desc = meta.description || "";
|
|
@@ -117,7 +117,7 @@ function themeCss(dir) {
|
|
|
117
117
|
// local pages/_theme.js; else the built-in default. A theme may `export const css`
|
|
118
118
|
// (served at /_theme.css, shared with the editor); otherwise pages/_theme.css or
|
|
119
119
|
// the default CSS is used.
|
|
120
|
-
async function loadTheme(dir, env) {
|
|
120
|
+
export async function loadTheme(dir, env) {
|
|
121
121
|
const wrap = (m) => {
|
|
122
122
|
const layout = m && (m.layout || m.default);
|
|
123
123
|
return layout ? { layout, css: m.css || themeCss(dir) } : null;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// posts.js — a blog content type. Markdown files in posts/ become a paginated
|
|
2
|
+
// index at /blog, single posts at /blog/<slug>, plus /category/<name>,
|
|
3
|
+
// /tag/<name>, and an RSS feed at /feed.xml. Renders in the site theme and
|
|
4
|
+
// reuses pages' front-matter + SEO helpers (OG, Twitter, JSON-LD).
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
// express + marked are imported lazily in postsRouter() so the pure helpers load
|
|
8
|
+
// without those deps. Theme + SEO come from the pages add-on (a dependency).
|
|
9
|
+
import { parseFrontMatter, isSafeSlug, metaHead, loadTheme } from "../../../pages/files/lib/pages.js";
|
|
10
|
+
|
|
11
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
12
|
+
const slugify = (s) => String(s).toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
13
|
+
const postLink = (slug) => "/blog/" + slug;
|
|
14
|
+
const catLink = (c) => `<a href="/category/${slugify(c)}">${esc(c)}</a>`;
|
|
15
|
+
const tagLink = (t) => `<a class="post-tag" href="/tag/${slugify(t)}">${esc(t)}</a>`;
|
|
16
|
+
const tagsOf = (meta) => String(meta.tags || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
17
|
+
|
|
18
|
+
function fmtDate(d) {
|
|
19
|
+
if (!d) return "";
|
|
20
|
+
const t = new Date(d);
|
|
21
|
+
return isNaN(t.getTime()) ? esc(d) : t.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function excerpt(p) {
|
|
25
|
+
if (p.meta.description) return p.meta.description;
|
|
26
|
+
const text = String(p.body).replace(/<[^>]+>/g, " ").replace(/[#>*_`[\]()!]+/g, " ").replace(/\s+/g, " ").trim();
|
|
27
|
+
return text.slice(0, 160) + (text.length > 160 ? "…" : "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Read + parse every post, newest first. Date comes from front-matter `date:` or
|
|
31
|
+
// a YYYY-MM-DD- filename prefix; slug from front-matter `slug:` or the filename
|
|
32
|
+
// (prefix stripped). Drafts (draft: true) are skipped.
|
|
33
|
+
export function readPosts(dir) {
|
|
34
|
+
if (!fs.existsSync(dir)) return [];
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const f of fs.readdirSync(dir)) {
|
|
37
|
+
if (!f.endsWith(".md")) continue;
|
|
38
|
+
const { meta, body } = parseFrontMatter(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
39
|
+
if (String(meta.draft).toLowerCase() === "true") continue;
|
|
40
|
+
let name = f.replace(/\.md$/, "");
|
|
41
|
+
let date = meta.date || "";
|
|
42
|
+
const m = name.match(/^(\d{4}-\d{2}-\d{2})-(.+)$/);
|
|
43
|
+
if (m) {
|
|
44
|
+
if (!date) date = m[1];
|
|
45
|
+
name = m[2];
|
|
46
|
+
}
|
|
47
|
+
const slug = slugify(meta.slug || name);
|
|
48
|
+
if (!isSafeSlug(slug)) continue;
|
|
49
|
+
out.push({ slug, date, meta, body });
|
|
50
|
+
}
|
|
51
|
+
return out.sort((a, b) => String(b.date).localeCompare(String(a.date)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderList(posts, { heading, page = 1, totalPages = 1, baseUrl }) {
|
|
55
|
+
const items = posts.length
|
|
56
|
+
? posts
|
|
57
|
+
.map(
|
|
58
|
+
(p) => `<li class="post-item" style="margin:0 0 1.5rem">
|
|
59
|
+
<a href="${postLink(p.slug)}" style="font-size:1.2rem;font-weight:600">${esc(p.meta.title || p.slug)}</a>
|
|
60
|
+
<div class="post-meta" style="opacity:.7;font-size:.9rem">${fmtDate(p.date)}${p.meta.category ? " · " + catLink(p.meta.category) : ""}</div>
|
|
61
|
+
<p style="margin:.3rem 0 0">${esc(excerpt(p))}</p>
|
|
62
|
+
</li>`,
|
|
63
|
+
)
|
|
64
|
+
.join("\n")
|
|
65
|
+
: "<li>No posts yet.</li>";
|
|
66
|
+
let nav = "";
|
|
67
|
+
if (totalPages > 1) {
|
|
68
|
+
const prev = page > 1 ? `<a href="${baseUrl}?page=${page - 1}">← Newer</a>` : "<span></span>";
|
|
69
|
+
const next = page < totalPages ? `<a href="${baseUrl}?page=${page + 1}" style="margin-left:auto">Older →</a>` : "";
|
|
70
|
+
nav = `<nav class="post-pager" style="display:flex;margin-top:1.5rem">${prev}${next}</nav>`;
|
|
71
|
+
}
|
|
72
|
+
return `<h1>${esc(heading)}</h1><ul class="post-list" style="list-style:none;padding:0">${items}</ul>${nav}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function renderPost(p, marked) {
|
|
76
|
+
const title = p.meta.title || p.slug;
|
|
77
|
+
const body = p.meta.format === "html" ? p.body : marked.parse(p.body);
|
|
78
|
+
const tags = tagsOf(p.meta);
|
|
79
|
+
const meta = `<div class="post-meta" style="opacity:.7;font-size:.9rem;margin-bottom:1rem">${fmtDate(p.date)}${p.meta.author ? " · " + esc(p.meta.author) : ""}${p.meta.category ? " · " + catLink(p.meta.category) : ""}</div>`;
|
|
80
|
+
const tagHtml = tags.length ? `<div class="post-tags" style="margin-top:1.5rem">Tags: ${tags.map(tagLink).join(" ")}</div>` : "";
|
|
81
|
+
return `<article><h1>${esc(title)}</h1>${meta}${body}${tagHtml}</article>`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function feedXml(posts) {
|
|
85
|
+
const base = (process.env.SITE_URL || "").replace(/\/+$/, "");
|
|
86
|
+
const name = process.env.SITE_NAME || "Blog";
|
|
87
|
+
const items = posts
|
|
88
|
+
.slice(0, 20)
|
|
89
|
+
.map(
|
|
90
|
+
(p) => ` <item>
|
|
91
|
+
<title>${esc(p.meta.title || p.slug)}</title>
|
|
92
|
+
<link>${esc(base + postLink(p.slug))}</link>
|
|
93
|
+
<guid isPermaLink="${base ? "true" : "false"}">${esc(base + postLink(p.slug))}</guid>${p.date && !isNaN(new Date(p.date).getTime()) ? `\n <pubDate>${new Date(p.date).toUTCString()}</pubDate>` : ""}
|
|
94
|
+
<description>${esc(excerpt(p))}</description>
|
|
95
|
+
</item>`,
|
|
96
|
+
)
|
|
97
|
+
.join("\n");
|
|
98
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
99
|
+
<rss version="2.0"><channel>
|
|
100
|
+
<title>${esc(name)}</title>
|
|
101
|
+
<link>${esc(base + "/blog")}</link>
|
|
102
|
+
<description>${esc(name)} — posts</description>
|
|
103
|
+
${items}
|
|
104
|
+
</channel></rss>`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function postsRouter({ dir, themeDir }) {
|
|
108
|
+
const express = (await import("express")).default;
|
|
109
|
+
const { marked } = await import("marked");
|
|
110
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
111
|
+
const { layout } = await loadTheme(themeDir || dir, process.env); // same theme as pages
|
|
112
|
+
const PER = Math.max(1, Number(process.env.POSTS_PER_PAGE) || 10);
|
|
113
|
+
const render = ({ title, content, meta = {} }) => {
|
|
114
|
+
const m = { ...meta, title };
|
|
115
|
+
return layout({ title, head: metaHead(m), content, meta: m });
|
|
116
|
+
};
|
|
117
|
+
const r = express.Router();
|
|
118
|
+
|
|
119
|
+
r.get("/blog", (req, res) => {
|
|
120
|
+
const all = readPosts(dir);
|
|
121
|
+
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
|
|
122
|
+
const totalPages = Math.max(1, Math.ceil(all.length / PER));
|
|
123
|
+
const slice = all.slice((page - 1) * PER, page * PER);
|
|
124
|
+
const heading = process.env.SITE_NAME || "Blog";
|
|
125
|
+
res.type("html").send(render({ title: heading, meta: { description: heading + " — latest posts" }, content: renderList(slice, { heading, page, totalPages, baseUrl: "/blog" }) }));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
r.get("/blog/:slug", (req, res, next) => {
|
|
129
|
+
if (!isSafeSlug(req.params.slug)) return next();
|
|
130
|
+
const post = readPosts(dir).find((p) => p.slug === req.params.slug);
|
|
131
|
+
if (!post) return next();
|
|
132
|
+
const title = post.meta.title || post.slug;
|
|
133
|
+
const autoLd = JSON.stringify({
|
|
134
|
+
"@context": "https://schema.org",
|
|
135
|
+
"@type": "Article",
|
|
136
|
+
headline: title,
|
|
137
|
+
...(post.date ? { datePublished: post.date } : {}),
|
|
138
|
+
...(post.meta.author ? { author: { "@type": "Person", name: post.meta.author } } : {}),
|
|
139
|
+
});
|
|
140
|
+
res.type("html").send(
|
|
141
|
+
render({
|
|
142
|
+
title,
|
|
143
|
+
meta: { ...post.meta, title, type: "article", jsonld: post.meta.jsonld || autoLd },
|
|
144
|
+
content: renderPost(post, marked),
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
r.get("/category/:name", (req, res, next) => {
|
|
150
|
+
if (!isSafeSlug(req.params.name)) return next();
|
|
151
|
+
const list = readPosts(dir).filter((p) => slugify(p.meta.category) === req.params.name);
|
|
152
|
+
if (!list.length) return next();
|
|
153
|
+
res.type("html").send(render({ title: "Category: " + req.params.name, content: renderList(list, { heading: "Category: " + req.params.name, baseUrl: "/category/" + req.params.name }) }));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
r.get("/tag/:name", (req, res, next) => {
|
|
157
|
+
if (!isSafeSlug(req.params.name)) return next();
|
|
158
|
+
const list = readPosts(dir).filter((p) => tagsOf(p.meta).some((t) => slugify(t) === req.params.name));
|
|
159
|
+
if (!list.length) return next();
|
|
160
|
+
res.type("html").send(render({ title: "Tag: " + req.params.name, content: renderList(list, { heading: "Tag: " + req.params.name, baseUrl: "/tag/" + req.params.name }) }));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
r.get("/feed.xml", (_req, res) => {
|
|
164
|
+
res.type("application/rss+xml").send(feedXml(readPosts(dir)));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return r;
|
|
168
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Blog posts with dates, authors, tags + categories. Drop markdown in posts/ → a paginated index at /blog, single posts at /blog/<slug>, /category/<name>, /tag/<name>, and an RSS feed at /feed.xml. Renders in your site theme and reuses pages' SEO (OG + JSON-LD).",
|
|
3
|
+
"dependsOn": ["pages"],
|
|
4
|
+
"sentinel": "lib/posts.js",
|
|
5
|
+
"install": ["marked"],
|
|
6
|
+
"optional": {},
|
|
7
|
+
"wiring": "Add markdown files to posts/ (created on first run), e.g. posts/2026-06-29-hello.md with front-matter: title, date, author, category, tags (comma-separated), description. The date can also come from a YYYY-MM-DD- filename prefix. Set SITE_URL in .env for absolute links in the RSS feed; POSTS_PER_PAGE (default 10) controls pagination."
|
|
8
|
+
}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// server.js
|
|
1
|
+
// server.js â dev server with a built-in first-run setup wizard.
|
|
2
2
|
//
|
|
3
3
|
// First run (no .env) or `node server.js --edit` (-e) opens a disposable, local
|
|
4
4
|
// config page: tick add-ons, fill settings, Apply. Apply writes .env (a
|
|
5
5
|
// VOLT_ADDONS list + settings) and adds any needed packages to package.json,
|
|
6
|
-
// runs npm install, then starts the app
|
|
6
|
+
// runs npm install, then starts the app â which wires whatever .env enables.
|
|
7
7
|
// Add-on code is bundled under .volt/addons; nothing is copied into your code.
|
|
8
8
|
//
|
|
9
9
|
// No build step, no env-file flag: .env is auto-loaded below.
|
|
@@ -22,7 +22,7 @@ const PKG_PATH = path.join(__dirname, "package.json");
|
|
|
22
22
|
const ADDONS_DIR = path.join(__dirname, ".volt", "addons"); // bundled add-on sources
|
|
23
23
|
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
24
24
|
const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1", marked: "^18.0.5", busboy: "^1.6.0", "@aws-sdk/client-s3": "^3.1075.0" };
|
|
25
|
-
const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js", pages: "pages.js", media: "media.js" };
|
|
25
|
+
const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js", pages: "pages.js", posts: "posts.js", media: "media.js" };
|
|
26
26
|
|
|
27
27
|
// --- tiny .env loader (no dependency); never overrides an existing env var ---
|
|
28
28
|
function readEnvFile() {
|
|
@@ -67,7 +67,7 @@ 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
69
|
// Built-in add-ons are wired explicitly below; everything else in VOLT_ADDONS is
|
|
70
|
-
// a third-party add-on
|
|
70
|
+
// a third-party add-on â a local .volt/addons/<name>/index.js or an installed
|
|
71
71
|
// npm package "volt-addon-<name>" exporting register(ctx). See /docs/plugins.
|
|
72
72
|
const BUILTINS = new Set(Object.keys(LIB_FILE));
|
|
73
73
|
async function loadAddon(name) {
|
|
@@ -91,7 +91,7 @@ function openBrowser(url) {
|
|
|
91
91
|
const args = plat === "win32" ? ["/c", "start", "", url] : [url];
|
|
92
92
|
try {
|
|
93
93
|
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
94
|
-
child.on("error", () => {}); // launcher missing
|
|
94
|
+
child.on("error", () => {}); // launcher missing â emits async, don't crash
|
|
95
95
|
child.unref();
|
|
96
96
|
return true;
|
|
97
97
|
} catch {
|
|
@@ -134,14 +134,16 @@ async function startApp() {
|
|
|
134
134
|
app.use(await (await addonMod("media")).mediaRouter({ requireAuth, dir: path.join(__dirname, "media") }));
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
// markdown pages (/<slug>
|
|
137
|
+
// markdown pages (/<slug> â pages/<slug>.md) â mounted last, so app routes win
|
|
138
|
+
// blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) — before pages so /blog wins; renders in the same theme.
|
|
139
|
+
if (enabled.has("posts")) app.use(await (await addonMod("posts")).postsRouter({ dir: path.join(__dirname, "posts"), themeDir: path.join(__dirname, "pages") }));
|
|
138
140
|
if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
|
|
139
141
|
|
|
140
142
|
const server = http.createServer(app);
|
|
141
143
|
const io = new SocketServer(server);
|
|
142
144
|
if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
|
|
143
145
|
|
|
144
|
-
// third-party add-ons
|
|
146
|
+
// third-party add-ons â register(ctx). When auth is on, requireAuth/sessionFromReq
|
|
145
147
|
// are provided so add-ons can gate routes by login.
|
|
146
148
|
let requireAuth = null;
|
|
147
149
|
let sessionFromReq = null;
|
|
@@ -157,7 +159,7 @@ async function startApp() {
|
|
|
157
159
|
if (typeof register === "function") {
|
|
158
160
|
await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
|
|
159
161
|
} else {
|
|
160
|
-
console.warn(`[volt] add-on "${name}" not found or missing a register() export
|
|
162
|
+
console.warn(`[volt] add-on "${name}" not found or missing a register() export â skipped`);
|
|
161
163
|
}
|
|
162
164
|
}
|
|
163
165
|
|
|
@@ -165,7 +167,7 @@ async function startApp() {
|
|
|
165
167
|
const onChange = (file) => {
|
|
166
168
|
clearTimeout(timer);
|
|
167
169
|
timer = setTimeout(() => {
|
|
168
|
-
console.log(`[volt] change: ${file ?? "?"}
|
|
170
|
+
console.log(`[volt] change: ${file ?? "?"} â reload`);
|
|
169
171
|
io.emit("volt:reload");
|
|
170
172
|
}, 80);
|
|
171
173
|
};
|
|
@@ -189,7 +191,7 @@ async function startApp() {
|
|
|
189
191
|
for (const d of ["views", "public"]) watchRecursive(path.join(__dirname, d));
|
|
190
192
|
|
|
191
193
|
const on = [...enabled];
|
|
192
|
-
server.listen(PORT, () => console.log(
|
|
194
|
+
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
// Packages an .env's selections need, beyond what package.json already has.
|
|
@@ -202,7 +204,7 @@ function neededPackages(env) {
|
|
|
202
204
|
if (driver === "mysql") want.push("mysql2");
|
|
203
205
|
if (driver === "postgres") want.push("pg");
|
|
204
206
|
if (/^\s*SMTP_URL\s*=\s*\S/m.test(env)) want.push("nodemailer");
|
|
205
|
-
if (/^\s*VOLT_ADDONS\s*=.*\
|
|
207
|
+
if (/^\s*VOLT_ADDONS\s*=.*\b(pages|posts)\b/m.test(env)) want.push("marked");
|
|
206
208
|
if (/^\s*VOLT_ADDONS\s*=.*\bmedia\b/m.test(env)) want.push("busboy");
|
|
207
209
|
if (/^\s*MEDIA_DRIVER\s*=\s*s3\b/m.test(env)) want.push("@aws-sdk/client-s3");
|
|
208
210
|
return want.filter((p) => !deps[p]);
|
|
@@ -213,7 +215,7 @@ function neededPackages(env) {
|
|
|
213
215
|
function ensureDriverInstalled(driver) {
|
|
214
216
|
const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
|
|
215
217
|
if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
|
|
216
|
-
console.log(`[volt] installing ${pkg} for the connection test
|
|
218
|
+
console.log(`[volt] installing ${pkg} for the connection testâ¦`);
|
|
217
219
|
spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
218
220
|
}
|
|
219
221
|
|
|
@@ -223,6 +225,8 @@ function startSetup() {
|
|
|
223
225
|
const assets = {
|
|
224
226
|
"/setup.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "setup", "setup.js"))],
|
|
225
227
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
228
|
+
"/logo.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "logo.webp"))],
|
|
229
|
+
"/favicon.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "favicon.webp"))],
|
|
226
230
|
};
|
|
227
231
|
const indexHtml = fs.readFileSync(path.join(__dirname, "setup", "index.html"));
|
|
228
232
|
|
|
@@ -314,15 +318,15 @@ function startSetup() {
|
|
|
314
318
|
server.closeIdleConnections?.();
|
|
315
319
|
};
|
|
316
320
|
if (added.length) {
|
|
317
|
-
console.log(`[volt] installing ${added.join(", ")}
|
|
321
|
+
console.log(`[volt] installing ${added.join(", ")}â¦`);
|
|
318
322
|
const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
319
323
|
npm.on("error", () => handoff());
|
|
320
324
|
npm.on("close", () => {
|
|
321
|
-
console.log("[volt] saved .env
|
|
325
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
322
326
|
handoff();
|
|
323
327
|
});
|
|
324
328
|
} else {
|
|
325
|
-
console.log("[volt] saved .env
|
|
329
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
326
330
|
handoff();
|
|
327
331
|
}
|
|
328
332
|
});
|
|
@@ -339,18 +343,18 @@ function startSetup() {
|
|
|
339
343
|
|
|
340
344
|
server.listen(PORT, "127.0.0.1", () => {
|
|
341
345
|
const url = `http://localhost:${PORT}`;
|
|
342
|
-
console.log(`\n
|
|
346
|
+
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
343
347
|
console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
|
|
344
348
|
const ssh = process.env.SSH_CONNECTION;
|
|
345
349
|
if (ssh) {
|
|
346
350
|
const host = ssh.split(" ")[2];
|
|
347
351
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
348
|
-
console.log(" Remote box
|
|
352
|
+
console.log(" Remote box â the server is up here; bridge it from your LOCAL machine:");
|
|
349
353
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
350
|
-
console.log(`
|
|
354
|
+
console.log(` â¦then open ${url} on your machine (the tunnel points it here).`);
|
|
351
355
|
}
|
|
352
356
|
console.log("");
|
|
353
|
-
if (openBrowser(url)) console.log(" (opening your browser
|
|
357
|
+
if (openBrowser(url)) console.log(" (opening your browserâ¦)\n");
|
|
354
358
|
});
|
|
355
359
|
}
|
|
356
360
|
|
|
@@ -358,8 +362,8 @@ function readEnvFileLines() {
|
|
|
358
362
|
return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
|
|
359
363
|
}
|
|
360
364
|
|
|
361
|
-
// --- Studio: an ephemeral, localhost-only data browser (
|
|
362
|
-
// Not a route in the running app
|
|
365
|
+
// --- Studio: an ephemeral, localhost-only data browser (Ã la Prisma Studio).
|
|
366
|
+
// Not a route in the running app â it only exists while you run `--studio`, on
|
|
363
367
|
// loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
|
|
364
368
|
const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
|
|
365
369
|
async function startStudio() {
|
|
@@ -372,13 +376,15 @@ async function startStudio() {
|
|
|
372
376
|
try {
|
|
373
377
|
store = await (await addonMod("db")).createStore();
|
|
374
378
|
} catch (e) {
|
|
375
|
-
console.error("Studio: couldn't connect the store
|
|
379
|
+
console.error("Studio: couldn't connect the store â " + e.message);
|
|
376
380
|
process.exit(1);
|
|
377
381
|
}
|
|
378
382
|
const PORT = Number(process.env.PORT) || Number(readEnvFile().PORT) || DEFAULT_PORT;
|
|
379
383
|
const visible = (n) => n && !HIDDEN_COLLECTIONS.has(n);
|
|
380
384
|
const assets = {
|
|
381
385
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
386
|
+
"/logo.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "logo.webp"))],
|
|
387
|
+
"/favicon.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "favicon.webp"))],
|
|
382
388
|
"/db-admin-ui.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(ADDONS_DIR, "db", "files", "public", "db-admin-ui.js"))],
|
|
383
389
|
};
|
|
384
390
|
const studioHtml = fs.readFileSync(path.join(__dirname, "setup", "studio.html"));
|
|
@@ -427,15 +433,15 @@ async function startStudio() {
|
|
|
427
433
|
|
|
428
434
|
server.listen(PORT, "127.0.0.1", () => {
|
|
429
435
|
const url = `http://localhost:${PORT}`;
|
|
430
|
-
console.log(`\n
|
|
431
|
-
console.log(" Browse your data. localhost-only, disposable
|
|
436
|
+
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
437
|
+
console.log(" Browse your data. localhost-only, disposable â Ctrl-C when done.");
|
|
432
438
|
const ssh = process.env.SSH_CONNECTION;
|
|
433
439
|
if (ssh) {
|
|
434
440
|
const host = ssh.split(" ")[2];
|
|
435
441
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
436
|
-
console.log(" Remote box
|
|
442
|
+
console.log(" Remote box â from your LOCAL machine:");
|
|
437
443
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
438
|
-
console.log(`
|
|
444
|
+
console.log(` â¦then open ${url}.`);
|
|
439
445
|
}
|
|
440
446
|
console.log("");
|
|
441
447
|
openBrowser(url);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
6
7
|
<title>Set up your Volt app</title>
|
|
7
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
9
|
<style>
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
<body>
|
|
18
19
|
<main class="container py-5" style="max-width: 640px;">
|
|
19
20
|
<header class="mb-4">
|
|
20
|
-
<h1 class="h3"><span class="accent"
|
|
21
|
+
<h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Set up your Volt app</span></h1>
|
|
21
22
|
<p class="text-muted mb-0">Fill these in and the app starts. This page is disposable — it disappears once you click Apply. Re-open it anytime with <code>npm run dev -- --edit</code>.</p>
|
|
22
23
|
</header>
|
|
23
24
|
<div id="app"></div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
6
7
|
<title>Volt Studio</title>
|
|
7
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
9
|
<style>
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
<body>
|
|
15
16
|
<main class="container py-5" style="max-width: 820px;">
|
|
16
17
|
<header class="mb-4">
|
|
17
|
-
<h1 class="h3"><span class="accent"
|
|
18
|
+
<h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt Studio</span> <small class="text-muted">data browser</small></h1>
|
|
18
19
|
<p class="text-muted mb-0">Browse the database in your <code>.env</code>. Disposable & localhost-only — close the terminal when done.</p>
|
|
19
20
|
</header>
|
|
20
21
|
<div id="app"></div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
6
7
|
<title>Volt — signals, no build, hot reload</title>
|
|
7
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
9
|
<style>
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
<body>
|
|
20
21
|
<main class="container py-5" style="max-width: 720px;">
|
|
21
22
|
<header class="text-center mb-5">
|
|
22
|
-
<h1 class="brand display-5"><span class="accent"
|
|
23
|
+
<h1 class="brand display-5"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt</span></h1>
|
|
23
24
|
<p class="text-muted mb-0">
|
|
24
25
|
Fine-grained signals · no JSX · no virtual DOM · live hot reload.
|
|
25
26
|
Edit <code>public/app.js</code> and save — this page reloads itself.
|
|
@@ -147,7 +147,7 @@ const TABS = [
|
|
|
147
147
|
const nav = () =>
|
|
148
148
|
html`<nav class="navx py-2 mb-4">
|
|
149
149
|
<div class="container d-flex align-items-center" style="max-width:760px">
|
|
150
|
-
<span class="brand me-3"><span class="accent"
|
|
150
|
+
<span class="brand me-3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt</span></span>
|
|
151
151
|
${TABS.map(([key, label]) => html`<button class=${() => "btn btn-link btn-sm " + (section() === key ? "active" : "")} onclick=${() => section(key)}>${label}</button>`)}
|
|
152
152
|
<span class="ms-auto small text-muted">${() => (me() ? shortName(me()) : "guest")}</span>
|
|
153
153
|
</div>
|
|
Binary file
|
|
Binary file
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// server.js
|
|
1
|
+
// server.js â dev server with a built-in first-run setup wizard.
|
|
2
2
|
//
|
|
3
3
|
// First run (no .env) or `node server.js --edit` (-e) opens a disposable, local
|
|
4
4
|
// config page: tick add-ons, fill settings, Apply. Apply writes .env (a
|
|
5
5
|
// VOLT_ADDONS list + settings) and adds any needed packages to package.json,
|
|
6
|
-
// runs npm install, then starts the app
|
|
6
|
+
// runs npm install, then starts the app â which wires whatever .env enables.
|
|
7
7
|
// Add-on code is bundled under .volt/addons; nothing is copied into your code.
|
|
8
8
|
//
|
|
9
9
|
// No build step, no env-file flag: .env is auto-loaded below.
|
|
@@ -23,7 +23,7 @@ const PKG_PATH = path.join(__dirname, "package.json");
|
|
|
23
23
|
const ADDONS_DIR = path.join(__dirname, ".volt", "addons"); // bundled add-on sources
|
|
24
24
|
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
25
25
|
const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1", marked: "^18.0.5", busboy: "^1.6.0", "@aws-sdk/client-s3": "^3.1075.0" };
|
|
26
|
-
const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js", pages: "pages.js", media: "media.js" };
|
|
26
|
+
const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js", pages: "pages.js", posts: "posts.js", media: "media.js" };
|
|
27
27
|
|
|
28
28
|
// --- tiny .env loader (no dependency); never overrides an existing env var ---
|
|
29
29
|
function readEnvFile() {
|
|
@@ -68,7 +68,7 @@ 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
70
|
// Built-in add-ons are wired explicitly below; everything else in VOLT_ADDONS is
|
|
71
|
-
// a third-party add-on
|
|
71
|
+
// a third-party add-on â a local .volt/addons/<name>/index.js or an installed
|
|
72
72
|
// npm package "volt-addon-<name>" exporting register(ctx). See /docs/plugins.
|
|
73
73
|
const BUILTINS = new Set(Object.keys(LIB_FILE));
|
|
74
74
|
async function loadAddon(name) {
|
|
@@ -92,7 +92,7 @@ function openBrowser(url) {
|
|
|
92
92
|
const args = plat === "win32" ? ["/c", "start", "", url] : [url];
|
|
93
93
|
try {
|
|
94
94
|
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
95
|
-
child.on("error", () => {}); // launcher missing
|
|
95
|
+
child.on("error", () => {}); // launcher missing â emits async, don't crash
|
|
96
96
|
child.unref();
|
|
97
97
|
return true;
|
|
98
98
|
} catch {
|
|
@@ -120,7 +120,7 @@ async function startApp() {
|
|
|
120
120
|
if (enabled.has("mailer")) mailer = await (await addonMod("mailer")).createMailer();
|
|
121
121
|
if (enabled.has("auth") && store && mailer) app.use((await addonMod("auth")).authRouter({ store, mailer }));
|
|
122
122
|
|
|
123
|
-
// notes
|
|
123
|
+
// notes â a per-user CRUD example (auth-gated, owner-scoped, db-backed)
|
|
124
124
|
if (enabled.has("db") && enabled.has("auth") && store) {
|
|
125
125
|
const guard = (await addonMod("auth")).requireAuth(store);
|
|
126
126
|
const notes = store.collection("notes");
|
|
@@ -160,14 +160,16 @@ async function startApp() {
|
|
|
160
160
|
app.use(await (await addonMod("media")).mediaRouter({ requireAuth, dir: path.join(__dirname, "media") }));
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// markdown pages (/<slug>
|
|
163
|
+
// markdown pages (/<slug> â pages/<slug>.md) â mounted last, so app routes win
|
|
164
|
+
// blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) — before pages so /blog wins; renders in the same theme.
|
|
165
|
+
if (enabled.has("posts")) app.use(await (await addonMod("posts")).postsRouter({ dir: path.join(__dirname, "posts"), themeDir: path.join(__dirname, "pages") }));
|
|
164
166
|
if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
|
|
165
167
|
|
|
166
168
|
const server = http.createServer(app);
|
|
167
169
|
const io = new SocketServer(server);
|
|
168
170
|
if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
|
|
169
171
|
|
|
170
|
-
// third-party add-ons
|
|
172
|
+
// third-party add-ons â register(ctx). When auth is on, requireAuth/sessionFromReq
|
|
171
173
|
// are provided so add-ons can gate routes by login.
|
|
172
174
|
let requireAuth = null;
|
|
173
175
|
let sessionFromReq = null;
|
|
@@ -183,7 +185,7 @@ async function startApp() {
|
|
|
183
185
|
if (typeof register === "function") {
|
|
184
186
|
await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
|
|
185
187
|
} else {
|
|
186
|
-
console.warn(`[volt] add-on "${name}" not found or missing a register() export
|
|
188
|
+
console.warn(`[volt] add-on "${name}" not found or missing a register() export â skipped`);
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
|
|
@@ -191,7 +193,7 @@ async function startApp() {
|
|
|
191
193
|
const onChange = (file) => {
|
|
192
194
|
clearTimeout(timer);
|
|
193
195
|
timer = setTimeout(() => {
|
|
194
|
-
console.log(`[volt] change: ${file ?? "?"}
|
|
196
|
+
console.log(`[volt] change: ${file ?? "?"} â reload`);
|
|
195
197
|
io.emit("volt:reload");
|
|
196
198
|
}, 80);
|
|
197
199
|
};
|
|
@@ -215,7 +217,7 @@ async function startApp() {
|
|
|
215
217
|
for (const d of ["views", "public"]) watchRecursive(path.join(__dirname, d));
|
|
216
218
|
|
|
217
219
|
const on = [...enabled];
|
|
218
|
-
server.listen(PORT, () => console.log(
|
|
220
|
+
server.listen(PORT, () => console.log(`â¡ Volt â http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
|
|
219
221
|
}
|
|
220
222
|
|
|
221
223
|
// Packages an .env's selections need, beyond what package.json already has.
|
|
@@ -228,7 +230,7 @@ function neededPackages(env) {
|
|
|
228
230
|
if (driver === "mysql") want.push("mysql2");
|
|
229
231
|
if (driver === "postgres") want.push("pg");
|
|
230
232
|
if (/^\s*SMTP_URL\s*=\s*\S/m.test(env)) want.push("nodemailer");
|
|
231
|
-
if (/^\s*VOLT_ADDONS\s*=.*\
|
|
233
|
+
if (/^\s*VOLT_ADDONS\s*=.*\b(pages|posts)\b/m.test(env)) want.push("marked");
|
|
232
234
|
if (/^\s*VOLT_ADDONS\s*=.*\bmedia\b/m.test(env)) want.push("busboy");
|
|
233
235
|
if (/^\s*MEDIA_DRIVER\s*=\s*s3\b/m.test(env)) want.push("@aws-sdk/client-s3");
|
|
234
236
|
return want.filter((p) => !deps[p]);
|
|
@@ -239,7 +241,7 @@ function neededPackages(env) {
|
|
|
239
241
|
function ensureDriverInstalled(driver) {
|
|
240
242
|
const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
|
|
241
243
|
if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
|
|
242
|
-
console.log(`[volt] installing ${pkg} for the connection test
|
|
244
|
+
console.log(`[volt] installing ${pkg} for the connection testâ¦`);
|
|
243
245
|
spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
244
246
|
}
|
|
245
247
|
|
|
@@ -249,6 +251,8 @@ function startSetup() {
|
|
|
249
251
|
const assets = {
|
|
250
252
|
"/setup.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "setup", "setup.js"))],
|
|
251
253
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
254
|
+
"/logo.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "logo.webp"))],
|
|
255
|
+
"/favicon.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "favicon.webp"))],
|
|
252
256
|
};
|
|
253
257
|
const indexHtml = fs.readFileSync(path.join(__dirname, "setup", "index.html"));
|
|
254
258
|
|
|
@@ -340,15 +344,15 @@ function startSetup() {
|
|
|
340
344
|
server.closeIdleConnections?.();
|
|
341
345
|
};
|
|
342
346
|
if (added.length) {
|
|
343
|
-
console.log(`[volt] installing ${added.join(", ")}
|
|
347
|
+
console.log(`[volt] installing ${added.join(", ")}â¦`);
|
|
344
348
|
const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
|
|
345
349
|
npm.on("error", () => handoff());
|
|
346
350
|
npm.on("close", () => {
|
|
347
|
-
console.log("[volt] saved .env
|
|
351
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
348
352
|
handoff();
|
|
349
353
|
});
|
|
350
354
|
} else {
|
|
351
|
-
console.log("[volt] saved .env
|
|
355
|
+
console.log("[volt] saved .env â starting the appâ¦");
|
|
352
356
|
handoff();
|
|
353
357
|
}
|
|
354
358
|
});
|
|
@@ -365,18 +369,18 @@ function startSetup() {
|
|
|
365
369
|
|
|
366
370
|
server.listen(PORT, "127.0.0.1", () => {
|
|
367
371
|
const url = `http://localhost:${PORT}`;
|
|
368
|
-
console.log(`\n
|
|
372
|
+
console.log(`\nâ¡ Volt setup â ${url}`);
|
|
369
373
|
console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
|
|
370
374
|
const ssh = process.env.SSH_CONNECTION;
|
|
371
375
|
if (ssh) {
|
|
372
376
|
const host = ssh.split(" ")[2];
|
|
373
377
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
374
|
-
console.log(" Remote box
|
|
378
|
+
console.log(" Remote box â the server is up here; bridge it from your LOCAL machine:");
|
|
375
379
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
376
|
-
console.log(`
|
|
380
|
+
console.log(` â¦then open ${url} on your machine (the tunnel points it here).`);
|
|
377
381
|
}
|
|
378
382
|
console.log("");
|
|
379
|
-
if (openBrowser(url)) console.log(" (opening your browser
|
|
383
|
+
if (openBrowser(url)) console.log(" (opening your browserâ¦)\n");
|
|
380
384
|
});
|
|
381
385
|
}
|
|
382
386
|
|
|
@@ -384,8 +388,8 @@ function readEnvFileLines() {
|
|
|
384
388
|
return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
|
|
385
389
|
}
|
|
386
390
|
|
|
387
|
-
// --- Studio: an ephemeral, localhost-only data browser (
|
|
388
|
-
// Not a route in the running app
|
|
391
|
+
// --- Studio: an ephemeral, localhost-only data browser (Ã la Prisma Studio).
|
|
392
|
+
// Not a route in the running app â it only exists while you run `--studio`, on
|
|
389
393
|
// loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
|
|
390
394
|
const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
|
|
391
395
|
async function startStudio() {
|
|
@@ -398,13 +402,15 @@ async function startStudio() {
|
|
|
398
402
|
try {
|
|
399
403
|
store = await (await addonMod("db")).createStore();
|
|
400
404
|
} catch (e) {
|
|
401
|
-
console.error("Studio: couldn't connect the store
|
|
405
|
+
console.error("Studio: couldn't connect the store â " + e.message);
|
|
402
406
|
process.exit(1);
|
|
403
407
|
}
|
|
404
408
|
const PORT = Number(process.env.PORT) || Number(readEnvFile().PORT) || DEFAULT_PORT;
|
|
405
409
|
const visible = (n) => n && !HIDDEN_COLLECTIONS.has(n);
|
|
406
410
|
const assets = {
|
|
407
411
|
"/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "public", "volt.js"))],
|
|
412
|
+
"/logo.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "logo.webp"))],
|
|
413
|
+
"/favicon.webp": ["image/webp", fs.readFileSync(path.join(__dirname, "public", "favicon.webp"))],
|
|
408
414
|
"/db-admin-ui.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(ADDONS_DIR, "db", "files", "public", "db-admin-ui.js"))],
|
|
409
415
|
};
|
|
410
416
|
const studioHtml = fs.readFileSync(path.join(__dirname, "setup", "studio.html"));
|
|
@@ -453,15 +459,15 @@ async function startStudio() {
|
|
|
453
459
|
|
|
454
460
|
server.listen(PORT, "127.0.0.1", () => {
|
|
455
461
|
const url = `http://localhost:${PORT}`;
|
|
456
|
-
console.log(`\n
|
|
457
|
-
console.log(" Browse your data. localhost-only, disposable
|
|
462
|
+
console.log(`\nâ¡ Volt Studio â ${url} (${store.name})`);
|
|
463
|
+
console.log(" Browse your data. localhost-only, disposable â Ctrl-C when done.");
|
|
458
464
|
const ssh = process.env.SSH_CONNECTION;
|
|
459
465
|
if (ssh) {
|
|
460
466
|
const host = ssh.split(" ")[2];
|
|
461
467
|
const user = process.env.USER || process.env.USERNAME || "you";
|
|
462
|
-
console.log(" Remote box
|
|
468
|
+
console.log(" Remote box â from your LOCAL machine:");
|
|
463
469
|
console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
|
|
464
|
-
console.log(`
|
|
470
|
+
console.log(` â¦then open ${url}.`);
|
|
465
471
|
}
|
|
466
472
|
console.log("");
|
|
467
473
|
openBrowser(url);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
6
7
|
<title>Set up your Volt app</title>
|
|
7
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
9
|
<style>
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
<body>
|
|
18
19
|
<main class="container py-5" style="max-width: 640px;">
|
|
19
20
|
<header class="mb-4">
|
|
20
|
-
<h1 class="h3"><span class="accent"
|
|
21
|
+
<h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Set up your Volt app</span></h1>
|
|
21
22
|
<p class="text-muted mb-0">Fill these in and the app starts. This page is disposable — it disappears once you click Apply. Re-open it anytime with <code>npm run dev -- --edit</code>.</p>
|
|
22
23
|
</header>
|
|
23
24
|
<div id="app"></div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
6
7
|
<title>Volt Studio</title>
|
|
7
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
9
|
<style>
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
<body>
|
|
15
16
|
<main class="container py-5" style="max-width: 820px;">
|
|
16
17
|
<header class="mb-4">
|
|
17
|
-
<h1 class="h3"><span class="accent"
|
|
18
|
+
<h1 class="h3"><span class="accent"><img src="/logo.webp" alt="" style="height:1em;vertical-align:-.15em" /> Volt Studio</span> <small class="text-muted">data browser</small></h1>
|
|
18
19
|
<p class="text-muted mb-0">Browse the database in your <code>.env</code>. Disposable & localhost-only — close the terminal when done.</p>
|
|
19
20
|
</header>
|
|
20
21
|
<div id="app"></div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<link rel="icon" href="/favicon.webp" />
|
|
6
7
|
<title>Volt starter</title>
|
|
7
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
9
|
<style>
|