tw-framework 1.0.0 → 1.0.2
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/dist/hydration-runtime.js +452 -0
- package/dist/tw.mjs +72 -7
- package/package.json +1 -1
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TW Framework -- client runtime v2 (~5KB, no dependencies).
|
|
3
|
+
*
|
|
4
|
+
* Loaded on every TW page. Provides:
|
|
5
|
+
* 1. Reactive state (seeded from <script id="__tw_state">)
|
|
6
|
+
* 2. Event delegation for [data-tw-event-*] handlers (.prevent/.stop)
|
|
7
|
+
* 3. Live interpolation spans [data-tw-i]
|
|
8
|
+
* 4. Two-way input binding [data-tw-model]
|
|
9
|
+
* 5. Conditional blocks [data-tw-if] / [data-tw-not]
|
|
10
|
+
* 6. Live list rendering <tw-for data-tw-var data-tw-list data-tw-tpl>
|
|
11
|
+
* 7. SPA client-side router (link interception, prefetch, popstate)
|
|
12
|
+
*/
|
|
13
|
+
(function () {
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
var stateEl = document.getElementById("__tw_state");
|
|
17
|
+
var state = {};
|
|
18
|
+
try { state = stateEl ? JSON.parse(stateEl.textContent) : {}; } catch (e) { state = {}; }
|
|
19
|
+
|
|
20
|
+
// Strict symbol scope (spec): only the page's OWN imports are exposed
|
|
21
|
+
// via __twClient.$page; layout imports carry dependency, not symbols.
|
|
22
|
+
var twClientScope = function () {
|
|
23
|
+
try { var c = globalThis.__twClient; return (c && c.$page) || {}; } catch (e) { return {}; }
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function evalExpr(expr) {
|
|
27
|
+
try {
|
|
28
|
+
return (new Function("state", "tw", "with (tw) { with (state) { return (" + expr + "); } }"))(state, twClientScope());
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function runHandler(code) {
|
|
35
|
+
try {
|
|
36
|
+
(new Function("state", "event", "tw", "with (tw) { with (state) { " + code + "; } }"))(state, window.__twEvent || {}, twClientScope());
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error("[tw] handler error:", code, e.message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setSpanText(el, expr) {
|
|
43
|
+
var val = evalExpr(expr);
|
|
44
|
+
var txt = val === undefined || val === null ? "" : String(val);
|
|
45
|
+
if (el.textContent !== txt) el.textContent = txt;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Conditional blocks -----------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function refreshIfs(scope) {
|
|
51
|
+
var ifs = (scope || document).querySelectorAll("[data-tw-if]");
|
|
52
|
+
for (var i = 0; i < ifs.length; i++) {
|
|
53
|
+
var el = ifs[i];
|
|
54
|
+
var val = evalExpr(el.getAttribute("data-tw-if"));
|
|
55
|
+
var show = !!val && val !== "false" && val !== "0" && val !== "";
|
|
56
|
+
el.style.display = show ? "" : "none";
|
|
57
|
+
}
|
|
58
|
+
var nots = (scope || document).querySelectorAll("[data-tw-not]");
|
|
59
|
+
for (var j = 0; j < nots.length; j++) {
|
|
60
|
+
var el2 = nots[j];
|
|
61
|
+
var val2 = evalExpr(el2.getAttribute("data-tw-not"));
|
|
62
|
+
var hide = !!val2 && val2 !== "false" && val2 !== "0" && val2 !== "";
|
|
63
|
+
el2.style.display = hide ? "none" : "";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Live lists (<tw-for>) ---------------------------------------------------
|
|
68
|
+
|
|
69
|
+
function renderTemplateString(tpl, loopVar, itemRef) {
|
|
70
|
+
// Evaluate all data-tw-i spans in the template HTML string, resolve
|
|
71
|
+
// nested conditionals, and substitute the loop variable inside event
|
|
72
|
+
// handlers so `todos.filter(x => x !== t)` works per item.
|
|
73
|
+
var holder = document.createElement("tw-render");
|
|
74
|
+
holder.innerHTML = tpl;
|
|
75
|
+
var spans = holder.querySelectorAll("[data-tw-i]");
|
|
76
|
+
for (var i = 0; i < spans.length; i++) setSpanText(spans[i], spans[i].getAttribute("data-tw-i"));
|
|
77
|
+
|
|
78
|
+
// Nested if/not inside the loop body (evaluated while loopVar is bound)
|
|
79
|
+
var all = holder.querySelectorAll("*");
|
|
80
|
+
for (var a = 0; a < all.length; a++) {
|
|
81
|
+
var el = all[a];
|
|
82
|
+
var ifAttr = el.getAttribute && el.getAttribute("data-tw-if");
|
|
83
|
+
if (ifAttr !== null && ifAttr !== undefined && el.hasAttribute("data-tw-if")) {
|
|
84
|
+
var v1 = evalExpr(ifAttr);
|
|
85
|
+
el.style.display = (v1 && v1 !== "false" && v1 !== "0" && v1 !== "") ? "" : "none";
|
|
86
|
+
}
|
|
87
|
+
var notAttr = el.getAttribute("data-tw-not");
|
|
88
|
+
if (notAttr !== null && notAttr !== undefined && el.hasAttribute("data-tw-not")) {
|
|
89
|
+
var v2 = evalExpr(notAttr);
|
|
90
|
+
el.style.display = (v2 && v2 !== "false" && v2 !== "0" && v2 !== "") ? "none" : "";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Substitute the loop variable inside event handler expressions.
|
|
95
|
+
// itemRef is a state key bound to the ACTUAL array item (identity is
|
|
96
|
+
// preserved, so `todos.filter(x => x !== t)` works by reference).
|
|
97
|
+
if (loopVar && itemRef) {
|
|
98
|
+
var re = new RegExp("\\b" + loopVar + "\\b", "g");
|
|
99
|
+
for (var e2 = 0; e2 < all.length; e2++) {
|
|
100
|
+
var el2 = all[e2];
|
|
101
|
+
if (!el2.attributes) continue;
|
|
102
|
+
for (var ai = 0; ai < el2.attributes.length; ai++) {
|
|
103
|
+
var attr = el2.attributes[ai];
|
|
104
|
+
if (attr.name && attr.name.indexOf("data-tw-event-") === 0 && attr.name.indexOf("-prevent") === -1 && attr.name.indexOf("-stop") === -1) {
|
|
105
|
+
var nv = attr.value.replace(re, itemRef);
|
|
106
|
+
if (nv !== attr.value) el2.setAttribute(attr.name, nv);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var out = holder.innerHTML;
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
var twItemSeq = 0;
|
|
117
|
+
|
|
118
|
+
function refreshLoops() {
|
|
119
|
+
var loops = document.querySelectorAll("tw-for[data-tw-list]");
|
|
120
|
+
for (var i = 0; i < loops.length; i++) {
|
|
121
|
+
var el = loops[i];
|
|
122
|
+
var arr = evalExpr(el.getAttribute("data-tw-list"));
|
|
123
|
+
var v = el.getAttribute("data-tw-var");
|
|
124
|
+
var tpl = el.getAttribute("data-tw-tpl") || "";
|
|
125
|
+
if (!Array.isArray(arr)) arr = [];
|
|
126
|
+
// clean refs from the previous render
|
|
127
|
+
if (el.__twRefs) {
|
|
128
|
+
for (var r = 0; r < el.__twRefs.length; r++) delete state[el.__twRefs[r]];
|
|
129
|
+
}
|
|
130
|
+
el.__twRefs = [];
|
|
131
|
+
var html = "";
|
|
132
|
+
for (var k = 0; k < arr.length; k++) {
|
|
133
|
+
var ref = "__tw_item_" + (++twItemSeq);
|
|
134
|
+
state[ref] = arr[k];
|
|
135
|
+
el.__twRefs.push(ref);
|
|
136
|
+
state[v] = arr[k];
|
|
137
|
+
html += renderTemplateString(tpl, v, ref);
|
|
138
|
+
}
|
|
139
|
+
delete state[v];
|
|
140
|
+
if (el.__twLen !== arr.length + ":" + html.length || el.__twHtml !== html) {
|
|
141
|
+
el.innerHTML = html;
|
|
142
|
+
el.__twLen = arr.length + ":" + html.length;
|
|
143
|
+
el.__twHtml = html;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Full refresh --------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
function refresh() {
|
|
151
|
+
try { applyActiveClass(); } catch (e) { /* non-DOM context */ }
|
|
152
|
+
try { scanPrefetch(); } catch (e) { /* non-DOM context */ }
|
|
153
|
+
var spans = document.querySelectorAll("[data-tw-i]");
|
|
154
|
+
for (var i = 0; i < spans.length; i++) setSpanText(spans[i], spans[i].getAttribute("data-tw-i"));
|
|
155
|
+
var inputs = document.querySelectorAll("[data-tw-model]");
|
|
156
|
+
for (var j = 0; j < inputs.length; j++) {
|
|
157
|
+
var key = inputs[j].getAttribute("data-tw-model");
|
|
158
|
+
if (key in state) {
|
|
159
|
+
var v = state[key];
|
|
160
|
+
if (String(inputs[j].value) !== String(v)) inputs[j].value = v;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
refreshLoops();
|
|
164
|
+
refreshIfs();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- Event delegation ----------------------------------------------------
|
|
168
|
+
|
|
169
|
+
var EVENTS = ["click", "dblclick", "input", "change", "submit", "keydown",
|
|
170
|
+
"keyup", "keypress", "focus", "blur", "mouseenter", "mouseleave", "wheel"];
|
|
171
|
+
|
|
172
|
+
EVENTS.forEach(function (ev) {
|
|
173
|
+
document.addEventListener(ev, function (e) {
|
|
174
|
+
var t = e.target;
|
|
175
|
+
if (!t || !t.closest) return;
|
|
176
|
+
var host = t.closest("[data-tw-event-" + ev + "]");
|
|
177
|
+
if (!host) return;
|
|
178
|
+
var code = host.getAttribute("data-tw-event-" + ev);
|
|
179
|
+
if (!code) return;
|
|
180
|
+
if (host.getAttribute("data-tw-event-" + ev + "-prevent") === "true") e.preventDefault();
|
|
181
|
+
if (host.getAttribute("data-tw-event-" + ev + "-stop") === "true") e.stopPropagation();
|
|
182
|
+
window.__twEvent = { target: t, value: t.value, type: ev };
|
|
183
|
+
runHandler(code);
|
|
184
|
+
refresh();
|
|
185
|
+
}, true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Two-way: input with data-tw-model writes to state
|
|
189
|
+
document.addEventListener("input", function (e) {
|
|
190
|
+
var t = e.target;
|
|
191
|
+
if (t && t.getAttribute && t.getAttribute("data-tw-model")) {
|
|
192
|
+
state[t.getAttribute("data-tw-model")] = t.value;
|
|
193
|
+
refresh();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// --- SPA client-side router ----------------------------------------------
|
|
198
|
+
|
|
199
|
+
var prefetched = {};
|
|
200
|
+
|
|
201
|
+
function sameOrigin(href) {
|
|
202
|
+
return href.charAt(0) === "/" && href.charAt(1) !== "/";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function swapPage(html, path) {
|
|
206
|
+
var newDoc = null;
|
|
207
|
+
try {
|
|
208
|
+
newDoc = new DOMParser().parseFromString(html, "text/html");
|
|
209
|
+
} catch (e) { newDoc = null; }
|
|
210
|
+
|
|
211
|
+
// Swap roots must stay symmetric: a page compiled with an interactive
|
|
212
|
+
// root ([data-tw-root]) swaps that node only; a fully static page (no
|
|
213
|
+
// interactive root) swaps the whole <body>, so layout chrome that only
|
|
214
|
+
// exists on the incoming page is not lost.
|
|
215
|
+
var liveRoot = document.querySelector("[data-tw-root]");
|
|
216
|
+
var newRoot = newDoc ? (liveRoot ? (newDoc.querySelector("[data-tw-root]") || newDoc.body) : newDoc.body) : null;
|
|
217
|
+
var oldRoot = liveRoot || document.body;
|
|
218
|
+
if (!newRoot) { window.location.href = path; return; }
|
|
219
|
+
|
|
220
|
+
// Merge new state into the live state object (keep identity)
|
|
221
|
+
var seedEl = newDoc ? newDoc.getElementById("__tw_state") : null;
|
|
222
|
+
if (seedEl) {
|
|
223
|
+
try {
|
|
224
|
+
var fresh = JSON.parse(seedEl.textContent);
|
|
225
|
+
for (var k in state) delete state[k];
|
|
226
|
+
for (var k2 in fresh) state[k2] = fresh[k2];
|
|
227
|
+
} catch (e) { /* keep state */ }
|
|
228
|
+
}
|
|
229
|
+
// The new page may stream signals (or may not) -- re-evaluate the
|
|
230
|
+
// connection and derived specs against the swapped-in DOM.
|
|
231
|
+
loadDerivedSpecs();
|
|
232
|
+
recomputeDerived();
|
|
233
|
+
connectSignalStream();
|
|
234
|
+
|
|
235
|
+
// Sync route stylesheets: SPA navigation must load any stylesheet the
|
|
236
|
+
// incoming page links that the live document does not have yet
|
|
237
|
+
// (route-split css assets). Links already present are kept as-is.
|
|
238
|
+
try {
|
|
239
|
+
var newLinks = newDoc ? newDoc.querySelectorAll('link[rel="stylesheet"]') : [];
|
|
240
|
+
var have = {};
|
|
241
|
+
var cur = document.querySelectorAll('link[rel="stylesheet"]');
|
|
242
|
+
for (var ci = 0; ci < cur.length; ci++) have[cur[ci].getAttribute("href")] = true;
|
|
243
|
+
for (var ni = 0; ni < newLinks.length; ni++) {
|
|
244
|
+
var href = newLinks[ni].getAttribute("href");
|
|
245
|
+
if (href && !have[href]) {
|
|
246
|
+
var l = document.createElement("link");
|
|
247
|
+
l.setAttribute("rel", "stylesheet");
|
|
248
|
+
l.setAttribute("href", href);
|
|
249
|
+
document.head.appendChild(l);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch (e) { /* stylesheet sync is best-effort on SPA swap */ }
|
|
253
|
+
|
|
254
|
+
// Keep the live <body> element (preserves document.body identity and
|
|
255
|
+
// any listeners bound to it); swap its content instead of replacing it.
|
|
256
|
+
if (oldRoot === document.body) {
|
|
257
|
+
oldRoot.innerHTML = newRoot.innerHTML;
|
|
258
|
+
} else {
|
|
259
|
+
var imported = document.importNode ? document.importNode(newRoot, true) : newRoot.cloneNode(true);
|
|
260
|
+
if (oldRoot.replaceWith) oldRoot.replaceWith(imported);
|
|
261
|
+
else oldRoot.innerHTML = newRoot.innerHTML;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
var t = newDoc ? newDoc.querySelector("title") : null;
|
|
265
|
+
if (t) document.title = t.textContent;
|
|
266
|
+
|
|
267
|
+
// re-run scripts inside the new root? (SSR pages usually have none)
|
|
268
|
+
refresh();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function navigate(path, push) {
|
|
272
|
+
fetch(path, { headers: { "X-TW-Navigate": "1" } })
|
|
273
|
+
.then(function (r) { return r.text(); })
|
|
274
|
+
.then(function (html) {
|
|
275
|
+
swapPage(html, path);
|
|
276
|
+
if (push !== false) {
|
|
277
|
+
try { history.pushState({ tw: 1 }, "", path); } catch (e) { /* ignore */ }
|
|
278
|
+
}
|
|
279
|
+
window.scrollTo(0, 0);
|
|
280
|
+
})
|
|
281
|
+
.catch(function () { window.location.href = path; });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
document.addEventListener("click", function (e) {
|
|
285
|
+
if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
|
|
286
|
+
var a = e.target.closest ? e.target.closest("a[href]") : null;
|
|
287
|
+
if (!a) return;
|
|
288
|
+
var href = a.getAttribute("href");
|
|
289
|
+
if (!href || !sameOrigin(href) || href.startsWith("/api/")) return;
|
|
290
|
+
if (a.hasAttribute("data-tw-native") || a.target === "_blank") return;
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
navigate(href, true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
window.addEventListener("popstate", function () {
|
|
296
|
+
navigate(window.location.pathname + window.location.search, false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Prefetch on hover (warms the HTTP cache)
|
|
300
|
+
document.addEventListener("mouseover", function (e) {
|
|
301
|
+
var a = e.target.closest ? e.target.closest("a[href]") : null;
|
|
302
|
+
if (!a) return;
|
|
303
|
+
var href = a.getAttribute("href");
|
|
304
|
+
if (!href || !sameOrigin(href) || prefetched[href]) return;
|
|
305
|
+
if (a.hasAttribute("data-tw-no-prefetch")) return;
|
|
306
|
+
twPrefetch(href);
|
|
307
|
+
}, true);
|
|
308
|
+
|
|
309
|
+
// RouterLink prefetch strategies. The hover handler below covers the
|
|
310
|
+
// default; these cover the explicit ones:
|
|
311
|
+
// data-tw-prefetch="always" -> fetched immediately on load
|
|
312
|
+
// data-tw-prefetch="viewport" -> fetched when the link scrolls into view
|
|
313
|
+
// data-tw-no-prefetch -> never fetched
|
|
314
|
+
function twPrefetch(href) {
|
|
315
|
+
if (!href || !sameOrigin(href) || prefetched[href]) return;
|
|
316
|
+
prefetched[href] = true;
|
|
317
|
+
try { fetch(href).catch(function () {}); } catch (e) { /* ignore */ }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function scanPrefetch() {
|
|
321
|
+
var now = document.querySelectorAll('a[data-tw-prefetch="always"]');
|
|
322
|
+
for (var i = 0; i < now.length; i++) twPrefetch(now[i].getAttribute("href"));
|
|
323
|
+
|
|
324
|
+
var vp = document.querySelectorAll('a[data-tw-prefetch="viewport"]');
|
|
325
|
+
for (var j = 0; j < vp.length; j++) {
|
|
326
|
+
var a = vp[j];
|
|
327
|
+
if (a.__twObserved) continue;
|
|
328
|
+
a.__twObserved = true;
|
|
329
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
330
|
+
twPrefetch(a.getAttribute("href"));
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
var io = new IntersectionObserver(function (entries, obs) {
|
|
335
|
+
for (var k = 0; k < entries.length; k++) {
|
|
336
|
+
if (entries[k].isIntersecting) {
|
|
337
|
+
var el = entries[k].target;
|
|
338
|
+
el.__twViewportDone = true;
|
|
339
|
+
twPrefetch(el.getAttribute("href"));
|
|
340
|
+
obs.unobserve(el);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}, { rootMargin: "200px" });
|
|
344
|
+
io.observe(a);
|
|
345
|
+
} catch (e) { twPrefetch(a.getAttribute("href")); }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// RouterLink active-class: highlight the link whose route is current.
|
|
350
|
+
// Exact match wins; "/blog" also matches "/blog/post-1" (prefix).
|
|
351
|
+
function applyActiveClass() {
|
|
352
|
+
var links = document.querySelectorAll("a[data-tw-active-class]");
|
|
353
|
+
var path = window.location.pathname;
|
|
354
|
+
for (var i = 0; i < links.length; i++) {
|
|
355
|
+
var a = links[i];
|
|
356
|
+
var href = a.getAttribute("href") || "";
|
|
357
|
+
var on = href === path || (href !== "/" && path.indexOf(href + "/") === 0);
|
|
358
|
+
var cls = a.getAttribute("data-tw-active-class");
|
|
359
|
+
if (on) a.classList.add(cls);
|
|
360
|
+
else a.classList.remove(cls);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Signal Streaming (docs/183): pages with a __TW_SIGNALS__ manifest open
|
|
365
|
+
// a persistent stream and patch their signal bindings in place. Reconnect
|
|
366
|
+
// resumes from the last applied sequence number.
|
|
367
|
+
var activeStream = null;
|
|
368
|
+
|
|
369
|
+
// Derived signals (docs/183): named computed state, recomputed from the
|
|
370
|
+
// other state vars on boot and after every stream frame.
|
|
371
|
+
var derivedSpecs = null;
|
|
372
|
+
function loadDerivedSpecs() {
|
|
373
|
+
var el = document.getElementById("__TW_DERIVED__");
|
|
374
|
+
derivedSpecs = null;
|
|
375
|
+
if (!el) return;
|
|
376
|
+
try {
|
|
377
|
+
var spec = JSON.parse(el.textContent || "{}");
|
|
378
|
+
derivedSpecs = spec.derived || null;
|
|
379
|
+
} catch (e) { derivedSpecs = null; }
|
|
380
|
+
}
|
|
381
|
+
function recomputeDerived() {
|
|
382
|
+
if (!derivedSpecs) return;
|
|
383
|
+
for (var name in derivedSpecs) {
|
|
384
|
+
try { state[name] = evalExpr(derivedSpecs[name]); } catch (e) { /* keep old */ }
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
loadDerivedSpecs();
|
|
388
|
+
recomputeDerived();
|
|
389
|
+
|
|
390
|
+
function connectSignalStream() {
|
|
391
|
+
if (activeStream) { activeStream.close(); activeStream = null; }
|
|
392
|
+
var el = document.getElementById("__TW_SIGNALS__");
|
|
393
|
+
if (!el) return;
|
|
394
|
+
var manifest;
|
|
395
|
+
try { manifest = JSON.parse(el.textContent || "{}"); } catch (e) { return; }
|
|
396
|
+
var names = Object.keys(manifest.signals || {});
|
|
397
|
+
if (!names.length) return;
|
|
398
|
+
var lastSeq = 0;
|
|
399
|
+
|
|
400
|
+
function apply(payload) {
|
|
401
|
+
if (!payload || payload.v !== 1) return;
|
|
402
|
+
if (payload.snapshot) {
|
|
403
|
+
for (var k in payload.snapshot) {
|
|
404
|
+
if (state[k] !== undefined || names.indexOf(k) >= 0) state[k] = payload.snapshot[k];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
var ups = payload.updates || [];
|
|
408
|
+
for (var i = 0; i < ups.length; i++) {
|
|
409
|
+
var name = ups[i][0], value = ups[i][1];
|
|
410
|
+
if (names.indexOf(name) >= 0) state[name] = value;
|
|
411
|
+
}
|
|
412
|
+
if (payload.seq) lastSeq = payload.seq;
|
|
413
|
+
recomputeDerived();
|
|
414
|
+
refresh();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
var es = null;
|
|
418
|
+
function open() {
|
|
419
|
+
es = new EventSource(
|
|
420
|
+
"/_tw/stream?s=" + encodeURIComponent(names.join(",")) + "&since=" + lastSeq
|
|
421
|
+
);
|
|
422
|
+
es.onmessage = function (ev) {
|
|
423
|
+
try { apply(JSON.parse(ev.data)); } catch (e) { /* malformed frame */ }
|
|
424
|
+
};
|
|
425
|
+
es.onerror = function () {
|
|
426
|
+
// EventSource retries on its own, but with the original URL; close
|
|
427
|
+
// and reopen so the connection resumes from lastSeq.
|
|
428
|
+
try { es.close(); } catch (e) { /* already closed */ }
|
|
429
|
+
setTimeout(open, 1500);
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
open();
|
|
433
|
+
activeStream = {
|
|
434
|
+
close: function () {
|
|
435
|
+
try { es.close(); } catch (e) { /* already closed */ }
|
|
436
|
+
activeStream = null;
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
window.__tw.signalStream = { reconnect: open, lastSeq: function () { return lastSeq; } };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// expose for debugging / userland
|
|
443
|
+
window.__tw = {
|
|
444
|
+
state: state,
|
|
445
|
+
refresh: refresh,
|
|
446
|
+
eval: evalExpr,
|
|
447
|
+
navigate: navigate,
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
refresh();
|
|
451
|
+
connectSignalStream();
|
|
452
|
+
})();
|
package/dist/tw.mjs
CHANGED
|
@@ -7596,6 +7596,7 @@ function parseTWSelector(cursor, errors, state, parseBody) {
|
|
|
7596
7596
|
if (token.type === "DOT") {
|
|
7597
7597
|
cursor.advance();
|
|
7598
7598
|
let classVal = "";
|
|
7599
|
+
let prevWasMinus = false;
|
|
7599
7600
|
const classStart = cursor.peek()?.pos;
|
|
7600
7601
|
while (!cursor.done) {
|
|
7601
7602
|
const ct = cursor.peek();
|
|
@@ -7604,8 +7605,12 @@ function parseTWSelector(cursor, errors, state, parseBody) {
|
|
|
7604
7605
|
const after = cursor.peek(1);
|
|
7605
7606
|
if (after && after.type === "COLON") break;
|
|
7606
7607
|
}
|
|
7608
|
+
if (ct.type === "IDENT" && !prevWasMinus && classVal.length > 0 && (CLASS_BOUNDARY_BOOLEAN_ATTRS.has(ct.value) || inlineAttrStartsAt(cursor))) {
|
|
7609
|
+
break;
|
|
7610
|
+
}
|
|
7607
7611
|
if (ct.type === "IDENT" || ct.type === "TEXT" || ct.type === "KEYWORD") {
|
|
7608
7612
|
classVal += ct.value;
|
|
7613
|
+
prevWasMinus = false;
|
|
7609
7614
|
cursor.advance();
|
|
7610
7615
|
} else if (ct.type === "NUMBER") {
|
|
7611
7616
|
classVal += ct.value;
|
|
@@ -7615,6 +7620,7 @@ function parseTWSelector(cursor, errors, state, parseBody) {
|
|
|
7615
7620
|
cursor.advance();
|
|
7616
7621
|
} else if (ct.type === "MINUS") {
|
|
7617
7622
|
classVal += "-";
|
|
7623
|
+
prevWasMinus = true;
|
|
7618
7624
|
cursor.advance();
|
|
7619
7625
|
} else {
|
|
7620
7626
|
break;
|
|
@@ -7623,11 +7629,16 @@ function parseTWSelector(cursor, errors, state, parseBody) {
|
|
|
7623
7629
|
while (!cursor.done && cursor.peek()?.type === "DOT") {
|
|
7624
7630
|
cursor.advance();
|
|
7625
7631
|
let nextClass = "";
|
|
7632
|
+
let nextPrevMinus = false;
|
|
7626
7633
|
while (!cursor.done) {
|
|
7627
7634
|
const ct = cursor.peek();
|
|
7628
7635
|
if (!ct) break;
|
|
7636
|
+
if (ct.type === "IDENT" && !nextPrevMinus && nextClass.length > 0 && (CLASS_BOUNDARY_BOOLEAN_ATTRS.has(ct.value) || inlineAttrStartsAt(cursor))) {
|
|
7637
|
+
break;
|
|
7638
|
+
}
|
|
7629
7639
|
if (ct.type === "IDENT" || ct.type === "TEXT" || ct.type === "KEYWORD") {
|
|
7630
7640
|
nextClass += ct.value;
|
|
7641
|
+
nextPrevMinus = false;
|
|
7631
7642
|
cursor.advance();
|
|
7632
7643
|
} else if (ct.type === "NUMBER") {
|
|
7633
7644
|
nextClass += ct.value;
|
|
@@ -7665,7 +7676,7 @@ function parseTWSelector(cursor, errors, state, parseBody) {
|
|
|
7665
7676
|
const t2 = cursor.peek();
|
|
7666
7677
|
if (!t2) break;
|
|
7667
7678
|
if (t2.type === "LBRACE" || t2.type === "STRING") break;
|
|
7668
|
-
if (t2.type === "IDENT" && VOID_HEAD_TAGS_ATTR.has(t2.value)) {
|
|
7679
|
+
if (t2.type === "IDENT" && VOID_HEAD_TAGS_ATTR.has(t2.value) && (el.voidElement || VOID_HEAD_TAGS_ATTR.has(tag.toLowerCase()))) {
|
|
7669
7680
|
const after = cursor.peek(1);
|
|
7670
7681
|
if (after && (after.type === "IDENT" || after.type === "STRING")) break;
|
|
7671
7682
|
}
|
|
@@ -7734,13 +7745,21 @@ function parseTWSelector(cursor, errors, state, parseBody) {
|
|
|
7734
7745
|
}
|
|
7735
7746
|
if (t2.type === "IDENT" || t2.type === "KEYWORD") {
|
|
7736
7747
|
if (t2.pos.line > tagToken.pos.line) break;
|
|
7737
|
-
|
|
7748
|
+
let attrName = t2.value;
|
|
7738
7749
|
cursor.advance();
|
|
7750
|
+
while (cursor.peek() && cursor.peek().type === "MINUS") {
|
|
7751
|
+
const hyphNext = cursor.peek(1);
|
|
7752
|
+
if (hyphNext && (hyphNext.type === "IDENT" || hyphNext.type === "TEXT" || hyphNext.type === "KEYWORD")) {
|
|
7753
|
+
cursor.advance();
|
|
7754
|
+
cursor.advance();
|
|
7755
|
+
attrName += "-" + hyphNext.value;
|
|
7756
|
+
} else break;
|
|
7757
|
+
}
|
|
7739
7758
|
const vToken = cursor.peek();
|
|
7740
7759
|
if (vToken && (vToken.type === "STRING" || vToken.type === "NUMBER")) {
|
|
7741
7760
|
cursor.advance();
|
|
7742
7761
|
el.attrs.push(createAttribute(attrName, vToken.value, t2.pos.line, t2.pos.col));
|
|
7743
|
-
} else if (vToken && (vToken.type === "IDENT" || vToken.type === "KEYWORD")) {
|
|
7762
|
+
} else if (vToken && (vToken.type === "IDENT" || vToken.type === "KEYWORD") && vToken.pos.line === t2.pos.line) {
|
|
7744
7763
|
cursor.advance();
|
|
7745
7764
|
el.attrs.push(createAttribute(attrName, vToken.value, t2.pos.line, t2.pos.col));
|
|
7746
7765
|
} else {
|
|
@@ -7931,6 +7950,19 @@ function parseTextFromString(cursor) {
|
|
|
7931
7950
|
if (!token) return null;
|
|
7932
7951
|
return createText(token.value, token.pos.line, token.pos.col, false);
|
|
7933
7952
|
}
|
|
7953
|
+
function inlineAttrStartsAt(cursor) {
|
|
7954
|
+
let k = 1;
|
|
7955
|
+
for (; ; ) {
|
|
7956
|
+
const a = cursor.peek(k);
|
|
7957
|
+
if (!a) return false;
|
|
7958
|
+
if (a.type === "STRING") return true;
|
|
7959
|
+
if (a.type === "MINUS") {
|
|
7960
|
+
k += 2;
|
|
7961
|
+
continue;
|
|
7962
|
+
}
|
|
7963
|
+
return false;
|
|
7964
|
+
}
|
|
7965
|
+
}
|
|
7934
7966
|
function parseElement(cursor, errors, state, parseBody) {
|
|
7935
7967
|
const tagToken = cursor.advance();
|
|
7936
7968
|
if (!tagToken) return null;
|
|
@@ -8607,12 +8639,37 @@ function parseBlockBody2(cursor, parseBody) {
|
|
|
8607
8639
|
}
|
|
8608
8640
|
return parseBody(["KEYWORD", "RBRACE"]);
|
|
8609
8641
|
}
|
|
8642
|
+
var CLASS_BOUNDARY_BOOLEAN_ATTRS;
|
|
8610
8643
|
var init_statements = __esm({
|
|
8611
8644
|
"../../packages/compiler/tw/parser/statements/index.ts"() {
|
|
8612
8645
|
init_nodes();
|
|
8613
8646
|
init_recovery();
|
|
8614
8647
|
init_directives();
|
|
8615
8648
|
init_tw();
|
|
8649
|
+
CLASS_BOUNDARY_BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
|
|
8650
|
+
"disabled",
|
|
8651
|
+
"required",
|
|
8652
|
+
"readonly",
|
|
8653
|
+
"checked",
|
|
8654
|
+
"selected",
|
|
8655
|
+
"autofocus",
|
|
8656
|
+
"hidden",
|
|
8657
|
+
"multiple",
|
|
8658
|
+
"muted",
|
|
8659
|
+
"loop",
|
|
8660
|
+
"controls",
|
|
8661
|
+
"defer",
|
|
8662
|
+
"async",
|
|
8663
|
+
"autoplay",
|
|
8664
|
+
"reversed",
|
|
8665
|
+
"open",
|
|
8666
|
+
"inert",
|
|
8667
|
+
"itemscope",
|
|
8668
|
+
"novalidate",
|
|
8669
|
+
"allowfullscreen",
|
|
8670
|
+
"formnovalidate",
|
|
8671
|
+
"playsinline"
|
|
8672
|
+
]);
|
|
8616
8673
|
}
|
|
8617
8674
|
});
|
|
8618
8675
|
|
|
@@ -18429,6 +18486,10 @@ function wrapInterpolations(rendered, raw) {
|
|
|
18429
18486
|
for (const seg of segs) {
|
|
18430
18487
|
if (/^\{[^{}]+\}$/.test(seg)) {
|
|
18431
18488
|
const expr = seg.slice(1, -1);
|
|
18489
|
+
if (ctx_global_inhead) {
|
|
18490
|
+
out += escapeHTML(interpolate2(seg, lastStateVars));
|
|
18491
|
+
continue;
|
|
18492
|
+
}
|
|
18432
18493
|
out += `<span data-tw-i="${escapeHTML(expr)}"${dataTwSFor(expr)}>${escapeHTML(interpolate2(seg, lastStateVars))}</span>`;
|
|
18433
18494
|
} else if (seg) {
|
|
18434
18495
|
out += escapeHTML(seg);
|
|
@@ -18438,9 +18499,10 @@ function wrapInterpolations(rendered, raw) {
|
|
|
18438
18499
|
}
|
|
18439
18500
|
function generateText(text, ctx) {
|
|
18440
18501
|
ctx_global_interactive = ctx.interactive === true;
|
|
18502
|
+
ctx_global_inhead = ctx.inHead === true;
|
|
18441
18503
|
lastStateVars = ctx.stateVars;
|
|
18442
18504
|
if (text.isInterpolated) {
|
|
18443
|
-
if (ctx.interactive) {
|
|
18505
|
+
if (ctx.interactive && ctx.inHead !== true) {
|
|
18444
18506
|
return `<span data-tw-i="${escapeHTML(text.value)}"${dataTwSFor(text.value)}>${escapeHTML(interpolate2(`{${text.value}}`, ctx.stateVars))}</span>`;
|
|
18445
18507
|
}
|
|
18446
18508
|
return escapeHTML(interpolate2(`{${text.value}}`, ctx.stateVars));
|
|
@@ -18645,7 +18707,7 @@ function generateSlot(node, ctx) {
|
|
|
18645
18707
|
const fallback = node.fallback.map((c) => generateNode(c, ctx)).join("");
|
|
18646
18708
|
return `<!-- tw:slot:${node.name} -->${fallback}<!-- /tw:slot:${node.name} -->`;
|
|
18647
18709
|
}
|
|
18648
|
-
var VOID_TAGS5, BOOLEAN_ATTRS2, CSS_CHUNKS, CSS_ROUTE_CHUNKS, CSS_CAPTURE, activeBuiltinImports, SUSPENSE_BOOT_RUNTIME, suspenseCounter, ctx_global_interactive, lastStateVars, componentRegistry;
|
|
18710
|
+
var VOID_TAGS5, BOOLEAN_ATTRS2, CSS_CHUNKS, CSS_ROUTE_CHUNKS, CSS_CAPTURE, activeBuiltinImports, SUSPENSE_BOOT_RUNTIME, suspenseCounter, ctx_global_interactive, ctx_global_inhead, lastStateVars, componentRegistry;
|
|
18649
18711
|
var init_html2 = __esm({
|
|
18650
18712
|
"../../packages/compiler/tw/codegen/html.ts"() {
|
|
18651
18713
|
init_types4();
|
|
@@ -18733,6 +18795,7 @@ var init_html2 = __esm({
|
|
|
18733
18795
|
].join("\n");
|
|
18734
18796
|
suspenseCounter = 0;
|
|
18735
18797
|
ctx_global_interactive = false;
|
|
18798
|
+
ctx_global_inhead = false;
|
|
18736
18799
|
lastStateVars = {};
|
|
18737
18800
|
componentRegistry = /* @__PURE__ */ new Map();
|
|
18738
18801
|
}
|
|
@@ -48334,7 +48397,7 @@ async function buildCommand() {
|
|
|
48334
48397
|
const _bundleDir = globalThis.__TW_BUNDLE_DIR;
|
|
48335
48398
|
const _fu = import.meta.url.replace("file://", "");
|
|
48336
48399
|
const _srcDir = _fu.substring(0, _fu.lastIndexOf("/"));
|
|
48337
|
-
const runtimeSrc = _bundleDir ? resolve6(_bundleDir, "
|
|
48400
|
+
const runtimeSrc = _bundleDir ? resolve6(_bundleDir, "hydration-runtime.js") : resolve6(_srcDir, "../../../..", "packages/runtime/tw/client/hydration-runtime.js");
|
|
48338
48401
|
if (existsSync16(runtimeSrc)) {
|
|
48339
48402
|
copyFileSync2(runtimeSrc, join15(outDir, "__tw_runtime.js"));
|
|
48340
48403
|
console.log(" \x1B[36m\u26A1\x1B[0m hydration runtime: .tw/__tw_runtime.js");
|
|
@@ -54433,7 +54496,9 @@ var helpText = `
|
|
|
54433
54496
|
|
|
54434
54497
|
ship [target] Deploy to platform
|
|
54435
54498
|
adapter [name] Generate deployment adapter files (node, bun, docker, vercel)
|
|
54436
|
-
targets: vercel, netlify, cloudflare,
|
|
54499
|
+
targets: node, bun, docker, vercel, netlify, cloudflare,
|
|
54500
|
+
aws, digitalocean, render, railway, fly,
|
|
54501
|
+
github-pages, firebase, nginx, caddy
|
|
54437
54502
|
(default: auto-detect)
|
|
54438
54503
|
|
|
54439
54504
|
plugin <action> Manage plugins
|
package/package.json
CHANGED