@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli/build.d.ts +7 -0
  3. package/dist/cli/build.js +183 -0
  4. package/dist/cli/dev.d.ts +4 -0
  5. package/dist/cli/dev.js +13 -0
  6. package/dist/cli/preview.d.ts +1 -0
  7. package/dist/cli/preview.js +47 -0
  8. package/dist/cli.js +4 -238
  9. package/dist/index.js +2 -0
  10. package/dist/internal/browser-router/link.d.ts +17 -0
  11. package/dist/internal/{navigation → browser-router}/link.js +22 -16
  12. package/dist/internal/browser-router/router.d.ts +184 -0
  13. package/dist/internal/{router/router-provider.js → browser-router/router.js} +81 -12
  14. package/dist/internal/{router → browser-router}/use-router.d.ts +1 -1
  15. package/dist/internal/browser-router/use-router.js +5 -0
  16. package/dist/internal/{navigation → browser-router}/use-search-params.js +1 -1
  17. package/dist/internal/build.js +2 -2
  18. package/dist/internal/codegen/config.js +17 -8
  19. package/dist/internal/codegen/environments.js +7 -7
  20. package/dist/internal/codegen/manifest.js +3 -3
  21. package/dist/internal/codegen/maps.js +11 -15
  22. package/dist/internal/codegen/types.d.ts +5 -0
  23. package/dist/internal/codegen/types.js +48 -0
  24. package/dist/internal/codegen/utils.d.ts +10 -0
  25. package/dist/internal/codegen/utils.js +27 -2
  26. package/dist/internal/env/browser.js +6 -6
  27. package/dist/internal/env/flight.d.ts +29 -0
  28. package/dist/internal/env/flight.js +187 -0
  29. package/dist/internal/env/request-context.d.ts +1 -1
  30. package/dist/internal/env/rsc.d.ts +1 -1
  31. package/dist/internal/env/rsc.js +23 -28
  32. package/dist/internal/env/ssr.d.ts +2 -2
  33. package/dist/internal/env/ssr.js +27 -13
  34. package/dist/internal/env/utils.js +13 -1
  35. package/dist/internal/http-router/create-http-router.d.ts +6 -0
  36. package/dist/internal/{router/create-router.js → http-router/create-http-router.js} +5 -5
  37. package/dist/internal/{router → http-router}/router.d.ts +9 -9
  38. package/dist/internal/{router → http-router}/router.js +20 -19
  39. package/dist/internal/{router → http-router}/utils.d.ts +11 -3
  40. package/dist/internal/{router → http-router}/utils.js +9 -1
  41. package/dist/internal/metadata.js +10 -10
  42. package/dist/internal/prerender.d.ts +4 -9
  43. package/dist/internal/prerender.js +6 -23
  44. package/dist/internal/render/head.js +1 -1
  45. package/dist/internal/render/tree.d.ts +1 -1
  46. package/dist/internal/render/tree.js +17 -13
  47. package/dist/internal/{router/resolver.d.ts → resolver.d.ts} +41 -41
  48. package/dist/internal/{router/resolver.js → resolver.js} +7 -7
  49. package/dist/internal/server/actions.js +1 -1
  50. package/dist/internal/server/cookies.d.ts +3 -2
  51. package/dist/internal/server/cookies.js +4 -3
  52. package/dist/internal/server/dynamic.d.ts +1 -3
  53. package/dist/internal/server/dynamic.js +3 -11
  54. package/dist/internal/server/headers.d.ts +2 -2
  55. package/dist/internal/server/headers.js +3 -3
  56. package/dist/internal/server/url.d.ts +2 -2
  57. package/dist/internal/server/url.js +3 -3
  58. package/dist/navigation.d.ts +2 -4
  59. package/dist/navigation.js +2 -4
  60. package/dist/router.d.ts +3 -4
  61. package/dist/router.js +3 -4
  62. package/dist/solas.d.ts +3 -1
  63. package/dist/solas.js +1 -1
  64. package/dist/types.d.ts +15 -7
  65. package/dist/utils/logger.js +1 -1
  66. package/package.json +2 -7
  67. package/dist/internal/navigation/link.d.ts +0 -13
  68. package/dist/internal/router/create-router.d.ts +0 -6
  69. package/dist/internal/router/router-context.d.ts +0 -15
  70. package/dist/internal/router/router-context.js +0 -8
  71. package/dist/internal/router/router-provider.d.ts +0 -10
  72. package/dist/internal/router/use-router.js +0 -5
  73. /package/dist/internal/{navigation → browser-router}/use-search-params.d.ts +0 -0
  74. /package/dist/internal/{router/prefetcher.d.ts → prefetcher.d.ts} +0 -0
  75. /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 RSCPayload = {
5
+ export type RscPayload = {
6
6
  returnValue?: {
7
7
  ok: boolean;
8
8
  data: unknown;
@@ -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 { createRouter } from '../router/create-router.js';
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
- * Get the streamed RSC payload and response metadata for a single request.
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 getPayload(req, manifest, importMap, baseMetadata, returnValue, formState, temporaryReferences) {
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.REQUEST_META].match, req[Solas.Config.REQUEST_META].error));
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.REQUEST_META].error ?? new HttpException(404, 'Not found');
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.REQUEST_META].action)
177
+ if (req[Solas.Config.REQUEST_META_KEY].action)
178
178
  opts = await processActionRequest(req);
