@jk2908/solas 0.3.0 → 0.3.2
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/CHANGELOG.md +12 -0
- package/dist/cli/build.d.ts +7 -0
- package/dist/cli/build.js +183 -0
- package/dist/cli/dev.d.ts +4 -0
- package/dist/cli/dev.js +13 -0
- package/dist/cli/preview.d.ts +1 -0
- package/dist/cli/preview.js +47 -0
- package/dist/cli.js +4 -238
- package/dist/index.js +2 -0
- package/dist/internal/browser-router/link.d.ts +17 -0
- package/dist/internal/{navigation → browser-router}/link.js +22 -16
- package/dist/internal/browser-router/router.d.ts +184 -0
- package/dist/internal/{router/router-provider.js → browser-router/router.js} +81 -12
- package/dist/internal/{router → browser-router}/use-router.d.ts +1 -1
- package/dist/internal/browser-router/use-router.js +5 -0
- package/dist/internal/{navigation → browser-router}/use-search-params.js +1 -1
- package/dist/internal/build.js +2 -2
- package/dist/internal/codegen/config.js +17 -8
- package/dist/internal/codegen/environments.js +7 -7
- package/dist/internal/codegen/manifest.js +3 -3
- package/dist/internal/codegen/maps.js +11 -15
- package/dist/internal/codegen/types.d.ts +5 -0
- package/dist/internal/codegen/types.js +48 -0
- package/dist/internal/codegen/utils.d.ts +10 -0
- package/dist/internal/codegen/utils.js +27 -2
- package/dist/internal/env/browser.js +6 -6
- package/dist/internal/env/flight.d.ts +29 -0
- package/dist/internal/env/flight.js +187 -0
- package/dist/internal/env/request-context.d.ts +1 -1
- package/dist/internal/env/rsc.d.ts +1 -1
- package/dist/internal/env/rsc.js +23 -28
- package/dist/internal/env/ssr.d.ts +2 -2
- package/dist/internal/env/ssr.js +27 -13
- package/dist/internal/env/utils.js +13 -1
- package/dist/internal/http-router/create-http-router.d.ts +6 -0
- package/dist/internal/{router/create-router.js → http-router/create-http-router.js} +5 -5
- package/dist/internal/{router → http-router}/router.d.ts +9 -9
- package/dist/internal/{router → http-router}/router.js +20 -19
- package/dist/internal/{router → http-router}/utils.d.ts +11 -3
- package/dist/internal/{router → http-router}/utils.js +9 -1
- package/dist/internal/metadata.js +10 -10
- package/dist/internal/prerender.d.ts +4 -9
- package/dist/internal/prerender.js +6 -23
- package/dist/internal/render/head.js +1 -1
- package/dist/internal/render/tree.d.ts +1 -1
- package/dist/internal/render/tree.js +17 -13
- package/dist/internal/{router/resolver.d.ts → resolver.d.ts} +41 -41
- package/dist/internal/{router/resolver.js → resolver.js} +7 -7
- package/dist/internal/server/actions.js +1 -1
- package/dist/internal/server/cookies.d.ts +3 -2
- package/dist/internal/server/cookies.js +4 -3
- package/dist/internal/server/dynamic.d.ts +1 -3
- package/dist/internal/server/dynamic.js +3 -11
- package/dist/internal/server/headers.d.ts +2 -2
- package/dist/internal/server/headers.js +3 -3
- package/dist/internal/server/url.d.ts +2 -2
- package/dist/internal/server/url.js +3 -3
- package/dist/navigation.d.ts +2 -4
- package/dist/navigation.js +2 -4
- package/dist/router.d.ts +3 -4
- package/dist/router.js +3 -4
- package/dist/solas.d.ts +3 -1
- package/dist/solas.js +1 -1
- package/dist/types.d.ts +15 -7
- package/dist/utils/logger.js +1 -1
- package/package.json +2 -7
- package/dist/internal/navigation/link.d.ts +0 -13
- package/dist/internal/router/create-router.d.ts +0 -6
- package/dist/internal/router/router-context.d.ts +0 -15
- package/dist/internal/router/router-context.js +0 -8
- package/dist/internal/router/router-provider.d.ts +0 -10
- package/dist/internal/router/use-router.js +0 -5
- /package/dist/internal/{navigation → browser-router}/use-search-params.d.ts +0 -0
- /package/dist/internal/{router/prefetcher.d.ts → prefetcher.d.ts} +0 -0
- /package/dist/internal/{router/prefetcher.js → prefetcher.js} +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
type Chunk = string | Uint8Array;
|
|
2
|
+
type Opts = {
|
|
3
|
+
nonce?: string;
|
|
4
|
+
};
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
__FLIGHT_DATA?: Chunk[];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Capture only the payload rows that are already buffered in a stream.
|
|
12
|
+
* Used by ppr prerender so the cached prelude carries the static
|
|
13
|
+
* payload, while postponed work is left for request-time resume
|
|
14
|
+
*/
|
|
15
|
+
export declare function captureBuffered(stream: ReadableStream<Uint8Array>): Promise<ReadableStream<Uint8Array<ArrayBufferLike>>>;
|
|
16
|
+
/**
|
|
17
|
+
* Read the inline payload rows written into the html document. Stays open
|
|
18
|
+
* for the lifetime of the document so ppr resume can keep appending rows
|
|
19
|
+
* without tripping React's connection-closed path
|
|
20
|
+
*/
|
|
21
|
+
export declare const rscStream: ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
22
|
+
/**
|
|
23
|
+
* Inject the payload into the outgoing HTML as small inline script pushes. This keeps
|
|
24
|
+
* hydration on the first document load instead of doing a follow-up fetch. HTML still
|
|
25
|
+
* streams through, but the closing body/html tags are held back until the payload
|
|
26
|
+
* is written
|
|
27
|
+
*/
|
|
28
|
+
export declare function injectPayload(payload: ReadableStream<Uint8Array>, opts?: Opts): TransformStream<Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const encoder = new TextEncoder();
|
|
2
|
+
const HTML_TRAIL = '</body></html>';
|
|
3
|
+
/**
|
|
4
|
+
* Capture only the payload rows that are already buffered in a stream.
|
|
5
|
+
* Used by ppr prerender so the cached prelude carries the static
|
|
6
|
+
* payload, while postponed work is left for request-time resume
|
|
7
|
+
*/
|
|
8
|
+
export async function captureBuffered(stream) {
|
|
9
|
+
const reader = stream.getReader();
|
|
10
|
+
const chunks = [];
|
|
11
|
+
try {
|
|
12
|
+
while (true) {
|
|
13
|
+
// only take what is already queued. anything still pending belongs
|
|
14
|
+
// to the later resume step, not the cached prelude
|
|
15
|
+
const result = await Promise.race([
|
|
16
|
+
reader.read(),
|
|
17
|
+
new Promise(r => setTimeout(r, 0, null)),
|
|
18
|
+
]);
|
|
19
|
+
if (result === null || result.done)
|
|
20
|
+
break;
|
|
21
|
+
if (result.value)
|
|
22
|
+
chunks.push(result.value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
reader.cancel();
|
|
27
|
+
}
|
|
28
|
+
return new ReadableStream({
|
|
29
|
+
start(controller) {
|
|
30
|
+
for (const chunk of chunks)
|
|
31
|
+
controller.enqueue(chunk);
|
|
32
|
+
controller.close();
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read the inline payload rows written into the html document. Stays open
|
|
38
|
+
* for the lifetime of the document so ppr resume can keep appending rows
|
|
39
|
+
* without tripping React's connection-closed path
|
|
40
|
+
*/
|
|
41
|
+
export const rscStream = new ReadableStream({
|
|
42
|
+
start(controller) {
|
|
43
|
+
if (typeof window === 'undefined')
|
|
44
|
+
return;
|
|
45
|
+
// start with any rows already written into the page. Later resume
|
|
46
|
+
// work keeps adding to this same array
|
|
47
|
+
const flightData = (window.__FLIGHT_DATA ??= []);
|
|
48
|
+
// save the real array push before we replace it. We still want
|
|
49
|
+
// __FLIGHT_DATA to behave like a normal array
|
|
50
|
+
const push = flightData.push.bind(flightData);
|
|
51
|
+
// each row can be plain text or binary. normalise both into bytes
|
|
52
|
+
// before handing them to the browser-side RSC reader
|
|
53
|
+
function handle(entry) {
|
|
54
|
+
controller.enqueue(typeof entry === 'string' ? encoder.encode(entry) : entry);
|
|
55
|
+
}
|
|
56
|
+
// replay anything the page already wrote before this stream started.
|
|
57
|
+
// That lets hydration read the early rows first
|
|
58
|
+
for (const entry of flightData)
|
|
59
|
+
handle(entry);
|
|
60
|
+
// clear the array to release memory
|
|
61
|
+
window.__FLIGHT_DATA.length = 0;
|
|
62
|
+
// later inline scripts call __FLIGHT_DATA.push(...). Forward each new row
|
|
63
|
+
// into the open stream, then clear the array so old rows do not pile up
|
|
64
|
+
// in memory
|
|
65
|
+
flightData.push = (...entries) => {
|
|
66
|
+
const length = push(...entries);
|
|
67
|
+
for (const entry of entries)
|
|
68
|
+
handle(entry);
|
|
69
|
+
// once React has the row, we no longer need to keep it in the array
|
|
70
|
+
if (typeof window !== 'undefined' && window.__FLIGHT_DATA) {
|
|
71
|
+
window.__FLIGHT_DATA.length = 0;
|
|
72
|
+
}
|
|
73
|
+
// return the new length so the array behaves as expected
|
|
74
|
+
return length;
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Inject the payload into the outgoing HTML as small inline script pushes. This keeps
|
|
80
|
+
* hydration on the first document load instead of doing a follow-up fetch. HTML still
|
|
81
|
+
* streams through, but the closing body/html tags are held back until the payload
|
|
82
|
+
* is written
|
|
83
|
+
*/
|
|
84
|
+
export function injectPayload(payload, opts = {}) {
|
|
85
|
+
const decoder = new TextDecoder();
|
|
86
|
+
let payloadWrite;
|
|
87
|
+
let buffered = [];
|
|
88
|
+
let timeout;
|
|
89
|
+
function flush(controller) {
|
|
90
|
+
for (const chunk of buffered) {
|
|
91
|
+
let html = decoder.decode(chunk, { stream: true });
|
|
92
|
+
// hold the final closing tags back so payload scripts land inside the document,
|
|
93
|
+
// not after it
|
|
94
|
+
if (html.endsWith(HTML_TRAIL)) {
|
|
95
|
+
html = html.slice(0, -HTML_TRAIL.length);
|
|
96
|
+
}
|
|
97
|
+
if (html)
|
|
98
|
+
controller.enqueue(encoder.encode(html));
|
|
99
|
+
}
|
|
100
|
+
// flush any decoder state left over from split utf-8/html chunks
|
|
101
|
+
let remaining = decoder.decode();
|
|
102
|
+
if (remaining.endsWith(HTML_TRAIL)) {
|
|
103
|
+
remaining = remaining.slice(0, -HTML_TRAIL.length);
|
|
104
|
+
}
|
|
105
|
+
if (remaining)
|
|
106
|
+
controller.enqueue(encoder.encode(remaining));
|
|
107
|
+
buffered = [];
|
|
108
|
+
timeout = undefined;
|
|
109
|
+
}
|
|
110
|
+
function start(controller) {
|
|
111
|
+
// only start writing payload rows once, even if html keeps arriving
|
|
112
|
+
payloadWrite ??= writePayload(payload, controller, opts.nonce);
|
|
113
|
+
return payloadWrite;
|
|
114
|
+
}
|
|
115
|
+
return new TransformStream({
|
|
116
|
+
transform(chunk, controller) {
|
|
117
|
+
// collect html first so we can decide where the payload scripts belong
|
|
118
|
+
buffered.push(chunk);
|
|
119
|
+
if (timeout != null)
|
|
120
|
+
return;
|
|
121
|
+
// html can arrive split in awkward places, so wait one tick before flushing.
|
|
122
|
+
// That gives the next chunk a chance to join up and keeps scripts out of
|
|
123
|
+
// half a tag
|
|
124
|
+
timeout = setTimeout(() => {
|
|
125
|
+
try {
|
|
126
|
+
// once the buffered html is safe to write, start the payload writer too
|
|
127
|
+
flush(controller);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
controller.error(err);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
start(controller).catch(err => controller.error(err));
|
|
134
|
+
}, 0);
|
|
135
|
+
},
|
|
136
|
+
async flush(controller) {
|
|
137
|
+
if (timeout != null) {
|
|
138
|
+
clearTimeout(timeout);
|
|
139
|
+
flush(controller);
|
|
140
|
+
}
|
|
141
|
+
// finish every payload row before restoring the closing html tags
|
|
142
|
+
await start(controller);
|
|
143
|
+
controller.enqueue(encoder.encode(HTML_TRAIL));
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Turn each payload row into a tiny inline script that pushes into __FLIGHT_DATA.
|
|
149
|
+
* Text rows stay as strings when possible, and binary rows fall back to base64.
|
|
150
|
+
* The browser-side patched push then forwards those rows into the open stream
|
|
151
|
+
*/
|
|
152
|
+
async function writePayload(payload, controller, nonce) {
|
|
153
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
154
|
+
for await (const chunk of payload) {
|
|
155
|
+
try {
|
|
156
|
+
// most payload rows are plain text, so write the simplest script we can
|
|
157
|
+
writePayloadScript(JSON.stringify(decoder.decode(chunk, { stream: true })), controller, nonce);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// most rows are text, but keep binary chunks intact when a payload
|
|
161
|
+
// row cannot be decoded as utf-8
|
|
162
|
+
const base64 = JSON.stringify(btoa(String.fromCodePoint(...chunk)));
|
|
163
|
+
writePayloadScript(`Uint8Array.from(atob(${base64}), value => value.codePointAt(0))`, controller, nonce);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// flush any trailing decoder state after the stream ends
|
|
167
|
+
const remaining = decoder.decode();
|
|
168
|
+
if (remaining) {
|
|
169
|
+
writePayloadScript(JSON.stringify(remaining), controller, nonce);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Wrap one payload row in a script tag that appends into the shared browser queue.
|
|
174
|
+
* The script stays deliberately small: just push the row and let the patched push
|
|
175
|
+
* do the rest
|
|
176
|
+
*/
|
|
177
|
+
function writePayloadScript(chunk, controller, nonce) {
|
|
178
|
+
// each script only does a normal __FLIGHT_DATA.push(...). The patched push
|
|
179
|
+
// above forwards that row into the open stream. Escape the inline JS first
|
|
180
|
+
// so HTML parsing cannot break the script body
|
|
181
|
+
const script = `<script${nonce ? ` nonce="${nonce}"` : ''}>${escapeInlineScript(`(self.__FLIGHT_DATA||=[]).push(${chunk})`)}</script>`;
|
|
182
|
+
controller.enqueue(encoder.encode(script));
|
|
183
|
+
}
|
|
184
|
+
// Escape closing script tags and HTML comments inside inline JS
|
|
185
|
+
function escapeInlineScript(script) {
|
|
186
|
+
return script.replace(/<!--/g, '<\\!--').replace(/<\/(script)/gi, '</\\$1');
|
|
187
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SolasRequest } from '../../types.js';
|
|
2
1
|
import type { Cookies } from '../../utils/cookies.js';
|
|
2
|
+
import type { SolasRequest } from '../../types.js';
|
|
3
3
|
export type RequestCache = {
|
|
4
4
|
cookies?: Readonly<ReturnType<typeof Cookies.parse>>;
|
|
5
5
|
headers?: ReadonlyMap<string, string>;
|
|
@@ -2,7 +2,7 @@ import type { ReactFormState } from 'react-dom/client';
|
|
|
2
2
|
import type { ImportMap, Manifest, RuntimeConfig } from '../../types.js';
|
|
3
3
|
import { Metadata } from '../metadata.js';
|
|
4
4
|
import { Prerender } from '../prerender.js';
|
|
5
|
-
export type
|
|
5
|
+
export type RscPayload = {
|
|
6
6
|
returnValue?: {
|
|
7
7
|
ok: boolean;
|
|
8
8
|
data: unknown;
|
package/dist/internal/env/rsc.js
CHANGED
|
@@ -2,24 +2,24 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { renderToReadableStream } from '@vitejs/plugin-rsc/rsc';
|
|
3
3
|
import { Logger } from '../../utils/logger.js';
|
|
4
4
|
import { Solas } from '../../solas.js';
|
|
5
|
+
import { createHttpRouter } from '../http-router/create-http-router.js';
|
|
6
|
+
import { HttpRouter } from '../http-router/router.js';
|
|
7
|
+
import { normalisePathname } from '../http-router/utils.js';
|
|
5
8
|
import { Metadata } from '../metadata.js';
|
|
6
9
|
import { HttpException, isHttpException } from '../navigation/http-exception.js';
|
|
7
10
|
import { Prerender } from '../prerender.js';
|
|
8
11
|
import { Tree } from '../render/tree.js';
|
|
9
|
-
import {
|
|
10
|
-
import { Resolver } from '../router/resolver.js';
|
|
11
|
-
import { Router } from '../router/router.js';
|
|
12
|
-
import { normalisePathname } from '../router/utils.js';
|
|
12
|
+
import { Resolver } from '../resolver.js';
|
|
13
13
|
import { processActionRequest } from '../server/actions.js';
|
|
14
14
|
import DefaultErr from '../ui/defaults/error.js';
|
|
15
15
|
import { RequestContext } from './request-context.js';
|
|
16
16
|
import { getKnownDigest, isKnownError } from './utils.js';
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* Create the streamed RSC payload and response metadata for a single request.
|
|
19
19
|
* Resolves the route match, collects metadata, and returns the stream,
|
|
20
20
|
* status code, and prerender mode needed by the response layer
|
|
21
21
|
*/
|
|
22
|
-
async function
|
|
22
|
+
async function createPayload(req, manifest, importMap, baseMetadata, returnValue, formState, temporaryReferences) {
|
|
23
23
|
const resolver = new Resolver(manifest, importMap);
|
|
24
24
|
const logger = new Logger();
|
|
25
25
|
const prerender = req.headers.get(`x-${Solas.Config.SLUG}-prerender`) === '1';
|
|
@@ -27,12 +27,12 @@ async function getPayload(req, manifest, importMap, baseMetadata, returnValue, f
|
|
|
27
27
|
const pathname = url.pathname.endsWith('/') && url.pathname !== '/'
|
|
28
28
|
? url.pathname.slice(0, -1)
|
|
29
29
|
: url.pathname;
|
|
30
|
-
const match = resolver.enhance(resolver.reconcile(pathname, req[Solas.Config.
|
|
30
|
+
const match = resolver.enhance(resolver.reconcile(pathname, req[Solas.Config.REQUEST_META_KEY].match, req[Solas.Config.REQUEST_META_KEY].error));
|
|
31
31
|
// if there's no match then no user supplied error boundary
|
|
32
32
|
// has been found, and we should server render a default
|
|
33
33
|
// error screen
|
|
34
34
|
if (!match) {
|
|
35
|
-
const error = req[Solas.Config.
|
|
35
|
+
const error = req[Solas.Config.REQUEST_META_KEY].error ?? new HttpException(404, 'Not found');
|
|
36
36
|
const title = `${'status' in error ? `${error.status} -` : ''}${error.message}`;
|
|
37
37
|
const rscPayload = {
|
|
38
38
|
root: (_jsxs("html", { lang: "en", children: [
|
|
@@ -174,9 +174,9 @@ export function createHandler(config, manifest, importMap, artifactManifest = nu
|
|
|
174
174
|
temporaryReferences: undefined,
|
|
175
175
|
returnValue: undefined,
|
|
176
176
|
};
|
|
177
|
-
if (req[Solas.Config.
|
|
177
|
+
if (req[Solas.Config.REQUEST_META_KEY].action)
|
|
178
178
|
opts = await processActionRequest(req);
|
|
179
|
-
const { stream: rscStream, status, ppr, } = await
|
|
179
|
+
const { stream: rscStream, status, ppr, } = await createPayload(req, manifest, importMap, config.metadata, opts.returnValue, opts.formState, opts.temporaryReferences);
|
|
180
180
|
const stream = await rscStream;
|
|
181
181
|
if (!req.headers.get('accept')?.includes('text/html')) {
|
|
182
182
|
return new Response(stream, {
|
|
@@ -211,27 +211,20 @@ export function createHandler(config, manifest, importMap, artifactManifest = nu
|
|
|
211
211
|
});
|
|
212
212
|
}
|
|
213
213
|
const artifactManifestEntry = runtimePpr
|
|
214
|
-
? (artifactManifest?.
|
|
214
|
+
? (artifactManifest?.[lookupPath] ?? null)
|
|
215
215
|
: null;
|
|
216
|
-
|
|
217
|
-
if (artifactManifestEntry) {
|
|
218
|
-
tryPrelude = artifactManifestEntry.mode === 'ppr';
|
|
219
|
-
}
|
|
220
|
-
else if (runtimePpr) {
|
|
221
|
-
const artifactMetadata = await Prerender.Artifact.loadMetadata(Solas.Config.OUT_DIR, lookupPath);
|
|
222
|
-
tryPrelude =
|
|
223
|
-
!!artifactMetadata &&
|
|
224
|
-
Prerender.Artifact.isCompatible(artifactMetadata, lookupPath, 'ppr');
|
|
225
|
-
}
|
|
216
|
+
const tryPrelude = artifactManifestEntry?.mode === 'ppr';
|
|
226
217
|
if (tryPrelude) {
|
|
227
218
|
const postponedState = await Prerender.Artifact.loadPostponedState(Solas.Config.OUT_DIR, lookupPath);
|
|
228
219
|
const prelude = await Prerender.Artifact.loadPrelude(Solas.Config.OUT_DIR, lookupPath);
|
|
229
220
|
// resumable ppr responses splice fresh streamed content into the cached
|
|
230
221
|
// prelude when postponed state is available for this route
|
|
231
222
|
if (postponedState) {
|
|
223
|
+
// the cached prelude already carries the static payload, only needs to
|
|
224
|
+
// stream the html completions for postponed boundaries
|
|
232
225
|
const resumeStream = await mod.resume(stream, postponedState, {
|
|
233
226
|
nonce: undefined,
|
|
234
|
-
injectPayload:
|
|
227
|
+
injectPayload: false,
|
|
235
228
|
});
|
|
236
229
|
const body = prelude
|
|
237
230
|
? Prerender.Artifact.composePreludeAndResume(prelude, resumeStream)
|
|
@@ -259,7 +252,8 @@ export function createHandler(config, manifest, importMap, artifactManifest = nu
|
|
|
259
252
|
status,
|
|
260
253
|
});
|
|
261
254
|
}
|
|
262
|
-
const
|
|
255
|
+
const httpRouter = createHttpRouter(config, manifest, importMap, createResponse);
|
|
256
|
+
// vite-plugin-rsc entrypoint
|
|
263
257
|
return {
|
|
264
258
|
async fetch(req) {
|
|
265
259
|
const url = new URL(req.url);
|
|
@@ -280,13 +274,14 @@ export function createHandler(config, manifest, importMap, artifactManifest = nu
|
|
|
280
274
|
if (!import.meta.env.DEV &&
|
|
281
275
|
accept.includes('text/html') &&
|
|
282
276
|
req.headers.get(`x-${Solas.Config.SLUG}-prerender-artifact`) !== '1') {
|
|
277
|
+
// turn the request path into the normal route shape we use for artifact lookups
|
|
283
278
|
const lookupPath = normalisePathname(canonicalPath, prerenderPathMode);
|
|
284
|
-
|
|
285
|
-
const prerenderPath =
|
|
286
|
-
? Prerender.Artifact.getFilePath(Solas.Config.OUT_DIR, lookupPath,
|
|
279
|
+
// only full prerender routes have a saved html file we can serve directly
|
|
280
|
+
const prerenderPath = artifactManifest?.[lookupPath]?.mode === 'full'
|
|
281
|
+
? Prerender.Artifact.getFilePath(Solas.Config.OUT_DIR, lookupPath, Prerender.Artifact.FULL_PRERENDER_FILENAME)
|
|
287
282
|
: null;
|
|
288
283
|
if (prerenderPath) {
|
|
289
|
-
const res = await
|
|
284
|
+
const res = await HttpRouter.serve(prerenderPath, req, config.precompress, {
|
|
290
285
|
// avoid shared or proxy caching unless users opt into public caching later
|
|
291
286
|
'Cache-Control': 'private, no-store',
|
|
292
287
|
'Content-Type': 'text/html; charset=utf-8',
|
|
@@ -295,7 +290,7 @@ export function createHandler(config, manifest, importMap, artifactManifest = nu
|
|
|
295
290
|
return res;
|
|
296
291
|
}
|
|
297
292
|
}
|
|
298
|
-
return
|
|
293
|
+
return httpRouter.fetch(req);
|
|
299
294
|
},
|
|
300
295
|
};
|
|
301
296
|
}
|
|
@@ -8,7 +8,7 @@ type Opts = {
|
|
|
8
8
|
/**
|
|
9
9
|
* SSR handler - returns a ReadableStream response for HTML requests
|
|
10
10
|
*/
|
|
11
|
-
declare function ssr(rscStream: ReadableStream<Uint8Array>, opts?: Opts): Promise<ReadableStream<
|
|
11
|
+
declare function ssr(rscStream: ReadableStream<Uint8Array>, opts?: Opts): Promise<ReadableStream<Uint8Array<ArrayBufferLike>>>;
|
|
12
12
|
/**
|
|
13
13
|
* Build-time prerender artifact generation
|
|
14
14
|
* @description for PPR routes this returns static prelude HTML + opaque postponed state
|
|
@@ -33,7 +33,7 @@ declare function prerender(rscStream: ReadableStream<Uint8Array>, opts?: Opts):
|
|
|
33
33
|
*/
|
|
34
34
|
declare function resume(rscStream: ReadableStream<Uint8Array>, postponedState: unknown, opts?: Pick<Opts, 'nonce'> & {
|
|
35
35
|
injectPayload?: boolean;
|
|
36
|
-
}): Promise<ReadableStream<
|
|
36
|
+
}): Promise<import("react-dom/server").ReactDOMServerReadableStream | ReadableStream<Uint8Array<ArrayBufferLike>>>;
|
|
37
37
|
export type SSRModule = {
|
|
38
38
|
prerender: typeof prerender;
|
|
39
39
|
resume: typeof resume;
|
package/dist/internal/env/ssr.js
CHANGED
|
@@ -3,19 +3,19 @@ import { Suspense, use } from 'react';
|
|
|
3
3
|
import { resume as reactResume, renderToReadableStream } from 'react-dom/server.edge';
|
|
4
4
|
import { prerender as reactPrerender } from 'react-dom/static.edge';
|
|
5
5
|
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr';
|
|
6
|
-
import { injectRSCPayload } from 'rsc-html-stream/server';
|
|
7
6
|
import { Logger } from '../../utils/logger.js';
|
|
8
7
|
import { Solas } from '../../solas.js';
|
|
8
|
+
import { BrowserRouterProvider } from '../browser-router/router.js';
|
|
9
9
|
import { RedirectBoundary } from '../navigation/redirect-boundary.js';
|
|
10
10
|
import { Prerender } from '../prerender.js';
|
|
11
11
|
import { Head } from '../render/head.js';
|
|
12
|
-
import { RouterProvider } from '../router/router-provider.js';
|
|
13
12
|
import { ErrorBoundary } from '../ui/error-boundary.js';
|
|
14
|
-
import {
|
|
13
|
+
import { captureBuffered, injectPayload } from './flight.js';
|
|
14
|
+
import { getKnownDigest, isKnownError } from './utils.js';
|
|
15
15
|
const logger = new Logger();
|
|
16
16
|
function A({ payloadPromise }) {
|
|
17
17
|
const payload = use(payloadPromise);
|
|
18
|
-
return (_jsx(RedirectBoundary, { children: _jsxs(
|
|
18
|
+
return (_jsx(RedirectBoundary, { children: _jsxs(BrowserRouterProvider, { url: payload.url, children: [
|
|
19
19
|
_jsx(ErrorBoundary, { fallback: null, children: _jsx(Suspense, { fallback: null, children: _jsx(Head, { metadata: payload.metadata }) }) }), payload.root] }) }));
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
@@ -36,10 +36,12 @@ async function ssr(rscStream, opts = {}) {
|
|
|
36
36
|
const digest = getKnownDigest(err);
|
|
37
37
|
if (digest)
|
|
38
38
|
return digest;
|
|
39
|
+
if (isKnownError(err))
|
|
40
|
+
return;
|
|
39
41
|
logger.error('[ssr:ppr]', err);
|
|
40
42
|
},
|
|
41
43
|
});
|
|
42
|
-
return prelude.pipeThrough(
|
|
44
|
+
return prelude.pipeThrough(injectPayload(s2, { nonce }));
|
|
43
45
|
}
|
|
44
46
|
const htmlStream = await renderToReadableStream(_jsx(A, { payloadPromise: payloadPromise }), {
|
|
45
47
|
bootstrapScriptContent,
|
|
@@ -49,10 +51,12 @@ async function ssr(rscStream, opts = {}) {
|
|
|
49
51
|
const digest = getKnownDigest(err);
|
|
50
52
|
if (digest)
|
|
51
53
|
return digest;
|
|
54
|
+
if (isKnownError(err))
|
|
55
|
+
return;
|
|
52
56
|
logger.error('[ssr]', err);
|
|
53
57
|
},
|
|
54
58
|
});
|
|
55
|
-
return htmlStream.pipeThrough(
|
|
59
|
+
return htmlStream.pipeThrough(injectPayload(s2, { nonce }));
|
|
56
60
|
}
|
|
57
61
|
/**
|
|
58
62
|
* Build-time prerender artifact generation
|
|
@@ -85,6 +89,8 @@ async function prerender(rscStream, opts = {}) {
|
|
|
85
89
|
const digest = getKnownDigest(err);
|
|
86
90
|
if (digest)
|
|
87
91
|
return digest;
|
|
92
|
+
if (isKnownError(err))
|
|
93
|
+
return;
|
|
88
94
|
logger.error('[ssr:prerender:ppr]', err);
|
|
89
95
|
},
|
|
90
96
|
});
|
|
@@ -96,15 +102,18 @@ async function prerender(rscStream, opts = {}) {
|
|
|
96
102
|
route,
|
|
97
103
|
createdAt: Date.now(),
|
|
98
104
|
mode: 'full',
|
|
99
|
-
html: await new Response(prelude.pipeThrough(
|
|
105
|
+
html: await new Response(prelude.pipeThrough(injectPayload(s2, { nonce }))).text(),
|
|
100
106
|
};
|
|
101
107
|
}
|
|
108
|
+
// save the static payload rows in the cached prelude and leave the
|
|
109
|
+
// postponed work for the later resume response
|
|
110
|
+
const partialPayload = await captureBuffered(s2);
|
|
102
111
|
return {
|
|
103
112
|
schema,
|
|
104
113
|
route,
|
|
105
114
|
createdAt: Date.now(),
|
|
106
115
|
mode: 'ppr',
|
|
107
|
-
html: await new Response(prelude).text(),
|
|
116
|
+
html: await new Response(prelude.pipeThrough(injectPayload(partialPayload, { nonce }))).text(),
|
|
108
117
|
postponed,
|
|
109
118
|
};
|
|
110
119
|
}
|
|
@@ -114,6 +123,8 @@ async function prerender(rscStream, opts = {}) {
|
|
|
114
123
|
const digest = getKnownDigest(err);
|
|
115
124
|
if (digest)
|
|
116
125
|
return digest;
|
|
126
|
+
if (isKnownError(err))
|
|
127
|
+
return;
|
|
117
128
|
logger.error('[ssr:prerender:full]', err);
|
|
118
129
|
},
|
|
119
130
|
});
|
|
@@ -123,27 +134,30 @@ async function prerender(rscStream, opts = {}) {
|
|
|
123
134
|
route,
|
|
124
135
|
createdAt: Date.now(),
|
|
125
136
|
mode: 'full',
|
|
126
|
-
html: await new Response(stream.pipeThrough(
|
|
137
|
+
html: await new Response(stream.pipeThrough(injectPayload(s2, { nonce }))).text(),
|
|
127
138
|
};
|
|
128
139
|
}
|
|
129
140
|
/**
|
|
130
141
|
* Request-time resume for PPR routes
|
|
131
142
|
*/
|
|
132
143
|
async function resume(rscStream, postponedState, opts = {}) {
|
|
133
|
-
const { nonce, injectPayload = true } = opts;
|
|
134
144
|
const [s1, s2] = rscStream.tee();
|
|
135
145
|
const payloadPromise = createFromReadableStream(s1);
|
|
136
146
|
const htmlStream = await reactResume(_jsx(A, { payloadPromise: payloadPromise }), postponedState, {
|
|
137
|
-
nonce,
|
|
147
|
+
nonce: opts.nonce,
|
|
138
148
|
onError(err) {
|
|
139
149
|
const digest = getKnownDigest(err);
|
|
140
150
|
if (digest)
|
|
141
151
|
return digest;
|
|
152
|
+
if (isKnownError(err))
|
|
153
|
+
return;
|
|
142
154
|
logger.error('[ssr:resume]', err);
|
|
143
155
|
},
|
|
144
156
|
});
|
|
145
|
-
|
|
157
|
+
// cached ppr preludes already embed the static payload, so resume usually
|
|
158
|
+
// only needs to send the html completion scripts
|
|
159
|
+
if (opts.injectPayload === false)
|
|
146
160
|
return htmlStream;
|
|
147
|
-
return htmlStream.pipeThrough(
|
|
161
|
+
return htmlStream.pipeThrough(injectPayload(s2, { nonce: opts.nonce }));
|
|
148
162
|
}
|
|
149
163
|
export { prerender, resume, ssr };
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { HTTP_EXCEPTION_DIGEST_PREFIX } from '../navigation/http-exception.js';
|
|
2
2
|
import { REDIRECT_DIGEST_PREFIX } from '../navigation/redirect.js';
|
|
3
3
|
const possibilities = [HTTP_EXCEPTION_DIGEST_PREFIX, REDIRECT_DIGEST_PREFIX];
|
|
4
|
+
const RENDER_ABORT_MESSAGE = 'The render was aborted by the server without a reason';
|
|
5
|
+
function isRenderAbortMessage(value) {
|
|
6
|
+
return typeof value === 'string' && value.includes(RENDER_ABORT_MESSAGE);
|
|
7
|
+
}
|
|
4
8
|
export function getKnownDigest(err) {
|
|
5
9
|
if (typeof err === 'object' &&
|
|
6
10
|
err !== null &&
|
|
@@ -17,10 +21,18 @@ export function getKnownDigest(err) {
|
|
|
17
21
|
export function isKnownError(err) {
|
|
18
22
|
if (getKnownDigest(err))
|
|
19
23
|
return true;
|
|
24
|
+
if (isRenderAbortMessage(err))
|
|
25
|
+
return true;
|
|
26
|
+
if (typeof err === 'object' &&
|
|
27
|
+
err !== null &&
|
|
28
|
+
'message' in err &&
|
|
29
|
+
isRenderAbortMessage(err.message)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
20
32
|
if (err instanceof Error) {
|
|
21
33
|
if (err.name === 'AbortError')
|
|
22
34
|
return true;
|
|
23
|
-
if (err.message
|
|
35
|
+
if (isRenderAbortMessage(err.message)) {
|
|
24
36
|
return true;
|
|
25
37
|
}
|
|
26
38
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ImportMap, Manifest, PluginConfig, SolasRequest } from '../../types.js';
|
|
2
|
+
import { HttpRouter } from './router.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create the HTTP router from the generated manifest and import map
|
|
5
|
+
*/
|
|
6
|
+
export declare function createHttpRouter(config: Pick<PluginConfig, 'precompress' | 'trailingSlash'>, manifest: Manifest, importMap: ImportMap, rsc: (req: SolasRequest) => Response | Promise<Response>): HttpRouter;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HttpRouter } from './router.js';
|
|
2
2
|
function callEndpoint(fn, req) {
|
|
3
3
|
// endpoint modules may export either a zero-arg handler or one that expects the request
|
|
4
4
|
if (fn.length === 0)
|
|
@@ -44,14 +44,14 @@ function mergeMiddlewares(left, right) {
|
|
|
44
44
|
return merged;
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
* Create the
|
|
47
|
+
* Create the HTTP router from the generated manifest and import map
|
|
48
48
|
*/
|
|
49
|
-
export function
|
|
50
|
-
const router = new
|
|
49
|
+
export function createHttpRouter(config, manifest, importMap, rsc) {
|
|
50
|
+
const router = new HttpRouter({
|
|
51
51
|
trailingSlash: config.trailingSlash,
|
|
52
52
|
});
|
|
53
53
|
// static assets stay outside route middleware conventions and are registered once
|
|
54
|
-
router.add('/assets/*', 'GET',
|
|
54
|
+
router.add('/assets/*', 'GET', HttpRouter.static(config));
|
|
55
55
|
for (const [, group] of createHandlerGroups(manifest)) {
|
|
56
56
|
if (!Array.isArray(group)) {
|
|
57
57
|
if ('paths' in group) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpMethod, PluginConfig, SolasRequest } from '../../types.js';
|
|
2
|
-
export declare namespace
|
|
2
|
+
export declare namespace HttpRouter {
|
|
3
3
|
type Params = Record<string, string | string[]>;
|
|
4
4
|
type Handler = (req: SolasRequest) => Response | Promise<Response>;
|
|
5
5
|
type ErrorHandler = (err: Error, req: SolasRequest) => Response | Promise<Response>;
|
|
@@ -40,28 +40,28 @@ export declare namespace Router {
|
|
|
40
40
|
/**
|
|
41
41
|
* Handle routing and matching for server requests
|
|
42
42
|
*/
|
|
43
|
-
export declare class
|
|
43
|
+
export declare class HttpRouter {
|
|
44
44
|
#private;
|
|
45
|
-
opts:
|
|
46
|
-
constructor(opts?:
|
|
45
|
+
opts: HttpRouter.Options;
|
|
46
|
+
constructor(opts?: HttpRouter.Options);
|
|
47
47
|
/**
|
|
48
48
|
* Register middleware for all routes
|
|
49
49
|
*/
|
|
50
|
-
use(...middleware:
|
|
50
|
+
use(...middleware: HttpRouter.Middleware[]): this;
|
|
51
51
|
/**
|
|
52
52
|
* Register an error handler for routing failures
|
|
53
53
|
*/
|
|
54
|
-
error(handler:
|
|
54
|
+
error(handler: HttpRouter.ErrorHandler): this;
|
|
55
55
|
/**
|
|
56
56
|
* Register a route handler
|
|
57
57
|
*/
|
|
58
|
-
add(path: string, method: string, handler:
|
|
58
|
+
add(path: string, method: string, handler: HttpRouter.Handler, params?: string[], middleware?: HttpRouter.Middleware[]): this;
|
|
59
59
|
/**
|
|
60
60
|
* Match a path and method, returning params and route
|
|
61
61
|
*/
|
|
62
62
|
match(path: string, method: HttpMethod): {
|
|
63
|
-
route:
|
|
64
|
-
params:
|
|
63
|
+
route: HttpRouter.Route;
|
|
64
|
+
params: HttpRouter.Params;
|
|
65
65
|
} | null;
|
|
66
66
|
/**
|
|
67
67
|
* Handle an incoming request
|