@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.
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +219 -0
- package/dist/error-boundary.d.ts +1 -0
- package/dist/error-boundary.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +235 -0
- package/dist/internal/build.d.ts +104 -0
- package/dist/internal/build.js +633 -0
- package/dist/internal/codegen/config.d.ts +5 -0
- package/dist/internal/codegen/config.js +19 -0
- package/dist/internal/codegen/environments.d.ts +12 -0
- package/dist/internal/codegen/environments.js +42 -0
- package/dist/internal/codegen/manifest.d.ts +5 -0
- package/dist/internal/codegen/manifest.js +15 -0
- package/dist/internal/codegen/maps.d.ts +5 -0
- package/dist/internal/codegen/maps.js +75 -0
- package/dist/internal/codegen/utils.d.ts +1 -0
- package/dist/internal/codegen/utils.js +2 -0
- package/dist/internal/env/browser.d.ts +4 -0
- package/dist/internal/env/browser.js +58 -0
- package/dist/internal/env/request-context.d.ts +19 -0
- package/dist/internal/env/request-context.js +2 -0
- package/dist/internal/env/rsc.d.ts +39 -0
- package/dist/internal/env/rsc.js +368 -0
- package/dist/internal/env/ssr.d.ts +42 -0
- package/dist/internal/env/ssr.js +149 -0
- package/dist/internal/env/utils.d.ts +2 -0
- package/dist/internal/env/utils.js +28 -0
- package/dist/internal/metadata.d.ts +81 -0
- package/dist/internal/metadata.js +185 -0
- package/dist/internal/navigation/http-exception-boundary.d.ts +12 -0
- package/dist/internal/navigation/http-exception-boundary.js +48 -0
- package/dist/internal/navigation/http-exception.d.ts +33 -0
- package/dist/internal/navigation/http-exception.js +45 -0
- package/dist/internal/navigation/link.d.ts +13 -0
- package/dist/internal/navigation/link.js +63 -0
- package/dist/internal/navigation/redirect-boundary.d.ts +12 -0
- package/dist/internal/navigation/redirect-boundary.js +39 -0
- package/dist/internal/navigation/redirect.d.ts +21 -0
- package/dist/internal/navigation/redirect.js +63 -0
- package/dist/internal/navigation/use-search-params.d.ts +1 -0
- package/dist/internal/navigation/use-search-params.js +13 -0
- package/dist/internal/prerender.d.ts +151 -0
- package/dist/internal/prerender.js +422 -0
- package/dist/internal/render/head.d.ts +4 -0
- package/dist/internal/render/head.js +38 -0
- package/dist/internal/render/tree.d.ts +47 -0
- package/dist/internal/render/tree.js +108 -0
- package/dist/internal/router/create-router.d.ts +6 -0
- package/dist/internal/router/create-router.js +95 -0
- package/dist/internal/router/pattern.d.ts +8 -0
- package/dist/internal/router/pattern.js +31 -0
- package/dist/internal/router/prefetcher.d.ts +47 -0
- package/dist/internal/router/prefetcher.js +90 -0
- package/dist/internal/router/resolver.d.ts +174 -0
- package/dist/internal/router/resolver.js +356 -0
- package/dist/internal/router/router-context.d.ts +11 -0
- package/dist/internal/router/router-context.js +7 -0
- package/dist/internal/router/router-provider.d.ts +6 -0
- package/dist/internal/router/router-provider.js +131 -0
- package/dist/internal/router/router.d.ts +79 -0
- package/dist/internal/router/router.js +417 -0
- package/dist/internal/router/use-router.d.ts +5 -0
- package/dist/internal/router/use-router.js +5 -0
- package/dist/internal/server/cookies.d.ts +6 -0
- package/dist/internal/server/cookies.js +17 -0
- package/dist/internal/server/dynamic.d.ts +9 -0
- package/dist/internal/server/dynamic.js +22 -0
- package/dist/internal/server/headers.d.ts +5 -0
- package/dist/internal/server/headers.js +19 -0
- package/dist/internal/server/url.d.ts +5 -0
- package/dist/internal/server/url.js +16 -0
- package/dist/internal/ui/defaults/error.d.ts +4 -0
- package/dist/internal/ui/defaults/error.js +6 -0
- package/dist/internal/ui/error-boundary.d.ts +26 -0
- package/dist/internal/ui/error-boundary.js +41 -0
- package/dist/navigation.d.ts +6 -0
- package/dist/navigation.js +6 -0
- package/dist/prerender.d.ts +1 -0
- package/dist/prerender.js +1 -0
- package/dist/router.d.ts +4 -0
- package/dist/router.js +4 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +4 -0
- package/dist/solas.d.ts +32 -0
- package/dist/solas.js +125 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.js +1 -0
- package/dist/utils/compress.d.ts +11 -0
- package/dist/utils/compress.js +76 -0
- package/dist/utils/context.d.ts +6 -0
- package/dist/utils/context.js +25 -0
- package/dist/utils/cookies.d.ts +3 -0
- package/dist/utils/cookies.js +35 -0
- package/dist/utils/export-reader.d.ts +29 -0
- package/dist/utils/export-reader.js +117 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.js +72 -0
- package/dist/utils/logger.d.ts +52 -0
- package/dist/utils/logger.js +105 -0
- package/dist/utils/time.d.ts +4 -0
- package/dist/utils/time.js +29 -0
- package/package.json +111 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { lazy } from 'react';
|
|
2
|
+
import { Logger } from '../../utils/logger';
|
|
3
|
+
import { Build } from '../build';
|
|
4
|
+
import { Metadata } from '../metadata';
|
|
5
|
+
import { HttpException, isHttpException } from '../navigation/http-exception';
|
|
6
|
+
const logger = new Logger();
|
|
7
|
+
const IS_DEV = import.meta.env.DEV;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve router matches against the application manifest and import map
|
|
10
|
+
*/
|
|
11
|
+
export class Resolver {
|
|
12
|
+
/**
|
|
13
|
+
* Cache of enhanced matches
|
|
14
|
+
*/
|
|
15
|
+
static #enhancedMatchCache = new Map();
|
|
16
|
+
/**
|
|
17
|
+
* Cache of loaded modules from dynamic imports
|
|
18
|
+
*/
|
|
19
|
+
static #moduleCache = new WeakMap();
|
|
20
|
+
#manifest = {};
|
|
21
|
+
#importMap = {};
|
|
22
|
+
/**
|
|
23
|
+
* @see {@link Manifest} for the structure of the manifest
|
|
24
|
+
* @see {@link ImportMap} for the structure of the import map
|
|
25
|
+
*/
|
|
26
|
+
constructor(manifest, importMap) {
|
|
27
|
+
this.#manifest = manifest;
|
|
28
|
+
this.#importMap = importMap;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Narrow down a route entry to a page entry if it exists
|
|
32
|
+
*/
|
|
33
|
+
static narrow(entry) {
|
|
34
|
+
if (Array.isArray(entry)) {
|
|
35
|
+
return entry.find(e => e.__kind === Build.EntryKind.PAGE) || null;
|
|
36
|
+
}
|
|
37
|
+
return entry?.__kind === Build.EntryKind.PAGE ? entry : null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get the status code for a matched route that may or may not have errored
|
|
41
|
+
*/
|
|
42
|
+
static getMatchStatusCode(match) {
|
|
43
|
+
if (!match)
|
|
44
|
+
return 404;
|
|
45
|
+
if ('error' in match) {
|
|
46
|
+
return match.error instanceof HttpException ? match.error.status : 500;
|
|
47
|
+
}
|
|
48
|
+
return 200;
|
|
49
|
+
}
|
|
50
|
+
static #withRequestState(cached, match) {
|
|
51
|
+
// the cached match only stores route structure, while params and errors
|
|
52
|
+
// still belong to this request so merge them back in here
|
|
53
|
+
return {
|
|
54
|
+
...cached,
|
|
55
|
+
params: match.params,
|
|
56
|
+
error: match.error,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Load and cache a module from a dynamic import
|
|
61
|
+
*/
|
|
62
|
+
static #load(loader) {
|
|
63
|
+
if (IS_DEV) {
|
|
64
|
+
// in dev always call the loader directly so hot updates are not hidden
|
|
65
|
+
// behind the prod cache
|
|
66
|
+
return {
|
|
67
|
+
promise: loader(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
let entry = Resolver.#moduleCache.get(loader);
|
|
71
|
+
if (entry)
|
|
72
|
+
return entry;
|
|
73
|
+
// cache the in-flight import so repeated lookups share one load
|
|
74
|
+
const promise = loader()
|
|
75
|
+
.then(mod => {
|
|
76
|
+
const entry = Resolver.#moduleCache.get(loader);
|
|
77
|
+
if (entry)
|
|
78
|
+
entry.module = mod;
|
|
79
|
+
return mod;
|
|
80
|
+
})
|
|
81
|
+
.catch(err => {
|
|
82
|
+
Resolver.#moduleCache.delete(loader);
|
|
83
|
+
throw err;
|
|
84
|
+
});
|
|
85
|
+
entry = { promise };
|
|
86
|
+
Resolver.#moduleCache.set(loader, entry);
|
|
87
|
+
return entry;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Lazily load and cache a component from a dynamic import
|
|
91
|
+
*/
|
|
92
|
+
static #view(loader) {
|
|
93
|
+
const entry = Resolver.#load(loader);
|
|
94
|
+
logger.debug('[#view]', loader.toString().slice(0, 60), entry.module ? 'SYNC' : 'LAZY');
|
|
95
|
+
if (entry.module?.default) {
|
|
96
|
+
// if the module already loaded, return the component directly
|
|
97
|
+
entry.Component = entry.module.default;
|
|
98
|
+
return entry.Component;
|
|
99
|
+
}
|
|
100
|
+
// if we already created a lazy wrapper for this module
|
|
101
|
+
// reuse it
|
|
102
|
+
if (entry.Component)
|
|
103
|
+
return entry.Component;
|
|
104
|
+
// otherwise create the lazy wrapper once and keep it on the cache entry
|
|
105
|
+
const Component = lazy(() => entry.promise.then(mod => ({ default: mod.default })));
|
|
106
|
+
entry.Component = Component;
|
|
107
|
+
return entry.Component;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Reconcile a router match against a manifest entry
|
|
111
|
+
*/
|
|
112
|
+
reconcile(path, match, error) {
|
|
113
|
+
if (match) {
|
|
114
|
+
const entry = Resolver.narrow(this.#manifest[match.route.path]);
|
|
115
|
+
if (entry) {
|
|
116
|
+
// normal case, the router matched a page route so just attach request state
|
|
117
|
+
return {
|
|
118
|
+
...entry,
|
|
119
|
+
params: match.params,
|
|
120
|
+
error,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// if nothing matched directly, walk back up the path
|
|
125
|
+
// and look for the nearest user 404 boundary
|
|
126
|
+
const entry = this.closest(path, 'paths.404s');
|
|
127
|
+
if (entry) {
|
|
128
|
+
// reuse that route entry but force it into a 404 state
|
|
129
|
+
return {
|
|
130
|
+
...entry,
|
|
131
|
+
params: {},
|
|
132
|
+
error: isHttpException(error) && error.status === 404
|
|
133
|
+
? error
|
|
134
|
+
: new HttpException(404, 'Not found'),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Enhance a matched route with its associated components
|
|
141
|
+
*/
|
|
142
|
+
enhance(match) {
|
|
143
|
+
if (!match)
|
|
144
|
+
return null;
|
|
145
|
+
const { __id } = match;
|
|
146
|
+
// if in dev skip the cache to always get the latest changes
|
|
147
|
+
// and not break hmr
|
|
148
|
+
const cached = IS_DEV ? undefined : Resolver.#enhancedMatchCache.get(__id);
|
|
149
|
+
if (cached) {
|
|
150
|
+
logger.debug('[enhance]', __id, 'CACHED');
|
|
151
|
+
// cached ui can be reused, but params and errors still come from
|
|
152
|
+
// this request
|
|
153
|
+
return Resolver.#withRequestState(cached, match);
|
|
154
|
+
}
|
|
155
|
+
const entry = this.#importMap[__id];
|
|
156
|
+
if (!entry)
|
|
157
|
+
return null;
|
|
158
|
+
const { params, error, ...rest } = match;
|
|
159
|
+
const enhanced = {
|
|
160
|
+
ui: {
|
|
161
|
+
layouts: [],
|
|
162
|
+
Page: null,
|
|
163
|
+
'401s': [],
|
|
164
|
+
'403s': [],
|
|
165
|
+
'404s': [],
|
|
166
|
+
'500s': [],
|
|
167
|
+
loaders: [],
|
|
168
|
+
},
|
|
169
|
+
...rest,
|
|
170
|
+
};
|
|
171
|
+
// build the renderable ui shape from the import map, with a static shell
|
|
172
|
+
// and dynamic imports for everything else
|
|
173
|
+
if (entry.shell) {
|
|
174
|
+
enhanced.ui.layouts = [
|
|
175
|
+
entry.shell.default,
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
if (entry.layouts?.length) {
|
|
179
|
+
// layouts are stored after the shell and can each load lazily
|
|
180
|
+
const dynamicLayouts = entry.layouts.map(l => l
|
|
181
|
+
? Resolver.#view(l)
|
|
182
|
+
: null);
|
|
183
|
+
enhanced.ui.layouts = [...enhanced.ui.layouts, ...dynamicLayouts];
|
|
184
|
+
}
|
|
185
|
+
if (entry.page) {
|
|
186
|
+
// the page is the leaf view for this route
|
|
187
|
+
enhanced.ui.Page = Resolver.#view(entry.page);
|
|
188
|
+
}
|
|
189
|
+
if (entry['401s']?.length) {
|
|
190
|
+
enhanced.ui['401s'] = entry['401s'].map(e => e
|
|
191
|
+
? Resolver.#view(e)
|
|
192
|
+
: null);
|
|
193
|
+
}
|
|
194
|
+
if (entry['403s']?.length) {
|
|
195
|
+
enhanced.ui['403s'] = entry['403s'].map(e => e
|
|
196
|
+
? Resolver.#view(e)
|
|
197
|
+
: null);
|
|
198
|
+
}
|
|
199
|
+
// load 404 boundaries
|
|
200
|
+
if (entry['404s']?.length) {
|
|
201
|
+
enhanced.ui['404s'] = entry['404s'].map(e => e
|
|
202
|
+
? Resolver.#view(e)
|
|
203
|
+
: null);
|
|
204
|
+
}
|
|
205
|
+
if (entry['500s']?.length) {
|
|
206
|
+
enhanced.ui['500s'] = entry['500s'].map(e => e
|
|
207
|
+
? Resolver.#view(e)
|
|
208
|
+
: null);
|
|
209
|
+
}
|
|
210
|
+
// loading components are per route level they are not inherited like layouts
|
|
211
|
+
// or boundaries
|
|
212
|
+
if (entry.loaders?.length) {
|
|
213
|
+
enhanced.ui.loaders = entry.loaders.map(l => l
|
|
214
|
+
? Resolver.#view(l)
|
|
215
|
+
: null);
|
|
216
|
+
}
|
|
217
|
+
if (entry.endpoint)
|
|
218
|
+
enhanced.endpoint = entry.endpoint;
|
|
219
|
+
// collect metadata loaders once per route so they can turn into request
|
|
220
|
+
// specific tasks later when params and errors are known
|
|
221
|
+
const metadataSources = [];
|
|
222
|
+
if (entry.shell) {
|
|
223
|
+
metadataSources.push({
|
|
224
|
+
priority: Metadata.PRIORITY[Build.EntryKind.SHELL],
|
|
225
|
+
load: () => Promise.resolve(entry.shell?.metadata),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (entry.layouts?.length) {
|
|
229
|
+
for (const layout of entry.layouts) {
|
|
230
|
+
if (!layout)
|
|
231
|
+
continue;
|
|
232
|
+
const loaded = Resolver.#load(layout);
|
|
233
|
+
metadataSources.push({
|
|
234
|
+
priority: Metadata.PRIORITY[Build.EntryKind.LAYOUT],
|
|
235
|
+
load: () => loaded.module
|
|
236
|
+
? Promise.resolve(loaded.module.metadata)
|
|
237
|
+
: loaded.promise.then(module => module.metadata),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (entry.page) {
|
|
242
|
+
const loaded = Resolver.#load(entry.page);
|
|
243
|
+
metadataSources.push({
|
|
244
|
+
priority: Metadata.PRIORITY[Build.EntryKind.PAGE],
|
|
245
|
+
load: () => loaded.module
|
|
246
|
+
? Promise.resolve(loaded.module.metadata)
|
|
247
|
+
: loaded.promise.then(module => module.metadata),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (entry['401s']?.length) {
|
|
251
|
+
for (const errLoader of entry['401s']) {
|
|
252
|
+
if (!errLoader)
|
|
253
|
+
continue;
|
|
254
|
+
const loaded = Resolver.#load(errLoader);
|
|
255
|
+
metadataSources.push({
|
|
256
|
+
priority: Metadata.PRIORITY[Build.EntryKind['401']],
|
|
257
|
+
when: 'error',
|
|
258
|
+
status: 401,
|
|
259
|
+
load: () => loaded.module
|
|
260
|
+
? Promise.resolve(loaded.module.metadata)
|
|
261
|
+
: loaded.promise.then(module => module.metadata),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (entry['403s']?.length) {
|
|
266
|
+
for (const errLoader of entry['403s']) {
|
|
267
|
+
if (!errLoader)
|
|
268
|
+
continue;
|
|
269
|
+
const loaded = Resolver.#load(errLoader);
|
|
270
|
+
metadataSources.push({
|
|
271
|
+
priority: Metadata.PRIORITY[Build.EntryKind['403']],
|
|
272
|
+
when: 'error',
|
|
273
|
+
status: 403,
|
|
274
|
+
load: () => loaded.module
|
|
275
|
+
? Promise.resolve(loaded.module.metadata)
|
|
276
|
+
: loaded.promise.then(module => module.metadata),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (entry['404s']?.length) {
|
|
281
|
+
for (const errLoader of entry['404s']) {
|
|
282
|
+
if (!errLoader)
|
|
283
|
+
continue;
|
|
284
|
+
const loaded = Resolver.#load(errLoader);
|
|
285
|
+
metadataSources.push({
|
|
286
|
+
priority: Metadata.PRIORITY[Build.EntryKind['404']],
|
|
287
|
+
when: 'error',
|
|
288
|
+
status: 404,
|
|
289
|
+
load: () => loaded.module
|
|
290
|
+
? Promise.resolve(loaded.module.metadata)
|
|
291
|
+
: loaded.promise.then(module => module.metadata),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (entry['500s']?.length) {
|
|
296
|
+
for (const errLoader of entry['500s']) {
|
|
297
|
+
if (!errLoader)
|
|
298
|
+
continue;
|
|
299
|
+
const loaded = Resolver.#load(errLoader);
|
|
300
|
+
metadataSources.push({
|
|
301
|
+
priority: Metadata.PRIORITY[Build.EntryKind['500']],
|
|
302
|
+
when: 'error',
|
|
303
|
+
status: 500,
|
|
304
|
+
load: () => loaded.module
|
|
305
|
+
? Promise.resolve(loaded.module.metadata)
|
|
306
|
+
: loaded.promise.then(module => module.metadata),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
enhanced.metadata = ({ params, error }) =>
|
|
311
|
+
// metadata execution still happens per request because params and errors
|
|
312
|
+
// can differ
|
|
313
|
+
Metadata.tasks(metadataSources, { params, error }, err => {
|
|
314
|
+
logger.error(`[enhance.metadata]: ${__id}`, err);
|
|
315
|
+
});
|
|
316
|
+
if (!IS_DEV)
|
|
317
|
+
Resolver.#enhancedMatchCache.set(__id, enhanced);
|
|
318
|
+
return {
|
|
319
|
+
...enhanced,
|
|
320
|
+
params,
|
|
321
|
+
error,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Find the closest ancestor entry for a given path and property
|
|
326
|
+
*/
|
|
327
|
+
closest(path, property, value) {
|
|
328
|
+
const parts = path.split('/').filter(Boolean);
|
|
329
|
+
const segments = property.split('.');
|
|
330
|
+
// walk from the current path back towards the root until we find a match
|
|
331
|
+
for (let i = parts.length; i >= 0; i--) {
|
|
332
|
+
const testPath = i === 0 ? '/' : `/${parts.slice(0, i).join('/')}`;
|
|
333
|
+
const entry = this.#manifest[testPath];
|
|
334
|
+
if (!entry)
|
|
335
|
+
continue;
|
|
336
|
+
const pageEntry = Resolver.narrow(entry);
|
|
337
|
+
if (!pageEntry)
|
|
338
|
+
continue;
|
|
339
|
+
let curr = pageEntry;
|
|
340
|
+
// follow the dotted property path step by step on the matched entry
|
|
341
|
+
for (const segment of segments) {
|
|
342
|
+
if (!curr || typeof curr !== 'object')
|
|
343
|
+
break;
|
|
344
|
+
if (!(segment in curr))
|
|
345
|
+
break;
|
|
346
|
+
curr = curr[segment];
|
|
347
|
+
}
|
|
348
|
+
if (curr === undefined)
|
|
349
|
+
continue;
|
|
350
|
+
if (value && curr !== value)
|
|
351
|
+
continue;
|
|
352
|
+
return pageEntry;
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare namespace Navigation {
|
|
2
|
+
type GoOptions = {
|
|
3
|
+
replace?: boolean;
|
|
4
|
+
query?: Record<string, string | number | boolean>;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export declare const RouterContext: import("react").Context<{
|
|
8
|
+
go: (to: string, opts?: Navigation.GoOptions | undefined) => Promise<string>;
|
|
9
|
+
prefetch: (path: string) => void;
|
|
10
|
+
isNavigating: boolean;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RSCPayload } from '../env/rsc';
|
|
2
|
+
export declare function RouterProvider({ children, setPayload, isNavigating }: {
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
setPayload?: (payload: RSCPayload) => void;
|
|
5
|
+
isNavigating?: boolean;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
4
|
+
import { createFromFetch } from '@vitejs/plugin-rsc/browser';
|
|
5
|
+
import { Solas } from '../../solas';
|
|
6
|
+
import { Logger } from '../../utils/logger';
|
|
7
|
+
import { Prefetcher } from './prefetcher';
|
|
8
|
+
import { RouterContext } from './router-context';
|
|
9
|
+
const DEFAULT_GO_CONFIG = {
|
|
10
|
+
replace: false,
|
|
11
|
+
};
|
|
12
|
+
const logger = new Logger();
|
|
13
|
+
const prefetcher = new Prefetcher();
|
|
14
|
+
export function RouterProvider({ children, setPayload, isNavigating = false, }) {
|
|
15
|
+
// id to track active navigations
|
|
16
|
+
const id = useRef(0);
|
|
17
|
+
// abort controller for in-flight navigation
|
|
18
|
+
const controller = useRef(null);
|
|
19
|
+
/**
|
|
20
|
+
* Navigate to a new route
|
|
21
|
+
* @param to the destination url (absolute or relative to origin)
|
|
22
|
+
* @param opts navigation options
|
|
23
|
+
* @returns the path that was navigated to (relative to origin)
|
|
24
|
+
*/
|
|
25
|
+
const go = useCallback(async (to, opts = {}) => {
|
|
26
|
+
id.current += 1;
|
|
27
|
+
const navigationId = id.current;
|
|
28
|
+
controller.current?.abort();
|
|
29
|
+
controller.current = null;
|
|
30
|
+
const url = new URL(to, window.location.origin);
|
|
31
|
+
const replace = opts?.replace ?? DEFAULT_GO_CONFIG.replace;
|
|
32
|
+
if (opts?.query) {
|
|
33
|
+
for (const [key, value] of Object.entries(opts.query)) {
|
|
34
|
+
url.searchParams.set(key, String(value));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const path = Prefetcher.key(url.toString(), window.location.origin);
|
|
38
|
+
// distinguish an actual prior prefetch from a cache entry we create
|
|
39
|
+
// opportunistically for this navigation
|
|
40
|
+
const existing = prefetcher.has(path);
|
|
41
|
+
try {
|
|
42
|
+
let promise = prefetcher.get(path);
|
|
43
|
+
if (!promise) {
|
|
44
|
+
const ctrl = new AbortController();
|
|
45
|
+
controller.current = ctrl;
|
|
46
|
+
promise = fetch(path, {
|
|
47
|
+
headers: { accept: 'text/x-component' },
|
|
48
|
+
signal: ctrl.signal,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (!prefetcher.has(path))
|
|
52
|
+
prefetcher.set(path, promise);
|
|
53
|
+
// if another navigation has started since this one, ignore the result
|
|
54
|
+
// and return early
|
|
55
|
+
if (navigationId !== id.current)
|
|
56
|
+
return path;
|
|
57
|
+
const res = await createFromFetch(promise);
|
|
58
|
+
// check again if another navigation has started while we were awaiting
|
|
59
|
+
// the response
|
|
60
|
+
if (navigationId !== id.current)
|
|
61
|
+
return path;
|
|
62
|
+
// this state update is already wrapped in a
|
|
63
|
+
// transition before being passed as props
|
|
64
|
+
setPayload?.(res);
|
|
65
|
+
if (replace) {
|
|
66
|
+
window.history.replaceState(null, '', path);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
window.history.pushState(null, '', path);
|
|
70
|
+
}
|
|
71
|
+
window.dispatchEvent(new CustomEvent(Solas.Events.names.NAVIGATION, { detail: { path } }));
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
75
|
+
return path;
|
|
76
|
+
}
|
|
77
|
+
window.dispatchEvent(new CustomEvent(Solas.Events.names.NAVIGATION_ERROR, {
|
|
78
|
+
detail: {
|
|
79
|
+
path,
|
|
80
|
+
error: err instanceof Error ? err.message : Logger.print(err),
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
logger.error('[navigation] failed', err);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
if (navigationId === id.current)
|
|
87
|
+
controller.current = null;
|
|
88
|
+
// preserve entries that were already prefetched so nearby follow-up
|
|
89
|
+
// navigations can still reuse them within the prefetch TTL window
|
|
90
|
+
if (!existing) {
|
|
91
|
+
// entries created by go() only serve as in-flight dedupe for this
|
|
92
|
+
// navigation (i.e. not intentionally prefetched)
|
|
93
|
+
prefetcher.remove(path);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return path;
|
|
97
|
+
}, [setPayload]);
|
|
98
|
+
/**
|
|
99
|
+
* Prefetch a route's assets by fetching the RSC payload
|
|
100
|
+
* @param path the route path to prefetch (absolute or relative to origin)
|
|
101
|
+
* @returns void
|
|
102
|
+
*/
|
|
103
|
+
const prefetch = useCallback((path) => {
|
|
104
|
+
const connection = window.navigator.connection;
|
|
105
|
+
if (document.visibilityState === 'hidden')
|
|
106
|
+
return;
|
|
107
|
+
if (connection?.saveData)
|
|
108
|
+
return;
|
|
109
|
+
if (['2g', 'slow-2g'].includes(connection?.effectiveType ?? ''))
|
|
110
|
+
return;
|
|
111
|
+
const key = Prefetcher.key(path, window.location.origin);
|
|
112
|
+
if (prefetcher.has(key))
|
|
113
|
+
return;
|
|
114
|
+
prefetcher.set(key, fetch(key, { headers: { Accept: 'text/x-component' } }));
|
|
115
|
+
}, []);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const handler = () => go(window.location.href, { replace: true });
|
|
118
|
+
window.addEventListener('popstate', handler);
|
|
119
|
+
return () => {
|
|
120
|
+
controller.current?.abort();
|
|
121
|
+
controller.current = null;
|
|
122
|
+
window.removeEventListener('popstate', handler);
|
|
123
|
+
};
|
|
124
|
+
}, [go]);
|
|
125
|
+
const value = useMemo(() => ({
|
|
126
|
+
go,
|
|
127
|
+
prefetch,
|
|
128
|
+
isNavigating,
|
|
129
|
+
}), [go, prefetch, isNavigating]);
|
|
130
|
+
return _jsx(RouterContext, { value: value, children: children });
|
|
131
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { HttpMethod, PluginConfig, SolasRequest } from '../../types';
|
|
2
|
+
export declare namespace Router {
|
|
3
|
+
type Params = Record<string, string | string[]>;
|
|
4
|
+
type Handler = (req: SolasRequest) => Response | Promise<Response>;
|
|
5
|
+
type ErrorHandler = (err: Error, req: SolasRequest) => Response | Promise<Response>;
|
|
6
|
+
type Middleware = (req: SolasRequest, next: () => Promise<Response>) => Response | Promise<Response>;
|
|
7
|
+
type Token = {
|
|
8
|
+
kind: 'static' | 'dynamic' | 'wildcard';
|
|
9
|
+
value: string;
|
|
10
|
+
};
|
|
11
|
+
type Route = {
|
|
12
|
+
path: string;
|
|
13
|
+
method: string;
|
|
14
|
+
handler?: Handler;
|
|
15
|
+
middleware: Middleware[];
|
|
16
|
+
tokens: Token[];
|
|
17
|
+
length: number;
|
|
18
|
+
score: number;
|
|
19
|
+
wildcard: boolean;
|
|
20
|
+
};
|
|
21
|
+
type Match = {
|
|
22
|
+
route: Route;
|
|
23
|
+
params: Params;
|
|
24
|
+
};
|
|
25
|
+
type Options = {
|
|
26
|
+
trailingSlash?: boolean;
|
|
27
|
+
};
|
|
28
|
+
type Registry = {
|
|
29
|
+
static: Map<string, Route>;
|
|
30
|
+
dynamic: {
|
|
31
|
+
byLength: Map<number, Route[]>;
|
|
32
|
+
byPrefix: Map<string, Route[]>;
|
|
33
|
+
};
|
|
34
|
+
wildcard: {
|
|
35
|
+
byPrefix: Map<string, Route[]>;
|
|
36
|
+
fallback: Route[];
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Handle routing and matching for server requests
|
|
42
|
+
*/
|
|
43
|
+
export declare class Router {
|
|
44
|
+
#private;
|
|
45
|
+
opts: Router.Options;
|
|
46
|
+
constructor(opts?: Router.Options);
|
|
47
|
+
/**
|
|
48
|
+
* Register middleware for all routes
|
|
49
|
+
*/
|
|
50
|
+
use(...middleware: Router.Middleware[]): this;
|
|
51
|
+
/**
|
|
52
|
+
* Register an error handler for routing failures
|
|
53
|
+
*/
|
|
54
|
+
error(handler: Router.ErrorHandler): this;
|
|
55
|
+
/**
|
|
56
|
+
* Register a route handler
|
|
57
|
+
*/
|
|
58
|
+
add(path: string, method: string, handler: Router.Handler, params?: string[], middleware?: Router.Middleware[]): this;
|
|
59
|
+
/**
|
|
60
|
+
* Match a path and method, returning params and route
|
|
61
|
+
*/
|
|
62
|
+
match(path: string, method: HttpMethod): {
|
|
63
|
+
route: Router.Route;
|
|
64
|
+
params: Router.Params;
|
|
65
|
+
} | null;
|
|
66
|
+
/**
|
|
67
|
+
* Handle an incoming request
|
|
68
|
+
*/
|
|
69
|
+
fetch(req: Request): Promise<Response>;
|
|
70
|
+
/**
|
|
71
|
+
* Serve static assets from the output directory
|
|
72
|
+
* @note generated /assets/* handlers bypass +middleware conventions
|
|
73
|
+
*/
|
|
74
|
+
static static(config: PluginConfig): (req: Request) => Promise<Response>;
|
|
75
|
+
/**
|
|
76
|
+
* Serve a file with optional compression content negotiation
|
|
77
|
+
*/
|
|
78
|
+
static serve(filePath: string, req: Request, precompress?: boolean, headers?: Record<string, string>): Promise<Response>;
|
|
79
|
+
}
|