@jk2908/solas 0.2.2 → 0.3.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 (82) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +5 -3
  3. package/dist/cli.js +77 -83
  4. package/dist/error-boundary.d.ts +1 -1
  5. package/dist/error-boundary.js +1 -1
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +10 -14
  8. package/dist/internal/build.d.ts +1 -1
  9. package/dist/internal/build.js +4 -4
  10. package/dist/internal/codegen/config.d.ts +1 -1
  11. package/dist/internal/codegen/config.js +10 -10
  12. package/dist/internal/codegen/environments.js +22 -18
  13. package/dist/internal/codegen/manifest.d.ts +1 -1
  14. package/dist/internal/codegen/manifest.js +6 -7
  15. package/dist/internal/codegen/maps.d.ts +1 -1
  16. package/dist/internal/codegen/maps.js +38 -27
  17. package/dist/internal/codegen/utils.d.ts +20 -0
  18. package/dist/internal/codegen/utils.js +140 -1
  19. package/dist/internal/env/browser.js +20 -16
  20. package/dist/internal/env/request-context.d.ts +2 -2
  21. package/dist/internal/env/request-context.js +1 -1
  22. package/dist/internal/env/rsc.d.ts +8 -22
  23. package/dist/internal/env/rsc.js +38 -117
  24. package/dist/internal/env/ssr.js +9 -9
  25. package/dist/internal/env/utils.js +2 -2
  26. package/dist/internal/metadata.d.ts +2 -2
  27. package/dist/internal/metadata.js +18 -6
  28. package/dist/internal/navigation/http-exception-boundary.d.ts +2 -2
  29. package/dist/internal/navigation/http-exception-boundary.js +1 -1
  30. package/dist/internal/navigation/link.js +1 -1
  31. package/dist/internal/navigation/redirect-boundary.d.ts +1 -1
  32. package/dist/internal/navigation/redirect-boundary.js +1 -1
  33. package/dist/internal/navigation/redirect.js +1 -1
  34. package/dist/internal/navigation/use-search-params.js +4 -2
  35. package/dist/internal/prerender.d.ts +10 -1
  36. package/dist/internal/prerender.js +55 -5
  37. package/dist/internal/render/head.d.ts +4 -1
  38. package/dist/internal/render/head.js +37 -18
  39. package/dist/internal/render/tree.d.ts +1 -1
  40. package/dist/internal/render/tree.js +3 -3
  41. package/dist/internal/router/create-router.d.ts +2 -2
  42. package/dist/internal/router/create-router.js +1 -1
  43. package/dist/internal/router/prefetcher.d.ts +1 -1
  44. package/dist/internal/router/prefetcher.js +8 -3
  45. package/dist/internal/router/resolver.d.ts +29 -29
  46. package/dist/internal/router/resolver.js +4 -4
  47. package/dist/internal/router/router-context.d.ts +4 -0
  48. package/dist/internal/router/router-context.js +1 -0
  49. package/dist/internal/router/router-provider.d.ts +6 -2
  50. package/dist/internal/router/router-provider.js +38 -22
  51. package/dist/internal/router/router.d.ts +1 -1
  52. package/dist/internal/router/router.js +4 -4
  53. package/dist/internal/router/use-router.d.ts +5 -1
  54. package/dist/internal/router/use-router.js +1 -1
  55. package/dist/internal/router/utils.d.ts +1 -1
  56. package/dist/internal/server/actions.d.ts +30 -0
  57. package/dist/internal/server/actions.js +107 -0
  58. package/dist/internal/server/cookies.d.ts +1 -1
  59. package/dist/internal/server/cookies.js +3 -3
  60. package/dist/internal/server/dynamic.js +2 -2
  61. package/dist/internal/server/headers.js +2 -2
  62. package/dist/internal/server/url.js +14 -3
  63. package/dist/internal/ui/defaults/error.d.ts +1 -1
  64. package/dist/internal/ui/error-boundary.d.ts +1 -1
  65. package/dist/internal/ui/error-boundary.js +1 -1
  66. package/dist/navigation.d.ts +6 -6
  67. package/dist/navigation.js +6 -6
  68. package/dist/prerender.d.ts +1 -1
  69. package/dist/prerender.js +1 -1
  70. package/dist/router.d.ts +4 -4
  71. package/dist/router.js +4 -4
  72. package/dist/server.d.ts +4 -4
  73. package/dist/server.js +4 -4
  74. package/dist/solas.d.ts +1 -1
  75. package/dist/solas.js +1 -0
  76. package/dist/types.d.ts +6 -6
  77. package/dist/types.js +1 -1
  78. package/dist/utils/context.js +1 -1
  79. package/dist/utils/logger.js +2 -2
  80. package/package.json +3 -1
  81. package/dist/utils/format.d.ts +0 -6
  82. package/dist/utils/format.js +0 -72
