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.
- package/README.md +5 -2
- package/dist/customElement/index.d.ts.map +1 -1
- package/dist/customElement/index.js +23 -0
- package/dist/experimental/index.d.ts +1 -0
- package/dist/experimental/index.d.ts.map +1 -1
- package/dist/experimental/index.js +1 -0
- package/dist/experimental/machine/machine.d.ts +22 -3
- package/dist/experimental/machine/machine.d.ts.map +1 -1
- package/dist/experimental/machine/machine.js +8 -0
- package/dist/experimental/server/entry.d.ts +73 -0
- package/dist/experimental/server/entry.d.ts.map +1 -0
- package/dist/experimental/server/entry.js +41 -0
- package/dist/experimental/server/host.d.ts +42 -0
- package/dist/experimental/server/host.d.ts.map +1 -0
- package/dist/experimental/server/host.js +169 -0
- package/dist/experimental/server/index.d.ts +5 -0
- package/dist/experimental/server/index.d.ts.map +1 -0
- package/dist/experimental/server/index.js +4 -0
- package/dist/experimental/server/public.d.ts +3 -0
- package/dist/experimental/server/public.d.ts.map +1 -0
- package/dist/experimental/server/public.js +1 -0
- package/dist/experimental/server/serialize.d.ts +33 -0
- package/dist/experimental/server/serialize.d.ts.map +1 -0
- package/dist/experimental/server/serialize.js +563 -0
- package/dist/experimental/server/server.d.ts +201 -0
- package/dist/experimental/server/server.d.ts.map +1 -0
- package/dist/experimental/server/server.js +423 -0
- package/dist/experimental/server/template.d.ts +45 -0
- package/dist/experimental/server/template.d.ts.map +1 -0
- package/dist/experimental/server/template.js +182 -0
- package/dist/html/index.d.ts +5 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +38 -4
- package/dist/hydrate.d.ts +4 -0
- package/dist/hydrate.d.ts.map +1 -0
- package/dist/hydrate.js +522 -0
- package/dist/hydrationMarker.d.ts +10 -0
- package/dist/hydrationMarker.d.ts.map +1 -0
- package/dist/hydrationMarker.js +9 -0
- package/dist/mount/index.d.ts +1 -1
- package/dist/runtime/public.d.ts +2 -2
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/public.js +1 -1
- package/dist/runtime/runtime.d.ts +75 -36
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +209 -71
- package/dist/snabbdom/h.d.ts +1 -0
- package/dist/snabbdom/h.d.ts.map +1 -1
- package/dist/snabbdom/h.js +85 -4
- package/dist/snabbdom/tovnode.d.ts.map +1 -1
- package/dist/snabbdom/tovnode.js +5 -1
- package/dist/tagName.d.ts +6 -0
- package/dist/tagName.d.ts.map +1 -0
- package/dist/tagName.js +11 -0
- package/dist/vdom.d.ts.map +1 -1
- package/dist/vdom.js +25 -1
- 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
|
+
}
|