@uniflowed/router 0.0.0-alpha.9 → 0.2.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/action.js +344 -0
- package/client.js +263 -7
- package/handler.js +113 -99
- package/http-client.js +104 -0
- package/index.js +49 -6
- package/instrumentation.js +92 -0
- package/internal/action-endpoint.js +646 -0
- package/internal/action-wire.js +608 -0
- package/internal/base-path.js +175 -0
- package/internal/boundaries.js +481 -0
- package/internal/boundary-data.js +88 -0
- package/internal/compose.js +490 -0
- package/internal/deployment.js +160 -0
- package/internal/devtools.js +131 -0
- package/internal/diagnostics.js +169 -0
- package/internal/error-view.js +193 -0
- package/internal/flight-browser.js +242 -0
- package/internal/flight-chunks.js +205 -0
- package/internal/flight-rows.js +135 -0
- package/internal/flight-ssr.js +91 -0
- package/internal/flight.js +192 -0
- package/internal/form-action.js +243 -0
- package/internal/head.js +219 -0
- package/internal/hydrate-options.js +38 -0
- package/internal/hydration.js +1085 -0
- package/internal/inspector.js +626 -0
- package/internal/native-links.js +67 -0
- package/internal/native-tree.js +89 -0
- package/internal/navigation-cache.js +181 -0
- package/internal/payload-rows.js +270 -0
- package/internal/payload.js +685 -0
- package/internal/prepare-document.js +54 -0
- package/internal/react-version.js +77 -0
- package/internal/resolve.js +1617 -0
- package/internal/resolved-summary.js +199 -0
- package/internal/routing.js +478 -0
- package/internal/runtime.js +1593 -1341
- package/internal/server-instrumentation.js +12 -0
- package/internal/server-route.js +58 -0
- package/internal/shell.js +132 -0
- package/internal/stream.js +766 -21
- package/middleware.js +274 -22
- package/native-navigation.js +217 -0
- package/native.js +416 -0
- package/package.json +48 -7
- package/routing.js +51 -0
- package/rsc-client.js +122 -0
- package/rsc-ssr.js +641 -0
- package/rsc.js +402 -0
- package/server-components.js +159 -0
- package/server.js +263 -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
|
|
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 {
|
|
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
|
|
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,22 @@ import {
|
|
|
45
48
|
resolveMatch,
|
|
46
49
|
} from "./internal/runtime.js";
|
|
47
50
|
|
|
51
|
+
import type { FormState } from "./internal/form-action.js";
|
|
52
|
+
import { type StreamDiagnostic, streamReporter } from "./internal/inspector.js";
|
|
53
|
+
|
|
54
|
+
import { redirectDocument, redirectResult, shellFor } from "./internal/shell.js";
|
|
55
|
+
|
|
48
56
|
/** Asset URLs to reference from the document. */
|
|
49
57
|
export type RenderAssets = {|
|
|
50
58
|
readonly scripts: $ReadOnlyArray<string>,
|
|
51
59
|
readonly styles: $ReadOnlyArray<string>,
|
|
52
60
|
readonly preloads: $ReadOnlyArray<string>,
|
|
61
|
+
/**
|
|
62
|
+
* The build the document belongs to, written into its head as
|
|
63
|
+
* `<meta name="uf:deployment">`. Absent under `uf dev`, where there is no
|
|
64
|
+
* other build to be skewed against. See `./internal/deployment.js`.
|
|
65
|
+
*/
|
|
66
|
+
readonly deployment?: string,
|
|
53
67
|
|};
|
|
54
68
|
|
|
55
69
|
/**
|
|
@@ -98,6 +112,42 @@ export type PrerenderResult = {|
|
|
|
98
112
|
readonly headers?: { readonly [string]: string },
|
|
99
113
|
/** The exception this render fell back to its error boundary for; see [`RenderResult`]. */
|
|
100
114
|
readonly error?: mixed,
|
|
115
|
+
/**
|
|
116
|
+
* The Flight payload the document was rendered from, for a document React
|
|
117
|
+
* Server Components rendered.
|
|
118
|
+
*
|
|
119
|
+
* `uf build` writes it beside the document as the route's payload file, so a
|
|
120
|
+
* browser that navigates to a prerendered route fetches a file rather than
|
|
121
|
+
* asking a server — which is what makes a static host able to serve client
|
|
122
|
+
* navigation at all. Absent for a document rendered from its modules.
|
|
123
|
+
*/
|
|
124
|
+
readonly payload?: Uint8Array,
|
|
125
|
+
/**
|
|
126
|
+
* The page's static shell, when the prerender was partial and the page read
|
|
127
|
+
* the request inside a `<Suspense>` boundary.
|
|
128
|
+
*
|
|
129
|
+
* `html` is then the shell's markup and not a document to write at the
|
|
130
|
+
* page's URL: a server answers the page by sending the shell and rendering
|
|
131
|
+
* the holes per request, through [`Renderer`]'s `resume`. No payload comes
|
|
132
|
+
* with it, because the browser hydrates from the request's. Only a renderer
|
|
133
|
+
* for React Server Components writes one.
|
|
134
|
+
*/
|
|
135
|
+
readonly shell?: PrerenderedShell,
|
|
136
|
+
|};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A route's payload, as a browser navigating to it is answered.
|
|
140
|
+
*
|
|
141
|
+
* `stream` is `null` for a redirect, whose `location` is already the target's
|
|
142
|
+
* payload URL when the target is on this origin: `fetch` follows it and lands
|
|
143
|
+
* on a payload.
|
|
144
|
+
*/
|
|
145
|
+
export type FlightResponse = {|
|
|
146
|
+
readonly status: number,
|
|
147
|
+
readonly headers: { readonly [string]: string },
|
|
148
|
+
readonly stream: ReadableStream<Uint8Array> | null,
|
|
149
|
+
/** The exception the route resolved to its error boundary for; see [`RenderResult`]. */
|
|
150
|
+
readonly error?: mixed,
|
|
101
151
|
|};
|
|
102
152
|
|
|
103
153
|
/** What a host may tell the renderer about one request. */
|
|
@@ -111,6 +161,82 @@ export type RenderOptions = {|
|
|
|
111
161
|
* bytes. `uf dev` reports them in the terminal; a production host logs them.
|
|
112
162
|
*/
|
|
113
163
|
readonly onError?: (error: mixed) => void,
|
|
164
|
+
/**
|
|
165
|
+
* Rewrite the document opening — the head and, when present, the body start
|
|
166
|
+
* tag — before it goes out.
|
|
167
|
+
*
|
|
168
|
+
* For `uf dev` and nothing else. Vite's `transformIndexHtml` injects
|
|
169
|
+
* `/@vite/client` and the refresh preamble and rewrites asset URLs, and it
|
|
170
|
+
* is a *whole document* hook, so the development server used to collect the
|
|
171
|
+
* page and transform it at the end. That made the one place a developer
|
|
172
|
+
* would notice streaming the one place it did not happen: a slow page showed
|
|
173
|
+
* nothing until it was finished, and `$loading.js` looked broken.
|
|
174
|
+
* See ubugeeei-prod/uf#374.
|
|
175
|
+
*
|
|
176
|
+
* A production host passes nothing here and streams as it always did.
|
|
177
|
+
*
|
|
178
|
+
* # What a plugin that injects into the body gets
|
|
179
|
+
*
|
|
180
|
+
* `transformIndexHtml` is a whole-document hook and this hands it only the
|
|
181
|
+
* parseable opening of the document. Measured against Vite 8.2.2, injecting
|
|
182
|
+
* all four positions into a whole document and into the streamed opening:
|
|
183
|
+
*
|
|
184
|
+
* | `injectTo` | whole document | streamed |
|
|
185
|
+
* | -------------- | ------------------- | -------- |
|
|
186
|
+
* | `head-prepend` | after `<head>` | same |
|
|
187
|
+
* | `head` | before `</head>` | same |
|
|
188
|
+
* | `body-prepend` | after `<body>` | same |
|
|
189
|
+
* | `body` | before `</body>` | after `<body>` |
|
|
190
|
+
*
|
|
191
|
+
* Nothing is dropped — every tag still reaches the document — but a `body`
|
|
192
|
+
* tag lands at the top of the body rather than after the content, because
|
|
193
|
+
* the content is deliberately not passed to the hook. That keeps Vite's
|
|
194
|
+
* parser away from chunk boundaries that may sit inside an attribute.
|
|
195
|
+
*
|
|
196
|
+
* uf's own injections are `head` and `head-prepend`, and Vite's client is
|
|
197
|
+
* head-injected, so this is about a third-party plugin.
|
|
198
|
+
* `packages/vite/dev-head-transform.test.js` pins the table above, so the day
|
|
199
|
+
* it changes is a failing test rather than a surprise.
|
|
200
|
+
*/
|
|
201
|
+
readonly transformHead?: (html: string) => Promise<string>,
|
|
202
|
+
/**
|
|
203
|
+
* React's `formState` for a page rendered in answer to a form posted before
|
|
204
|
+
* hydration; see `internal/form-action.js`. Only a host's `postback` passes
|
|
205
|
+
* one.
|
|
206
|
+
*/
|
|
207
|
+
readonly formState?: FormState,
|
|
208
|
+
/**
|
|
209
|
+
* Told, in words, when a document streamed differently than it did last time.
|
|
210
|
+
*
|
|
211
|
+
* For `uf dev` and nothing else, like `transformHead` above. It answers the
|
|
212
|
+
* half of ubugeeei-prod/uf#520 that is about the wire — what arrived, in what
|
|
213
|
+
* order, and which part of the tree each chunk built — for the stream uf has
|
|
214
|
+
* today, which is a document whose Suspense boundaries resolve independently.
|
|
215
|
+
* `internal/inspector.js` is what it is and what it deliberately is not.
|
|
216
|
+
*
|
|
217
|
+
* A host that passes nothing here records nothing: no recorder is
|
|
218
|
+
* constructed, and the chunks a production stream yields are untouched.
|
|
219
|
+
*
|
|
220
|
+
* It is handed a message and its detail lines rather than the record they
|
|
221
|
+
* came from, because the caller is `@uniflowed/vite` — plain JavaScript, run
|
|
222
|
+
* by Vite before any Flow transform exists, which is why `DEVTOOLS_HOOK` and
|
|
223
|
+
* `DIAGNOSTIC_ENDPOINT` are spelled twice rather than imported. The
|
|
224
|
+
* vocabulary of the report belongs on this side of that line.
|
|
225
|
+
*/
|
|
226
|
+
readonly onStream?: (diagnostic: StreamDiagnostic) => void,
|
|
227
|
+
/**
|
|
228
|
+
* For `prerender` only: whether a read of the request may be left for the
|
|
229
|
+
* request rather than fail the page.
|
|
230
|
+
*
|
|
231
|
+
* `uf build` passes it when `app.rendering.modes` allows `ppr` and the build
|
|
232
|
+
* leaves a server behind. A page that reads `cookies()`, `headers()` or
|
|
233
|
+
* `draftMode()` inside a `<Suspense>` boundary is then written as a static
|
|
234
|
+
* shell with that boundary as a hole — [`PrerenderResult`]'s `shell` — and a
|
|
235
|
+
* read outside every boundary still fails the page, naming what it read.
|
|
236
|
+
* The renderer for React Server Components honours it; a route rendered from
|
|
237
|
+
* its modules (`app.rsc: false`) is prerendered whole or not at all.
|
|
238
|
+
*/
|
|
239
|
+
readonly partial?: boolean,
|
|
114
240
|
|};
|
|
115
241
|
|
|
116
242
|
/** The two ids the server writes and the client reads. */
|
|
@@ -121,13 +247,13 @@ export { DATA_ID, ROOT_ID } from "./internal/document.js";
|
|
|
121
247
|
*
|
|
122
248
|
* Re-exported rather than left to the host to import, and the reason is the
|
|
123
249
|
* one thing about `@uniflowed/server` that is easy to get wrong: the request
|
|
124
|
-
*
|
|
125
|
-
* that resolved `@uniflowed/server/host` for itself — from its own
|
|
126
|
-
* `node_modules`, or from outside the bundle a build produced —
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* it is the same import.
|
|
250
|
+
* store is shared by every copy of one *release* of that package, and no more.
|
|
251
|
+
* A host that resolved `@uniflowed/server/host` for itself — from its own
|
|
252
|
+
* `node_modules`, or from outside the bundle a build produced — may hold a
|
|
253
|
+
* different release, and would begin a request in a store the application
|
|
254
|
+
* never reads, so every `cookies()` in it would still be outside one, silently.
|
|
255
|
+
* Handing it out from here makes the copy the host begins with the copy this
|
|
256
|
+
* module dispatches and renders with, because it is the same import.
|
|
131
257
|
*
|
|
132
258
|
* `run` wraps everything that decides the response; `settle` is called once
|
|
133
259
|
* the response has been *written*, which is a different line in every host.
|
|
@@ -148,6 +274,31 @@ export type {
|
|
|
148
274
|
} from "./middleware.js";
|
|
149
275
|
export { createMiddlewareRunner } from "./middleware.js";
|
|
150
276
|
|
|
277
|
+
/**
|
|
278
|
+
* `app.router.basePath` and `trailingSlash`, for `virtual:uf/server` to install
|
|
279
|
+
* before the first render, and `basePath()` for a middleware or a route handler
|
|
280
|
+
* that builds an address itself. See `./internal/base-path.js`.
|
|
281
|
+
*/
|
|
282
|
+
export type { RoutingSettings, TrailingSlash } from "./internal/base-path.js";
|
|
283
|
+
export { basePath, installRouting } from "./internal/base-path.js";
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The endpoint a `"use server"` export is dialled at.
|
|
287
|
+
*
|
|
288
|
+
* Here rather than beside `@uniflowed/router/action`, which is the browser's
|
|
289
|
+
* half of the same feature and must stay reachable from a client component:
|
|
290
|
+
* this one refuses outside a request, so it imports `internal/request.js` and
|
|
291
|
+
* through it `node:async_hooks`. The two halves share `internal/action-wire.js`
|
|
292
|
+
* and nothing else, which is what keeps one grammar rather than two.
|
|
293
|
+
*
|
|
294
|
+
* `virtual:uf/server` calls it with the table `virtual:uf/actions` built from
|
|
295
|
+
* the RSC manifest, and every host runs it between the middleware and the
|
|
296
|
+
* route handlers. See `internal/action-endpoint.js` for what the endpoint
|
|
297
|
+
* refuses and why.
|
|
298
|
+
*/
|
|
299
|
+
export type { ActionModule, ActionRecord } from "./internal/action-endpoint.js";
|
|
300
|
+
export { createActionDispatcher } from "./internal/action-endpoint.js";
|
|
301
|
+
|
|
151
302
|
/**
|
|
152
303
|
* What a URL turned out to be: a route to render, or a redirect to answer with.
|
|
153
304
|
*
|
|
@@ -160,15 +311,6 @@ type Resolution =
|
|
|
160
311
|
| {| readonly kind: "route", readonly route: ResolvedRoute |}
|
|
161
312
|
| {| readonly kind: "redirect", readonly error: RedirectError |};
|
|
162
313
|
|
|
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
314
|
/** The two ways one app answers for a URL. */
|
|
173
315
|
export type Renderer = {|
|
|
174
316
|
readonly render: (
|
|
@@ -181,6 +323,28 @@ export type Renderer = {|
|
|
|
181
323
|
assets: RenderAssets,
|
|
182
324
|
options?: RenderOptions,
|
|
183
325
|
) => Promise<PrerenderResult>,
|
|
326
|
+
/**
|
|
327
|
+
* A route's payload, for a browser that is navigating rather than loading a
|
|
328
|
+
* document. Only a renderer for React Server Components has one.
|
|
329
|
+
*/
|
|
330
|
+
readonly flight?: (
|
|
331
|
+
url: string,
|
|
332
|
+
options?: {|
|
|
333
|
+
readonly onError?: (error: mixed) => void,
|
|
334
|
+
readonly interceptedFrom?: string,
|
|
335
|
+
|},
|
|
336
|
+
) => Promise<FlightResponse>,
|
|
337
|
+
/**
|
|
338
|
+
* A page `prerender` wrote as a static shell: the shell first, then its holes
|
|
339
|
+
* as this request renders them. Only a renderer for React Server Components
|
|
340
|
+
* has one, because only it writes a shell.
|
|
341
|
+
*/
|
|
342
|
+
readonly resume?: (
|
|
343
|
+
url: string,
|
|
344
|
+
assets: RenderAssets,
|
|
345
|
+
shell: PrerenderedShell,
|
|
346
|
+
options?: RenderOptions,
|
|
347
|
+
) => Promise<RenderResult>,
|
|
184
348
|
|};
|
|
185
349
|
|
|
186
350
|
export function createRenderer(options: {|
|
|
@@ -201,13 +365,35 @@ export function createRenderer(options: {|
|
|
|
201
365
|
* The route to render, or the redirect to answer with instead.
|
|
202
366
|
*
|
|
203
367
|
* Shared by both entry points, because *what* a URL resolves to has nothing
|
|
204
|
-
* to do with how the answer is delivered
|
|
205
|
-
*
|
|
206
|
-
*
|
|
368
|
+
* to do with how the answer is delivered — with one exception, which is
|
|
369
|
+
* `defer` and is the exception that proves it. Whether the router may hand
|
|
370
|
+
* the page a loader that has not answered yet *is* a question about delivery:
|
|
371
|
+
* only a renderer with a `<Suspense>` fallback to send first has anywhere to
|
|
372
|
+
* put the wait. `render` says yes and `prerender` says no; see
|
|
373
|
+
* `ResolveOptions.defer` and ubugeeei-prod/uf#373.
|
|
374
|
+
*
|
|
375
|
+
* Returning the redirect rather than throwing it keeps the two callers from
|
|
376
|
+
* each having to remember that a redirect is the one thing `resolveMatch`
|
|
377
|
+
* lets out.
|
|
378
|
+
*
|
|
379
|
+
* `onMatch` is what makes the request's log line say `/orders/:id` rather
|
|
380
|
+
* than `/orders/8813`. It is handed to `resolveMatch` rather than read off
|
|
381
|
+
* the route this returns, because a loader runs *inside* that call and a
|
|
382
|
+
* loader has things to log: recording the route afterwards would leave every
|
|
383
|
+
* line the loader wrote claiming to belong to no route at all. `noteRoute`
|
|
384
|
+
* does nothing outside a request, which is what lets `prerender` — a build,
|
|
385
|
+
* with no request anywhere — call the same function.
|
|
207
386
|
*/
|
|
208
|
-
async function resolve(url: string): Promise<Resolution> {
|
|
387
|
+
async function resolve(url: string, defer: boolean): Promise<Resolution> {
|
|
209
388
|
try {
|
|
210
|
-
return {
|
|
389
|
+
return {
|
|
390
|
+
kind: "route",
|
|
391
|
+
route: await resolveMatch(table, url, {
|
|
392
|
+
defer,
|
|
393
|
+
onMatch: noteRoute,
|
|
394
|
+
runLoader: traceLoader,
|
|
395
|
+
}),
|
|
396
|
+
};
|
|
211
397
|
} catch (error) {
|
|
212
398
|
if (error instanceof RedirectError) {
|
|
213
399
|
return { kind: "redirect", error };
|
|
@@ -221,12 +407,22 @@ export function createRenderer(options: {|
|
|
|
221
407
|
assets: RenderAssets,
|
|
222
408
|
settings?: RenderOptions,
|
|
223
409
|
): Promise<RenderResult> {
|
|
224
|
-
const resolution = await resolve(url);
|
|
410
|
+
const resolution = await resolve(url, true);
|
|
225
411
|
if (resolution.kind === "redirect") {
|
|
226
412
|
return redirectDocument(resolution.error);
|
|
227
413
|
}
|
|
228
414
|
let resolved: ResolvedRoute = resolution.route;
|
|
229
415
|
const report = settings?.onError ?? (() => {});
|
|
416
|
+
// Built once and shared by both renders below, so a page that threw its
|
|
417
|
+
// shell away and rendered its error boundary instead reports the stream the
|
|
418
|
+
// browser was actually sent rather than the one that was abandoned.
|
|
419
|
+
const send = settings?.onStream;
|
|
420
|
+
const onStream = send == null ? undefined : streamReporter(url, send);
|
|
421
|
+
// Read rather than minted: a project that has not asked for a nonce gets
|
|
422
|
+
// `null` and the document it has always had. Read once for both renders
|
|
423
|
+
// below, so the error document a failed shell falls back to carries the
|
|
424
|
+
// same nonce as the policy already on the response.
|
|
425
|
+
const nonce = currentNonce();
|
|
230
426
|
|
|
231
427
|
// React reports an exception to `onError` *and*, if it was in the shell, to
|
|
232
428
|
// `onShellError` — so forwarding both would tell the host about one failure
|
|
@@ -247,8 +443,12 @@ export function createRenderer(options: {|
|
|
|
247
443
|
let body: DocumentBody;
|
|
248
444
|
try {
|
|
249
445
|
body = await renderDocument(<App url={url} initial={resolved} />, {
|
|
250
|
-
shell: shellFor(
|
|
446
|
+
shell: shellFor(assets, nonce, settings?.formState),
|
|
251
447
|
onError,
|
|
448
|
+
transformHead: settings?.transformHead,
|
|
449
|
+
onStream,
|
|
450
|
+
nonce,
|
|
451
|
+
formState: settings?.formState,
|
|
252
452
|
});
|
|
253
453
|
streaming = true;
|
|
254
454
|
// Recovered before the shell was ready: a `<Suspense>` boundary whose
|
|
@@ -277,8 +477,12 @@ export function createRenderer(options: {|
|
|
|
277
477
|
// where somebody can fix it.
|
|
278
478
|
streaming = true;
|
|
279
479
|
body = await renderDocument(<App url={url} initial={resolved} />, {
|
|
280
|
-
shell: shellFor(
|
|
480
|
+
shell: shellFor(assets, nonce, settings?.formState),
|
|
281
481
|
onError,
|
|
482
|
+
transformHead: settings?.transformHead,
|
|
483
|
+
onStream,
|
|
484
|
+
nonce,
|
|
485
|
+
formState: settings?.formState,
|
|
282
486
|
});
|
|
283
487
|
}
|
|
284
488
|
|
|
@@ -296,7 +500,7 @@ export function createRenderer(options: {|
|
|
|
296
500
|
assets: RenderAssets,
|
|
297
501
|
settings?: RenderOptions,
|
|
298
502
|
): Promise<PrerenderResult> {
|
|
299
|
-
const resolution = await resolve(url);
|
|
503
|
+
const resolution = await resolve(url, false);
|
|
300
504
|
if (resolution.kind === "redirect") {
|
|
301
505
|
return redirectResult(redirectDocument(resolution.error));
|
|
302
506
|
}
|
|
@@ -306,7 +510,7 @@ export function createRenderer(options: {|
|
|
|
306
510
|
let html: string;
|
|
307
511
|
try {
|
|
308
512
|
html = await prerenderDocument(<App url={url} initial={resolved} />, {
|
|
309
|
-
shell: shellFor(
|
|
513
|
+
shell: shellFor(assets),
|
|
310
514
|
onError: report,
|
|
311
515
|
});
|
|
312
516
|
} catch (error) {
|
|
@@ -315,7 +519,7 @@ export function createRenderer(options: {|
|
|
|
315
519
|
}
|
|
316
520
|
resolved = await resolveFailure(table, url, error);
|
|
317
521
|
html = await prerenderDocument(<App url={url} initial={resolved} />, {
|
|
318
|
-
shell: shellFor(
|
|
522
|
+
shell: shellFor(assets),
|
|
319
523
|
onError: report,
|
|
320
524
|
});
|
|
321
525
|
}
|
|
@@ -338,85 +542,38 @@ function renderFailure(resolved: ResolvedRoute): mixed {
|
|
|
338
542
|
};
|
|
339
543
|
}
|
|
340
544
|
|
|
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
545
|
/**
|
|
359
|
-
* The document
|
|
546
|
+
* The document a single-page build writes, and the only one it writes.
|
|
360
547
|
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
548
|
+
* `app.rendering.modes: ["csr"]` renders no route at build time: the client
|
|
549
|
+
* router resolves and renders every one of them in the browser, so what the
|
|
550
|
+
* build has to leave behind is the *chrome* — the stylesheets, the module
|
|
551
|
+
* script, and the empty root the client renders into. That is exactly
|
|
552
|
+
* [`shellFor`]'s three strings with nothing between them, which is why this is
|
|
553
|
+
* three concatenations rather than a fourth shape of document to keep in step
|
|
554
|
+
* with the other three.
|
|
368
555
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
|
|
373
|
-
|
|
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.
|
|
556
|
+
* No React runs. There is nothing to render: no URL has been asked for, and
|
|
557
|
+
* whatever this document is served for is decided by the host rather than by
|
|
558
|
+
* this build.
|
|
559
|
+
*
|
|
560
|
+
* # What it costs, said here because it is not visible from the file
|
|
400
561
|
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
562
|
+
* The document has no `<title>`, no `<meta name="description">` and no content.
|
|
563
|
+
* A crawler that runs no JavaScript sees an empty page for **every** URL, and a
|
|
564
|
+
* reader sees nothing until the bundle has loaded and the route has resolved.
|
|
565
|
+
* That is what a single-page application is, and it is why `modes: ["csr"]` is
|
|
566
|
+
* a declaration a project makes rather than something a build falls back to.
|
|
567
|
+
* A project that wants a document per route has `ssg`, and one that wants a
|
|
568
|
+
* document per request has `ssr`.
|
|
404
569
|
*/
|
|
405
|
-
function
|
|
406
|
-
|
|
407
|
-
|
|
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, "&").replace(/"/g, """).replace(/</g, "<");
|
|
570
|
+
export function shellDocument(assets: RenderAssets): string {
|
|
571
|
+
const shell = shellFor(assets);
|
|
572
|
+
return `${shell.open}${shell.body}${shell.close}`;
|
|
418
573
|
}
|
|
419
574
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
575
|
+
export {
|
|
576
|
+
createInstrumentation,
|
|
577
|
+
instrumentRender,
|
|
578
|
+
traceRequestPhase,
|
|
579
|
+
} from "@uniflowed/server/instrumentation";
|