@jk2908/solas 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +2 -0
- package/dist/cli.js +77 -83
- package/dist/error-boundary.d.ts +1 -1
- package/dist/error-boundary.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +10 -14
- package/dist/internal/build.d.ts +1 -1
- package/dist/internal/build.js +4 -4
- package/dist/internal/codegen/config.d.ts +1 -1
- package/dist/internal/codegen/config.js +10 -10
- package/dist/internal/codegen/environments.js +22 -18
- package/dist/internal/codegen/manifest.d.ts +1 -1
- package/dist/internal/codegen/manifest.js +6 -7
- package/dist/internal/codegen/maps.d.ts +1 -1
- package/dist/internal/codegen/maps.js +38 -27
- package/dist/internal/codegen/utils.d.ts +20 -0
- package/dist/internal/codegen/utils.js +140 -1
- package/dist/internal/env/browser.js +20 -16
- package/dist/internal/env/request-context.d.ts +2 -2
- package/dist/internal/env/request-context.js +1 -1
- package/dist/internal/env/rsc.d.ts +8 -22
- package/dist/internal/env/rsc.js +38 -117
- package/dist/internal/env/ssr.js +9 -9
- package/dist/internal/env/utils.js +2 -2
- package/dist/internal/metadata.d.ts +2 -2
- package/dist/internal/metadata.js +18 -6
- package/dist/internal/navigation/http-exception-boundary.d.ts +2 -2
- package/dist/internal/navigation/http-exception-boundary.js +1 -1
- package/dist/internal/navigation/link.js +1 -1
- package/dist/internal/navigation/redirect-boundary.d.ts +1 -1
- package/dist/internal/navigation/redirect-boundary.js +1 -1
- package/dist/internal/navigation/redirect.js +1 -1
- package/dist/internal/navigation/use-search-params.js +4 -2
- package/dist/internal/prerender.d.ts +10 -1
- package/dist/internal/prerender.js +55 -5
- package/dist/internal/render/head.d.ts +4 -1
- package/dist/internal/render/head.js +37 -18
- package/dist/internal/render/tree.d.ts +1 -1
- package/dist/internal/render/tree.js +3 -3
- package/dist/internal/router/create-router.d.ts +2 -2
- package/dist/internal/router/create-router.js +1 -1
- package/dist/internal/router/prefetcher.d.ts +1 -1
- package/dist/internal/router/prefetcher.js +8 -3
- package/dist/internal/router/resolver.d.ts +29 -29
- package/dist/internal/router/resolver.js +4 -4
- package/dist/internal/router/router-context.d.ts +4 -0
- package/dist/internal/router/router-context.js +1 -0
- package/dist/internal/router/router-provider.d.ts +6 -2
- package/dist/internal/router/router-provider.js +38 -22
- package/dist/internal/router/router.d.ts +1 -1
- package/dist/internal/router/router.js +4 -4
- package/dist/internal/router/use-router.d.ts +5 -1
- package/dist/internal/router/use-router.js +1 -1
- package/dist/internal/router/utils.d.ts +1 -1
- package/dist/internal/server/actions.d.ts +30 -0
- package/dist/internal/server/actions.js +107 -0
- package/dist/internal/server/cookies.d.ts +1 -1
- package/dist/internal/server/cookies.js +3 -3
- package/dist/internal/server/dynamic.js +2 -2
- package/dist/internal/server/headers.js +2 -2
- package/dist/internal/server/url.js +14 -3
- package/dist/internal/ui/defaults/error.d.ts +1 -1
- package/dist/internal/ui/error-boundary.d.ts +1 -1
- package/dist/internal/ui/error-boundary.js +1 -1
- package/dist/navigation.d.ts +6 -6
- package/dist/navigation.js +6 -6
- package/dist/prerender.d.ts +1 -1
- package/dist/prerender.js +1 -1
- package/dist/router.d.ts +4 -4
- package/dist/router.js +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/server.js +4 -4
- package/dist/solas.d.ts +1 -1
- package/dist/solas.js +1 -0
- package/dist/types.d.ts +6 -6
- package/dist/types.js +1 -1
- package/dist/utils/context.js +1 -1
- package/dist/utils/logger.js +2 -2
- package/package.json +3 -1
- package/dist/utils/format.d.ts +0 -6
- package/dist/utils/format.js +0 -72
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useMemo, useSyncExternalStore } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { useRouter } from '../../router.js';
|
|
3
|
+
import { Solas } from '../../solas.js';
|
|
3
4
|
export function useSearchParams() {
|
|
5
|
+
const { url } = useRouter();
|
|
4
6
|
const search = useSyncExternalStore(fn => {
|
|
5
7
|
window.addEventListener('popstate', fn);
|
|
6
8
|
window.addEventListener(Solas.Events.names.NAVIGATION, fn);
|
|
@@ -8,6 +10,6 @@ export function useSearchParams() {
|
|
|
8
10
|
window.removeEventListener('popstate', fn);
|
|
9
11
|
window.removeEventListener(Solas.Events.names.NAVIGATION, fn);
|
|
10
12
|
};
|
|
11
|
-
}, () => window.location.search, () =>
|
|
13
|
+
}, () => window.location.search, () => url?.search);
|
|
12
14
|
return useMemo(() => new URLSearchParams(search), [search]);
|
|
13
15
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuildContext } from '../types';
|
|
1
|
+
import type { BuildContext } from '../types.js';
|
|
2
2
|
export declare namespace Prerender {
|
|
3
3
|
namespace Artifact {
|
|
4
4
|
type Mode = 'full' | 'ppr';
|
|
@@ -16,6 +16,7 @@ export declare namespace Prerender {
|
|
|
16
16
|
mode: Mode;
|
|
17
17
|
createdAt: number;
|
|
18
18
|
files?: File[];
|
|
19
|
+
fullPrerenderFilename?: string;
|
|
19
20
|
};
|
|
20
21
|
type Manifest = {
|
|
21
22
|
generatedAt: number;
|
|
@@ -35,6 +36,14 @@ export declare namespace Prerender {
|
|
|
35
36
|
* Get the file system path for storing prerender artifacts for a given route
|
|
36
37
|
*/
|
|
37
38
|
function getPath(outDir: string, pathname: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Get the file system path for a single prerender artifact file under a route directory
|
|
41
|
+
*/
|
|
42
|
+
function getFilePath(outDir: string, pathname: string, fileName: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Build a deterministic file name for a full prerender html artifact
|
|
45
|
+
*/
|
|
46
|
+
function getFullHtmlFileName(html: string): string;
|
|
38
47
|
/**
|
|
39
48
|
* Load the prerender artifact manifest for faster runtime route mode checks
|
|
40
49
|
*/
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
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/utils';
|
|
3
|
+
import { Solas } from '../solas.js';
|
|
4
|
+
import { Logger } from '../utils/logger.js';
|
|
5
|
+
import { Time } from '../utils/time.js';
|
|
6
|
+
import { toPathPattern } from './router/utils.js';
|
|
7
7
|
const logger = new Logger();
|
|
8
8
|
export { Prerender };
|
|
9
9
|
var Prerender;
|
|
@@ -13,6 +13,15 @@ var Prerender;
|
|
|
13
13
|
let Artifact;
|
|
14
14
|
(function (Artifact) {
|
|
15
15
|
const manifestCache = new Map();
|
|
16
|
+
/**
|
|
17
|
+
* Check whether a file name is safe to join under an artifact directory
|
|
18
|
+
*/
|
|
19
|
+
function isArtifactFileName(value) {
|
|
20
|
+
return (typeof value === 'string' &&
|
|
21
|
+
value.length > 0 &&
|
|
22
|
+
path.basename(value) === value &&
|
|
23
|
+
!value.includes(path.sep));
|
|
24
|
+
}
|
|
16
25
|
/**
|
|
17
26
|
* Get the root directory path where prerender artifacts are stored,
|
|
18
27
|
* based on the output directory specified in the configuration
|
|
@@ -43,6 +52,23 @@ var Prerender;
|
|
|
43
52
|
return artifactPath;
|
|
44
53
|
}
|
|
45
54
|
Artifact.getPath = getPath;
|
|
55
|
+
/**
|
|
56
|
+
* Get the file system path for a single prerender artifact file under a route directory
|
|
57
|
+
*/
|
|
58
|
+
function getFilePath(outDir, pathname, fileName) {
|
|
59
|
+
if (!isArtifactFileName(fileName)) {
|
|
60
|
+
throw new Error('[prerender] invalid artifact file name');
|
|
61
|
+
}
|
|
62
|
+
return path.join(getPath(outDir, pathname), fileName);
|
|
63
|
+
}
|
|
64
|
+
Artifact.getFilePath = getFilePath;
|
|
65
|
+
/**
|
|
66
|
+
* Build a deterministic file name for a full prerender html artifact
|
|
67
|
+
*/
|
|
68
|
+
function getFullHtmlFileName(html) {
|
|
69
|
+
return `html.${Bun.hash(html).toString(16)}.html`;
|
|
70
|
+
}
|
|
71
|
+
Artifact.getFullHtmlFileName = getFullHtmlFileName;
|
|
46
72
|
/**
|
|
47
73
|
* Load the prerender artifact manifest for faster runtime route mode checks
|
|
48
74
|
*/
|
|
@@ -107,6 +133,11 @@ var Prerender;
|
|
|
107
133
|
}
|
|
108
134
|
}
|
|
109
135
|
}
|
|
136
|
+
if (entry.fullPrerenderFilename !== undefined &&
|
|
137
|
+
!isArtifactFileName(entry.fullPrerenderFilename)) {
|
|
138
|
+
manifestCache.set(outDir, null);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
110
141
|
}
|
|
111
142
|
const manifest = { generatedAt, routes };
|
|
112
143
|
// cache validated manifest to avoid reparsing on every request
|
|
@@ -193,7 +224,12 @@ var Prerender;
|
|
|
193
224
|
return null;
|
|
194
225
|
if (mode !== 'full' && mode !== 'ppr')
|
|
195
226
|
return null;
|
|
196
|
-
return {
|
|
227
|
+
return {
|
|
228
|
+
schema,
|
|
229
|
+
route,
|
|
230
|
+
createdAt,
|
|
231
|
+
mode,
|
|
232
|
+
};
|
|
197
233
|
}
|
|
198
234
|
catch {
|
|
199
235
|
return null;
|
|
@@ -219,14 +255,22 @@ var Prerender;
|
|
|
219
255
|
* into the prelude at the appropriate location (before </body> or </html>)
|
|
220
256
|
*/
|
|
221
257
|
function composePreludeAndResume(prelude, resumeStream) {
|
|
258
|
+
// `prelude` is the static shell html as one complete string, usually shaped like
|
|
259
|
+
// `<html>...<body>static shell...</body></html>` or an html fragment with no close tags
|
|
222
260
|
// search both cases to avoid duplicating the full string with toLowerCase
|
|
223
261
|
const bodyClose = Math.max(prelude.lastIndexOf('</body>'), prelude.lastIndexOf('</BODY>'));
|
|
224
262
|
const htmlClose = Math.max(prelude.lastIndexOf('</html>'), prelude.lastIndexOf('</HTML>'));
|
|
263
|
+
// prefer inserting before </body>, then before </html>, and fall back
|
|
264
|
+
// to appending when the prelude is only a fragment with no close tags
|
|
225
265
|
const splitAt = bodyClose >= 0 ? bodyClose : htmlClose >= 0 ? htmlClose : prelude.length;
|
|
226
266
|
return new ReadableStream({
|
|
227
267
|
async start(controller) {
|
|
228
268
|
// send everything before the closing tags so the resume stream can be injected
|
|
229
269
|
controller.enqueue(encoder.encode(prelude.slice(0, splitAt)));
|
|
270
|
+
// resumeStream is the html React emits when it resumes postponed work for this page. Its
|
|
271
|
+
// first chunk begins with an extra `</body></html>` pair, then continues with the
|
|
272
|
+
// resumed scripts and markup for the unfinished work. Strip that leading
|
|
273
|
+
// pair once, then pass the rest through unchanged
|
|
230
274
|
const reader = resumeStream.getReader();
|
|
231
275
|
let strippedLeadingClose = false;
|
|
232
276
|
try {
|
|
@@ -239,11 +283,17 @@ var Prerender;
|
|
|
239
283
|
if (!strippedLeadingClose) {
|
|
240
284
|
strippedLeadingClose = true;
|
|
241
285
|
const text = decoder.decode(value);
|
|
286
|
+
// we already wrote the prelude up to the insertion point before its closing tags.
|
|
287
|
+
// React's first resumed chunk starts with an extra `</body></html>` pair,
|
|
288
|
+
// so strip that prefix and keep the rest of the chunk
|
|
242
289
|
const trimmed = text.replace(/^\s*<\/body>\s*<\/html>/i, '');
|
|
243
290
|
if (trimmed.length > 0)
|
|
244
291
|
controller.enqueue(encoder.encode(trimmed));
|
|
245
292
|
continue;
|
|
246
293
|
}
|
|
294
|
+
// once the duplicated `</body></html>` prefix is removed, stop trying to
|
|
295
|
+
// interpret the stream and forward each remaining chunk exactly as React
|
|
296
|
+
// emitted it
|
|
247
297
|
controller.enqueue(value);
|
|
248
298
|
}
|
|
249
299
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { type Metadata as Collection } from '../metadata';
|
|
1
|
+
import { type Metadata as Collection } from '../metadata.js';
|
|
2
|
+
/**
|
|
3
|
+
* Renders title, meta, and link tags based on the provided metadata payload
|
|
4
|
+
*/
|
|
2
5
|
export declare function Head({ metadata: m }: {
|
|
3
6
|
metadata?: Collection.Item | Promise<Collection.Item>;
|
|
4
7
|
}): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,38 +1,57 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { use } from 'react';
|
|
3
|
-
import { Solas } from '../../solas';
|
|
4
|
-
import { Logger } from '../../utils/logger';
|
|
3
|
+
import { Solas } from '../../solas.js';
|
|
4
|
+
import { Logger } from '../../utils/logger.js';
|
|
5
5
|
const logger = new Logger();
|
|
6
6
|
const cache = new WeakMap();
|
|
7
|
+
/**
|
|
8
|
+
* Convert supported metadata primitives to string for meta tag content
|
|
9
|
+
*/
|
|
10
|
+
function toContent(value) {
|
|
11
|
+
return typeof value === 'string' ||
|
|
12
|
+
typeof value === 'number' ||
|
|
13
|
+
typeof value === 'boolean'
|
|
14
|
+
? String(value)
|
|
15
|
+
: undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Convert a metadata item or promise of an item to a promise that always resolves
|
|
19
|
+
* successfully, caching the result for future use
|
|
20
|
+
*/
|
|
21
|
+
function toSafeUsable(metadata) {
|
|
22
|
+
const cached = cache.get(metadata);
|
|
23
|
+
if (cached)
|
|
24
|
+
return cached;
|
|
25
|
+
const safe = Promise.resolve(metadata).catch(err => {
|
|
26
|
+
logger.error('[head] failed to resolve metadata', err);
|
|
27
|
+
return {};
|
|
28
|
+
});
|
|
29
|
+
cache.set(metadata, safe);
|
|
30
|
+
return safe;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Renders title, meta, and link tags based on the provided metadata payload
|
|
34
|
+
*/
|
|
7
35
|
export function Head({ metadata: m, }) {
|
|
8
36
|
if (!m)
|
|
9
37
|
return null;
|
|
10
38
|
const metadata = use(toSafeUsable(m));
|
|
11
39
|
return (_jsxs(_Fragment, { children: [
|
|
12
|
-
_jsx("meta", { name: "generator", content: Solas.Config.NAME }), metadata.title && _jsx("title", { children: metadata.title
|
|
40
|
+
_jsx("meta", { name: "generator", content: Solas.Config.NAME }), toContent(metadata.title) !== undefined && (_jsx("title", { children: toContent(metadata.title) })), metadata.meta?.map(meta => {
|
|
13
41
|
if ('charSet' in meta) {
|
|
14
42
|
return _jsx("meta", { charSet: meta.charSet }, meta.charSet);
|
|
15
43
|
}
|
|
16
44
|
if ('name' in meta) {
|
|
17
|
-
return (_jsx("meta", { name: meta.name, content: meta.content
|
|
45
|
+
return (_jsx("meta", { name: meta.name, content: toContent(meta.content) }, meta.name));
|
|
18
46
|
}
|
|
19
47
|
if ('httpEquiv' in meta) {
|
|
20
|
-
return (_jsx("meta", { httpEquiv: meta.httpEquiv, content: meta.content
|
|
48
|
+
return (_jsx("meta", { httpEquiv: meta.httpEquiv, content: toContent(meta.content) }, meta.httpEquiv));
|
|
21
49
|
}
|
|
22
50
|
if ('property' in meta) {
|
|
23
|
-
return (_jsx("meta", { property: meta.property, content: meta.content
|
|
51
|
+
return (_jsx("meta", { property: meta.property, content: toContent(meta.content) }, meta.property));
|
|
24
52
|
}
|
|
25
53
|
return null;
|
|
26
|
-
}), metadata.link?.map(link => (_jsx("link", {
|
|
27
|
-
|
|
28
|
-
|
|
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;
|
|
54
|
+
}), metadata.link?.map(link => (_jsx("link", { rel: link.rel, href: typeof link.href === 'string' ? link.href : undefined, as: typeof link.as === 'string' ? link.as : undefined, type: typeof link.type === 'string' ? link.type : undefined, media: typeof link.media === 'string' ? link.media : undefined, sizes: typeof link.sizes === 'string' ? link.sizes : undefined, crossOrigin: link.crossOrigin === 'anonymous' || link.crossOrigin === 'use-credentials'
|
|
55
|
+
? link.crossOrigin
|
|
56
|
+
: undefined }, `${link.rel}${link.href ?? ''}`)))] }));
|
|
38
57
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
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';
|
|
3
|
+
import { HttpException, isHttpException } from '../navigation/http-exception.js';
|
|
4
|
+
import { HttpExceptionBoundary } from '../navigation/http-exception-boundary.js';
|
|
5
|
+
import DefaultErr from '../ui/defaults/error.js';
|
|
6
6
|
/**
|
|
7
7
|
* Render the resolved route tree for a matched page
|
|
8
8
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ImportMap, Manifest, PluginConfig, SolasRequest } from '../../types';
|
|
2
|
-
import { Router } from './router';
|
|
1
|
+
import type { ImportMap, Manifest, PluginConfig, SolasRequest } from '../../types.js';
|
|
2
|
+
import { Router } from './router.js';
|
|
3
3
|
/**
|
|
4
4
|
* Create the application router from the generated manifest and import map
|
|
5
5
|
*/
|
|
@@ -16,7 +16,7 @@ export declare class Prefetcher {
|
|
|
16
16
|
* Converts a url path to a cache key by normalising it
|
|
17
17
|
* against a base url
|
|
18
18
|
*/
|
|
19
|
-
static key(path: string, base: string): string;
|
|
19
|
+
static key(path: string, base: string): string | null;
|
|
20
20
|
/**
|
|
21
21
|
* Evicts the oldest entry from the cache
|
|
22
22
|
*/
|
|
@@ -11,9 +11,14 @@ export class Prefetcher {
|
|
|
11
11
|
* against a base url
|
|
12
12
|
*/
|
|
13
13
|
static key(path, base) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(path, base);
|
|
16
|
+
// hash is client-only and never sent to the server, so exclude it
|
|
17
|
+
return url.pathname + url.search;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
17
22
|
}
|
|
18
23
|
/**
|
|
19
24
|
* Evicts the oldest entry from the cache
|
|
@@ -1,7 +1,7 @@
|
|
|
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';
|
|
1
|
+
import type { ImportMap, Manifest, ManifestEntry, Primitive, View } from '../../types.js';
|
|
2
|
+
import type { Router } from './router.js';
|
|
3
|
+
import { Metadata } from '../metadata.js';
|
|
4
|
+
import { HttpException } from '../navigation/http-exception.js';
|
|
5
5
|
export declare namespace Resolver {
|
|
6
6
|
type ReconciledMatch = ReturnType<Resolver['reconcile']>;
|
|
7
7
|
type CachedEnhancedMatch = Omit<EnhancedMatch, 'params' | 'error'>;
|
|
@@ -55,7 +55,7 @@ export declare class Resolver {
|
|
|
55
55
|
/**
|
|
56
56
|
* Narrow down a route entry to a page entry if it exists
|
|
57
57
|
*/
|
|
58
|
-
static narrow(entry?: ManifestEntry | ManifestEntry[]): import("
|
|
58
|
+
static narrow(entry?: ManifestEntry | ManifestEntry[]): import("../../types.js").Segment | null;
|
|
59
59
|
/**
|
|
60
60
|
* Get the status code for a matched route that may or may not have errored
|
|
61
61
|
*/
|
|
@@ -64,8 +64,6 @@ export declare class Resolver {
|
|
|
64
64
|
* Reconcile a router match against a manifest entry
|
|
65
65
|
*/
|
|
66
66
|
reconcile(path: string, match: Router.Match | null, error?: Error): {
|
|
67
|
-
params: Router.Params;
|
|
68
|
-
error: Error | undefined;
|
|
69
67
|
__id: string;
|
|
70
68
|
__path: string;
|
|
71
69
|
__params: string[];
|
|
@@ -85,9 +83,9 @@ export declare class Resolver {
|
|
|
85
83
|
prerender: "full" | "ppr" | false;
|
|
86
84
|
dynamic: boolean;
|
|
87
85
|
wildcard: boolean;
|
|
86
|
+
params: Router.Params;
|
|
87
|
+
error: Error | undefined;
|
|
88
88
|
} | {
|
|
89
|
-
params: {};
|
|
90
|
-
error: HttpException;
|
|
91
89
|
__id: string;
|
|
92
90
|
__path: string;
|
|
93
91
|
__params: string[];
|
|
@@ -107,11 +105,32 @@ export declare class Resolver {
|
|
|
107
105
|
prerender: "full" | "ppr" | false;
|
|
108
106
|
dynamic: boolean;
|
|
109
107
|
wildcard: boolean;
|
|
108
|
+
params: {};
|
|
109
|
+
error: HttpException;
|
|
110
110
|
} | null;
|
|
111
111
|
/**
|
|
112
112
|
* Enhance a matched route with its associated components
|
|
113
113
|
*/
|
|
114
114
|
enhance(match: Resolver.ReconciledMatch | null): {
|
|
115
|
+
__id: string;
|
|
116
|
+
__path: string;
|
|
117
|
+
__params: string[];
|
|
118
|
+
__kind: "$P";
|
|
119
|
+
__depth: number;
|
|
120
|
+
method: "get";
|
|
121
|
+
paths: {
|
|
122
|
+
layouts: (string | null)[];
|
|
123
|
+
'401s': (string | null)[];
|
|
124
|
+
'403s': (string | null)[];
|
|
125
|
+
'404s': (string | null)[];
|
|
126
|
+
'500s': (string | null)[];
|
|
127
|
+
loaders: (string | null)[];
|
|
128
|
+
middlewares: (string | null)[];
|
|
129
|
+
page?: string | null | undefined;
|
|
130
|
+
};
|
|
131
|
+
prerender: "full" | "ppr" | false;
|
|
132
|
+
dynamic: boolean;
|
|
133
|
+
wildcard: boolean;
|
|
115
134
|
ui: {
|
|
116
135
|
layouts: (View<{
|
|
117
136
|
children?: import("react").ReactNode;
|
|
@@ -147,28 +166,9 @@ export declare class Resolver {
|
|
|
147
166
|
metadata?: ((input: Metadata.Input<Router.Params, Error>) => Metadata.Task[]) | undefined;
|
|
148
167
|
params: Router.Params | {};
|
|
149
168
|
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
169
|
} | null;
|
|
170
170
|
/**
|
|
171
171
|
* Find the closest ancestor entry for a given path and property
|
|
172
172
|
*/
|
|
173
|
-
closest(path: string, property: string, value?: Omit<Primitive, 'undefined'>): import("
|
|
173
|
+
closest(path: string, property: string, value?: Omit<Primitive, 'undefined'>): import("../../types.js").Segment | null;
|
|
174
174
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
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';
|
|
2
|
+
import { Logger } from '../../utils/logger.js';
|
|
3
|
+
import { Build } from '../build.js';
|
|
4
|
+
import { Metadata } from '../metadata.js';
|
|
5
|
+
import { HttpException, isHttpException } from '../navigation/http-exception.js';
|
|
6
6
|
const logger = new Logger();
|
|
7
7
|
const IS_DEV = import.meta.env.DEV;
|
|
8
8
|
/**
|
|
@@ -8,4 +8,8 @@ export declare const RouterContext: import("react").Context<{
|
|
|
8
8
|
go: (to: string, opts?: Navigation.GoOptions | undefined) => Promise<string>;
|
|
9
9
|
prefetch: (path: string) => void;
|
|
10
10
|
isNavigating: boolean;
|
|
11
|
+
url: {
|
|
12
|
+
pathname?: string | undefined;
|
|
13
|
+
search?: string | undefined;
|
|
14
|
+
};
|
|
11
15
|
}>;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type { RSCPayload } from '../env/rsc';
|
|
2
|
-
export declare function RouterProvider({ children, setPayload, isNavigating }: {
|
|
1
|
+
import type { RSCPayload } from '../env/rsc.js';
|
|
2
|
+
export declare function RouterProvider({ children, setPayload, isNavigating, url }: {
|
|
3
3
|
children: React.ReactNode;
|
|
4
4
|
setPayload?: (payload: RSCPayload) => void;
|
|
5
5
|
isNavigating?: boolean;
|
|
6
|
+
url?: {
|
|
7
|
+
pathname?: string;
|
|
8
|
+
search?: string;
|
|
9
|
+
};
|
|
6
10
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
4
4
|
import { createFromFetch } from '@vitejs/plugin-rsc/browser';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { Prefetcher } from './prefetcher';
|
|
8
|
-
import { RouterContext } from './router-context';
|
|
5
|
+
import { Logger } from '../../utils/logger.js';
|
|
6
|
+
import { Solas } from '../../solas.js';
|
|
7
|
+
import { Prefetcher } from './prefetcher.js';
|
|
8
|
+
import { RouterContext } from './router-context.js';
|
|
9
9
|
const DEFAULT_GO_CONFIG = {
|
|
10
10
|
replace: false,
|
|
11
11
|
};
|
|
12
12
|
const logger = new Logger();
|
|
13
13
|
const prefetcher = new Prefetcher();
|
|
14
|
-
export function RouterProvider({ children, setPayload, isNavigating = false, }) {
|
|
14
|
+
export function RouterProvider({ children, setPayload, isNavigating = false, url, }) {
|
|
15
15
|
// id to track active navigations
|
|
16
16
|
const id = useRef(0);
|
|
17
17
|
// abort controller for in-flight navigation
|
|
@@ -23,23 +23,34 @@ export function RouterProvider({ children, setPayload, isNavigating = false, })
|
|
|
23
23
|
* @returns the path that was navigated to (relative to origin)
|
|
24
24
|
*/
|
|
25
25
|
const go = useCallback(async (to, opts = {}) => {
|
|
26
|
+
// increment navigation id to invalidate any in-flight navigations
|
|
26
27
|
id.current += 1;
|
|
27
28
|
const navigationId = id.current;
|
|
29
|
+
// fallback for abort/error paths
|
|
30
|
+
let path = window.location.pathname + window.location.search;
|
|
31
|
+
const replace = opts?.replace ?? DEFAULT_GO_CONFIG.replace;
|
|
28
32
|
controller.current?.abort();
|
|
29
33
|
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
34
|
// distinguish an actual prior prefetch from a cache entry we create
|
|
39
35
|
// opportunistically for this navigation
|
|
40
|
-
|
|
36
|
+
let existing = false;
|
|
41
37
|
try {
|
|
38
|
+
const url = new URL(to, window.location.origin);
|
|
39
|
+
if (opts?.query) {
|
|
40
|
+
for (const [key, value] of Object.entries(opts.query)) {
|
|
41
|
+
url.searchParams.set(key, String(value));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const key = Prefetcher.key(url.toString(), window.location.origin);
|
|
45
|
+
if (!key)
|
|
46
|
+
throw new Error('Invalid navigation url');
|
|
47
|
+
// switch to the normalized target once the url is valid
|
|
48
|
+
path = key;
|
|
49
|
+
// if the target was already prefetched, use the cached response promise
|
|
50
|
+
// and set existing to true so we don't remove it from cache
|
|
51
|
+
// after navigation
|
|
42
52
|
let promise = prefetcher.get(path);
|
|
53
|
+
existing = promise !== undefined;
|
|
43
54
|
if (!promise) {
|
|
44
55
|
const ctrl = new AbortController();
|
|
45
56
|
controller.current = ctrl;
|
|
@@ -47,9 +58,8 @@ export function RouterProvider({ children, setPayload, isNavigating = false, })
|
|
|
47
58
|
headers: { accept: 'text/x-component' },
|
|
48
59
|
signal: ctrl.signal,
|
|
49
60
|
});
|
|
50
|
-
}
|
|
51
|
-
if (!prefetcher.has(path))
|
|
52
61
|
prefetcher.set(path, promise);
|
|
62
|
+
}
|
|
53
63
|
// if another navigation has started since this one, ignore the result
|
|
54
64
|
// and return early
|
|
55
65
|
if (navigationId !== id.current)
|
|
@@ -61,7 +71,7 @@ export function RouterProvider({ children, setPayload, isNavigating = false, })
|
|
|
61
71
|
createFromFetch(promise),
|
|
62
72
|
]);
|
|
63
73
|
// use the final response url so client history matches server redirects
|
|
64
|
-
const resolvedPath = Prefetcher.key(res.url, window.location.origin);
|
|
74
|
+
const resolvedPath = Prefetcher.key(res.url, window.location.origin) ?? path;
|
|
65
75
|
// check again if another navigation has started while we were awaiting
|
|
66
76
|
// the response
|
|
67
77
|
if (navigationId !== id.current)
|
|
@@ -95,11 +105,11 @@ export function RouterProvider({ children, setPayload, isNavigating = false, })
|
|
|
95
105
|
finally {
|
|
96
106
|
if (navigationId === id.current)
|
|
97
107
|
controller.current = null;
|
|
98
|
-
//
|
|
99
|
-
//
|
|
108
|
+
// keep entries that were already in the prefetch cache before go() ran. Only remove
|
|
109
|
+
// the temporary cache entry go() created for its own in-flight dedupe
|
|
100
110
|
if (!existing) {
|
|
101
|
-
//
|
|
102
|
-
//
|
|
111
|
+
// this fetch was not an intentional prefetch, so do not leave it behind
|
|
112
|
+
// as a reusable cache entry after navigation finishes
|
|
103
113
|
prefetcher.remove(path);
|
|
104
114
|
}
|
|
105
115
|
}
|
|
@@ -111,6 +121,8 @@ export function RouterProvider({ children, setPayload, isNavigating = false, })
|
|
|
111
121
|
*/
|
|
112
122
|
const prefetch = useCallback((path) => {
|
|
113
123
|
const key = Prefetcher.key(path, window.location.origin);
|
|
124
|
+
if (!key)
|
|
125
|
+
return;
|
|
114
126
|
if (prefetcher.has(key))
|
|
115
127
|
return;
|
|
116
128
|
prefetcher.set(key, fetch(key, { headers: { Accept: 'text/x-component' } }));
|
|
@@ -128,6 +140,10 @@ export function RouterProvider({ children, setPayload, isNavigating = false, })
|
|
|
128
140
|
go,
|
|
129
141
|
prefetch,
|
|
130
142
|
isNavigating,
|
|
131
|
-
|
|
143
|
+
url: {
|
|
144
|
+
pathname: url?.pathname,
|
|
145
|
+
search: url?.search,
|
|
146
|
+
},
|
|
147
|
+
}), [go, prefetch, isNavigating, url]);
|
|
132
148
|
return _jsx(RouterContext, { value: value, children: children });
|
|
133
149
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HttpMethod, PluginConfig, SolasRequest } from '../../types';
|
|
1
|
+
import type { HttpMethod, PluginConfig, SolasRequest } from '../../types.js';
|
|
2
2
|
export declare namespace Router {
|
|
3
3
|
type Params = Record<string, string | string[]>;
|
|
4
4
|
type Handler = (req: SolasRequest) => Response | Promise<Response>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { match as createMatch } from 'path-to-regexp';
|
|
3
|
-
import { Solas } from '../../solas';
|
|
4
|
-
import { getAlternatePathname, normalisePathname, toPathPattern } from './utils';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { Solas } from '../../solas.js';
|
|
4
|
+
import { getAlternatePathname, normalisePathname, toPathPattern } from './utils.js';
|
|
5
|
+
import { HttpException } from '../navigation/http-exception.js';
|
|
6
|
+
import { maybeAction } from '../server/actions.js';
|
|
7
7
|
/**
|
|
8
8
|
* Handle routing and matching for server requests
|
|
9
9
|
*/
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export declare function useRouter(): {
|
|
2
|
-
go: (to: string, opts?: import("./router-context").Navigation.GoOptions | undefined) => Promise<string>;
|
|
2
|
+
go: (to: string, opts?: import("./router-context.js").Navigation.GoOptions | undefined) => Promise<string>;
|
|
3
3
|
prefetch: (path: string) => void;
|
|
4
4
|
isNavigating: boolean;
|
|
5
|
+
url: {
|
|
6
|
+
pathname?: string | undefined;
|
|
7
|
+
search?: string | undefined;
|
|
8
|
+
};
|
|
5
9
|
};
|