create-volt 0.16.0 → 0.18.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 +27 -0
- package/index.js +5 -0
- package/package.json +1 -1
- package/templates/default/Dockerfile +20 -0
- package/templates/default/Procfile +1 -0
- package/templates/default/dockerignore +6 -0
- package/templates/default/fly.toml +15 -0
- package/templates/default/public/volt-ssr.js +63 -0
- package/templates/default/render.yaml +15 -0
- package/templates/default/server.js +5 -1
- package/templates/guestbook/Dockerfile +20 -0
- package/templates/guestbook/Procfile +1 -0
- package/templates/guestbook/dockerignore +6 -0
- package/templates/guestbook/fly.toml +15 -0
- package/templates/guestbook/public/volt-ssr.js +63 -0
- package/templates/guestbook/render.yaml +15 -0
- package/templates/starter/Dockerfile +20 -0
- package/templates/starter/Procfile +1 -0
- package/templates/starter/dockerignore +6 -0
- package/templates/starter/fly.toml +15 -0
- package/templates/starter/public/volt-ssr.js +63 -0
- package/templates/starter/render.yaml +15 -0
- package/templates/starter/server.js +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ 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.18.0] - 2026-06-29
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Volt SSR** — `volt-ssr.js`, a tiny server-side renderer: render the same
|
|
11
|
+
`html` / `h()` markup and signal values to an HTML string in Node (`${values}`
|
|
12
|
+
escaped by default, `raw()` for trusted HTML) via `renderToString`. Ships in
|
|
13
|
+
every template, so a scaffolded app can be server-rendered for SEO + fast first
|
|
14
|
+
paint and hydrate with `volt.js` on the client. The Volt site itself is now
|
|
15
|
+
built with it — marketing pages as Volt components, docs as markdown rendered
|
|
16
|
+
with `raw()`, the whole page composed by `renderToString`.
|
|
17
|
+
|
|
18
|
+
## [0.17.0] - 2026-06-29
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **PaaS deploy targets** — every scaffold now ships a `Dockerfile`,
|
|
22
|
+
`.dockerignore`, `render.yaml`, `fly.toml`, and `Procfile`, so a Volt app
|
|
23
|
+
deploys to Render / Fly.io / Railway / DO App Platform (which handle the
|
|
24
|
+
server, DNS, and TLS) with config supplied as platform env vars. New
|
|
25
|
+
`/docs/deploy` guide covering the PaaS and PM2+nginx paths.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- The server boots straight into app mode (no setup wizard) when
|
|
29
|
+
`NODE_ENV=production` or `VOLT_ADDONS` is set via env — so a container/PaaS
|
|
30
|
+
runs the app from platform env vars without a committed `.env`.
|
|
31
|
+
|
|
7
32
|
## [0.16.0] - 2026-06-29
|
|
8
33
|
|
|
9
34
|
### Added
|
|
@@ -242,6 +267,8 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
242
267
|
watching and full-page hot reload. Supports `--skip-install` and `--force`,
|
|
243
268
|
and auto-detects npm / pnpm / yarn / bun for the install step.
|
|
244
269
|
|
|
270
|
+
[0.18.0]: https://github.com/MIR-2025/volt/releases/tag/v0.18.0
|
|
271
|
+
[0.17.0]: https://github.com/MIR-2025/volt/releases/tag/v0.17.0
|
|
245
272
|
[0.16.0]: https://github.com/MIR-2025/volt/releases/tag/v0.16.0
|
|
246
273
|
[0.15.1]: https://github.com/MIR-2025/volt/releases/tag/v0.15.1
|
|
247
274
|
[0.15.0]: https://github.com/MIR-2025/volt/releases/tag/v0.15.0
|
package/index.js
CHANGED
|
@@ -250,6 +250,11 @@ const shippedEnv = path.join(targetDir, "env");
|
|
|
250
250
|
if (fs.existsSync(shippedEnv)) {
|
|
251
251
|
fs.renameSync(shippedEnv, path.join(targetDir, ".env"));
|
|
252
252
|
}
|
|
253
|
+
// ship "dockerignore" → ".dockerignore" (same npm-safety dance)
|
|
254
|
+
const shippedDockerignore = path.join(targetDir, "dockerignore");
|
|
255
|
+
if (fs.existsSync(shippedDockerignore)) {
|
|
256
|
+
fs.renameSync(shippedDockerignore, path.join(targetDir, ".dockerignore"));
|
|
257
|
+
}
|
|
253
258
|
|
|
254
259
|
// Bundle the add-on sources so the app's setup wizard can enable them later
|
|
255
260
|
// (only for templates that ship the wizard, i.e. have a setup/ dir).
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Volt app — production container. Runs on Render, Fly.io, Railway, DO App
|
|
2
|
+
# Platform, and any container host. They handle the server, DNS, and TLS; you
|
|
3
|
+
# just set config as env vars.
|
|
4
|
+
#
|
|
5
|
+
# Configure via the platform's env vars (NOT a committed .env):
|
|
6
|
+
# VOLT_ADDONS=db,auth,... DB_DRIVER=... MONGODB_URI / DATABASE_URL
|
|
7
|
+
# MEDIA_DRIVER=s3 S3_ENDPOINT/S3_REGION/S3_BUCKET/S3_KEY/S3_SECRET etc.
|
|
8
|
+
#
|
|
9
|
+
# Tip: run the local wizard first (`npm run dev`) so the add-on packages are
|
|
10
|
+
# saved into package.json; commit package.json, then deploy and set the same
|
|
11
|
+
# config as env vars here. NODE_ENV=production makes the app boot straight up
|
|
12
|
+
# (no setup wizard).
|
|
13
|
+
FROM node:22-alpine
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
ENV NODE_ENV=production
|
|
16
|
+
COPY package*.json ./
|
|
17
|
+
RUN npm install --omit=dev
|
|
18
|
+
COPY . .
|
|
19
|
+
EXPOSE 8080
|
|
20
|
+
CMD ["node", "server.js"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
web: node server.js
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Fly.io — run `fly launch` (it uses the Dockerfile), then set config:
|
|
2
|
+
# fly secrets set VOLT_ADDONS=db,auth DB_DRIVER=mongodb MONGODB_URI=...
|
|
3
|
+
app = "volt-app"
|
|
4
|
+
|
|
5
|
+
[build]
|
|
6
|
+
|
|
7
|
+
[http_service]
|
|
8
|
+
internal_port = 8080
|
|
9
|
+
force_https = true
|
|
10
|
+
auto_stop_machines = true
|
|
11
|
+
auto_start_machines = true
|
|
12
|
+
|
|
13
|
+
[env]
|
|
14
|
+
PORT = "8080"
|
|
15
|
+
NODE_ENV = "production"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// volt-ssr.js — server-side rendering for Volt. Renders the same html`` markup,
|
|
2
|
+
// h() elements, and signal values to an HTML string in Node (no DOM), so a Volt
|
|
3
|
+
// app can be fully server-rendered for SEO and hydrate interactive islands with
|
|
4
|
+
// volt.js on the client.
|
|
5
|
+
//
|
|
6
|
+
// import { html, h, raw, renderToString } from "./volt-ssr.js";
|
|
7
|
+
// renderToString(html`<p>${name}</p>`) // → "<p>Ada</p>" (name is escaped)
|
|
8
|
+
//
|
|
9
|
+
// Authoring matches the client: html`` interpolations render as escaped text;
|
|
10
|
+
// nest html``/h() nodes for structure; use raw() for trusted pre-built HTML.
|
|
11
|
+
|
|
12
|
+
const VOID = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
|
|
13
|
+
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
14
|
+
|
|
15
|
+
// trusted, pre-rendered HTML — emitted verbatim. Use only for content you control.
|
|
16
|
+
export const raw = (s) => ({ __raw: String(s) });
|
|
17
|
+
|
|
18
|
+
// tagged-template markup; the literal chunks are trusted, ${values} are escaped.
|
|
19
|
+
export const html = (strings, ...values) => ({ __tpl: true, strings, values });
|
|
20
|
+
|
|
21
|
+
const isNode = (x) => x && typeof x === "object" && (x.__tpl || x.__raw || x.__el);
|
|
22
|
+
|
|
23
|
+
// hyperscript element: h(tag, props?, ...children)
|
|
24
|
+
export function h(tag, props, ...children) {
|
|
25
|
+
if (props === undefined || props === null || isNode(props) || Array.isArray(props) || typeof props !== "object") {
|
|
26
|
+
if (props !== undefined && props !== null) children.unshift(props);
|
|
27
|
+
props = {};
|
|
28
|
+
}
|
|
29
|
+
return { __el: true, tag, props, children };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const read = (v) => (typeof v === "function" ? v() : v); // resolve signals/thunks (once)
|
|
33
|
+
|
|
34
|
+
function attrs(props) {
|
|
35
|
+
let out = "";
|
|
36
|
+
for (const [k, rawVal] of Object.entries(props)) {
|
|
37
|
+
if (k === "children" || k.startsWith("on")) continue; // event handlers don't SSR
|
|
38
|
+
const v = read(rawVal);
|
|
39
|
+
if (v == null || v === false) continue;
|
|
40
|
+
const name = k === "className" ? "class" : k;
|
|
41
|
+
out += v === true ? ` ${name}` : ` ${name}="${esc(v)}"`;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function renderToString(node) {
|
|
47
|
+
const v = read(node);
|
|
48
|
+
if (v == null || v === false || v === true) return "";
|
|
49
|
+
if (typeof v === "string" || typeof v === "number") return esc(v);
|
|
50
|
+
if (v.__raw != null) return v.__raw;
|
|
51
|
+
if (Array.isArray(v)) return v.map(renderToString).join("");
|
|
52
|
+
if (v.__tpl) {
|
|
53
|
+
let out = v.strings[0];
|
|
54
|
+
for (let i = 0; i < v.values.length; i++) out += renderToString(v.values[i]) + v.strings[i + 1];
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
if (v.__el) {
|
|
58
|
+
if (typeof v.tag === "function") return renderToString(v.tag({ ...v.props, children: v.children }));
|
|
59
|
+
const open = `<${v.tag}${attrs(v.props)}>`;
|
|
60
|
+
return VOID.has(v.tag) ? open : `${open}${v.children.map(renderToString).join("")}</${v.tag}>`;
|
|
61
|
+
}
|
|
62
|
+
return esc(String(v));
|
|
63
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Render blueprint — https://render.com/docs/blueprint-spec
|
|
2
|
+
# Push this repo to GitHub, then in Render: New → Blueprint → pick the repo.
|
|
3
|
+
# Render builds the Dockerfile, gives you HTTPS + a domain, and runs it.
|
|
4
|
+
services:
|
|
5
|
+
- type: web
|
|
6
|
+
name: volt-app
|
|
7
|
+
runtime: docker
|
|
8
|
+
plan: starter
|
|
9
|
+
envVars:
|
|
10
|
+
- key: VOLT_ADDONS
|
|
11
|
+
sync: false # set your add-ons (e.g. "db,auth") in the dashboard
|
|
12
|
+
# Add the rest in the dashboard as needed:
|
|
13
|
+
# DB_DRIVER, MONGODB_URI / DATABASE_URL,
|
|
14
|
+
# MEDIA_DRIVER, S3_ENDPOINT/S3_REGION/S3_BUCKET/S3_KEY/S3_SECRET,
|
|
15
|
+
# SMTP_URL, MAIL_FROM
|
|
@@ -397,9 +397,13 @@ async function startStudio() {
|
|
|
397
397
|
|
|
398
398
|
// --- gate: studio / setup (first run, --edit) / the app ---
|
|
399
399
|
const editMode = process.argv.includes("--edit") || process.argv.includes("-e");
|
|
400
|
+
// In production / on a PaaS there's no interactive wizard: config comes from the
|
|
401
|
+
// platform's env vars (a Dockerfile sets NODE_ENV=production). Only fall back to
|
|
402
|
+
// the first-run wizard when nothing is configured and we're not in production.
|
|
403
|
+
const configured = fs.existsSync(ENV_PATH) || process.env.VOLT_ADDONS != null || process.env.NODE_ENV === "production";
|
|
400
404
|
if (process.argv.includes("--studio")) {
|
|
401
405
|
startStudio();
|
|
402
|
-
} else if (editMode || !
|
|
406
|
+
} else if (editMode || !configured) {
|
|
403
407
|
startSetup();
|
|
404
408
|
} else {
|
|
405
409
|
loadEnv();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Volt app — production container. Runs on Render, Fly.io, Railway, DO App
|
|
2
|
+
# Platform, and any container host. They handle the server, DNS, and TLS; you
|
|
3
|
+
# just set config as env vars.
|
|
4
|
+
#
|
|
5
|
+
# Configure via the platform's env vars (NOT a committed .env):
|
|
6
|
+
# VOLT_ADDONS=db,auth,... DB_DRIVER=... MONGODB_URI / DATABASE_URL
|
|
7
|
+
# MEDIA_DRIVER=s3 S3_ENDPOINT/S3_REGION/S3_BUCKET/S3_KEY/S3_SECRET etc.
|
|
8
|
+
#
|
|
9
|
+
# Tip: run the local wizard first (`npm run dev`) so the add-on packages are
|
|
10
|
+
# saved into package.json; commit package.json, then deploy and set the same
|
|
11
|
+
# config as env vars here. NODE_ENV=production makes the app boot straight up
|
|
12
|
+
# (no setup wizard).
|
|
13
|
+
FROM node:22-alpine
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
ENV NODE_ENV=production
|
|
16
|
+
COPY package*.json ./
|
|
17
|
+
RUN npm install --omit=dev
|
|
18
|
+
COPY . .
|
|
19
|
+
EXPOSE 8080
|
|
20
|
+
CMD ["node", "server.js"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
web: node server.js
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Fly.io — run `fly launch` (it uses the Dockerfile), then set config:
|
|
2
|
+
# fly secrets set VOLT_ADDONS=db,auth DB_DRIVER=mongodb MONGODB_URI=...
|
|
3
|
+
app = "volt-app"
|
|
4
|
+
|
|
5
|
+
[build]
|
|
6
|
+
|
|
7
|
+
[http_service]
|
|
8
|
+
internal_port = 8080
|
|
9
|
+
force_https = true
|
|
10
|
+
auto_stop_machines = true
|
|
11
|
+
auto_start_machines = true
|
|
12
|
+
|
|
13
|
+
[env]
|
|
14
|
+
PORT = "8080"
|
|
15
|
+
NODE_ENV = "production"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// volt-ssr.js — server-side rendering for Volt. Renders the same html`` markup,
|
|
2
|
+
// h() elements, and signal values to an HTML string in Node (no DOM), so a Volt
|
|
3
|
+
// app can be fully server-rendered for SEO and hydrate interactive islands with
|
|
4
|
+
// volt.js on the client.
|
|
5
|
+
//
|
|
6
|
+
// import { html, h, raw, renderToString } from "./volt-ssr.js";
|
|
7
|
+
// renderToString(html`<p>${name}</p>`) // → "<p>Ada</p>" (name is escaped)
|
|
8
|
+
//
|
|
9
|
+
// Authoring matches the client: html`` interpolations render as escaped text;
|
|
10
|
+
// nest html``/h() nodes for structure; use raw() for trusted pre-built HTML.
|
|
11
|
+
|
|
12
|
+
const VOID = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
|
|
13
|
+
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
14
|
+
|
|
15
|
+
// trusted, pre-rendered HTML — emitted verbatim. Use only for content you control.
|
|
16
|
+
export const raw = (s) => ({ __raw: String(s) });
|
|
17
|
+
|
|
18
|
+
// tagged-template markup; the literal chunks are trusted, ${values} are escaped.
|
|
19
|
+
export const html = (strings, ...values) => ({ __tpl: true, strings, values });
|
|
20
|
+
|
|
21
|
+
const isNode = (x) => x && typeof x === "object" && (x.__tpl || x.__raw || x.__el);
|
|
22
|
+
|
|
23
|
+
// hyperscript element: h(tag, props?, ...children)
|
|
24
|
+
export function h(tag, props, ...children) {
|
|
25
|
+
if (props === undefined || props === null || isNode(props) || Array.isArray(props) || typeof props !== "object") {
|
|
26
|
+
if (props !== undefined && props !== null) children.unshift(props);
|
|
27
|
+
props = {};
|
|
28
|
+
}
|
|
29
|
+
return { __el: true, tag, props, children };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const read = (v) => (typeof v === "function" ? v() : v); // resolve signals/thunks (once)
|
|
33
|
+
|
|
34
|
+
function attrs(props) {
|
|
35
|
+
let out = "";
|
|
36
|
+
for (const [k, rawVal] of Object.entries(props)) {
|
|
37
|
+
if (k === "children" || k.startsWith("on")) continue; // event handlers don't SSR
|
|
38
|
+
const v = read(rawVal);
|
|
39
|
+
if (v == null || v === false) continue;
|
|
40
|
+
const name = k === "className" ? "class" : k;
|
|
41
|
+
out += v === true ? ` ${name}` : ` ${name}="${esc(v)}"`;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function renderToString(node) {
|
|
47
|
+
const v = read(node);
|
|
48
|
+
if (v == null || v === false || v === true) return "";
|
|
49
|
+
if (typeof v === "string" || typeof v === "number") return esc(v);
|
|
50
|
+
if (v.__raw != null) return v.__raw;
|
|
51
|
+
if (Array.isArray(v)) return v.map(renderToString).join("");
|
|
52
|
+
if (v.__tpl) {
|
|
53
|
+
let out = v.strings[0];
|
|
54
|
+
for (let i = 0; i < v.values.length; i++) out += renderToString(v.values[i]) + v.strings[i + 1];
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
if (v.__el) {
|
|
58
|
+
if (typeof v.tag === "function") return renderToString(v.tag({ ...v.props, children: v.children }));
|
|
59
|
+
const open = `<${v.tag}${attrs(v.props)}>`;
|
|
60
|
+
return VOID.has(v.tag) ? open : `${open}${v.children.map(renderToString).join("")}</${v.tag}>`;
|
|
61
|
+
}
|
|
62
|
+
return esc(String(v));
|
|
63
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Render blueprint — https://render.com/docs/blueprint-spec
|
|
2
|
+
# Push this repo to GitHub, then in Render: New → Blueprint → pick the repo.
|
|
3
|
+
# Render builds the Dockerfile, gives you HTTPS + a domain, and runs it.
|
|
4
|
+
services:
|
|
5
|
+
- type: web
|
|
6
|
+
name: volt-app
|
|
7
|
+
runtime: docker
|
|
8
|
+
plan: starter
|
|
9
|
+
envVars:
|
|
10
|
+
- key: VOLT_ADDONS
|
|
11
|
+
sync: false # set your add-ons (e.g. "db,auth") in the dashboard
|
|
12
|
+
# Add the rest in the dashboard as needed:
|
|
13
|
+
# DB_DRIVER, MONGODB_URI / DATABASE_URL,
|
|
14
|
+
# MEDIA_DRIVER, S3_ENDPOINT/S3_REGION/S3_BUCKET/S3_KEY/S3_SECRET,
|
|
15
|
+
# SMTP_URL, MAIL_FROM
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Volt app — production container. Runs on Render, Fly.io, Railway, DO App
|
|
2
|
+
# Platform, and any container host. They handle the server, DNS, and TLS; you
|
|
3
|
+
# just set config as env vars.
|
|
4
|
+
#
|
|
5
|
+
# Configure via the platform's env vars (NOT a committed .env):
|
|
6
|
+
# VOLT_ADDONS=db,auth,... DB_DRIVER=... MONGODB_URI / DATABASE_URL
|
|
7
|
+
# MEDIA_DRIVER=s3 S3_ENDPOINT/S3_REGION/S3_BUCKET/S3_KEY/S3_SECRET etc.
|
|
8
|
+
#
|
|
9
|
+
# Tip: run the local wizard first (`npm run dev`) so the add-on packages are
|
|
10
|
+
# saved into package.json; commit package.json, then deploy and set the same
|
|
11
|
+
# config as env vars here. NODE_ENV=production makes the app boot straight up
|
|
12
|
+
# (no setup wizard).
|
|
13
|
+
FROM node:22-alpine
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
ENV NODE_ENV=production
|
|
16
|
+
COPY package*.json ./
|
|
17
|
+
RUN npm install --omit=dev
|
|
18
|
+
COPY . .
|
|
19
|
+
EXPOSE 8080
|
|
20
|
+
CMD ["node", "server.js"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
web: node server.js
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Fly.io — run `fly launch` (it uses the Dockerfile), then set config:
|
|
2
|
+
# fly secrets set VOLT_ADDONS=db,auth DB_DRIVER=mongodb MONGODB_URI=...
|
|
3
|
+
app = "volt-app"
|
|
4
|
+
|
|
5
|
+
[build]
|
|
6
|
+
|
|
7
|
+
[http_service]
|
|
8
|
+
internal_port = 8080
|
|
9
|
+
force_https = true
|
|
10
|
+
auto_stop_machines = true
|
|
11
|
+
auto_start_machines = true
|
|
12
|
+
|
|
13
|
+
[env]
|
|
14
|
+
PORT = "8080"
|
|
15
|
+
NODE_ENV = "production"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// volt-ssr.js — server-side rendering for Volt. Renders the same html`` markup,
|
|
2
|
+
// h() elements, and signal values to an HTML string in Node (no DOM), so a Volt
|
|
3
|
+
// app can be fully server-rendered for SEO and hydrate interactive islands with
|
|
4
|
+
// volt.js on the client.
|
|
5
|
+
//
|
|
6
|
+
// import { html, h, raw, renderToString } from "./volt-ssr.js";
|
|
7
|
+
// renderToString(html`<p>${name}</p>`) // → "<p>Ada</p>" (name is escaped)
|
|
8
|
+
//
|
|
9
|
+
// Authoring matches the client: html`` interpolations render as escaped text;
|
|
10
|
+
// nest html``/h() nodes for structure; use raw() for trusted pre-built HTML.
|
|
11
|
+
|
|
12
|
+
const VOID = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
|
|
13
|
+
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
14
|
+
|
|
15
|
+
// trusted, pre-rendered HTML — emitted verbatim. Use only for content you control.
|
|
16
|
+
export const raw = (s) => ({ __raw: String(s) });
|
|
17
|
+
|
|
18
|
+
// tagged-template markup; the literal chunks are trusted, ${values} are escaped.
|
|
19
|
+
export const html = (strings, ...values) => ({ __tpl: true, strings, values });
|
|
20
|
+
|
|
21
|
+
const isNode = (x) => x && typeof x === "object" && (x.__tpl || x.__raw || x.__el);
|
|
22
|
+
|
|
23
|
+
// hyperscript element: h(tag, props?, ...children)
|
|
24
|
+
export function h(tag, props, ...children) {
|
|
25
|
+
if (props === undefined || props === null || isNode(props) || Array.isArray(props) || typeof props !== "object") {
|
|
26
|
+
if (props !== undefined && props !== null) children.unshift(props);
|
|
27
|
+
props = {};
|
|
28
|
+
}
|
|
29
|
+
return { __el: true, tag, props, children };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const read = (v) => (typeof v === "function" ? v() : v); // resolve signals/thunks (once)
|
|
33
|
+
|
|
34
|
+
function attrs(props) {
|
|
35
|
+
let out = "";
|
|
36
|
+
for (const [k, rawVal] of Object.entries(props)) {
|
|
37
|
+
if (k === "children" || k.startsWith("on")) continue; // event handlers don't SSR
|
|
38
|
+
const v = read(rawVal);
|
|
39
|
+
if (v == null || v === false) continue;
|
|
40
|
+
const name = k === "className" ? "class" : k;
|
|
41
|
+
out += v === true ? ` ${name}` : ` ${name}="${esc(v)}"`;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function renderToString(node) {
|
|
47
|
+
const v = read(node);
|
|
48
|
+
if (v == null || v === false || v === true) return "";
|
|
49
|
+
if (typeof v === "string" || typeof v === "number") return esc(v);
|
|
50
|
+
if (v.__raw != null) return v.__raw;
|
|
51
|
+
if (Array.isArray(v)) return v.map(renderToString).join("");
|
|
52
|
+
if (v.__tpl) {
|
|
53
|
+
let out = v.strings[0];
|
|
54
|
+
for (let i = 0; i < v.values.length; i++) out += renderToString(v.values[i]) + v.strings[i + 1];
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
if (v.__el) {
|
|
58
|
+
if (typeof v.tag === "function") return renderToString(v.tag({ ...v.props, children: v.children }));
|
|
59
|
+
const open = `<${v.tag}${attrs(v.props)}>`;
|
|
60
|
+
return VOID.has(v.tag) ? open : `${open}${v.children.map(renderToString).join("")}</${v.tag}>`;
|
|
61
|
+
}
|
|
62
|
+
return esc(String(v));
|
|
63
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Render blueprint — https://render.com/docs/blueprint-spec
|
|
2
|
+
# Push this repo to GitHub, then in Render: New → Blueprint → pick the repo.
|
|
3
|
+
# Render builds the Dockerfile, gives you HTTPS + a domain, and runs it.
|
|
4
|
+
services:
|
|
5
|
+
- type: web
|
|
6
|
+
name: volt-app
|
|
7
|
+
runtime: docker
|
|
8
|
+
plan: starter
|
|
9
|
+
envVars:
|
|
10
|
+
- key: VOLT_ADDONS
|
|
11
|
+
sync: false # set your add-ons (e.g. "db,auth") in the dashboard
|
|
12
|
+
# Add the rest in the dashboard as needed:
|
|
13
|
+
# DB_DRIVER, MONGODB_URI / DATABASE_URL,
|
|
14
|
+
# MEDIA_DRIVER, S3_ENDPOINT/S3_REGION/S3_BUCKET/S3_KEY/S3_SECRET,
|
|
15
|
+
# SMTP_URL, MAIL_FROM
|
|
@@ -423,9 +423,13 @@ async function startStudio() {
|
|
|
423
423
|
|
|
424
424
|
// --- gate: studio / setup (first run, --edit) / the app ---
|
|
425
425
|
const editMode = process.argv.includes("--edit") || process.argv.includes("-e");
|
|
426
|
+
// In production / on a PaaS there's no interactive wizard: config comes from the
|
|
427
|
+
// platform's env vars (a Dockerfile sets NODE_ENV=production). Only fall back to
|
|
428
|
+
// the first-run wizard when nothing is configured and we're not in production.
|
|
429
|
+
const configured = fs.existsSync(ENV_PATH) || process.env.VOLT_ADDONS != null || process.env.NODE_ENV === "production";
|
|
426
430
|
if (process.argv.includes("--studio")) {
|
|
427
431
|
startStudio();
|
|
428
|
-
} else if (editMode || !
|
|
432
|
+
} else if (editMode || !configured) {
|
|
429
433
|
startSetup();
|
|
430
434
|
} else {
|
|
431
435
|
loadEnv();
|