@jk2908/solas 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +66 -6
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +16 -2
  5. package/dist/internal/browser-router/link.d.ts +1 -1
  6. package/dist/internal/browser-router/link.js +1 -1
  7. package/dist/internal/browser-router/router.d.ts +2 -165
  8. package/dist/internal/browser-router/router.js +3 -99
  9. package/dist/internal/browser-router/shared.d.ts +169 -0
  10. package/dist/internal/browser-router/shared.js +71 -0
  11. package/dist/internal/browser-router/use-router.d.ts +1 -1
  12. package/dist/internal/codegen/environments.js +5 -4
  13. package/dist/internal/env/rsc.d.ts +2 -2
  14. package/dist/internal/env/rsc.js +82 -22
  15. package/dist/internal/http-router/create-http-router.d.ts +1 -1
  16. package/dist/internal/http-router/create-http-router.js +4 -2
  17. package/dist/internal/http-router/router.d.ts +4 -14
  18. package/dist/internal/http-router/router.js +32 -59
  19. package/dist/internal/navigation/http-exception.d.ts +4 -4
  20. package/dist/internal/navigation/http-exception.js +4 -5
  21. package/dist/internal/postbuild.d.ts +1 -0
  22. package/dist/{cli/build.js → internal/postbuild.js} +13 -48
  23. package/dist/internal/prerender.d.ts +4 -19
  24. package/dist/internal/prerender.js +8 -98
  25. package/dist/internal/public-files.d.ts +18 -0
  26. package/dist/internal/public-files.js +63 -0
  27. package/dist/internal/resolver.d.ts +23 -23
  28. package/dist/internal/server/actions.d.ts +2 -5
  29. package/dist/internal/server/actions.js +4 -35
  30. package/dist/internal/server/csrf.d.ts +14 -0
  31. package/dist/internal/server/csrf.js +98 -0
  32. package/dist/router.d.ts +1 -0
  33. package/dist/router.js +1 -0
  34. package/dist/solas.d.ts +12 -1
  35. package/dist/solas.js +116 -1
  36. package/dist/types.d.ts +8 -3
  37. package/dist/utils/base-path.d.ts +14 -0
  38. package/dist/utils/base-path.js +85 -0
  39. package/package.json +3 -6
  40. package/dist/cli/build.d.ts +0 -7
  41. package/dist/cli/dev.d.ts +0 -4
  42. package/dist/cli/dev.js +0 -13
  43. package/dist/cli/preview.d.ts +0 -1
  44. package/dist/cli/preview.js +0 -47
  45. package/dist/cli.d.ts +0 -2
  46. package/dist/cli.js +0 -28
