akanjs 2.1.1 → 2.1.2-rc.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/base/baseEnv.ts +2 -1
- package/client/csrTypes.ts +22 -0
- package/client/translator.ts +25 -11
- package/package.json +1 -1
- package/server/akanApp.ts +1 -1
- package/server/artifact/routeSeedIndexStore.ts +34 -0
- package/server/routeElementComposer.tsx +101 -1
- package/server/routeTreeBuilder.ts +82 -1
- package/server/rscClient.tsx +9 -1
- package/server/rscWorker.tsx +308 -66
- package/server/rscWorkerHost.ts +10 -5
- package/server/systemPages.tsx +165 -0
- package/server/webRouter.ts +116 -18
- package/service/predefinedAdaptor/database.adaptor.ts +2 -0
- package/service/predefinedAdaptor/solidSqlite.ts +1 -0
- package/service/predefinedAdaptor/sqlitePath.ts +4 -1
- package/service/predefinedAdaptor/storage.adaptor.ts +2 -2
- package/types/client/csrTypes.d.ts +21 -0
- package/types/client/translator.d.ts +1 -0
- package/types/server/artifact/routeSeedIndexStore.d.ts +1 -0
- package/types/server/routeElementComposer.d.ts +15 -1
- package/types/server/routeTreeBuilder.d.ts +6 -1
- package/types/server/rscWorkerHost.d.ts +1 -0
- package/types/server/systemPages.d.ts +27 -0
- package/types/service/predefinedAdaptor/sqlitePath.d.ts +2 -1
- package/types/ui/System/Client.d.ts +5 -9
- package/types/ui/System/Common.d.ts +2 -0
- package/types/ui/System/SSR.d.ts +2 -2
- package/ui/InfiniteScroll.tsx +0 -1
- package/ui/System/Client.tsx +78 -20
- package/ui/System/Common.tsx +11 -2
- package/ui/System/SSR.tsx +58 -11
- package/webkit/bootCsr.tsx +13 -2
package/base/baseEnv.ts
CHANGED
|
@@ -85,7 +85,8 @@ export const getEnv = (): ClientEnv => {
|
|
|
85
85
|
if (repoName === "unknown") throw new Error("environment variable AKAN_PUBLIC_REPO_NAME is required");
|
|
86
86
|
if (serveDomain === "unknown") throw new Error("environment variable AKAN_PUBLIC_SERVE_DOMAIN is required");
|
|
87
87
|
const environment = (process.env.AKAN_PUBLIC_ENV ?? "debug") as BaseEnv["environment"];
|
|
88
|
-
const operationMode = (process.env.AKAN_PUBLIC_OPERATION_MODE ??
|
|
88
|
+
const operationMode = (process.env.AKAN_PUBLIC_OPERATION_MODE ??
|
|
89
|
+
(environment === "local" ? "local" : "cloud")) as BaseEnv["operationMode"];
|
|
89
90
|
const tunnelUsername = process.env.SSH_TUNNEL_USERNAME ?? "root";
|
|
90
91
|
const tunnelPassword = process.env.SSH_TUNNEL_PASSWORD ?? repoName;
|
|
91
92
|
const baseEnv: BaseEnv = {
|
package/client/csrTypes.ts
CHANGED
|
@@ -51,6 +51,13 @@ export interface PageLoadingProps {
|
|
|
51
51
|
export interface LayoutLoadingProps extends PageLoadingProps {
|
|
52
52
|
children: ReactNode;
|
|
53
53
|
}
|
|
54
|
+
export interface LayoutNotFoundProps extends PageProps {
|
|
55
|
+
pathname: string;
|
|
56
|
+
}
|
|
57
|
+
export interface LayoutErrorProps extends LayoutNotFoundProps {
|
|
58
|
+
error?: unknown;
|
|
59
|
+
digest?: string;
|
|
60
|
+
}
|
|
54
61
|
export type Head = ReactNode;
|
|
55
62
|
export type GenerateHead = (props: PageProps) => PromiseOrObject<Head | null | undefined>;
|
|
56
63
|
export type ResolveHead = (props: PageProps) => PromiseOrObject<Head | null | undefined>;
|
|
@@ -59,9 +66,15 @@ export type PageRender = (props: PageProps) => PromiseOrObject<ReactNode>;
|
|
|
59
66
|
export type LayoutRender = (props: LayoutProps) => PromiseOrObject<ReactNode>;
|
|
60
67
|
export type PageLoadingRender = (props: PageLoadingProps) => PromiseOrObject<ReactNode>;
|
|
61
68
|
export type LayoutLoadingRender = (props: LayoutLoadingProps) => PromiseOrObject<ReactNode>;
|
|
69
|
+
export type LayoutNotFoundRender = (props: LayoutNotFoundProps) => PromiseOrObject<ReactNode>;
|
|
70
|
+
export type LayoutErrorRender = (props: LayoutErrorProps) => PromiseOrObject<ReactNode>;
|
|
62
71
|
export interface RouteRender {
|
|
63
72
|
render: LayoutRender | PageRender;
|
|
64
73
|
Loading?: LayoutLoadingRender | PageLoadingRender;
|
|
74
|
+
NotFound?: LayoutNotFoundRender;
|
|
75
|
+
Error?: LayoutErrorRender;
|
|
76
|
+
resolveNotFound?: () => PromiseOrObject<LayoutNotFoundRender | undefined>;
|
|
77
|
+
resolveError?: () => PromiseOrObject<LayoutErrorRender | undefined>;
|
|
65
78
|
resolveHead?: ResolveHead;
|
|
66
79
|
getPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
|
|
67
80
|
}
|
|
@@ -108,6 +121,8 @@ export interface LayoutModule {
|
|
|
108
121
|
layoutStyle?: "mobile" | "web";
|
|
109
122
|
gaTrackingId?: string;
|
|
110
123
|
Loading?: LayoutLoadingRender;
|
|
124
|
+
NotFound?: LayoutNotFoundRender;
|
|
125
|
+
Error?: LayoutErrorRender;
|
|
111
126
|
}
|
|
112
127
|
export type RouteModule = PageModule | LayoutModule;
|
|
113
128
|
export interface Route {
|
|
@@ -252,6 +267,13 @@ export interface PathRoute {
|
|
|
252
267
|
isSpecialRoute?: boolean;
|
|
253
268
|
}
|
|
254
269
|
|
|
270
|
+
export interface LayoutFallbackRoute {
|
|
271
|
+
path: string;
|
|
272
|
+
pathSegments: string[];
|
|
273
|
+
renderRootLayouts: RouteRender[];
|
|
274
|
+
renderLayouts: RouteRender[];
|
|
275
|
+
}
|
|
276
|
+
|
|
255
277
|
export interface RouteGuide {
|
|
256
278
|
pathSegment: string;
|
|
257
279
|
pathRoute?: PathRoute;
|
package/client/translator.ts
CHANGED
|
@@ -11,7 +11,9 @@ export interface AllDictionary {
|
|
|
11
11
|
|
|
12
12
|
export class Translator {
|
|
13
13
|
static #langDictionaryMap = new Map<string, Dictionary>();
|
|
14
|
-
constructor(
|
|
14
|
+
constructor(
|
|
15
|
+
dictionary: Record<string, Record<string, Record<string, unknown>>>,
|
|
16
|
+
) {
|
|
15
17
|
Object.entries(dictionary).forEach(([lang, dictionary]) => {
|
|
16
18
|
this.#setDictionary(lang, dictionary);
|
|
17
19
|
});
|
|
@@ -19,25 +21,37 @@ export class Translator {
|
|
|
19
21
|
hasDictionary(lang: string) {
|
|
20
22
|
return Translator.#langDictionaryMap.has(lang);
|
|
21
23
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
static seed(lang: string, dict: Dictionary | undefined) {
|
|
26
|
+
if (!dict) return;
|
|
27
|
+
const existingDictionary = Translator.#langDictionaryMap.get(lang) ?? {};
|
|
25
28
|
Object.entries(dict).forEach(([key, modelDict]) => {
|
|
26
|
-
if (
|
|
27
|
-
|
|
29
|
+
if (existingDictionary[key])
|
|
30
|
+
Object.assign(existingDictionary[key], modelDict);
|
|
31
|
+
else existingDictionary[key] = modelDict as Dictionary[string];
|
|
28
32
|
});
|
|
29
|
-
Translator.#langDictionaryMap.set(lang,
|
|
30
|
-
|
|
33
|
+
Translator.#langDictionaryMap.set(lang, existingDictionary);
|
|
34
|
+
}
|
|
35
|
+
#setDictionary(lang: string, dict: Dictionary) {
|
|
36
|
+
Translator.seed(lang, dict);
|
|
37
|
+
return Translator.#langDictionaryMap.get(lang) as Dictionary;
|
|
31
38
|
}
|
|
32
|
-
translate(
|
|
39
|
+
translate(
|
|
40
|
+
lang: string,
|
|
41
|
+
key: string,
|
|
42
|
+
param?: Record<string, string | number>,
|
|
43
|
+
): string {
|
|
33
44
|
const dictionary = Translator.#langDictionaryMap.get(lang);
|
|
34
45
|
if (!dictionary) return key;
|
|
35
46
|
const msg = (pathGet(key, dictionary, ".", { t: key }) as { t: string }).t;
|
|
36
|
-
return param
|
|
47
|
+
return param
|
|
48
|
+
? msg.replace(/{([^}]+)}/g, (_, key: string) => param[key] as string)
|
|
49
|
+
: msg;
|
|
37
50
|
}
|
|
38
51
|
async getDictionary(lang: string) {
|
|
39
52
|
const dictionary = Translator.#langDictionaryMap.get(lang);
|
|
40
|
-
if (!dictionary)
|
|
53
|
+
if (!dictionary)
|
|
54
|
+
throw new Error(`Dictionary for language ${lang} not found`);
|
|
41
55
|
return dictionary;
|
|
42
56
|
}
|
|
43
57
|
}
|
package/package.json
CHANGED
package/server/akanApp.ts
CHANGED
|
@@ -395,7 +395,7 @@ export class AkanApp {
|
|
|
395
395
|
data: {} as GatewayWsData,
|
|
396
396
|
},
|
|
397
397
|
});
|
|
398
|
-
this.logger.info(`AkanApp gateway is running on port
|
|
398
|
+
this.logger.info(`AkanApp gateway is running on port http://localhost:${this.#port}`);
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
async #handleFetch(req: Request, server: Bun.Server<GatewayWsData>): Promise<Response | undefined> {
|
|
@@ -82,6 +82,40 @@ export class RouteSeedIndexStore {
|
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
static matchPrefix(pathname: string, entries: RouteSeedEntry[]): MatchedRoute | null {
|
|
86
|
+
const candidates = entries
|
|
87
|
+
.map((entry) => ({
|
|
88
|
+
entry,
|
|
89
|
+
params: RouteSeedIndexStore.#matchRoutePrefix(entry.pattern ?? entry.routeId, pathname),
|
|
90
|
+
}))
|
|
91
|
+
.filter((entry): entry is { entry: RouteSeedEntry; params: Record<string, string> } => Boolean(entry.params))
|
|
92
|
+
.sort((a, b) => {
|
|
93
|
+
const lengthDelta =
|
|
94
|
+
b.entry.pattern.split("/").filter(Boolean).length - a.entry.pattern.split("/").filter(Boolean).length;
|
|
95
|
+
if (lengthDelta !== 0) return lengthDelta;
|
|
96
|
+
return a.entry.pattern < b.entry.pattern ? -1 : a.entry.pattern > b.entry.pattern ? 1 : 0;
|
|
97
|
+
});
|
|
98
|
+
return candidates[0] ?? null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static #matchRoutePrefix(pattern: string, pathname: string): Record<string, string> | null {
|
|
102
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
103
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
104
|
+
if (patternParts.length > pathParts.length) return null;
|
|
105
|
+
const params: Record<string, string> = {};
|
|
106
|
+
for (let index = 0; index < patternParts.length; index++) {
|
|
107
|
+
const patternPart = patternParts[index];
|
|
108
|
+
const pathPart = pathParts[index];
|
|
109
|
+
if (!patternPart || !pathPart) return null;
|
|
110
|
+
if (patternPart.startsWith(":")) {
|
|
111
|
+
params[patternPart.slice(1)] = decodeURIComponent(pathPart);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (patternPart !== pathPart) return null;
|
|
115
|
+
}
|
|
116
|
+
return params;
|
|
117
|
+
}
|
|
118
|
+
|
|
85
119
|
static #normalizeArtifactPath(artifactPath: string, artifactDir: string): string {
|
|
86
120
|
if (path.isAbsolute(artifactPath)) return artifactPath;
|
|
87
121
|
return path.resolve(artifactDir, artifactPath);
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Head,
|
|
3
|
+
LayoutErrorRender,
|
|
4
|
+
LayoutFallbackRoute,
|
|
5
|
+
LayoutNotFoundRender,
|
|
6
|
+
PathRoute,
|
|
7
|
+
RouteRender,
|
|
8
|
+
} from "akanjs/client";
|
|
2
9
|
import { Children, cloneElement, isValidElement, type ReactElement, type ReactNode, Suspense } from "react";
|
|
3
10
|
|
|
4
11
|
export class RouteElementComposer {
|
|
@@ -39,6 +46,71 @@ export class RouteElementComposer {
|
|
|
39
46
|
return pathRoute.resolveHead?.({ params, searchParams });
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
static async composeFallback({
|
|
50
|
+
kind,
|
|
51
|
+
route,
|
|
52
|
+
params,
|
|
53
|
+
searchParams,
|
|
54
|
+
pathname,
|
|
55
|
+
error,
|
|
56
|
+
digest,
|
|
57
|
+
}: {
|
|
58
|
+
kind: "not-found" | "error";
|
|
59
|
+
route: PathRoute | LayoutFallbackRoute;
|
|
60
|
+
params: Record<string, string>;
|
|
61
|
+
searchParams: Record<string, string | string[]>;
|
|
62
|
+
pathname: string;
|
|
63
|
+
error?: unknown;
|
|
64
|
+
digest?: string;
|
|
65
|
+
}): Promise<ReactNode | null> {
|
|
66
|
+
const layoutStack = [...route.renderRootLayouts, ...route.renderLayouts];
|
|
67
|
+
for (let index = layoutStack.length - 1; index >= 0; index--) {
|
|
68
|
+
const layoutRender = layoutStack[index];
|
|
69
|
+
if (!layoutRender) continue;
|
|
70
|
+
const fallback =
|
|
71
|
+
kind === "not-found" ? await layoutRender.resolveNotFound?.() : await layoutRender.resolveError?.();
|
|
72
|
+
if (!fallback) continue;
|
|
73
|
+
const renders = [
|
|
74
|
+
...layoutStack.slice(0, index + 1),
|
|
75
|
+
RouteElementComposer.#makeFallbackRouteRender({
|
|
76
|
+
kind,
|
|
77
|
+
fallback,
|
|
78
|
+
params,
|
|
79
|
+
searchParams,
|
|
80
|
+
pathname,
|
|
81
|
+
error,
|
|
82
|
+
digest,
|
|
83
|
+
}),
|
|
84
|
+
];
|
|
85
|
+
return RouteElementComposer.composeRenders({ renders, params, searchParams });
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static composeRenders({
|
|
91
|
+
renders,
|
|
92
|
+
params,
|
|
93
|
+
searchParams,
|
|
94
|
+
}: {
|
|
95
|
+
renders: RouteRender[];
|
|
96
|
+
params: Record<string, string>;
|
|
97
|
+
searchParams: Record<string, string | string[]>;
|
|
98
|
+
}): ReactNode {
|
|
99
|
+
let element: ReactNode = null;
|
|
100
|
+
for (let i = renders.length - 1; i >= 0; i--) {
|
|
101
|
+
const routeRender = renders[i];
|
|
102
|
+
if (!routeRender) continue;
|
|
103
|
+
element = (
|
|
104
|
+
<Suspense fallback={RouteElementComposer.#composeLoadingFallback(renders.slice(i), params)}>
|
|
105
|
+
<RouteElementComposer.AsyncRender routeRender={routeRender} params={params} searchParams={searchParams}>
|
|
106
|
+
{element}
|
|
107
|
+
</RouteElementComposer.AsyncRender>
|
|
108
|
+
</Suspense>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return element;
|
|
112
|
+
}
|
|
113
|
+
|
|
42
114
|
static async renderAsync({
|
|
43
115
|
routeRender,
|
|
44
116
|
children,
|
|
@@ -61,6 +133,34 @@ export class RouteElementComposer {
|
|
|
61
133
|
searchParams: Record<string, string | string[]>;
|
|
62
134
|
}) => RouteElementComposer.renderAsync(props);
|
|
63
135
|
|
|
136
|
+
static #makeFallbackRouteRender({
|
|
137
|
+
kind,
|
|
138
|
+
fallback,
|
|
139
|
+
pathname,
|
|
140
|
+
error,
|
|
141
|
+
digest,
|
|
142
|
+
}: {
|
|
143
|
+
kind: "not-found" | "error";
|
|
144
|
+
fallback: LayoutNotFoundRender | LayoutErrorRender;
|
|
145
|
+
params: Record<string, string>;
|
|
146
|
+
searchParams: Record<string, string | string[]>;
|
|
147
|
+
pathname: string;
|
|
148
|
+
error?: unknown;
|
|
149
|
+
digest?: string;
|
|
150
|
+
}): RouteRender {
|
|
151
|
+
return {
|
|
152
|
+
render: (props: { params: Record<string, string>; searchParams: Record<string, string | string[]> }) => {
|
|
153
|
+
const { params, searchParams } = props as {
|
|
154
|
+
params: Record<string, string>;
|
|
155
|
+
searchParams: Record<string, string | string[]>;
|
|
156
|
+
};
|
|
157
|
+
return kind === "not-found"
|
|
158
|
+
? (fallback as LayoutNotFoundRender)({ params, searchParams, pathname })
|
|
159
|
+
: (fallback as LayoutErrorRender)({ params, searchParams, pathname, error, digest });
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
64
164
|
static #normalizeReactNode(node: ReactNode): ReactNode {
|
|
65
165
|
if (Array.isArray(node)) return Children.toArray(node).map(RouteElementComposer.#normalizeReactNode);
|
|
66
166
|
if (!isValidElement(node)) return node;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Head,
|
|
3
|
+
LayoutFallbackRoute,
|
|
4
|
+
LayoutModule,
|
|
3
5
|
LayoutProps,
|
|
4
6
|
PageProps,
|
|
5
7
|
PageState,
|
|
@@ -54,8 +56,10 @@ export class RouteTreeBuilder {
|
|
|
54
56
|
"layoutStyle",
|
|
55
57
|
"gaTrackingId",
|
|
56
58
|
"Loading",
|
|
59
|
+
"NotFound",
|
|
60
|
+
"Error",
|
|
57
61
|
]);
|
|
58
|
-
static readonly #layoutRouteExports = new Set(["default", "head", "generateHead", "Loading"]);
|
|
62
|
+
static readonly #layoutRouteExports = new Set(["default", "head", "generateHead", "Loading", "NotFound", "Error"]);
|
|
59
63
|
static readonly #moduleCacheStats: RouteModuleCacheStats = {
|
|
60
64
|
moduleCount: 0,
|
|
61
65
|
loadedModuleCount: 0,
|
|
@@ -69,6 +73,7 @@ export class RouteTreeBuilder {
|
|
|
69
73
|
readonly #baseLayoutPaths: string[];
|
|
70
74
|
readonly #routeMap = new Map<string, Route>();
|
|
71
75
|
readonly #pagePatterns: { key: string; pattern: string }[] = [];
|
|
76
|
+
readonly #fallbackRoutes: LayoutFallbackRoute[] = [];
|
|
72
77
|
|
|
73
78
|
constructor(context: PagesContext) {
|
|
74
79
|
this.#context = context;
|
|
@@ -79,6 +84,7 @@ export class RouteTreeBuilder {
|
|
|
79
84
|
|
|
80
85
|
build(): PathRoute[] {
|
|
81
86
|
RouteTreeBuilder.resetCacheStats();
|
|
87
|
+
this.#fallbackRoutes.length = 0;
|
|
82
88
|
for (const [filePath, loader] of Object.entries(this.#context)) this.#addRouteModule(filePath, loader);
|
|
83
89
|
assertUniqueRoutePatterns(this.#pagePatterns);
|
|
84
90
|
|
|
@@ -87,6 +93,10 @@ export class RouteTreeBuilder {
|
|
|
87
93
|
return this.#getPathRoutes(rootRoute).sort((a, b) => compareRouteSpecificity(a.path, b.path));
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
getFallbackRoutes(): LayoutFallbackRoute[] {
|
|
97
|
+
return [...this.#fallbackRoutes].sort((a, b) => compareRouteSpecificity(a.path, b.path));
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
static getCacheStats(): RouteModuleCacheStats {
|
|
91
101
|
return {
|
|
92
102
|
...RouteTreeBuilder.#moduleCacheStats,
|
|
@@ -115,6 +125,28 @@ export class RouteTreeBuilder {
|
|
|
115
125
|
return null;
|
|
116
126
|
}
|
|
117
127
|
|
|
128
|
+
static matchFallback(
|
|
129
|
+
pathname: string,
|
|
130
|
+
fallbackRoutes: LayoutFallbackRoute[],
|
|
131
|
+
): { fallbackRoute: LayoutFallbackRoute; params: Record<string, string> } | null {
|
|
132
|
+
const candidates = fallbackRoutes
|
|
133
|
+
.map((fallbackRoute) => ({
|
|
134
|
+
fallbackRoute,
|
|
135
|
+
params: RouteTreeBuilder.#matchRoutePrefix(fallbackRoute.path, pathname),
|
|
136
|
+
}))
|
|
137
|
+
.filter((entry): entry is { fallbackRoute: LayoutFallbackRoute; params: Record<string, string> } =>
|
|
138
|
+
Boolean(entry.params),
|
|
139
|
+
)
|
|
140
|
+
.sort((a, b) => {
|
|
141
|
+
const lengthDelta =
|
|
142
|
+
b.fallbackRoute.path.split("/").filter(Boolean).length -
|
|
143
|
+
a.fallbackRoute.path.split("/").filter(Boolean).length;
|
|
144
|
+
if (lengthDelta !== 0) return lengthDelta;
|
|
145
|
+
return compareRouteSpecificity(a.fallbackRoute.path, b.fallbackRoute.path);
|
|
146
|
+
});
|
|
147
|
+
return candidates[0] ?? null;
|
|
148
|
+
}
|
|
149
|
+
|
|
118
150
|
static parseSearchParams(search: string): Record<string, string | string[]> {
|
|
119
151
|
const result: Record<string, string | string[]> = {};
|
|
120
152
|
const urlSearchParams = new URLSearchParams(search);
|
|
@@ -171,6 +203,14 @@ export class RouteTreeBuilder {
|
|
|
171
203
|
const currentLayout = !isRoot && route.renderLayout ? route.renderLayout : null;
|
|
172
204
|
const renderRootLayouts = [...parentRootLayouts, ...(currentRootLayout ? [currentRootLayout] : [])];
|
|
173
205
|
const renderLayouts = [...parentLayouts, ...(currentLayout ? [currentLayout] : [])];
|
|
206
|
+
if (route.renderLayout) {
|
|
207
|
+
this.#fallbackRoutes.push({
|
|
208
|
+
path: routePath,
|
|
209
|
+
pathSegments,
|
|
210
|
+
renderRootLayouts,
|
|
211
|
+
renderLayouts,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
174
214
|
const routeHead = RouteTreeBuilder.#composeHeadResolvers(route.renderLayout?.resolveHead, parentHead);
|
|
175
215
|
const pageRenderRootLayouts =
|
|
176
216
|
route.pageIncludesOwnLayout === false && currentRootLayout ? parentRootLayouts : renderRootLayouts;
|
|
@@ -246,12 +286,22 @@ export class RouteTreeBuilder {
|
|
|
246
286
|
render: async (props: LayoutProps | PageProps) => {
|
|
247
287
|
const mod = await loadModule();
|
|
248
288
|
routeRender.Loading = mod.Loading as never;
|
|
289
|
+
if (kind === "layout") {
|
|
290
|
+
const layoutMod = mod as LayoutModule;
|
|
291
|
+
routeRender.NotFound = layoutMod.NotFound;
|
|
292
|
+
routeRender.Error = layoutMod.Error;
|
|
293
|
+
}
|
|
249
294
|
if (!mod.default) throw new Error(`[route-convention] ${key} has no default export`);
|
|
250
295
|
return mod.default(props as never);
|
|
251
296
|
},
|
|
252
297
|
resolveHead: async (props: PageProps) => {
|
|
253
298
|
const mod = await loadModule();
|
|
254
299
|
routeRender.Loading = mod.Loading as never;
|
|
300
|
+
if (kind === "layout") {
|
|
301
|
+
const layoutMod = mod as LayoutModule;
|
|
302
|
+
routeRender.NotFound = layoutMod.NotFound;
|
|
303
|
+
routeRender.Error = layoutMod.Error;
|
|
304
|
+
}
|
|
255
305
|
if (mod.generateHead) return mod.generateHead(props);
|
|
256
306
|
return mod.head as Head | null | undefined;
|
|
257
307
|
},
|
|
@@ -261,6 +311,19 @@ export class RouteTreeBuilder {
|
|
|
261
311
|
const mod = await loadModule();
|
|
262
312
|
return "pageConfig" in mod ? mod.pageConfig : undefined;
|
|
263
313
|
};
|
|
314
|
+
} else {
|
|
315
|
+
routeRender.resolveNotFound = async () => {
|
|
316
|
+
const mod = (await loadModule()) as LayoutModule;
|
|
317
|
+
routeRender.NotFound = mod.NotFound;
|
|
318
|
+
routeRender.Error = mod.Error;
|
|
319
|
+
return mod.NotFound;
|
|
320
|
+
};
|
|
321
|
+
routeRender.resolveError = async () => {
|
|
322
|
+
const mod = (await loadModule()) as LayoutModule;
|
|
323
|
+
routeRender.NotFound = mod.NotFound;
|
|
324
|
+
routeRender.Error = mod.Error;
|
|
325
|
+
return mod.Error;
|
|
326
|
+
};
|
|
264
327
|
}
|
|
265
328
|
return routeRender;
|
|
266
329
|
}
|
|
@@ -276,4 +339,22 @@ export class RouteTreeBuilder {
|
|
|
276
339
|
return undefined;
|
|
277
340
|
};
|
|
278
341
|
}
|
|
342
|
+
|
|
343
|
+
static #matchRoutePrefix(pattern: string, pathname: string): Record<string, string> | null {
|
|
344
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
345
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
346
|
+
if (patternParts.length > pathParts.length) return null;
|
|
347
|
+
const params: Record<string, string> = {};
|
|
348
|
+
for (let index = 0; index < patternParts.length; index++) {
|
|
349
|
+
const patternPart = patternParts[index];
|
|
350
|
+
const pathPart = pathParts[index];
|
|
351
|
+
if (!patternPart || !pathPart) return null;
|
|
352
|
+
if (patternPart.startsWith(":")) {
|
|
353
|
+
params[patternPart.slice(1)] = decodeURIComponent(pathPart);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (patternPart !== pathPart) return null;
|
|
357
|
+
}
|
|
358
|
+
return params;
|
|
359
|
+
}
|
|
279
360
|
}
|
package/server/rscClient.tsx
CHANGED
|
@@ -67,7 +67,15 @@ async function fetchRsc(href: string, options: { buildId?: number } = {}): Promi
|
|
|
67
67
|
return { type: "redirected" };
|
|
68
68
|
}
|
|
69
69
|
if (!res.ok || !res.body) throw new Error(`[rscClient] RSC fetch failed ${res.status} ${res.statusText}`);
|
|
70
|
-
|
|
70
|
+
|
|
71
|
+
const buffer = await res.arrayBuffer();
|
|
72
|
+
const completeStream = new ReadableStream<Uint8Array>({
|
|
73
|
+
start(controller) {
|
|
74
|
+
controller.enqueue(new Uint8Array(buffer));
|
|
75
|
+
controller.close();
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
return { type: "rsc", thenable: createRscThenable(completeStream) };
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
const rscCache = new Map<string, RscThenable>();
|