@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.
Files changed (70) hide show
  1. package/dist/auto-ui/beam/routes/api-config.js +1 -1
  2. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  3. package/dist/auto-ui/beam/types.d.ts +1 -0
  4. package/dist/auto-ui/beam/types.d.ts.map +1 -1
  5. package/dist/auto-ui/beam.d.ts.map +1 -1
  6. package/dist/auto-ui/beam.js +58 -9
  7. package/dist/auto-ui/beam.js.map +1 -1
  8. package/dist/auto-ui/components/card.d.ts +1 -1
  9. package/dist/auto-ui/components/card.d.ts.map +1 -1
  10. package/dist/auto-ui/components/card.js +1 -1
  11. package/dist/auto-ui/components/card.js.map +1 -1
  12. package/dist/auto-ui/components/checklist.d.ts +1 -1
  13. package/dist/auto-ui/components/checklist.d.ts.map +1 -1
  14. package/dist/auto-ui/components/checklist.js +1 -1
  15. package/dist/auto-ui/components/checklist.js.map +1 -1
  16. package/dist/auto-ui/components/form.d.ts +1 -1
  17. package/dist/auto-ui/components/form.d.ts.map +1 -1
  18. package/dist/auto-ui/components/form.js +2 -2
  19. package/dist/auto-ui/components/form.js.map +1 -1
  20. package/dist/auto-ui/components/list.d.ts +1 -1
  21. package/dist/auto-ui/components/list.d.ts.map +1 -1
  22. package/dist/auto-ui/components/list.js +1 -1
  23. package/dist/auto-ui/components/list.js.map +1 -1
  24. package/dist/auto-ui/components/progress.d.ts +1 -1
  25. package/dist/auto-ui/components/progress.d.ts.map +1 -1
  26. package/dist/auto-ui/components/progress.js +1 -1
  27. package/dist/auto-ui/components/progress.js.map +1 -1
  28. package/dist/auto-ui/components/table.d.ts +1 -1
  29. package/dist/auto-ui/components/table.d.ts.map +1 -1
  30. package/dist/auto-ui/components/table.js +1 -1
  31. package/dist/auto-ui/components/table.js.map +1 -1
  32. package/dist/auto-ui/components/tree.d.ts +1 -1
  33. package/dist/auto-ui/components/tree.d.ts.map +1 -1
  34. package/dist/auto-ui/components/tree.js +1 -1
  35. package/dist/auto-ui/components/tree.js.map +1 -1
  36. package/dist/auto-ui/streamable-http-transport.d.ts +1 -0
  37. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  38. package/dist/auto-ui/streamable-http-transport.js +40 -5
  39. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  40. package/dist/auto-ui/ui-resolver.d.ts +12 -1
  41. package/dist/auto-ui/ui-resolver.d.ts.map +1 -1
  42. package/dist/auto-ui/ui-resolver.js +19 -3
  43. package/dist/auto-ui/ui-resolver.js.map +1 -1
  44. package/dist/cli/commands/build.d.ts.map +1 -1
  45. package/dist/cli/commands/build.js +13 -5
  46. package/dist/cli/commands/build.js.map +1 -1
  47. package/dist/cli/commands/ps.d.ts +4 -0
  48. package/dist/cli/commands/ps.d.ts.map +1 -1
  49. package/dist/cli/commands/ps.js +19 -5
  50. package/dist/cli/commands/ps.js.map +1 -1
  51. package/dist/daemon/manager.d.ts +8 -0
  52. package/dist/daemon/manager.d.ts.map +1 -1
  53. package/dist/daemon/manager.js +46 -9
  54. package/dist/daemon/manager.js.map +1 -1
  55. package/dist/deploy/cloudflare.d.ts.map +1 -1
  56. package/dist/deploy/cloudflare.js +55 -7
  57. package/dist/deploy/cloudflare.js.map +1 -1
  58. package/dist/resource-server.d.ts.map +1 -1
  59. package/dist/resource-server.js +5 -2
  60. package/dist/resource-server.js.map +1 -1
  61. package/dist/server.d.ts +1 -0
  62. package/dist/server.d.ts.map +1 -1
  63. package/dist/server.js +206 -14
  64. package/dist/server.js.map +1 -1
  65. package/dist/tsx-compiler.d.ts +65 -5
  66. package/dist/tsx-compiler.d.ts.map +1 -1
  67. package/dist/tsx-compiler.js +531 -52
  68. package/dist/tsx-compiler.js.map +1 -1
  69. package/package.json +3 -3
  70. package/templates/cloudflare/worker.ts.template +60 -0
@@ -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
- // Tiny DOM-based JSX factory. `h()` returns real DOM nodes, not virtual nodes.
18
- // Injected into every TSX build unless the user overrides via tsconfig.
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 h(type, props, ...children) {
21
- if (type === Fragment) {
22
- const frag = document.createDocumentFragment();
23
- _append(frag, children);
24
- return frag;
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
- if (typeof type === 'function') {
27
- return type(Object.assign({}, props, { children: children.length <= 1 ? children[0] : children }));
28
- }
29
- const el = document.createElement(type);
30
- if (props) {
31
- for (const [k, v] of Object.entries(props)) {
32
- if (k === 'children' || v == null || v === false) continue;
33
- if (k.startsWith('on') && typeof v === 'function') {
34
- el.addEventListener(k[2].toLowerCase() + k.slice(3), v);
35
- } else if (k === 'style' && typeof v === 'object') {
36
- Object.assign(el.style, v);
37
- } else if (k === 'className') {
38
- el.setAttribute('class', v);
39
- } else if (k === 'htmlFor') {
40
- el.setAttribute('for', v);
41
- } else if (k === 'dangerouslySetInnerHTML') {
42
- el.innerHTML = v.__html;
43
- } else if (v === true) {
44
- el.setAttribute(k, '');
45
- } else {
46
- el.setAttribute(k, String(v));
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
- export function Fragment() {}
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
- export function _append(parent, children) {
57
- for (const child of children) {
58
- if (child == null || child === false || child === true) continue;
59
- if (Array.isArray(child)) { _append(parent, child); continue; }
60
- parent.append(typeof child === 'object' ? child : String(child));
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.replaceChildren(element);
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 → { mtimeMs, html }
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
- * Wrap bundled JS in a self-contained HTML document.
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 wrapInHtml(js) {
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 self-contained HTML document.
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 wrapInHtml(js);
734
+ return writeArtifactsSync(filePath, js, resolveInputs(result.metafile));
260
735
  }
261
736
  catch (err) {
262
- return wrapError(filePath, err);
737
+ return errorResult(filePath, err);
263
738
  }
264
739
  }
265
740
  /**
266
- * Compile with mtime-based caching. Re-transpiles only when the file changes.
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 && cached.mtimeMs === stat.mtimeMs) {
272
- return cached.html;
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 html = await compileTsx(filePath);
275
- cache.set(filePath, { mtimeMs: stat.mtimeMs, html });
276
- return html;
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 wrapInHtml(js);
765
+ return writeArtifactsSync(filePath, js, resolveInputs(result.metafile));
287
766
  }
288
767
  catch (err) {
289
- return wrapError(filePath, err);
768
+ return errorResult(filePath, err);
290
769
  }
291
770
  }
292
771
  //# sourceMappingURL=tsx-compiler.js.map