@jk2908/solas 0.3.7 → 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 (55) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +66 -6
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +75 -6
  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/build.js +14 -14
  13. package/dist/internal/codegen/environments.js +5 -4
  14. package/dist/internal/env/browser.js +11 -9
  15. package/dist/internal/env/rsc.d.ts +2 -2
  16. package/dist/internal/env/rsc.js +170 -86
  17. package/dist/internal/http-router/create-http-router.d.ts +1 -1
  18. package/dist/internal/http-router/create-http-router.js +4 -2
  19. package/dist/internal/http-router/router.d.ts +4 -14
  20. package/dist/internal/http-router/router.js +32 -59
  21. package/dist/internal/navigation/http-exception.d.ts +8 -4
  22. package/dist/internal/navigation/http-exception.js +46 -6
  23. package/dist/internal/postbuild.d.ts +1 -0
  24. package/dist/{cli/build.js → internal/postbuild.js} +13 -48
  25. package/dist/internal/prerender.d.ts +4 -19
  26. package/dist/internal/prerender.js +8 -98
  27. package/dist/internal/public-files.d.ts +18 -0
  28. package/dist/internal/public-files.js +63 -0
  29. package/dist/internal/render/tree.d.ts +0 -3
  30. package/dist/internal/render/tree.js +1 -6
  31. package/dist/internal/resolver.d.ts +31 -23
  32. package/dist/internal/server/actions.d.ts +2 -5
  33. package/dist/internal/server/actions.js +4 -35
  34. package/dist/internal/server/csrf.d.ts +14 -0
  35. package/dist/internal/server/csrf.js +98 -0
  36. package/dist/internal/ui/defaults/error.d.ts +2 -0
  37. package/dist/internal/ui/defaults/error.js +1 -1
  38. package/dist/navigation.d.ts +1 -1
  39. package/dist/router.d.ts +1 -0
  40. package/dist/router.js +1 -0
  41. package/dist/solas.d.ts +12 -1
  42. package/dist/solas.js +116 -1
  43. package/dist/types.d.ts +27 -5
  44. package/dist/utils/base-path.d.ts +14 -0
  45. package/dist/utils/base-path.js +85 -0
  46. package/dist/utils/export-reader.d.ts +6 -1
  47. package/dist/utils/export-reader.js +24 -15
  48. package/package.json +3 -6
  49. package/dist/cli/build.d.ts +0 -7
  50. package/dist/cli/dev.d.ts +0 -4
  51. package/dist/cli/dev.js +0 -13
  52. package/dist/cli/preview.d.ts +0 -1
  53. package/dist/cli/preview.js +0 -47
  54. package/dist/cli.d.ts +0 -2
  55. package/dist/cli.js +0 -28
@@ -17,18 +17,22 @@ export declare namespace Resolver {
17
17
  }> | null;
18
18
  '401s': (View<{
19
19
  children?: React.ReactNode;
20
+ params?: HttpRouter.Params;
20
21
  error?: HttpException;
21
22
  }> | null)[];
22
23
  '403s': (View<{
23
24
  children?: React.ReactNode;
25
+ params?: HttpRouter.Params;
24
26
  error?: HttpException;
25
27
  }> | null)[];
26
28
  '404s': (View<{
27
29
  children?: React.ReactNode;
30
+ params?: HttpRouter.Params;
28
31
  error?: HttpException;
29
32
  }> | null)[];
30
33
  '500s': (View<{
31
34
  children?: React.ReactNode;
35
+ params?: HttpRouter.Params;
32
36
  error?: HttpException;
33
37
  }> | null)[];
