foldkit 0.147.0 → 0.148.1

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 (63) hide show
  1. package/dist/buildToken.d.ts +3 -0
  2. package/dist/buildToken.d.ts.map +1 -0
  3. package/dist/buildToken.js +21 -0
  4. package/dist/controlledDomState.d.ts +25 -0
  5. package/dist/controlledDomState.d.ts.map +1 -0
  6. package/dist/controlledDomState.js +240 -0
  7. package/dist/cssStyleProperties.d.ts +6 -0
  8. package/dist/cssStyleProperties.d.ts.map +1 -0
  9. package/dist/cssStyleProperties.js +91 -0
  10. package/dist/domReflection.d.ts +73 -0
  11. package/dist/domReflection.d.ts.map +1 -0
  12. package/dist/domReflection.js +557 -0
  13. package/dist/experimental/server/host.d.ts +102 -8
  14. package/dist/experimental/server/host.d.ts.map +1 -1
  15. package/dist/experimental/server/host.js +203 -13
  16. package/dist/experimental/server/public.d.ts +2 -2
  17. package/dist/experimental/server/public.d.ts.map +1 -1
  18. package/dist/experimental/server/public.js +1 -1
  19. package/dist/experimental/server/serialize.d.ts +15 -5
  20. package/dist/experimental/server/serialize.d.ts.map +1 -1
  21. package/dist/experimental/server/serialize.js +373 -144
  22. package/dist/experimental/server/server.d.ts +55 -14
  23. package/dist/experimental/server/server.d.ts.map +1 -1
  24. package/dist/experimental/server/server.js +546 -22
  25. package/dist/experimental/server/template.d.ts +8 -0
  26. package/dist/experimental/server/template.d.ts.map +1 -1
  27. package/dist/experimental/server/template.js +437 -2
  28. package/dist/html/index.d.ts.map +1 -1
  29. package/dist/html/index.js +436 -29
  30. package/dist/hydrate.d.ts +1 -1
  31. package/dist/hydrate.d.ts.map +1 -1
  32. package/dist/hydrate.js +449 -121
  33. package/dist/hydrationMarkers.d.ts +15 -0
  34. package/dist/hydrationMarkers.d.ts.map +1 -0
  35. package/dist/hydrationMarkers.js +72 -0
  36. package/dist/nativeInnerHtml.d.ts +13 -0
  37. package/dist/nativeInnerHtml.d.ts.map +1 -0
  38. package/dist/nativeInnerHtml.js +30 -0
  39. package/dist/propertyProvenance.d.ts +33 -0
  40. package/dist/propertyProvenance.d.ts.map +1 -0
  41. package/dist/propertyProvenance.js +78 -0
  42. package/dist/propsModule.d.ts.map +1 -1
  43. package/dist/propsModule.js +149 -15
  44. package/dist/runtime/public.d.ts +1 -1
  45. package/dist/runtime/public.d.ts.map +1 -1
  46. package/dist/runtime/runtime.d.ts +33 -6
  47. package/dist/runtime/runtime.d.ts.map +1 -1
  48. package/dist/runtime/runtime.js +240 -27
  49. package/dist/snabbdom/attributes.d.ts.map +1 -1
  50. package/dist/snabbdom/attributes.js +65 -37
  51. package/dist/snabbdom/class.d.ts.map +1 -1
  52. package/dist/snabbdom/class.js +61 -5
  53. package/dist/snabbdom/style.d.ts.map +1 -1
  54. package/dist/snabbdom/style.js +57 -34
  55. package/dist/test/apps/attributes.d.ts +1 -0
  56. package/dist/test/apps/attributes.d.ts.map +1 -1
  57. package/dist/test/apps/attributes.js +8 -1
  58. package/dist/test/apps/login.js +1 -1
  59. package/dist/test/matchers.d.ts.map +1 -1
  60. package/dist/test/matchers.js +2 -1
  61. package/dist/test/scene.d.ts.map +1 -1
  62. package/dist/test/scene.js +2 -1
  63. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import { Array, Cause, Context, Duration, Effect, Exit, Fiber, Function, Layer, Match, Option, Predicate, PubSub, Record, Ref, Runtime, Scheduler, Schema, Stream, SubscriptionRef, pipe, } from 'effect';
2
+ import { HYDRATION_BUILD_ATTRIBUTE } from '../buildToken.js';
2
3
  import { __CurrentRegistry as __CurrentInterruptRegistry, __makeRegistry as __makeInterruptRegistry, } from '../command/interruptible/index.js';
3
4
  import { createDevToolsStore, } from '../devTools/store.js';
4
5
  import { startWebSocketBridge } from '../devTools/webSocketBridge.js';
@@ -195,8 +196,8 @@ export class Dispatch extends Context.Service()('@foldkit/Dispatch') {
195
196
  }