@@ -0,0 +1,169 @@
1
+ import { Solas } from '../../solas.js';
2
+ export declare namespace BrowserRouter {
3
+ export type Params = Record<string, string>;
4
+ export type Query = Record<string, string | number | boolean>;
5
+ export type Path = keyof Solas.Routes & string;
6
+ type Replace = {
7
+ replace?: boolean;
8
+ };
9
+ export type GoOptions = {
10
+ replace?: boolean;
11
+ query?: Query;
12
+ params?: Params;
13
+ };
14
+ /**
15
+ * These targets are used as-is. They are not matched against the route table,
16
+ * so this covers normal external URLs and hash-only links
17
+ */
18
+ export type ExternalTarget = `${string}:${string}` | `//${string}` | `#${string}`;
19
+ export function isHashOnlyTarget(target: string): boolean;
20
+ export function isExternalTarget(target: string, origin: string): boolean;
21
+ /**
22
+ * Turn a route pattern into the real path shape a caller can use. In practice,
23
+ * every ':param' or '*' part becomes a plain string slot
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * // '/p/:id' becomes '/p/${string}'
28
+ * // '/test/*' becomes '/test/${string}'
29
+ * // '/posts' stays '/posts'
30
+ * ```
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * type A = ResolvedPath<'/posts/:id'>
35
+ * // '/posts/${string}'
36
+ * ```
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * type B = ResolvedPath<'/docs/*'>
41
+ * // '/docs/${string}'
42
+ * ```
43
+ */
44
+ export type ResolvedPath<TPath extends string> = TPath extends `${infer Start}:${string}/${infer Rest}` ? `${Start}${string}/${ResolvedPath<Rest>}` : TPath extends `${infer Start}:${string}` ? `${Start}${string}` : TPath extends `${infer Start}*${infer Rest}` ? `${Start}${string}${ResolvedPath<Rest>}` : TPath;
45
+ /**
46
+ * Once we have a real path, also allow the usual query-string and hash forms
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * type A = TargetSuffix<'/posts/123'>
51
+ * // '/posts/123' | '/posts/123?${string}' | '/posts/123#${string}' | '/posts/123?${string}#${string}'
52
+ * ```
53
+ */
54
+ export type TargetSuffix<TPath extends string> = TPath | `${TPath}?${string}` | `${TPath}#${string}` | `${TPath}?${string}#${string}`;
55
+ /**
56
+ * This is the final string form a caller can navigate to. It can be an external
57
+ * URL, or a concrete URL that matches one of the known routes
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * type A = Target
62
+ * // 'https://example.com'
63
+ * // '#intro'
64
+ * // '/posts/123'
65
+ * // '/posts/123?draft=true'
66
+ * ```
67
+ */
68
+ export type Target = ExternalTarget | TargetSuffix<ResolvedPath<Path>>;
69
+ /**
70
+ * Extra options for callers who already have a finished target string
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const a: TargetConfig = { query: { page: 2 } }
75
+ * // params is rejected here because the path is already complete
76
+ * ```
77
+ */
78
+ type TargetConfig = {
79
+ params?: never;
80
+ query?: Query;
81
+ };
82
+ /**
83
+ * Extra options for callers who pass a route pattern and params separately.
84
+ * If the route definition says that route needs params, this type makes
85
+ * those params required. If the route has no params, it rejects them
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * // if Solas.Routes['/posts/:id'] is { params: { id: string } }
90
+ * type A = PatternConfig<'/posts/:id'>
91
+ * // { query?: Query } & { params: { id: string } }
92
+ * ```
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * // if Solas.Routes['/about'] has no params field
97
+ * type B = PatternConfig<'/about'>
98
+ * // { query?: Query } & { params?: never }
99
+ * ```
100
+ */
101
+ type PatternConfig<TPath extends Path> = {
102
+ query?: Query;
103
+ } & (Solas.Routes[TPath] extends {
104
+ params: infer TParams extends Params;
105
+ } ? {
106
+ params: TParams;
107
+ } : {
108
+ params?: never;
109
+ });
110
+ /**
111
+ * Typed <Link /> props, using `href` instead of the internal `to` name
112
+ *
113
+ * `query` is always allowed
114
+ * `params` are only allowed when `href` is a known route pattern
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * const a: LinkProps = { href: '/posts/:id', params: { id: '123' } }
119
+ * const b: LinkProps = { href: '/posts/123?draft=true' }
120
+ * ```
121
+ */
122
+ export type LinkProps = ({
123
+ href: Target;
124
+ } & TargetConfig) | (keyof Solas.Routes extends never ? never : {
125
+ [TPath in Path]: {
126
+ href: TPath;
127
+ } & PatternConfig<TPath>;
128
+ }[Path]);
129
+ /**
130
+ * Typed input for router.go(), using the same route rules as <Link />
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * go('/p/post-2')
135
+ * go('/?foo=bar', { replace: true })
136
+ * ```
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * go('/p/:id', { params: { id: 'post-2' }, replace: true })
141
+ * ```
142
+ *
143
+ * The last overload is the fallback for plain `string` values. The
144
+ * `string extends TTo` check stops that fallback from taking over
145
+ * when TypeScript already knows the caller passed a more specific
146
+ * string literal
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * declare const dynamicPath: string
151
+ * go(dynamicPath, { replace: true })
152
+ * ```
153
+ */
154
+ export type Go = {
155
+ <TTo extends Path>(to: TTo, opts?: PatternConfig<TTo> & Replace): Promise<string>;
156
+ <TTo extends Target>(to: TTo, opts?: TargetConfig & Replace): Promise<string>;
157
+ <TTo extends string>(to: string extends TTo ? TTo : never, opts?: GoOptions): Promise<string>;
158
+ };
159
+ /**
160
+ * Convert a route pattern and params into a real path string. This is used internally
161
+ * to implement <Link /> and router.go
162
+ */
163
+ export function toTarget(path: string, params?: Record<string, string>, query?: Query): string;
164
+ export {};
165
+ }
166
+ /**
167
+ * Apply the base path to a target string when needed
168
+ */
169
+ export declare function withBase(target: string): string;
@@ -0,0 +1,71 @@
1
+ import { BasePath } from '../../utils/base-path.js';
2
+ const BASE_PATH = BasePath.normalise(import.meta.env.BASE_URL);
3
+ export { BrowserRouter };
4
+ var BrowserRouter;
5
+ (function (BrowserRouter) {
6
+ function isHashOnlyTarget(target) {
7
+ return target.startsWith('#');
8
+ }
9
+ BrowserRouter.isHashOnlyTarget = isHashOnlyTarget;
10
+ function isExternalTarget(target, origin) {
11
+ if (isHashOnlyTarget(target))
12
+ return false;
13
+ try {
14
+ return new URL(target, origin).origin !== origin;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ BrowserRouter.isExternalTarget = isExternalTarget;
21
+ /**
22
+ * Convert a route pattern and params into a real path string. This is used internally
23
+ * to implement <Link /> and router.go
24
+ */
25
+ function toTarget(path, params, query) {
26
+ const used = new Set();
27
+ let to = path.replaceAll(/:([A-Za-z0-9_]+)/g, (_, key) => {
28
+ const value = params?.[key];
29
+ if (value == null) {
30
+ throw new Error(`[Link]: missing route param: ${key}`);
31
+ }
32
+ used.add(key);
33
+ return encodeURIComponent(value);
34
+ });
35
+ if (to.includes('*')) {
36
+ const remaining = Object.entries(params ?? {}).filter(([key]) => !used.has(key));
37
+ if (remaining.length !== 1) {
38
+ throw new Error('[Link]: wildcard routes require exactly one unmatched param');
39
+ }
40
+ to = to.replace('*', remaining[0][1].split('/').map(encodeURIComponent).join('/'));
41
+ }
42
+ if (!query)
43
+ return withBase(to);
44
+ const hashIndex = to.indexOf('#');
45
+ const hash = hashIndex >= 0 ? to.slice(hashIndex) : '';
46
+ const pathWithSearch = hashIndex >= 0 ? to.slice(0, hashIndex) : to;
47
+ const searchIndex = pathWithSearch.indexOf('?');
48
+ const pathname = searchIndex >= 0 ? pathWithSearch.slice(0, searchIndex) : pathWithSearch;
49
+ const currentSearch = searchIndex >= 0 ? pathWithSearch.slice(searchIndex + 1) : '';
50
+ const search = new URLSearchParams(currentSearch);
51
+ for (const [key, value] of Object.entries(query)) {
52
+ search.set(key, String(value));
53
+ }
54
+ const value = search.toString();
55
+ return withBase(`${pathname}${value.length > 0 ? `?${value}` : ''}${hash}`);
56
+ }
57
+ BrowserRouter.toTarget = toTarget;
58
+ })(BrowserRouter || (BrowserRouter = {}));
59
+ /**
60
+ * Apply the base path to a target string when needed
61
+ */
62
+ export function withBase(target) {
63
+ if (BrowserRouter.isHashOnlyTarget(target))
64
+ return target;
65
+ if (target.startsWith('//') || /^[A-Za-z][A-Za-z\d+.-]*:/.test(target))
66
+ return target;
67
+ const suffixIndex = target.search(/[?#]/);
68
+ const pathname = suffixIndex === -1 ? target : target.slice(0, suffixIndex);
69
+ const suffix = suffixIndex === -1 ? '' : target.slice(suffixIndex);
70
+ return `${BasePath.apply(pathname || '/', BASE_PATH)}${suffix}`;
71
+ }
@@ -1,5 +1,5 @@
1
1
  export declare function useRouter(): {
2
- go: import("./router.js").BrowserRouter.Go;
2
+ go: import("./shared.js").BrowserRouter.Go;
3
3
  prefetch: (path: string) => void;
4
4
  isNavigating: boolean;
5
5
  url: {
@@ -8,18 +8,19 @@ export function writeRSCEntry() {
8
8
  ${AUTOGEN_MSG}
9
9
 
10
10
  import { createHandler } from '${Solas.Config.PKG_NAME}/env/rsc'
11
- import { Prerender } from '${Solas.Config.PKG_NAME}/prerender'
12
11
  import { Solas } from '${Solas.Config.PKG_NAME}'
13
12
 
14
13
  import { manifest } from './manifest.js'
15
14
  import { importMap } from './maps.js'
16
15
  import { config } from './config.js'
17
16
 
18
- const artifactManifest = await Prerender.Artifact.loadManifest(Solas.Config.OUT_DIR)
17
+ const runtimeManifest = await Solas.Runtime.loadManifest(Solas.Config.OUT_DIR)
19
18
 
20
- export default createHandler(config, manifest, importMap, artifactManifest)
19
+ export default createHandler(config, manifest, importMap, runtimeManifest)
21
20
 
22
- import.meta.hot?.accept()
21
+ if (import.meta.hot) {
22
+ import.meta.hot.accept()
23
+ }
23
24
  `;
24
25
  }
25
26
  /**
@@ -1,7 +1,7 @@
1
1
  import type { ReactFormState } from 'react-dom/client';
2
2
  import type { ImportMap, Manifest, RuntimeConfig } from '../../types.js';
3
+ import { Solas } from '../../solas.js';
3
4
  import { Metadata } from '../metadata.js';
4
- import { Prerender } from '../prerender.js';
5
5
  export type RscPayload = {
6
6
  returnValue?: {
7
7
  ok: boolean;
@@ -20,6 +20,6 @@ export type RscPayload = {
20
20
  * route manifest, and import map to build the router once, then returns an object
21
21
  * with a fetch method that handles requests
22
22
  */
23
- export declare function createHandler(config: RuntimeConfig, manifest: Manifest, importMap: ImportMap, artifactManifest?: Prerender.Artifact.Manifest | null): {
23
+ export declare function createHandler(config: RuntimeConfig, manifest: Manifest, importMap: ImportMap, runtimeManifest?: Solas.Runtime.Manifest | null): {
24
24
  fetch(req: Request): Promise<Response>;
25
25
  };
@@ -1,5 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import path from 'node:path';
2
3
  import { renderToReadableStream } from '@vitejs/plugin-rsc/rsc';
4
+ import { BasePath } from '../../utils/base-path.js';
3
5
  import { Logger } from '../../utils/logger.js';
4
6
  import { Solas } from '../../solas.js';
5
7
  import { createHttpRouter } from '../http-router/create-http-router.js';
@@ -15,6 +17,22 @@ import DefaultErr from '../ui/defaults/error.js';
15
17
  import { RequestContext } from './request-context.js';
16
18
  import { getKnownDigest, isKnownError } from './utils.js';
17
19
  const logger = new Logger();
20
+ const BASE_PATH = BasePath.normalise(import.meta.env.BASE_URL);
21
+ function resolveFilePath(root, relativePath) {
22
+ try {
23
+ const decodedPath = decodeURIComponent(relativePath);
24
+ if (!decodedPath)
25
+ return new Response('Forbidden', { status: 403 });
26
+ const filePath = path.resolve(root, decodedPath);
27
+ if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) {
28
+ return new Response('Forbidden', { status: 403 });
29
+ }
30
+ return filePath;
31
+ }
32
+ catch {
33
+ return new Response('Bad Request', { status: 400 });
34
+ }
35
+ }
18
36
  /**
19
37
  * Create the streamed RSC payload and response metadata for a single request.
20
38
  * Resolves the route match, collects metadata, and returns the stream,
@@ -24,9 +42,8 @@ async function createPayload(req, manifest, importMap, baseMetadata, returnValue
24
42
  const resolver = new Resolver(manifest, importMap);
25
43
  const prerender = req.headers.get(`x-${Solas.Config.SLUG}-prerender`) === '1';
26
44
  const url = new URL(req.url);
27
- const pathname = url.pathname.endsWith('/') && url.pathname !== '/'
28
- ? url.pathname.slice(0, -1)
29
- : url.pathname;
45
+ const routedPath = BasePath.strip(url.pathname, BASE_PATH) ?? url.pathname;
46
+ const pathname = routedPath.endsWith('/') && routedPath !== '/' ? routedPath.slice(0, -1) : routedPath;
30
47
  const match = resolver.enhance(resolver.reconcile(pathname, req[Solas.Config.REQUEST_META_KEY].match, req[Solas.Config.REQUEST_META_KEY].error));
31
48
  // if there's no match then no user supplied error boundary
32
49
  // has been found, and we should server render a default
@@ -141,7 +158,12 @@ async function createPayload(req, manifest, importMap, baseMetadata, returnValue
141
158
  * route manifest, and import map to build the router once, then returns an object
142
159
  * with a fetch method that handles requests
143
160
  */
144
- export function createHandler(config, manifest, importMap, artifactManifest = null) {
161
+ export function createHandler(config, manifest, importMap, runtimeManifest = null) {
162
+ const CLIENT_OUTPUT_DIR = path.resolve(Solas.Config.OUT_DIR, 'client');
163
+ // vite emits solas-controlled assets under dist/client/_solas
164
+ const SOLAS_ASSETS_DIR = path.resolve(CLIENT_OUTPUT_DIR, Solas.Config.ASSETS_DIR);
165
+ // requests for /_solas and /_solas/* are reserved
166
+ const SOLAS_ASSETS_URL_ROOT = `/${Solas.Config.ASSETS_DIR}`;
145
167
  const prerenderPathMode = config.trailingSlash === 'always' ? 'always' : 'never';
146
168
  /**
147
169
  * Create the HTTP response for a single incoming request. Runs actions when needed,
@@ -154,8 +176,12 @@ export function createHandler(config, manifest, importMap, artifactManifest = nu
154
176
  temporaryReferences: undefined,
155
177
  returnValue: undefined,
156
178
  };
157
- if (req[Solas.Config.REQUEST_META_KEY].action)
158
- opts = await processActionRequest(req);
179
+ if (req[Solas.Config.REQUEST_META_KEY].action) {
180
+ opts = await processActionRequest(req, {
181
+ trustedOrigins: config.trustedOrigins,
182
+ url: config.url,
183
+ });
184
+ }
159
185
  const { stream: rscStream, status, ppr, } = await createPayload(req, manifest, importMap, config.metadata, opts.returnValue, opts.formState, opts.temporaryReferences);
160
186
  const stream = await rscStream;
161
187
  if (!req.headers.get('accept')?.includes('text/html')) {
@@ -191,10 +217,10 @@ export function createHandler(config, manifest, importMap, artifactManifest = nu
191
217
  });
192
218
  }
193
219
  try {
194
- const artifactManifestEntry = runtimePpr
195
- ? (artifactManifest?.[lookupPath] ?? null)
220
+ const artifactEntry = runtimePpr
221
+ ? (runtimeManifest?.artifacts[lookupPath] ?? null)
196
222
  : null;
197
- const tryPrelude = artifactManifestEntry?.mode === 'ppr';
223
+ const tryPrelude = artifactEntry?.mode === 'ppr';
198
224
  if (tryPrelude) {
199
225
  const postponedState = await Prerender.Artifact.loadPostponedState(Solas.Config.OUT_DIR, lookupPath);
200
226
  const prelude = await Prerender.Artifact.loadPrelude(Solas.Config.OUT_DIR, lookupPath);
@@ -234,8 +260,8 @@ export function createHandler(config, manifest, importMap, artifactManifest = nu
234
260
  });
235
261
  }
236
262
  catch (err) {
237
- // resume/ssr can be the first place react surfaces an http exception from abort(...),
238
- // after the initial rsc pass was streamed without request error state. Rerun once
263
+ // resume/ssr can be the first place React surfaces an HttpException from abort(...),
264
+ // after the initial RSC pass was streamed without request error state. Rerun once
239
265
  // with that error attached so createPayload rebuilds the same route through its
240
266
  // nearest matching HttpExceptionBoundary. If request meta already has an error
241
267
  // or the error is not an HttpException, then this is a real failure
@@ -272,35 +298,69 @@ export function createHandler(config, manifest, importMap, artifactManifest = nu
272
298
  return {
273
299
  async fetch(req) {
274
300
  const url = new URL(req.url);
275
- const accept = req.headers.get('accept') ?? '';
276
301
  const method = req.method.toUpperCase();
277
- const canonicalPath = config.trailingSlash === 'ignore'
278
- ? url.pathname
279
- : normalisePathname(url.pathname, config.trailingSlash);
280
- if ((method === 'GET' || method === 'HEAD') &&
302
+ // fast path
303
+ if (method !== 'GET' && method !== 'HEAD')
304
+ return httpRouter.fetch(req);
305
+ const accept = req.headers.get('accept') ?? '';
306
+ const routedPath = BasePath.strip(url.pathname, BASE_PATH);
307
+ const canonicalPath = routedPath == null
308
+ ? null
309
+ : config.trailingSlash === 'ignore'
310
+ ? routedPath
311
+ : normalisePathname(routedPath, config.trailingSlash);
312
+ const canonicalPathname = canonicalPath == null ? null : BasePath.apply(canonicalPath, BASE_PATH);
313
+ if (canonicalPathname != null &&
314
+ (method === 'GET' || method === 'HEAD') &&
281
315
  config.trailingSlash !== 'ignore' &&
282
- canonicalPath !== url.pathname) {
283
- url.pathname = canonicalPath;
316
+ canonicalPathname !== url.pathname) {
317
+ url.pathname = canonicalPathname;
284
318
  return Response.redirect(url.toString(), 308);
285
319
  }
320
+ // block the bare /_solas namespace; only concrete solas asset files
321
+ // under /_solas/* are valid
322
+ if (routedPath === SOLAS_ASSETS_URL_ROOT) {
323
+ return new Response('Forbidden', { status: 403 });
324
+ }
325
+ if (routedPath?.startsWith(`${SOLAS_ASSETS_URL_ROOT}/`)) {
326
+ const resolvedPath = resolveFilePath(SOLAS_ASSETS_DIR, routedPath.slice(`${SOLAS_ASSETS_URL_ROOT}/`.length));
327
+ // pass through bad-request or forbidden responses from path resolution
328
+ if (resolvedPath instanceof Response)
329
+ return resolvedPath;
330
+ return HttpRouter.serveStatic(resolvedPath, req, config.precompress, {
331
+ 'Cache-Control': 'public, immutable, max-age=31536000',
332
+ });
333
+ }
334
+ if (routedPath && runtimeManifest?.publicFiles.has(routedPath)) {
335
+ const resolvedPath = resolveFilePath(CLIENT_OUTPUT_DIR, routedPath.slice(1));
336
+ // pass through bad-request or forbidden responses from path resolution
337
+ if (resolvedPath instanceof Response)
338
+ return resolvedPath;
339
+ return HttpRouter.serveStatic(resolvedPath, req, config.precompress);
340
+ }
286
341
  // fully prerendered html can be served straight from disk for normal
287
342
  // document requests, but build-time artifact requests must bypass
288
343
  // this shortcut so they still render fresh output
289
- if (!import.meta.env.DEV &&
344
+ if (canonicalPath != null &&
345
+ !import.meta.env.DEV &&
290
346
  accept.includes('text/html') &&
291
347
  req.headers.get(`x-${Solas.Config.SLUG}-prerender-artifact`) !== '1') {
292
348
  // turn the request path into the normal route shape we use for artifact lookups
293
349
  const lookupPath = normalisePathname(canonicalPath, prerenderPathMode);
294
350
  // only full prerender routes have a saved html file we can serve directly
295
- const prerenderPath = artifactManifest?.[lookupPath]?.mode === 'full'
351
+ const prerenderPath = runtimeManifest?.artifacts[lookupPath]?.mode === 'full'
296
352
  ? Prerender.Artifact.getFilePath(Solas.Config.OUT_DIR, lookupPath, Prerender.Artifact.FULL_PRERENDER_FILENAME)
297
353
  : null;
298
354
  if (prerenderPath) {
299
- const res = await HttpRouter.serve(prerenderPath, req, config.precompress, {
300
- // avoid shared or proxy caching unless users opt into public caching later
355
+ const res = await HttpRouter.serveStatic(prerenderPath, req, config.precompress, {
356
+ // keep prerendered html out of shared caches unless users opt into explicit public caching
357
+ // default to private, no-store for now
358
+ // @todo: public caching?
301
359
  'Cache-Control': 'private, no-store',
302
360
  'Content-Type': 'text/html; charset=utf-8',
303
361
  });
362
+ // only a missing prerendered file should fall back to normal request handling
363
+ // any other static-file response should be returned as-is
304
364
  if (res.status !== 404)
305
365
  return res;
306
366
  }
@@ -3,4 +3,4 @@ import { HttpRouter } from './router.js';
3
3
  /**
4
4
  * Create the HTTP router from the generated manifest and import map
5
5
  */
6
- export declare function createHttpRouter(config: Pick<PluginConfig, 'precompress' | 'trailingSlash'>, manifest: Manifest, importMap: ImportMap, rsc: (req: SolasRequest) => Response | Promise<Response>): HttpRouter;
6
+ export declare function createHttpRouter(config: Pick<PluginConfig, 'trailingSlash' | 'trustedOrigins' | 'url'>, manifest: Manifest, importMap: ImportMap, rsc: (req: SolasRequest) => Response | Promise<Response>): HttpRouter;
@@ -49,9 +49,11 @@ function mergeMiddlewares(left, right) {
49
49
  export function createHttpRouter(config, manifest, importMap, rsc) {
50
50
  const router = new HttpRouter({
51
51
  trailingSlash: config.trailingSlash,
52
+ csrf: {
53
+ trustedOrigins: config.trustedOrigins,
54
+ url: config.url,
55
+ },
52
56
  });
53
- // static assets stay outside route middleware conventions and are registered once
54
- router.add('/assets/*', 'GET', HttpRouter.static(config));
55
57
  for (const [, group] of createHandlerGroups(manifest)) {
56
58
  if (!Array.isArray(group)) {
57
59
  if ('paths' in group) {
@@ -1,4 +1,5 @@
1
- import type { HttpMethod, PluginConfig, SolasRequest } from '../../types.js';
1
+ import type { PluginConfig, SolasRequest } from '../../types.js';
2
+ import { CsrfConfig } from '../server/csrf.js';
2
3
  export declare namespace HttpRouter {
3
4
  type Params = Record<string, string | string[]>;
4
5
  type Handler = (req: SolasRequest) => Response | Promise<Response>;
@@ -24,6 +25,7 @@ export declare namespace HttpRouter {
24
25
  };
25
26
  type Options = {
26
27
  trailingSlash?: NonNullable<PluginConfig['trailingSlash']>;
28
+ csrf?: CsrfConfig;
27
29
  };
28
30
  type Registry = {
29
31
  static: Map<string, Route>;
@@ -56,24 +58,12 @@ export declare class HttpRouter {
56
58
  * Register a route handler
57
59
  */
58
60
  add(path: string, method: string, handler: HttpRouter.Handler, params?: string[], middleware?: HttpRouter.Middleware[]): this;
59
- /**
60
- * Match a path and method, returning params and route
61
- */
62
- match(path: string, method: HttpMethod): {
63
- route: HttpRouter.Route;
64
- params: HttpRouter.Params;
65
- } | null;
66
61
  /**
67
62
  * Handle an incoming request
68
63
  */
69
64
  fetch(req: Request): Promise<Response>;
70
- /**
71
- * Serve static assets from the output directory
72
- * @note generated /assets/* handlers bypass +middleware conventions
73
- */
74
- static static(config: PluginConfig): (req: Request) => Promise<Response>;
75
65
  /**
76
66
  * Serve a file with optional compression content negotiation
77
67
  */
78
- static serve(filePath: string, req: Request, precompress?: boolean, headers?: Record<string, string>): Promise<Response>;
68
+ static serveStatic(filePath: string, req: Request, precompress?: boolean, headers?: Record<string, string>): Promise<Response>;
79
69
  }