34
38
  loaders: (View<{
@@ -64,8 +68,6 @@ export declare class Resolver {
64
68
  * Reconcile a HttpRouter match against a manifest entry
65
69
  */
66
70
  reconcile(path: string, match: HttpRouter.Match | null, error?: Error): {
67
- params: HttpRouter.Params;
68
- error: Error | undefined;
69
71
  __id: string;
70
72
  __path: string;
71
73
  __params: string[];
@@ -85,9 +87,9 @@ export declare class Resolver {
85
87
  prerender: "full" | "ppr" | false;
86
88
  dynamic: boolean;
87
89
  wildcard: boolean;
90
+ params: HttpRouter.Params;
91
+ error: Error | undefined;
88
92
  } | {
89
- params: {};
90
- error: HttpException;
91
93
  __id: string;
92
94
  __path: string;
93
95
  __params: string[];
@@ -107,11 +109,32 @@ export declare class Resolver {
107
109
  prerender: "full" | "ppr" | false;
108
110
  dynamic: boolean;
109
111
  wildcard: boolean;
112
+ params: {};
113
+ error: HttpException;
110
114
  } | null;
111
115
  /**
112
116
  * Enhance a matched route with its associated components
113
117
  */
114
118
  enhance(match: Resolver.ReconciledMatch | null): {
119
+ __id: string;
120
+ __path: string;
121
+ __params: string[];
122
+ __kind: "$P";
123
+ __depth: number;
124
+ method: "get";
125
+ paths: {
126
+ layouts: (string | null)[];
127
+ '401s': (string | null)[];
128
+ '403s': (string | null)[];
129
+ '404s': (string | null)[];
130
+ '500s': (string | null)[];
131
+ loaders: (string | null)[];
132
+ middlewares: (string | null)[];
133
+ page?: string | null | undefined;
134
+ };
135
+ prerender: "full" | "ppr" | false;
136
+ dynamic: boolean;
137
+ wildcard: boolean;
115
138
  ui: {
116
139
  layouts: (View<{
117
140
  children?: import("react").ReactNode;
@@ -123,18 +146,22 @@ export declare class Resolver {
123
146
  }> | null;
124
147
  '401s': (View<{
125
148
  children?: import("react").ReactNode;
149
+ params?: HttpRouter.Params | undefined;
126
150
  error?: HttpException | undefined;
127
151
  }> | null)[];
128
152
  '403s': (View<{
129
153
  children?: import("react").ReactNode;
154
+ params?: HttpRouter.Params | undefined;
130
155
  error?: HttpException | undefined;
131
156
  }> | null)[];
132
157
  '404s': (View<{
133
158
  children?: import("react").ReactNode;
159
+ params?: HttpRouter.Params | undefined;
134
160
  error?: HttpException | undefined;
135
161
  }> | null)[];
136
162
  '500s': (View<{
137
163
  children?: import("react").ReactNode;
164
+ params?: HttpRouter.Params | undefined;
138
165
  error?: HttpException | undefined;
139
166
  }> | null)[];
140
167
  loaders: (View<{
@@ -147,25 +174,6 @@ export declare class Resolver {
147
174
  metadata?: ((input: Metadata.Input<HttpRouter.Params, Error>) => Metadata.Task[]) | undefined;
148
175
  params: HttpRouter.Params | {};
149
176
  error: Error | HttpException | undefined;
150
- __id: string;
151
- __path: string;
152
- __params: string[];
153
- __kind: "$P";
154
- __depth: number;
155
- method: "get";
156
- paths: {
157
- layouts: (string | null)[];
158
- '401s': (string | null)[];
159
- '403s': (string | null)[];
160
- '404s': (string | null)[];
161
- '500s': (string | null)[];
162
- loaders: (string | null)[];
163
- middlewares: (string | null)[];
164
- page?: string | null | undefined;
165
- };
166
- prerender: "full" | "ppr" | false;
167
- dynamic: boolean;
168
- wildcard: boolean;
169
177
  } | null;
170
178
  /**
171
179
  * Find the closest ancestor entry for a given path and property
@@ -1,5 +1,6 @@
1
1
  import { ReactFormState } from 'react-dom/client';
2
2
  import { SolasRequest } from '../../types.js';
3
+ import { CsrfConfig } from './csrf.js';
3
4
  /**
4
5
  * Check if a request is an action request and reuse parsed FormData
5
6
  * when multipart action detection already had to inspect the body
@@ -16,7 +17,7 @@ export declare function maybeAction(req: Request): Promise<{
16
17
  * @returns an object containing either the return value of the action or the form state, depending on the type
17
18
  * of action request
18
19
  */
19
- export declare function processActionRequest(req: SolasRequest): Promise<{
20
+ export declare function processActionRequest(req: SolasRequest, csrf?: CsrfConfig): Promise<{
20
21
  returnValue: {
21
22
  ok: boolean;
22
23
  data: unknown;
@@ -24,7 +25,3 @@ export declare function processActionRequest(req: SolasRequest): Promise<{
24
25
  formState: ReactFormState | undefined;
25
26
  temporaryReferences: unknown;
26
27
  }>;
27
- /**
28
- * Check whether an action request came from the same origin as the target app
29
- */
30
- export declare function isTrustedActionRequest(req: Request): boolean;
@@ -1,6 +1,6 @@
1
1
  import { createTemporaryReferenceSet, decodeAction, decodeFormState, decodeReply, loadServerAction, } from '@vitejs/plugin-rsc/rsc';
2
2
  import { Solas } from '../../solas.js';
3
- import { HttpException } from '../navigation/http-exception.js';
3
+ import { enforce } from './csrf.js';
4
4
  /**
5
5
  * Check if a request is an action request and reuse parsed FormData
6
6
  * when multipart action detection already had to inspect the body
@@ -34,14 +34,12 @@ export async function maybeAction(req) {
34
34
  * @returns an object containing either the return value of the action or the form state, depending on the type
35
35
  * of action request
36
36
  */
37
- export async function processActionRequest(req) {
37
+ export async function processActionRequest(req, csrf = {}) {
38
38
  let returnValue;
39
39
  let formState;
40
40
  let temporaryReferences;
41
- // reject cross-site action posts before any body decoding or action loading
42
- if (!isTrustedActionRequest(req)) {
43
- throw new HttpException(403, 'Cross-site action requests are forbidden');
44
- }
41
+ // enforce CSRF for all action requests
42
+ enforce(req, csrf);
45
43
  const id = req.headers.get('x-rsc-action-id');
46
44
  if (id) {
47
45
  // x-rsc-action-id header exists when action is
@@ -76,32 +74,3 @@ export async function processActionRequest(req) {
76
74
  }
77
75
  return { returnValue, formState, temporaryReferences };
78
76
  }
79
- /**
80
- * Reduce Origin and Referer headers to a comparable origin string
81
- */
82
- function toOrigin(value) {
83
- if (!value)
84
- return null;
85
- try {
86
- return new URL(value).origin;
87
- }
88
- catch {
89
- return null;
90
- }
91
- }
92
- /**
93
- * Check whether an action request came from the same origin as the target app
94
- */
95
- export function isTrustedActionRequest(req) {
96
- const requestOrigin = toOrigin(req.url);
97
- if (!requestOrigin)
98
- return false;
99
- const origin = toOrigin(req.headers.get('origin'));
100
- if (origin)
101
- return origin === requestOrigin;
102
- // some user agents omit Origin on same-origin form posts, so fall back to Referer
103
- const referer = toOrigin(req.headers.get('referer'));
104
- if (referer)
105
- return referer === requestOrigin;
106
- return false;
107
- }
@@ -0,0 +1,14 @@
1
+ import type { PluginConfig } from '../../types.js';
2
+ export type CsrfConfig = Pick<PluginConfig, 'trustedOrigins' | 'url'>;
3
+ /**
4
+ * Enforce the CSRF policy for one request
5
+ */
6
+ export declare function enforce(req: Request, config?: CsrfConfig): void;
7
+ /**
8
+ * Get the first value from a forwarded-style header chain
9
+ */
10
+ export declare function takeFirst(value: string | null | undefined): string | null;
11
+ /**
12
+ * Build an origin from host-style headers when there is no full origin value
13
+ */
14
+ export declare function toHostOrigin(host: string | null | undefined, protocol: string | null | undefined): string | null;
@@ -0,0 +1,98 @@
1
+ import { HttpException } from '../navigation/http-exception.js';
2
+ const UNSAFE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
3
+ const TRUSTED_FETCH_SITES = new Set(['same-origin', 'none']);
4
+ /**
5
+ * Reduce an origin-like value to just its origin for comparison
6
+ */
7
+ function toOrigin(value) {
8
+ if (!value)
9
+ return null;
10
+ try {
11
+ // csrf only cares which origin sent the request, not its path or query
12
+ return new URL(value).origin;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ /**
19
+ * Enforce the CSRF policy for one request
20
+ */
21
+ export function enforce(req, config = {}) {
22
+ // only unsafe methods can mutate state, so safe methods bypass the guard
23
+ if (!UNSAFE_METHODS.has(req.method.toUpperCase()))
24
+ return;
25
+ // first trust the browser's own source headers when they are present
26
+ const sourceOrigin = toOrigin(req.headers.get('origin')) ?? toOrigin(req.headers.get('referer'));
27
+ if (sourceOrigin) {
28
+ const origins = new Set();
29
+ const forwardedProtocol = takeFirst(req.headers.get('x-forwarded-proto'));
30
+ let protocol;
31
+ if (forwardedProtocol === 'http' || forwardedProtocol === 'https') {
32
+ protocol = forwardedProtocol;
33
+ }
34
+ else {
35
+ try {
36
+ // otherwise fall back to the protocol on the request url we received
37
+ protocol = new URL(req.url).protocol.replace(/:$/, '');
38
+ }
39
+ catch {
40
+ protocol = null;
41
+ }
42
+ }
43
+ // allow the current request origin and any configured public origin
44
+ const requestOrigin = toOrigin(req.url);
45
+ if (requestOrigin)
46
+ origins.add(requestOrigin);
47
+ const configuredOrigin = toOrigin(config.url ?? null);
48
+ if (configuredOrigin)
49
+ origins.add(configuredOrigin);
50
+ // also allow host-based origins so proxied deployments still match the public site
51
+ const forwardedHostOrigin = toHostOrigin(takeFirst(req.headers.get('x-forwarded-host')), protocol);
52
+ if (forwardedHostOrigin)
53
+ origins.add(forwardedHostOrigin);
54
+ const hostOrigin = toHostOrigin(takeFirst(req.headers.get('host')), protocol);
55
+ if (hostOrigin)
56
+ origins.add(hostOrigin);
57
+ // add any cross-origin browser sites the config explicitly trusts
58
+ for (const value of config.trustedOrigins ?? []) {
59
+ const origin = toOrigin(value);
60
+ if (origin)
61
+ origins.add(origin);
62
+ }
63
+ if (origins.has(sourceOrigin))
64
+ return;
65
+ }
66
+ // if origin and referer are missing, fall back to fetch metadata
67
+ const fetchSite = req.headers.get('sec-fetch-site')?.toLowerCase();
68
+ if (fetchSite && TRUSTED_FETCH_SITES.has(fetchSite))
69
+ return;
70
+ // if the browser sent no source hints at all, treat it like a non-browser client
71
+ if (!sourceOrigin && !fetchSite)
72
+ return;
73
+ throw new HttpException(403, 'Cross-site unsafe requests are forbidden');
74
+ }
75
+ /**
76
+ * Get the first value from a forwarded-style header chain
77
+ */
78
+ export function takeFirst(value) {
79
+ if (!value)
80
+ return null;
81
+ // use the client-facing value, not a later proxy hop
82
+ const first = value.split(',')[0]?.trim();
83
+ return first || null;
84
+ }
85
+ /**
86
+ * Build an origin from host-style headers when there is no full origin value
87
+ */
88
+ export function toHostOrigin(host, protocol) {
89
+ if (!host || !protocol)
90
+ return null;
91
+ try {
92
+ // this lets host and forwarded-host compare against trusted origins too
93
+ return new URL(`${protocol}://${host}`).origin;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
@@ -1,4 +1,6 @@
1
+ import type { HttpRouter } from '../../http-router/router.js';
1
2
  import type { HttpExceptionLike } from '../../navigation/http-exception.js';
2
3
  export default function Err({ error }: {
3
4
  error: HttpExceptionLike;
5
+ params?: HttpRouter.Params;
4
6
  }): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- export default function Err({ error }) {
2
+ export default function Err({ error, }) {
3
3
  const title = 'status' in error ? `${error.status} - ${error.message}` : error.message;
4
4
  return (_jsxs(_Fragment, { children: [
5
5
  _jsx("meta", { name: "robots", content: "noindex,nofollow" }), _jsx("title", { children: title }), _jsx("h1", { children: title }), _jsx("p", { children: error.message }), process.env.NODE_ENV === 'development' && error?.stack && _jsx("pre", { children: error.stack })] }));
@@ -1,4 +1,4 @@
1
1
  export { HttpExceptionBoundary } from './internal/navigation/http-exception-boundary.js';
2
- export { HttpException, abort, isHttpException, } from './internal/navigation/http-exception.js';
2
+ export { HttpException, HttpExceptionLike, abort, isHttpException, } from './internal/navigation/http-exception.js';
3
3
  export { RedirectBoundary } from './internal/navigation/redirect-boundary.js';
4
4
  export { Redirect, isRedirect, redirect } from './internal/navigation/redirect.js';
package/dist/router.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { Link } from './internal/browser-router/link.js';
2
+ export { withBase } from './internal/browser-router/shared.js';
2
3
  export { useRouter } from './internal/browser-router/use-router.js';
3
4
  export { useSearchParams } from './internal/browser-router/use-search-params.js';
package/dist/router.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { Link } from './internal/browser-router/link.js';
2
+ export { withBase } from './internal/browser-router/shared.js';
2
3
  export { useRouter } from './internal/browser-router/use-router.js';
3
4
  export { useSearchParams } from './internal/browser-router/use-search-params.js';
package/dist/solas.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Prerender } from './internal/prerender.js';
1
2
  import type { PluginConfig } from './types.js';
2
3
  export declare namespace Solas {
3
4
  interface Routes {
@@ -12,12 +13,14 @@ export declare namespace Solas {
12
13
  const ENTRY_RSC = "entry.rsc.tsx";
13
14
  const ENTRY_SSR = "entry.ssr.tsx";
14
15
  const ENTRY_BROWSER = "entry.browser.tsx";
15
- const ASSETS_DIR = "assets";
16
+ const ASSETS_DIR: string;
17
+ const PUBLIC_DIR = "public";
16
18
  const $: unique symbol;
17
19
  const REQUEST_META_KEY: string;
18
20
  const LOG_LEVELS: readonly ["debug", "info", "warn", "error", "fatal"];
19
21
  const PRERENDER_MODES: readonly ["full", "ppr", false];
20
22
  const TRAILING_SLASH_MODES: readonly ["always", "never", "ignore"];
23
+ const RUNTIME_MANIFEST = "runtime-manifest.json";
21
24
  /**
22
25
  * Validate the plugin configuration object, throwing an error if invalid
23
26
  * @param input - the unvalidated configuration object
@@ -26,6 +29,14 @@ export declare namespace Solas {
26
29
  function validate(input: unknown): PluginConfig;
27
30
  }
28
31
  function getVersion(): string;
32
+ namespace Runtime {
33
+ type Manifest = {
34
+ artifacts: Prerender.Artifact.Manifest;
35
+ publicFiles: ReadonlySet<string>;
36
+ };
37
+ function getManifestPath(outDir: string): string;
38
+ function loadManifest(outDir: string): Promise<Manifest | null>;
39
+ }
29
40
  namespace Events {
30
41
  const names: {
31
42
  readonly NAVIGATION: `${string}navigation`;
package/dist/solas.js CHANGED
@@ -12,12 +12,14 @@ var Solas;
12
12
  Config.ENTRY_RSC = 'entry.rsc.tsx';
13
13
  Config.ENTRY_SSR = 'entry.ssr.tsx';
14
14
  Config.ENTRY_BROWSER = 'entry.browser.tsx';
15
- Config.ASSETS_DIR = 'assets';
15
+ Config.ASSETS_DIR = `_${Config.SLUG}`;
16
+ Config.PUBLIC_DIR = 'public';
16
17
  Config.$ = Symbol(Config.SLUG);
17
18
  Config.REQUEST_META_KEY = `__${Config.SLUG.toUpperCase()}__`;
18
19
  Config.LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'];
19
20
  Config.PRERENDER_MODES = ['full', 'ppr', false];
20
21
  Config.TRAILING_SLASH_MODES = ['always', 'never', 'ignore'];
22
+ Config.RUNTIME_MANIFEST = 'runtime-manifest.json';
21
23
  const CONFIG_KEYS = new Set([
22
24
  'port',
23
25
  'logger',
@@ -25,6 +27,7 @@ var Solas;
25
27
  'precompress',
26
28
  'prerender',
27
29
  'sitemap',
30
+ 'trustedOrigins',
28
31
  'trailingSlash',
29
32
  'url',
30
33
  ]);
@@ -67,6 +70,33 @@ var Solas;
67
70
  errors.push('config.precompress must be a boolean');
68
71
  }
69
72
  }
73
+ if ('trustedOrigins' in input && input.trustedOrigins !== undefined) {
74
+ if (!Array.isArray(input.trustedOrigins)) {
75
+ errors.push('config.trustedOrigins must be an array of origins');
76
+ }
77
+ else {
78
+ for (const [index, value] of input.trustedOrigins.entries()) {
79
+ if (typeof value !== 'string') {
80
+ errors.push(`config.trustedOrigins[${index}] must be a string`);
81
+ continue;
82
+ }
83
+ try {
84
+ const url = new URL(value);
85
+ const canonical = value.replace(/\/$/, '');
86
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
87
+ errors.push(`config.trustedOrigins[${index}] must use http:// or https://`);
88
+ continue;
89
+ }
90
+ if (canonical !== url.origin) {
91
+ errors.push(`config.trustedOrigins[${index}] must be an origin without a path, query, or hash`);
92
+ }
93
+ }
94
+ catch {
95
+ errors.push(`config.trustedOrigins[${index}] must be a valid URL origin`);
96
+ }
97
+ }
98
+ }
99
+ }
70
100
  if ('sitemap' in input && input.sitemap !== undefined && input.sitemap !== false) {
71
101
  if (typeof input.sitemap !== 'boolean' && typeof input.sitemap !== 'object') {
72
102
  errors.push('config.sitemap must be a boolean or an object with a routes function');
@@ -129,6 +159,91 @@ var Solas;
129
159
  return value;
130
160
  }
131
161
  Solas.getVersion = getVersion;
162
+ let Runtime;
163
+ (function (Runtime) {
164
+ const manifestCache = new Map();
165
+ function getManifestPath(outDir) {
166
+ return [outDir, Config.GENERATED_DIR, Config.RUNTIME_MANIFEST]
167
+ .map((part, index) => {
168
+ const normalised = part.replace(/\\/g, '/').replace(/\/+/g, '/');
169
+ if (index === 0)
170
+ return normalised.replace(/\/+$/, '');
171
+ return normalised.replace(/^\/+/, '').replace(/\/+$/, '');
172
+ })
173
+ .join('/');
174
+ }
175
+ Runtime.getManifestPath = getManifestPath;
176
+ async function loadManifest(outDir) {
177
+ if (manifestCache.has(outDir)) {
178
+ return manifestCache.get(outDir) ?? null;
179
+ }
180
+ const file = Bun.file(getManifestPath(outDir));
181
+ if (!(await file.exists())) {
182
+ manifestCache.set(outDir, null);
183
+ return null;
184
+ }
185
+ try {
186
+ const value = JSON.parse(await file.text());
187
+ if (!isRecord(value)) {
188
+ manifestCache.set(outDir, null);
189
+ return null;
190
+ }
191
+ const artifacts = value.artifacts ?? value.routes;
192
+ const publicFiles = value.publicFiles;
193
+ if (!isRecord(artifacts)) {
194
+ manifestCache.set(outDir, null);
195
+ return null;
196
+ }
197
+ if (publicFiles !== undefined && !Array.isArray(publicFiles)) {
198
+ manifestCache.set(outDir, null);
199
+ return null;
200
+ }
201
+ for (const entry of Object.values(artifacts)) {
202
+ if (!isRecord(entry)) {
203
+ manifestCache.set(outDir, null);
204
+ return null;
205
+ }
206
+ const { mode, files } = entry;
207
+ if (mode !== 'full' && mode !== 'ppr') {
208
+ manifestCache.set(outDir, null);
209
+ return null;
210
+ }
211
+ if (files !== undefined) {
212
+ if (!Array.isArray(files)) {
213
+ manifestCache.set(outDir, null);
214
+ return null;
215
+ }
216
+ for (const file of files) {
217
+ if (file !== 'html' &&
218
+ file !== 'prelude' &&
219
+ file !== 'postponed' &&
220
+ file !== 'metadata') {
221
+ manifestCache.set(outDir, null);
222
+ return null;
223
+ }
224
+ }
225
+ }
226
+ }
227
+ for (const entry of publicFiles ?? []) {
228
+ if (typeof entry !== 'string' || !entry.startsWith('/')) {
229
+ manifestCache.set(outDir, null);
230
+ return null;
231
+ }
232
+ }
233
+ const manifest = {
234
+ artifacts: artifacts,
235
+ publicFiles: new Set(publicFiles ?? []),
236
+ };
237
+ manifestCache.set(outDir, manifest);
238
+ return manifest;
239
+ }
240
+ catch {
241
+ manifestCache.set(outDir, null);
242
+ return null;
243
+ }
244
+ }
245
+ Runtime.loadManifest = loadManifest;
246
+ })(Runtime = Solas.Runtime || (Solas.Runtime = {}));
132
247
  let Events;
133
248
  (function (Events) {
134
249
  Events.names = {
package/dist/types.d.ts CHANGED
@@ -2,40 +2,44 @@ type BunRequest = Request & {
2
2
  params?: Record<string, string | string[]>;
3
3
  };
4
4
  import { ExportReader } from './utils/export-reader.js';
5
+ import type { BrowserRouter } from './internal/browser-router/shared.js';
5
6
  import type { Build } from './internal/build.js';
6
7
  import type { HttpRouter } from './internal/http-router/router.js';
7
8
  import type { Metadata } from './internal/metadata.js';
8
9
  import type { HttpException } from './internal/navigation/http-exception.js';
9
- import { BrowserRouter } from './internal/browser-router/router.js';
10
10
  import { Solas } from './solas.js';
11
11
  export type LogLevel = (typeof Solas.Config.LOG_LEVELS)[number];
12
+ type Origin = `http://${string}` | `https://${string}`;
12
13
  type PluginConfigBase = {
13
14
  port?: number;
14
15
  precompress?: boolean;
15
16
  prerender?: Route.Prerender;
16
17
  metadata?: Metadata.Item;
17
18
  trailingSlash?: (typeof Solas.Config.TRAILING_SLASH_MODES)[number];
19
+ trustedOrigins?: readonly Origin[];
18
20
  readonly logger?: {
19
21
  level?: LogLevel;
20
22
  };
21
23
  };
22
24
  export type PluginConfig = PluginConfigBase & ({
23
- url: `http://${string}` | `https://${string}`;
25
+ url: Origin;
24
26
  sitemap: true | {
25
27
  routes: (existing: string[]) => string[] | Promise<string[]>;
26
28
  };
27
29
  } | {
28
- url?: `http://${string}` | `https://${string}`;
30
+ url?: Origin;
29
31
  sitemap?: false;
30
32
  });
31
33
  export type RuntimeConfig = PluginConfig & {
32
34
  precompress: NonNullable<PluginConfig['precompress']>;
33
35
  trailingSlash: NonNullable<PluginConfig['trailingSlash']>;
36
+ trustedOrigins: NonNullable<PluginConfig['trustedOrigins']>;
34
37
  };
35
38
  export type BuildContext = {
36
39
  prerenderRoutes: Set<string>;
37
40
  knownRoutes: Set<string>;
38
41
  exportReader: ExportReader;
42
+ command?: 'build' | 'serve';
39
43
  };
40
44
  export type RequestMeta = {
41
45
  error?: HttpException | Error;
@@ -98,6 +102,8 @@ export type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' |
98
102
  export type Primitive = string | number | boolean | bigint | symbol | null | undefined;
99
103
  export type LooseNumber<T extends number> = T | (number & {});
100
104
  export type BuildManifest = {
105
+ base: string;
106
+ publicFiles: string[];
101
107
  prerenderRoutes: string[];
102
108
  sitemapRoutes: string[];
103
109
  precompress: boolean;
@@ -105,8 +111,24 @@ export type BuildManifest = {
105
111
  url?: PluginConfig['url'];
106
112
  };
107
113
  export declare namespace Route {
108
- type Metadata = Metadata.Item | ((input: Metadata.Input<BrowserRouter.Params>) => Promise<Metadata.Item> | Metadata.Item);
109
- type Prerender = (typeof Solas.Config.PRERENDER_MODES)[number];
114
+ type ParamsOf<TRoute> = TRoute extends {
115
+ params?: infer TParams extends BrowserRouter.Params;
116
+ } ? TParams : never;
117
+ type ErrorPropsOf<TError> = [TError] extends [never] ? {} : {
118
+ error?: TError;
119
+ };
120
+ export type Params<TRoute> = ParamsOf<TRoute>;
121
+ export type Metadata<TRoute = {
122
+ params?: BrowserRouter.Params;
123
+ }, TError = never> = Metadata.Item | ((input: Metadata.Input<ParamsOf<TRoute>, TError>) => Promise<Metadata.Item> | Metadata.Item);
124
+ export type StaticParams<TRoute> = [ParamsOf<TRoute>] extends [never] ? never : () => readonly ParamsOf<TRoute>[] | Promise<readonly ParamsOf<TRoute>[]>;
125
+ export type Props<TRoute, TError = never> = ([ParamsOf<TRoute>] extends [never] ? {
126
+ params?: never;
127
+ } : {
128
+ params: ParamsOf<TRoute>;
129
+ }) & ErrorPropsOf<TError>;
130
+ export type Prerender = (typeof Solas.Config.PRERENDER_MODES)[number];
131
+ export {};
110
132
  }
111
133
  export type BoundaryError = Error & {
112
134
  digest?: string;
@@ -0,0 +1,14 @@
1
+ export declare namespace BasePath {
2
+ /**
3
+ * Normalise a base path so every check uses the same shape
4
+ */
5
+ function normalise(value: string | null | undefined): string;
6
+ /**
7
+ * Strip the base path from a request path
8
+ */
9
+ function strip(pathname: string, base: string | null | undefined): string | null;
10
+ /**
11
+ * Add the base path to a path when needed
12
+ */
13
+ function apply(pathname: string, base: string | null | undefined): string;
14
+ }