react-bun-ssr 0.3.2 → 0.4.1
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/README.md +39 -5
- package/framework/cli/dev-runtime.ts +18 -1
- package/framework/runtime/action-stub.ts +26 -0
- package/framework/runtime/client-runtime.tsx +66 -182
- package/framework/runtime/helpers.ts +75 -1
- package/framework/runtime/index.ts +53 -23
- package/framework/runtime/module-loader.ts +197 -35
- package/framework/runtime/render.tsx +1 -1
- package/framework/runtime/request-executor.ts +1705 -0
- package/framework/runtime/response-context.ts +206 -0
- package/framework/runtime/route-api.ts +51 -18
- package/framework/runtime/route-scanner.ts +104 -12
- package/framework/runtime/route-wire-protocol.ts +486 -0
- package/framework/runtime/server.ts +8 -1295
- package/framework/runtime/tree.tsx +45 -4
- package/framework/runtime/types.ts +71 -10
- package/package.json +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { ResponseContext, ResponseCookieOptions, ResponseCookies } from "./types";
|
|
2
|
+
|
|
3
|
+
type HeaderOperation =
|
|
4
|
+
| { type: "set"; name: string; value: string }
|
|
5
|
+
| { type: "append"; name: string; value: string }
|
|
6
|
+
| { type: "delete"; name: string };
|
|
7
|
+
|
|
8
|
+
interface ResponseContextState {
|
|
9
|
+
requestCookies: Map<string, string>;
|
|
10
|
+
pendingCookies: Map<string, string | undefined>;
|
|
11
|
+
headerOperations: HeaderOperation[];
|
|
12
|
+
headerPreview: Headers;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const responseContextState = new WeakMap<ResponseContext, ResponseContextState>();
|
|
16
|
+
|
|
17
|
+
function toHeaderName(name: string): string {
|
|
18
|
+
return String(name).trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toCookieDate(value: Date | string): string {
|
|
22
|
+
if (value instanceof Date) {
|
|
23
|
+
return value.toUTCString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parsed = new Date(value);
|
|
27
|
+
if (!Number.isNaN(parsed.valueOf())) {
|
|
28
|
+
return parsed.toUTCString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeSameSite(value: ResponseCookieOptions["sameSite"]): "Strict" | "Lax" | "None" | null {
|
|
35
|
+
if (!value) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const lowered = value.toLowerCase();
|
|
40
|
+
if (lowered === "strict") {
|
|
41
|
+
return "Strict";
|
|
42
|
+
}
|
|
43
|
+
if (lowered === "lax") {
|
|
44
|
+
return "Lax";
|
|
45
|
+
}
|
|
46
|
+
if (lowered === "none") {
|
|
47
|
+
return "None";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function serializeCookie(name: string, value: string, options: ResponseCookieOptions = {}): string {
|
|
54
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
55
|
+
|
|
56
|
+
if (options.path) {
|
|
57
|
+
parts.push(`Path=${options.path}`);
|
|
58
|
+
}
|
|
59
|
+
if (options.domain) {
|
|
60
|
+
parts.push(`Domain=${options.domain}`);
|
|
61
|
+
}
|
|
62
|
+
if (typeof options.maxAge === "number" && Number.isFinite(options.maxAge)) {
|
|
63
|
+
parts.push(`Max-Age=${Math.trunc(options.maxAge)}`);
|
|
64
|
+
}
|
|
65
|
+
if (options.expires) {
|
|
66
|
+
parts.push(`Expires=${toCookieDate(options.expires)}`);
|
|
67
|
+
}
|
|
68
|
+
if (options.httpOnly) {
|
|
69
|
+
parts.push("HttpOnly");
|
|
70
|
+
}
|
|
71
|
+
if (options.secure) {
|
|
72
|
+
parts.push("Secure");
|
|
73
|
+
}
|
|
74
|
+
const sameSite = normalizeSameSite(options.sameSite);
|
|
75
|
+
if (sameSite) {
|
|
76
|
+
parts.push(`SameSite=${sameSite}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return parts.join("; ");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function serializeDeleteCookie(
|
|
83
|
+
name: string,
|
|
84
|
+
options: Omit<ResponseCookieOptions, "expires" | "maxAge"> = {},
|
|
85
|
+
): string {
|
|
86
|
+
return serializeCookie(name, "", {
|
|
87
|
+
...options,
|
|
88
|
+
maxAge: 0,
|
|
89
|
+
expires: new Date(0),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createMutableHeaders(state: ResponseContextState): ResponseContext["headers"] {
|
|
94
|
+
return {
|
|
95
|
+
set(name, value) {
|
|
96
|
+
const normalizedName = toHeaderName(name);
|
|
97
|
+
state.headerOperations.push({
|
|
98
|
+
type: "set",
|
|
99
|
+
name: normalizedName,
|
|
100
|
+
value: String(value),
|
|
101
|
+
});
|
|
102
|
+
state.headerPreview.set(normalizedName, String(value));
|
|
103
|
+
},
|
|
104
|
+
append(name, value) {
|
|
105
|
+
const normalizedName = toHeaderName(name);
|
|
106
|
+
state.headerOperations.push({
|
|
107
|
+
type: "append",
|
|
108
|
+
name: normalizedName,
|
|
109
|
+
value: String(value),
|
|
110
|
+
});
|
|
111
|
+
state.headerPreview.append(normalizedName, String(value));
|
|
112
|
+
},
|
|
113
|
+
delete(name) {
|
|
114
|
+
const normalizedName = toHeaderName(name);
|
|
115
|
+
state.headerOperations.push({
|
|
116
|
+
type: "delete",
|
|
117
|
+
name: normalizedName,
|
|
118
|
+
});
|
|
119
|
+
state.headerPreview.delete(normalizedName);
|
|
120
|
+
},
|
|
121
|
+
get(name) {
|
|
122
|
+
return state.headerPreview.get(toHeaderName(name));
|
|
123
|
+
},
|
|
124
|
+
has(name) {
|
|
125
|
+
return state.headerPreview.has(toHeaderName(name));
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class MutableResponseCookies implements ResponseCookies {
|
|
131
|
+
constructor(private readonly state: ResponseContextState) {}
|
|
132
|
+
|
|
133
|
+
get(name: string): string | undefined {
|
|
134
|
+
if (this.state.pendingCookies.has(name)) {
|
|
135
|
+
return this.state.pendingCookies.get(name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return this.state.requestCookies.get(name);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
set(name: string, value: string, options: ResponseCookieOptions = {}): void {
|
|
142
|
+
const serialized = serializeCookie(name, value, options);
|
|
143
|
+
this.state.pendingCookies.set(name, value);
|
|
144
|
+
this.state.headerOperations.push({
|
|
145
|
+
type: "append",
|
|
146
|
+
name: "set-cookie",
|
|
147
|
+
value: serialized,
|
|
148
|
+
});
|
|
149
|
+
this.state.headerPreview.append("set-cookie", serialized);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
delete(name: string, options: Omit<ResponseCookieOptions, "expires" | "maxAge"> = {}): void {
|
|
153
|
+
const serialized = serializeDeleteCookie(name, options);
|
|
154
|
+
this.state.pendingCookies.set(name, undefined);
|
|
155
|
+
this.state.headerOperations.push({
|
|
156
|
+
type: "append",
|
|
157
|
+
name: "set-cookie",
|
|
158
|
+
value: serialized,
|
|
159
|
+
});
|
|
160
|
+
this.state.headerPreview.append("set-cookie", serialized);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function createResponseContext(requestCookies: Map<string, string>): ResponseContext {
|
|
165
|
+
const state: ResponseContextState = {
|
|
166
|
+
requestCookies,
|
|
167
|
+
pendingCookies: new Map(),
|
|
168
|
+
headerOperations: [],
|
|
169
|
+
headerPreview: new Headers(),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const context: ResponseContext = {
|
|
173
|
+
headers: createMutableHeaders(state),
|
|
174
|
+
cookies: new MutableResponseCookies(state),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
responseContextState.set(context, state);
|
|
178
|
+
return context;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function applyResponseContext(response: Response, context: ResponseContext): Response {
|
|
182
|
+
const state = responseContextState.get(context);
|
|
183
|
+
if (!state || state.headerOperations.length === 0) {
|
|
184
|
+
return response;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const headers = new Headers(response.headers);
|
|
188
|
+
for (const operation of state.headerOperations) {
|
|
189
|
+
if (operation.type === "set") {
|
|
190
|
+
headers.set(operation.name, operation.value);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (operation.type === "append") {
|
|
194
|
+
headers.append(operation.name, operation.value);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
headers.delete(operation.name);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return new Response(response.body, {
|
|
202
|
+
status: response.status,
|
|
203
|
+
statusText: response.statusText,
|
|
204
|
+
headers,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
@@ -1,23 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import type { AppRouteLocals as RootAppRouteLocals } from "react-bun-ssr";
|
|
2
|
+
import type {
|
|
3
|
+
Action as RuntimeAction,
|
|
4
|
+
ActionContext as RuntimeActionContext,
|
|
5
|
+
ActionResult as RuntimeActionResult,
|
|
6
|
+
DeferredLoaderResult as RuntimeDeferredLoaderResult,
|
|
7
|
+
DeferredToken as RuntimeDeferredToken,
|
|
8
|
+
Loader as RuntimeLoader,
|
|
9
|
+
LoaderContext as RuntimeLoaderContext,
|
|
10
|
+
LoaderResult as RuntimeLoaderResult,
|
|
11
|
+
Middleware as RuntimeMiddleware,
|
|
12
|
+
Params as RuntimeParams,
|
|
13
|
+
RedirectResult as RuntimeRedirectResult,
|
|
14
|
+
RequestContext as RuntimeRequestContext,
|
|
15
|
+
ResponseContext as RuntimeResponseContext,
|
|
16
|
+
ResponseCookies as RuntimeResponseCookies,
|
|
17
|
+
ResponseCookieOptions as RuntimeResponseCookieOptions,
|
|
18
|
+
RouteCatchContext as RuntimeRouteCatchContext,
|
|
19
|
+
RouteErrorContext as RuntimeRouteErrorContext,
|
|
20
|
+
RouteErrorResponse as RuntimeRouteErrorResponse,
|
|
17
21
|
} from "./types";
|
|
18
22
|
|
|
19
|
-
export
|
|
23
|
+
export interface AppRouteLocals extends RootAppRouteLocals, Record<string, unknown> {}
|
|
24
|
+
|
|
25
|
+
export type Action = RuntimeAction<AppRouteLocals>;
|
|
26
|
+
export type ActionContext = RuntimeActionContext<AppRouteLocals>;
|
|
27
|
+
export type ActionResult = RuntimeActionResult;
|
|
28
|
+
export type DeferredLoaderResult = RuntimeDeferredLoaderResult;
|
|
29
|
+
export type DeferredToken = RuntimeDeferredToken;
|
|
30
|
+
export type Loader = RuntimeLoader<AppRouteLocals>;
|
|
31
|
+
export type LoaderContext = RuntimeLoaderContext<AppRouteLocals>;
|
|
32
|
+
export type LoaderResult = RuntimeLoaderResult;
|
|
33
|
+
export type Middleware = RuntimeMiddleware<AppRouteLocals>;
|
|
34
|
+
export type Params = RuntimeParams;
|
|
35
|
+
export type RedirectResult = RuntimeRedirectResult;
|
|
36
|
+
export type RequestContext = RuntimeRequestContext<AppRouteLocals>;
|
|
37
|
+
export type ResponseContext = RuntimeResponseContext;
|
|
38
|
+
export type ResponseCookies = RuntimeResponseCookies;
|
|
39
|
+
export type ResponseCookieOptions = RuntimeResponseCookieOptions;
|
|
40
|
+
export type RouteCatchContext = RuntimeRouteCatchContext;
|
|
41
|
+
export type RouteErrorContext = RuntimeRouteErrorContext;
|
|
42
|
+
export type RouteErrorResponse = RuntimeRouteErrorResponse;
|
|
43
|
+
|
|
44
|
+
export { assertSameOriginAction, defer, json, redirect, sanitizeRedirectTarget } from "./helpers";
|
|
20
45
|
export { isRouteErrorResponse, notFound, routeError } from "./route-errors";
|
|
21
46
|
export { Link, type LinkProps } from "./link";
|
|
22
47
|
export { useRouter, type Router, type RouterNavigateInfo, type RouterNavigateListener, type RouterNavigateOptions } from "./router";
|
|
23
|
-
export {
|
|
48
|
+
export {
|
|
49
|
+
Outlet,
|
|
50
|
+
createRouteAction,
|
|
51
|
+
useLoaderData,
|
|
52
|
+
useParams,
|
|
53
|
+
useRequestUrl,
|
|
54
|
+
useRouteAction,
|
|
55
|
+
useRouteError,
|
|
56
|
+
} from "./tree";
|
|
@@ -21,6 +21,7 @@ const LAYOUT_FILE_RE = /\.(tsx|jsx|ts|js)$/;
|
|
|
21
21
|
const API_FILE_RE = /\.(ts|js|tsx|jsx)$/;
|
|
22
22
|
const MD_FILE_RE = /\.md$/;
|
|
23
23
|
const MDX_FILE_RE = /\.mdx$/;
|
|
24
|
+
const SERVER_SUFFIX_RE = /\.server$/;
|
|
24
25
|
|
|
25
26
|
async function walkFiles(rootDir: string): Promise<string[]> {
|
|
26
27
|
if (!(await existsPath(rootDir))) {
|
|
@@ -104,6 +105,23 @@ function getAncestorDirs(relativeDir: string): string[] {
|
|
|
104
105
|
return result;
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
function stripServerSuffix(value: string): string {
|
|
109
|
+
return value.replace(SERVER_SUFFIX_RE, "");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hasServerSuffix(value: string): boolean {
|
|
113
|
+
return SERVER_SUFFIX_RE.test(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function removeServerSuffixFromRelativePath(relativeFilePath: string): string {
|
|
117
|
+
const extension = path.extname(relativeFilePath);
|
|
118
|
+
if (!extension) {
|
|
119
|
+
return relativeFilePath;
|
|
120
|
+
}
|
|
121
|
+
const withoutExt = relativeFilePath.slice(0, -extension.length);
|
|
122
|
+
return `${stripServerSuffix(withoutExt)}${extension}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
107
125
|
export async function scanRoutes(
|
|
108
126
|
routesDir: string,
|
|
109
127
|
options: {
|
|
@@ -113,16 +131,21 @@ export async function scanRoutes(
|
|
|
113
131
|
const allFiles = (await walkFiles(routesDir)).sort((a, b) => a.localeCompare(b));
|
|
114
132
|
|
|
115
133
|
const layoutByDir = new Map<string, string>();
|
|
134
|
+
const layoutServerByDir = new Map<string, string>();
|
|
116
135
|
const middlewareByDir = new Map<string, string>();
|
|
136
|
+
const pageCompanionByKey = new Map<string, string>();
|
|
117
137
|
|
|
118
138
|
const pageRouteTasks: Array<Promise<PageRouteDefinition>> = [];
|
|
119
|
-
const
|
|
139
|
+
const pageRouteKeys = new Set<string>();
|
|
140
|
+
const apiRouteByKey = new Map<string, ApiRouteDefinition>();
|
|
120
141
|
|
|
121
142
|
for (const absoluteFilePath of allFiles) {
|
|
122
143
|
const relativeFilePath = normalizeSlashes(path.relative(routesDir, absoluteFilePath));
|
|
123
144
|
const relativeDir = normalizeSlashes(path.dirname(relativeFilePath) === "." ? "" : path.dirname(relativeFilePath));
|
|
124
145
|
const fileName = path.basename(relativeFilePath);
|
|
125
146
|
const fileBaseName = trimFileExtension(fileName);
|
|
147
|
+
const logicalBaseName = stripServerSuffix(fileBaseName);
|
|
148
|
+
const isServerVariant = hasServerSuffix(fileBaseName);
|
|
126
149
|
|
|
127
150
|
if (MDX_FILE_RE.test(fileName)) {
|
|
128
151
|
throw new Error(
|
|
@@ -130,13 +153,56 @@ export async function scanRoutes(
|
|
|
130
153
|
);
|
|
131
154
|
}
|
|
132
155
|
|
|
133
|
-
if (
|
|
134
|
-
|
|
156
|
+
if (logicalBaseName === "_layout" && LAYOUT_FILE_RE.test(fileName)) {
|
|
157
|
+
if (isServerVariant) {
|
|
158
|
+
if (layoutServerByDir.has(relativeDir)) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Duplicate layout companion route files in "${relativeDir || "/"}": ` +
|
|
161
|
+
`"${layoutServerByDir.get(relativeDir)!}" and "${absoluteFilePath}".`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
layoutServerByDir.set(relativeDir, absoluteFilePath);
|
|
165
|
+
} else {
|
|
166
|
+
layoutByDir.set(relativeDir, absoluteFilePath);
|
|
167
|
+
}
|
|
135
168
|
continue;
|
|
136
169
|
}
|
|
137
170
|
|
|
138
|
-
if (
|
|
171
|
+
if (logicalBaseName === "_middleware" && API_FILE_RE.test(fileName)) {
|
|
172
|
+
const existing = middlewareByDir.get(relativeDir);
|
|
173
|
+
if (existing && existing !== absoluteFilePath) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Middleware file collision in "${relativeDir || "/"}": ` +
|
|
176
|
+
`"${existing}" and "${absoluteFilePath}" both resolve to "_middleware".`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
139
179
|
middlewareByDir.set(relativeDir, absoluteFilePath);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (
|
|
184
|
+
isServerVariant
|
|
185
|
+
&& !relativeFilePath.startsWith("api/")
|
|
186
|
+
&& PAGE_FILE_RE.test(fileName)
|
|
187
|
+
&& !logicalBaseName.startsWith("_")
|
|
188
|
+
) {
|
|
189
|
+
const canonicalRelativeFilePath = removeServerSuffixFromRelativePath(relativeFilePath);
|
|
190
|
+
const routeKey = trimFileExtension(canonicalRelativeFilePath);
|
|
191
|
+
const existing = pageCompanionByKey.get(routeKey);
|
|
192
|
+
if (existing && existing !== absoluteFilePath) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Duplicate route companion files for "${routeKey}": "${existing}" and "${absoluteFilePath}".`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
pageCompanionByKey.set(routeKey, absoluteFilePath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const [relativeDir, layoutServerFilePath] of layoutServerByDir.entries()) {
|
|
202
|
+
if (!layoutByDir.has(relativeDir)) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Found layout companion "${layoutServerFilePath}" without base layout file "${relativeDir ? `${relativeDir}/` : ""}_layout.tsx".`,
|
|
205
|
+
);
|
|
140
206
|
}
|
|
141
207
|
}
|
|
142
208
|
|
|
@@ -145,10 +211,12 @@ export async function scanRoutes(
|
|
|
145
211
|
const relativeDir = normalizeSlashes(path.dirname(relativeFilePath) === "." ? "" : path.dirname(relativeFilePath));
|
|
146
212
|
const fileName = path.basename(relativeFilePath);
|
|
147
213
|
const fileBaseName = trimFileExtension(fileName);
|
|
214
|
+
const logicalBaseName = stripServerSuffix(fileBaseName);
|
|
215
|
+
const isServerVariant = hasServerSuffix(fileBaseName);
|
|
148
216
|
|
|
149
217
|
if (
|
|
150
|
-
(
|
|
151
|
-
|| (
|
|
218
|
+
(logicalBaseName === "_layout" && LAYOUT_FILE_RE.test(fileName))
|
|
219
|
+
|| (logicalBaseName === "_middleware" && API_FILE_RE.test(fileName))
|
|
152
220
|
) {
|
|
153
221
|
continue;
|
|
154
222
|
}
|
|
@@ -156,18 +224,31 @@ export async function scanRoutes(
|
|
|
156
224
|
const isApiRoute = relativeFilePath.startsWith("api/");
|
|
157
225
|
|
|
158
226
|
if (isApiRoute) {
|
|
159
|
-
if (!API_FILE_RE.test(fileName) ||
|
|
227
|
+
if (!API_FILE_RE.test(fileName) || logicalBaseName.startsWith("_")) {
|
|
160
228
|
continue;
|
|
161
229
|
}
|
|
162
230
|
|
|
163
|
-
const
|
|
231
|
+
const canonicalRelativeFilePath = isServerVariant
|
|
232
|
+
? removeServerSuffixFromRelativePath(relativeFilePath)
|
|
233
|
+
: relativeFilePath;
|
|
234
|
+
const withoutExt = trimFileExtension(canonicalRelativeFilePath);
|
|
235
|
+
if (apiRouteByKey.has(withoutExt)) {
|
|
236
|
+
const existing = apiRouteByKey.get(withoutExt)!;
|
|
237
|
+
throw new Error(
|
|
238
|
+
`API route collision for "${withoutExt}": "${existing.filePath}" and "${absoluteFilePath}". ` +
|
|
239
|
+
"Use only one of the plain route file or .server route file.",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
164
242
|
const shape = toUrlShape(withoutExt);
|
|
165
|
-
const
|
|
243
|
+
const canonicalRelativeDir = normalizeSlashes(
|
|
244
|
+
path.dirname(canonicalRelativeFilePath) === "." ? "" : path.dirname(canonicalRelativeFilePath),
|
|
245
|
+
);
|
|
246
|
+
const ancestors = getAncestorDirs(canonicalRelativeDir);
|
|
166
247
|
const middlewareFiles = ancestors
|
|
167
248
|
.map(dir => middlewareByDir.get(dir))
|
|
168
249
|
.filter((value): value is string => Boolean(value));
|
|
169
250
|
|
|
170
|
-
|
|
251
|
+
apiRouteByKey.set(withoutExt, {
|
|
171
252
|
type: "api",
|
|
172
253
|
id: toRouteId(withoutExt),
|
|
173
254
|
filePath: absoluteFilePath,
|
|
@@ -175,12 +256,12 @@ export async function scanRoutes(
|
|
|
175
256
|
segments: shape.segments,
|
|
176
257
|
score: getRouteScore(shape.segments),
|
|
177
258
|
middlewareFiles,
|
|
178
|
-
directory:
|
|
259
|
+
directory: canonicalRelativeDir,
|
|
179
260
|
});
|
|
180
261
|
continue;
|
|
181
262
|
}
|
|
182
263
|
|
|
183
|
-
if (!PAGE_FILE_RE.test(fileName) ||
|
|
264
|
+
if (!PAGE_FILE_RE.test(fileName) || logicalBaseName.startsWith("_") || isServerVariant) {
|
|
184
265
|
continue;
|
|
185
266
|
}
|
|
186
267
|
|
|
@@ -203,11 +284,13 @@ export async function scanRoutes(
|
|
|
203
284
|
.map(dir => middlewareByDir.get(dir))
|
|
204
285
|
.filter((value): value is string => Boolean(value));
|
|
205
286
|
|
|
287
|
+
pageRouteKeys.add(withoutExt);
|
|
206
288
|
pageRouteTasks.push(routeFilePath.then((resolvedRouteFilePath) => {
|
|
207
289
|
return {
|
|
208
290
|
type: "page",
|
|
209
291
|
id: toRouteId(withoutExt),
|
|
210
292
|
filePath: resolvedRouteFilePath,
|
|
293
|
+
serverFilePath: pageCompanionByKey.get(withoutExt),
|
|
211
294
|
routePath: shape.routePath,
|
|
212
295
|
segments: shape.segments,
|
|
213
296
|
score: getRouteScore(shape.segments),
|
|
@@ -218,7 +301,16 @@ export async function scanRoutes(
|
|
|
218
301
|
}));
|
|
219
302
|
}
|
|
220
303
|
|
|
304
|
+
for (const [routeKey, companionPath] of pageCompanionByKey.entries()) {
|
|
305
|
+
if (!pageRouteKeys.has(routeKey)) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Found route companion "${companionPath}" without base route module "${routeKey}.tsx".`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
221
312
|
const pageRoutes = await Promise.all(pageRouteTasks);
|
|
313
|
+
const apiRoutes = [...apiRouteByKey.values()];
|
|
222
314
|
|
|
223
315
|
return {
|
|
224
316
|
pages: sortRoutesBySpecificity(pageRoutes),
|