create-volt 0.32.0 → 0.34.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 +24 -0
- package/addons/posts/files/lib/posts.js +8 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/templates/blog/Dockerfile +20 -0
- package/templates/blog/Procfile +1 -0
- package/templates/blog/README.md +38 -0
- package/templates/blog/dockerignore +6 -0
- package/templates/blog/env +3 -0
- package/templates/blog/fly.toml +15 -0
- package/templates/blog/gitignore +5 -0
- package/templates/blog/package.json +17 -0
- package/templates/blog/pages/_theme.js +35 -0
- package/templates/blog/pages/about.md +8 -0
- package/templates/blog/posts/2026-06-10-themes.md +11 -0
- package/templates/blog/posts/2026-06-20-markdown-and-seo.md +14 -0
- package/templates/blog/posts/2026-06-28-hello-volt.md +17 -0
- package/templates/blog/public/favicon.webp +0 -0
- package/templates/blog/public/logo.webp +0 -0
- package/templates/blog/public/volt-ssr.js +63 -0
- package/templates/blog/public/volt.js +273 -0
- package/templates/blog/render.yaml +15 -0
- package/templates/blog/server.js +464 -0
- package/templates/blog/setup/index.html +28 -0
- package/templates/blog/setup/setup.js +200 -0
- package/templates/blog/setup/studio.html +29 -0
- package/templates/blog/views/index.html +25 -0
- package/templates/default/setup/setup.js +4 -0
- package/templates/default/views/index.html +2 -4
- package/templates/docs/Dockerfile +20 -0
- package/templates/docs/Procfile +1 -0
- package/templates/docs/README.md +24 -0
- package/templates/docs/dockerignore +6 -0
- package/templates/docs/env +2 -0
- package/templates/docs/fly.toml +15 -0
- package/templates/docs/gitignore +5 -0
- package/templates/docs/package.json +17 -0
- package/templates/docs/pages/_theme.js +32 -0
- package/templates/docs/pages/configuration.md +13 -0
- package/templates/docs/pages/deployment.md +9 -0
- package/templates/docs/pages/getting-started.md +14 -0
- package/templates/docs/public/favicon.webp +0 -0
- package/templates/docs/public/logo.webp +0 -0
- package/templates/docs/public/volt-ssr.js +63 -0
- package/templates/docs/public/volt.js +273 -0
- package/templates/docs/render.yaml +15 -0
- package/templates/docs/server.js +464 -0
- package/templates/docs/setup/index.html +28 -0
- package/templates/docs/setup/setup.js +200 -0
- package/templates/docs/setup/studio.html +29 -0
- package/templates/docs/views/index.html +15 -0
- package/templates/starter/setup/setup.js +4 -0
- package/templates/starter/views/index.html +1 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// volt.js — a tiny, no-build, signals-based UI library.
|
|
2
|
+
//
|
|
3
|
+
// Not React: there is no JSX, no virtual DOM, and no "re-render the whole
|
|
4
|
+
// component" step. State lives in *signals*; reading a signal inside a piece of
|
|
5
|
+
// UI subscribes that exact piece; writing the signal re-runs only those
|
|
6
|
+
// subscribers and touches only the precise text node / attribute that changed.
|
|
7
|
+
//
|
|
8
|
+
// Two ways to author UI, same engine underneath:
|
|
9
|
+
// 1. html`` — tagged-template markup with ${signal} holes
|
|
10
|
+
// 2. el(...) — imperative DOM helpers with function-children
|
|
11
|
+
// They interoperate freely (drop an el() node into an html`` template, etc.).
|
|
12
|
+
//
|
|
13
|
+
// Public API: signal, computed, effect, el, html, mount.
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Reactive core (signals + effects with ownership-based disposal)
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
let activeEffect = null;
|
|
20
|
+
|
|
21
|
+
// A signal is a function: call with no args to read, one arg to write.
|
|
22
|
+
// const n = signal(0); n(); // read → 0
|
|
23
|
+
// n(n()+1); // write → notifies subscribers
|
|
24
|
+
export function signal(value) {
|
|
25
|
+
const subs = new Set();
|
|
26
|
+
return function sig(...args) {
|
|
27
|
+
if (args.length) {
|
|
28
|
+
const next = args[0];
|
|
29
|
+
if (next === value) return value; // no-op on identical value
|
|
30
|
+
value = next;
|
|
31
|
+
for (const eff of [...subs]) eff.run(); // copy: run() mutates subs
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
if (activeEffect) {
|
|
35
|
+
subs.add(activeEffect);
|
|
36
|
+
activeEffect.deps.add(subs);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// effect(fn) runs fn now, tracks every signal it reads, and re-runs it whenever
|
|
43
|
+
// any of those change. Effects created *inside* another effect are owned by it
|
|
44
|
+
// and disposed before each re-run — so dynamic regions clean up after themselves.
|
|
45
|
+
export function effect(fn) {
|
|
46
|
+
const eff = {
|
|
47
|
+
deps: new Set(),
|
|
48
|
+
children: new Set(),
|
|
49
|
+
parent: activeEffect,
|
|
50
|
+
disposed: false,
|
|
51
|
+
run() {
|
|
52
|
+
// A signal write notifies a *snapshot* of subscribers; a parent re-render
|
|
53
|
+
// can dispose this effect before its turn in that snapshot — so skip if so.
|
|
54
|
+
if (eff.disposed) return;
|
|
55
|
+
disposeChildren(eff);
|
|
56
|
+
cleanupDeps(eff);
|
|
57
|
+
const prev = activeEffect;
|
|
58
|
+
activeEffect = eff;
|
|
59
|
+
try {
|
|
60
|
+
fn();
|
|
61
|
+
} finally {
|
|
62
|
+
activeEffect = prev;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
dispose() {
|
|
66
|
+
eff.disposed = true;
|
|
67
|
+
disposeChildren(eff);
|
|
68
|
+
cleanupDeps(eff);
|
|
69
|
+
if (eff.parent) eff.parent.children.delete(eff);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
if (activeEffect) activeEffect.children.add(eff);
|
|
73
|
+
eff.run();
|
|
74
|
+
return () => eff.dispose();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// computed(fn) is a read-only derived signal: () => value, auto-updating.
|
|
78
|
+
export function computed(fn) {
|
|
79
|
+
const s = signal(undefined);
|
|
80
|
+
effect(() => s(fn()));
|
|
81
|
+
return () => s();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cleanupDeps(eff) {
|
|
85
|
+
for (const subs of eff.deps) subs.delete(eff);
|
|
86
|
+
eff.deps.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function disposeChildren(eff) {
|
|
90
|
+
for (const child of [...eff.children]) child.dispose();
|
|
91
|
+
eff.children.clear();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// DOM helpers
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
// el(tag, props?, ...children) → a real DOM element.
|
|
99
|
+
// props: { onClick: fn } → event listener
|
|
100
|
+
// { class: () => ... } → reactive attribute (function = live)
|
|
101
|
+
// { id: 'x' } → static attribute
|
|
102
|
+
// children: strings, numbers, nodes, arrays, or functions (functions = live)
|
|
103
|
+
export function el(tag, props, ...children) {
|
|
104
|
+
const node = document.createElement(tag);
|
|
105
|
+
if (props) {
|
|
106
|
+
for (const [key, val] of Object.entries(props)) {
|
|
107
|
+
if (key.startsWith("on") && typeof val === "function") {
|
|
108
|
+
node.addEventListener(key.slice(2).toLowerCase(), val);
|
|
109
|
+
} else if (typeof val === "function") {
|
|
110
|
+
effect(() => setAttr(node, key, val()));
|
|
111
|
+
} else {
|
|
112
|
+
setAttr(node, key, val);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const child of children) appendChild(node, child);
|
|
117
|
+
return node;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// mount(target, ...children) appends children into target (selector or element).
|
|
121
|
+
// Top-level function-children are reactive too.
|
|
122
|
+
export function mount(target, ...children) {
|
|
123
|
+
const parent = typeof target === "string" ? document.querySelector(target) : target;
|
|
124
|
+
for (const child of children) appendChild(parent, child);
|
|
125
|
+
return parent;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function setAttr(node, name, value) {
|
|
129
|
+
if (name === "value") {
|
|
130
|
+
const v = value ?? "";
|
|
131
|
+
if (node.value !== v) node.value = v; // skip redundant writes — they reset the caret while typing
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (name === "checked" || name === "disabled" || name === "selected") {
|
|
135
|
+
node[name] = !!value && value !== "false";
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (value === false || value == null) {
|
|
139
|
+
node.removeAttribute(name);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
node.setAttribute(name, value);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Append a child, making function-children into self-updating dynamic regions
|
|
146
|
+
// bounded by two comment anchors (so they can render text, nodes, or lists).
|
|
147
|
+
function appendChild(parent, child) {
|
|
148
|
+
if (typeof child === "function") {
|
|
149
|
+
const start = document.createComment("");
|
|
150
|
+
const end = document.createComment("");
|
|
151
|
+
parent.appendChild(start);
|
|
152
|
+
parent.appendChild(end);
|
|
153
|
+
effect(() => renderRange(start, end, child()));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
for (const node of toNodes(child)) parent.appendChild(node);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Replace everything between the start/end anchors with `value`'s nodes.
|
|
160
|
+
function renderRange(start, end, value) {
|
|
161
|
+
if (!end.parentNode) return; // range detached (parent re-rendered) — nothing to do
|
|
162
|
+
let n = start.nextSibling;
|
|
163
|
+
while (n && n !== end) {
|
|
164
|
+
const t = n.nextSibling;
|
|
165
|
+
n.remove();
|
|
166
|
+
n = t;
|
|
167
|
+
}
|
|
168
|
+
for (const node of toNodes(value)) end.parentNode.insertBefore(node, end);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Normalize any child value into an array of DOM nodes.
|
|
172
|
+
function toNodes(value) {
|
|
173
|
+
if (value == null || value === false || value === true) return [];
|
|
174
|
+
if (Array.isArray(value)) return value.flatMap(toNodes);
|
|
175
|
+
if (value instanceof Node) return [value];
|
|
176
|
+
return [document.createTextNode(String(value))];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// html`` template layer (parses once, wires holes to the same primitives)
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
const PH = (i) => `__voltph${i}__`;
|
|
184
|
+
const PH_RE = /__voltph(\d+)__/g;
|
|
185
|
+
|
|
186
|
+
// We're inside an open tag (attribute context) if the last '<' comes after the
|
|
187
|
+
// last '>' in the accumulated string.
|
|
188
|
+
function isAttrContext(str) {
|
|
189
|
+
return str.lastIndexOf("<") > str.lastIndexOf(">");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function html(strings, ...values) {
|
|
193
|
+
let acc = "";
|
|
194
|
+
strings.forEach((str, i) => {
|
|
195
|
+
acc += str;
|
|
196
|
+
if (i < values.length) {
|
|
197
|
+
acc += isAttrContext(acc) ? PH(i) : `<!--${PH(i)}-->`;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const tpl = document.createElement("template");
|
|
202
|
+
tpl.innerHTML = acc.trim();
|
|
203
|
+
|
|
204
|
+
// Bind attribute holes.
|
|
205
|
+
for (const node of tpl.content.querySelectorAll("*")) {
|
|
206
|
+
for (const attr of [...node.attributes]) {
|
|
207
|
+
PH_RE.lastIndex = 0;
|
|
208
|
+
if (PH_RE.test(attr.value)) bindAttr(node, attr, values);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Bind node holes (comment placeholders).
|
|
213
|
+
const walker = document.createTreeWalker(tpl.content, NodeFilter.SHOW_COMMENT);
|
|
214
|
+
const holes = [];
|
|
215
|
+
let c;
|
|
216
|
+
while ((c = walker.nextNode())) {
|
|
217
|
+
const m = c.data.match(/^__voltph(\d+)__$/);
|
|
218
|
+
if (m) holes.push([c, Number(m[1])]);
|
|
219
|
+
}
|
|
220
|
+
for (const [comment, i] of holes) bindNodeHole(comment, values[i]);
|
|
221
|
+
|
|
222
|
+
const nodes = [...tpl.content.childNodes];
|
|
223
|
+
return nodes.length === 1 ? nodes[0] : nodes;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function bindAttr(node, attr, values) {
|
|
227
|
+
const name = attr.name;
|
|
228
|
+
const raw = attr.value;
|
|
229
|
+
const single = raw.match(/^__voltph(\d+)__$/);
|
|
230
|
+
node.removeAttribute(name);
|
|
231
|
+
|
|
232
|
+
// onX=${fn} → event listener
|
|
233
|
+
if (name.startsWith("on") && single) {
|
|
234
|
+
node.addEventListener(name.slice(2).toLowerCase(), values[Number(single[1])]);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Otherwise a (possibly mixed) attribute value. If any hole is a function it
|
|
239
|
+
// is read inside the effect, so the attribute stays live.
|
|
240
|
+
effect(() => {
|
|
241
|
+
const text = raw.replace(PH_RE, (_, j) => {
|
|
242
|
+
const v = values[Number(j)];
|
|
243
|
+
return String(typeof v === "function" ? v() : v ?? "");
|
|
244
|
+
});
|
|
245
|
+
setAttr(node, name, text);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function bindNodeHole(comment, value) {
|
|
250
|
+
const start = document.createComment("");
|
|
251
|
+
comment.parentNode.insertBefore(start, comment); // `comment` becomes the end anchor
|
|
252
|
+
if (typeof value === "function") {
|
|
253
|
+
effect(() => renderRange(start, comment, value()));
|
|
254
|
+
} else {
|
|
255
|
+
renderRange(start, comment, value);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Hot reload client — listens for the dev server's reload event over Socket.io
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
(function startHotReload() {
|
|
264
|
+
if (typeof window === "undefined") return; // not a browser (SSR / Node imports / tests)
|
|
265
|
+
const connect = () => {
|
|
266
|
+
if (!window.io) return false;
|
|
267
|
+
const socket = window.io();
|
|
268
|
+
socket.on("volt:reload", () => location.reload());
|
|
269
|
+
console.log("[volt] hot reload connected");
|
|
270
|
+
return true;
|
|
271
|
+
};
|
|
272
|
+
if (!connect()) window.addEventListener("load", connect);
|
|
273
|
+
})();
|
|
@@ -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
|