@@ -1,2 +1,141 @@
1
- import { Solas } from '../../solas';
1
+ import { Solas } from '../../solas.js';
2
2
  export const AUTOGEN_MSG = `// auto-generated by ${Solas.Config.NAME}`;
3
+ const INDENT = '\t';
4
+ const IDENTIFIER = /^[$A-Z_a-z][$\w]*$/;
5
+ /**
6
+ * Check whether a string contains control characters that should never appear in source
7
+ */
8
+ function hasControlChar(value) {
9
+ for (let index = 0; index < value.length; index += 1) {
10
+ const code = value.charCodeAt(index);
11
+ if ((code >= 0 && code <= 31) || code === 127) {
12
+ return true;
13
+ }
14
+ }
15
+ return false;
16
+ }
17
+ /**
18
+ * Validate an identifier before writing it into generated source unquoted
19
+ */
20
+ export function toIdentifier(value, label) {
21
+ if (!IDENTIFIER.test(value)) {
22
+ throw new Error(`Invalid ${label}: ${value}`);
23
+ }
24
+ return value;
25
+ }
26
+ /**
27
+ * Validate and quote a relative specifier before embedding it in generated imports
28
+ */
29
+ export function toRelativeModuleSpecifier(value, label) {
30
+ if (value.length === 0) {
31
+ throw new Error(`Invalid ${label}: module specifier cannot be empty`);
32
+ }
33
+ if (value.includes('\\')) {
34
+ throw new Error(`Invalid ${label}: module specifier must use forward slashes`);
35
+ }
36
+ if (hasControlChar(value)) {
37
+ throw new Error(`Invalid ${label}: module specifier contains control characters`);
38
+ }
39
+ if (!value.startsWith('./') && !value.startsWith('../')) {
40
+ throw new Error(`Invalid ${label}: module specifier must be relative`);
41
+ }
42
+ return toStringLiteral(value);
43
+ }
44
+ /**
45
+ * Validate a nullable identifier list whilst preserving explicit null holes
46
+ */
47
+ export function toIdentifierList(values, label) {
48
+ return values
49
+ .map((value, index) => value === null ? 'null' : toIdentifier(value, `${label}[${index}] identifier`))
50
+ .join(', ');
51
+ }
52
+ /**
53
+ * Escape text into a safe string literal for generated source
54
+ */
55
+ export function toStringLiteral(value, quoteStyle = "'") {
56
+ return `${quoteStyle}${value
57
+ .replace(/\\/g, '\\\\')
58
+ .replace(new RegExp(quoteStyle, 'g'), `\\${quoteStyle}`)
59
+ .replace(/\n/g, '\\n')
60
+ .replace(/\r/g, '\\r')
61
+ .replace(/\t/g, '\\t')}${quoteStyle}`;
62
+ }
63
+ /**
64
+ * Convert a string into a valid unquoted property key if possible, otherwise quote it
65
+ * as a string literal for generated source
66
+ */
67
+ function toPropertyKey(value) {
68
+ return IDENTIFIER.test(value) ? value : toStringLiteral(value);
69
+ }
70
+ /**
71
+ * Check whether a value is a simple literal that can be safely inlined in generated
72
+ * source without risking syntax errors or readability issues, or whether it should
73
+ * be printed on multiple lines for clarity
74
+ */
75
+ function isInlineValue(value) {
76
+ return (value === null ||
77
+ typeof value === 'boolean' ||
78
+ typeof value === 'number' ||
79
+ typeof value === 'string');
80
+ }
81
+ /**
82
+ * Emit readable ts source for generated config and manifest data
83
+ */
84
+ export function toSourceLiteral(value, level = 0) {
85
+ if (value === null)
86
+ return 'null';
87
+ if (typeof value === 'string')
88
+ return toStringLiteral(value);
89
+ if (typeof value === 'boolean' || typeof value === 'number') {
90
+ return String(value);
91
+ }
92
+ if (typeof value === 'function')
93
+ return value.toString();
94
+ if (Array.isArray(value)) {
95
+ if (value.length === 0)
96
+ return '[]';
97
+ const items = value.map(item => toSourceLiteral(item, level + 1));
98
+ const inline = value.every(isInlineValue) && items.join(', ').length <= 60;
99
+ if (inline)
100
+ return `[${items.join(', ')}]`;
101
+ return [
102
+ '[',
103
+ items.map(item => indent(item, level + 1)).join(',\n'),
104
+ `${INDENT.repeat(level)}]`,
105
+ ].join('\n');
106
+ }
107
+ if (typeof value === 'object') {
108
+ const entries = Object.entries(value);
109
+ if (entries.length === 0)
110
+ return '{}';
111
+ return [
112
+ '{',
113
+ entries
114
+ .map(([key, entryValue]) => {
115
+ const prefix = `${INDENT.repeat(level + 1)}${toPropertyKey(key)}: `;
116
+ if (typeof entryValue === 'function') {
117
+ const source = entryValue.toString();
118
+ if (source.startsWith(`${key}(`) || source.startsWith(`async ${key}(`)) {
119
+ return indent(source, level + 1);
120
+ }
121
+ return `${prefix}${source.replace(/\n/g, `\n${INDENT.repeat(level + 1)}`)}`;
122
+ }
123
+ return `${prefix}${toSourceLiteral(entryValue, level + 1).replace(/\n/g, `\n${INDENT.repeat(level + 1)}`)}`;
124
+ })
125
+ .join(',\n'),
126
+ `${INDENT.repeat(level)}}`,
127
+ ].join('\n');
128
+ }
129
+ throw new Error(`Unsupported generated value type: ${typeof value}`);
130
+ }
131
+ /**
132
+ * Indent each line of a block of source code by the specified level for embedding in
133
+ * generated output
134
+ */
135
+ function indent(value, level = 1) {
136
+ const prefix = INDENT.repeat(level);
137
+ return value
138
+ .split('\n')
139
+ .map(line => (line.length > 0 ? `${prefix}${line}` : line))
140
+ .join('\n');
141
+ }
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { StrictMode, Suspense, useCallback, useEffect, useState, useTransition, } from 'react';
2
+ import { StrictMode, Suspense, useCallback, useState, useTransition } from 'react';
3
3
  import { hydrateRoot } from 'react-dom/client';
