@uniflowed/router 0.0.0-alpha.8 → 0.1.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 (49) hide show
  1. package/action.js +324 -0
  2. package/client.js +261 -7
  3. package/handler.js +113 -99
  4. package/http-client.js +104 -0
  5. package/index.js +49 -6
  6. package/instrumentation.js +92 -0
  7. package/internal/action-endpoint.js +438 -0
  8. package/internal/action-wire.js +608 -0
  9. package/internal/base-path.js +175 -0
  10. package/internal/boundaries.js +481 -0
  11. package/internal/boundary-data.js +88 -0
  12. package/internal/compose.js +490 -0
  13. package/internal/deployment.js +160 -0
  14. package/internal/devtools.js +131 -0
  15. package/internal/diagnostics.js +169 -0
  16. package/internal/error-view.js +193 -0
  17. package/internal/flight-browser.js +242 -0
  18. package/internal/flight-chunks.js +205 -0
  19. package/internal/flight-rows.js +135 -0
  20. package/internal/flight-ssr.js +91 -0
  21. package/internal/flight.js +192 -0
  22. package/internal/head.js +219 -0
  23. package/internal/hydration.js +1085 -0
  24. package/internal/inspector.js +626 -0
  25. package/internal/native-links.js +67 -0
  26. package/internal/native-tree.js +89 -0
  27. package/internal/navigation-cache.js +181 -0
  28. package/internal/payload-rows.js +270 -0
  29. package/internal/payload.js +685 -0
  30. package/internal/prepare-document.js +54 -0
  31. package/internal/react-version.js +77 -0
  32. package/internal/resolve.js +1617 -0
  33. package/internal/resolved-summary.js +199 -0
  34. package/internal/routing.js +478 -0
  35. package/internal/runtime.js +1597 -1329
  36. package/internal/server-instrumentation.js +12 -0
  37. package/internal/server-route.js +58 -0
  38. package/internal/shell.js +125 -0
  39. package/internal/stream.js +754 -21
  40. package/middleware.js +161 -22
  41. package/native-navigation.js +217 -0
  42. package/native.js +416 -0
  43. package/package.json +48 -7
  44. package/routing.js +51 -0
  45. package/rsc-client.js +120 -0
  46. package/rsc-ssr.js +637 -0
  47. package/rsc.js +402 -0
  48. package/server-components.js +159 -0
  49. package/server.js +254 -106
package/server.js CHANGED
@@ -18,23 +18,26 @@
18
18
  //
19
19
  // They were one function with `renderToString` behind it, which answered the
20
20
  // first question by giving up on it: nothing streamed, so nothing could
21
- // usefully suspend, so `_uf.loading.js` had nothing to be. Making the split
21
+ // usefully suspend, so `$loading.js` had nothing to be. Making the split
22
22
  // explicit is the point of ubugeeei-prod/uf#254 rather than a side effect —
23
23
  // `internal/stream.js` holds the mechanics and says which React renderer serves
24
24
  // which.
25
25
 
26
- import { DATA_ID, ROOT_ID } from "./internal/document.js";
26
+ import { currentNonce, noteRoute } from "@uniflowed/server/host";
27
+ import { traceLoader } from "./internal/server-instrumentation.js";
28
+
27
29
  import * as React from "react";
28
30
 
29
31
  import {
30
32
  type DocumentBody,
31
- type DocumentShell,
33
+ type PrerenderedShell,
32
34
  type WritableLike,
33
- bodyOfText,
34
35
  prerenderDocument,
35
36
  renderDocument,
36
37
  } from "./internal/stream.js";
37
38
 
