foldkit 0.145.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 (57) 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/mount/index.d.ts +1 -1
  41. package/dist/runtime/public.d.ts +2 -2
  42. package/dist/runtime/public.d.ts.map +1 -1
  43. package/dist/runtime/public.js +1 -1
  44. package/dist/runtime/runtime.d.ts +75 -36
  45. package/dist/runtime/runtime.d.ts.map +1 -1
  46. package/dist/runtime/runtime.js +209 -71
  47. package/dist/snabbdom/h.d.ts +1 -0
  48. package/dist/snabbdom/h.d.ts.map +1 -1
  49. package/dist/snabbdom/h.js +85 -4
  50. package/dist/snabbdom/tovnode.d.ts.map +1 -1
  51. package/dist/snabbdom/tovnode.js +5 -1
  52. package/dist/tagName.d.ts +6 -0
  53. package/dist/tagName.d.ts.map +1 -0
  54. package/dist/tagName.js +11 -0
  55. package/dist/vdom.d.ts.map +1 -1
  56. package/dist/vdom.js +25 -1
  57. package/package.json +11 -4
@@ -0,0 +1,201 @@
1
+ import { Effect, Schema } from 'effect';
2
+ import { type Document, type HtmlBuilder } from '../../html/index.js';
3
+ import { FOLDKIT_APP_ATTRIBUTE, FOLDKIT_FLAGS_ATTRIBUTE } from '../../hydrationMarker.js';
4
+ import { Url } from '../../url/index.js';
5
+ export { FOLDKIT_APP_ATTRIBUTE, FOLDKIT_FLAGS_ATTRIBUTE };
6
+ /** The server render of one request: the stamped root markup (plus the Flags
7
+ * payload script when the application declares Flags) and the `Document`
8
+ * head fields for the host to place into its HTML template.
9
+ *
10
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
11
+ */
12
+ export type RenderedApplication = Readonly<{
13
+ html: string;
14
+ title: string;
15
+ lang?: string;
16
+ dir?: 'ltr' | 'rtl' | 'auto';
17
+ canonical?: string;
18
+ ogUrl?: string;
19
+ }>;
20
+ declare const InvalidUrl_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
21
+ readonly _tag: "InvalidUrl";
22
+ } & Readonly<A>;
23
+ /** Failure of a routing render whose `url` option cannot be parsed.
24
+ *
25
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
26
+ */
27
+ export declare class InvalidUrl extends InvalidUrl_base<{
28
+ url: string;
29
+ }> {
30
+ }
31
+ declare const FlagsEncodeError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
32
+ readonly _tag: "FlagsEncodeError";
33
+ } & Readonly<A>;
34
+ /** Failure producing the Flags payload: the Schema encode step rejected the
35
+ * Flags value, the encoded value could not be serialized to JSON, or the
36
+ * encoded value could not be decoded back for the hydration-consistent
37
+ * render.
38
+ *
39
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
40
+ */
41
+ export declare class FlagsEncodeError extends FlagsEncodeError_base<{
42
+ cause: unknown;
43
+ }> {
44
+ }
45
+ declare const SerializationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
46
+ readonly _tag: "SerializationError";
47
+ } & Readonly<A>;
48
+ /** Failure serializing the view-produced vnode tree to safe HTML.
49
+ *
50
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
51
+ */
52
+ export declare class SerializationError extends SerializationError_base<{
53
+ cause: unknown;
54
+ }> {
55
+ }
56
+ declare const InvalidRuntimeId_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
57
+ readonly _tag: "InvalidRuntimeId";
58
+ } & Readonly<A>;
59
+ /** Failure of a render whose `runtimeId` is empty.
60
+ *
61
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
62
+ */
63
+ export declare class InvalidRuntimeId extends InvalidRuntimeId_base<{
64
+ runtimeId: string;
65
+ }> {
66
+ }
67
+ declare const InvalidHydrationRoot_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
68
+ readonly _tag: "InvalidHydrationRoot";
69
+ } & Readonly<A>;
70
+ /** Failure of a hydratable render whose view did not return an element root.
71
+ * Text, comments, and an empty body cannot carry the hydration marker the
72
+ * client runtime uses to adopt the server-rendered DOM.
73
+ *
74
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
75
+ */
76
+ export declare class InvalidHydrationRoot extends InvalidHydrationRoot_base<{
77
+ rootKind: 'Empty' | 'Text' | 'Comment';
78
+ }> {
79
+ }
80
+ /** Union of the failures {@link renderToString} can produce.
81
+ *
82
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
83
+ */
84
+ export type RenderError = InvalidUrl | FlagsEncodeError | SerializationError | InvalidRuntimeId | InvalidHydrationRoot;
85
+ type InitReturn<Model> = readonly [Model, ReadonlyArray<unknown>];
86
+ /** Server-side subset of a routing `makeApplication` config with Flags. The
87
+ * full application config is structurally assignable; `container`, `update`,
88
+ * and `subscriptions` play no part in a server render.
89
+ *
90
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
91
+ */
92
+ export type RoutingApplicationConfigWithFlags<Model, Message, Flags> = Readonly<{
93
+ Flags: Schema.Codec<Flags, any, never, never>;
94
+ routing: unknown;
95
+ init: (flags: Flags, url: Url) => InitReturn<Model>;
96
+ view: (model: Model, h: HtmlBuilder<Message>) => Document;
97
+ }>;
98
+ /** Server-side subset of a routing `makeApplication` config without Flags.
99
+ *
100
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
101
+ */
102
+ export type RoutingApplicationConfig<Model, Message> = Readonly<{
103
+ routing: unknown;
104
+ init: (url: Url) => InitReturn<Model>;
105
+ view: (model: Model, h: HtmlBuilder<Message>) => Document;
106
+ }>;
107
+ /** Server-side subset of a non-routing `makeApplication` config with Flags.
108
+ *
109
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
110
+ */
111
+ export type ApplicationConfigWithFlags<Model, Message, Flags> = Readonly<{
112
+ Flags: Schema.Codec<Flags, any, never, never>;
113
+ init: (flags: Flags) => InitReturn<Model>;
114
+ view: (model: Model, h: HtmlBuilder<Message>) => Document;
115
+ }>;
116
+ /** Server-side subset of a non-routing `makeApplication` config.
117
+ *
118
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
119
+ */
120
+ export type ApplicationConfig<Model, Message> = Readonly<{
121
+ init: () => InitReturn<Model>;
122
+ view: (model: Model, h: HtmlBuilder<Message>) => Document;
123
+ }>;
124
+ /** Options common to every render. `runtimeId` names the application in the
125
+ * root stamp and Flags payload; it defaults to `'app'` and must be non-empty.
126
+ *
127
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
128
+ */
129
+ export type RenderOptions = Readonly<{
130
+ runtimeId?: string;
131
+ /**
132
+ * Whether the output carries the hydration contract: the
133
+ * `data-foldkit-app` root stamp and, for Flags applications, the Flags
134
+ * payload script. Defaults to `true` for both request-time rendering and
135
+ * build-time static generation. A hydratable static page must use universal,
136
+ * build-stable Flags; resolve visitor-specific browser facts after hydration
137
+ * through Commands or Subscriptions. Pass `false` only when producing static
138
+ * markup that the client will not hydrate.
139
+ */
140
+ isHydratable?: boolean;
141
+ }>;
142
+ /** Render options for a routing application, adding the request URL.
143
+ *
144
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
145
+ */
146
+ export type RenderUrlOptions = RenderOptions & Readonly<{
147
+ url: string;
148
+ }>;
149
+ /** Render options for a Flags application, adding the per-request Flags.
150
+ *
151
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
152
+ */
153
+ export type RenderFlagsOptions<Flags> = RenderOptions & Readonly<{
154
+ flags: Flags;
155
+ }>;
156
+ /** Render options for a routing Flags application: the request URL plus the
157
+ * per-request Flags.
158
+ *
159
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
160
+ */
161
+ export type RenderUrlFlagsOptions<Flags> = RenderUrlOptions & RenderFlagsOptions<Flags>;
162
+ /**
163
+ * Renders a `makeApplication`-shaped config to an HTML string on the server.
164
+ *
165
+ * Resolves `init` for the request (with the given Flags and URL when the
166
+ * config declares them), runs the pure `view` under a no-op dispatch frame,
167
+ * and serializes the resulting `Document` body. The root element is stamped
168
+ * with {@link FOLDKIT_APP_ATTRIBUTE} and, when the config declares `Flags`,
169
+ * the Schema-encoded Flags ride along in a JSON script tag so a hydrating
170
+ * client boots from the same Model.
171
+ *
172
+ * Commands returned by `init` are not run: the rendered HTML is the
173
+ * post-`init` state, and the client runs those Commands after hydration.
174
+ *
175
+ * A hydratable Flags render calls `init` with the encode-then-decode round
176
+ * trip of the given Flags, the exact value the hydrating client will
177
+ * reconstruct, so the served DOM and the client's first render agree by
178
+ * construction even for codecs whose round trip is not the identity.
179
+ *
180
+ * When a routing view omits `Document.canonical`, the render defaults it (and
181
+ * `ogUrl`) to the request URL, normalized the way the client computes the
182
+ * current location but with the query string kept. Set `Document.canonical`
183
+ * explicitly when the query string is not part of the page's identity, such as
184
+ * tracking parameters or a session token, so a crawler does not index every
185
+ * variant as its own canonical page.
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * const renderedApplication = yield* Server.renderToString(config, {
190
+ * url: request.url,
191
+ * flags: { theme },
192
+ * })
193
+ * ```
194
+ *
195
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
196
+ */
197
+ export declare function renderToString<Model, Message, Flags>(config: RoutingApplicationConfigWithFlags<Model, Message, Flags>, options: RenderUrlFlagsOptions<Flags>): Effect.Effect<RenderedApplication, RenderError>;
198
+ export declare function renderToString<Model, Message>(config: RoutingApplicationConfig<Model, Message>, options: RenderUrlOptions): Effect.Effect<RenderedApplication, RenderError>;
199
+ export declare function renderToString<Model, Message, Flags>(config: ApplicationConfigWithFlags<Model, Message, Flags>, options: RenderFlagsOptions<Flags>): Effect.Effect<RenderedApplication, RenderError>;
200
+ export declare function renderToString<Model, Message>(config: ApplicationConfig<Model, Message>, options?: RenderOptions): Effect.Effect<RenderedApplication, RenderError>;
201
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/experimental/server/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,423 @@
1
+ import { Array as Array_, Context, Data, Effect, Option, Predicate, Schema, pipe, } from 'effect';
2
+ import { parseFragment } from 'parse5';
3
+ import { beginRender, createBoundaryRegistry } from '../../html/boundary.js';
4
+ import { __htmlBuilder as htmlBuilderFor, textDirectionToAttribute, } from '../../html/index.js';
5
+ import { clearRuntime, setRuntime, } from '../../html/runtimeSingleton.js';
6
+ import { FOLDKIT_APP_ATTRIBUTE, FOLDKIT_FLAGS_ATTRIBUTE, } from '../../hydrationMarker.js';
7
+ import { tagNameFromSelector } from '../../tagName.js';
8
+ import { fromString } from '../../url/index.js';
9
+ import { controlledValueContent, escapeAttributeValue, serializeHtml, } from './serialize.js';
10
+ const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
11
+ const isParse5Element = (node) => 'tagName' in node;
12
+ // Only ASCII whitespace is inter-element whitespace an HTML parser may add or
13
+ // drop without changing meaning. Trimming with String.prototype.trim would also
14
+ // treat a non-breaking space or other Unicode whitespace as ignorable, letting
15
+ // a visible text node the parser foster-parented out of the root (a stray
16
+ // ` ` a <table> spills before itself) slip past the single-root guard.
17
+ const ASCII_WHITESPACE_ONLY = /^[ \t\n\f\r]*$/;
18
+ const isIgnorableText = (node) => node.nodeName === '#text' &&
19
+ 'value' in node &&
20
+ ASCII_WHITESPACE_ONLY.test(node.value);
21
+ // The children a vnode declares, with consecutive text merged into one run.
22
+ // The serializer emits adjacent text children back to back, so the parser
23
+ // reads them as a single text node; merging both sides makes them comparable.
24
+ // A zero-length run is dropped: the serializer emits no node for empty text,
25
+ // so the parser produces none either.
26
+ const normalizeVnodeChildren = (vnode) => {
27
+ const children = vnode.children;
28
+ if (children === undefined) {
29
+ return [];
30
+ }
31
+ const items = [];
32
+ let text = '';
33
+ const flush = () => {
34
+ if (text !== '') {
35
+ items.push({ kind: 'Text', text });
36
+ }
37
+ text = '';
38
+ };
39
+ for (const child of children) {
40
+ if (typeof child === 'string') {
41
+ text += child;
42
+ continue;
43
+ }
44
+ const selector = child.sel;
45
+ if (selector === undefined || selector === '') {
46
+ text += child.text ?? '';
47
+ }
48
+ else if (selector === '!') {
49
+ flush();
50
+ items.push({ kind: 'Comment', text: child.text ?? '' });
51
+ }
52
+ else {
53
+ flush();
54
+ items.push({ kind: 'Element', vnode: child });
55
+ }
56
+ }
57
+ flush();
58
+ return items;
59
+ };
60
+ const normalizeParsedChildren = (node) => {
61
+ const items = [];
62
+ let text = '';
63
+ const flush = () => {
64
+ if (text !== '') {
65
+ items.push({ kind: 'Text', text });
66
+ }
67
+ text = '';
68
+ };
69
+ for (const child of node.childNodes) {
70
+ if (isParse5Element(child)) {
71
+ flush();
72
+ items.push({ kind: 'Element', element: child });
73
+ }
74
+ else if (child.nodeName === '#text' && 'value' in child) {
75
+ text += child.value;
76
+ }
77
+ else if (child.nodeName === '#comment' && 'data' in child) {
78
+ flush();
79
+ items.push({ kind: 'Comment', text: child.data });
80
+ }
81
+ }
82
+ flush();
83
+ return items;
84
+ };
85
+ // The children a controlled `<textarea>` or `<output>` serializes to. Both emit
86
+ // their `value` prop as text content (for a textarea the serializer's
87
+ // leading-newline padding and the parser's leading-newline strip cancel, so the
88
+ // parsed text is the value verbatim), which the walk represents as a single text
89
+ // run, empty value omitted. An uncontrolled element serializes its own children,
90
+ // so they are validated normally, rejecting element children the parser folds
91
+ // into text.
92
+ const expectedControlledChildren = (vnode) => {
93
+ const content = controlledValueContent(vnode.data?.props);
94
+ if (content === undefined) {
95
+ return normalizeVnodeChildren(vnode);
96
+ }
97
+ return content === '' ? [] : [{ kind: 'Text', text: content }];
98
+ };
99
+ // <noscript> and <template> parse their children into a place the differ never
100
+ // walks (noscript content is raw text while scripting is enabled; template
101
+ // children live in a separate content fragment), so a view that puts elements in
102
+ // either can never hydrate. They surface here as an ordinary child-structure
103
+ // mismatch, so name the real cause rather than the generic table guidance.
104
+ const structureMismatch = (parsed) => {
105
+ const tagName = parsed.tagName.toLowerCase();
106
+ if (tagName === 'noscript') {
107
+ return new Error('[foldkit] <noscript> content cannot be server-rendered as elements. A ' +
108
+ 'browser parses <noscript> as raw text while scripting is enabled, so ' +
109
+ 'the child elements the view declares arrive as one text node and ' +
110
+ 'hydration cannot converge. A <noscript> with plain text works; put ' +
111
+ 'richer fallback markup in the HTML shell instead.');
112
+ }
113
+ if (tagName === 'template') {
114
+ return new Error('[foldkit] <template> content cannot be server-rendered. A browser holds ' +
115
+ 'template children in a separate content fragment the differ does not ' +
116
+ 'walk, so hydration cannot reconcile them. Keep <template> markup in ' +
117
+ 'the HTML shell rather than the view.');
118
+ }
119
+ return new Error(`[foldkit] HTML parsing changed the child structure inside ` +
120
+ `<${parsed.tagName}>. It inserts, moves, or drops nodes the view did ` +
121
+ 'not write (a <tbody> around a bare <tr> in a <table>, text ' +
122
+ 'foster-parented out of a <table>), which hydration would rebuild as a ' +
123
+ 'mismatch. Write the structure HTML parsing produces, such as explicit ' +
124
+ 'table sections.');
125
+ };
126
+ // Compare the child structure the browser parsed against the structure the
127
+ // view declared, recursively. The top-level check rejects a root that splits
128
+ // into siblings; this rejects a parser correction inside the root, whether an
129
+ // inserted element (the `<tbody>` a browser adds around a bare `<tr>`) or
130
+ // foster-parented text, which hydration would otherwise see as a whole-subtree
131
+ // mismatch.
132
+ const assertStructureMatches = (parsed, vnode) => {
133
+ // InnerHTML owns an opaque, parser-produced subtree that the vnode does not
134
+ // model as children, so it is left unwalked.
135
+ if (vnode.data?.props?.['innerHTML'] !== undefined) {
136
+ return;
137
+ }
138
+ const parsedChildren = normalizeParsedChildren(parsed);
139
+ const parsedTag = parsed.tagName.toLowerCase();
140
+ const vnodeChildren = parsedTag === 'textarea' || parsedTag === 'output'
141
+ ? expectedControlledChildren(vnode)
142
+ : normalizeVnodeChildren(vnode);
143
+ if (parsedChildren.length !== vnodeChildren.length) {
144
+ throw structureMismatch(parsed);
145
+ }
146
+ for (const [parsedChild, vnodeChild] of Array_.zip(parsedChildren, vnodeChildren)) {
147
+ if (parsedChild.kind !== vnodeChild.kind) {
148
+ throw structureMismatch(parsed);
149
+ }
150
+ if (parsedChild.kind === 'Element' && vnodeChild.kind === 'Element') {
151
+ const expectedTag = tagNameFromSelector(vnodeChild.vnode.sel ?? '').toLowerCase();
152
+ const expectedNamespace = typeof vnodeChild.vnode.data?.ns === 'string'
153
+ ? vnodeChild.vnode.data.ns
154
+ : HTML_NAMESPACE;
155
+ if (parsedChild.element.tagName.toLowerCase() !== expectedTag ||
156
+ parsedChild.element.namespaceURI !== expectedNamespace) {
157
+ throw new Error(`[foldkit] HTML parsing produced <${parsedChild.element.tagName}> ` +
158
+ `where the view declared <${expectedTag}> inside ` +
159
+ `<${parsed.tagName}>. The browser inserts or reorders elements (a ` +
160
+ '<tbody> around a bare <tr> in a <table>) that hydration would ' +
161
+ 'rebuild as a mismatch. Write the structure HTML parsing produces.');
162
+ }
163
+ assertStructureMatches(parsedChild.element, vnodeChild.vnode);
164
+ }
165
+ else if (parsedChild.kind === 'Text' && vnodeChild.kind === 'Text') {
166
+ if (parsedChild.text !== vnodeChild.text) {
167
+ throw structureMismatch(parsed);
168
+ }
169
+ }
170
+ else if (parsedChild.kind === 'Comment' &&
171
+ vnodeChild.kind === 'Comment') {
172
+ if (parsedChild.text !== vnodeChild.text) {
173
+ throw structureMismatch(parsed);
174
+ }
175
+ }
176
+ }
177
+ };
178
+ // injectIntoTemplate splices the rendered root where <div id="root"></div> was,
179
+ // so a browser parses the served markup in a <div> (in body) insertion context.
180
+ // parseFragment defaults to a <template> context, whose insertion mode keeps
181
+ // table-section tags (<td>, <tr>, <caption>, ...) that an in-body parse
182
+ // foster-parents or drops. Reusing a real <div> context node makes this check
183
+ // model the true insertion point, so a table-section root is rejected here
184
+ // instead of silently escaping the application root on the served page.
185
+ const buildDivFragmentContext = () => {
186
+ const [onlyChild] = parseFragment('<div></div>').childNodes;
187
+ if (onlyChild === undefined || !isParse5Element(onlyChild)) {
188
+ throw new Error('[foldkit] internal: could not build the <div> parse context');
189
+ }
190
+ return onlyChild;
191
+ };
192
+ const DIV_FRAGMENT_CONTEXT = buildDivFragmentContext();
193
+ // After serialization, parse the stamped root markup with the same HTML parser
194
+ // a browser uses and require it to describe exactly one element: the stamped
195
+ // root, with the tag and namespace the view produced. A view can serialize a
196
+ // structurally invalid shape the parser rearranges (a block element inside a
197
+ // `<p>`, a stray element inside a `<table>`, an HTML element inside `<svg>`),
198
+ // moving nodes outside the element the client hydrates. Hydration owns only
199
+ // that root, so it cannot remove escaped siblings; rejecting here keeps the
200
+ // invariant that the served root parses back to the single intended element.
201
+ const assertSingleStampedRoot = (html, runtimeId, root) => {
202
+ const fragment = parseFragment(DIV_FRAGMENT_CONTEXT, html, {});
203
+ const significant = fragment.childNodes.filter(node => !isIgnorableText(node));
204
+ const only = significant[0];
205
+ if (significant.length !== 1 ||
206
+ only === undefined ||
207
+ !isParse5Element(only)) {
208
+ throw new Error('[foldkit] The rendered root serialized to markup that HTML parsing ' +
209
+ 'splits into more than one top-level node. An element the parser ' +
210
+ 'moves out of its parent (a block element inside a <p>, a stray ' +
211
+ 'element inside a <table>, or an HTML element inside an <svg>) ' +
212
+ 'leaves content outside the application root that hydration cannot ' +
213
+ 'own. Keep the view root a single, structurally valid element tree.');
214
+ }
215
+ const stamp = only.attrs.find(attribute => attribute.name === FOLDKIT_APP_ATTRIBUTE);
216
+ if (stamp?.value !== runtimeId) {
217
+ throw new Error('[foldkit] HTML parsing moved the hydration marker off the rendered ' +
218
+ 'root, so the served DOM would not carry the stamp the client ' +
219
+ 'adopts. Keep the view root a single, structurally valid element.');
220
+ }
221
+ const expectedTag = tagNameFromSelector(root.sel ?? '').toLowerCase();
222
+ const expectedNamespace = typeof root.data?.ns === 'string' ? root.data.ns : HTML_NAMESPACE;
223
+ if (only.tagName.toLowerCase() !== expectedTag ||
224
+ only.namespaceURI !== expectedNamespace) {
225
+ throw new Error('[foldkit] HTML parsing reinterpreted the rendered root as a ' +
226
+ `<${only.tagName}>, not the view's <${expectedTag}>. Keep the view ` +
227
+ 'root a single, structurally valid element.');
228
+ }
229
+ assertStructureMatches(only, root);
230
+ };
231
+ export { FOLDKIT_APP_ATTRIBUTE, FOLDKIT_FLAGS_ATTRIBUTE };
232
+ const DEFAULT_RUNTIME_ID = 'app';
233
+ /** Failure of a routing render whose `url` option cannot be parsed.
234
+ *
235
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
236
+ */
237
+ export class InvalidUrl extends Data.TaggedError('InvalidUrl') {
238
+ }
239
+ /** Failure producing the Flags payload: the Schema encode step rejected the
240
+ * Flags value, the encoded value could not be serialized to JSON, or the
241
+ * encoded value could not be decoded back for the hydration-consistent
242
+ * render.
243
+ *
244
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
245
+ */
246
+ export class FlagsEncodeError extends Data.TaggedError('FlagsEncodeError') {
247
+ }
248
+ /** Failure serializing the view-produced vnode tree to safe HTML.
249
+ *
250
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
251
+ */
252
+ export class SerializationError extends Data.TaggedError('SerializationError') {
253
+ }
254
+ /** Failure of a render whose `runtimeId` is empty.
255
+ *
256
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
257
+ */
258
+ export class InvalidRuntimeId extends Data.TaggedError('InvalidRuntimeId') {
259
+ }
260
+ /** Failure of a hydratable render whose view did not return an element root.
261
+ * Text, comments, and an empty body cannot carry the hydration marker the
262
+ * client runtime uses to adopt the server-rendered DOM.
263
+ *
264
+ * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
265
+ */
266
+ export class InvalidHydrationRoot extends Data.TaggedError('InvalidHydrationRoot') {
267
+ }
268
+ const noOpDispatch = () => { };
269
+ // NOTE: the html builder reads its dispatch context from a process-wide
270
+ // frame stack (`setRuntime` / `clearRuntime`) rather than an argument, so
271
+ // this push/render/pop bracket must never interleave with another render's.
272
+ // It cannot: JavaScript switches tasks only at async boundaries, and the
273
+ // bracket is fully synchronous (`view` returns a Document without awaiting),
274
+ // so it runs to completion before any other render can start. That atomicity
275
+ // is why no per-request context (AsyncLocalStorage) is needed. The Scene
276
+ // test harness and the client runtime drive a view the same way. A `view`
277
+ // that suspended mid-render would break the invariant; views are pure and
278
+ // cannot.
279
+ const runView = (view, model) => {
280
+ const boundaryRegistry = createBoundaryRegistry();
281
+ beginRender(boundaryRegistry);
282
+ setRuntime(noOpDispatch, Context.empty(), boundaryRegistry);
283
+ try {
284
+ return view(model, htmlBuilderFor());
285
+ }
286
+ finally {
287
+ clearRuntime();
288
+ }
289
+ };
290
+ // NOTE: `<` becomes `\u003c` inside the payload so no embedded value can
291
+ // form a `</script>` sequence and close the element early. The escape is
292
+ // JSON-native, so `JSON.parse` restores the original character during
293
+ // hydration.
294
+ const escapeJsonForScriptElement = (json) => json.replace(/</g, '\\u003c');
295
+ const flagsPayloadScript = (runtimeId, json) => `<script type="application/json" ${FOLDKIT_FLAGS_ATTRIBUTE}="${escapeAttributeValue(runtimeId)}">${escapeJsonForScriptElement(json)}</script>`;
296
+ const encodeFlagsHandoff = (FlagsCodec, flags, runtimeId) => Effect.gen(function* () {
297
+ const FlagsJsonCodec = Schema.toCodecJson(FlagsCodec);
298
+ const encodedFlags = yield* pipe(flags, Schema.encodeEffect(FlagsJsonCodec), Effect.mapError(cause => new FlagsEncodeError({ cause })));
299
+ const json = yield* Effect.try({
300
+ try: () => JSON.stringify(encodedFlags),
301
+ catch: cause => new FlagsEncodeError({ cause }),
302
+ });
303
+ if (!Predicate.isString(json)) {
304
+ return yield* Effect.fail(new FlagsEncodeError({
305
+ cause: new Error('Flags encoded to a value JSON cannot represent, so no payload can be embedded'),
306
+ }));
307
+ }
308
+ // NOTE: the hydrating client reconstructs Flags by parsing the payload
309
+ // JSON and decoding it synchronously, so the server render must call init
310
+ // with that same value. Decoding the in-memory encoded value instead would
311
+ // diverge wherever JSON is not the identity (a -0 serializes to 0, a
312
+ // non-finite number to null), and decoding through an Effect would accept an
313
+ // asynchronous codec the synchronous client cannot, deferring the failure to
314
+ // a client crash. Parsing json and decoding it the same synchronous way the
315
+ // client does avoids both: the served DOM matches the client's first render,
316
+ // and an incompatible codec fails here as a typed error.
317
+ const hydrationFlags = yield* Effect.try({
318
+ try: () => Schema.decodeUnknownSync(FlagsJsonCodec)(JSON.parse(json)),
319
+ catch: cause => new FlagsEncodeError({ cause }),
320
+ });
321
+ return {
322
+ payloadScript: flagsPayloadScript(runtimeId, json),
323
+ hydrationFlags,
324
+ };
325
+ });
326
+ const parseUrl = (url) => Option.match(fromString(url), {
327
+ onNone: () => Effect.fail(new InvalidUrl({ url })),
328
+ onSome: Effect.succeed,
329
+ });
330
+ // The client defaults canonical to `origin + pathname + search` of the current
331
+ // location, which drops the fragment and normalizes host case and default
332
+ // ports. Building the server default with the WHATWG URL parser reproduces that
333
+ // exact string, so the metadata a crawler reads before hydration matches what
334
+ // the hydrated page computes.
335
+ const normalizedRequestUrl = (rawUrl) => {
336
+ if (rawUrl === undefined) {
337
+ return undefined;
338
+ }
339
+ try {
340
+ const parsed = new URL(rawUrl);
341
+ return `${parsed.origin}${parsed.pathname}${parsed.search}`;
342
+ }
343
+ catch {
344
+ return undefined;
345
+ }
346
+ };
347
+ const validateHydrationRoot = (body) => {
348
+ if (body === null) {
349
+ return Effect.fail(new InvalidHydrationRoot({ rootKind: 'Empty' }));
350
+ }
351
+ if (body.sel === undefined || body.sel === '') {
352
+ return Effect.fail(new InvalidHydrationRoot({ rootKind: 'Text' }));
353
+ }
354
+ if (body.sel === '!') {
355
+ return Effect.fail(new InvalidHydrationRoot({ rootKind: 'Comment' }));
356
+ }
357
+ return Effect.void;
358
+ };
359
+ export function renderToString(config, options) {
360
+ return Effect.gen(function* () {
361
+ const runtimeId = options?.runtimeId ?? DEFAULT_RUNTIME_ID;
362
+ if (runtimeId === '') {
363
+ return yield* Effect.fail(new InvalidRuntimeId({
364
+ runtimeId,
365
+ }));
366
+ }
367
+ const hasRouting = config.routing !== undefined;
368
+ const FlagsCodec = config.Flags;
369
+ const isHydratable = options?.isHydratable ?? true;
370
+ const url = hasRouting ? yield* parseUrl(options?.url ?? '') : undefined;
371
+ const flagsHandoff = isHydratable && FlagsCodec !== undefined
372
+ ? yield* encodeFlagsHandoff(FlagsCodec, options?.flags, runtimeId)
373
+ : undefined;
374
+ const flagsForInit = flagsHandoff !== undefined ? flagsHandoff.hydrationFlags : options?.flags;
375
+ const initReturn = (() => {
376
+ if (FlagsCodec !== undefined) {
377
+ return hasRouting
378
+ ? config.init(flagsForInit, url)
379
+ : config.init(flagsForInit);
380
+ }
381
+ return hasRouting ? config.init(url) : config.init();
382
+ })();
383
+ const [model] = initReturn;
384
+ const nextDocument = runView(config.view, model);
385
+ if (isHydratable) {
386
+ yield* validateHydrationRoot(nextDocument.body);
387
+ }
388
+ const rootHtml = yield* Effect.try({
389
+ try: () => {
390
+ const html = serializeHtml(nextDocument.body, isHydratable
391
+ ? { rootAttributes: { [FOLDKIT_APP_ATTRIBUTE]: runtimeId } }
392
+ : {});
393
+ if (isHydratable && nextDocument.body !== null) {
394
+ assertSingleStampedRoot(html, runtimeId, nextDocument.body);
395
+ }
396
+ return html;
397
+ },
398
+ catch: cause => new SerializationError({ cause }),
399
+ });
400
+ const flagsPayload = flagsHandoff !== undefined ? flagsHandoff.payloadScript : '';
401
+ // Mirror the client's document-metadata defaults so the served HTML a
402
+ // crawler reads carries the same canonical and Open Graph URL the hydrated
403
+ // page computes: canonical falls back to the request URL, and ogUrl to the
404
+ // resolved canonical, the chain the runtime applies on the client. A
405
+ // non-routing render has no request URL, so it inherits only an explicitly
406
+ // set canonical.
407
+ const resolvedCanonical = nextDocument.canonical ??
408
+ (hasRouting ? normalizedRequestUrl(options?.url) : undefined);
409
+ const resolvedOgUrl = nextDocument.ogUrl ?? resolvedCanonical;
410
+ return {
411
+ html: `${rootHtml}${flagsPayload}`,
412
+ title: nextDocument.title,
413
+ ...(nextDocument.lang !== undefined ? { lang: nextDocument.lang } : {}),
414
+ ...(nextDocument.dir !== undefined
415
+ ? { dir: textDirectionToAttribute(nextDocument.dir) }
416
+ : {}),
417
+ ...(resolvedCanonical !== undefined
418
+ ? { canonical: resolvedCanonical }
419
+ : {}),
420
+ ...(resolvedOgUrl !== undefined ? { ogUrl: resolvedOgUrl } : {}),
421
+ };
422
+ });
423
+ }