react-bun-ssr 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.
@@ -0,0 +1,242 @@
1
+ import path from "node:path";
2
+ import { compileMarkdownRouteModule } from "./markdown-routes";
3
+ import { existsPath, glob } from "./io";
4
+ import type {
5
+ ApiRouteDefinition,
6
+ PageRouteDefinition,
7
+ RouteManifest,
8
+ RouteSegment,
9
+ } from "./types";
10
+ import {
11
+ isRouteGroup,
12
+ normalizeSlashes,
13
+ routePathFromSegments,
14
+ toRouteId,
15
+ trimFileExtension,
16
+ } from "./utils";
17
+
18
+ const PAGE_FILE_RE = /\.(tsx|jsx|ts|js|md|mdx)$/;
19
+ const LAYOUT_FILE_RE = /\.(tsx|jsx|ts|js)$/;
20
+ const API_FILE_RE = /\.(ts|js|tsx|jsx)$/;
21
+ const MD_FILE_RE = /\.md$/;
22
+ const MDX_FILE_RE = /\.mdx$/;
23
+
24
+ async function walkFiles(rootDir: string): Promise<string[]> {
25
+ if (!(await existsPath(rootDir))) {
26
+ return [];
27
+ }
28
+
29
+ return glob("**/*", { cwd: rootDir, absolute: true });
30
+ }
31
+
32
+ function toUrlShape(relativePathWithoutExt: string): {
33
+ routePath: string;
34
+ segments: RouteSegment[];
35
+ } {
36
+ const fsSegments = normalizeSlashes(relativePathWithoutExt).split("/");
37
+ const urlSegments: string[] = [];
38
+ const segments: RouteSegment[] = [];
39
+
40
+ for (let index = 0; index < fsSegments.length; index += 1) {
41
+ const segment = fsSegments[index]!;
42
+
43
+ if (isRouteGroup(segment)) {
44
+ continue;
45
+ }
46
+
47
+ const isLast = index === fsSegments.length - 1;
48
+ if (isLast && segment === "index") {
49
+ continue;
50
+ }
51
+
52
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
53
+ const value = segment.slice(4, -1);
54
+ segments.push({ kind: "catchall", value });
55
+ urlSegments.push(`*${value}`);
56
+ continue;
57
+ }
58
+
59
+ if (segment.startsWith("[") && segment.endsWith("]")) {
60
+ const value = segment.slice(1, -1);
61
+ segments.push({ kind: "dynamic", value });
62
+ urlSegments.push(`:${value}`);
63
+ continue;
64
+ }
65
+
66
+ segments.push({ kind: "static", value: segment });
67
+ urlSegments.push(segment);
68
+ }
69
+
70
+ return {
71
+ routePath: routePathFromSegments(urlSegments),
72
+ segments,
73
+ };
74
+ }
75
+
76
+ function getRouteScore(segments: RouteSegment[]): number {
77
+ return segments.reduce((score, segment) => {
78
+ if (segment.kind === "static") {
79
+ return score + 30;
80
+ }
81
+ if (segment.kind === "dynamic") {
82
+ return score + 20;
83
+ }
84
+ return score + 1;
85
+ }, 0);
86
+ }
87
+
88
+ function getAncestorDirs(relativeDir: string): string[] {
89
+ const normalized = normalizeSlashes(relativeDir);
90
+ if (!normalized || normalized === ".") {
91
+ return [""];
92
+ }
93
+
94
+ const parts = normalized.split("/");
95
+ const result = [""];
96
+ let cursor = "";
97
+
98
+ for (const part of parts) {
99
+ cursor = cursor ? `${cursor}/${part}` : part;
100
+ result.push(cursor);
101
+ }
102
+
103
+ return result;
104
+ }
105
+
106
+ function sortRoutes<T extends { score: number; segments: RouteSegment[]; routePath: string }>(
107
+ routes: T[],
108
+ ): T[] {
109
+ return routes.sort((a, b) => {
110
+ if (a.score !== b.score) {
111
+ return b.score - a.score;
112
+ }
113
+
114
+ if (a.segments.length !== b.segments.length) {
115
+ return b.segments.length - a.segments.length;
116
+ }
117
+
118
+ return a.routePath.localeCompare(b.routePath);
119
+ });
120
+ }
121
+
122
+ export async function scanRoutes(
123
+ routesDir: string,
124
+ options: {
125
+ generatedMarkdownRootDir?: string;
126
+ } = {},
127
+ ): Promise<RouteManifest> {
128
+ const allFiles = (await walkFiles(routesDir)).sort((a, b) => a.localeCompare(b));
129
+
130
+ const layoutByDir = new Map<string, string>();
131
+ const middlewareByDir = new Map<string, string>();
132
+
133
+ const pageRouteTasks: Array<Promise<PageRouteDefinition>> = [];
134
+ const apiRoutes: ApiRouteDefinition[] = [];
135
+
136
+ for (const absoluteFilePath of allFiles) {
137
+ const relativeFilePath = normalizeSlashes(path.relative(routesDir, absoluteFilePath));
138
+ const relativeDir = normalizeSlashes(path.dirname(relativeFilePath) === "." ? "" : path.dirname(relativeFilePath));
139
+ const fileName = path.basename(relativeFilePath);
140
+ const fileBaseName = trimFileExtension(fileName);
141
+
142
+ if (MDX_FILE_RE.test(fileName)) {
143
+ throw new Error(
144
+ `Unsupported route file "${relativeFilePath}": .mdx route files are not supported yet; use .md or TSX route module.`,
145
+ );
146
+ }
147
+
148
+ if (fileBaseName === "_layout" && LAYOUT_FILE_RE.test(fileName)) {
149
+ layoutByDir.set(relativeDir, absoluteFilePath);
150
+ continue;
151
+ }
152
+
153
+ if (fileBaseName === "_middleware" && API_FILE_RE.test(fileName)) {
154
+ middlewareByDir.set(relativeDir, absoluteFilePath);
155
+ }
156
+ }
157
+
158
+ for (const absoluteFilePath of allFiles) {
159
+ const relativeFilePath = normalizeSlashes(path.relative(routesDir, absoluteFilePath));
160
+ const relativeDir = normalizeSlashes(path.dirname(relativeFilePath) === "." ? "" : path.dirname(relativeFilePath));
161
+ const fileName = path.basename(relativeFilePath);
162
+ const fileBaseName = trimFileExtension(fileName);
163
+
164
+ if (
165
+ (fileBaseName === "_layout" && LAYOUT_FILE_RE.test(fileName))
166
+ || (fileBaseName === "_middleware" && API_FILE_RE.test(fileName))
167
+ ) {
168
+ continue;
169
+ }
170
+
171
+ const isApiRoute = relativeFilePath.startsWith("api/");
172
+
173
+ if (isApiRoute) {
174
+ if (!API_FILE_RE.test(fileName) || fileBaseName.startsWith("_")) {
175
+ continue;
176
+ }
177
+
178
+ const withoutExt = trimFileExtension(relativeFilePath);
179
+ const shape = toUrlShape(withoutExt);
180
+ const ancestors = getAncestorDirs(relativeDir);
181
+ const middlewareFiles = ancestors
182
+ .map(dir => middlewareByDir.get(dir))
183
+ .filter((value): value is string => Boolean(value));
184
+
185
+ apiRoutes.push({
186
+ type: "api",
187
+ id: toRouteId(withoutExt),
188
+ filePath: absoluteFilePath,
189
+ routePath: shape.routePath,
190
+ segments: shape.segments,
191
+ score: getRouteScore(shape.segments),
192
+ middlewareFiles,
193
+ directory: relativeDir,
194
+ });
195
+ continue;
196
+ }
197
+
198
+ if (!PAGE_FILE_RE.test(fileName) || fileBaseName.startsWith("_")) {
199
+ continue;
200
+ }
201
+
202
+ const withoutExt = trimFileExtension(relativeFilePath);
203
+ const shape = toUrlShape(withoutExt);
204
+ const ancestors = getAncestorDirs(relativeDir);
205
+ const routeFilePath = MD_FILE_RE.test(fileName)
206
+ ? compileMarkdownRouteModule({
207
+ routesDir,
208
+ sourceFilePath: absoluteFilePath,
209
+ generatedMarkdownRootDir: options.generatedMarkdownRootDir,
210
+ })
211
+ : Promise.resolve(absoluteFilePath);
212
+
213
+ const layoutFiles = ancestors
214
+ .map(dir => layoutByDir.get(dir))
215
+ .filter((value): value is string => Boolean(value));
216
+
217
+ const middlewareFiles = ancestors
218
+ .map(dir => middlewareByDir.get(dir))
219
+ .filter((value): value is string => Boolean(value));
220
+
221
+ pageRouteTasks.push(routeFilePath.then((resolvedRouteFilePath) => {
222
+ return {
223
+ type: "page",
224
+ id: toRouteId(withoutExt),
225
+ filePath: resolvedRouteFilePath,
226
+ routePath: shape.routePath,
227
+ segments: shape.segments,
228
+ score: getRouteScore(shape.segments),
229
+ layoutFiles,
230
+ middlewareFiles,
231
+ directory: relativeDir,
232
+ };
233
+ }));
234
+ }
235
+
236
+ const pageRoutes = await Promise.all(pageRouteTasks);
237
+
238
+ return {
239
+ pages: sortRoutes(pageRoutes),
240
+ api: sortRoutes(apiRoutes),
241
+ };
242
+ }