@jk2908/solas 0.3.1 → 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 +5 -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/browser-router/use-search-params.d.ts +1 -0
- package/dist/internal/browser-router/use-search-params.js +15 -0
- package/dist/internal/build.js +2 -2
- package/dist/internal/codegen/types.d.ts +5 -0
- package/dist/internal/codegen/types.js +48 -0
- 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 +0 -2
- package/dist/navigation.js +0 -2
- 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/navigation/use-search-params.d.ts +0 -11
- package/dist/internal/navigation/use-search-params.js +0 -34
- 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/{router/prefetcher.d.ts → prefetcher.d.ts} +0 -0
- /package/dist/internal/{router/prefetcher.js → prefetcher.js} +0 -0
|
@@ -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
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { match as createMatch } from 'path-to-regexp';
|
|
3
3
|
import { Solas } from '../../solas.js';
|
|
4
|
-
import { getAlternatePathname, normalisePathname, toPathPattern } from './utils.js';
|
|
5
4
|
import { HttpException } from '../navigation/http-exception.js';
|
|
6
5
|
import { maybeAction } from '../server/actions.js';
|
|
6
|
+
import { getAlternatePathname, normalisePathname, toPathPattern } from './utils.js';
|
|
7
7
|
/**
|
|
8
8
|
* Handle routing and matching for server requests
|
|
9
9
|
*/
|
|
10
|
-
export class
|
|
10
|
+
export class HttpRouter {
|
|
11
11
|
opts;
|
|
12
12
|
static #matchers = new WeakMap();
|
|
13
13
|
#routes = {
|
|
@@ -54,7 +54,7 @@ export class Router {
|
|
|
54
54
|
const routePath = !path.includes(':') && !path.includes('*')
|
|
55
55
|
? normalisePathname(path, this.opts.trailingSlash ?? 'never')
|
|
56
56
|
: path;
|
|
57
|
-
const segments =
|
|
57
|
+
const segments = HttpRouter.#split(routePath);
|
|
58
58
|
const tokens = [];
|
|
59
59
|
let score = 0;
|
|
60
60
|
let wildcard = false;
|
|
@@ -122,7 +122,7 @@ export class Router {
|
|
|
122
122
|
* Match a path and method, returning params and route
|
|
123
123
|
*/
|
|
124
124
|
match(path, method) {
|
|
125
|
-
for (const candidate of
|
|
125
|
+
for (const candidate of HttpRouter.#candidates(path)) {
|
|
126
126
|
const direct = this.#routes.static.get(`${method}:${candidate}`);
|
|
127
127
|
if (direct)
|
|
128
128
|
return { route: direct, params: {} };
|
|
@@ -133,25 +133,25 @@ export class Router {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
// else dynamic/wildcard match
|
|
136
|
-
const segments =
|
|
136
|
+
const segments = HttpRouter.#split(path);
|
|
137
137
|
// try the leading-static prefix bucket first
|
|
138
138
|
const prefixed = this.#routes.dynamic.byPrefix.get(segments[0] ?? '');
|
|
139
|
-
const prefixedMatch = prefixed ?
|
|
139
|
+
const prefixedMatch = prefixed ? HttpRouter.#pick(prefixed, segments, method) : null;
|
|
140
140
|
if (prefixedMatch)
|
|
141
141
|
return prefixedMatch;
|
|
142
142
|
// if the prefix bucket has no winner, fall back to all dynamic
|
|
143
143
|
// routes with the same segment count
|
|
144
|
-
const dynamicMatch =
|
|
144
|
+
const dynamicMatch = HttpRouter.#pick(this.#routes.dynamic.byLength.get(segments.length) ?? [], segments, method);
|
|
145
145
|
if (dynamicMatch)
|
|
146
146
|
return dynamicMatch;
|
|
147
147
|
// finally check wildcard routes, prefixed first, then fully generic ones
|
|
148
148
|
const wildcardPrefixed = this.#routes.wildcard.byPrefix.get(segments[0] ?? '');
|
|
149
149
|
const wildcardMatch = wildcardPrefixed
|
|
150
|
-
?
|
|
150
|
+
? HttpRouter.#pick(wildcardPrefixed, segments, method)
|
|
151
151
|
: null;
|
|
152
152
|
if (wildcardMatch)
|
|
153
153
|
return wildcardMatch;
|
|
154
|
-
const wildcardFallbackMatch =
|
|
154
|
+
const wildcardFallbackMatch = HttpRouter.#pick(this.#routes.wildcard.fallback, segments, method);
|
|
155
155
|
if (wildcardFallbackMatch)
|
|
156
156
|
return wildcardFallbackMatch;
|
|
157
157
|
// no match
|
|
@@ -192,14 +192,14 @@ export class Router {
|
|
|
192
192
|
// unmatched requests still pass through the shared error hook with the
|
|
193
193
|
// same request metadata shape as matched requests
|
|
194
194
|
return (this.#onError?.(error, Object.assign(req, {
|
|
195
|
-
[Solas.Config.
|
|
195
|
+
[Solas.Config.REQUEST_META_KEY]: { match: null, error, action },
|
|
196
196
|
})) ?? new Response(error.message, { status: error.status }));
|
|
197
197
|
}
|
|
198
198
|
const matched = match;
|
|
199
199
|
// attach routing state to the request once so middleware and handlers can
|
|
200
200
|
// read the same per-request metadata
|
|
201
201
|
const request = Object.assign(req, {
|
|
202
|
-
[Solas.Config.
|
|
202
|
+
[Solas.Config.REQUEST_META_KEY]: { match: matched, action, parsedFormData },
|
|
203
203
|
});
|
|
204
204
|
// global middleware stays outside route middleware by preserving
|
|
205
205
|
// registration order here before composition in #run
|
|
@@ -210,7 +210,7 @@ export class Router {
|
|
|
210
210
|
// normalise unknown throwables so the error hook always receives an Error
|
|
211
211
|
const error = err instanceof Error ? err : new Error(String(err), { cause: err });
|
|
212
212
|
const request = Object.assign(req, {
|
|
213
|
-
[Solas.Config.
|
|
213
|
+
[Solas.Config.REQUEST_META_KEY]: { match, error, action },
|
|
214
214
|
});
|
|
215
215
|
if (this.#onError)
|
|
216
216
|
return this.#onError(error, request);
|
|
@@ -270,7 +270,7 @@ export class Router {
|
|
|
270
270
|
return new Response('Forbidden', { status: 403 });
|
|
271
271
|
}
|
|
272
272
|
// emitted assets are fingerprinted so they can be cached aggressively
|
|
273
|
-
return
|
|
273
|
+
return HttpRouter.serve(filePath, req, config.precompress, {
|
|
274
274
|
'Cache-Control': 'public, immutable, max-age=31536000',
|
|
275
275
|
});
|
|
276
276
|
};
|
|
@@ -343,7 +343,7 @@ export class Router {
|
|
|
343
343
|
* Get or create a path matcher for a route using path-to-regexp
|
|
344
344
|
*/
|
|
345
345
|
static #getMatcher(route) {
|
|
346
|
-
const cached =
|
|
346
|
+
const cached = HttpRouter.#matchers.get(route);
|
|
347
347
|
if (cached)
|
|
348
348
|
return cached;
|
|
349
349
|
// convert route tokens back into a path pattern for path-to-regexp to compile
|
|
@@ -352,7 +352,7 @@ export class Router {
|
|
|
352
352
|
const matcher = createMatch(path, {
|
|
353
353
|
decode: false,
|
|
354
354
|
});
|
|
355
|
-
|
|
355
|
+
HttpRouter.#matchers.set(route, matcher);
|
|
356
356
|
return matcher;
|
|
357
357
|
}
|
|
358
358
|
/**
|
|
@@ -375,7 +375,8 @@ export class Router {
|
|
|
375
375
|
for (let index = 0; index < length; index += 1) {
|
|
376
376
|
// prefer static over dynamic and dynamic over wildcard at the
|
|
377
377
|
// first segment position where the two routes differ
|
|
378
|
-
const diff =
|
|
378
|
+
const diff = HttpRouter.#getTokenRank(a.tokens[index]) -
|
|
379
|
+
HttpRouter.#getTokenRank(b.tokens[index]);
|
|
379
380
|
if (diff !== 0)
|
|
380
381
|
return diff;
|
|
381
382
|
}
|
|
@@ -399,11 +400,11 @@ export class Router {
|
|
|
399
400
|
}
|
|
400
401
|
// skip routes that do not fit this path. Only compare specificity
|
|
401
402
|
// across matched routes
|
|
402
|
-
const params =
|
|
403
|
+
const params = HttpRouter.#fit(route, segments);
|
|
403
404
|
if (!params)
|
|
404
405
|
continue;
|
|
405
406
|
// replace the winner only when this route is strictly more specific
|
|
406
|
-
if (!best ||
|
|
407
|
+
if (!best || HttpRouter.#compare(route, best) > 0) {
|
|
407
408
|
best = route;
|
|
408
409
|
bestParams = params;
|
|
409
410
|
}
|
|
@@ -426,7 +427,7 @@ export class Router {
|
|
|
426
427
|
}
|
|
427
428
|
// defer the actual param extraction to the cached path-to-regexp matcher so
|
|
428
429
|
// dynamic and wildcard params stay consistent with registration
|
|
429
|
-
const matched =
|
|
430
|
+
const matched = HttpRouter.#getMatcher(route)(segments.length ? `/${segments.join('/')}` : '/');
|
|
430
431
|
if (!matched)
|
|
431
432
|
return null;
|
|
432
433
|
return matched.params;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PluginConfig } from '../../types.js';
|
|
2
2
|
export type PathPattern = {
|
|
3
3
|
path: string;
|
|
4
4
|
wildcardNames: Set<string>;
|
|
@@ -14,8 +14,16 @@ export declare function toPathPattern(route: string, paramNames?: string[]): {
|
|
|
14
14
|
/**
|
|
15
15
|
* Apply the configured trailing-slash policy to a pathname
|
|
16
16
|
*/
|
|
17
|
-
export declare function normalisePathname(pathname: string, trailingSlash?:
|
|
17
|
+
export declare function normalisePathname(pathname: string, trailingSlash?: PluginConfig['trailingSlash']): string;
|
|
18
18
|
/**
|
|
19
|
-
* Return the other pathname shape for a non-root route
|
|
19
|
+
* Return the other pathname shape for a non-root route. For use within
|
|
20
|
+
* trailingSlash logic to easily switch between shapes
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* getAlternatePathname('/about') // '/about/'
|
|
25
|
+
* getAlternatePathname('/about/') // '/about'
|
|
26
|
+
* getAlternatePathname('/') // '/'
|
|
27
|
+
* ```
|
|
20
28
|
*/
|
|
21
29
|
export declare function getAlternatePathname(pathname: string): string;
|
|
@@ -53,7 +53,15 @@ export function normalisePathname(pathname, trailingSlash = 'never') {
|
|
|
53
53
|
return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* Return the other pathname shape for a non-root route
|
|
56
|
+
* Return the other pathname shape for a non-root route. For use within
|
|
57
|
+
* trailingSlash logic to easily switch between shapes
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* getAlternatePathname('/about') // '/about/'
|
|
62
|
+
* getAlternatePathname('/about/') // '/about'
|
|
63
|
+
* getAlternatePathname('/') // '/'
|
|
64
|
+
* ```
|
|
57
65
|
*/
|
|
58
66
|
export function getAlternatePathname(pathname) {
|
|
59
67
|
if (pathname === '/')
|
|
@@ -85,15 +85,6 @@ var Metadata;
|
|
|
85
85
|
metadata.link = [...linkMap.values()];
|
|
86
86
|
return metadata;
|
|
87
87
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Clones an object using structuredClone w/ JSON fallback
|
|
90
|
-
*/
|
|
91
|
-
static #clone(obj) {
|
|
92
|
-
if (typeof structuredClone === 'function') {
|
|
93
|
-
return structuredClone(obj);
|
|
94
|
-
}
|
|
95
|
-
return JSON.parse(JSON.stringify(obj));
|
|
96
|
-
}
|
|
97
88
|
/**
|
|
98
89
|
* Gets a unique key for the meta tag
|
|
99
90
|
*/
|
|
@@ -123,6 +114,15 @@ var Metadata;
|
|
|
123
114
|
}
|
|
124
115
|
return this;
|
|
125
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Clones an object using structuredClone w/ JSON fallback
|
|
119
|
+
*/
|
|
120
|
+
static #clone(obj) {
|
|
121
|
+
if (typeof structuredClone === 'function') {
|
|
122
|
+
return structuredClone(obj);
|
|
123
|
+
}
|
|
124
|
+
return JSON.parse(JSON.stringify(obj));
|
|
125
|
+
}
|
|
126
126
|
/**
|
|
127
127
|
* Merges metadata from all sources, sorted by priority
|
|
128
128
|
*/
|
|
@@ -130,7 +130,7 @@ var Metadata;
|
|
|
130
130
|
const items = [...this.#collection].sort((a, b) => a.priority - b.priority);
|
|
131
131
|
if (items.length === 0)
|
|
132
132
|
return Collection.#clone(this.#base);
|
|
133
|
-
let merged =
|
|
133
|
+
let merged = this.#base;
|
|
134
134
|
const res = await Promise.allSettled(items.map(item => item.task));
|
|
135
135
|
const ok = res
|
|
136
136
|
.filter((result) => result.status === 'fulfilled')
|
|
@@ -14,14 +14,9 @@ export declare namespace Prerender {
|
|
|
14
14
|
type Metadata = Pick<Value, 'schema' | 'route' | 'createdAt' | 'mode'>;
|
|
15
15
|
type ManifestEntry = {
|
|
16
16
|
mode: Mode;
|
|
17
|
-
|
|
18
|
-
files?: File[];
|
|
19
|
-
fullPrerenderFilename?: string;
|
|
20
|
-
};
|
|
21
|
-
type Manifest = {
|
|
22
|
-
generatedAt: number;
|
|
23
|
-
routes: Record<string, ManifestEntry>;
|
|
17
|
+
files?: readonly File[];
|
|
24
18
|
};
|
|
19
|
+
type Manifest = Record<string, ManifestEntry>;
|
|
25
20
|
/**
|
|
26
21
|
* Get the root directory path where prerender artifacts are stored,
|
|
27
22
|
* based on the output directory specified in the configuration
|
|
@@ -41,9 +36,9 @@ export declare namespace Prerender {
|
|
|
41
36
|
*/
|
|
42
37
|
function getFilePath(outDir: string, pathname: string, fileName: string): string;
|
|
43
38
|
/**
|
|
44
|
-
*
|
|
39
|
+
* File name used for saved full-prerender html inside each route artifact directory
|
|
45
40
|
*/
|
|
46
|
-
|
|
41
|
+
const FULL_PRERENDER_FILENAME = "prerendered.html";
|
|
47
42
|
/**
|
|
48
43
|
* Load the prerender artifact manifest for faster runtime route mode checks
|
|
49
44
|
*/
|