foldkit 0.146.0 → 0.147.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 (56) hide show
  1. package/README.md +5 -2
  2. package/dist/customElement/index.d.ts.map +1 -1
  3. package/dist/customElement/index.js +23 -0
  4. package/dist/experimental/index.d.ts +1 -0
  5. package/dist/experimental/index.d.ts.map +1 -1
  6. package/dist/experimental/index.js +1 -0
  7. package/dist/experimental/machine/machine.d.ts +22 -3
  8. package/dist/experimental/machine/machine.d.ts.map +1 -1
  9. package/dist/experimental/machine/machine.js +8 -0
  10. package/dist/experimental/server/entry.d.ts +73 -0
  11. package/dist/experimental/server/entry.d.ts.map +1 -0
  12. package/dist/experimental/server/entry.js +41 -0
  13. package/dist/experimental/server/host.d.ts +42 -0
  14. package/dist/experimental/server/host.d.ts.map +1 -0
  15. package/dist/experimental/server/host.js +169 -0
  16. package/dist/experimental/server/index.d.ts +5 -0
  17. package/dist/experimental/server/index.d.ts.map +1 -0
  18. package/dist/experimental/server/index.js +4 -0
  19. package/dist/experimental/server/public.d.ts +3 -0
  20. package/dist/experimental/server/public.d.ts.map +1 -0
  21. package/dist/experimental/server/public.js +1 -0
  22. package/dist/experimental/server/serialize.d.ts +33 -0
  23. package/dist/experimental/server/serialize.d.ts.map +1 -0
  24. package/dist/experimental/server/serialize.js +563 -0
  25. package/dist/experimental/server/server.d.ts +201 -0
  26. package/dist/experimental/server/server.d.ts.map +1 -0
  27. package/dist/experimental/server/server.js +423 -0
  28. package/dist/experimental/server/template.d.ts +45 -0
  29. package/dist/experimental/server/template.d.ts.map +1 -0
  30. package/dist/experimental/server/template.js +182 -0
  31. package/dist/html/index.d.ts +5 -0
  32. package/dist/html/index.d.ts.map +1 -1
  33. package/dist/html/index.js +38 -4
  34. package/dist/hydrate.d.ts +4 -0
  35. package/dist/hydrate.d.ts.map +1 -0
  36. package/dist/hydrate.js +522 -0
  37. package/dist/hydrationMarker.d.ts +10 -0
  38. package/dist/hydrationMarker.d.ts.map +1 -0
  39. package/dist/hydrationMarker.js +9 -0
  40. package/dist/runtime/public.d.ts +2 -2
  41. package/dist/runtime/public.d.ts.map +1 -1
  42. package/dist/runtime/public.js +1 -1
  43. package/dist/runtime/runtime.d.ts +75 -36
  44. package/dist/runtime/runtime.d.ts.map +1 -1
  45. package/dist/runtime/runtime.js +209 -71
  46. package/dist/snabbdom/h.d.ts +1 -0
  47. package/dist/snabbdom/h.d.ts.map +1 -1
  48. package/dist/snabbdom/h.js +85 -4
  49. package/dist/snabbdom/tovnode.d.ts.map +1 -1
  50. package/dist/snabbdom/tovnode.js +5 -1
  51. package/dist/tagName.d.ts +6 -0
  52. package/dist/tagName.d.ts.map +1 -0
  53. package/dist/tagName.js +11 -0
  54. package/dist/vdom.d.ts.map +1 -1
  55. package/dist/vdom.js +25 -1
  56. package/package.json +8 -1
