@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,417 @@
1
+ import path from 'node:path';
2
+ import { match as createMatch } from 'path-to-regexp';
3
+ import { Solas } from '../../solas';
4
+ import { maybeActionWithParsedFormData } from '../env/rsc';
5
+ import { HttpException } from '../navigation/http-exception';
6
+ import { toPathPattern } from './pattern';
7
+ /**
8
+ * Handle routing and matching for server requests
9
+ */
10
+ export class Router {
11
+ opts;
12
+ static #matchers = new WeakMap();
13
+ #routes = {
14
+ // exact match by method + path
15
+ static: new Map(),
16
+ dynamic: {
17
+ // candidate routes bucketed by segment length
18
+ byLength: new Map(),
19
+ // fast path for static prefixes
20
+ byPrefix: new Map(),
21
+ },
22
+ // wildcard routes checked last, narrowed by first literal segment when possible
23
+ wildcard: {
24
+ byPrefix: new Map(),
25
+ fallback: [],
26
+ },
27
+ };
28
+ #middleware = { global: [] };
29
+ #onError;
30
+ constructor(opts = {}) {
31
+ this.opts = opts;
32
+ this.fetch = this.fetch.bind(this);
33
+ }
34
+ /**
35
+ * Register middleware for all routes
36
+ */
37
+ use(...middleware) {
38
+ this.#middleware.global.push(...middleware);
39
+ return this;
40
+ }
41
+ /**
42
+ * Register an error handler for routing failures
43
+ */
44
+ error(handler) {
45
+ this.#onError = handler;
46
+ return this;
47
+ }
48
+ /**
49
+ * Register a route handler
50
+ */
51
+ add(path, method, handler, params, middleware = []) {
52
+ const segments = Router.#split(path);
53
+ const tokens = [];
54
+ let score = 0;
55
+ let wildcard = false;
56
+ // turn the route path into tokens once so registration and matching can
57
+ // share the same specificity rules
58
+ for (const segment of segments) {
59
+ if (segment === '*') {
60
+ wildcard = true;
61
+ tokens.push({ kind: 'wildcard', value: params?.[0] ?? '*' });
62
+ continue;
63
+ }
64
+ if (segment.startsWith(':')) {
65
+ tokens.push({ kind: 'dynamic', value: segment.slice(1) });
66
+ score += 1;
67
+ continue;
68
+ }
69
+ tokens.push({ kind: 'static', value: segment });
70
+ score += 2;
71
+ }
72
+ const route = {
73
+ path,
74
+ method: method.toUpperCase(),
75
+ handler,
76
+ middleware: [...middleware],
77
+ tokens,
78
+ length: segments.length,
79
+ score,
80
+ wildcard,
81
+ };
82
+ // static route, easy map set
83
+ if (!path.includes(':') && !path.includes('*')) {
84
+ this.#routes.static.set(`${route.method}:${path}`, route);
85
+ return this;
86
+ }
87
+ // wildcard route, push to end of list
88
+ if (wildcard) {
89
+ const prefix = route.tokens[0]?.kind === 'static' ? route.tokens[0].value : undefined;
90
+ if (prefix) {
91
+ const prefixed = this.#routes.wildcard.byPrefix.get(prefix) ?? [];
92
+ prefixed.push(route);
93
+ this.#routes.wildcard.byPrefix.set(prefix, prefixed);
94
+ }
95
+ else {
96
+ this.#routes.wildcard.fallback.push(route);
97
+ }
98
+ return this;
99
+ }
100
+ // dynamic routes are looked up through two indexes; one grouped
101
+ // by segment count, and one grouped by the first static segment
102
+ const bucket = this.#routes.dynamic.byLength.get(route.length) ?? [];
103
+ bucket.push(route);
104
+ this.#routes.dynamic.byLength.set(route.length, bucket);
105
+ // only routes that start with a literal segment go into the prefix index.
106
+ // Routes that start dynamically still fall back to the length-based
107
+ // lookup, so this shortcut doesn't accidentally skip a better match
108
+ const prefix = route.tokens[0]?.kind === 'static' ? route.tokens[0].value : undefined;
109
+ if (prefix) {
110
+ const prefixed = this.#routes.dynamic.byPrefix.get(prefix) ?? [];
111
+ prefixed.push(route);
112
+ this.#routes.dynamic.byPrefix.set(prefix, prefixed);
113
+ }
114
+ return this;
115
+ }
116
+ /**
117
+ * Match a path and method, returning params and route
118
+ */
119
+ match(path, method) {
120
+ const direct = this.#routes.static.get(`${method}:${path}`);
121
+ // direct match - quick return
122
+ if (direct)
123
+ return { route: direct, params: {} };
124
+ // HEAD falls back to GET when HEAD is not explicitly defined
125
+ if (method === 'HEAD') {
126
+ const directGet = this.#routes.static.get(`GET:${path}`);
127
+ if (directGet)
128
+ return { route: directGet, params: {} };
129
+ }
130
+ // else dynamic/wildcard match
131
+ const segments = Router.#split(path);
132
+ // try the leading-static prefix bucket first
133
+ const prefixed = this.#routes.dynamic.byPrefix.get(segments[0] ?? '');
134
+ const prefixedMatch = prefixed ? Router.#pick(prefixed, segments, method) : null;
135
+ if (prefixedMatch)
136
+ return prefixedMatch;
137
+ // if the prefix bucket has no winner, fall back to all dynamic
138
+ // routes with the same segment count
139
+ const dynamicMatch = Router.#pick(this.#routes.dynamic.byLength.get(segments.length) ?? [], segments, method);
140
+ if (dynamicMatch)
141
+ return dynamicMatch;
142
+ // finally check wildcard routes, prefixed first, then fully generic ones
143
+ const wildcardPrefixed = this.#routes.wildcard.byPrefix.get(segments[0] ?? '');
144
+ const wildcardMatch = wildcardPrefixed
145
+ ? Router.#pick(wildcardPrefixed, segments, method)
146
+ : null;
147
+ if (wildcardMatch)
148
+ return wildcardMatch;
149
+ const wildcardFallbackMatch = Router.#pick(this.#routes.wildcard.fallback, segments, method);
150
+ if (wildcardFallbackMatch)
151
+ return wildcardFallbackMatch;
152
+ // no match
153
+ return null;
154
+ }
155
+ /**
156
+ * Handle an incoming request
157
+ */
158
+ async fetch(req) {
159
+ const url = new URL(req.url);
160
+ const path = Router.#normalise(url.pathname, this.opts.trailingSlash);
161
+ let match = null;
162
+ let action = false;
163
+ try {
164
+ if (path !== url.pathname) {
165
+ // rebuild the request with the canonical pathname so downstream code
166
+ // sees the same url the router matched against
167
+ url.pathname = path;
168
+ req = new Request(url.toString(), req);
169
+ }
170
+ const { action: isAction, formData: parsedFormData } = await maybeActionWithParsedFormData(req);
171
+ action = isAction;
172
+ const method = req.method.toUpperCase();
173
+ // action requests stay on the same pathname only the method is
174
+ // normalised to GET this lets page/layout routes match for
175
+ // rerender action execution still reads POST body and
176
+ // may redirect()
177
+ match = this.match(path, action ? 'GET' : method);
178
+ if (!match) {
179
+ const error = new HttpException(404, 'Not found');
180
+ // unmatched requests still pass through the shared error hook with the
181
+ // same request metadata shape as matched requests
182
+ return (this.#onError?.(error, Object.assign(req, {
183
+ [Solas.Config.REQUEST_META]: { match: null, error, action },
184
+ })) ?? new Response(error.message, { status: error.status }));
185
+ }
186
+ const matched = match;
187
+ // attach routing state to the request once so middleware and handlers can
188
+ // read the same per-request metadata
189
+ const request = Object.assign(req, {
190
+ [Solas.Config.REQUEST_META]: { match: matched, action, parsedFormData },
191
+ });
192
+ // global middleware stays outside route middleware by preserving
193
+ // registration order here before composition in #run
194
+ const stack = [...this.#middleware.global, ...matched.route.middleware];
195
+ return this.#run(stack, request, () => matched.route.handler?.(request) ?? new Response('Not found', { status: 404 }));
196
+ }
197
+ catch (err) {
198
+ // normalise unknown throwables so the error hook always receives an Error
199
+ const error = err instanceof Error ? err : new Error(String(err), { cause: err });
200
+ const request = Object.assign(req, {
201
+ [Solas.Config.REQUEST_META]: { match, error, action },
202
+ });
203
+ if (this.#onError)
204
+ return this.#onError(error, request);
205
+ if (error instanceof HttpException) {
206
+ return new Response(error.message, { status: error.status });
207
+ }
208
+ return new Response('Internal Server Error', { status: 500 });
209
+ }
210
+ }
211
+ /**
212
+ * Run middleware stack
213
+ */
214
+ #run(stack, req, next) {
215
+ // compose middleware stack
216
+ let run = () => Promise.resolve(next());
217
+ // unwind stack
218
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
219
+ const handler = stack[i];
220
+ const prev = run;
221
+ run = () => Promise.resolve(handler(req, prev));
222
+ }
223
+ // run composed middleware stack
224
+ return run();
225
+ }
226
+ /**
227
+ * Serve static assets from the output directory
228
+ * @note generated /assets/* handlers bypass +middleware conventions
229
+ */
230
+ static static(config) {
231
+ return async (req) => {
232
+ const pathname = new URL(req.url).pathname;
233
+ const outDir = path.resolve(Solas.Config.OUT_DIR);
234
+ const staticRoot = path.resolve(outDir, 'client');
235
+ let decodedPathname = pathname;
236
+ try {
237
+ // validate any percent-encoding before resolving the asset path
238
+ decodedPathname = decodeURIComponent(pathname);
239
+ }
240
+ catch {
241
+ return new Response('Bad Request', { status: 400 });
242
+ }
243
+ const relativePath = decodedPathname.replace(/^\/+/, '');
244
+ const filePath = path.resolve(staticRoot, relativePath);
245
+ // keep asset requests pinned under the client output root even if the
246
+ // incoming path contains traversal segments
247
+ if (filePath !== staticRoot && !filePath.startsWith(`${staticRoot}${path.sep}`)) {
248
+ return new Response('Forbidden', { status: 403 });
249
+ }
250
+ // emitted assets are fingerprinted so they can be cached aggressively
251
+ return Router.serve(filePath, req, config.precompress, {
252
+ 'Cache-Control': 'public, immutable, max-age=31536000',
253
+ });
254
+ };
255
+ }
256
+ /**
257
+ * Serve a file with optional compression content negotiation
258
+ */
259
+ static async serve(filePath, req, precompress = false, headers = {}) {
260
+ const accept = req.headers.get('accept-encoding') ?? '';
261
+ let file = Bun.file(filePath);
262
+ let encoding = null;
263
+ if (precompress) {
264
+ // prefer a precompressed variant when the client accepts it and one was emitted
265
+ if (accept.includes('br')) {
266
+ const brotli = Bun.file(`${filePath}.br`);
267
+ if (await brotli.exists()) {
268
+ file = brotli;
269
+ encoding = 'br';
270
+ }
271
+ }
272
+ }
273
+ if (!(await file.exists())) {
274
+ return new Response('Not found', { status: 404 });
275
+ }
276
+ // get mime type from original path, not compressed variant
277
+ const mimeType = Bun.file(filePath).type;
278
+ const res = new Response(file, {
279
+ headers: {
280
+ 'Content-Type': headers['Content-Type'] ?? mimeType,
281
+ },
282
+ });
283
+ for (const [key, value] of Object.entries(headers)) {
284
+ res.headers.set(key, value);
285
+ }
286
+ if (precompress)
287
+ res.headers.set('Vary', 'Accept-Encoding');
288
+ if (encoding)
289
+ res.headers.set('Content-Encoding', encoding);
290
+ return res;
291
+ }
292
+ /**
293
+ * Normalise a path based on router options
294
+ */
295
+ static #normalise(path, trailingSlash = true) {
296
+ if (!trailingSlash) {
297
+ // collapse non-root trailing slashes when the router runs in slashless mode
298
+ return path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
299
+ }
300
+ if (path === '/')
301
+ return path;
302
+ // otherwise make non-root paths canonical with a trailing slash
303
+ return path.endsWith('/') ? path : `${path}/`;
304
+ }
305
+ /**
306
+ * Split a path into segments
307
+ */
308
+ static #split(path) {
309
+ if (path === '/')
310
+ return [];
311
+ const parts = [];
312
+ let start = 0;
313
+ // walk the string once so we avoid empty segments from repeated or edge slashes
314
+ for (let i = 0; i <= path.length; i += 1) {
315
+ const char = path[i];
316
+ if (char !== '/' && i !== path.length)
317
+ continue;
318
+ if (i > start) {
319
+ parts.push(path.slice(start, i));
320
+ }
321
+ start = i + 1;
322
+ }
323
+ return parts;
324
+ }
325
+ /**
326
+ * Get or create a path matcher for a route using path-to-regexp
327
+ */
328
+ static #getMatcher(route) {
329
+ const cached = Router.#matchers.get(route);
330
+ if (cached)
331
+ return cached;
332
+ // convert route tokens back into a path pattern for path-to-regexp to compile
333
+ const { path } = toPathPattern(route.path, route.tokens.filter(token => token.kind !== 'static').map(token => token.value));
334
+ // create a matcher function for this route and cache it
335
+ const matcher = createMatch(path, {
336
+ decode: false,
337
+ });
338
+ Router.#matchers.set(route, matcher);
339
+ return matcher;
340
+ }
341
+ /**
342
+ * Rank token kinds so more specific segments win before broader ones
343
+ */
344
+ static #getTokenRank(token) {
345
+ if (!token)
346
+ return -1;
347
+ if (token.kind === 'static')
348
+ return 2;
349
+ if (token.kind === 'dynamic')
350
+ return 1;
351
+ return 0;
352
+ }
353
+ /**
354
+ * Compare two routes and prefer the one with the more specific segment pattern
355
+ */
356
+ static #compare(a, b) {
357
+ const length = Math.max(a.tokens.length, b.tokens.length);
358
+ for (let index = 0; index < length; index += 1) {
359
+ // prefer static over dynamic and dynamic over wildcard at the
360
+ // first segment position where the two routes differ
361
+ const diff = Router.#getTokenRank(a.tokens[index]) - Router.#getTokenRank(b.tokens[index]);
362
+ if (diff !== 0)
363
+ return diff;
364
+ }
365
+ // if the token kinds line up, reuse the old coarse score
366
+ if (a.score !== b.score)
367
+ return a.score - b.score;
368
+ // final stable tie-break for routes with the same pattern shape
369
+ // sort alphabetically by path string
370
+ return a.path < b.path ? 1 : a.path > b.path ? -1 : 0;
371
+ }
372
+ /**
373
+ * Find the best matching route from a candidate list using explicit specificity rules
374
+ */
375
+ static #pick(routes, segments, method) {
376
+ let best = null;
377
+ let bestParams = null;
378
+ for (const route of routes) {
379
+ // HEAD can reuse GET routes when HEAD is not registered explicitly
380
+ if (route.method !== method && !(method === 'HEAD' && route.method === 'GET')) {
381
+ continue;
382
+ }
383
+ // skip routes that do not fit this path. Only compare specificity
384
+ // across matched routes
385
+ const params = Router.#fit(route, segments);
386
+ if (!params)
387
+ continue;
388
+ // replace the winner only when this route is strictly more specific
389
+ if (!best || Router.#compare(route, best) > 0) {
390
+ best = route;
391
+ bestParams = params;
392
+ }
393
+ }
394
+ if (!best)
395
+ return null;
396
+ return { route: best, params: bestParams ?? {} };
397
+ }
398
+ /**
399
+ * Fit a route against path segments
400
+ */
401
+ static #fit(route, segments) {
402
+ if (route.wildcard) {
403
+ // wildcard routes only require the fixed prefix before the catch-all segment
404
+ if (segments.length < route.length - 1)
405
+ return null;
406
+ }
407
+ else if (route.length !== segments.length) {
408
+ return null;
409
+ }
410
+ // defer the actual param extraction to the cached path-to-regexp matcher so
411
+ // dynamic and wildcard params stay consistent with registration
412
+ const matched = Router.#getMatcher(route)(segments.length ? `/${segments.join('/')}` : '/');
413
+ if (!matched)
414
+ return null;
415
+ return matched.params;
416
+ }
417
+ }
@@ -0,0 +1,5 @@
1
+ export declare function useRouter(): {
2
+ go: (to: string, opts?: import("./router-context").Navigation.GoOptions | undefined) => Promise<string>;
3
+ prefetch: (path: string) => void;
4
+ isNavigating: boolean;
5
+ };
@@ -0,0 +1,5 @@
1
+ import { use } from 'react';
2
+ import { RouterContext } from './router-context';
3
+ export function useRouter() {
4
+ return use(RouterContext);
5
+ }
@@ -0,0 +1,6 @@
1
+ import { Cookies } from '../../utils/cookies';
2
+ /**
3
+ * Get the request cookies as a Cookies instance
4
+ * @returns a read-only Cookies instance containing the request cookies
5
+ */
6
+ export declare function cookies(): Readonly<ReturnType<typeof Cookies.parse>>;
@@ -0,0 +1,17 @@
1
+ import { Cookies } from '../../utils/cookies';
2
+ import { RequestContext } from '../env/request-context';
3
+ import { dynamic } from './dynamic';
4
+ /**
5
+ * Get the request cookies as a Cookies instance
6
+ * @returns a read-only Cookies instance containing the request cookies
7
+ */
8
+ export function cookies() {
9
+ dynamic();
10
+ const { req, cache } = RequestContext.use();
11
+ // use request cache if possible to avoid reparsing
12
+ if (cache.cookies)
13
+ return cache.cookies;
14
+ const parsed = Cookies.parse(req.headers.get('cookie'));
15
+ cache.cookies = parsed;
16
+ return parsed;
17
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Declaratively mark render below this call as request-time only
3
+ * @description in prerender mode this suspends forever so the nearest Suspense
4
+ * boundary renders its fallback into the static shell. In request mode this
5
+ * resolves immediately
6
+ * @returns void during normal requests or prerender not in ppr mode
7
+ * @throws if called in prerender mode (the desired effect)
8
+ */
9
+ export declare function dynamic(): void;
@@ -0,0 +1,22 @@
1
+ import { Logger } from '../../utils/logger';
2
+ import { RequestContext } from '../env/request-context';
3
+ const logger = new Logger();
4
+ const NEVER = new Promise(() => { });
5
+ /**
6
+ * Declaratively mark render below this call as request-time only
7
+ * @description in prerender mode this suspends forever so the nearest Suspense
8
+ * boundary renders its fallback into the static shell. In request mode this
9
+ * resolves immediately
10
+ * @returns void during normal requests or prerender not in ppr mode
11
+ * @throws if called in prerender mode (the desired effect)
12
+ */
13
+ export function dynamic() {
14
+ const { prerender } = RequestContext.use();
15
+ if (!prerender)
16
+ return;
17
+ if (prerender !== 'ppr') {
18
+ logger.warn('[dynamic]', "dynamic() was called but prerender mode is not 'ppr'. This means the component will be rendered at build time, which may not be what you intended");
19
+ return;
20
+ }
21
+ throw NEVER;
22
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Get the request headers as a read-only map
3
+ * @returns a read-only map of request headers
4
+ */
5
+ export declare function headers(): ReadonlyMap<string, string>;
@@ -0,0 +1,19 @@
1
+ import { RequestContext } from '../env/request-context';
2
+ import { dynamic } from './dynamic';
3
+ /**
4
+ * Get the request headers as a read-only map
5
+ * @returns a read-only map of request headers
6
+ */
7
+ export function headers() {
8
+ dynamic();
9
+ const { req, cache } = RequestContext.use();
10
+ // use request cache if possible to avoid reconstructing the map
11
+ if (cache.headers)
12
+ return cache.headers;
13
+ const map = new Map();
14
+ req.headers.forEach((value, key) => {
15
+ map.set(key, value);
16
+ });
17
+ cache.headers = map;
18
+ return map;
19
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Get the request url as a URL instance
3
+ * @returns a URL instance containing the request url
4
+ */
5
+ export declare function url(): URL;
@@ -0,0 +1,16 @@
1
+ import { RequestContext } from '../env/request-context';
2
+ import { dynamic } from './dynamic';
3
+ /**
4
+ * Get the request url as a URL instance
5
+ * @returns a URL instance containing the request url
6
+ */
7
+ export function url() {
8
+ dynamic();
9
+ const { req, cache } = RequestContext.use();
10
+ // use request cache if possible
11
+ if (cache.url)
12
+ return cache.url;
13
+ const parsed = new URL(req.url);
14
+ cache.url = parsed;
15
+ return parsed;
16
+ }
@@ -0,0 +1,4 @@
1
+ import type { HttpException } from '../../navigation/http-exception';
2
+ export default function Err({ error }: {
3
+ error: HttpException | Error;
4
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export default function Err({ error }) {
3
+ const title = 'status' in error ? `${error.status} - ${error.message}` : error.message;
4
+ return (_jsxs(_Fragment, { children: [
5
+ _jsx("meta", { name: "robots", content: "noindex,nofollow" }), _jsx("title", { children: title }), _jsx("h1", { children: title }), _jsx("p", { children: error.message }), error?.stack && _jsx("pre", { children: error.stack })] }));
6
+ }
@@ -0,0 +1,26 @@
1
+ import { Component } from 'react';
2
+ import type { BoundaryError } from '../../types';
3
+ export type Props = {
4
+ fallback: ((error: BoundaryError, reset: () => void) => React.ReactNode) | React.ReactNode;
5
+ onError?: (error: BoundaryError) => void;
6
+ children: React.ReactNode;
7
+ };
8
+ /**
9
+ * A component that catches synchronous errors in its child component tree and displays a fallback UI
10
+ * @param props - the props for the component
11
+ * @param props.fallback - the fallback UI to display when an error occurs, can be a function, React node or component
12
+ * @param props.onReset - a callback function to call when the error is reset
13
+ * @param props.children - the child components to render
14
+ * @returns Component
15
+ */
16
+ export declare class ErrorBoundary extends Component<Props, {
17
+ error: BoundaryError | null;
18
+ }> {
19
+ constructor(props: Props);
20
+ static getDerivedStateFromError(error: Error): {
21
+ error: Error;
22
+ };
23
+ componentDidCatch(error: Error): void;
24
+ reset(onReset?: () => void): void;
25
+ render(): import("react").ReactNode;
26
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+ import { Component } from 'react';
3
+ import { isKnownError } from '../env/utils';
4
+ /**
5
+ * A component that catches synchronous errors in its child component tree and displays a fallback UI
6
+ * @param props - the props for the component
7
+ * @param props.fallback - the fallback UI to display when an error occurs, can be a function, React node or component
8
+ * @param props.onReset - a callback function to call when the error is reset
9
+ * @param props.children - the child components to render
10
+ * @returns Component
11
+ */
12
+ export class ErrorBoundary extends Component {
13
+ constructor(props) {
14
+ super(props);
15
+ this.state = { error: null };
16
+ this.reset = this.reset.bind(this);
17
+ }
18
+ static getDerivedStateFromError(error) {
19
+ return { error };
20
+ }
21
+ componentDidCatch(error) {
22
+ this.props.onError?.(error);
23
+ }
24
+ reset(onReset) {
25
+ if (this.state.error)
26
+ this.setState({ error: null });
27
+ onReset?.();
28
+ }
29
+ render() {
30
+ const { error } = this.state;
31
+ if (!error)
32
+ return this.props.children;
33
+ // possible framework control-flow error, re-throw to be caught
34
+ // by appropriate HttpException or Redirect boundaries
35
+ if (isKnownError(error))
36
+ throw error;
37
+ return typeof this.props.fallback === 'function'
38
+ ? this.props.fallback(error, this.reset)
39
+ : this.props.fallback;
40
+ }
41
+ }
@@ -0,0 +1,6 @@
1
+ export { HttpException, abort, isHttpException, } from './internal/navigation/http-exception';
2
+ export { HttpExceptionBoundary } from './internal/navigation/http-exception-boundary';
3
+ export { Link } from './internal/navigation/link';
4
+ export { Redirect, isRedirect, redirect } from './internal/navigation/redirect';
5
+ export { RedirectBoundary } from './internal/navigation/redirect-boundary';
6
+ export { useSearchParams } from './internal/navigation/use-search-params';
@@ -0,0 +1,6 @@
1
+ export { HttpException, abort, isHttpException, } from './internal/navigation/http-exception';
2
+ export { HttpExceptionBoundary } from './internal/navigation/http-exception-boundary';
3
+ export { Link } from './internal/navigation/link';
4
+ export { Redirect, isRedirect, redirect } from './internal/navigation/redirect';
5
+ export { RedirectBoundary } from './internal/navigation/redirect-boundary';
6
+ export { useSearchParams } from './internal/navigation/use-search-params';
@@ -0,0 +1 @@
1
+ export { Prerender } from './internal/prerender';
@@ -0,0 +1 @@
1
+ export { Prerender } from './internal/prerender';
@@ -0,0 +1,4 @@
1
+ export { createRouter } from './internal/router/create-router';
2
+ export { Router } from './internal/router/router';
3
+ export { RouterProvider } from './internal/router/router-provider';
4
+ export { useRouter } from './internal/router/use-router';
package/dist/router.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createRouter } from './internal/router/create-router';
2
+ export { Router } from './internal/router/router';
3
+ export { RouterProvider } from './internal/router/router-provider';
4
+ export { useRouter } from './internal/router/use-router';
@@ -0,0 +1,4 @@
1
+ export { cookies } from './internal/server/cookies';
2
+ export { dynamic } from './internal/server/dynamic';
3
+ export { headers } from './internal/server/headers';
4
+ export { url } from './internal/server/url';
package/dist/server.js ADDED
@@ -0,0 +1,4 @@
1
+ export { cookies } from './internal/server/cookies';
2
+ export { dynamic } from './internal/server/dynamic';
3
+ export { headers } from './internal/server/headers';
4
+ export { url } from './internal/server/url';