@portel/photon 1.32.5 → 1.33.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/dist/auto-ui/beam/routes/api-config.js +1 -1
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
- package/dist/auto-ui/beam/types.d.ts +1 -0
- package/dist/auto-ui/beam/types.d.ts.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +58 -9
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/components/card.d.ts +1 -1
- package/dist/auto-ui/components/card.d.ts.map +1 -1
- package/dist/auto-ui/components/card.js +1 -1
- package/dist/auto-ui/components/card.js.map +1 -1
- package/dist/auto-ui/components/checklist.d.ts +1 -1
- package/dist/auto-ui/components/checklist.d.ts.map +1 -1
- package/dist/auto-ui/components/checklist.js +1 -1
- package/dist/auto-ui/components/checklist.js.map +1 -1
- package/dist/auto-ui/components/form.d.ts +1 -1
- package/dist/auto-ui/components/form.d.ts.map +1 -1
- package/dist/auto-ui/components/form.js +2 -2
- package/dist/auto-ui/components/form.js.map +1 -1
- package/dist/auto-ui/components/list.d.ts +1 -1
- package/dist/auto-ui/components/list.d.ts.map +1 -1
- package/dist/auto-ui/components/list.js +1 -1
- package/dist/auto-ui/components/list.js.map +1 -1
- package/dist/auto-ui/components/progress.d.ts +1 -1
- package/dist/auto-ui/components/progress.d.ts.map +1 -1
- package/dist/auto-ui/components/progress.js +1 -1
- package/dist/auto-ui/components/progress.js.map +1 -1
- package/dist/auto-ui/components/table.d.ts +1 -1
- package/dist/auto-ui/components/table.d.ts.map +1 -1
- package/dist/auto-ui/components/table.js +1 -1
- package/dist/auto-ui/components/table.js.map +1 -1
- package/dist/auto-ui/components/tree.d.ts +1 -1
- package/dist/auto-ui/components/tree.d.ts.map +1 -1
- package/dist/auto-ui/components/tree.js +1 -1
- package/dist/auto-ui/components/tree.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +1 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +40 -5
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/ui-resolver.d.ts +12 -1
- package/dist/auto-ui/ui-resolver.d.ts.map +1 -1
- package/dist/auto-ui/ui-resolver.js +19 -3
- package/dist/auto-ui/ui-resolver.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +13 -5
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/ps.d.ts +4 -0
- package/dist/cli/commands/ps.d.ts.map +1 -1
- package/dist/cli/commands/ps.js +19 -5
- package/dist/cli/commands/ps.js.map +1 -1
- package/dist/daemon/manager.d.ts +8 -0
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +46 -9
- package/dist/daemon/manager.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +55 -7
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/resource-server.d.ts.map +1 -1
- package/dist/resource-server.js +5 -2
- package/dist/resource-server.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +206 -14
- package/dist/server.js.map +1 -1
- package/dist/tsx-compiler.d.ts +65 -5
- package/dist/tsx-compiler.d.ts.map +1 -1
- package/dist/tsx-compiler.js +531 -52
- package/dist/tsx-compiler.js.map +1 -1
- package/package.json +3 -3
- package/templates/cloudflare/worker.ts.template +60 -0
package/dist/tsx-compiler.js
CHANGED
|
@@ -8,62 +8,374 @@
|
|
|
8
8
|
* Users can override with `@jsxImportSource` pragma or tsconfig.json in the
|
|
9
9
|
* ui/ folder to use React/Preact/Solid if they prefer.
|
|
10
10
|
*/
|
|
11
|
+
import * as crypto from 'crypto';
|
|
11
12
|
import * as esbuild from 'esbuild';
|
|
12
13
|
import * as fs from 'fs';
|
|
13
|
-
import * as fsAsync from 'fs/promises';
|
|
14
14
|
import * as os from 'os';
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
// ─── Built-in JSX Runtime ──────────────────────────────────────────────────
|
|
17
|
-
//
|
|
18
|
-
//
|
|
17
|
+
// Lightweight virtual-DOM with focus-preserving reconciliation. `h()` now
|
|
18
|
+
// returns plain descriptor objects (not DOM nodes); `render()` diffs them
|
|
19
|
+
// against the previous tree and patches the existing DOM in place.
|
|
20
|
+
//
|
|
21
|
+
// Rendering contract (see also docs/tsx-rendering.md):
|
|
22
|
+
// - DOM nodes are preserved across `render()` calls when the element's
|
|
23
|
+
// position and type (and key, if present) are stable. Patching keeps
|
|
24
|
+
// the same node so focus, selection, scrollTop, and other UA state
|
|
25
|
+
// survive a rerender.
|
|
26
|
+
// - For form controls (input/textarea/select), the `value` prop only
|
|
27
|
+
// touches the DOM when it actually differs, and selection/cursor is
|
|
28
|
+
// restored when the element is focused — controlled inputs work
|
|
29
|
+
// without losing focus per keystroke.
|
|
30
|
+
// - Use `defaultValue` (mapped to the DOM attribute) for uncontrolled
|
|
31
|
+
// inputs you want to manage with refs instead.
|
|
32
|
+
// - Provide a `key` on items in dynamic lists so reorders preserve the
|
|
33
|
+
// correct DOM nodes (and their focus/scroll state).
|
|
34
|
+
// - Event handlers (`onClick`, `onInput`, …) are stored on the node and
|
|
35
|
+
// dispatched via a single delegating listener per event type, so
|
|
36
|
+
// rerenders don't stack listeners.
|
|
19
37
|
const JSX_RUNTIME = `
|
|
20
|
-
export function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
export function Fragment() {}
|
|
39
|
+
|
|
40
|
+
function flattenInto(out, c) {
|
|
41
|
+
if (c == null || c === false || c === true) return;
|
|
42
|
+
if (Array.isArray(c)) { for (var i = 0; i < c.length; i++) flattenInto(out, c[i]); return; }
|
|
43
|
+
if (c && typeof c === 'object' && c.__phv === true && c.type === Fragment) {
|
|
44
|
+
for (var j = 0; j < c.children.length; j++) flattenInto(out, c.children[j]);
|
|
45
|
+
return;
|
|
25
46
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
out.push(c);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function h(type, props) {
|
|
51
|
+
var children = [];
|
|
52
|
+
for (var ai = 2; ai < arguments.length; ai++) flattenInto(children, arguments[ai]);
|
|
53
|
+
props = props || {};
|
|
54
|
+
if (typeof type === 'function' && type !== Fragment) {
|
|
55
|
+
var cp = {};
|
|
56
|
+
for (var ck in props) cp[ck] = props[ck];
|
|
57
|
+
cp.children = children.length <= 1 ? children[0] : children;
|
|
58
|
+
var rv = type(cp);
|
|
59
|
+
if (rv == null || rv === false || rv === true) {
|
|
60
|
+
return { __phv: true, type: Fragment, props: {}, children: [], key: null };
|
|
61
|
+
}
|
|
62
|
+
return rv;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
__phv: true,
|
|
66
|
+
type: type,
|
|
67
|
+
props: props,
|
|
68
|
+
children: children,
|
|
69
|
+
key: props.key != null ? props.key : null
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isVNode(x) { return x != null && typeof x === 'object' && x.__phv === true; }
|
|
74
|
+
function isTextLike(x) { return typeof x === 'string' || typeof x === 'number'; }
|
|
75
|
+
function isOnProp(k) {
|
|
76
|
+
return k.length > 2 && k.charCodeAt(0) === 111 && k.charCodeAt(1) === 110;
|
|
77
|
+
}
|
|
78
|
+
function isFormCtl(el) {
|
|
79
|
+
var t = el.tagName;
|
|
80
|
+
return t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function setProp(el, key, value, prev) {
|
|
84
|
+
if (key === 'children' || key === 'key' || key === 'ref') return;
|
|
85
|
+
|
|
86
|
+
if (isOnProp(key) && typeof value === 'function') {
|
|
87
|
+
var evt = key.slice(2).toLowerCase();
|
|
88
|
+
if (!el.__phH) {
|
|
89
|
+
el.__phH = {};
|
|
90
|
+
el.__phD = function (e) { var f = el.__phH[e.type]; if (f) f(e); };
|
|
91
|
+
}
|
|
92
|
+
if (!el.__phH[evt]) el.addEventListener(evt, el.__phD);
|
|
93
|
+
el.__phH[evt] = value;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (key === 'style' && value && typeof value === 'object') {
|
|
98
|
+
if (prev && typeof prev === 'object') {
|
|
99
|
+
for (var pk in prev) if (!(pk in value)) el.style[pk] = '';
|
|
100
|
+
}
|
|
101
|
+
for (var sk in value) {
|
|
102
|
+
var sv = value[sk];
|
|
103
|
+
el.style[sk] = sv == null ? '' : sv;
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (key === 'className') {
|
|
109
|
+
if (value == null || value === false || value === '') el.removeAttribute('class');
|
|
110
|
+
else el.setAttribute('class', String(value));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (key === 'htmlFor') {
|
|
115
|
+
if (value == null || value === false) el.removeAttribute('for');
|
|
116
|
+
else el.setAttribute('for', String(value));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (key === 'dangerouslySetInnerHTML') {
|
|
121
|
+
var nh = value && value.__html != null ? value.__html : '';
|
|
122
|
+
var ph = prev && prev.__html != null ? prev.__html : null;
|
|
123
|
+
if (nh !== ph) el.innerHTML = nh;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Controlled value: only touch the DOM when the live value differs, and
|
|
128
|
+
// preserve cursor/selection when the element is focused. This is what
|
|
129
|
+
// lets <input value={state}> work without losing focus per keystroke.
|
|
130
|
+
if (key === 'value' && isFormCtl(el)) {
|
|
131
|
+
var nv = value == null ? '' : String(value);
|
|
132
|
+
if (el.value !== nv) {
|
|
133
|
+
var focused = el.ownerDocument && el.ownerDocument.activeElement === el;
|
|
134
|
+
var s = null, e2 = null;
|
|
135
|
+
if (focused && 'selectionStart' in el) {
|
|
136
|
+
try { s = el.selectionStart; e2 = el.selectionEnd; } catch (_) {}
|
|
137
|
+
}
|
|
138
|
+
el.value = nv;
|
|
139
|
+
if (focused && s != null) {
|
|
140
|
+
try { el.setSelectionRange(s, e2 == null ? s : e2); } catch (_) {}
|
|
47
141
|
}
|
|
48
142
|
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (key === 'checked' && el.tagName === 'INPUT') {
|
|
147
|
+
var b = !!value;
|
|
148
|
+
if (el.checked !== b) el.checked = b;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 'defaultValue' / 'defaultChecked' map to the corresponding attribute
|
|
153
|
+
// and only seed the DOM on creation — the runtime never overwrites the
|
|
154
|
+
// user's input after that.
|
|
155
|
+
if (key === 'defaultValue' && isFormCtl(el)) {
|
|
156
|
+
if (prev === undefined) el.setAttribute('value', value == null ? '' : String(value));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (key === 'defaultChecked' && el.tagName === 'INPUT') {
|
|
160
|
+
if (prev === undefined && value) el.setAttribute('checked', '');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (value === false || value == null) { el.removeAttribute(key); return; }
|
|
165
|
+
if (value === true) { el.setAttribute(key, ''); return; }
|
|
166
|
+
el.setAttribute(key, String(value));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function unsetProp(el, key, prev) {
|
|
170
|
+
if (key === 'children' || key === 'key' || key === 'ref') return;
|
|
171
|
+
if (isOnProp(key) && typeof prev === 'function') {
|
|
172
|
+
var evt = key.slice(2).toLowerCase();
|
|
173
|
+
if (el.__phH) delete el.__phH[evt];
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (key === 'className') { el.removeAttribute('class'); return; }
|
|
177
|
+
if (key === 'htmlFor') { el.removeAttribute('for'); return; }
|
|
178
|
+
if (key === 'style') { el.removeAttribute('style'); return; }
|
|
179
|
+
if (key === 'value' && isFormCtl(el)) {
|
|
180
|
+
if (el.value !== '') el.value = '';
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (key === 'checked' && el.tagName === 'INPUT') {
|
|
184
|
+
if (el.checked) el.checked = false;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
el.removeAttribute(key);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function createDom(vnode) {
|
|
191
|
+
if (vnode == null || vnode === false || vnode === true) return null;
|
|
192
|
+
if (isTextLike(vnode)) return document.createTextNode(String(vnode));
|
|
193
|
+
if (vnode.type === Fragment) {
|
|
194
|
+
var frag = document.createDocumentFragment();
|
|
195
|
+
for (var fi = 0; fi < vnode.children.length; fi++) {
|
|
196
|
+
var fn = createDom(vnode.children[fi]);
|
|
197
|
+
if (fn) frag.appendChild(fn);
|
|
198
|
+
}
|
|
199
|
+
return frag;
|
|
200
|
+
}
|
|
201
|
+
var el = document.createElement(vnode.type);
|
|
202
|
+
el.__phV = vnode;
|
|
203
|
+
var props = vnode.props || {};
|
|
204
|
+
for (var pk in props) setProp(el, pk, props[pk], undefined);
|
|
205
|
+
for (var ci = 0; ci < vnode.children.length; ci++) {
|
|
206
|
+
var cn = createDom(vnode.children[ci]);
|
|
207
|
+
if (cn) el.appendChild(cn);
|
|
49
208
|
}
|
|
50
|
-
_append(el, children);
|
|
51
209
|
return el;
|
|
52
210
|
}
|
|
53
211
|
|
|
54
|
-
|
|
212
|
+
function sameType(a, b) {
|
|
213
|
+
if (isTextLike(a) && isTextLike(b)) return true;
|
|
214
|
+
if (isVNode(a) && isVNode(b)) {
|
|
215
|
+
if (a.type !== b.type) return false;
|
|
216
|
+
// Keys differing for the same type still count as different identities
|
|
217
|
+
// when reconciliation is keyed. Caller decides via key match first.
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function patchNode(parent, oldV, newV, dom) {
|
|
224
|
+
if (newV == null || newV === false || newV === true) {
|
|
225
|
+
if (dom && dom.parentNode === parent) parent.removeChild(dom);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
if (!dom) {
|
|
229
|
+
var fresh = createDom(newV);
|
|
230
|
+
if (fresh) parent.appendChild(fresh);
|
|
231
|
+
return fresh;
|
|
232
|
+
}
|
|
233
|
+
if (!sameType(oldV, newV)) {
|
|
234
|
+
var rep = createDom(newV);
|
|
235
|
+
if (rep) parent.replaceChild(rep, dom);
|
|
236
|
+
else if (dom.parentNode === parent) parent.removeChild(dom);
|
|
237
|
+
return rep;
|
|
238
|
+
}
|
|
239
|
+
if (isTextLike(newV)) {
|
|
240
|
+
var s2 = String(newV);
|
|
241
|
+
if (dom.nodeValue !== s2) dom.nodeValue = s2;
|
|
242
|
+
return dom;
|
|
243
|
+
}
|
|
244
|
+
if (newV.type === Fragment) {
|
|
245
|
+
var rep2 = createDom(newV);
|
|
246
|
+
if (rep2) parent.replaceChild(rep2, dom);
|
|
247
|
+
return rep2;
|
|
248
|
+
}
|
|
249
|
+
var oldProps = (isVNode(oldV) && oldV.props) || {};
|
|
250
|
+
var newProps = newV.props || {};
|
|
251
|
+
for (var ok in oldProps) {
|
|
252
|
+
if (!(ok in newProps)) unsetProp(dom, ok, oldProps[ok]);
|
|
253
|
+
}
|
|
254
|
+
for (var nk in newProps) {
|
|
255
|
+
// Always rebind event handler (we swap the stored fn). For others,
|
|
256
|
+
// skip when reference-equal.
|
|
257
|
+
if (oldProps[nk] !== newProps[nk] || isOnProp(nk)) {
|
|
258
|
+
setProp(dom, nk, newProps[nk], oldProps[nk]);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
dom.__phV = newV;
|
|
262
|
+
patchChildren(dom, isVNode(oldV) ? oldV.children : [], newV.children);
|
|
263
|
+
return dom;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function patchChildren(parent, oldChildren, newChildren) {
|
|
267
|
+
var oldLen = oldChildren.length;
|
|
268
|
+
var newLen = newChildren.length;
|
|
269
|
+
|
|
270
|
+
var keyed = false;
|
|
271
|
+
for (var ki = 0; ki < newLen && !keyed; ki++) {
|
|
272
|
+
var c = newChildren[ki];
|
|
273
|
+
if (isVNode(c) && c.key != null) keyed = true;
|
|
274
|
+
}
|
|
275
|
+
for (var kj = 0; kj < oldLen && !keyed; kj++) {
|
|
276
|
+
var oc = oldChildren[kj];
|
|
277
|
+
if (isVNode(oc) && oc.key != null) keyed = true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
var existing = [];
|
|
281
|
+
for (var dx = parent.firstChild; dx; dx = dx.nextSibling) existing.push(dx);
|
|
55
282
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
283
|
+
if (!keyed) {
|
|
284
|
+
var max = oldLen > newLen ? oldLen : newLen;
|
|
285
|
+
for (var i = 0; i < max; i++) {
|
|
286
|
+
var oldC = i < oldLen ? oldChildren[i] : undefined;
|
|
287
|
+
var newC = i < newLen ? newChildren[i] : undefined;
|
|
288
|
+
var dom = i < existing.length ? existing[i] : null;
|
|
289
|
+
if (newC === undefined) {
|
|
290
|
+
if (dom && dom.parentNode === parent) parent.removeChild(dom);
|
|
291
|
+
} else if (oldC === undefined || dom == null) {
|
|
292
|
+
var freshU = createDom(newC);
|
|
293
|
+
if (freshU) parent.appendChild(freshU);
|
|
294
|
+
} else {
|
|
295
|
+
patchNode(parent, oldC, newC, dom);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Keyed reconciliation: move existing nodes by key, create/remove the
|
|
302
|
+
// rest. Unkeyed siblings are matched by their position among unkeyed
|
|
303
|
+
// siblings (a forgiving extension to all-or-nothing keying).
|
|
304
|
+
var oldByKey = {};
|
|
305
|
+
for (var oi = 0; oi < oldLen; oi++) {
|
|
306
|
+
var oo = oldChildren[oi];
|
|
307
|
+
if (isVNode(oo) && oo.key != null) {
|
|
308
|
+
oldByKey[oo.key] = { v: oo, dom: existing[oi] };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
var unkeyedOld = [];
|
|
312
|
+
var unkeyedDom = [];
|
|
313
|
+
for (var oj = 0; oj < oldLen; oj++) {
|
|
314
|
+
var op = oldChildren[oj];
|
|
315
|
+
if (!(isVNode(op) && op.key != null)) {
|
|
316
|
+
unkeyedOld.push(op);
|
|
317
|
+
unkeyedDom.push(existing[oj]);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
var used = {};
|
|
321
|
+
var unkCursor = 0;
|
|
322
|
+
for (var ni = 0; ni < newLen; ni++) {
|
|
323
|
+
var nc = newChildren[ni];
|
|
324
|
+
var key = (isVNode(nc) && nc.key != null) ? nc.key : null;
|
|
325
|
+
var anchor = parent.childNodes[ni] || null;
|
|
326
|
+
var placed = null;
|
|
327
|
+
if (key != null && oldByKey[key]) {
|
|
328
|
+
var entry = oldByKey[key];
|
|
329
|
+
used[key] = true;
|
|
330
|
+
placed = patchNode(parent, entry.v, nc, entry.dom);
|
|
331
|
+
} else if (key == null && unkCursor < unkeyedOld.length) {
|
|
332
|
+
var oldUn = unkeyedOld[unkCursor];
|
|
333
|
+
var oldUnDom = unkeyedDom[unkCursor];
|
|
334
|
+
unkCursor++;
|
|
335
|
+
if (oldUnDom && sameType(oldUn, nc)) {
|
|
336
|
+
placed = patchNode(parent, oldUn, nc, oldUnDom);
|
|
337
|
+
} else {
|
|
338
|
+
if (oldUnDom && oldUnDom.parentNode === parent) parent.removeChild(oldUnDom);
|
|
339
|
+
placed = createDom(nc);
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
placed = createDom(nc);
|
|
343
|
+
}
|
|
344
|
+
if (placed && placed !== anchor) {
|
|
345
|
+
parent.insertBefore(placed, anchor);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Drop old keyed nodes that the new tree didn't claim.
|
|
350
|
+
for (var rk in oldByKey) {
|
|
351
|
+
if (!used[rk]) {
|
|
352
|
+
var dead = oldByKey[rk].dom;
|
|
353
|
+
if (dead && dead.parentNode === parent) parent.removeChild(dead);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Drop any trailing unkeyed leftovers we didn't consume.
|
|
357
|
+
while (unkCursor < unkeyedOld.length) {
|
|
358
|
+
var deadUn = unkeyedDom[unkCursor++];
|
|
359
|
+
if (deadUn && deadUn.parentNode === parent) parent.removeChild(deadUn);
|
|
360
|
+
}
|
|
361
|
+
// Belt-and-braces: trim any excess if our bookkeeping missed something.
|
|
362
|
+
while (parent.childNodes.length > newLen) {
|
|
363
|
+
parent.removeChild(parent.lastChild);
|
|
61
364
|
}
|
|
62
365
|
}
|
|
63
366
|
|
|
64
367
|
export function render(element, container) {
|
|
65
368
|
if (typeof container === 'string') container = document.querySelector(container);
|
|
66
|
-
container
|
|
369
|
+
if (!container) return null;
|
|
370
|
+
var prevTree = container.__phRoot;
|
|
371
|
+
var prevChildren = prevTree
|
|
372
|
+
? (Array.isArray(prevTree) ? prevTree : [prevTree])
|
|
373
|
+
: [];
|
|
374
|
+
var newChildren = (isVNode(element) && element.type === Fragment)
|
|
375
|
+
? element.children
|
|
376
|
+
: [element];
|
|
377
|
+
patchChildren(container, prevChildren, newChildren);
|
|
378
|
+
container.__phRoot = newChildren.length === 1 ? newChildren[0] : newChildren;
|
|
67
379
|
return element;
|
|
68
380
|
}
|
|
69
381
|
`.trim();
|
|
@@ -76,8 +388,103 @@ function getRuntimePath() {
|
|
|
76
388
|
}
|
|
77
389
|
return _runtimePath;
|
|
78
390
|
}
|
|
79
|
-
// In-memory cache: filePath → {
|
|
391
|
+
// In-memory cache: filePath → { sig, result }. `sig` is the newest mtime
|
|
392
|
+
// across the whole input graph, so a change to any imported module — not
|
|
393
|
+
// just the entry file — invalidates the cache.
|
|
80
394
|
const cache = new Map();
|
|
395
|
+
/** Resolve esbuild metafile input keys to absolute paths. */
|
|
396
|
+
function resolveInputs(metafile) {
|
|
397
|
+
if (!metafile)
|
|
398
|
+
return [];
|
|
399
|
+
const out = [];
|
|
400
|
+
for (const key of Object.keys(metafile.inputs)) {
|
|
401
|
+
// Skip esbuild virtual/injected entries (e.g. the JSX runtime shim).
|
|
402
|
+
if (key.includes('<') || key.startsWith('\0'))
|
|
403
|
+
continue;
|
|
404
|
+
out.push(path.resolve(process.cwd(), key));
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
/** Newest mtime across the input graph; -1 if any input is missing. */
|
|
409
|
+
function inputsSignature(inputs) {
|
|
410
|
+
let newest = 0;
|
|
411
|
+
for (const f of inputs) {
|
|
412
|
+
try {
|
|
413
|
+
newest = Math.max(newest, fs.statSync(f).mtimeMs);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return -1; // a vanished input forces a rebuild
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return newest;
|
|
420
|
+
}
|
|
421
|
+
/** Stable per-source cache directory under the OS temp dir. */
|
|
422
|
+
function cacheDirFor(filePath) {
|
|
423
|
+
const key = crypto.createHash('sha256').update(path.resolve(filePath)).digest('hex').slice(0, 16);
|
|
424
|
+
return path.join(os.tmpdir(), 'photon-tsx-cache', key);
|
|
425
|
+
}
|
|
426
|
+
/** Content hash of the bundle, salted with the esbuild version. */
|
|
427
|
+
function hashBundle(js) {
|
|
428
|
+
return crypto
|
|
429
|
+
.createHash('sha256')
|
|
430
|
+
.update(`${js}:::esbuild@${esbuild.version}`)
|
|
431
|
+
.digest('hex')
|
|
432
|
+
.slice(0, 12);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Write the shell + hashed JS to the cache dir and return the descriptor.
|
|
436
|
+
* Stale `*.js` from a previous hash are pruned so the dir stays bounded
|
|
437
|
+
* and a `photon build` copy never ships an orphaned old bundle.
|
|
438
|
+
*/
|
|
439
|
+
function writeArtifactsSync(filePath, js, inputs) {
|
|
440
|
+
const dir = cacheDirFor(filePath);
|
|
441
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
442
|
+
const base = path.basename(filePath, path.extname(filePath)).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
443
|
+
const hash = hashBundle(js);
|
|
444
|
+
const jsFileName = `${base}.${hash}.js`;
|
|
445
|
+
const html = wrapInHtml(jsFileName);
|
|
446
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
447
|
+
if (entry.endsWith('.js') && entry !== jsFileName) {
|
|
448
|
+
try {
|
|
449
|
+
fs.unlinkSync(path.join(dir, entry));
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// best-effort prune
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const jsPath = path.join(dir, jsFileName);
|
|
457
|
+
const htmlPath = path.join(dir, 'index.html');
|
|
458
|
+
fs.writeFileSync(jsPath, js);
|
|
459
|
+
fs.writeFileSync(htmlPath, html);
|
|
460
|
+
const entryAbs = path.resolve(filePath);
|
|
461
|
+
const allInputs = inputs.length ? Array.from(new Set([entryAbs, ...inputs])) : [entryAbs];
|
|
462
|
+
return { hash, jsFileName, html, js, dir, htmlPath, jsPath, inputs: allInputs };
|
|
463
|
+
}
|
|
464
|
+
/** Build-error descriptor: an HTML error page, no JS sidecar. */
|
|
465
|
+
function errorResult(filePath, error) {
|
|
466
|
+
const dir = cacheDirFor(filePath);
|
|
467
|
+
const html = wrapError(filePath, error);
|
|
468
|
+
let htmlPath = '';
|
|
469
|
+
try {
|
|
470
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
471
|
+
htmlPath = path.join(dir, 'index.html');
|
|
472
|
+
fs.writeFileSync(htmlPath, html);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
// serving paths fall back to the in-memory `html`
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
hash: '',
|
|
479
|
+
jsFileName: '',
|
|
480
|
+
html,
|
|
481
|
+
js: '',
|
|
482
|
+
dir,
|
|
483
|
+
htmlPath,
|
|
484
|
+
jsPath: '',
|
|
485
|
+
inputs: [path.resolve(filePath)],
|
|
486
|
+
};
|
|
487
|
+
}
|
|
81
488
|
/**
|
|
82
489
|
* Find the nearest tsconfig.json walking up from startDir.
|
|
83
490
|
* Stops at the photon asset folder root (parent of ui/).
|
|
@@ -163,6 +570,9 @@ function buildOptions(filePath, tsconfigPath) {
|
|
|
163
570
|
entryPoints: [filePath],
|
|
164
571
|
bundle: true,
|
|
165
572
|
write: false,
|
|
573
|
+
// Needed to invalidate the cache on any imported module change, not
|
|
574
|
+
// just the entry file — `metafile.inputs` lists the full graph.
|
|
575
|
+
metafile: true,
|
|
166
576
|
format: 'esm',
|
|
167
577
|
platform: 'browser',
|
|
168
578
|
target: 'es2020',
|
|
@@ -188,9 +598,35 @@ function buildOptions(filePath, tsconfigPath) {
|
|
|
188
598
|
};
|
|
189
599
|
}
|
|
190
600
|
/**
|
|
191
|
-
*
|
|
601
|
+
* The HTML shell. Carries no application code — it only references the
|
|
602
|
+
* content-hashed bundle as a sibling module, so the document itself is
|
|
603
|
+
* tiny and safe to serve with short-lived/revalidated caching while the
|
|
604
|
+
* hashed bundle is cached immutably. The reference is relative so it
|
|
605
|
+
* resolves to `/api/ui/<id>/<jsFileName>` (and the equivalent CF asset
|
|
606
|
+
* path) regardless of the mount point.
|
|
607
|
+
*/
|
|
608
|
+
function wrapInHtml(jsFileName) {
|
|
609
|
+
return `<!doctype html>
|
|
610
|
+
<html lang="en">
|
|
611
|
+
<head>
|
|
612
|
+
<meta charset="UTF-8">
|
|
613
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
614
|
+
<style>*, *::before, *::after { box-sizing: border-box; } body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }</style>
|
|
615
|
+
</head>
|
|
616
|
+
<body>
|
|
617
|
+
<div id="root"></div>
|
|
618
|
+
<script type="module" src="./${jsFileName}"></script>
|
|
619
|
+
</body>
|
|
620
|
+
</html>`;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Self-contained document with the bundle inlined. Used only by the MCP
|
|
624
|
+
* resource path (Claude Desktop apps): an MCP-app webview renders the
|
|
625
|
+
* returned HTML with no HTTP origin, so a `./<hash>.js` sibling reference
|
|
626
|
+
* would not resolve. Cache-busting is irrelevant there — the client
|
|
627
|
+
* re-reads the resource on every `resources/read`.
|
|
192
628
|
*/
|
|
193
|
-
function
|
|
629
|
+
export function inlineHtml(js) {
|
|
194
630
|
return `<!doctype html>
|
|
195
631
|
<html lang="en">
|
|
196
632
|
<head>
|
|
@@ -206,6 +642,45 @@ ${js}
|
|
|
206
642
|
</body>
|
|
207
643
|
</html>`;
|
|
208
644
|
}
|
|
645
|
+
/** Immutable: hash in the URL changes whenever the bundle changes. */
|
|
646
|
+
export const TSX_JS_CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
647
|
+
/** Tiny, code-free shell — always revalidate so a new hash is picked up. */
|
|
648
|
+
export const TSX_SHELL_CACHE_CONTROL = 'no-cache';
|
|
649
|
+
/**
|
|
650
|
+
* Resolve an HTTP request for a compiled `.tsx` view into a response.
|
|
651
|
+
*
|
|
652
|
+
* - `restPath` empty / `index.html` → the HTML shell (revalidated, ETag).
|
|
653
|
+
* - `restPath` === the hashed JS filename → the bundle (immutable).
|
|
654
|
+
* - anything else → 404 (caller may then try its own sibling resolution).
|
|
655
|
+
*
|
|
656
|
+
* Used by every browser-facing serving path (local server, Beam,
|
|
657
|
+
* streamable-http, and — via precompiled files — the Cloudflare
|
|
658
|
+
* [assets] binding) so the cache contract is identical everywhere.
|
|
659
|
+
*/
|
|
660
|
+
export function tsxHttpResponse(result, restPath) {
|
|
661
|
+
const rest = restPath.replace(/^\/+/, '');
|
|
662
|
+
if (rest === '' || rest === 'index.html') {
|
|
663
|
+
const headers = {
|
|
664
|
+
'Content-Type': 'text/html',
|
|
665
|
+
'Cache-Control': TSX_SHELL_CACHE_CONTROL,
|
|
666
|
+
};
|
|
667
|
+
// No hash on a build-error page — let it always revalidate without a tag.
|
|
668
|
+
if (result.hash)
|
|
669
|
+
headers['ETag'] = `"${result.hash}"`;
|
|
670
|
+
return { status: 200, body: result.html, headers };
|
|
671
|
+
}
|
|
672
|
+
if (result.jsFileName && rest === result.jsFileName) {
|
|
673
|
+
return {
|
|
674
|
+
status: 200,
|
|
675
|
+
body: result.js,
|
|
676
|
+
headers: {
|
|
677
|
+
'Content-Type': 'text/javascript; charset=utf-8',
|
|
678
|
+
'Cache-Control': TSX_JS_CACHE_CONTROL,
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return { status: 404, body: 'Not found', headers: {} };
|
|
683
|
+
}
|
|
209
684
|
/**
|
|
210
685
|
* Detect the "esbuild binary not installed" failure shape. Fires when
|
|
211
686
|
* Bun blocked esbuild's postinstall (default for non-trusted packages),
|
|
@@ -249,31 +724,35 @@ pre { white-space: pre-wrap; word-break: break-word; font-size: 13px; line-heigh
|
|
|
249
724
|
</html>`;
|
|
250
725
|
}
|
|
251
726
|
/**
|
|
252
|
-
* Compile a TSX file into a
|
|
727
|
+
* Compile a TSX file into a hashed JS bundle plus an HTML shell.
|
|
253
728
|
*/
|
|
254
729
|
export async function compileTsx(filePath) {
|
|
255
730
|
const tsconfigPath = findTsconfig(path.dirname(filePath));
|
|
256
731
|
try {
|
|
257
732
|
const result = await esbuild.build(buildOptions(filePath, tsconfigPath));
|
|
258
733
|
const js = result.outputFiles?.[0]?.text ?? '';
|
|
259
|
-
return
|
|
734
|
+
return writeArtifactsSync(filePath, js, resolveInputs(result.metafile));
|
|
260
735
|
}
|
|
261
736
|
catch (err) {
|
|
262
|
-
return
|
|
737
|
+
return errorResult(filePath, err);
|
|
263
738
|
}
|
|
264
739
|
}
|
|
265
740
|
/**
|
|
266
|
-
* Compile with
|
|
741
|
+
* Compile with dependency-graph-aware caching. Re-transpiles when the
|
|
742
|
+
* entry file OR any imported module changes (the previous mtime-only
|
|
743
|
+
* cache silently served a stale bundle after an imported-component edit).
|
|
267
744
|
*/
|
|
268
745
|
export async function compileTsxCached(filePath) {
|
|
269
|
-
const stat = await fsAsync.stat(filePath);
|
|
270
746
|
const cached = cache.get(filePath);
|
|
271
|
-
if (cached
|
|
272
|
-
|
|
747
|
+
if (cached) {
|
|
748
|
+
const sig = inputsSignature(cached.result.inputs);
|
|
749
|
+
if (sig !== -1 && sig === cached.sig) {
|
|
750
|
+
return cached.result;
|
|
751
|
+
}
|
|
273
752
|
}
|
|
274
|
-
const
|
|
275
|
-
cache.set(filePath, {
|
|
276
|
-
return
|
|
753
|
+
const result = await compileTsx(filePath);
|
|
754
|
+
cache.set(filePath, { sig: inputsSignature(result.inputs), result });
|
|
755
|
+
return result;
|
|
277
756
|
}
|
|
278
757
|
/**
|
|
279
758
|
* Synchronous variant for the build command (uses esbuild.buildSync).
|
|
@@ -283,10 +762,10 @@ export function compileTsxSync(filePath) {
|
|
|
283
762
|
try {
|
|
284
763
|
const result = esbuild.buildSync(buildOptions(filePath, tsconfigPath));
|
|
285
764
|
const js = result.outputFiles?.[0]?.text ?? '';
|
|
286
|
-
return
|
|
765
|
+
return writeArtifactsSync(filePath, js, resolveInputs(result.metafile));
|
|
287
766
|
}
|
|
288
767
|
catch (err) {
|
|
289
|
-
return
|
|
768
|
+
return errorResult(filePath, err);
|
|
290
769
|
}
|
|
291
770
|
}
|
|
292
771
|
//# sourceMappingURL=tsx-compiler.js.map
|