179
- const { stream: rscStream, status, ppr, } = await getPayload(req, manifest, importMap, config.metadata, opts.returnValue, opts.formState, opts.temporaryReferences);
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?.routes[lookupPath] ?? null)
214
+ ? (artifactManifest?.[lookupPath] ?? null)
215
215
  : null;
216
- let tryPrelude = false;
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: true,
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 router = createRouter(config, manifest, importMap, createResponse);
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
- const fullPrerenderFilename = artifactManifest?.routes[lookupPath]?.fullPrerenderFilename;
285
- const prerenderPath = fullPrerenderFilename
286
- ? Prerender.Artifact.getFilePath(Solas.Config.OUT_DIR, lookupPath, fullPrerenderFilename)
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 Router.serve(prerenderPath, req, config.precompress, {
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 router.fetch(req);
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<any>>;
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<any>>;
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;
@@ -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 { getKnownDigest } from './utils.js';
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(RouterProvider, { url: payload.url, children: [
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(injectRSCPayload(s2, { nonce }));
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(injectRSCPayload(s2, { nonce }));
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(injectRSCPayload(s2, { nonce }))).text(),
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(injectRSCPayload(s2, { nonce }))).text(),
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
- if (!injectPayload)
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(injectRSCPayload(s2, { nonce }));
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 === 'The render was aborted by the server without a reason') {
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 { Router } from './router.js';
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 application router from the generated manifest and import map
47
+ * Create the HTTP router from the generated manifest and import map
48
48
  */
49
- export function createRouter(config, manifest, importMap, rsc) {
50
- const router = new Router({
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', Router.static(config));
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 Router {
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 Router {
43
+ export declare class HttpRouter {
44
44
  #private;
45
- opts: Router.Options;
46
- constructor(opts?: Router.Options);
45
+ opts: HttpRouter.Options;
46
+ constructor(opts?: HttpRouter.Options);
47
47
  /**
48
48
  * Register middleware for all routes
49
49
  */
50
- use(...middleware: Router.Middleware[]): this;
50
+ use(...middleware: HttpRouter.Middleware[]): this;
51
51
  /**
52
52
  * Register an error handler for routing failures
53
53
  */
54
- error(handler: Router.ErrorHandler): this;
54
+ error(handler: HttpRouter.ErrorHandler): this;
55
55
  /**
56
56
  * Register a route handler
57
57
  */
58
- add(path: string, method: string, handler: Router.Handler, params?: string[], middleware?: Router.Middleware[]): this;
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: Router.Route;
64
- params: Router.Params;
63
+ route: HttpRouter.Route;
64
+ params: HttpRouter.Params;
65
65
  } | null;
66
66
  /**
67
67
  * Handle an incoming request