@jk2908/solas 0.1.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 (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +219 -0
  5. package/dist/error-boundary.d.ts +1 -0
  6. package/dist/error-boundary.js +1 -0
  7. package/dist/index.d.ts +7 -0
  8. package/dist/index.js +235 -0
  9. package/dist/internal/build.d.ts +104 -0
  10. package/dist/internal/build.js +633 -0
  11. package/dist/internal/codegen/config.d.ts +5 -0
  12. package/dist/internal/codegen/config.js +19 -0
  13. package/dist/internal/codegen/environments.d.ts +12 -0
  14. package/dist/internal/codegen/environments.js +42 -0
  15. package/dist/internal/codegen/manifest.d.ts +5 -0
  16. package/dist/internal/codegen/manifest.js +15 -0
  17. package/dist/internal/codegen/maps.d.ts +5 -0
  18. package/dist/internal/codegen/maps.js +75 -0
  19. package/dist/internal/codegen/utils.d.ts +1 -0
  20. package/dist/internal/codegen/utils.js +2 -0
  21. package/dist/internal/env/browser.d.ts +4 -0
  22. package/dist/internal/env/browser.js +58 -0
  23. package/dist/internal/env/request-context.d.ts +19 -0
  24. package/dist/internal/env/request-context.js +2 -0
  25. package/dist/internal/env/rsc.d.ts +39 -0
  26. package/dist/internal/env/rsc.js +368 -0
  27. package/dist/internal/env/ssr.d.ts +42 -0
  28. package/dist/internal/env/ssr.js +149 -0
  29. package/dist/internal/env/utils.d.ts +2 -0
  30. package/dist/internal/env/utils.js +28 -0
  31. package/dist/internal/metadata.d.ts +81 -0
  32. package/dist/internal/metadata.js +185 -0
  33. package/dist/internal/navigation/http-exception-boundary.d.ts +12 -0
  34. package/dist/internal/navigation/http-exception-boundary.js +48 -0
  35. package/dist/internal/navigation/http-exception.d.ts +33 -0
  36. package/dist/internal/navigation/http-exception.js +45 -0
  37. package/dist/internal/navigation/link.d.ts +13 -0
  38. package/dist/internal/navigation/link.js +63 -0
  39. package/dist/internal/navigation/redirect-boundary.d.ts +12 -0
  40. package/dist/internal/navigation/redirect-boundary.js +39 -0
  41. package/dist/internal/navigation/redirect.d.ts +21 -0
  42. package/dist/internal/navigation/redirect.js +63 -0
  43. package/dist/internal/navigation/use-search-params.d.ts +1 -0
  44. package/dist/internal/navigation/use-search-params.js +13 -0
  45. package/dist/internal/prerender.d.ts +151 -0
  46. package/dist/internal/prerender.js +422 -0
  47. package/dist/internal/render/head.d.ts +4 -0
  48. package/dist/internal/render/head.js +38 -0
  49. package/dist/internal/render/tree.d.ts +47 -0
  50. package/dist/internal/render/tree.js +108 -0
  51. package/dist/internal/router/create-router.d.ts +6 -0
  52. package/dist/internal/router/create-router.js +95 -0
  53. package/dist/internal/router/pattern.d.ts +8 -0
  54. package/dist/internal/router/pattern.js +31 -0
  55. package/dist/internal/router/prefetcher.d.ts +47 -0
  56. package/dist/internal/router/prefetcher.js +90 -0
  57. package/dist/internal/router/resolver.d.ts +174 -0
  58. package/dist/internal/router/resolver.js +356 -0
  59. package/dist/internal/router/router-context.d.ts +11 -0
  60. package/dist/internal/router/router-context.js +7 -0
  61. package/dist/internal/router/router-provider.d.ts +6 -0
  62. package/dist/internal/router/router-provider.js +131 -0
  63. package/dist/internal/router/router.d.ts +79 -0
  64. package/dist/internal/router/router.js +417 -0
  65. package/dist/internal/router/use-router.d.ts +5 -0
  66. package/dist/internal/router/use-router.js +5 -0
  67. package/dist/internal/server/cookies.d.ts +6 -0
  68. package/dist/internal/server/cookies.js +17 -0
  69. package/dist/internal/server/dynamic.d.ts +9 -0
  70. package/dist/internal/server/dynamic.js +22 -0
  71. package/dist/internal/server/headers.d.ts +5 -0
  72. package/dist/internal/server/headers.js +19 -0
  73. package/dist/internal/server/url.d.ts +5 -0
  74. package/dist/internal/server/url.js +16 -0
  75. package/dist/internal/ui/defaults/error.d.ts +4 -0
  76. package/dist/internal/ui/defaults/error.js +6 -0
  77. package/dist/internal/ui/error-boundary.d.ts +26 -0
  78. package/dist/internal/ui/error-boundary.js +41 -0
  79. package/dist/navigation.d.ts +6 -0
  80. package/dist/navigation.js +6 -0
  81. package/dist/prerender.d.ts +1 -0
  82. package/dist/prerender.js +1 -0
  83. package/dist/router.d.ts +4 -0
  84. package/dist/router.js +4 -0
  85. package/dist/server.d.ts +4 -0
  86. package/dist/server.js +4 -0
  87. package/dist/solas.d.ts +32 -0
  88. package/dist/solas.js +125 -0
  89. package/dist/types.d.ts +93 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils/compress.d.ts +11 -0
  92. package/dist/utils/compress.js +76 -0
  93. package/dist/utils/context.d.ts +6 -0
  94. package/dist/utils/context.js +25 -0
  95. package/dist/utils/cookies.d.ts +3 -0
  96. package/dist/utils/cookies.js +35 -0
  97. package/dist/utils/export-reader.d.ts +29 -0
  98. package/dist/utils/export-reader.js +117 -0
  99. package/dist/utils/format.d.ts +6 -0
  100. package/dist/utils/format.js +72 -0
  101. package/dist/utils/logger.d.ts +52 -0
  102. package/dist/utils/logger.js +105 -0
  103. package/dist/utils/time.d.ts +4 -0
  104. package/dist/utils/time.js +29 -0
  105. package/package.json +111 -0
@@ -0,0 +1,422 @@
1
+ import path from 'node:path';
2
+ import { compile } from 'path-to-regexp';
3
+ import { Solas } from '../solas';
4
+ import { Logger } from '../utils/logger';
5
+ import { Time } from '../utils/time';
6
+ import { toPathPattern } from './router/pattern';
7
+ const logger = new Logger();
8
+ export { Prerender };
9
+ var Prerender;
10
+ (function (Prerender) {
11
+ const DEFAULT_TIMEOUT_MS = 15_000;
12
+ const DEFAULT_CONCURRENCY = 4;
13
+ let Artifact;
14
+ (function (Artifact) {
15
+ const manifestCache = new Map();
16
+ /**
17
+ * Get the root directory path where prerender artifacts are stored,
18
+ * based on the output directory specified in the configuration
19
+ */
20
+ function getRootPath(outDir) {
21
+ return path.join(outDir, Solas.Config.GENERATED_DIR, 'ppr');
22
+ }
23
+ Artifact.getRootPath = getRootPath;
24
+ /**
25
+ * Get the file system path for the prerender artifact manifest, which
26
+ * contains metadata about all prerendered routes and their artifacts
27
+ */
28
+ function getManifestPath(outDir) {
29
+ return path.join(getRootPath(outDir), 'manifest.json');
30
+ }
31
+ Artifact.getManifestPath = getManifestPath;
32
+ /**
33
+ * Get the file system path for storing prerender artifacts for a given route
34
+ */
35
+ function getPath(outDir, pathname) {
36
+ const root = path.resolve(getRootPath(outDir));
37
+ const dir = pathname === '/' ? 'index' : pathname.replace(/^\//, '');
38
+ const artifactPath = path.resolve(root, dir);
39
+ // this also runs at request time, so make sure the pathname cannot escape the artifact folder
40
+ if (artifactPath !== root && !artifactPath.startsWith(`${root}${path.sep}`)) {
41
+ throw new Error('[prerender] invalid artifact path');
42
+ }
43
+ return artifactPath;
44
+ }
45
+ Artifact.getPath = getPath;
46
+ /**
47
+ * Load the prerender artifact manifest for faster runtime route mode checks
48
+ */
49
+ async function loadManifest(outDir) {
50
+ // if we already loaded this outDir, return cached result
51
+ // (either a valid manifest or null when it was missing/invalid)
52
+ if (manifestCache.has(outDir)) {
53
+ return manifestCache.get(outDir) ?? null;
54
+ }
55
+ const file = Bun.file(getManifestPath(outDir));
56
+ // no manifest means no prerender metadata to use
57
+ if (!(await file.exists())) {
58
+ manifestCache.set(outDir, null);
59
+ return null;
60
+ }
61
+ try {
62
+ // parse once, then validate the shape before trusting any fields
63
+ const value = JSON.parse(await file.text());
64
+ if (!value || typeof value !== 'object') {
65
+ manifestCache.set(outDir, null);
66
+ return null;
67
+ }
68
+ const generatedAt = value.generatedAt;
69
+ const routes = value.routes;
70
+ if (typeof generatedAt !== 'number') {
71
+ manifestCache.set(outDir, null);
72
+ return null;
73
+ }
74
+ if (!routes || typeof routes !== 'object') {
75
+ manifestCache.set(outDir, null);
76
+ return null;
77
+ }
78
+ // verify each route entry so runtime can rely on mode/files safely
79
+ for (const entry of Object.values(routes)) {
80
+ if (!entry || typeof entry !== 'object') {
81
+ manifestCache.set(outDir, null);
82
+ return null;
83
+ }
84
+ const { mode, createdAt, files } = entry;
85
+ // only allow known modes
86
+ if (mode !== 'full' && mode !== 'ppr') {
87
+ manifestCache.set(outDir, null);
88
+ return null;
89
+ }
90
+ if (typeof createdAt !== 'number') {
91
+ manifestCache.set(outDir, null);
92
+ return null;
93
+ }
94
+ if (files !== undefined) {
95
+ if (!Array.isArray(files)) {
96
+ manifestCache.set(outDir, null);
97
+ return null;
98
+ }
99
+ // only allow known artifact file labels
100
+ for (const f of files) {
101
+ if (f !== 'html' &&
102
+ f !== 'prelude' &&
103
+ f !== 'postponed' &&
104
+ f !== 'metadata') {
105
+ manifestCache.set(outDir, null);
106
+ return null;
107
+ }
108
+ }
109
+ }
110
+ }
111
+ const manifest = { generatedAt, routes };
112
+ // cache validated manifest to avoid reparsing on every request
113
+ manifestCache.set(outDir, manifest);
114
+ return manifest;
115
+ }
116
+ catch {
117
+ manifestCache.set(outDir, null);
118
+ return null;
119
+ }
120
+ }
121
+ Artifact.loadManifest = loadManifest;
122
+ /**
123
+ * Load the postponed state for a given route from the file system, if it exists
124
+ */
125
+ async function loadPostponedState(outDir, pathname) {
126
+ let file;
127
+ try {
128
+ file = Bun.file(path.join(getPath(outDir, pathname), 'postponed.json'));
129
+ }
130
+ catch (err) {
131
+ logger.warn(`[prerender:artifacts] rejected postponed state path for ${pathname}`, Logger.print(err));
132
+ return null;
133
+ }
134
+ if (!(await file.exists()))
135
+ return null;
136
+ try {
137
+ return JSON.parse(await file.text());
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ Artifact.loadPostponedState = loadPostponedState;
144
+ /**
145
+ * Load the prelude HTML for a given route from the file system, if it exists
146
+ */
147
+ async function loadPrelude(outDir, pathname) {
148
+ let file;
149
+ try {
150
+ file = Bun.file(path.join(getPath(outDir, pathname), 'prelude.html'));
151
+ }
152
+ catch (err) {
153
+ logger.warn(`[prerender:artifacts] rejected prelude path for ${pathname}`, Logger.print(err));
154
+ return null;
155
+ }
156
+ if (!(await file.exists()))
157
+ return null;
158
+ try {
159
+ return await file.text();
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ }
165
+ Artifact.loadPrelude = loadPrelude;
166
+ /**
167
+ * Load the prerender artifact metadata for a given route from the file system, if it exists and is valid
168
+ */
169
+ async function loadMetadata(outDir, pathname) {
170
+ let file;
171
+ try {
172
+ file = Bun.file(path.join(getPath(outDir, pathname), 'metadata.json'));
173
+ }
174
+ catch (err) {
175
+ logger.warn(`[prerender:artifacts] rejected metadata path for ${pathname}`, Logger.print(err));
176
+ return null;
177
+ }
178
+ if (!(await file.exists()))
179
+ return null;
180
+ try {
181
+ const value = JSON.parse(await file.text());
182
+ if (!value || typeof value !== 'object')
183
+ return null;
184
+ const schema = value.schema;
185
+ const route = value.route;
186
+ const createdAt = value.createdAt;
187
+ const mode = value.mode;
188
+ if (typeof schema !== 'string')
189
+ return null;
190
+ if (typeof route !== 'string')
191
+ return null;
192
+ if (typeof createdAt !== 'number')
193
+ return null;
194
+ if (mode !== 'full' && mode !== 'ppr')
195
+ return null;
196
+ return { schema, route, createdAt, mode };
197
+ }
198
+ catch {
199
+ return null;
200
+ }
201
+ }
202
+ Artifact.loadMetadata = loadMetadata;
203
+ /**
204
+ * Check if a prerender artifact is compatible with the current application version and route,
205
+ * based on its metadata
206
+ */
207
+ function isCompatible(artifactMetadata, pathname, mode) {
208
+ const schema = Solas.getVersion();
209
+ return (artifactMetadata.schema === schema &&
210
+ artifactMetadata.route === pathname &&
211
+ artifactMetadata.mode === mode);
212
+ }
213
+ Artifact.isCompatible = isCompatible;
214
+ /**
215
+ * Compose the prelude HTML and the resume stream into a single HTML stream, by injecting the resume stream
216
+ * into the prelude at the appropriate location (before </body> or </html>)
217
+ */
218
+ function composePreludeAndResume(prelude, resumeStream) {
219
+ const lower = prelude.toLowerCase();
220
+ const bodyClose = lower.lastIndexOf('</body>');
221
+ const htmlClose = lower.lastIndexOf('</html>');
222
+ const splitAt = bodyClose >= 0 && htmlClose > bodyClose ? bodyClose : prelude.length;
223
+ return new ReadableStream({
224
+ async start(controller) {
225
+ const encoder = new TextEncoder();
226
+ const decoder = new TextDecoder();
227
+ controller.enqueue(new TextEncoder().encode(prelude.slice(0, splitAt)));
228
+ const reader = resumeStream.getReader();
229
+ let strippedLeadingClose = false;
230
+ try {
231
+ while (true) {
232
+ const { value, done } = await reader.read();
233
+ if (done)
234
+ break;
235
+ if (!value)
236
+ continue;
237
+ if (!strippedLeadingClose) {
238
+ strippedLeadingClose = true;
239
+ const text = decoder.decode(value);
240
+ const trimmed = text.replace(/^\s*<\/body>\s*<\/html>/i, '');
241
+ if (trimmed.length > 0)
242
+ controller.enqueue(encoder.encode(trimmed));
243
+ continue;
244
+ }
245
+ controller.enqueue(value);
246
+ }
247
+ }
248
+ finally {
249
+ reader.releaseLock();
250
+ controller.close();
251
+ }
252
+ },
253
+ });
254
+ }
255
+ Artifact.composePreludeAndResume = composePreludeAndResume;
256
+ })(Artifact = Prerender.Artifact || (Prerender.Artifact = {}));
257
+ let Runtime;
258
+ (function (Runtime) {
259
+ /**
260
+ * Custom error class to indicate that prerendering has been postponed to request-time
261
+ */
262
+ class Postponed extends Error {
263
+ constructor(message = 'postponed') {
264
+ super(message);
265
+ this.name = 'Postponed';
266
+ }
267
+ }
268
+ Runtime.Postponed = Postponed;
269
+ /**
270
+ * Type guard to check if an error is a Postponed error, including wrapped errors like
271
+ * AbortError or TimeoutError
272
+ */
273
+ function isPostponed(error) {
274
+ if (error instanceof Postponed)
275
+ return true;
276
+ if (error instanceof Error &&
277
+ (error.name === 'AbortError' || error.name === 'TimeoutError')) {
278
+ if (error.cause instanceof Postponed)
279
+ return true;
280
+ }
281
+ return false;
282
+ }
283
+ Runtime.isPostponed = isPostponed;
284
+ })(Runtime = Prerender.Runtime || (Prerender.Runtime = {}));
285
+ let Build;
286
+ (function (Build) {
287
+ /**
288
+ * Get the prerender timeout value from the environment variable, or return the default
289
+ * if it's not set or invalid
290
+ */
291
+ function getTimeout() {
292
+ const v = Number(process.env.SOLAS_PRERENDER_TIMEOUT_MS);
293
+ if (!Number.isFinite(v) || v <= 0) {
294
+ return DEFAULT_TIMEOUT_MS;
295
+ }
296
+ return v;
297
+ }
298
+ Build.getTimeout = getTimeout;
299
+ /**
300
+ * Get the prerender concurrency value from the environment variable, or return the default
301
+ * if it's not set or invalid
302
+ */
303
+ function getConcurrency() {
304
+ const v = Number(process.env.SOLAS_PRERENDER_CONCURRENCY);
305
+ if (!Number.isInteger(v) || v <= 0) {
306
+ return DEFAULT_CONCURRENCY;
307
+ }
308
+ return v;
309
+ }
310
+ Build.getConcurrency = getConcurrency;
311
+ /**
312
+ * Extract the prerendering mode ('full', 'ppr', or false) from the source code of a route module, by
313
+ * looking for an exported `prerender` binding and validating its value
314
+ */
315
+ async function getStaticFlag(filePath, buildContext) {
316
+ return buildContext.exportReader.literal(filePath, 'prerender', (v) => v === 'full' || v === 'ppr' || v === false);
317
+ }
318
+ Build.getStaticFlag = getStaticFlag;
319
+ /**
320
+ * Get the list of static parameters for a dynamic route, by looking for an exported `params` function
321
+ * in the route module and calling it to get the list of parameter objects
322
+ */
323
+ async function getStaticParams(filePath, buildContext) {
324
+ const params = await buildContext.exportReader.value(filePath, 'params', (v) => typeof v === 'function');
325
+ if (!params)
326
+ return [];
327
+ const resolved = await Time.timeout(Promise.try(() => params()), getTimeout(), `static params for ${filePath}`);
328
+ if (!Array.isArray(resolved))
329
+ return [];
330
+ return resolved;
331
+ }
332
+ Build.getStaticParams = getStaticParams;
333
+ /**
334
+ * Generate the list of prerenderable routes for a dynamic route, by combining the static parameters obtained from
335
+ * the route module with the route pattern, and filtering out any routes that still contain dynamic segments
336
+ */
337
+ function getDynamicRouteList(route, paramNames, staticParams) {
338
+ if (!staticParams.length)
339
+ return [];
340
+ const { path: compilePath, wildcardNames } = toPathPattern(route, paramNames);
341
+ const toPath = compile(compilePath);
342
+ return staticParams
343
+ .map(value => {
344
+ try {
345
+ return toPath(Object.fromEntries(Object.entries(value).map(([key, entry]) => [
346
+ key,
347
+ wildcardNames.has(key)
348
+ ? Array.isArray(entry)
349
+ ? entry.map(part => String(part))
350
+ : [String(entry)]
351
+ : Array.isArray(entry)
352
+ ? entry.map(part => String(part)).join('/')
353
+ : String(entry),
354
+ ])));
355
+ }
356
+ catch {
357
+ return null;
358
+ }
359
+ })
360
+ .filter((value) => value !== null)
361
+ .filter(r => !r.includes(':') && !r.includes('*'));
362
+ }
363
+ Build.getDynamicRouteList = getDynamicRouteList;
364
+ /**
365
+ * Function to prerender a single route by making a request to the route with special headers, and returning the
366
+ * result which includes either the prerender artifact or an error/status code if the prerendering failed or was
367
+ * postponed to request-time
368
+ */
369
+ async function get(app, route, opts) {
370
+ const url = `${opts.origin ?? `http://${Solas.Config.SLUG}.local`}${route}`;
371
+ const res = await Time.timeout(app.fetch(new Request(url, {
372
+ headers: {
373
+ Accept: 'text/html',
374
+ [`x-${Solas.Config.SLUG}-prerender`]: '1',
375
+ [`x-${Solas.Config.SLUG}-prerender-artifact`]: '1',
376
+ },
377
+ })), opts.timeout, `route ${route}`);
378
+ if (!(res instanceof Response)) {
379
+ const error = new TypeError(`Invalid response for ${route}`);
380
+ logger.error(`[prerender:get] ${error.message}`, error);
381
+ throw error;
382
+ }
383
+ if (!res.ok)
384
+ return { route, status: res.status };
385
+ return { route, artifact: await res.json() };
386
+ }
387
+ Build.get = get;
388
+ /**
389
+ * Run the prerendering process for a list of routes, with a specified concurrency limit and timeout for
390
+ * each route, by calling the 'get' function for each route and yielding the results as they
391
+ * become available
392
+ */
393
+ async function* run(app, routes, opts) {
394
+ const limit = Math.max(1, Math.min(opts.concurrency ?? 4, routes.length || 1));
395
+ let index = 0;
396
+ const pending = new Map();
397
+ function enqueue() {
398
+ while (index < routes.length && pending.size < limit) {
399
+ const i = index++;
400
+ const value = routes[i];
401
+ pending.set(i, get(app, value, {
402
+ timeout: opts.timeout,
403
+ origin: opts.origin,
404
+ })
405
+ .then(result => ({ index: i, result }))
406
+ .catch(err => ({
407
+ index: i,
408
+ result: { route: value, error: err },
409
+ })));
410
+ }
411
+ }
412
+ enqueue();
413
+ while (pending.size > 0) {
414
+ const settled = await Promise.race(pending.values());
415
+ pending.delete(settled.index);
416
+ yield settled.result;
417
+ enqueue();
418
+ }
419
+ }
420
+ Build.run = run;
421
+ })(Build = Prerender.Build || (Prerender.Build = {}));
422
+ })(Prerender || (Prerender = {}));
@@ -0,0 +1,4 @@
1
+ import { type Metadata as Collection } from '../metadata';
2
+ export declare function Head({ metadata: m }: {
3
+ metadata?: Collection.Item | Promise<Collection.Item>;
4
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,38 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { use } from 'react';
3
+ import { Solas } from '../../solas';
4
+ import { Logger } from '../../utils/logger';
5
+ const logger = new Logger();
6
+ const cache = new WeakMap();
7
+ export function Head({ metadata: m, }) {
8
+ if (!m)
9
+ return null;
10
+ const metadata = use(toSafeUsable(m));
11
+ return (_jsxs(_Fragment, { children: [
12
+ _jsx("meta", { name: "generator", content: Solas.Config.NAME }), metadata.title && _jsx("title", { children: metadata.title.toString() }), metadata.meta?.map(meta => {
13
+ if ('charSet' in meta) {
14
+ return _jsx("meta", { charSet: meta.charSet }, meta.charSet);
15
+ }
16
+ if ('name' in meta) {
17
+ return (_jsx("meta", { name: meta.name, content: meta.content?.toString() }, meta.name));
18
+ }
19
+ if ('httpEquiv' in meta) {
20
+ return (_jsx("meta", { httpEquiv: meta.httpEquiv, content: meta.content?.toString() }, meta.httpEquiv));
21
+ }
22
+ if ('property' in meta) {
23
+ return (_jsx("meta", { property: meta.property, content: meta.content?.toString() }, meta.property));
24
+ }
25
+ return null;
26
+ }), metadata.link?.map(link => (_jsx("link", { ...link }, `${link.rel}${link.href ?? ''}`)))] }));
27
+ }
28
+ function toSafeUsable(metadata) {
29
+ const cached = cache.get(metadata);
30
+ if (cached)
31
+ return cached;
32
+ const safe = Promise.resolve(metadata).catch(err => {
33
+ logger.error('[head] failed to resolve metadata', err);
34
+ return {};
35
+ });
36
+ cache.set(metadata, safe);
37
+ return safe;
38
+ }
@@ -0,0 +1,47 @@
1
+ import type { Resolver } from '../router/resolver';
2
+ type Match = NonNullable<Resolver.EnhancedMatch>;
3
+ /**
4
+ * Render the resolved route tree for a matched page
5
+ *
6
+ * The shell is always `layouts[0]`. Every deeper segment is then wrapped from
7
+ * the inside out in this order:
8
+ *
9
+ * 1. `Layout`
10
+ * 2. `Suspense` with that segment's loading fallback
11
+ * 3. `HttpExceptionBoundary` with that segment's status boundaries
12
+ *
13
+ * The shell level is applied last using the same outer wrapper order:
14
+ * `HttpExceptionBoundary` -> `Suspense` -> `Shell`
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * <HttpExceptionBoundary shell>
19
+ * <Suspense fallback={<ShellLoading />}>
20
+ * <Shell>
21
+ * <HttpExceptionBoundary segmentN>
22
+ * <Suspense fallback={<LoadingN />}>
23
+ * <LayoutN>
24
+ * ...
25
+ * <HttpExceptionBoundary segment1>
26
+ * <Suspense fallback={<Loading1 />}>
27
+ * <Layout1>
28
+ * <Page />
29
+ * </Layout1>
30
+ * </Suspense>
31
+ * </HttpExceptionBoundary>
32
+ * ...
33
+ * </LayoutN>
34
+ * </Suspense>
35
+ * </HttpExceptionBoundary>
36
+ * </Shell>
37
+ * </Suspense>
38
+ * </HttpExceptionBoundary>
39
+ * ```
40
+ */
41
+ export declare function Tree({ depth, params, error, ui }: {
42
+ depth: Match['__depth'];
43
+ params: Match['params'];
44
+ error: Match['error'];
45
+ ui: Match['ui'];
46
+ }): import("react/jsx-runtime").JSX.Element;
47
+ export {};
@@ -0,0 +1,108 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Suspense } from 'react';
3
+ import { HttpException, isHttpException } from '../navigation/http-exception';
4
+ import { HttpExceptionBoundary } from '../navigation/http-exception-boundary';
5
+ import DefaultErr from '../ui/defaults/error';
6
+ /**
7
+ * Render the resolved route tree for a matched page
8
+ *
9
+ * The shell is always `layouts[0]`. Every deeper segment is then wrapped from
10
+ * the inside out in this order:
11
+ *
12
+ * 1. `Layout`
13
+ * 2. `Suspense` with that segment's loading fallback
14
+ * 3. `HttpExceptionBoundary` with that segment's status boundaries
15
+ *
16
+ * The shell level is applied last using the same outer wrapper order:
17
+ * `HttpExceptionBoundary` -> `Suspense` -> `Shell`
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <HttpExceptionBoundary shell>
22
+ * <Suspense fallback={<ShellLoading />}>
23
+ * <Shell>
24
+ * <HttpExceptionBoundary segmentN>
25
+ * <Suspense fallback={<LoadingN />}>
26
+ * <LayoutN>
27
+ * ...
28
+ * <HttpExceptionBoundary segment1>
29
+ * <Suspense fallback={<Loading1 />}>
30
+ * <Layout1>
31
+ * <Page />
32
+ * </Layout1>
33
+ * </Suspense>
34
+ * </HttpExceptionBoundary>
35
+ * ...
36
+ * </LayoutN>
37
+ * </Suspense>
38
+ * </HttpExceptionBoundary>
39
+ * </Shell>
40
+ * </Suspense>
41
+ * </HttpExceptionBoundary>
42
+ * ```
43
+ */
44
+ export function Tree({ depth, params, error, ui, }) {
45
+ const { layouts, Page, '401s': unauthorized, '403s': forbidden, '404s': notFounds, '500s': serverErrors, loaders, } = ui;
46
+ const Shell = layouts[0];
47
+ if (!Shell)
48
+ throw new Error('Shell layout is required in the route tree');
49
+ // build the inner inner (everything after shell)
50
+ let inner = null;
51
+ // map http status codes to exception components
52
+ const httpExceptionMap = {
53
+ 401: unauthorized,
54
+ 403: forbidden,
55
+ 404: notFounds,
56
+ 500: serverErrors,
57
+ };
58
+ if (error && isHttpException(error)) {
59
+ const Exception = httpExceptionMap[error.status].slice(0, depth + 1).findLast(e => e !== null) ??
60
+ DefaultErr;
61
+ inner = (_jsxs(_Fragment, { children: [
62
+ _jsx("meta", { name: "robots", content: "noindex,nofollow" }), _jsx(Exception, { error: error })
63
+ ] }));
64
+ }
65
+ else if (Page) {
66
+ inner = _jsx(Page, { params: params });
67
+ }
68
+ // wrap from innermost to layouts[1] (skip shell)
69
+ for (let idx = layouts.length - 1; idx >= 1; idx--) {
70
+ const Layout = layouts[idx];
71
+ const Loading = loaders[idx];
72
+ const Unauthorized = unauthorized[idx];
73
+ const Forbidden = forbidden[idx];
74
+ const NotFound = notFounds[idx];
75
+ const ServerError = serverErrors[idx];
76
+ // wrap in layout
77
+ if (Layout) {
78
+ inner = (_jsx(Layout, { params: params, children: inner }, `l:${idx}`));
79
+ }
80
+ // wrap in suspense (for this segment's loading state)
81
+ if (Loading) {
82
+ inner = _jsx(Suspense, { fallback: _jsx(Loading, {}), children: inner });
83
+ }
84
+ const errorBoundaries = {
85
+ 401: Unauthorized ? (_jsx(Unauthorized, { error: new HttpException(401, 'Unauthorized') })) : null,
86
+ 403: Forbidden ? _jsx(Forbidden, { error: new HttpException(403, 'Forbidden') }) : null,
87
+ 404: NotFound ? _jsx(NotFound, { error: new HttpException(404, 'Not found') }) : null,
88
+ 500: ServerError ? (_jsx(ServerError, { error: new HttpException(500, 'Internal Server Error') })) : null,
89
+ };
90
+ // wrap in error boundaries (if supplied for this segment's http errors)
91
+ if (Object.values(errorBoundaries).some(c => c !== null)) {
92
+ inner = (_jsx(HttpExceptionBoundary, { components: errorBoundaries, children: inner }));
93
+ }
94
+ }
95
+ // now wrap with shell structure: shell renders immediately,
96
+ // inner streams inside Suspense
97
+ const ShellLoading = loaders[0];
98
+ const ShellUnauthorized = unauthorized[0];
99
+ const ShellForbidden = forbidden[0];
100
+ const ShellNotFound = notFounds[0];
101
+ const ShellServerError = serverErrors[0];
102
+ return (_jsx(HttpExceptionBoundary, { components: {
103
+ 401: ShellUnauthorized ? _jsx(ShellUnauthorized, {}) : null,
104
+ 403: ShellForbidden ? _jsx(ShellForbidden, {}) : null,
105
+ 404: ShellNotFound ? _jsx(ShellNotFound, {}) : null,
106
+ 500: ShellServerError ? _jsx(ShellServerError, {}) : null,
107
+ }, children: _jsx(Suspense, { fallback: ShellLoading ? _jsx(ShellLoading, {}) : null, children: _jsx(Shell, { params: params, children: inner }) }) }));
108
+ }
@@ -0,0 +1,6 @@
1
+ import type { ImportMap, Manifest, PluginConfig, SolasRequest } from '../../types';
2
+ import { Router } from './router';
3
+ /**
4
+ * Create the application router from the generated manifest and import map
5
+ */
6
+ export declare function createRouter(config: Pick<PluginConfig, 'precompress' | 'trailingSlash'>, manifest: Manifest, importMap: ImportMap, rsc: (req: SolasRequest) => Response | Promise<Response>): Router;