foldkit 0.147.0 → 0.148.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 (61) 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 +367 -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 +544 -21
  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 +70 -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 +30 -6
  47. package/dist/runtime/runtime.d.ts.map +1 -1
  48. package/dist/runtime/runtime.js +230 -27
  49. package/dist/snabbdom/attributes.d.ts.map +1 -1
  50. package/dist/snabbdom/attributes.js +65 -37
  51. package/dist/snabbdom/style.d.ts.map +1 -1
  52. package/dist/snabbdom/style.js +53 -34
  53. package/dist/test/apps/attributes.d.ts +1 -0
  54. package/dist/test/apps/attributes.d.ts.map +1 -1
  55. package/dist/test/apps/attributes.js +8 -1
  56. package/dist/test/apps/login.js +1 -1
  57. package/dist/test/matchers.d.ts.map +1 -1
  58. package/dist/test/matchers.js +2 -1
  59. package/dist/test/scene.d.ts.map +1 -1
  60. package/dist/test/scene.js +2 -1
  61. 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,159 @@ 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 (buildId === undefined || 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 preventRefusalShieldInteraction = (event) => {
289
+ event.preventDefault();
290
+ event.stopImmediatePropagation();
291
+ };
292
+ const openRefusalShield = (shield) => {
293
+ shield.inert = true;
294
+ shield.setAttribute('inert', '');
295
+ if (shield.open && typeof shield.close === 'function') {
296
+ shield.close();
297
+ }
298
+ if (typeof shield.showModal === 'function') {
299
+ shield.showModal();
300
+ }
301
+ else {
302
+ shield.setAttribute('open', '');
303
+ }
304
+ shield.inert = false;
305
+ shield.removeAttribute('inert');
306
+ shield.focus({ preventScroll: true });
307
+ };
308
+ const installRefusalShield = (ownerDocument) => {
309
+ const existing = refusalShields.get(ownerDocument);
310
+ if (existing !== undefined && existing.isConnected) {
311
+ openRefusalShield(existing);
312
+ return;
313
+ }
314
+ const shield = ownerDocument.createElement('dialog');
315
+ shield.setAttribute(REFUSAL_SHIELD_ATTRIBUTE, '');
316
+ shield.setAttribute('aria-label', 'Page unavailable');
317
+ shield.setAttribute('aria-modal', 'true');
318
+ shield.setAttribute('closedby', 'none');
319
+ shield.tabIndex = -1;
320
+ shield.textContent =
321
+ 'This page could not start safely. Reload to get the current version.';
322
+ shield.style.alignItems = 'center';
323
+ shield.style.background = 'rgba(15, 23, 42, 0.96)';
324
+ shield.style.border = '0';
325
+ shield.style.boxSizing = 'border-box';
326
+ shield.style.color = 'white';
327
+ shield.style.font = '600 1rem/1.5 system-ui, sans-serif';
328
+ shield.style.display = 'grid';
329
+ shield.style.height = '100vh';
330
+ shield.style.inset = '0';
331
+ shield.style.margin = '0';
332
+ shield.style.maxHeight = 'none';
333
+ shield.style.maxWidth = 'none';
334
+ shield.style.overflow = 'hidden';
335
+ shield.style.padding = '2rem';
336
+ shield.style.position = 'fixed';
337
+ shield.style.touchAction = 'none';
338
+ shield.style.userSelect = 'none';
339
+ shield.style.width = '100vw';
340
+ shield.addEventListener('cancel', preventRefusalShieldInteraction);
341
+ for (const eventName of REFUSAL_SHIELD_INPUT_EVENTS) {
342
+ shield.addEventListener(eventName, preventRefusalShieldInteraction, {
343
+ capture: true,
344
+ passive: false,
345
+ });
346
+ }
347
+ ownerDocument.documentElement.appendChild(shield);
348
+ refusalShields.set(ownerDocument, shield);
349
+ openRefusalShield(shield);
350
+ };
351
+ const containRefusedPage = (ownerDocument) => {
352
+ const boundary = ownerDocument.body ?? ownerDocument.documentElement;
353
+ boundary.inert = true;
354
+ boundary.setAttribute('inert', '');
355
+ boundary.setAttribute('aria-hidden', 'true');
356
+ boundary.setAttribute(REFUSED_ATTRIBUTE, '');
357
+ installRefusalShield(ownerDocument);
358
+ };
359
+ // Whether this page carries anything a Foldkit server render leaves behind. A
360
+ // resolution failure on such a page is a refused handoff, and the markup is
361
+ // contained; the same failure on a page with none of these markers is a client
362
+ // application whose container never existed, where there is no server render to
363
+ // refuse and nothing to take out of reach.
364
+ const hasServerRenderedMarkup = (ownerDocument) => ownerDocument.querySelector(`[${FOLDKIT_APP_ATTRIBUTE}], [${HYDRATION_BUILD_ATTRIBUTE}], ` +
365
+ `[${FOLDKIT_FLAGS_ATTRIBUTE}]`) !== null;
215
366
  const findDocumentHydration = (container, isFlagsRequired) => {
216
367
  const stampedRoots = Array.fromIterable(document.querySelectorAll(`[${FOLDKIT_APP_ATTRIBUTE}]`));
368
+ assertRuntimeIdsAreUnique(stampedRoots);
217
369
  if (container !== null) {
218
370
  if (container.hasAttribute(FOLDKIT_APP_ATTRIBUTE)) {
219
371
  return hydrationForRoot(container, isFlagsRequired);
@@ -230,6 +382,7 @@ const findDocumentHydration = (container, isFlagsRequired) => {
230
382
  });
231
383
  }
232
384
  if (stampedRoots.length > 1) {
385
+ containRefusedPage(document);
233
386
  throw new Error('[foldkit] Found multiple server-rendered roots stamped with ' +
234
387
  `\`${FOLDKIT_APP_ATTRIBUTE}\` but no container to disambiguate them. ` +
235
388
  'Give each app its own container element so the runtime can tell ' +
@@ -404,7 +557,7 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
404
557
  const runtimeId = hydration !== undefined && hydration.runtimeId !== ''
405
558
  ? hydration.runtimeId
406
559
  : (container?.id ?? '');
407
- const startWith = (maybeConnector, hmrModel, bootMode = 'Fresh', bootFlags) => {
560
+ const startWith = (maybeConnector, hmrModel, bootMode = 'Fresh', bootFlags, buildId) => {
408
561
  // NOTE: one notifier per runtime, provided across the whole runtime
409
562
  // Effect so Commands, Subscriptions, and Mount-forked Effects all resolve
410
563
  // the same signal. A commit in one embedded application must never wake a
@@ -532,25 +685,54 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
532
685
  Effect.succeed(undefined),
533
686
  onSome: provideResources,
534
687
  });
535
- const decodeFlagsPayload = (payload, runtimeId) => Effect.try({
688
+ // Every hydration refusal that knows which root it was going to adopt
689
+ // contains that root first. The build id is one reason to refuse; a
690
+ // missing, duplicated, malformed, or Schema-incompatible Flags payload
691
+ // is another, and the page left behind is just as live in each case.
692
+ const refuseHydration = (root, message, cause) => {
693
+ containRefusedPage(root.ownerDocument);
694
+ return Effect.die(cause === undefined
695
+ ? new Error(message)
696
+ : new Error(message, { cause }));
697
+ };
698
+ const decodeFlagsPayload = (payload, runtimeId, root) => Effect.try({
536
699
  try: () => {
537
700
  const parsedPayload = JSON.parse(payload);
538
701
  return pipe(
539
702
  /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
540
703
  Schema.toCodecJson(FlagsCodec), Schema.decodeUnknownSync, decode => decode(parsedPayload));
541
704
  },
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);
705
+ catch: cause => cause,
706
+ }).pipe(Effect.catch(cause => refuseHydration(root, '[foldkit] Runtime.hydrate could not decode the server ' +
707
+ `Flags payload for application "${runtimeId}". The HTML ` +
708
+ 'and client bundle must use the same Flags Schema.', cause)));
546
709
  const maybeRequestedHydration = bootMode === 'Hydrate'
547
710
  ? Option.fromNullishOr(hydration)
548
711
  : Option.none();
549
712
  if (bootMode === 'Hydrate' && Option.isNone(maybeRequestedHydration)) {
713
+ // A hydrating client that finds no stamped root will not adopt
714
+ // whatever the page holds, so the page is contained whether or not the
715
+ // caller named a container.
716
+ containRefusedPage(container === null ? document : container.ownerDocument);
550
717
  return yield* Effect.die(new Error('[foldkit] Runtime.hydrate could not find a server-rendered ' +
551
718
  `root stamped with \`${FOLDKIT_APP_ATTRIBUTE}\`. Use ` +
552
719
  'Runtime.run for a fresh client boot.'));
553
720
  }
721
+ // The build the served page came from is settled here, before the Flags
722
+ // payload text is accessed, parsed, or decoded, before `init` runs, and
723
+ // therefore before any Command, Subscription, ManagedResource, or port
724
+ // this boot would start. A page from another deployment carries that
725
+ // deployment's Flags, which the current Schema may well accept while
726
+ // every value in them means something else, so deferring the comparison
727
+ // to the DOM patch lets stale data reach new code that already acted on
728
+ // it.
729
+ if (Option.isSome(maybeRequestedHydration)) {
730
+ const skew = buildSkew(maybeRequestedHydration.value.root, buildId, maybeRequestedHydration.value.runtimeId);
731
+ if (skew !== undefined) {
732
+ containRefusedPage(maybeRequestedHydration.value.root.ownerDocument);
733
+ return yield* Effect.die(skew);
734
+ }
735
+ }
554
736
  // NOTE: an HMR-restored Model wins over DOM adoption because the
555
737
  // server DOM reflects older code. The hydration handoff is still
556
738
  // required, but the restored Model gets a fresh patch against its
@@ -561,15 +743,15 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
561
743
  const maybeHydrationFlags = yield* Option.match(maybeRequestedHydration, {
562
744
  onNone: () => Effect.succeed(Option.none()),
563
745
  onSome: requestedHydration => Effect.map(requestedHydration.isFlagsRequired
564
- ? Array.match(requestedHydration.flagsPayloads, {
565
- onEmpty: () => Effect.die(new Error('[foldkit] Runtime.hydrate found application ' +
746
+ ? Array.match(requestedHydration.flagsScripts, {
747
+ onEmpty: () => refuseHydration(requestedHydration.root, '[foldkit] Runtime.hydrate found application ' +
566
748
  `"${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 ' +
749
+ 'server Flags payload is missing.'),
750
+ onNonEmpty: ([payloadScript, ...remainingScripts]) => Array.isArrayNonEmpty(remainingScripts)
751
+ ? refuseHydration(requestedHydration.root, '[foldkit] Runtime.hydrate found multiple ' +
570
752
  'server Flags payloads for application ' +
571
- `"${requestedHydration.runtimeId}".`))
572
- : decodeFlagsPayload(payload, requestedHydration.runtimeId),
753
+ `"${requestedHydration.runtimeId}".`)
754
+ : decodeFlagsPayload(payloadScript.textContent ?? '', requestedHydration.runtimeId, requestedHydration.root),
573
755
  })
574
756
  : /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
575
757
  Effect.succeed(undefined), Option.some),
@@ -1061,7 +1243,11 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
1061
1243
  // container (a dispose-then-embed remount) from re-detecting
1062
1244
  // this now-consumed root as hydratable.
1063
1245
  hydrationRoot.removeAttribute(FOLDKIT_APP_ATTRIBUTE);
1064
- return __hydrateVNode(hydrationRoot, nextVNode, boundaryRegistry.dedupeSeen);
1246
+ // An empty id reaches the adoption step's own check as a
1247
+ // value that matches nothing. Boot already refused a
1248
+ // hydration without an id, so this stands in only for a
1249
+ // caller that reached here another way.
1250
+ return __hydrateVNode(hydrationRoot, nextVNode, boundaryRegistry.dedupeSeen, buildId ?? '');
1065
1251
  }
1066
1252
  return __patchVNode(maybeCurrentVNode, nextVNode, container, boundaryRegistry.dedupeSeen);
1067
1253
  }));
@@ -1502,7 +1688,7 @@ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, i
1502
1688
  ports,
1503
1689
  };
1504
1690
  runtimeInternals.set(program, {
1505
- startWith: (maybeConnector, hmrModel, bootMode, flags) => startWith(maybeConnector, hmrModel, bootMode, flags),
1691
+ startWith: (maybeConnector, hmrModel, bootMode, flags, buildId) => startWith(maybeConnector, hmrModel, bootMode, flags, buildId),
1506
1692
  kind,
1507
1693
  isEmbedActive: false,
1508
1694
  maybeActiveFiber: Option.none(),
@@ -1631,6 +1817,14 @@ export function makeApplication(config) {
1631
1817
  const hydration = findDocumentHydration(container, hasFlags);
1632
1818
  const resolvedContainer = hydration?.root ?? container;
1633
1819
  if (resolvedContainer === null) {
1820
+ // A server-rendered page whose root lost its stamp reaches exactly here:
1821
+ // template injection put the render where the placeholder was, so
1822
+ // `getElementById` finds nothing and the stamp that would have named the
1823
+ // root is gone. There is no handoff to refuse further along, and the markup
1824
+ // is as live as any other refused page, so it is contained here.
1825
+ if (hasServerRenderedMarkup(document)) {
1826
+ containRefusedPage(document);
1827
+ }
1634
1828
  throw new Error('[foldkit] Container is null. Make sure the element exists in the DOM ' +
1635
1829
  'before calling makeApplication (e.g. that your <div id="root"></div> has ' +
1636
1830
  'rendered, and your script runs after it). On a server-rendered page ' +
@@ -1853,7 +2047,7 @@ const resolveHmrModel = (runtimeId) => {
1853
2047
  };
1854
2048
  /** Starts a program Effect with explicit boot inputs for runtime tests.
1855
2049
  * @internal */
1856
- export const __startProgram = (program, hmrModel, bootMode, flags) => {
2050
+ export const __startProgram = (program, hmrModel, bootMode, flags, buildId) => {
1857
2051
  const internals = runtimeInternals.get(program);
1858
2052
  if (Predicate.isUndefined(internals)) {
1859
2053
  return Effect.die(new Error('[foldkit] Runtime boot expects a program created by ' +
@@ -1863,7 +2057,7 @@ export const __startProgram = (program, hmrModel, bootMode, flags) => {
1863
2057
  return Effect.die(new Error('[foldkit] Runtime.hydrate expects a program created by ' +
1864
2058
  'makeApplication.'));
1865
2059
  }
1866
- return internals.startWith(Option.none(), hmrModel, bootMode, flags);
2060
+ return internals.startWith(Option.none(), hmrModel, bootMode, flags, buildId);
1867
2061
  };
1868
2062
  // NOTE: deliberately not `BrowserRuntime.runMain`, which interrupts the
1869
2063
  // runtime on `beforeunload`. `beforeunload` is a question, not a commitment:
@@ -1876,8 +2070,8 @@ export const __startProgram = (program, hmrModel, bootMode, flags) => {
1876
2070
  // interrupt at all and lets the document take the runtime with it. Error
1877
2071
  // reporting and the keep-alive interval come from `makeRunMain` either way.
1878
2072
  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))));
2073
+ const startProgram = (program, bootMode, flags, buildId) => {
2074
+ runMainWithoutUnloadInterrupt(provideBrowserScheduler(Effect.flatMap(resolveHmrModel(program.runtimeId), hmrModel => __startProgram(program, hmrModel, bootMode, flags, buildId))));
1881
2075
  };
1882
2076
  export function run(program, options) {
1883
2077
  startProgram(program, 'Fresh', options?.flags);
@@ -1886,15 +2080,24 @@ export function run(program, options) {
1886
2080
  * of building it fresh. Use this as the client entry for a page served by
1887
2081
  * `renderToString`: the first render attaches to the stamped root, keeps the
1888
2082
  * 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.
2083
+ * embedded. The handoff is strict: a missing server root, a root stamped more
2084
+ * than once, more than one root with no container to choose between them, a
2085
+ * missing Flags payload, an undecodable payload, or a page from another
2086
+ * deployment terminates startup. Every one of those contains the page first:
2087
+ * the document's body is marked `inert` and a nondismissable modal shield is
2088
+ * opened above existing top-layer content, so native links, forms, controls,
2089
+ * and focus targets stop responding. Containment leaves author-owned dialogs
2090
+ * open without calling `close` or dispatching `cancel`. Nothing is moved, so
2091
+ * no custom element reconnects and no frame reloads. This is not a script or
2092
+ * event sandbox: existing capture-phase handlers, browser-generated top-layer
2093
+ * events, timers, and other stale scripts can still run and can open newer
2094
+ * top-layer UI. Use `run` in a separate client-only entry when the page should
2095
+ * boot without server output.
1893
2096
  *
1894
2097
  * @experimental Server rendering and hydration are experimental while their
1895
2098
  * contracts settle. */
1896
- export const hydrate = (program) => {
1897
- startProgram(program, 'Hydrate');
2099
+ export const hydrate = (program, options) => {
2100
+ startProgram(program, 'Hydrate', undefined, options.buildId);
1898
2101
  };
1899
2102
  const buildPortHandles = (ports, connector) => {
1900
2103
  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":"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":"AAEA,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;AAyJH,eAAO,MAAM,WAAW,EAAE,MAOzB,CAAA"}
@@ -1,3 +1,5 @@
1
+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
2
+ import { serializedStylePropertyName } from '../domReflection.js';
1
3
  import { VNodeDataMask } from './vnode.js';
2
4
  // Binding `requestAnimationFrame` like this fixes a bug in IE/Edge. See #360 and #409.
3
5
  const raf = typeof window !== 'undefined' &&
@@ -10,11 +12,35 @@ const nextFrame = function (fn) {
10
12
  });
11
13
  };
12
14
  let reflowForced = false;
13
- function setNextFrame(obj, prop, val) {
15
+ function setStyle(style, propertyName, value) {
16
+ style.setProperty(serializedStylePropertyName(propertyName), String(value));
17
+ }
18
+ function removeStyle(style, propertyName) {
19
+ style.removeProperty(serializedStylePropertyName(propertyName));
20
+ }
21
+ function setNextFrame(style, propertyName, value) {
14
22
  nextFrame(function () {
15
- obj[prop] = val;
23
+ setStyle(style, propertyName, value);
16
24
  });
17
25
  }
26
+ function createStyle(_oldVnode, vnode) {
27
+ const element = vnode.elm;
28
+ const style = vnode.data.style;
29
+ if (!style) {
30
+ return;
31
+ }
32
+ for (const name of Object.keys(style)) {
33
+ if (name !== 'delayed' && name !== 'destroy' && name !== 'remove') {
34
+ setStyle(element.style, name, style[name]);
35
+ }
36
+ }
37
+ const delayed = Object.hasOwn(style, 'delayed') ? style.delayed : undefined;
38
+ if (typeof delayed === 'object' && delayed !== null) {
39
+ for (const name of Object.keys(delayed)) {
40
+ setNextFrame(element.style, name, delayed[name]);
41
+ }
42
+ }
43
+ }
18
44
  function updateStyle(oldVnode, vnode) {
19
45
  let cur;
20
46
  let name;
@@ -27,38 +53,31 @@ function updateStyle(oldVnode, vnode) {
27
53
  return;
28
54
  oldStyle = oldStyle || {};
29
55
  style = style || {};
30
- const oldHasDel = 'delayed' in oldStyle;
31
- for (name in oldStyle) {
32
- if (!(name in style)) {
33
- if (name[0] === '-' && name[1] === '-') {
34
- ;
35
- elm.style.removeProperty(name);
36
- }
37
- else {
38
- ;
39
- elm.style[name] = '';
40
- }
56
+ const oldHasDel = Object.hasOwn(oldStyle, 'delayed');
57
+ for (name of Object.keys(oldStyle)) {
58
+ if (name !== 'delayed' &&
59
+ name !== 'destroy' &&
60
+ name !== 'remove' &&
61
+ !Object.hasOwn(style, name)) {
62
+ removeStyle(elm.style, name);
41
63
  }
42
64
  }
43
- for (name in style) {
65
+ for (name of Object.keys(style)) {
44
66
  cur = style[name];
45
- if (name === 'delayed' && style.delayed) {
46
- for (const name2 in style.delayed) {
67
+ if (name === 'delayed' &&
68
+ typeof style.delayed === 'object' &&
69
+ style.delayed !== null) {
70
+ for (const name2 of Object.keys(style.delayed)) {
47
71
  cur = style.delayed[name2];
48
72
  if (!oldHasDel || cur !== oldStyle.delayed[name2]) {
49
73
  setNextFrame(elm.style, name2, cur);
50
74
  }
51
75
  }
52
76
  }
53
- else if (name !== 'remove' && cur !== oldStyle[name]) {
54
- if (name[0] === '-' && name[1] === '-') {
55
- ;
56
- elm.style.setProperty(name, cur);
57
- }
58
- else {
59
- ;
60
- elm.style[name] = cur;
61
- }
77
+ else if (name !== 'destroy' &&
78
+ name !== 'remove' &&
79
+ cur !== oldStyle[name]) {
80
+ setStyle(elm.style, name, cur);
62
81
  }
63
82
  }
64
83
  }
@@ -67,16 +86,15 @@ function applyDestroyStyle(vnode) {
67
86
  let name;
68
87
  const elm = vnode.elm;
69
88
  const s = vnode.data.style;
70
- if (!s || !(style = s['destroy']))
89
+ if (!s || !Object.hasOwn(s, 'destroy') || !(style = s['destroy']))
71
90
  return;
72
- for (name in style) {
73
- ;
74
- elm.style[name] = style[name];
91
+ for (name of Object.keys(style)) {
92
+ setStyle(elm.style, name, style[name]);
75
93
  }
76
94
  }
77
95
  function applyRemoveStyle(vnode, rm) {
78
96
  const s = vnode.data.style;
79
- if (!s || !s.remove) {
97
+ if (!s || !Object.hasOwn(s, 'remove') || !s.remove) {
80
98
  rm();
81
99
  return;
82
100
  }
@@ -92,9 +110,10 @@ function applyRemoveStyle(vnode, rm) {
92
110
  const style = s.remove;
93
111
  let amount = 0;
94
112
  const applied = [];
95
- for (name in style) {
96
- applied.push(name);
97
- elm.style[name] = style[name];
113
+ for (name of Object.keys(style)) {
114
+ const propertyName = serializedStylePropertyName(name);
115
+ applied.push(propertyName);
116
+ setStyle(elm.style, name, style[name]);
98
117
  }
99
118
  const compStyle = getComputedStyle(elm);
100
119
  const props = compStyle['transition-property'].split(', ');
@@ -116,7 +135,7 @@ function forceReflow() {
116
135
  export const styleModule = {
117
136
  dataMask: VNodeDataMask.Style,
118
137
  pre: forceReflow,
119
- create: updateStyle,
138
+ create: createStyle,
120
139
  update: updateStyle,
121
140
  destroy: applyDestroyStyle,
122
141
  remove: applyRemoveStyle,
@@ -5,6 +5,7 @@ export declare const Message: S.Union<readonly [import("../../schema/index.js").
5
5
  export type Message = typeof Message.Type;
6
6
  export type Model = Readonly<{
7
7
  attribute: Attribute<Message>;
8
+ tagName?: string;
8
9
  }>;
9
10
  export declare const update: (model: Model, message: Message) => readonly [Model, ReadonlyArray<never>];
10
11
  export declare const testId = "attribute-host";