@@ -2,7 +2,9 @@ import { Array, Cause, Context, Duration, Effect, Exit, Fiber, Function, Layer,
2
2
  import { __CurrentRegistry as __CurrentInterruptRegistry, __makeRegistry as __makeInterruptRegistry, } from '../command/interruptible/index.js';
3
3
  import { createDevToolsStore, } from '../devTools/store.js';
4
4
  import { startWebSocketBridge } from '../devTools/webSocketBridge.js';
5
- import { __beginRender as beginHtmlRender, __beginReplayRender as beginReplayHtmlRender, __clearRuntime as clearHtmlRuntime, __createBoundaryRegistry as createHtmlBoundaryRegistry, __endReplayRender as endReplayHtmlRender, __htmlBuilder as htmlBuilderFor, __setRuntime as setHtmlRuntime, } from '../html/index.js';
5
+ import { __beginRender as beginHtmlRender, __beginReplayRender as beginReplayHtmlRender, __clearRuntime as clearHtmlRuntime, __createBoundaryRegistry as createHtmlBoundaryRegistry, __endReplayRender as endReplayHtmlRender, __htmlBuilder as htmlBuilderFor, __setRuntime as setHtmlRuntime, textDirectionToAttribute, } from '../html/index.js';
6
+ import { __hydrateVNode } from '../hydrate.js';
7
+ import { FOLDKIT_APP_ATTRIBUTE, FOLDKIT_FLAGS_ATTRIBUTE, } from '../hydrationMarker.js';
6
8
  import { MountTracker } from '../mount/index.js';
7
9
  import { __CurrentPortChannels, __makeInboundChannel, } from '../port/index.js';
8
10
  import { RenderCommit, createCommitNotifier } from '../render/commit.js';
@@ -191,6 +193,53 @@ const createDuplicateIdScanner = () => {
191
193
  /** Effect service tag that provides message dispatching to the view layer. */
192
194
  export class Dispatch extends Context.Service()('@foldkit/Dispatch') {
193
195
  }
196
+ const hydrationForRoot = (root, isFlagsRequired) => {
197
+ 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 };
200
+ };
201
+ // NOTE: hydration is scoped to the app's own stamped root so a server-rendered
202
+ // app never adopts another app's DOM. A container that carries the stamp is that
203
+ // root. A container that does not is a non-root element the caller resolved,
204
+ // which happens when the rendered view has its own element with the container's
205
+ // id (a descendant `id="root"`) and `getElementById` returned that inner element
206
+ // instead of the stamped root above it. That inner element resolves to the app
207
+ // root only when the page has exactly one stamped root and this container sits
208
+ // inside it, so a sibling widget (a stamped root the container is not inside) or
209
+ // an outer application's root (when a nested page carries more than one stamped
210
+ // root) is never wrongly adopted; those fall through to a fresh boot or a hard
211
+ // failure instead. A null container is the replace-parity case, where the server
212
+ // root took the placeholder's place and `getElementById` no longer finds it, so
213
+ // the stamp is the only handle; more than one stamped root is then ambiguous and
214
+ // a hard error rather than a silent wrong-DOM adoption.
215
+ const findDocumentHydration = (container, isFlagsRequired) => {
216
+ const stampedRoots = Array.fromIterable(document.querySelectorAll(`[${FOLDKIT_APP_ATTRIBUTE}]`));
217
+ if (container !== null) {
218
+ if (container.hasAttribute(FOLDKIT_APP_ATTRIBUTE)) {
219
+ return hydrationForRoot(container, isFlagsRequired);
220
+ }
221
+ return Array.match(stampedRoots, {
222
+ onEmpty: () => undefined,
223
+ onNonEmpty: roots => {
224
+ const onlyRoot = Array.headNonEmpty(roots);
225
+ return roots.length === 1 &&
226
+ container.closest(`[${FOLDKIT_APP_ATTRIBUTE}]`) === onlyRoot
227
+ ? hydrationForRoot(onlyRoot, isFlagsRequired)
228
+ : undefined;
229
+ },
230
+ });
231
+ }
232
+ if (stampedRoots.length > 1) {
233
+ throw new Error('[foldkit] Found multiple server-rendered roots stamped with ' +
234
+ `\`${FOLDKIT_APP_ATTRIBUTE}\` but no container to disambiguate them. ` +
235
+ 'Give each app its own container element so the runtime can tell ' +
236
+ 'which root to hydrate.');
237
+ }
238
+ return Option.match(Array.head(stampedRoots), {
239
+ onNone: () => undefined,
240
+ onSome: root => hydrationForRoot(root, isFlagsRequired),
241
+ });
242
+ };
194
243
  const makeHostConnector = () => {
195
244
  let isDisposed = false;
196
245
  let maybeDeliverInbound = Option.none();
@@ -314,7 +363,7 @@ const validatePorts = (ports) => {
314
363
  });
315
364
  };
316
365
  const runtimeInternals = new WeakMap();
317
- const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, view, manageDocument, subscriptions, container, routing: routingConfig, crash, slow, viewTransition, freezeModel, preserveScroll, resources, managedResources, devTools, }) => {
366
+ const makeRuntime = ({ ports, kind, Model, Flags: FlagsCodec, configuredFlags, isFlagsRequired, init, update, view, manageDocument, subscriptions, container, hydration, routing: routingConfig, crash, slow, viewTransition, freezeModel, preserveScroll, resources, managedResources, devTools, }) => {
318
367
  const isSlowVisible = (show) => Match.value(show).pipe(Match.when('Always', () => true), Match.when('Development', () => !!import.meta.hot), Match.exhaustive);
319
368
  const htmlBuilder = htmlBuilderFor();
320
369
  const resolvedSlow = __resolveSlowConfig(slow, isSlowVisible);
@@ -352,8 +401,10 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
352
401
  if (Predicate.isNotUndefined(ports)) {
353
402
  validatePorts(ports);
354
403
  }
355
- const runtimeId = container?.id ?? '';
356
- const startWith = (maybeConnector, hmrModel) => {
404
+ const runtimeId = hydration !== undefined && hydration.runtimeId !== ''
405
+ ? hydration.runtimeId
406
+ : (container?.id ?? '');
407
+ const startWith = (maybeConnector, hmrModel, bootMode = 'Fresh', bootFlags) => {
357
408
  // NOTE: one notifier per runtime, provided across the whole runtime
358
409
  // Effect so Commands, Subscriptions, and Mount-forked Effects all resolve
359
410
  // the same signal. A commit in one embedded application must never wake a
@@ -362,7 +413,8 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
362
413
  return Effect.scoped(Effect.gen(function* () {
363
414
  if (runtimeId === '') {
364
415
  return yield* Effect.die(new Error('[foldkit] Runtime container must have an `id` for HMR model preservation. ' +
365
- 'Set `container.id = "app"` (or any unique string) before passing it to makeApplication or makeElement.'));
416
+ 'Set `container.id = "app"` (or any unique string) before passing it to makeApplication or makeElement. ' +
417
+ 'On a server-rendered page the id comes from the `data-foldkit-app` root stamp instead.'));
366
418
  }
367
419
  // NOTE: every perpetual fiber (for example, Subscription streams
368
420
  // and ManagedResource lifecycles) and every Command fiber forks
@@ -434,28 +486,28 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
434
486
  });
435
487
  return Effect.provideService(withPortChannels, __CurrentInterruptRegistry, interruptRegistry);
436
488
  };
437
- // NOTE: flags run through the same cached build that Commands and
489
+ // NOTE: Flags run through the same cached build that Commands and
438
490
  // Subscriptions use, rather than being handed the Layer again, so a
439
491
  // service needed both at startup and by a Command is constructed
440
- // once. An app without flags never reaches it, which keeps the Layer
492
+ // once. An app without Flags never reaches it, which keeps the Layer
441
493
  // lazy when the first thing that needs it is a Command.
442
494
  //
443
495
  // NOTE: a Layer that fails to build is not fatal here. Flags resolve
444
496
  // before `init`, so there is no Model for a crash view to render
445
497
  // against and a failure escaping this point kills the app with a
446
- // blank container. Running flags against an empty context instead
447
- // lets an app whose flags never touch the Layer boot as it did
448
- // before flags could consume `resources`: the cached failure then
498
+ // blank container. Running Flags against an empty context instead
499
+ // lets an app whose Flags never touch the Layer boot as it did
500
+ // before Flags could consume `resources`: the cached failure then
449
501
  // surfaces at the first Command or Subscription, where `crashWith`
450
502
  // does render the crash view. Flags that do need the Layer still
451
503
  // fail here, and both causes are reported: the `Service not found`
452
504
  // defect the empty context produced is useless on its own, and the
453
505
  // build failure that explains it would be lost if it replaced the
454
- // flags cause outright. Combining them also keeps a flags Effect
506
+ // Flags cause outright. Combining them also keeps a Flags Effect
455
507
  // that fails for its own unrelated reason visible instead of
456
508
  // attributing its defect to the Layer. Interrupts propagate
457
509
  // untouched on both sides, because dispose racing either the build
458
- // or the flags run is not a failure to recover from, and
510
+ // or the Flags run is not a failure to recover from, and
459
511
  // `Effect.catchCause` hands the handler interrupt causes too.
460
512
  const provideResources = (effect) => Option.match(maybeAcquireResourceContext, {
461
513
  /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
@@ -471,23 +523,73 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
471
523
  onSuccess: resourceContext => Effect.provideContext(effect, resourceContext),
472
524
  }),
473
525
  });