4
4
  import { createFromFetch, createFromReadableStream, createTemporaryReferenceSet, encodeReply, setServerCallback, } from '@vitejs/plugin-rsc/browser';
5
5
  import { rscStream } from 'rsc-html-stream/client';
6
- import { RedirectBoundary } from '../navigation/redirect-boundary';
7
- import { Head } from '../render/head';
8
- import { RouterProvider } from '../router/router-provider';
9
- import { ErrorBoundary } from '../ui/error-boundary';
6
+ import { RedirectBoundary } from '../navigation/redirect-boundary.js';
7
+ import { Head } from '../render/head.js';
8
+ import { RouterProvider } from '../router/router-provider.js';
9
+ import { ErrorBoundary } from '../ui/error-boundary.js';
10
10
  /**
11
11
  * Browser RSC hydration entry point
12
12
  */
@@ -14,7 +14,9 @@ export async function browser() {
14
14
  const payload = await createFromReadableStream(rscStream, {
15
15
  unstable_allowPartialStream: true,
16
16
  });
17
- let setPayload = () => { };
17
+ const payloadSetter = {
18
+ current: () => { },
19
+ };
18
20
  function A() {
19
21
  const [p, setP] = useState(payload);
20
22
  const [isPending, startTransition] = useTransition();
@@ -23,13 +25,10 @@ export async function browser() {
23
25
  setP(payload);
24
26
  });
25
27
  }, []);