39
+ export type { PrerenderedShell } from "./internal/stream.js";
40
+
38
41
  import {
39
42
  type AppProps,
40
43
  type ResolvedRoute,
@@ -45,11 +48,21 @@ import {
45
48
  resolveMatch,
46
49
  } from "./internal/runtime.js";
47
50
 
51
+ import { type StreamDiagnostic, streamReporter } from "./internal/inspector.js";
52
+
53
+ import { redirectDocument, redirectResult, shellFor } from "./internal/shell.js";
54
+
48
55
  /** Asset URLs to reference from the document. */
49
56
  export type RenderAssets = {|
50
57
  readonly scripts: $ReadOnlyArray<string>,
51
58
  readonly styles: $ReadOnlyArray<string>,
52
59
  readonly preloads: $ReadOnlyArray<string>,
60
+ /**
61
+ * The build the document belongs to, written into its head as
62
+ * `<meta name="uf:deployment">`. Absent under `uf dev`, where there is no
63
+ * other build to be skewed against. See `./internal/deployment.js`.
64
+ */
65
+ readonly deployment?: string,
53
66
  |};
54
67
 
55
68
  /**
@@ -98,6 +111,42 @@ export type PrerenderResult = {|
98
111
  readonly headers?: { readonly [string]: string },
99
112
  /** The exception this render fell back to its error boundary for; see [`RenderResult`]. */
100
113
  readonly error?: mixed,
114
+ /**
115
+ * The Flight payload the document was rendered from, for a document React
116
+ * Server Components rendered.
117
+ *
118
+ * `uf build` writes it beside the document as the route's payload file, so a
119
+ * browser that navigates to a prerendered route fetches a file rather than
120
+ * asking a server — which is what makes a static host able to serve client
121
+ * navigation at all. Absent for a document rendered from its modules.
122
+ */
123
+ readonly payload?: Uint8Array,
124
+ /**
125
+ * The page's static shell, when the prerender was partial and the page read
126
+ * the request inside a `<Suspense>` boundary.
127
+ *
128
+ * `html` is then the shell's markup and not a document to write at the
129
+ * page's URL: a server answers the page by sending the shell and rendering
130
+ * the holes per request, through [`Renderer`]'s `resume`. No payload comes
131
+ * with it, because the browser hydrates from the request's. Only a renderer
132
+ * for React Server Components writes one.
133
+ */
134
+ readonly shell?: PrerenderedShell,
135
+ |};
136
+
137
+ /**
138
+ * A route's payload, as a browser navigating to it is answered.
139
+ *
140
+ * `stream` is `null` for a redirect, whose `location` is already the target's
141
+ * payload URL when the target is on this origin: `fetch` follows it and lands
142
+ * on a payload.
143
+ */
144
+ export type FlightResponse = {|
145
+ readonly status: number,
146
+ readonly headers: { readonly [string]: string },
147
+ readonly stream: ReadableStream<Uint8Array> | null,
148
+ /** The exception the route resolved to its error boundary for; see [`RenderResult`]. */
149
+ readonly error?: mixed,
101
150
  |};
102
151
 
103
152
  /** What a host may tell the renderer about one request. */
@@ -111,6 +160,76 @@ export type RenderOptions = {|
111
160
  * bytes. `uf dev` reports them in the terminal; a production host logs them.
112
161
  */
113
162
  readonly onError?: (error: mixed) => void,
163
+ /**
164
+ * Rewrite the document opening — the head and, when present, the body start
165
+ * tag — before it goes out.
166
+ *
167
+ * For `uf dev` and nothing else. Vite's `transformIndexHtml` injects
168
+ * `/@vite/client` and the refresh preamble and rewrites asset URLs, and it
169
+ * is a *whole document* hook, so the development server used to collect the
170
+ * page and transform it at the end. That made the one place a developer
171
+ * would notice streaming the one place it did not happen: a slow page showed
172
+ * nothing until it was finished, and `$loading.js` looked broken.
173
+ * See ubugeeei-prod/uf#374.
174
+ *
175
+ * A production host passes nothing here and streams as it always did.
176
+ *
177
+ * # What a plugin that injects into the body gets
178
+ *
179
+ * `transformIndexHtml` is a whole-document hook and this hands it only the
180
+ * parseable opening of the document. Measured against Vite 8.2.2, injecting
181
+ * all four positions into a whole document and into the streamed opening:
182
+ *
183
+ * | `injectTo` | whole document | streamed |
184
+ * | -------------- | ------------------- | -------- |
185
+ * | `head-prepend` | after `<head>` | same |
186
+ * | `head` | before `</head>` | same |
187
+ * | `body-prepend` | after `<body>` | same |
188
+ * | `body` | before `</body>` | after `<body>` |
189
+ *
190
+ * Nothing is dropped — every tag still reaches the document — but a `body`
191
+ * tag lands at the top of the body rather than after the content, because
192
+ * the content is deliberately not passed to the hook. That keeps Vite's
193
+ * parser away from chunk boundaries that may sit inside an attribute.
194
+ *
195
+ * uf's own injections are `head` and `head-prepend`, and Vite's client is
196
+ * head-injected, so this is about a third-party plugin.
197
+ * `packages/vite/dev-head-transform.test.js` pins the table above, so the day
198
+ * it changes is a failing test rather than a surprise.
199
+ */
200
+ readonly transformHead?: (html: string) => Promise<string>,
201
+ /**
202
+ * Told, in words, when a document streamed differently than it did last time.
203
+ *
204
+ * For `uf dev` and nothing else, like `transformHead` above. It answers the
205
+ * half of ubugeeei-prod/uf#520 that is about the wire — what arrived, in what
206
+ * order, and which part of the tree each chunk built — for the stream uf has
207
+ * today, which is a document whose Suspense boundaries resolve independently.
208
+ * `internal/inspector.js` is what it is and what it deliberately is not.
209
+ *
210
+ * A host that passes nothing here records nothing: no recorder is
211
+ * constructed, and the chunks a production stream yields are untouched.
212
+ *
213
+ * It is handed a message and its detail lines rather than the record they
214
+ * came from, because the caller is `@uniflowed/vite` — plain JavaScript, run
215
+ * by Vite before any Flow transform exists, which is why `DEVTOOLS_HOOK` and
216
+ * `DIAGNOSTIC_ENDPOINT` are spelled twice rather than imported. The
217
+ * vocabulary of the report belongs on this side of that line.
218
+ */
219
+ readonly onStream?: (diagnostic: StreamDiagnostic) => void,
220
+ /**
221
+ * For `prerender` only: whether a read of the request may be left for the
222
+ * request rather than fail the page.
223
+ *
224
+ * `uf build` passes it when `app.rendering.modes` allows `ppr` and the build
225
+ * leaves a server behind. A page that reads `cookies()`, `headers()` or
226
+ * `draftMode()` inside a `<Suspense>` boundary is then written as a static
227
+ * shell with that boundary as a hole — [`PrerenderResult`]'s `shell` — and a
228
+ * read outside every boundary still fails the page, naming what it read.
229
+ * The renderer for React Server Components honours it; a route rendered from
230
+ * its modules (`app.rsc: false`) is prerendered whole or not at all.
231
+ */
232
+ readonly partial?: boolean,
114
233
  |};