474
- const resolveFlags = Option.match(maybeResolveFlags, {
475
- /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
476
- onNone: () => Effect.succeed(undefined),
526
+ const maybeResolveFreshFlags = Option.orElse(Option.fromNullishOr(bootFlags), () => configuredFlags);
527
+ const resolveFreshFlags = Option.match(maybeResolveFreshFlags, {
528
+ onNone: () => isFlagsRequired
529
+ ? Effect.die(new Error('[foldkit] This application declares Flags. Pass its ' +
530
+ 'Flags Effect to Runtime.run or Runtime.embed.'))
531
+ : /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
532
+ Effect.succeed(undefined),
477
533
  onSome: provideResources,
478
534
  });
535
+ const decodeFlagsPayload = (payload, runtimeId) => Effect.try({
536
+ try: () => {
537
+ const parsedPayload = JSON.parse(payload);
538
+ return pipe(
539
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
540
+ Schema.toCodecJson(FlagsCodec), Schema.decodeUnknownSync, decode => decode(parsedPayload));
541
+ },
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);
546
+ const maybeRequestedHydration = bootMode === 'Hydrate'
547
+ ? Option.fromNullishOr(hydration)
548
+ : Option.none();
549
+ if (bootMode === 'Hydrate' && Option.isNone(maybeRequestedHydration)) {
550
+ return yield* Effect.die(new Error('[foldkit] Runtime.hydrate could not find a server-rendered ' +
551
+ `root stamped with \`${FOLDKIT_APP_ATTRIBUTE}\`. Use ` +
552
+ 'Runtime.run for a fresh client boot.'));
553
+ }
554
+ // NOTE: an HMR-restored Model wins over DOM adoption because the
555
+ // server DOM reflects older code. The hydration handoff is still
556
+ // required, but the restored Model gets a fresh patch against its
557
+ // stamped root.
558
+ const maybeHydrationRoot = Predicate.isUndefined(hmrModel)
559
+ ? Option.map(maybeRequestedHydration, requestedHydration => requestedHydration.root)
560
+ : Option.none();
561
+ const maybeHydrationFlags = yield* Option.match(maybeRequestedHydration, {
562
+ onNone: () => Effect.succeed(Option.none()),
563
+ onSome: requestedHydration => Effect.map(requestedHydration.isFlagsRequired
564
+ ? Array.match(requestedHydration.flagsPayloads, {
565
+ onEmpty: () => Effect.die(new Error('[foldkit] Runtime.hydrate found application ' +
566
+ `"${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 ' +
570
+ 'server Flags payloads for application ' +
571
+ `"${requestedHydration.runtimeId}".`))
572
+ : decodeFlagsPayload(payload, requestedHydration.runtimeId),
573
+ })
574
+ : /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
575
+ Effect.succeed(undefined), Option.some),
576
+ });
577
+ const resolveFlags = Option.match(maybeHydrationFlags, {
578
+ onNone: () => resolveFreshFlags,
579
+ onSome: Effect.succeed,
580
+ });
479
581
  const ModelJsonCodec = Schema.toCodecJson(
480
582
  /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
481
583
  Model);
482
584
  const decodeHmrModel = Schema.decodeUnknownExit(ModelJsonCodec);
483
585
  const encodeHmrModel = Schema.encodeUnknownSync(ModelJsonCodec);
484
586
  const currentUrl = Option.fromNullishOr(routingConfig).pipe(Option.flatMap(() => urlFromString(window.location.href)));
485
- // NOTE: a restored Model skips `init`, so resolving flags on that
587
+ // NOTE: a restored Model skips `init`, so resolving Flags on that
486
588
  // path would build the `resources` Layer only to discard what it
487
589
  // produced. Gating the resolution on the restore decision is what
488
590
  // stops a reload from reconnecting whatever the Layer holds. It has
489
591
  // to stay ahead of the preserve-scheduler and HMR finalizers: a
490
- // flags Effect that fails after those are registered tears down more
592
+ // Flags Effect that fails after those are registered tears down more
491
593
  // than it used to, and their release defects would bury its cause.
492
594
  const runInit = Effect.map(resolveFlags, flags => init(flags, Option.getOrUndefined(currentUrl)));
493
595
  const [initModelRaw, initCommands] = yield* hmrModel !== undefined
@@ -637,6 +739,10 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
637
739
  }
638
740
  };
639
741
  const vnodeSlot = { maybeCurrentVNode: Option.none() };
742
+ // NOTE: consumed by the first render only. Set when this boot found
743
+ // an adoptable server-rendered root; the first patch then goes
744
+ // through `__hydrateVNode` instead of replacing the container.
745
+ let pendingHydrationRoot = Option.getOrNull(maybeHydrationRoot);
640
746
  // NOTE: registered before any perpetual fiber is forked so it runs
641
747
  // after they are interrupted (scope finalizers are LIFO). Patching to
642
748
  // an empty tree fires snabbdom destroy hooks, which is what releases
@@ -941,7 +1047,24 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
941
1047
  thresholdMs,
942
1048
  }));
943
1049
  const maybeCurrentVNode = vnodeSlot.maybeCurrentVNode;
944
- const [patchedVNode, maybePatchDuration] = yield* Effect.sync(() => measureSlowPhase(maybeLiveSlowPatch, () => __patchVNode(maybeCurrentVNode, nextVNode, container, boundaryRegistry.dedupeSeen)));
1050
+ const [patchedVNode, maybePatchDuration] = yield* Effect.sync(() => measureSlowPhase(maybeLiveSlowPatch, () => {
1051
+ if (Option.isNone(maybeCurrentVNode) &&
1052
+ pendingHydrationRoot !== null) {
1053
+ const hydrationRoot = pendingHydrationRoot;
1054
+ pendingHydrationRoot = null;
1055
+ // NOTE: strip the stamp before the patch, not after, so the
1056
+ // patch is the sole owner of the root's attributes. It has
1057
+ // already served its purpose of locating the root, and
1058
+ // removing it after would delete a `data-foldkit-app` the view
1059
+ // itself declares, which a later equal-vnode patch would not
1060
+ // restore. Removing it here also stops a later boot on the same
1061
+ // container (a dispose-then-embed remount) from re-detecting
1062
+ // this now-consumed root as hydratable.
1063
+ hydrationRoot.removeAttribute(FOLDKIT_APP_ATTRIBUTE);
1064
+ return __hydrateVNode(hydrationRoot, nextVNode, boundaryRegistry.dedupeSeen);
1065
+ }
1066
+ return __patchVNode(maybeCurrentVNode, nextVNode, container, boundaryRegistry.dedupeSeen);
1067
+ }));
945
1068
  vnodeSlot.maybeCurrentVNode = Option.some(patchedVNode);
946
1069
  reportSlowPhase(maybeLiveSlowPatch, maybePatchDuration, (durationMs, thresholdMs) => ({
947
1070
  _tag: 'Patch',
@@ -1372,10 +1495,15 @@ const makeRuntime = ({ ports, Model, flags: maybeResolveFlags, init, update, vie
1372
1495
  yield* Effect.never;
1373
1496
  })).pipe(Effect.provideService(RenderCommit, commitNotifier.service));
1374
1497
  };
1375
- const start = (hmrModel) => startWith(Option.none(), hmrModel);
1376
- const program = { runtimeId, start, ports };
1498
+ const start = (hmrModel) => startWith(Option.none(), hmrModel, 'Fresh');
1499
+ const program = {
1500
+ runtimeId,
1501
+ start,
1502
+ ports,
1503
+ };
1377
1504
  runtimeInternals.set(program, {
1378
- startWith,
1505
+ startWith: (maybeConnector, hmrModel, bootMode, flags) => startWith(maybeConnector, hmrModel, bootMode, flags),
1506
+ kind,
1379
1507
  isEmbedActive: false,
1380
1508
  maybeActiveFiber: Option.none(),
1381
1509
  });
@@ -1390,11 +1518,6 @@ const currentLocationUrl = () => {
1390
1518
  const { origin, pathname, search } = window.location;
1391
1519
  return `${origin}${pathname}${search}`;
1392
1520
  };
1393
- const textDirectionForHtmlElement = {
1394
- Ltr: 'ltr',
1395
- Rtl: 'rtl',
1396
- Auto: 'auto',
1397
- };
1398
1521
  const documentMetadataElements = new WeakMap();
1399
1522
  const metadataElementsForDocument = () => {
1400
1523
  let elements = documentMetadataElements.get(document);
@@ -1431,7 +1554,7 @@ const applyDocumentMetadata = (nextDocument, mountedRoot) => {
1431
1554
  documentElement.lang = nextDocument.lang;
1432
1555
  }
1433
1556
  if (nextDocument.dir !== undefined) {
1434
- const dir = textDirectionForHtmlElement[nextDocument.dir];
1557
+ const dir = textDirectionToAttribute(nextDocument.dir);
1435
1558
  if (documentElement.dir !== dir) {
1436
1559
  documentElement.dir = dir;
1437
1560
  }
@@ -1503,24 +1626,29 @@ const renderCrashView = (context, crash, container, vnodeSlot, manageDocument) =
1503
1626
  };
1504
1627
  export function makeApplication(config) {
1505
1628
  const { container } = config;
1506
- if (container === null) {
1629
+ const hasRouting = 'routing' in config;
1630
+ const hasFlags = 'Flags' in config;
1631
+ const hydration = findDocumentHydration(container, hasFlags);
1632
+ const resolvedContainer = hydration?.root ?? container;
1633
+ if (resolvedContainer === null) {
1507
1634
  throw new Error('[foldkit] Container is null. Make sure the element exists in the DOM ' +
1508
1635
  'before calling makeApplication (e.g. that your <div id="root"></div> has ' +
1509
- 'rendered, and your script runs after it).');
1636
+ 'rendered, and your script runs after it). On a server-rendered page ' +
1637
+ 'the runtime instead finds the root by its `data-foldkit-app` stamp.');
1510
1638
  }
1511
- const hasRouting = 'routing' in config;
1512
- const hasFlags = 'Flags' in config;
1513
1639
  const currentUrl = hasRouting
1514
1640
  ? Option.getOrThrow(urlFromString(window.location.href))
1515
1641
  : undefined;
1516
1642
  const baseConfig = {
1643
+ kind: 'Application',
1517
1644
  Model: config.Model,
1518
1645
  update: config.update,
1519
1646
  view: config.view,
1520
1647
  manageDocument: true,
1521
1648
  ports: config.ports,
1522
1649
  ...(config.subscriptions && { subscriptions: config.subscriptions }),
1523
- container,
1650
+ container: resolvedContainer,
1651
+ ...(hydration && { hydration }),
1524
1652
  ...(hasRouting && { routing: config.routing }),
1525
1653
  ...(config.crash && { crash: config.crash }),
1526
1654
  ...(Predicate.isNotUndefined(config.slow) && {
@@ -1548,7 +1676,8 @@ export function makeApplication(config) {
1548
1676
  return makeRuntime({
1549
1677
  ...baseConfig,
1550
1678
  Flags: config.Flags,
1551
- flags: Option.some(config.flags),
1679
+ configuredFlags: Option.none(),
1680
+ isFlagsRequired: true,
1552
1681
  init: (flags, url) => config.init(flags, url ?? currentUrl),
1553
1682
  });
1554
1683
  }
@@ -1556,7 +1685,8 @@ export function makeApplication(config) {
1556
1685
  return makeRuntime({
1557
1686
  ...baseConfig,
1558
1687
  Flags: Schema.Void,
1559
- flags: Option.none(),
1688
+ configuredFlags: Option.none(),
1689
+ isFlagsRequired: false,
1560
1690
  init: (_flags, url) => config.init(url ?? currentUrl),
1561
1691
  });
1562
1692
  }
@@ -1564,7 +1694,8 @@ export function makeApplication(config) {
1564
1694
  return makeRuntime({
1565
1695
  ...baseConfig,
1566
1696
  Flags: config.Flags,
1567
- flags: Option.some(config.flags),
1697
+ configuredFlags: Option.none(),
1698
+ isFlagsRequired: true,
1568
1699
  init: (flags) => config.init(flags),
1569
1700
  });
1570
1701
  }
@@ -1572,7 +1703,8 @@ export function makeApplication(config) {
1572
1703
  return makeRuntime({
1573
1704
  ...baseConfig,
1574
1705
  Flags: Schema.Void,
1575
- flags: Option.none(),
1706
+ configuredFlags: Option.none(),
1707
+ isFlagsRequired: false,
1576
1708
  init: () => config.init(),
1577
1709
  });
1578
1710
  }
@@ -1610,6 +1742,7 @@ export function makeElement(config) {
1610
1742
  });
1611
1743
  const crash = toCrashConfig(config.crash);
1612
1744
  const baseConfig = {
1745
+ kind: 'Element',
1613
1746
  Model: config.Model,
1614
1747
  update: config.update,
1615
1748
  view,
@@ -1640,7 +1773,8 @@ export function makeElement(config) {
1640
1773
  return makeRuntime({
1641
1774
  ...baseConfig,
1642
1775
  Flags: config.Flags,
1643
- flags: Option.some(config.flags),
1776
+ configuredFlags: Option.some(config.flags),
1777
+ isFlagsRequired: true,
1644
1778
  init: (flags) => config.init(flags),
1645
1779
  });
1646
1780
  }
@@ -1648,7 +1782,8 @@ export function makeElement(config) {
1648
1782
  return makeRuntime({
1649
1783
  ...baseConfig,
1650
1784
  Flags: Schema.Void,
1651
- flags: Option.none(),
1785
+ configuredFlags: Option.none(),
1786
+ isFlagsRequired: false,
1652
1787
  init: () => config.init(),
1653
1788
  });
1654
1789
  }
@@ -1716,6 +1851,20 @@ const resolveHmrModel = (runtimeId) => {
1716
1851
  return Effect.succeed(undefined);
1717
1852
  }));
1718
1853
  };
1854
+ /** Starts a program Effect with explicit boot inputs for runtime tests.
1855
+ * @internal */
1856
+ export const __startProgram = (program, hmrModel, bootMode, flags) => {
1857
+ const internals = runtimeInternals.get(program);
1858
+ if (Predicate.isUndefined(internals)) {
1859
+ return Effect.die(new Error('[foldkit] Runtime boot expects a program created by ' +
1860
+ 'makeApplication or makeElement.'));
1861
+ }
1862
+ if (bootMode === 'Hydrate' && internals.kind !== 'Application') {
1863
+ return Effect.die(new Error('[foldkit] Runtime.hydrate expects a program created by ' +
1864
+ 'makeApplication.'));
1865
+ }
1866
+ return internals.startWith(Option.none(), hmrModel, bootMode, flags);
1867
+ };
1719
1868
  // NOTE: deliberately not `BrowserRuntime.runMain`, which interrupts the
1720
1869
  // runtime on `beforeunload`. `beforeunload` is a question, not a commitment:
1721
1870
  // the browser also fires it for a click on a download link, for a navigation
@@ -1727,11 +1876,25 @@ const resolveHmrModel = (runtimeId) => {
1727
1876
  // interrupt at all and lets the document take the runtime with it. Error
1728
1877
  // reporting and the keep-alive interval come from `makeRunMain` either way.
1729
1878
  const runMainWithoutUnloadInterrupt = Runtime.makeRunMain(Function.constVoid);
1730
- /** Starts a Foldkit runtime that owns the page for the page's whole lifetime,
1731
- * with HMR support for development. To start a runtime under a
1732
- * host-controlled lifecycle instead, use `embed`. */
1733
- export const run = (program) => {
1734
- runMainWithoutUnloadInterrupt(provideBrowserScheduler(Effect.flatMap(resolveHmrModel(program.runtimeId), program.start)));
1879
+ const startProgram = (program, bootMode, flags) => {
1880
+ runMainWithoutUnloadInterrupt(provideBrowserScheduler(Effect.flatMap(resolveHmrModel(program.runtimeId), hmrModel => __startProgram(program, hmrModel, bootMode, flags))));
1881
+ };
1882
+ export function run(program, options) {
1883
+ startProgram(program, 'Fresh', options?.flags);
1884
+ }
1885
+ /** Starts a Foldkit runtime by adopting a server-rendered DOM in place instead
1886
+ * of building it fresh. Use this as the client entry for a page served by
1887
+ * `renderToString`: the first render attaches to the stamped root, keeps the
1888
+ * 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.
1893
+ *
1894
+ * @experimental Server rendering and hydration are experimental while their
1895
+ * contracts settle. */
1896
+ export const hydrate = (program) => {
1897
+ startProgram(program, 'Hydrate');
1735
1898
  };
1736
1899
  const buildPortHandles = (ports, connector) => {
1737
1900
  const handles = {};
@@ -1750,32 +1913,7 @@ const buildPortHandles = (ports, connector) => {
1750
1913
  /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
1751
1914
  return handles;
1752
1915
  };
1753
- /**
1754
- * Starts a Foldkit runtime under a host-controlled lifecycle and returns an
1755
- * `EmbedHandle`. This is the entry point for embedding a Foldkit app inside
1756
- * another application: the host pushes values in through the handle's inbound
1757
- * Ports, listens to outbound Ports, and calls `dispose` when it unmounts the
1758
- * app. The host never touches the Model or dispatches Messages directly; the
1759
- * Schema-typed Ports are the whole boundary.
1760
- *
1761
- * Works with programs from both `makeApplication` and `makeElement`; for a
1762
- * widget on a page the host owns, `makeElement` is the natural fit.
1763
- *
1764
- * A program can be embedded once at a time (it owns one container). After
1765
- * `dispose`, the same container can be embedded again with a fresh program.
1766
- *
1767
- * ```ts
1768
- * const handle = Runtime.embed(element)
1769
- *
1770
- * handle.ports.stepChanged.send(5)
1771
- * const unsubscribe = handle.ports.countChanged.subscribe(count => {
1772
- * console.log(count)
1773
- * })
1774
- *
1775
- * handle.dispose()
1776
- * ```
1777
- */
1778
- export const embed = (program) => {
1916
+ export function embed(program, options) {
1779
1917
  const internals = runtimeInternals.get(program);
1780
1918
  if (Predicate.isUndefined(internals)) {
1781
1919
  throw new Error('[foldkit] embed expects a program created by makeApplication or makeElement.');
@@ -1795,7 +1933,7 @@ export const embed = (program) => {
1795
1933
  const startEffect = pipe(Option.match(internals.maybeActiveFiber, {
1796
1934
  onNone: () => Effect.void,
1797
1935
  onSome: previousFiber => Effect.asVoid(Fiber.await(previousFiber)),
1798
- }), Effect.andThen(resolveHmrModel(program.runtimeId)), Effect.flatMap(hmrModel => internals.startWith(Option.some(connector), hmrModel)));
1936
+ }), Effect.andThen(resolveHmrModel(program.runtimeId)), Effect.flatMap(hmrModel => internals.startWith(Option.some(connector), hmrModel, 'Fresh', options?.flags)));
1799
1937
  const fiber = Effect.runFork(provideBrowserScheduler(startEffect));
1800
1938
  internals.maybeActiveFiber = Option.some(fiber);
1801
1939
  let isHandleDisposed = false;
@@ -1810,4 +1948,4 @@ export const embed = (program) => {
1810
1948
  };
1811
1949
  const ports = buildPortHandles(program.ports, connector);
1812
1950
  return { ports, dispose };
1813
- };
1951
+ }
@@ -4,6 +4,7 @@ export type VNodeChildElement = VNode | string | number | String | Number | unde
4
4
  export type ArrayOrElement<T> = T | Array<T>;
5
5
  export type VNodeChildren = ArrayOrElement<VNodeChildElement>;
6
6
  export declare function addNS(data: any, children: Array<VNode | string> | undefined, sel: string | undefined): void;
7
+ export declare function addMathmlNS(data: any, children: Array<VNode | string> | undefined, sel: string | undefined): void;
7
8
  export declare function h(sel: string): VNode;
8
9
  export declare function h(sel: string, data: VNodeData | null): VNode;
9
10
  export declare function h(sel: string, children: VNodeChildren): VNode;
@@ -1 +1 @@
1
- {"version":3,"file":"h.d.ts","sourceRoot":"","sources":["../../src/snabbdom/h.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,SAAS,EAAS,MAAM,YAAY,CAAA;AAE9D,MAAM,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;AACjC,MAAM,MAAM,iBAAiB,GACzB,KAAK,GACL,MAAM,GACN,MAAM,GACN,MAAM,GACN,MAAM,GACN,SAAS,GACT,IAAI,CAAA;AACR,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;AAC5C,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC,iBAAiB,CAAC,CAAA;AAE7D,wBAAgB,KAAK,CACnB,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,SAAS,EAC3C,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,IAAI,CAYN;AAED,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAA;AACrC,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,KAAK,CAAA;AAC7D,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,KAAK,CAAA;AAC9D,wBAAgB,CAAC,CACf,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,SAAS,GAAG,IAAI,EACtB,QAAQ,EAAE,aAAa,GACtB,KAAK,CAAA"}
1
+ {"version":3,"file":"h.d.ts","sourceRoot":"","sources":["../../src/snabbdom/h.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,SAAS,EAAS,MAAM,YAAY,CAAA;AAE9D,MAAM,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;AACjC,MAAM,MAAM,iBAAiB,GACzB,KAAK,GACL,MAAM,GACN,MAAM,GACN,MAAM,GACN,MAAM,GACN,SAAS,GACT,IAAI,CAAA;AACR,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;AAC5C,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC,iBAAiB,CAAC,CAAA;AAsD7D,wBAAgB,KAAK,CACnB,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,SAAS,EAC3C,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,IAAI,CAQN;AA6DD,wBAAgB,WAAW,CACzB,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,SAAS,EAC3C,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,IAAI,CAEN;AAED,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAA;AACrC,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,KAAK,CAAA;AAC7D,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,KAAK,CAAA;AAC9D,wBAAgB,CAAC,CACf,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,SAAS,GAAG,IAAI,EACtB,QAAQ,EAAE,aAAa,GACtB,KAAK,CAAA"}
@@ -1,18 +1,95 @@
1
+ import { tagNameFromSelector } from '../tagName.js';
1
2
  import * as is from './is.js';
2
3
  import { vnode } from './vnode.js';
3
- export function addNS(data, children, sel) {
4
- data.ns = 'http://www.w3.org/2000/svg';
5
- if (sel !== 'foreignObject' && children !== undefined) {
4
+ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
5
+ const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
6
+ // NOTE: inside SVG, the content of foreignObject, desc, and title is parsed in
7
+ // the HTML namespace (SVG's HTML integration points), so the SVG namespace must
8
+ // not propagate into their children.
9
+ const SVG_HTML_INTEGRATION_POINTS = new Set(['foreignObject', 'desc', 'title']);
10
+ // NOTE: inside a MathML text integration point (mi, mo, mn, ms, mtext) the
11
+ // content is parsed as HTML, so the MathML namespace stops there, except for
12
+ // mglyph and malignmark, which the parser keeps in the MathML namespace. A
13
+ // MathML annotation-xml element is an HTML integration point when its encoding
14
+ // is text/html or application/xhtml+xml, so its children are HTML too.
15
+ const MATHML_TEXT_INTEGRATION_POINTS = new Set([
16
+ 'mi',
17
+ 'mo',
18
+ 'mn',
19
+ 'ms',
20
+ 'mtext',
21
+ ]);
22
+ const MATHML_TEXT_INTEGRATION_EXCEPTIONS = new Set(['mglyph', 'malignmark']);
23
+ const MATHML_HTML_ENCODINGS = new Set(['text/html', 'application/xhtml+xml']);
24
+ const propagateNamespace = (data, children, sel, namespace, integrationPoints) => {
25
+ data.ns = namespace;
26
+ if ((sel === undefined || !integrationPoints.has(sel)) &&
27
+ children !== undefined) {
6
28
  for (let i = 0; i < children.length; ++i) {
7
29
  const child = children[i];
8
30
  if (typeof child === 'string')
9
31
  continue;
10
32
  const childData = child.data;
11
33
  if (childData !== undefined) {
12
- addNS(childData, child.children, child.sel);
34
+ propagateNamespace(childData, child.children, child.sel, namespace, integrationPoints);
13
35
  }
14
36
  }
15
37
  }
38
+ };
39
+ export function addNS(data, children, sel) {
40
+ propagateNamespace(data, children, sel, SVG_NAMESPACE, SVG_HTML_INTEGRATION_POINTS);
41
+ }
42
+ // NOTE: an annotation-xml is an HTML integration point when its encoding
43
+ // attribute is text/html or application/xhtml+xml. HTML attribute names are
44
+ // case-insensitive, so the name is matched case-insensitively as well as the
45
+ // value.
46
+ const isHtmlIntegrationAnnotationXml = (data) => {
47
+ const attrs = data?.attrs;
48
+ if (attrs === undefined || attrs === null) {
49
+ return false;
50
+ }
51
+ for (const name of Object.keys(attrs)) {
52
+ if (name.toLowerCase() === 'encoding') {
53
+ const value = attrs[name];
54
+ return (typeof value === 'string' &&
55
+ MATHML_HTML_ENCODINGS.has(value.toLowerCase()));
56
+ }
57
+ }
58
+ return false;
59
+ };
60
+ // NOTE: MathML namespace propagation carries the integration-point exceptions
61
+ // the generic walk cannot express: children of a text integration point are
62
+ // HTML except mglyph and malignmark, and children of an annotation-xml with an
63
+ // HTML encoding are HTML. Everything else inherits the MathML namespace.
64
+ const propagateMathmlNamespace = (data, children, sel) => {
65
+ data.ns = MATHML_NAMESPACE;
66
+ if (children === undefined) {
67
+ return;
68
+ }
69
+ const tag = sel === undefined ? undefined : tagNameFromSelector(sel);
70
+ if (tag === 'annotation-xml' && isHtmlIntegrationAnnotationXml(data)) {
71
+ return;
72
+ }
73
+ const isTextIntegrationPoint = tag !== undefined && MATHML_TEXT_INTEGRATION_POINTS.has(tag);
74
+ for (let i = 0; i < children.length; ++i) {
75
+ const child = children[i];
76
+ if (typeof child === 'string')
77
+ continue;
78
+ const childData = child.data;
79
+ if (childData === undefined)
80
+ continue;
81
+ if (isTextIntegrationPoint) {
82
+ const childTag = child.sel === undefined ? undefined : tagNameFromSelector(child.sel);
83
+ if (childTag === undefined ||
84
+ !MATHML_TEXT_INTEGRATION_EXCEPTIONS.has(childTag)) {
85
+ continue;
86
+ }
87
+ }
88
+ propagateMathmlNamespace(childData, child.children, child.sel);
89
+ }
90
+ };
91
+ export function addMathmlNS(data, children, sel) {
92
+ propagateMathmlNamespace(data, children, sel);
16
93
  }
17
94
  export function h(sel, b, c) {
18
95
  let data = {};
@@ -57,5 +134,9 @@ export function h(sel, b, c) {
57
134
  (sel.length === 3 || sel[3] === '.' || sel[3] === '#')) {
58
135
  addNS(data, children, sel);
59
136
  }
137
+ else if (sel.startsWith('math') &&
138
+ (sel.length === 4 || sel[4] === '.' || sel[4] === '#')) {
139
+ addMathmlNS(data, children, sel);
140
+ }
60
141
  return vnode(sel, data, children, text, undefined);
61
142
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tovnode.d.ts","sourceRoot":"","sources":["../../src/snabbdom/tovnode.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,MAAM,EAAc,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,KAAK,KAAK,EAAS,MAAM,YAAY,CAAA;AAc9C,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,CAiD1D"}
1
+ {"version":3,"file":"tovnode.d.ts","sourceRoot":"","sources":["../../src/snabbdom/tovnode.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,MAAM,EAAc,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,KAAK,KAAK,EAAS,MAAM,YAAY,CAAA;AAc9C,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,CAsD1D"}
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/consistent-type-assertions */
2
- import { addNS } from './h.js';
2
+ import { addMathmlNS, addNS } from './h.js';
3
3
  import { htmlDomApi } from './htmldomapi.js';
4
4
  import { vnode } from './vnode.js';
5
5
  /**
@@ -53,6 +53,10 @@ export function toVNode(node, domApi) {
53
53
  (sel.length === 3 || sel[3] === '.' || sel[3] === '#')) {
54
54
  addNS(data, children, sel);
55
55
  }
56
+ else if (sel.startsWith('math') &&
57
+ (sel.length === 4 || sel[4] === '.' || sel[4] === '#')) {
58
+ addMathmlNS(data, children, sel);
59
+ }
56
60
  return vnode(sel, data, children, undefined, node);
57
61
  }
58
62
  else if (api.isText(node)) {
@@ -0,0 +1,6 @@
1
+ /** Extracts the tag name from a snabbdom selector, dropping any `#id` or
2
+ * `.class` suffix. Foldkit builds bare-tag selectors, so in practice this
3
+ * returns the selector unchanged, but the id/class handling keeps it correct
4
+ * for any selector snabbdom itself would accept. */
5
+ export declare const tagNameFromSelector: (selector: string) => string;
6
+ //# sourceMappingURL=tagName.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tagName.d.ts","sourceRoot":"","sources":["../src/tagName.ts"],"names":[],"mappings":"AAAA;;;qDAGqD;AACrD,eAAO,MAAM,mBAAmB,GAAI,UAAU,MAAM,KAAG,MAMtD,CAAA"}