26
- useEffect(() => {
27
- // expose external setPayload - used inside
28
- // server callback to update payload after
29
- // action execution
30
- setPayload = setPayloadInTransition;
31
- }, [setPayloadInTransition]);
32
- return (_jsx(RedirectBoundary, { children: _jsxs(RouterProvider, { setPayload: setPayloadInTransition, isNavigating: isPending, children: [
28
+ // make the latest payload updater available to action/hmr callbacks
29
+ // immediately during render, without waiting for an effect to run
30
+ payloadSetter.current = setPayloadInTransition;
31
+ return (_jsx(RedirectBoundary, { children: _jsxs(RouterProvider, { setPayload: setPayloadInTransition, isNavigating: isPending, url: p.url, children: [
33
32
  _jsx(ErrorBoundary, { fallback: null, children: _jsx(Suspense, { fallback: null, children: _jsx(Head, { metadata: p.metadata }) }) }), p.root] }) }));
34
33
  }
35
34
  setServerCallback(async (id, args) => {
@@ -42,7 +41,7 @@ export async function browser() {
42
41
  'x-rsc-action-id': id,
43
42
  },
44
43
  }), { temporaryReferences });
45
- setPayload(payload);
44
+ payloadSetter.current(payload);
46
45
  const { ok, data } = payload.returnValue ?? {};
47
46
  if (!ok)
48
47
  throw data;
@@ -52,7 +51,12 @@ export async function browser() {
52
51
  formState: payload.formState,
53
52
  });
54
53
  import.meta.hot?.on?.('rsc:update', async () => {
55
- const p = await createFromFetch(fetch(window.location.href, { headers: { Accept: 'text/x-component' } }));
56
- setPayload(p);
54
+ try {
55
+ const p = await createFromFetch(fetch(window.location.href, { headers: { Accept: 'text/x-component' } }));
56
+ payloadSetter.current(p);
57
+ }
58
+ catch (err) {
59
+ console.error('[hmr] failed to refresh rsc payload', err);
60
+ }
57
61
  });
58
62
  }
@@ -1,5 +1,5 @@
1
- import type { SolasRequest } from '../../types';
2
- import type { Cookies } from '../../utils/cookies';
1
+ import type { SolasRequest } from '../../types.js';
2
+ import type { Cookies } from '../../utils/cookies.js';
3
3
  export type RequestCache = {
4
4
  cookies?: Readonly<ReturnType<typeof Cookies.parse>>;
5
5
  headers?: ReadonlyMap<string, string>;
@@ -1,2 +1,2 @@
1
- import { Context } from '../../utils/context';
1
+ import { Context } from '../../utils/context.js';
2
2
  export const RequestContext = Context.create('request');
@@ -1,6 +1,7 @@
1
1
  import type { ReactFormState } from 'react-dom/client';
2
- import type { ImportMap, Manifest, RuntimeConfig, SolasRequest } from '../../types';
3
- import { Metadata } from '../metadata';
2
+ import type { ImportMap, Manifest, RuntimeConfig } from '../../types.js';
3
+ import { Metadata } from '../metadata.js';
4
+ import { Prerender } from '../prerender.js';
4
5
  export type RSCPayload = {
5
6
  returnValue?: {
6
7
  ok: boolean;
@@ -9,31 +10,16 @@ export type RSCPayload = {
9
10
  formState?: ReactFormState;
10
11
  root: React.ReactNode;
11
12
  metadata?: Promise<Metadata.Item>;
13
+ url?: {
14
+ pathname?: string;
15
+ search?: string;
16
+ };
12
17
  };
13
- export declare function action(req: SolasRequest): Promise<{
14
- returnValue: {
15
- ok: boolean;
16
- data: unknown;
17
- } | undefined;
18
- formState: ReactFormState | undefined;
19
- temporaryReferences: unknown;
20
- }>;
21
- /**
22
- * Check if a request is an action request and reuse parsed FormData
23
- * when multipart action detection already had to inspect the body
24
- */
25
- export declare function maybeAction(req: Request): Promise<{
26
- action: boolean;
27
- formData: null;
28
- } | {
29
- action: boolean;
30
- formData: FormData;
31
- }>;
32
18
  /**
33
19
  * Create the object exported by the generated RSC entry. Uses the generated config,
34
20
  * route manifest, and import map to build the router once, then returns an object
35
21
  * with a fetch method that handles requests
36
22
  */
37
- export declare function createHandler(config: RuntimeConfig, manifest: Manifest, importMap: ImportMap): {
23
+ export declare function createHandler(config: RuntimeConfig, manifest: Manifest, importMap: ImportMap, artifactManifest?: Prerender.Artifact.Manifest | null): {
38
24
  fetch(req: Request): Promise<Response>;
39
25
  };
@@ -1,19 +1,19 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import path from 'node:path';
3
- import { createTemporaryReferenceSet, decodeAction, decodeFormState, decodeReply, loadServerAction, renderToReadableStream, } from '@vitejs/plugin-rsc/rsc';
4
- import { Solas } from '../../solas';
5
- import { Logger } from '../../utils/logger';
6
- import { normalisePathname } from '../router/utils';
7
- import { getKnownDigest, isKnownError } from './utils';
8
- import { Metadata } from '../metadata';
9
- import { HttpException, isHttpException } from '../navigation/http-exception';
10
- import { Prerender } from '../prerender';
11
- import { Tree } from '../render/tree';
12
- import { createRouter } from '../router/create-router';
13
- import { Resolver } from '../router/resolver';
14
- import { Router } from '../router/router';
15
- import DefaultErr from '../ui/defaults/error';
16
- import { RequestContext } from './request-context';
2
+ import { renderToReadableStream } from '@vitejs/plugin-rsc/rsc';
3
+ import { Logger } from '../../utils/logger.js';
4
+ import { Solas } from '../../solas.js';
5
+ import { Metadata } from '../metadata.js';
6
+ import { HttpException, isHttpException } from '../navigation/http-exception.js';
7
+ import { Prerender } from '../prerender.js';
8
+ 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';
13
+ import { processActionRequest } from '../server/actions.js';
14
+ import DefaultErr from '../ui/defaults/error.js';
15
+ import { RequestContext } from './request-context.js';
16
+ import { getKnownDigest, isKnownError } from './utils.js';
17
17
  /**
18
18
  * Get the streamed RSC payload and response metadata for a single request.
19
19
  * Resolves the route match, collects metadata, and returns the stream,
@@ -42,6 +42,10 @@ async function getPayload(req, manifest, importMap, baseMetadata, returnValue, f
42
42
  ] })),
43
43
  returnValue,
44
44
  formState,
45
+ url: {
46
+ pathname: url.pathname,
47
+ search: url.search,
48
+ },
45
49
  };
46
50
  return {
47
51
  // this path is a safety fallback when a prerender request
@@ -81,6 +85,10 @@ async function getPayload(req, manifest, importMap, baseMetadata, returnValue, f
81
85
  returnValue,
82
86
  formState,
83
87
  metadata,
88
+ url: {
89
+ pathname: url.pathname,
90
+ search: url.search,
91
+ },
84
92
  };
85
93
  // status code comes from route match error if any
86
94
  const status = isHttpException(match.error) ? match.error.status : 200;
@@ -136,6 +144,10 @@ async function getPayload(req, manifest, importMap, baseMetadata, returnValue, f
136
144
  ] })),
137
145
  returnValue,
138
146
  formState,
147
+ url: {
148
+ pathname: url.pathname,
149
+ search: url.search,
150
+ },
139
151
  }, {
140
152
  temporaryReferences,
141
153
  })),
@@ -144,83 +156,13 @@ async function getPayload(req, manifest, importMap, baseMetadata, returnValue, f
144
156
  };
145
157
  }
146
158
  }
147
- export async function action(req) {
148
- let returnValue;
149
- let formState;
150
- let temporaryReferences;
151
- const id = req.headers.get('x-rsc-action-id');
152
- if (id) {
153
- // x-rsc-action-id header exists when action is
154
- // called via ReactClient.setServerCallback
155
- const body = req.headers.get('content-type')?.startsWith('multipart/form-data')
156
- ? await req.formData()
157
- : await req.text();
158
- temporaryReferences = createTemporaryReferenceSet();
159
- const args = await decodeReply(body, {
160
- temporaryReferences,
161
- });
162
- const action = await loadServerAction(id);
163
- try {
164
- const data = await action.apply(null, args);
165
- returnValue = { ok: true, data };
166
- }
167
- catch (err) {
168
- returnValue = { ok: false, data: err };
169
- }
170
- }
171
- else {
172
- // otherwise server function is called via
173
- // <form action={...}>
174
- // we might have already parsed FormData in the router for multipart action
175
- // detection should be attached to the SolasRequest, so we can reuse that
176
- // to avoid parsing twice
177
- const parsedFormData = req[Solas.Config.REQUEST_META]?.parsedFormData;
178
- const formData = parsedFormData ?? (await req.formData());
179
- const decodedAction = await decodeAction(formData);
180
- const result = await decodedAction();
181
- formState = await decodeFormState(result, formData);
182
- }
183
- return { returnValue, formState, temporaryReferences };
184
- }
185
- /**
186
- * Check if a request is an action request and reuse parsed FormData
187
- * when multipart action detection already had to inspect the body
188
- */
189
- export async function maybeAction(req) {
190
- if (req.method !== 'POST')
191
- return { action: false, formData: null };
192
- if (req.headers.has('x-rsc-action-id'))
193
- return { action: true, formData: null };
194
- const contentType = req.headers.get('content-type') ?? '';
195
- if (!contentType.startsWith('multipart/form-data')) {
196
- return { action: false, formData: null };
197
- }
198
- try {
199
- const formData = await req.clone().formData();
200
- for (const key of formData.keys()) {
201
- if (key === '$ACTION_KEY' ||
202
- key.startsWith('$ACTION_') ||
203
- key.startsWith('$ACTION_REF_')) {
204
- return { action: true, formData };
205
- }
206
- }
207
- }
208
- catch {
209
- return { action: false, formData: null };
210
- }
211
- return { action: false, formData: null };
212
- }
213
159
  /**
214
160
  * Create the object exported by the generated RSC entry. Uses the generated config,
215
161
  * route manifest, and import map to build the router once, then returns an object
216
162
  * with a fetch method that handles requests
217
163
  */
218
- export function createHandler(config, manifest, importMap) {
164
+ export function createHandler(config, manifest, importMap, artifactManifest = null) {
219
165
  const prerenderPathMode = config.trailingSlash === 'always' ? 'always' : 'never';
220
- const fullyPrerenderedRoutes = new Set(Object.values(manifest)
221
- .flat()
222
- .filter(entry => 'prerender' in entry && String(entry.prerender) === 'full')
223
- .map(entry => normalisePathname(entry.__path, prerenderPathMode)));
224
166
  /**
225
167
  * Create the HTTP response for a single incoming request. Runs actions when needed,
226
168
  * converts the payload into component, HTML, or prerender artifact responses, and
@@ -233,7 +175,7 @@ export function createHandler(config, manifest, importMap) {
233
175
  returnValue: undefined,
234
176
  };
235
177
  if (req[Solas.Config.REQUEST_META].action)
236
- opts = await action(req);
178
+ opts = await processActionRequest(req);
237
179
  const { stream: rscStream, status, ppr, } = await getPayload(req, manifest, importMap, config.metadata, opts.returnValue, opts.formState, opts.temporaryReferences);
238
180
  const stream = await rscStream;
239
181
  if (!req.headers.get('accept')?.includes('text/html')) {
@@ -268,10 +210,9 @@ export function createHandler(config, manifest, importMap) {
268
210
  status,
269
211
  });
270
212
  }
271
- const artifactManifest = runtimePpr
272
- ? await Prerender.Artifact.loadManifest(Solas.Config.OUT_DIR)
213
+ const artifactManifestEntry = runtimePpr
214
+ ? (artifactManifest?.routes[lookupPath] ?? null)
273
215
  : null;
274
- const artifactManifestEntry = artifactManifest?.routes[lookupPath] ?? null;
275
216
  let tryPrelude = false;
276
217
  if (artifactManifestEntry) {
277
218
  tryPrelude = artifactManifestEntry.mode === 'ppr';
@@ -334,36 +275,16 @@ export function createHandler(config, manifest, importMap) {
334
275
  return Response.redirect(url.toString(), 308);
335
276
  }
336
277
  // fully prerendered html can be served straight from disk for normal
337
- // document requests, but artifact generation must still hit the runtime path
278
+ // document requests, but build-time artifact requests must bypass
279
+ // this shortcut so they still render fresh output
338
280
  if (!import.meta.env.DEV &&
339
281
  accept.includes('text/html') &&
340
282
  req.headers.get(`x-${Solas.Config.SLUG}-prerender-artifact`) !== '1') {
341
- const pathname = canonicalPath;
342
- const lookupPath = normalisePathname(pathname, prerenderPathMode);
343
- const routePath = lookupPath.replace(/^\//, '').replace(/\/$/, '');
344
- let prerenderPath = null;
345
- const artifactManifest = await Prerender.Artifact.loadManifest(Solas.Config.OUT_DIR);
346
- const artifactManifestEntry = artifactManifest?.routes[lookupPath] ?? null;
347
- const fullHtmlPath = lookupPath === '/'
348
- ? path.join(Solas.Config.OUT_DIR, 'index.html')
349
- : config.trailingSlash === 'always'
350
- ? path.join(Solas.Config.OUT_DIR, routePath, 'index.html')
351
- : path.join(Solas.Config.OUT_DIR, `${routePath}.html`);
352
- if (fullyPrerenderedRoutes.has(lookupPath)) {
353
- prerenderPath = fullHtmlPath;
354
- }
355
- else if (artifactManifestEntry) {
356
- if (artifactManifestEntry.mode === 'full') {
357
- prerenderPath = fullHtmlPath;
358
- }
359
- }
360
- else {
361
- const artifactMetadata = await Prerender.Artifact.loadMetadata(Solas.Config.OUT_DIR, lookupPath);
362
- if (artifactMetadata &&
363
- Prerender.Artifact.isCompatible(artifactMetadata, lookupPath, 'full')) {
364
- prerenderPath = fullHtmlPath;
365
- }
366
- }
283
+ 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)
287
+ : null;
367
288
  if (prerenderPath) {
368
289
  const res = await Router.serve(prerenderPath, req, config.precompress, {
369
290
  // avoid shared or proxy caching unless users opt into public caching later
@@ -4,18 +4,18 @@ import { resume as reactResume, renderToReadableStream } from 'react-dom/server.
4
4
  import { prerender as reactPrerender } from 'react-dom/static.edge';
5
5
  import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr';
6
6
  import { injectRSCPayload } from 'rsc-html-stream/server';
7
- import { Solas } from '../../solas';
8
- import { Logger } from '../../utils/logger';
9
- import { getKnownDigest } from './utils';
10
- import { RedirectBoundary } from '../navigation/redirect-boundary';
11
- import { Prerender } from '../prerender';
12
- import { Head } from '../render/head';
13
- import { RouterProvider } from '../router/router-provider';
14
- import { ErrorBoundary } from '../ui/error-boundary';
7
+ import { Logger } from '../../utils/logger.js';
8
+ import { Solas } from '../../solas.js';
9
+ import { RedirectBoundary } from '../navigation/redirect-boundary.js';
10
+ import { Prerender } from '../prerender.js';
11
+ import { Head } from '../render/head.js';
12
+ import { RouterProvider } from '../router/router-provider.js';
13
+ import { ErrorBoundary } from '../ui/error-boundary.js';
14
+ import { getKnownDigest } 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, { children: [
18
+ return (_jsx(RedirectBoundary, { children: _jsxs(RouterProvider, { 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
  /**
@@ -1,5 +1,5 @@
1
- import { HTTP_EXCEPTION_DIGEST_PREFIX } from '../navigation/http-exception';
2
- import { REDIRECT_DIGEST_PREFIX } from '../navigation/redirect';
1
+ import { HTTP_EXCEPTION_DIGEST_PREFIX } from '../navigation/http-exception.js';
2
+ import { REDIRECT_DIGEST_PREFIX } from '../navigation/redirect.js';
3
3
  const possibilities = [HTTP_EXCEPTION_DIGEST_PREFIX, REDIRECT_DIGEST_PREFIX];
4
4
  export function getKnownDigest(err) {
5
5
  if (typeof err === 'object' &&
@@ -1,5 +1,5 @@
1
- import type { HttpException } from './navigation/http-exception';
2
- import { Build } from './build';
1
+ import type { HttpException } from './navigation/http-exception.js';
2
+ import { Build } from './build.js';
3
3
  type EntryKind = typeof Build.EntryKind;
4
4
  export declare namespace Metadata {
5
5
  type EntrySource = Exclude<EntryKind[keyof EntryKind], typeof Build.EntryKind.ENDPOINT | typeof Build.EntryKind.MIDDLEWARE>;
@@ -1,5 +1,5 @@
1
- import { Build } from './build';
2
- import { isHttpException } from './navigation/http-exception';
1
+ import { Build } from './build.js';
2
+ import { isHttpException } from './navigation/http-exception.js';
3
3
  const TITLE_TEMPLATE_STR = '%s';
4
4
  export { Metadata };
5
5
  var Metadata;
@@ -14,6 +14,18 @@ var Metadata;
14
14
  [Build.EntryKind['500']]: 40,
15
15
  [Build.EntryKind.LOADING]: 50,
16
16
  };
17
+ /**
18
+ * Check whether a value is a supported metadata primitive
19
+ */
20
+ function isTagValue(value) {
21
+ return (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean');
22
+ }
23
+ /**
24
+ * Convert supported metadata primitives to string for title handling
25
+ */
26
+ function toTitleString(value) {
27
+ return isTagValue(value) ? String(value) : undefined;
28
+ }
17
29
  class Collection {
18
30
  /**
19
31
  * The base metadata object
@@ -40,8 +52,8 @@ var Metadata;
40
52
  const metaMap = new Map();
41
53
  const linkMap = new Map();
42
54
  for (const item of items) {
43
- if (item.title) {
44
- const titleStr = item.title.toString();
55
+ const titleStr = toTitleString(item.title);
56
+ if (titleStr !== undefined) {
45
57
  if (titleStr.includes(TITLE_TEMPLATE_STR)) {
46
58
  titleTemplate = titleStr;
47
59
  }
@@ -119,10 +131,10 @@ var Metadata;
119
131
  if (items.length === 0)
120
132
  return Collection.#clone(this.#base);
121
133
  let merged = Collection.#clone(this.#base);
122
- const res = await Promise.allSettled(items.map(entry => entry.task));
134
+ const res = await Promise.allSettled(items.map(item => item.task));
123
135
  const ok = res
124
136
  .filter((result) => result.status === 'fulfilled')
125
- .map(result => result.value);
137
+ .map((result) => result.value);
126
138
  if (ok.length)
127
139
  merged = Collection.#merge(merged, ...ok);
128
140
  return merged;
@@ -1,5 +1,5 @@
1
- import type { BoundaryError } from '../../types';
2
- import { type HttpException } from './http-exception';
1
+ import type { BoundaryError } from '../../types.js';
2
+ import { type HttpException } from './http-exception.js';
3
3
  type ComponentsMap = Partial<Record<HttpException.StatusCode, React.ReactElement | null>>;
4
4
  export type Props = {
5
5
  fallback: ((error: BoundaryError) => React.ReactNode) | React.ReactNode;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Component } from 'react';
4
- import { HTTP_EXCEPTION_DIGEST_PREFIX, isHttpException, } from './http-exception';
4
+ import { HTTP_EXCEPTION_DIGEST_PREFIX, isHttpException, } from './http-exception.js';
5
5
  function isSupportedStatusCode(value) {
6
6
  return value === 401 || value === 403 || value === 404 || value === 500;
7
7
  }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useEffect, useRef } from 'react';
4
- import { useRouter } from '../router/use-router';
4
+ import { useRouter } from '../router/use-router.js';
5
5
  function guard(path, prefetcher) {
6
6
  const connection = window.navigator.connection;
7
7
  if (document.visibilityState === 'hidden')
@@ -1,4 +1,4 @@
1
- import type { BoundaryError } from '../../types';
1
+ import type { BoundaryError } from '../../types.js';
2
2
  export type Props = {
3
3
  fallback: ((error: BoundaryError) => React.ReactNode) | React.ReactNode;
4
4
  children: React.ReactNode;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { Component } from 'react';
4
- import { isRedirect, REDIRECT_DIGEST_PREFIX } from './redirect';
4
+ import { isRedirect, REDIRECT_DIGEST_PREFIX } from './redirect.js';
5
5
  class Boundary extends Component {
6
6
  constructor(props) {
7
7
  super(props);
@@ -1,4 +1,4 @@
1
- import { Solas } from '../../solas';
1
+ import { Solas } from '../../solas.js';
2
2
  /**
3
3
  * Redirect exception class to signal a redirect
4
4
  */