115
234
 
116
235
  /** The two ids the server writes and the client reads. */
@@ -121,13 +240,13 @@ export { DATA_ID, ROOT_ID } from "./internal/document.js";
121
240
  *
122
241
  * Re-exported rather than left to the host to import, and the reason is the
123
242
  * one thing about `@uniflowed/server` that is easy to get wrong: the request
124
- * lives in an `AsyncLocalStorage` belonging to *that module instance*. A host
125
- * that resolved `@uniflowed/server/host` for itself — from its own
126
- * `node_modules`, or from outside the bundle a build produced — would begin a
127
- * request in a second storage, and every `cookies()` in the application would
128
- * still be outside one, silently. Handing it out from here makes the copy the
129
- * host begins with the copy this module dispatches and renders with, because
130
- * it is the same import.
243
+ * store is shared by every copy of one *release* of that package, and no more.
244
+ * A host that resolved `@uniflowed/server/host` for itself — from its own
245
+ * `node_modules`, or from outside the bundle a build produced — may hold a
246
+ * different release, and would begin a request in a store the application
247
+ * never reads, so every `cookies()` in it would still be outside one, silently.
248
+ * Handing it out from here makes the copy the host begins with the copy this
249
+ * module dispatches and renders with, because it is the same import.
131
250
  *
132
251
  * `run` wraps everything that decides the response; `settle` is called once
133
252
  * the response has been *written*, which is a different line in every host.
@@ -148,6 +267,31 @@ export type {
148
267
  } from "./middleware.js";
149
268
  export { createMiddlewareRunner } from "./middleware.js";
150
269
 