196
197
  const hydrationForRoot = (root, isFlagsRequired) => {
197
198
  const runtimeId = root.getAttribute(FOLDKIT_APP_ATTRIBUTE) ?? '';
198
- const flagsPayloads = pipe(Array.fromIterable(document.querySelectorAll(`script[${FOLDKIT_FLAGS_ATTRIBUTE}]`)), Array.filter(script => script.getAttribute(FOLDKIT_FLAGS_ATTRIBUTE) === runtimeId), Array.map(script => script.textContent ?? ''));
199
- return { root, runtimeId, flagsPayloads, isFlagsRequired };
199
+ const flagsScripts = pipe(Array.fromIterable(document.querySelectorAll(`script[${FOLDKIT_FLAGS_ATTRIBUTE}]`)), Array.filter(script => script.getAttribute(FOLDKIT_FLAGS_ATTRIBUTE) === runtimeId));
200
+ return { root, runtimeId, flagsScripts, isFlagsRequired };
200
201
  };
201
202
  // NOTE: hydration is scoped to the app's own stamped root so a server-rendered
202
203
  // app never adopts another app's DOM. A container that carries the stamp is that
@@ -212,8 +213,166 @@ const hydrationForRoot = (root, isFlagsRequired) => {
212
213
  // root took the placeholder's place and `getElementById` no longer finds it, so
213
214
  // the stamp is the only handle; more than one stamped root is then ambiguous and
214
215
  // a hard error rather than a silent wrong-DOM adoption.
216
+ // A runtime id names one application for the whole page: it pairs a root with
217
+ // its Flags payload, and it keys the Model and scroll position hot reloading
218
+ // preserves. Two roots sharing one are not two applications but one claimed
219
+ // twice, so whichever boots second would read the other's handoff and restore
220
+ // the other's Model. `injectIntoTemplate` refuses to build such a page; this is
221
+ // the check for a page assembled some other way.
222
+ //
223
+ // Distinct ids do not make two hydrated applications independent, which is not
224
+ // a supported arrangement: each page-owning application rewrites the document's
225
+ // metadata and installs document-wide navigation listeners.
226
+ const assertRuntimeIdsAreUnique = (stampedRoots) => {
227
+ const seen = new Set();
228
+ for (const root of stampedRoots) {
229
+ const runtimeId = root.getAttribute(FOLDKIT_APP_ATTRIBUTE) ?? '';
230
+ if (seen.has(runtimeId)) {
231
+ containRefusedPage(root.ownerDocument);
232
+ throw new Error(`[foldkit] Found more than one server-rendered root stamped ` +
233
+ `"${runtimeId}". A runtime id names one application for the whole ` +
234
+ 'page: it pairs a root with its Flags payload and keys the Model and ' +
235
+ 'scroll position hot reloading preserves, so two roots sharing one ' +
236
+ "would take each other's state. Render each application with its " +
237
+ 'own `runtimeId`.');
238
+ }
239
+ seen.add(runtimeId);
240
+ }
241
+ };
242
+ // The reason this page cannot be adopted by this client, or `undefined` when
243
+ // the two name the same deployment.
244
+ //
245
+ // The client's id is required and must be non-empty. An absent one would
246
+ // otherwise equal the absent marker on a page served before build ids existed,
247
+ // which reads a page from an unknown deployment as one of this build's own: the
248
+ // exact case the id exists to refuse.
249
+ const buildSkew = (root, buildId, runtimeId) => {
250
+ if (!Predicate.isString(buildId) || buildId === '') {
251
+ return new Error('[foldkit] Runtime.hydrate was given no build id. Hydration compares ' +
252
+ 'the id the server stamped on the root with this client’s own before ' +
253
+ 'it adopts any DOM, and without one a page from any deployment would ' +
254
+ 'be adopted as this one. Pass ' +
255
+ '`buildId: import.meta.env.FOLDKIT_BUILD_ID`, the same value the ' +
256
+ 'server entry passes to `renderToString`.');
257
+ }
258
+ const servedBuild = root.getAttribute(HYDRATION_BUILD_ATTRIBUTE);
259
+ if (servedBuild === buildId) {
260
+ return undefined;
261
+ }
262
+ return new Error(`[foldkit] Runtime.hydrate found application "${runtimeId}" served by ` +
263
+ `${servedBuild === null ? 'no known deployment' : `deployment "${servedBuild}"`}` +
264
+ `, but this client belongs to deployment "${buildId}". Startup stops ` +
265
+ 'here rather than reading a handoff written by other code: the Flags in ' +
266
+ 'the page are that deployment’s, and this build could accept them while ' +
267
+ 'every value in them means something else. Serve the page from the ' +
268
+ 'running deployment, and keep stale HTML out of shared caches.');
269
+ };
270
+ const REFUSED_ATTRIBUTE = 'data-foldkit-refused';
271
+ const REFUSAL_SHIELD_ATTRIBUTE = 'data-foldkit-refusal-shield';
272
+ const refusalShields = new WeakMap();
273
+ const REFUSAL_SHIELD_INPUT_EVENTS = [
274
+ 'auxclick',
275
+ 'click',
276
+ 'contextmenu',
277
+ 'dblclick',
278
+ 'keydown',
279
+ 'keypress',
280
+ 'keyup',
281
+ 'mousedown',
282
+ 'mouseup',
283
+ 'pointerdown',
284
+ 'pointerup',
285
+ 'touchend',
286
+ 'touchstart',
287
+ ];
288
+ const REFUSAL_DOCUMENT_INPUT_EVENTS = ['keydown', 'keypress', 'keyup'];
289
+ const preventRefusalShieldInteraction = (event) => {
290
+ event.preventDefault();
291
+ event.stopImmediatePropagation();
292
+ };
293
+ const openRefusalShield = (shield) => {
294
+ shield.inert = true;
295
+ shield.setAttribute('inert', '');
296
+ if (shield.open && typeof shield.close === 'function') {
297
+ shield.close();
298
+ }
299
+ if (typeof shield.showModal === 'function') {
300
+ shield.showModal();
301
+ }
302
+ else {
303
+ shield.setAttribute('open', '');
304
+ }
305
+ shield.inert = false;
306
+ shield.removeAttribute('inert');
307
+ shield.focus({ preventScroll: true });
308
+ };
309
+ const installRefusalShield = (ownerDocument) => {
310
+ const existing = refusalShields.get(ownerDocument);
311
+ if (existing !== undefined && existing.isConnected) {
312
+ openRefusalShield(existing);
313
+ return;
314
+ }
315
+ const shield = ownerDocument.createElement('dialog');
316
+ shield.setAttribute(REFUSAL_SHIELD_ATTRIBUTE, '');
317
+ shield.setAttribute('aria-label', 'Page unavailable');
318
+ shield.setAttribute('aria-modal', 'true');
319
+ shield.setAttribute('closedby', 'none');
320
+ shield.tabIndex = -1;
321
+ shield.textContent =
322
+ 'This page could not start safely. Reload to get the current version.';
323
+ shield.style.alignItems = 'center';
324
+ shield.style.background = 'rgba(15, 23, 42, 0.96)';
325
+ shield.style.border = '0';
326
+ shield.style.boxSizing = 'border-box';
327
+ shield.style.color = 'white';
328
+ shield.style.font = '600 1rem/1.5 system-ui, sans-serif';
329
+ shield.style.display = 'grid';
330
+ shield.style.height = '100vh';
331
+ shield.style.inset = '0';
332
+ shield.style.margin = '0';
333
+ shield.style.maxHeight = 'none';
334
+ shield.style.maxWidth = 'none';
335
+ shield.style.overflow = 'hidden';
336
+ shield.style.padding = '2rem';
337
+ shield.style.position = 'fixed';
338
+ shield.style.touchAction = 'none';
339
+ shield.style.userSelect = 'none';
340
+ shield.style.width = '100vw';
341
+ shield.addEventListener('cancel', preventRefusalShieldInteraction);
342
+ for (const eventName of REFUSAL_SHIELD_INPUT_EVENTS) {
343
+ shield.addEventListener(eventName, preventRefusalShieldInteraction, {
344
+ capture: true,
345
+ passive: false,
346
+ });
347
+ }
348
+ for (const eventName of REFUSAL_DOCUMENT_INPUT_EVENTS) {
349
+ ownerDocument.addEventListener(eventName, preventRefusalShieldInteraction, {
350
+ capture: true,
351
+ passive: false,
352
+ });
353
+ }
354
+ ownerDocument.documentElement.appendChild(shield);
355
+ refusalShields.set(ownerDocument, shield);
356
+ openRefusalShield(shield);
357
+ };
358
+ const containRefusedPage = (ownerDocument) => {
359
+ const boundary = ownerDocument.body ?? ownerDocument.documentElement;
360
+ boundary.inert = true;
361
+ boundary.setAttribute('inert', '');
362
+ boundary.setAttribute('aria-hidden', 'true');
363
+ boundary.setAttribute(REFUSED_ATTRIBUTE, '');
364
+ installRefusalShield(ownerDocument);
365
+ };
366
+ // Whether this page carries anything a Foldkit server render leaves behind. A
367
+ // resolution failure on such a page is a refused handoff, and the markup is
368
+ // contained; the same failure on a page with none of these markers is a client
369
+ // application whose container never existed, where there is no server render to
370
+ // refuse and nothing to take out of reach.
371
+ const hasServerRenderedMarkup = (ownerDocument) => ownerDocument.querySelector(`[${FOLDKIT_APP_ATTRIBUTE}], [${HYDRATION_BUILD_ATTRIBUTE}], ` +
372
+ `[${FOLDKIT_FLAGS_ATTRIBUTE}]`) !== null;
215
373
  const findDocumentHydration = (container, isFlagsRequired) => {
216
374
  const stampedRoots = Array.fromIterable(document.querySelectorAll(`[${FOLDKIT_APP_ATTRIBUTE}]`));
375
+ assertRuntimeIdsAreUnique(stampedRoots);
217
376
  if (container !== null) {
218
377
  if (container.hasAttribute(FOLDKIT_APP_ATTRIBUTE)) {
219
378
  return hydrationForRoot(container, isFlagsRequired);
@@ -230,6 +389,7 @@ const findDocumentHydration = (container, isFlagsRequired) => {
230
389
  });
231
390
  }
232
391
  if (stampedRoots.length > 1) {
392
+ containRefusedPage(document);
233
393
  throw new Error('[foldkit] Found multiple server-rendered roots stamped with ' +
234
394
  `\`${FOLDKIT_APP_ATTRIBUTE}\` but no container to disambiguate them. ` +
235
395
  'Give each app its own container element so the runtime can tell ' +
@@ -404,7 +564,7 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
404
564
  const runtimeId = hydration !== undefined && hydration.runtimeId !== ''
405
565
  ? hydration.runtimeId
406
566
  : (container?.id ?? '');
407
- const startWith = (maybeConnector, hmrModel, bootMode = 'Fresh', bootFlags) => {
567
+ const startWith = (maybeConnector, hmrModel, bootMode = 'Fresh', bootFlags, buildId) => {
408
568
  // NOTE: one notifier per runtime, provided across the whole runtime
409
569
  // Effect so Commands, Subscriptions, and Mount-forked Effects all resolve
410
570
  // the same signal. A commit in one embedded application must never wake a
@@ -532,25 +692,54 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
532
692
  Effect.succeed(undefined),
533
693
  onSome: provideResources,
534
694
  });
535
- const decodeFlagsPayload = (payload, runtimeId) => Effect.try({
695
+ // Every hydration refusal that knows which root it was going to adopt
696
+ // contains that root first. The build id is one reason to refuse; a
697
+ // missing, duplicated, malformed, or Schema-incompatible Flags payload
698
+ // is another, and the page left behind is just as live in each case.
699
+ const refuseHydration = (root, message, cause) => {
700
+ containRefusedPage(root.ownerDocument);
701
+ return Effect.die(cause === undefined
702
+ ? new Error(message)
703
+ : new Error(message, { cause }));
704
+ };
705
+ const decodeFlagsPayload = (payload, runtimeId, root) => Effect.try({
536
706
  try: () => {
537
707
  const parsedPayload = JSON.parse(payload);
538
708
  return pipe(
539
709
  /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
540
710
  Schema.toCodecJson(FlagsCodec), Schema.decodeUnknownSync, decode => decode(parsedPayload));
541
711
  },
542
- catch: cause => new Error('[foldkit] Runtime.hydrate could not decode the server ' +
543
- `Flags payload for application "${runtimeId}". The HTML ` +
544
- 'and client bundle must use the same Flags Schema.', { cause }),
545
- }).pipe(Effect.orDie);
712
+ catch: cause => cause,
713
+ }).pipe(Effect.catch(cause => refuseHydration(root, '[foldkit] Runtime.hydrate could not decode the server ' +
714
+ `Flags payload for application "${runtimeId}". The HTML ` +
715
+ 'and client bundle must use the same Flags Schema.', cause)));
546
716
  const maybeRequestedHydration = bootMode === 'Hydrate'
547
717
  ? Option.fromNullishOr(hydration)
548
718
  : Option.none();
549
719
  if (bootMode === 'Hydrate' && Option.isNone(maybeRequestedHydration)) {
720
+ // A hydrating client that finds no stamped root will not adopt
721
+ // whatever the page holds, so the page is contained whether or not the
722
+ // caller named a container.
723
+ containRefusedPage(container === null ? document : container.ownerDocument);
550
724
  return yield* Effect.die(new Error('[foldkit] Runtime.hydrate could not find a server-rendered ' +
551
725
  `root stamped with \`${FOLDKIT_APP_ATTRIBUTE}\`. Use ` +
552
726
  'Runtime.run for a fresh client boot.'));
553
727
  }
728
+ // The build the served page came from is settled here, before the Flags
729
+ // payload text is accessed, parsed, or decoded, before `init` runs, and
730
+ // therefore before any Command, Subscription, ManagedResource, or port
731
+ // this boot would start. A page from another deployment carries that
732
+ // deployment's Flags, which the current Schema may well accept while
733
+ // every value in them means something else, so deferring the comparison
734
+ // to the DOM patch lets stale data reach new code that already acted on
735
+ // it.
736
+ if (Option.isSome(maybeRequestedHydration)) {
737
+ const skew = buildSkew(maybeRequestedHydration.value.root, buildId, maybeRequestedHydration.value.runtimeId);
738
+ if (skew !== undefined) {
739
+ containRefusedPage(maybeRequestedHydration.value.root.ownerDocument);
740
+ return yield* Effect.die(skew);
741
+ }
742
+ }
554
743
  // NOTE: an HMR-restored Model wins over DOM adoption because the
555
744
  // server DOM reflects older code. The hydration handoff is still
556
745
  // required, but the restored Model gets a fresh patch against its
@@ -561,15 +750,15 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
561
750
  const maybeHydrationFlags = yield* Option.match(maybeRequestedHydration, {
562
751
  onNone: () => Effect.succeed(Option.none()),
563
752
  onSome: requestedHydration => Effect.map(requestedHydration.isFlagsRequired
564
- ? Array.match(requestedHydration.flagsPayloads, {
565
- onEmpty: () => Effect.die(new Error('[foldkit] Runtime.hydrate found application ' +
753
+ ? Array.match(requestedHydration.flagsScripts, {
754
+ onEmpty: () => refuseHydration(requestedHydration.root, '[foldkit] Runtime.hydrate found application ' +
566
755
  `"${requestedHydration.runtimeId}" but its ` +
567
- 'server Flags payload is missing.')),
568
- onNonEmpty: ([payload, ...remainingPayloads]) => Array.isArrayNonEmpty(remainingPayloads)
569
- ? Effect.die(new Error('[foldkit] Runtime.hydrate found multiple ' +
756
+ 'server Flags payload is missing.'),
757
+ onNonEmpty: ([payloadScript, ...remainingScripts]) => Array.isArrayNonEmpty(remainingScripts)
758
+ ? refuseHydration(requestedHydration.root, '[foldkit] Runtime.hydrate found multiple ' +
570
759
  'server Flags payloads for application ' +
571
- `"${requestedHydration.runtimeId}".`))
572
- : decodeFlagsPayload(payload, requestedHydration.runtimeId),
760
+ `"${requestedHydration.runtimeId}".`)
761
+ : decodeFlagsPayload(payloadScript.textContent ?? '', requestedHydration.runtimeId, requestedHydration.root),
573
762
  })
574
763
  : /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
575
764
  Effect.succeed(undefined), Option.some),
@@ -1061,7 +1250,11 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
1061
1250
  // container (a dispose-then-embed remount) from re-detecting
1062
1251
  // this now-consumed root as hydratable.
1063
1252
  hydrationRoot.removeAttribute(FOLDKIT_APP_ATTRIBUTE);
1064
- return __hydrateVNode(hydrationRoot, nextVNode, boundaryRegistry.dedupeSeen);
1253
+ // An empty id reaches the adoption step's own check as a
1254
+ // value that matches nothing. Boot already refused a
1255
+ // hydration without an id, so this stands in only for a
1256
+ // caller that reached here another way.
1257
+ return __hydrateVNode(hydrationRoot, nextVNode, boundaryRegistry.dedupeSeen, buildId ?? '');
1065
1258
  }
1066
1259
  return __patchVNode(maybeCurrentVNode, nextVNode, container, boundaryRegistry.dedupeSeen);
1067
1260
  }));
@@ -1502,7 +1695,7 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
1502
1695
  ports,
1503
1696
  };
1504
1697
  runtimeInternals.set(program, {
1505
- startWith: (maybeConnector, hmrModel, bootMode, flags) => startWith(maybeConnector, hmrModel, bootMode, flags),
1698
+ startWith: (maybeConnector, hmrModel, bootMode, flags, buildId) => startWith(maybeConnector, hmrModel, bootMode, flags, buildId),
1506
1699
  kind,
1507
1700
  isEmbedActive: false,
1508
1701
  maybeActiveFiber: Option.none(),
@@ -1631,6 +1824,14 @@ export function makeApplication(config) {
1631
1824
  const hydration = findDocumentHydration(container, hasFlags);
1632
1825
  const resolvedContainer = hydration?.root ?? container;
1633
1826
  if (resolvedContainer === null) {
1827
+ // A server-rendered page whose root lost its stamp reaches exactly here:
1828
+ // template injection put the render where the placeholder was, so
1829
+ // `getElementById` finds nothing and the stamp that would have named the
1830
+ // root is gone. There is no handoff to refuse further along, and the markup
1831
+ // is as live as any other refused page, so it is contained here.
1832
+ if (hasServerRenderedMarkup(document)) {
1833
+ containRefusedPage(document);
1834
+ }
1634
1835
  throw new Error('[foldkit] Container is null. Make sure the element exists in the DOM ' +
1635
1836
  'before calling makeApplication (e.g. that your <div id="root"></div> has ' +
1636
1837
  'rendered, and your script runs after it). On a server-rendered page ' +
@@ -1853,7 +2054,7 @@ const resolveHmrModel = (runtimeId) => {
1853
2054
  };
1854
2055
  /** Starts a program Effect with explicit boot inputs for runtime tests.
1855
2056
  * @internal */
1856
- export const __startProgram = (program, hmrModel, bootMode, flags) => {
2057
+ export const __startProgram = (program, hmrModel, bootMode, flags, buildId) => {
1857
2058
  const internals = runtimeInternals.get(program);
1858
2059
  if (Predicate.isUndefined(internals)) {
1859
2060
  return Effect.die(new Error('[foldkit] Runtime boot expects a program created by ' +
@@ -1863,7 +2064,7 @@ export const __startProgram = (program, hmrModel, bootMode, flags) => {
1863
2064
  return Effect.die(new Error('[foldkit] Runtime.hydrate expects a program created by ' +
1864
2065
  'makeApplication.'));
1865
2066
  }
1866
- return internals.startWith(Option.none(), hmrModel, bootMode, flags);
2067
+ return internals.startWith(Option.none(), hmrModel, bootMode, flags, buildId);
1867
2068
  };
1868
2069
  // NOTE: deliberately not `BrowserRuntime.runMain`, which interrupts the
1869
2070
  // runtime on `beforeunload`. `beforeunload` is a question, not a commitment:
@@ -1876,8 +2077,8 @@ export const __startProgram = (program, hmrModel, bootMode, flags) => {
1876
2077
  // interrupt at all and lets the document take the runtime with it. Error
1877
2078
  // reporting and the keep-alive interval come from `makeRunMain` either way.
1878
2079
  const runMainWithoutUnloadInterrupt = Runtime.makeRunMain(Function.constVoid);
1879
- const startProgram = (program, bootMode, flags) => {
1880
- runMainWithoutUnloadInterrupt(provideBrowserScheduler(Effect.flatMap(resolveHmrModel(program.runtimeId), hmrModel => __startProgram(program, hmrModel, bootMode, flags))));
2080
+ const startProgram = (program, bootMode, flags, buildId) => {
2081
+ runMainWithoutUnloadInterrupt(provideBrowserScheduler(Effect.flatMap(resolveHmrModel(program.runtimeId), hmrModel => __startProgram(program, hmrModel, bootMode, flags, buildId))));
1881
2082
  };
1882
2083
  export function run(program, options) {
1883
2084
  startProgram(program, 'Fresh', options?.flags);
@@ -1886,15 +2087,27 @@ export function run(program, options) {
1886
2087
  * of building it fresh. Use this as the client entry for a page served by
1887
2088
  * `renderToString`: the first render attaches to the stamped root, keeps the
1888
2089
  * existing nodes, and reconstructs the Model from the Flags the server
1889
- * embedded. The handoff is strict: a missing server root, missing Flags
1890
- * payload, or undecodable payload terminates startup and leaves the server
1891
- * HTML visible but inert. Use `run` in a separate client-only entry when the
1892
- * page should boot without server output.
2090
+ * embedded. The handoff is strict: a missing server root, a root stamped more
2091
+ * than once, more than one root with no container to choose between them, a
2092
+ * missing Flags payload, an undecodable payload, or a page from another
2093
+ * deployment terminates startup. Every one of those contains the page first:
2094
+ * the document's body is marked `inert` and a nondismissable modal shield is
2095
+ * opened above existing top-layer content, so pointer and physical keyboard
2096
+ * input do not activate same-document native links, forms, or controls.
2097
+ * Containment leaves author-owned dialogs open without calling `close` or
2098
+ * dispatching `cancel`.
2099
+ * Nothing is moved, so no custom element reconnects and no frame reloads.
2100
+ * This is not a script, event, or embedded-document sandbox: existing
2101
+ * capture-phase handlers, browser-generated top-layer events, timers, and
2102
+ * stale scripts can still run. Controls in embedded documents can still
2103
+ * receive input if stale code focuses them. Stale code can also open newer
2104
+ * top-layer UI. Use `run` in a separate client-only entry when the page should
2105
+ * boot without server output.
1893
2106
  *
1894
2107
  * @experimental Server rendering and hydration are experimental while their
1895
2108
  * contracts settle. */
1896
- export const hydrate = (program) => {
1897
- startProgram(program, 'Hydrate');
2109
+ export const hydrate = (program, options) => {
2110
+ startProgram(program, 'Hydrate', undefined, options?.buildId);
1898
2111
  };
1899
2112
  const buildPortHandles = (ports, connector) => {
1900
2113
  const handles = {};
@@ -1 +1 @@
1
- {"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../src/snabbdom/attributes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;AAwD7D,eAAO,MAAM,gBAAgB,EAAE,MAI9B,CAAA"}
1
+ {"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../src/snabbdom/attributes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;AAkG7D,eAAO,MAAM,gBAAgB,EAAE,MAI9B,CAAA"}
@@ -1,12 +1,65 @@
1
+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
2
+ import { parsedAttributeName } from '../domReflection.js';
1
3
  import { VNodeDataMask } from './vnode.js';
2
4
  const xlinkNS = 'http://www.w3.org/1999/xlink';
3
5
  const xmlnsNS = 'http://www.w3.org/2000/xmlns/';
4
6
  const xmlNS = 'http://www.w3.org/XML/1998/namespace';
5
- const colonChar = 58;
6
- const xChar = 120;
7
- const mChar = 109;
7
+ const htmlNS = 'http://www.w3.org/1999/xhtml';
8
+ const FOREIGN_ATTRIBUTE_NAMESPACES = {
9
+ 'xlink:actuate': xlinkNS,
10
+ 'xlink:arcrole': xlinkNS,
11
+ 'xlink:href': xlinkNS,
12
+ 'xlink:role': xlinkNS,
13
+ 'xlink:show': xlinkNS,
14
+ 'xlink:title': xlinkNS,
15
+ 'xlink:type': xlinkNS,
16
+ 'xml:base': xmlNS,
17
+ 'xml:lang': xmlNS,
18
+ 'xml:space': xmlNS,
19
+ xmlns: xmlnsNS,
20
+ 'xmlns:xlink': xmlnsNS,
21
+ };
22
+ const namespaceFor = (elementNamespace, name) => {
23
+ if (elementNamespace === null || elementNamespace === htmlNS) {
24
+ return undefined;
25
+ }
26
+ return FOREIGN_ATTRIBUTE_NAMESPACES[name];
27
+ };
28
+ const normalizedAttributes = (attrs, namespace) => {
29
+ const normalized = new Map();
30
+ for (const name of Object.keys(attrs)) {
31
+ normalized.set(parsedAttributeName(namespace, name), attrs[name]);
32
+ }
33
+ return normalized;
34
+ };
35
+ const setAttribute = (element, name, value) => {
36
+ const namespace = namespaceFor(element.namespaceURI, name);
37
+ if (value === true) {
38
+ if (namespace === undefined) {
39
+ element.setAttribute(name, '');
40
+ }
41
+ else {
42
+ element.setAttributeNS(namespace, name, '');
43
+ }
44
+ }
45
+ else if (value === false) {
46
+ if (namespace === undefined) {
47
+ element.removeAttribute(name);
48
+ }
49
+ else {
50
+ const separator = name.indexOf(':');
51
+ const localName = separator === -1 ? name : name.slice(separator + 1);
52
+ element.removeAttributeNS(namespace, localName);
53
+ }
54
+ }
55
+ else if (namespace === undefined) {
56
+ element.setAttribute(name, value);
57
+ }
58
+ else {
59
+ element.setAttributeNS(namespace, name, value);
60
+ }
61
+ };
8
62
  function updateAttrs(oldVnode, vnode) {
9
- let key;
10
63
  const elm = vnode.elm;
11
64
  let oldAttrs = oldVnode.data.attrs;
12
65
  let attrs = vnode.data.attrs;
@@ -16,43 +69,18 @@ function updateAttrs(oldVnode, vnode) {
16
69
  return;
17
70
  oldAttrs = oldAttrs || {};
18
71
  attrs = attrs || {};
72
+ const normalizedOld = normalizedAttributes(oldAttrs, elm.namespaceURI);
73
+ const normalizedNext = normalizedAttributes(attrs, elm.namespaceURI);
19
74
  // update modified attributes, add new attributes
20
- for (key in attrs) {
21
- const cur = attrs[key];
22
- const old = oldAttrs[key];
75
+ for (const [name, cur] of normalizedNext) {
76
+ const old = normalizedOld.get(name);
23
77
  if (old !== cur) {
24
- if (cur === true) {
25
- elm.setAttribute(key, '');
26
- }
27
- else if (cur === false) {
28
- elm.removeAttribute(key);
29
- }
30
- else {
31
- if (key.charCodeAt(0) !== xChar) {
32
- elm.setAttribute(key, cur);
33
- }
34
- else if (key.charCodeAt(3) === colonChar) {
35
- // Assume xml namespace
36
- elm.setAttributeNS(xmlNS, key, cur);
37
- }
38
- else if (key.charCodeAt(5) === colonChar) {
39
- // Assume 'xmlns' or 'xlink' namespace
40
- key.charCodeAt(1) === mChar
41
- ? elm.setAttributeNS(xmlnsNS, key, cur)
42
- : elm.setAttributeNS(xlinkNS, key, cur);
43
- }
44
- else {
45
- elm.setAttribute(key, cur);
46
- }
47
- }
78
+ setAttribute(elm, name, cur);
48
79
  }
49
80
  }
50
- // remove removed attributes
51
- // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
52
- // the other option is to remove all attributes with value == undefined
53
- for (key in oldAttrs) {
54
- if (!(key in attrs)) {
55
- elm.removeAttribute(key);
81
+ for (const name of normalizedOld.keys()) {
82
+ if (!normalizedNext.has(name)) {
83
+ setAttribute(elm, name, false);
56
84
  }
57
85
  }
58
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"class.d.ts","sourceRoot":"","sources":["../../src/snabbdom/class.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AA4B7C,eAAO,MAAM,WAAW,EAAE,MAIzB,CAAA"}
1
+ {"version":3,"file":"class.d.ts","sourceRoot":"","sources":["../../src/snabbdom/class.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAwG7C,eAAO,MAAM,WAAW,EAAE,MAIzB,CAAA"}
@@ -1,26 +1,82 @@
1
+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
2
+ import { parsedAttributeName } from '../domReflection.js';
1
3
  import { VNodeDataMask } from './vnode.js';
4
+ const absentRawClassAttribute = { _tag: 'Absent' };
5
+ const ASCII_WHITESPACE = /[\t\n\f\r ]+/;
6
+ const rawClassAttributeFor = (vnode, namespace) => {
7
+ const attrs = vnode.data.attrs;
8
+ if (attrs === undefined) {
9
+ return absentRawClassAttribute;
10
+ }
11
+ let attribute = absentRawClassAttribute;
12
+ for (const name of Object.keys(attrs)) {
13
+ if (parsedAttributeName(namespace, name) === 'class') {
14
+ attribute = { _tag: 'Present', value: attrs[name] };
15
+ }
16
+ }
17
+ return attribute;
18
+ };
19
+ const rawClassAttributesAgree = (previous, next) => {
20
+ if (previous._tag === 'Absent') {
21
+ return next._tag === 'Absent';
22
+ }
23
+ return next._tag === 'Present' && previous.value === next.value;
24
+ };
25
+ const rawClassNamesFor = (attribute) => {
26
+ const names = new Set();
27
+ if (attribute._tag === 'Absent' || typeof attribute.value === 'boolean') {
28
+ return names;
29
+ }
30
+ for (const name of String(attribute.value).split(ASCII_WHITESPACE)) {
31
+ if (name !== '') {
32
+ names.add(name);
33
+ }
34
+ }
35
+ return names;
36
+ };
2
37
  function updateClass(oldVnode, vnode) {
3
38
  let cur;
4
39
  let name;
5
40
  const elm = vnode.elm;
6
41
  let oldClass = oldVnode.data.class;
7
42
  let klass = vnode.data.class;
43
+ const oldAttrs = oldVnode.data.attrs;
44
+ const attrs = vnode.data.attrs;
8
45
  if (!oldClass && !klass)
9
46
  return;
10
- if (oldClass === klass)
47
+ if (oldClass === klass && oldAttrs === attrs)
48
+ return;
49
+ const previousRawClass = rawClassAttributeFor(oldVnode, elm.namespaceURI);
50
+ const nextRawClass = rawClassAttributeFor(vnode, elm.namespaceURI);
51
+ if (oldClass === klass &&
52
+ rawClassAttributesAgree(previousRawClass, nextRawClass)) {
11
53
  return;
54
+ }
12
55
  oldClass = oldClass || {};
13
56
  klass = klass || {};
57
+ if (oldClass === klass) {
58
+ for (name in klass) {
59
+ if (klass[name] && !elm.classList.contains(name)) {
60
+ elm.classList.add(name);
61
+ }
62
+ }
63
+ return;
64
+ }
65
+ const nextRawClassNames = rawClassNamesFor(nextRawClass);
14
66
  for (name in oldClass) {
15
- if (oldClass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) {
16
- // was `true` and now not provided
67
+ if (oldClass[name] &&
68
+ klass[name] !== true &&
69
+ !nextRawClassNames.has(name)) {
17
70
  elm.classList.remove(name);
18
71
  }
19
72
  }
20
73
  for (name in klass) {
21
74
  cur = klass[name];
22
- if (cur !== oldClass[name]) {
23
- elm.classList[cur ? 'add' : 'remove'](name);
75
+ if (cur && !elm.classList.contains(name)) {
76
+ elm.classList.add(name);
77
+ }
78
+ else if (cur !== oldClass[name] && !nextRawClassNames.has(name)) {
79
+ elm.classList.remove(name);
24
80
  }
25
81
  }
26
82
  }
@@ -1 +1 @@
1
- {"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../../src/snabbdom/style.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;AAEvD,MAAM,MAAM,UAAU,GAAG,YAAY,GACnC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;IACvB,OAAO,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/C,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9C,OAAO,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChD,CAAA;AAgHH,eAAO,MAAM,WAAW,EAAE,MAOzB,CAAA"}
1
+ {"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../../src/snabbdom/style.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;AAEvD,MAAM,MAAM,UAAU,GAAG,YAAY,GACnC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;IACvB,OAAO,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/C,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9C,OAAO,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChD,CAAA;AA+JH,eAAO,MAAM,WAAW,EAAE,MAOzB,CAAA"}