@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,95 @@
1
+ import { Router } from './router';
2
+ function callEndpoint(fn, req) {
3
+ // endpoint modules may export either a zero-arg handler or one that expects the request
4
+ if (fn.length === 0)
5
+ return fn();
6
+ return fn(req);
7
+ }
8
+ function createHandlerGroups(manifest) {
9
+ return Object.values(manifest)
10
+ .flat()
11
+ .reduce((acc, entry) => {
12
+ // group by method + path so page and endpoint pairs can share one GET handler
13
+ const key = `${entry.method}/${entry.__path}`;
14
+ const existing = acc.get(key);
15
+ if (!existing) {
16
+ acc.set(key, entry);
17
+ }
18
+ else if (Array.isArray(existing)) {
19
+ existing.push(entry);
20
+ }
21
+ else {
22
+ acc.set(key, [existing, entry]);
23
+ }
24
+ return acc;
25
+ }, new Map());
26
+ }
27
+ function resolveMiddlewares(entry, importMap) {
28
+ // the manifest keeps middleware paths for serialisable route data, whilst
29
+ // the import map carries the actual runtime middleware functions
30
+ const middlewares = importMap[entry.__id]?.middlewares ?? [];
31
+ return middlewares.filter((value) => value !== null);
32
+ }
33
+ function mergeMiddlewares(left, right) {
34
+ const merged = [];
35
+ // unified page plus endpoint routes need one middleware chain, preserving the
36
+ // declared order but dropping duplicate functions shared by both sides
37
+ for (const middleware of [...(left ?? []), ...(right ?? [])]) {
38
+ if (!middleware)
39
+ continue;
40
+ if (merged.includes(middleware))
41
+ continue;
42
+ merged.push(middleware);
43
+ }
44
+ return merged;
45
+ }
46
+ /**
47
+ * Create the application router from the generated manifest and import map
48
+ */
49
+ export function createRouter(config, manifest, importMap, rsc) {
50
+ const router = new Router({
51
+ trailingSlash: config.trailingSlash,
52
+ });
53
+ // static assets stay outside route middleware conventions and are registered once
54
+ router.add('/assets/*', 'GET', Router.static(config));
55
+ for (const [, group] of createHandlerGroups(manifest)) {
56
+ if (!Array.isArray(group)) {
57
+ if ('paths' in group) {
58
+ // plain page routes always hand off to the RSC request pipeline
59
+ router.add(group.__path, group.method.toUpperCase(), req => rsc(req), group.__params, resolveMiddlewares(group, importMap));
60
+ continue;
61
+ }
62
+ const endpoint = importMap[group.__id]?.endpoint;
63
+ if (!endpoint) {
64
+ throw new Error(`Missing endpoint handler for ${group.__id}`);
65
+ }
66
+ // standalone endpoints resolve directly from the import map and bypass RSC
67
+ router.add(group.__path, group.method.toUpperCase(), req => callEndpoint(endpoint, req), group.__params, resolveMiddlewares(group, importMap));
68
+ continue;
69
+ }
70
+ if (group.length > 2)
71
+ throw new Error('Unexpected route group length');
72
+ const page = group.find((entry) => 'paths' in entry);
73
+ const endpointEntry = group.find((entry) => !('paths' in entry));
74
+ if (!page) {
75
+ throw new Error('Unified route group missing page entry');
76
+ }
77
+ const endpoint = endpointEntry ? importMap[endpointEntry.__id]?.endpoint : undefined;
78
+ // page plus endpoint pairs share one GET registration that chooses between
79
+ // document or RSC rendering and the endpoint implementation at request time
80
+ const middlewares = mergeMiddlewares(importMap[page.__id]?.middlewares, endpointEntry ? importMap[endpointEntry.__id]?.middlewares : undefined);
81
+ router.add(page.__path, 'GET', async req => {
82
+ const accept = req.headers.get('accept') ?? '';
83
+ if (accept.includes('text/html') || accept.includes('text/x-component')) {
84
+ return rsc(req);
85
+ }
86
+ if (!endpoint) {
87
+ throw new Error(`Missing unified endpoint handler for ${page.__path}`);
88
+ }
89
+ return callEndpoint(endpoint, req);
90
+ }, page.__params, middlewares);
91
+ }
92
+ // the router already stores the thrown error on the request metadata before
93
+ // invoking the error hook, so the RSC pipeline can read it back from req
94
+ return router.error((_err, req) => rsc(req));
95
+ }
@@ -0,0 +1,8 @@
1
+ export type PathPattern = {
2
+ path: string;
3
+ wildcardNames: Set<string>;
4
+ };
5
+ export declare function toPathPattern(route: string, paramNames?: string[]): {
6
+ path: string;
7
+ wildcardNames: Set<string>;
8
+ };
@@ -0,0 +1,31 @@
1
+ function escapePathSegment(value) {
2
+ return value.replace(/[\\.+*?^${}()[\]|!:]/g, '\\$&');
3
+ }
4
+ export function toPathPattern(route, paramNames = []) {
5
+ if (route === '/') {
6
+ return { path: route, wildcardNames: new Set() };
7
+ }
8
+ let paramIndex = 0;
9
+ let wildcardIndex = 0;
10
+ const wildcardNames = new Set();
11
+ const path = route
12
+ .split('/')
13
+ .filter(Boolean)
14
+ .map(segment => {
15
+ if (segment.startsWith(':')) {
16
+ paramIndex += 1;
17
+ return `/${segment}`;
18
+ }
19
+ if (segment === '*') {
20
+ const value = paramNames[paramIndex];
21
+ const name = value && value !== '*' ? value : `wildcard${wildcardIndex}`;
22
+ paramIndex += 1;
23
+ wildcardIndex += 1;
24
+ wildcardNames.add(name);
25
+ return `/*${name}`;
26
+ }
27
+ return `/${escapePathSegment(segment)}`;
28
+ })
29
+ .join('');
30
+ return { path: path || '/', wildcardNames };
31
+ }
@@ -0,0 +1,47 @@
1
+ export declare namespace Prefetcher {
2
+ type Entry = {
3
+ promise: Promise<Response>;
4
+ timeoutId: ReturnType<typeof setTimeout>;
5
+ };
6
+ }
7
+ export declare class Prefetcher {
8
+ #private;
9
+ ttl: number;
10
+ maxSize: number;
11
+ constructor({ ttl, maxSize }?: {
12
+ ttl?: number;
13
+ maxSize?: number;
14
+ });
15
+ /**
16
+ * Converts a url path to a cache key by normalising it
17
+ * against a base url
18
+ */
19
+ static key(path: string, base: string): string;
20
+ /**
21
+ * Evicts the oldest entry from the cache
22
+ */
23
+ evict(): void;
24
+ /**
25
+ * Returns a boolean indicating whether a cached response exists for the given path
26
+ */
27
+ has(path: string): boolean;
28
+ /**
29
+ * Retrieves a fresh response promise for the given path if it exists
30
+ * by cloning the cached response so each consumer gets an unread stream
31
+ */
32
+ get(path: string): Promise<Response> | undefined;
33
+ /**
34
+ * Caches a response promise for the given path with a timeout to automatically
35
+ * clear the cache after a certain period (TTL_MS)
36
+ */
37
+ set(path: string, promise: Promise<Response>): void;
38
+ /**
39
+ * Removes the cached response for the given path and clears the associated timeout
40
+ */
41
+ remove(path: string): void;
42
+ /**
43
+ * Clears the entire cache and all associated timeouts
44
+ */
45
+ clear(): void;
46
+ [Symbol.dispose](): void;
47
+ }
@@ -0,0 +1,90 @@
1
+ export class Prefetcher {
2
+ #cache = new Map();
3
+ ttl = 60_000;
4
+ maxSize = 32;
5
+ constructor({ ttl = 60_000, maxSize = 32 } = {}) {
6
+ this.ttl = ttl;
7
+ this.maxSize = maxSize;
8
+ }
9
+ /**
10
+ * Converts a url path to a cache key by normalising it
11
+ * against a base url
12
+ */
13
+ static key(path, base) {
14
+ const url = new URL(path, base);
15
+ return url.pathname + url.search + url.hash;
16
+ }
17
+ /**
18
+ * Evicts the oldest entry from the cache
19
+ */
20
+ evict() {
21
+ if (this.#cache.size === 0)
22
+ return;
23
+ const candidate = this.#cache.entries().next().value;
24
+ if (!candidate)
25
+ return;
26
+ const [key, entry] = candidate;
27
+ clearTimeout(entry.timeoutId);
28
+ this.#cache.delete(key);
29
+ }
30
+ /**
31
+ * Returns a boolean indicating whether a cached response exists for the given path
32
+ */
33
+ has(path) {
34
+ return this.#cache.has(path);
35
+ }
36
+ /**
37
+ * Retrieves a fresh response promise for the given path if it exists
38
+ * by cloning the cached response so each consumer gets an unread stream
39
+ */
40
+ get(path) {
41
+ const promise = this.#cache.get(path)?.promise;
42
+ if (!promise)
43
+ return;
44
+ return promise.then(res => res.clone());
45
+ }
46
+ /**
47
+ * Caches a response promise for the given path with a timeout to automatically
48
+ * clear the cache after a certain period (TTL_MS)
49
+ */
50
+ set(path, promise) {
51
+ const existing = this.#cache.get(path);
52
+ if (existing) {
53
+ clearTimeout(existing.timeoutId);
54
+ this.#cache.delete(path);
55
+ }
56
+ else if (this.#cache.size >= this.maxSize) {
57
+ this.evict();
58
+ }
59
+ const timeoutId = setTimeout(() => {
60
+ const cached = this.#cache.get(path);
61
+ if (!cached)
62
+ return;
63
+ clearTimeout(cached.timeoutId);
64
+ this.#cache.delete(path);
65
+ }, this.ttl);
66
+ this.#cache.set(path, { promise, timeoutId });
67
+ }
68
+ /**
69
+ * Removes the cached response for the given path and clears the associated timeout
70
+ */
71
+ remove(path) {
72
+ const cached = this.#cache.get(path);
73
+ if (!cached)
74
+ return;
75
+ clearTimeout(cached.timeoutId);
76
+ this.#cache.delete(path);
77
+ }
78
+ /**
79
+ * Clears the entire cache and all associated timeouts
80
+ */
81
+ clear() {
82
+ for (const entry of this.#cache.values()) {
83
+ clearTimeout(entry.timeoutId);
84
+ }
85
+ this.#cache.clear();
86
+ }
87
+ [Symbol.dispose]() {
88
+ this.clear();
89
+ }
90
+ }
@@ -0,0 +1,174 @@
1
+ import type { ImportMap, Manifest, ManifestEntry, Primitive, View } from '../../types';
2
+ import type { Router } from './router';
3
+ import { Metadata } from '../metadata';
4
+ import { HttpException } from '../navigation/http-exception';
5
+ export declare namespace Resolver {
6
+ type ReconciledMatch = ReturnType<Resolver['reconcile']>;
7
+ type CachedEnhancedMatch = Omit<EnhancedMatch, 'params' | 'error'>;
8
+ type EnhancedMatch = ReconciledMatch & {
9
+ ui: {
10
+ layouts: (View<{
11
+ children?: React.ReactNode;
12
+ params?: Router.Params;
13
+ }> | null)[];
14
+ Page: View<{
15
+ children?: React.ReactNode;
16
+ params?: Router.Params;
17
+ }> | null;
18
+ '401s': (View<{
19
+ children?: React.ReactNode;
20
+ error?: HttpException;
21
+ }> | null)[];
22
+ '403s': (View<{
23
+ children?: React.ReactNode;
24
+ error?: HttpException;
25
+ }> | null)[];
26
+ '404s': (View<{
27
+ children?: React.ReactNode;
28
+ error?: HttpException;
29
+ }> | null)[];
30
+ '500s': (View<{
31
+ children?: React.ReactNode;
32
+ error?: HttpException;
33
+ }> | null)[];
34
+ loaders: (View<{
35
+ children?: React.ReactNode;
36
+ }> | null)[];
37
+ };
38
+ error?: HttpException | Error;
39
+ endpoint?: (req?: Request & {
40
+ params?: Router.Params;
41
+ }) => unknown;
42
+ metadata?: (input: Metadata.Input<Router.Params>) => Metadata.Task[];
43
+ };
44
+ }
45
+ /**
46
+ * Resolve router matches against the application manifest and import map
47
+ */
48
+ export declare class Resolver {
49
+ #private;
50
+ /**
51
+ * @see {@link Manifest} for the structure of the manifest
52
+ * @see {@link ImportMap} for the structure of the import map
53
+ */
54
+ constructor(manifest: Manifest, importMap: ImportMap);
55
+ /**
56
+ * Narrow down a route entry to a page entry if it exists
57
+ */
58
+ static narrow(entry?: ManifestEntry | ManifestEntry[]): import("../..").Segment | null;
59
+ /**
60
+ * Get the status code for a matched route that may or may not have errored
61
+ */
62
+ static getMatchStatusCode(match: Resolver.ReconciledMatch | Resolver.EnhancedMatch | null): 200 | HttpException.StatusCode;
63
+ /**
64
+ * Reconcile a router match against a manifest entry
65
+ */
66
+ reconcile(path: string, match: Router.Match | null, error?: Error): {
67
+ params: Router.Params;
68
+ error: Error | undefined;
69
+ __id: string;
70
+ __path: string;
71
+ __params: string[];
72
+ __kind: "$P";
73
+ __depth: number;
74
+ method: "get";
75
+ paths: {
76
+ layouts: (string | null)[];
77
+ '401s': (string | null)[];
78
+ '403s': (string | null)[];
79
+ '404s': (string | null)[];
80
+ '500s': (string | null)[];
81
+ loaders: (string | null)[];
82
+ middlewares: (string | null)[];
83
+ page?: string | null | undefined;
84
+ };
85
+ prerender: "full" | "ppr" | false;
86
+ dynamic: boolean;
87
+ wildcard: boolean;
88
+ } | {
89
+ params: {};
90
+ error: HttpException;
91
+ __id: string;
92
+ __path: string;
93
+ __params: string[];
94
+ __kind: "$P";
95
+ __depth: number;
96
+ method: "get";
97
+ paths: {
98
+ layouts: (string | null)[];
99
+ '401s': (string | null)[];
100
+ '403s': (string | null)[];
101
+ '404s': (string | null)[];
102
+ '500s': (string | null)[];
103
+ loaders: (string | null)[];
104
+ middlewares: (string | null)[];
105
+ page?: string | null | undefined;
106
+ };
107
+ prerender: "full" | "ppr" | false;
108
+ dynamic: boolean;
109
+ wildcard: boolean;
110
+ } | null;
111
+ /**
112
+ * Enhance a matched route with its associated components
113
+ */
114
+ enhance(match: Resolver.ReconciledMatch | null): {
115
+ ui: {
116
+ layouts: (View<{
117
+ children?: import("react").ReactNode;
118
+ params?: Router.Params | undefined;
119
+ }> | null)[];
120
+ Page: View<{
121
+ children?: import("react").ReactNode;
122
+ params?: Router.Params | undefined;
123
+ }> | null;
124
+ '401s': (View<{
125
+ children?: import("react").ReactNode;
126
+ error?: HttpException | undefined;
127
+ }> | null)[];
128
+ '403s': (View<{
129
+ children?: import("react").ReactNode;
130
+ error?: HttpException | undefined;
131
+ }> | null)[];
132
+ '404s': (View<{
133
+ children?: import("react").ReactNode;
134
+ error?: HttpException | undefined;
135
+ }> | null)[];
136
+ '500s': (View<{
137
+ children?: import("react").ReactNode;
138
+ error?: HttpException | undefined;
139
+ }> | null)[];
140
+ loaders: (View<{
141
+ children?: import("react").ReactNode;
142
+ }> | null)[];
143
+ };
144
+ endpoint?: ((req?: (Request & {
145
+ params?: Router.Params | undefined;
146
+ }) | undefined) => unknown) | undefined;
147
+ metadata?: ((input: Metadata.Input<Router.Params, Error>) => Metadata.Task[]) | undefined;
148
+ params: Router.Params | {};
149
+ 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
+ } | null;
170
+ /**
171
+ * Find the closest ancestor entry for a given path and property
172
+ */
173
+ closest(path: string, property: string, value?: Omit<Primitive, 'undefined'>): import("../..").Segment | null;
174
+ }