270
+ /**
271
+ * `app.router.basePath` and `trailingSlash`, for `virtual:uf/server` to install
272
+ * before the first render, and `basePath()` for a middleware or a route handler
273
+ * that builds an address itself. See `./internal/base-path.js`.
274
+ */
275
+ export type { RoutingSettings, TrailingSlash } from "./internal/base-path.js";
276
+ export { basePath, installRouting } from "./internal/base-path.js";
277
+
278
+ /**
279
+ * The endpoint a `"use server"` export is dialled at.
280
+ *
281
+ * Here rather than beside `@uniflowed/router/action`, which is the browser's
282
+ * half of the same feature and must stay reachable from a client component:
283
+ * this one refuses outside a request, so it imports `internal/request.js` and
284
+ * through it `node:async_hooks`. The two halves share `internal/action-wire.js`
285
+ * and nothing else, which is what keeps one grammar rather than two.
286
+ *
287
+ * `virtual:uf/server` calls it with the table `virtual:uf/actions` built from
288
+ * the RSC manifest, and every host runs it between the middleware and the
289
+ * route handlers. See `internal/action-endpoint.js` for what the endpoint
290
+ * refuses and why.
291
+ */
292
+ export type { ActionModule, ActionRecord } from "./internal/action-endpoint.js";
293
+ export { createActionDispatcher } from "./internal/action-endpoint.js";
294
+
151
295
  /**
152
296
  * What a URL turned out to be: a route to render, or a redirect to answer with.
153
297
  *
@@ -160,15 +304,6 @@ type Resolution =
160
304
  | {| readonly kind: "route", readonly route: ResolvedRoute |}
161
305
  | {| readonly kind: "redirect", readonly error: RedirectError |};
162
306
 
163
- /** A redirect, as the finished document `prerender` answers with. */
164
- async function redirectResult(document: RenderResult): Promise<PrerenderResult> {
165
- return {
166
- status: document.status,
167
- headers: document.headers,
168
- html: await document.text(),
169
- };
170
- }
171
-
172
307
  /** The two ways one app answers for a URL. */
173
308
  export type Renderer = {|
174
309
  readonly render: (
@@ -181,6 +316,28 @@ export type Renderer = {|
181
316
  assets: RenderAssets,
182
317
  options?: RenderOptions,
183
318
  ) => Promise<PrerenderResult>,
319
+ /**
320
+ * A route's payload, for a browser that is navigating rather than loading a
321
+ * document. Only a renderer for React Server Components has one.
322
+ */
323
+ readonly flight?: (
324
+ url: string,
325
+ options?: {|
326
+ readonly onError?: (error: mixed) => void,
327
+ readonly interceptedFrom?: string,
328
+ |},
329
+ ) => Promise<FlightResponse>,
330
+ /**
331
+ * A page `prerender` wrote as a static shell: the shell first, then its holes
332
+ * as this request renders them. Only a renderer for React Server Components
333
+ * has one, because only it writes a shell.
334
+ */
335
+ readonly resume?: (
336
+ url: string,
337
+ assets: RenderAssets,
338
+ shell: PrerenderedShell,
339
+ options?: RenderOptions,
340
+ ) => Promise<RenderResult>,
184
341
  |};
185
342
 
186
343
  export function createRenderer(options: {|
@@ -201,13 +358,35 @@ export function createRenderer(options: {|
201
358
  * The route to render, or the redirect to answer with instead.
202
359
  *
203
360
  * Shared by both entry points, because *what* a URL resolves to has nothing
204
- * to do with how the answer is delivered. Returning the redirect rather than
205
- * throwing it keeps the two callers from each having to remember that a
206
- * redirect is the one thing `resolveMatch` lets out.
361
+ * to do with how the answer is delivered — with one exception, which is
362
+ * `defer` and is the exception that proves it. Whether the router may hand
363
+ * the page a loader that has not answered yet *is* a question about delivery:
364
+ * only a renderer with a `<Suspense>` fallback to send first has anywhere to
365
+ * put the wait. `render` says yes and `prerender` says no; see
366
+ * `ResolveOptions.defer` and ubugeeei-prod/uf#373.
367
+ *
368
+ * Returning the redirect rather than throwing it keeps the two callers from
369
+ * each having to remember that a redirect is the one thing `resolveMatch`
370
+ * lets out.
371
+ *
372
+ * `onMatch` is what makes the request's log line say `/orders/:id` rather
373
+ * than `/orders/8813`. It is handed to `resolveMatch` rather than read off
374
+ * the route this returns, because a loader runs *inside* that call and a
375
+ * loader has things to log: recording the route afterwards would leave every
376
+ * line the loader wrote claiming to belong to no route at all. `noteRoute`
377
+ * does nothing outside a request, which is what lets `prerender` — a build,
378
+ * with no request anywhere — call the same function.
207
379
  */
208
- async function resolve(url: string): Promise<Resolution> {
380
+ async function resolve(url: string, defer: boolean): Promise<Resolution> {
209
381
  try {
210
- return { kind: "route", route: await resolveMatch(table, url) };
382
+ return {
383
+ kind: "route",
384
+ route: await resolveMatch(table, url, {
385
+ defer,
386
+ onMatch: noteRoute,
387
+ runLoader: traceLoader,
388
+ }),
389
+ };
211
390
  } catch (error) {
212
391
  if (error instanceof RedirectError) {
213
392
  return { kind: "redirect", error };
@@ -221,12 +400,22 @@ export function createRenderer(options: {|
221
400
  assets: RenderAssets,
222
401
  settings?: RenderOptions,
223
402
  ): Promise<RenderResult> {
224
- const resolution = await resolve(url);
403
+ const resolution = await resolve(url, true);
225
404
  if (resolution.kind === "redirect") {
226
405
  return redirectDocument(resolution.error);
227
406
  }
228
407
  let resolved: ResolvedRoute = resolution.route;
229
408
  const report = settings?.onError ?? (() => {});
409
+ // Built once and shared by both renders below, so a page that threw its
410
+ // shell away and rendered its error boundary instead reports the stream the
411
+ // browser was actually sent rather than the one that was abandoned.
412
+ const send = settings?.onStream;
413
+ const onStream = send == null ? undefined : streamReporter(url, send);
414
+ // Read rather than minted: a project that has not asked for a nonce gets
415
+ // `null` and the document it has always had. Read once for both renders
416
+ // below, so the error document a failed shell falls back to carries the
417
+ // same nonce as the policy already on the response.
418
+ const nonce = currentNonce();
230
419
 
231
420
  // React reports an exception to `onError` *and*, if it was in the shell, to
232
421
  // `onShellError` — so forwarding both would tell the host about one failure
@@ -247,8 +436,11 @@ export function createRenderer(options: {|
247
436
  let body: DocumentBody;
248
437
  try {
249
438
  body = await renderDocument(<App url={url} initial={resolved} />, {
250
- shell: shellFor(resolved, assets),
439
+ shell: shellFor(assets, nonce),
251
440
  onError,
441
+ transformHead: settings?.transformHead,
442
+ onStream,
443
+ nonce,
252
444
  });
253
445
  streaming = true;
254
446
  // Recovered before the shell was ready: a `<Suspense>` boundary whose
@@ -277,8 +469,11 @@ export function createRenderer(options: {|
277
469
  // where somebody can fix it.
278
470
  streaming = true;
279
471
  body = await renderDocument(<App url={url} initial={resolved} />, {
280
- shell: shellFor(resolved, assets),
472
+ shell: shellFor(assets, nonce),
281
473
  onError,
474
+ transformHead: settings?.transformHead,
475
+ onStream,
476
+ nonce,
282
477
  });
283
478
  }
284
479
 
@@ -296,7 +491,7 @@ export function createRenderer(options: {|
296
491
  assets: RenderAssets,
297
492
  settings?: RenderOptions,
298
493
  ): Promise<PrerenderResult> {
299
- const resolution = await resolve(url);
494
+ const resolution = await resolve(url, false);
300
495
  if (resolution.kind === "redirect") {
301
496
  return redirectResult(redirectDocument(resolution.error));
302
497
  }
@@ -306,7 +501,7 @@ export function createRenderer(options: {|
306
501
  let html: string;
307
502
  try {
308
503
  html = await prerenderDocument(<App url={url} initial={resolved} />, {
309
- shell: shellFor(resolved, assets),
504
+ shell: shellFor(assets),
310
505
  onError: report,
311
506
  });
312
507
  } catch (error) {
@@ -315,7 +510,7 @@ export function createRenderer(options: {|
315
510
  }
316
511
  resolved = await resolveFailure(table, url, error);
317
512
  html = await prerenderDocument(<App url={url} initial={resolved} />, {
318
- shell: shellFor(resolved, assets),
513
+ shell: shellFor(assets),
319
514
  onError: report,
320
515
  });
321
516
  }
@@ -338,85 +533,38 @@ function renderFailure(resolved: ResolvedRoute): mixed {
338
533
  };
339
534
  }
340
535
 
341
- function redirectDocument(error: RedirectError): RenderResult {
342
- const target = escapeAttribute(error.to);
343
- // A document rather than an empty body, because a redirect is still an answer
344
- // a browser may be shown; it goes through the same three methods as a
345
- // rendered one so that a host has one shape to write, not two.
346
- const body = bodyOfText(
347
- `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0; url=${target}"><title>Redirecting</title></head><body><a href="${target}">Redirecting…</a></body></html>\n`,
348
- );
349
- return {
350
- status: error.permanent ? 308 : 307,
351
- headers: { Location: error.to },
352
- pipe: body.pipe,
353
- stream: body.stream,
354
- text: body.text,
355
- };
356
- }
357
-
358
536
  /**
359
- * The document uf writes around the app's markup.
537
+ * The document a single-page build writes, and the only one it writes.
360
538
  *
361
- * The same two shapes `assemble` chose between, decided from the same evidence
362
- * — whether the markup opens with `<html>` — but stated up front instead of
363
- * afterwards, because a stream has no "afterwards" in which to splice a head.
364
- * An app whose root layout renders `<html>` owns the whole document and the
365
- * client hydrates `document`, so uf contributes only the tags that go before
366
- * `</head>`. An app that renders only content is wrapped in a minimal shell
367
- * around `<div id="uf-root">`, which is what the client hydrates instead.
539
+ * `app.rendering.modes: ["csr"]` renders no route at build time: the client
540
+ * router resolves and renders every one of them in the browser, so what the
541
+ * build has to leave behind is the *chrome* — the stylesheets, the module
542
+ * script, and the empty root the client renders into. That is exactly
543
+ * [`shellFor`]'s three strings with nothing between them, which is why this is
544
+ * three concatenations rather than a fourth shape of document to keep in step
545
+ * with the other three.
368
546
  *
369
- * `internal/stream.js` picks between them on the opening bytes React writes;
370
- * everything either shape is made of is here, so what a uf document contains is
371
- * still readable in one place.
372
- */
373
- function shellFor(resolved: ResolvedRoute, assets: RenderAssets): DocumentShell {
374
- const head = headTags(assets) + dataScript(resolved.data);
375
- const title =
376
- resolved.metadata.title != null ? `<title>${escapeText(resolved.metadata.title)}</title>` : "";
377
- return {
378
- head,
379
- open: `<!doctype html>\n<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${title}${head}</head><body><div id="${ROOT_ID}">`,
380
- close: `</div></body></html>\n`,
381
- };
382
- }
383
-
384
- function headTags(assets: RenderAssets): string {
385
- let tags = "";
386
- for (const href of assets.styles) {
387
- tags += `<link rel="stylesheet" href="${escapeAttribute(href)}">`;
388
- }
389
- for (const href of assets.preloads) {
390
- tags += `<link rel="modulepreload" href="${escapeAttribute(href)}">`;
391
- }
392
- for (const src of assets.scripts) {
393
- tags += `<script type="module" src="${escapeAttribute(src)}"></script>`;
394
- }
395
- return tags;
396
- }
397
-
398
- /**
399
- * The loader data, embedded for hydration.
547
+ * No React runs. There is nothing to render: no URL has been asked for, and
548
+ * whatever this document is served for is decided by the host rather than by
549
+ * this build.
550
+ *
551
+ * # What it costs, said here because it is not visible from the file
400
552
  *
401
- * `<` is escaped inside the JSON so a string holding `</script>` cannot end
402
- * the element early, and the script's type keeps the browser from executing
403
- * it.
553
+ * The document has no `<title>`, no `<meta name="description">` and no content.
554
+ * A crawler that runs no JavaScript sees an empty page for **every** URL, and a
555
+ * reader sees nothing until the bundle has loaded and the route has resolved.
556
+ * That is what a single-page application is, and it is why `modes: ["csr"]` is
557
+ * a declaration a project makes rather than something a build falls back to.
558
+ * A project that wants a document per route has `ssg`, and one that wants a
559
+ * document per request has `ssr`.
404
560
  */
405
- function dataScript(data: mixed): string {
406
- if (data === undefined) {
407
- return "";
408
- }
409
- const json = JSON.stringify(data)
410
- .replace(/</g, "\\u003c")
411
- .replace(/\u2028/g, "\\u2028")
412
- .replace(/\u2029/g, "\\u2029");
413
- return `<script id="${DATA_ID}" type="application/json">${json}</script>`;
414
- }
415
-
416
- function escapeAttribute(value: string): string {
417
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
561
+ export function shellDocument(assets: RenderAssets): string {
562
+ const shell = shellFor(assets);
563
+ return `${shell.open}${shell.body}${shell.close}`;
418
564
  }
419
565
 
420
- function escapeText(value: string): string {
421
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
422
- }
566
+ export {
567
+ createInstrumentation,
568
+ instrumentRender,
569
+ traceRequestPhase,
570
+ } from "@uniflowed/server/instrumentation";