react-bun-ssr 0.1.0 → 0.2.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/README.md +116 -132
- package/framework/cli/commands.ts +59 -223
- package/framework/cli/dev-client-watch.ts +281 -0
- package/framework/cli/dev-route-table.ts +71 -0
- package/framework/cli/dev-runtime.ts +382 -0
- package/framework/cli/internal.ts +138 -0
- package/framework/cli/main.ts +27 -31
- package/framework/runtime/build-tools.ts +280 -57
- package/framework/runtime/bun-route-adapter.ts +20 -7
- package/framework/runtime/client-runtime.tsx +1218 -15
- package/framework/runtime/client-transition-core.ts +159 -0
- package/framework/runtime/config.ts +4 -1
- package/framework/runtime/index.ts +6 -0
- package/framework/runtime/io.ts +1 -1
- package/framework/runtime/link.tsx +205 -0
- package/framework/runtime/markdown-headings.ts +54 -0
- package/framework/runtime/markdown-routes.ts +9 -40
- package/framework/runtime/matcher.ts +11 -11
- package/framework/runtime/module-loader.ts +215 -52
- package/framework/runtime/navigation-api.ts +223 -0
- package/framework/runtime/render.tsx +105 -105
- package/framework/runtime/route-api.ts +6 -0
- package/framework/runtime/route-errors.ts +166 -0
- package/framework/runtime/router.ts +80 -0
- package/framework/runtime/runtime-constants.ts +4 -0
- package/framework/runtime/server.ts +713 -145
- package/framework/runtime/tree.tsx +171 -3
- package/framework/runtime/types.ts +81 -3
- package/framework/runtime/utils.ts +9 -5
- package/package.json +19 -5
|
@@ -1,34 +1,66 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import { createBunRouteAdapter } from
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createBunRouteAdapter } from './bun-route-adapter';
|
|
3
3
|
import {
|
|
4
4
|
ensureCleanDir,
|
|
5
5
|
ensureDir,
|
|
6
6
|
existsPath,
|
|
7
7
|
glob,
|
|
8
|
+
removePath,
|
|
8
9
|
statPath,
|
|
9
10
|
writeTextIfChanged,
|
|
10
|
-
} from
|
|
11
|
+
} from './io';
|
|
11
12
|
import type {
|
|
12
13
|
BuildManifest,
|
|
13
14
|
BuildRouteAsset,
|
|
14
15
|
PageRouteDefinition,
|
|
15
16
|
ResolvedConfig,
|
|
16
17
|
RouteManifest,
|
|
17
|
-
} from
|
|
18
|
-
import { normalizeSlashes, stableHash, toImportPath } from
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
} from './types';
|
|
19
|
+
import { normalizeSlashes, stableHash, toImportPath } from './utils';
|
|
20
|
+
|
|
21
|
+
const BUILD_OPTIMIZE_IMPORTS = [
|
|
22
|
+
'react-bun-ssr',
|
|
23
|
+
'react-bun-ssr/route',
|
|
24
|
+
'react',
|
|
25
|
+
'react-dom',
|
|
26
|
+
'@datadog/browser-rum-react',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export interface ClientEntryFile {
|
|
21
30
|
routeId: string;
|
|
22
31
|
entryFilePath: string;
|
|
23
32
|
route: PageRouteDefinition;
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
export interface ClientEntrySyncResult {
|
|
36
|
+
entries: ClientEntryFile[];
|
|
37
|
+
addedEntryPaths: string[];
|
|
38
|
+
changedEntryPaths: string[];
|
|
39
|
+
removedEntryPaths: string[];
|
|
40
|
+
entrySetChanged: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
async function walkFiles(rootDir: string): Promise<string[]> {
|
|
27
44
|
if (!(await existsPath(rootDir))) {
|
|
28
45
|
return [];
|
|
29
46
|
}
|
|
30
47
|
|
|
31
|
-
return glob(
|
|
48
|
+
return glob('**/*', { cwd: rootDir, absolute: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildClientModuleProjectionSource(defaultRef: string, moduleRef: string): string {
|
|
52
|
+
return `projectClientModule(${defaultRef}, ${moduleRef})`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toClientEntryFile(options: {
|
|
56
|
+
route: PageRouteDefinition;
|
|
57
|
+
entryFilePath: string;
|
|
58
|
+
}): ClientEntryFile {
|
|
59
|
+
return {
|
|
60
|
+
routeId: options.route.id,
|
|
61
|
+
entryFilePath: options.entryFilePath,
|
|
62
|
+
route: options.route,
|
|
63
|
+
};
|
|
32
64
|
}
|
|
33
65
|
|
|
34
66
|
function buildClientEntrySource(options: {
|
|
@@ -41,11 +73,13 @@ function buildClientEntrySource(options: {
|
|
|
41
73
|
|
|
42
74
|
const imports: string[] = [];
|
|
43
75
|
|
|
44
|
-
const runtimeImport =
|
|
76
|
+
const runtimeImport = normalizeSlashes(path.resolve(runtimeClientFile));
|
|
45
77
|
const rootImport = toImportPath(generatedDir, rootModulePath);
|
|
46
78
|
const routeImport = toImportPath(generatedDir, route.filePath);
|
|
47
79
|
|
|
48
|
-
imports.push(
|
|
80
|
+
imports.push(
|
|
81
|
+
`import { hydrateInitialRoute, projectClientModule, registerRouteModules } from "${runtimeImport}";`,
|
|
82
|
+
);
|
|
49
83
|
|
|
50
84
|
imports.push(`import RootDefault from "${rootImport}";`);
|
|
51
85
|
imports.push(`import * as RootModule from "${rootImport}";`);
|
|
@@ -58,19 +92,24 @@ function buildClientEntrySource(options: {
|
|
|
58
92
|
const layoutFilePath = route.layoutFiles[index]!;
|
|
59
93
|
const layoutImportPath = toImportPath(generatedDir, layoutFilePath);
|
|
60
94
|
imports.push(`import Layout${index}Default from "${layoutImportPath}";`);
|
|
61
|
-
imports.push(
|
|
62
|
-
|
|
95
|
+
imports.push(
|
|
96
|
+
`import * as Layout${index}Module from "${layoutImportPath}";`,
|
|
97
|
+
);
|
|
98
|
+
layoutModuleRefs.push(
|
|
99
|
+
buildClientModuleProjectionSource(`Layout${index}Default`, `Layout${index}Module`),
|
|
100
|
+
);
|
|
63
101
|
}
|
|
64
102
|
|
|
65
|
-
return `${imports.join(
|
|
103
|
+
return `${imports.join('\n')}
|
|
66
104
|
|
|
67
105
|
const modules = {
|
|
68
|
-
root: {
|
|
69
|
-
layouts: [${layoutModuleRefs.join(
|
|
70
|
-
route: {
|
|
106
|
+
root: ${buildClientModuleProjectionSource('RootDefault', 'RootModule')},
|
|
107
|
+
layouts: [${layoutModuleRefs.join(', ')}],
|
|
108
|
+
route: ${buildClientModuleProjectionSource('RouteDefault', 'RouteModule')},
|
|
71
109
|
};
|
|
72
110
|
|
|
73
|
-
|
|
111
|
+
registerRouteModules(${JSON.stringify(route.id)}, modules);
|
|
112
|
+
hydrateInitialRoute(${JSON.stringify(route.id)});
|
|
74
113
|
`;
|
|
75
114
|
}
|
|
76
115
|
|
|
@@ -82,10 +121,10 @@ export async function generateClientEntries(options: {
|
|
|
82
121
|
const { config, manifest, generatedDir } = options;
|
|
83
122
|
await ensureDir(generatedDir);
|
|
84
123
|
|
|
85
|
-
const runtimeClientFile = path.resolve(
|
|
124
|
+
const runtimeClientFile = path.resolve(import.meta.dir, 'client-runtime.tsx');
|
|
86
125
|
|
|
87
126
|
return Promise.all(
|
|
88
|
-
manifest.pages.map(async route => {
|
|
127
|
+
manifest.pages.map(async (route) => {
|
|
89
128
|
const entryName = `route__${route.id}.tsx`;
|
|
90
129
|
const entryFilePath = path.join(generatedDir, entryName);
|
|
91
130
|
const source = buildClientEntrySource({
|
|
@@ -97,13 +136,84 @@ export async function generateClientEntries(options: {
|
|
|
97
136
|
|
|
98
137
|
await writeTextIfChanged(entryFilePath, source);
|
|
99
138
|
|
|
100
|
-
return {
|
|
101
|
-
|
|
139
|
+
return toClientEntryFile({
|
|
140
|
+
route,
|
|
102
141
|
entryFilePath,
|
|
142
|
+
});
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function syncClientEntries(options: {
|
|
148
|
+
config: ResolvedConfig;
|
|
149
|
+
manifest: RouteManifest;
|
|
150
|
+
generatedDir: string;
|
|
151
|
+
}): Promise<ClientEntrySyncResult> {
|
|
152
|
+
const { config, manifest, generatedDir } = options;
|
|
153
|
+
await ensureDir(generatedDir);
|
|
154
|
+
|
|
155
|
+
const runtimeClientFile = path.resolve(import.meta.dir, 'client-runtime.tsx');
|
|
156
|
+
const desiredEntries = new Map<string, { route: PageRouteDefinition; entryFilePath: string }>();
|
|
157
|
+
for (const route of manifest.pages) {
|
|
158
|
+
const entryFilePath = path.join(generatedDir, `route__${route.id}.tsx`);
|
|
159
|
+
desiredEntries.set(entryFilePath, {
|
|
160
|
+
route,
|
|
161
|
+
entryFilePath,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const existingEntryPaths = new Set(
|
|
166
|
+
(await glob('route__*.tsx', {
|
|
167
|
+
cwd: generatedDir,
|
|
168
|
+
absolute: true,
|
|
169
|
+
}))
|
|
170
|
+
.map((filePath) => path.resolve(filePath)),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const addedEntryPaths: string[] = [];
|
|
174
|
+
const changedEntryPaths: string[] = [];
|
|
175
|
+
const removedEntryPaths: string[] = [];
|
|
176
|
+
|
|
177
|
+
await Promise.all(
|
|
178
|
+
[...desiredEntries.values()].map(async ({ route, entryFilePath }) => {
|
|
179
|
+
const source = buildClientEntrySource({
|
|
180
|
+
generatedDir,
|
|
103
181
|
route,
|
|
104
|
-
|
|
182
|
+
rootModulePath: config.rootModule,
|
|
183
|
+
runtimeClientFile,
|
|
184
|
+
});
|
|
185
|
+
const existed = existingEntryPaths.has(entryFilePath);
|
|
186
|
+
const changed = await writeTextIfChanged(entryFilePath, source);
|
|
187
|
+
if (!existed) {
|
|
188
|
+
addedEntryPaths.push(entryFilePath);
|
|
189
|
+
} else if (changed) {
|
|
190
|
+
changedEntryPaths.push(entryFilePath);
|
|
191
|
+
}
|
|
192
|
+
existingEntryPaths.delete(entryFilePath);
|
|
105
193
|
}),
|
|
106
194
|
);
|
|
195
|
+
|
|
196
|
+
await Promise.all(
|
|
197
|
+
[...existingEntryPaths].map(async (entryFilePath) => {
|
|
198
|
+
removedEntryPaths.push(entryFilePath);
|
|
199
|
+
await removePath(entryFilePath);
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const entries = manifest.pages.map((route) => {
|
|
204
|
+
return toClientEntryFile({
|
|
205
|
+
route,
|
|
206
|
+
entryFilePath: path.join(generatedDir, `route__${route.id}.tsx`),
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
entries,
|
|
212
|
+
addedEntryPaths: addedEntryPaths.sort(),
|
|
213
|
+
changedEntryPaths: changedEntryPaths.sort(),
|
|
214
|
+
removedEntryPaths: removedEntryPaths.sort(),
|
|
215
|
+
entrySetChanged: addedEntryPaths.length > 0 || removedEntryPaths.length > 0,
|
|
216
|
+
};
|
|
107
217
|
}
|
|
108
218
|
|
|
109
219
|
async function mapBuildOutputsByPrefix(options: {
|
|
@@ -112,14 +222,20 @@ async function mapBuildOutputsByPrefix(options: {
|
|
|
112
222
|
publicPrefix: string;
|
|
113
223
|
}): Promise<Record<string, BuildRouteAsset>> {
|
|
114
224
|
const { outDir, routeIds, publicPrefix } = options;
|
|
115
|
-
const files = (await walkFiles(outDir)).map(filePath =>
|
|
225
|
+
const files = (await walkFiles(outDir)).map((filePath) =>
|
|
226
|
+
normalizeSlashes(path.relative(outDir, filePath)),
|
|
227
|
+
);
|
|
116
228
|
|
|
117
229
|
const routeAssets: Record<string, BuildRouteAsset> = {};
|
|
118
230
|
|
|
119
231
|
for (const routeId of routeIds) {
|
|
120
232
|
const base = `route__${routeId}`;
|
|
121
|
-
const script = files.find(
|
|
122
|
-
|
|
233
|
+
const script = files.find(
|
|
234
|
+
(file) => file.startsWith(base) && file.endsWith('.js'),
|
|
235
|
+
);
|
|
236
|
+
const css = files.filter(
|
|
237
|
+
(file) => file.startsWith(base) && file.endsWith('.css'),
|
|
238
|
+
);
|
|
123
239
|
|
|
124
240
|
if (!script) {
|
|
125
241
|
continue;
|
|
@@ -127,13 +243,77 @@ async function mapBuildOutputsByPrefix(options: {
|
|
|
127
243
|
|
|
128
244
|
routeAssets[routeId] = {
|
|
129
245
|
script: `${publicPrefix}${script}`,
|
|
130
|
-
css: css.map(file => `${publicPrefix}${file}`),
|
|
246
|
+
css: css.map((file) => `${publicPrefix}${file}`),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return routeAssets;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function normalizeMetafilePath(filePath: string): string {
|
|
254
|
+
return normalizeSlashes(filePath).replace(/^\.\//, "");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function toPublicBuildPath(publicPrefix: string, filePath: string): string {
|
|
258
|
+
return `${publicPrefix}${normalizeMetafilePath(filePath)}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function mapBuildOutputsFromMetafile(options: {
|
|
262
|
+
metafile: Bun.BuildMetafile;
|
|
263
|
+
entries: ClientEntryFile[];
|
|
264
|
+
publicPrefix: string;
|
|
265
|
+
}): Record<string, BuildRouteAsset> {
|
|
266
|
+
const routeIdByEntrypoint = new Map<string, string>();
|
|
267
|
+
const routeIdByEntryName = new Map<string, string>();
|
|
268
|
+
for (const entry of options.entries) {
|
|
269
|
+
const absoluteEntrypoint = normalizeMetafilePath(path.resolve(entry.entryFilePath));
|
|
270
|
+
const relativeEntrypoint = normalizeMetafilePath(path.relative(process.cwd(), entry.entryFilePath));
|
|
271
|
+
routeIdByEntrypoint.set(absoluteEntrypoint, entry.routeId);
|
|
272
|
+
routeIdByEntrypoint.set(relativeEntrypoint, entry.routeId);
|
|
273
|
+
routeIdByEntryName.set(path.basename(entry.entryFilePath), entry.routeId);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const routeAssets: Record<string, BuildRouteAsset> = {};
|
|
277
|
+
|
|
278
|
+
for (const [outputPath, metadata] of Object.entries(options.metafile.outputs)) {
|
|
279
|
+
if (!outputPath.endsWith(".js") || !metadata.entryPoint) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const normalizedEntrypoint = normalizeMetafilePath(metadata.entryPoint);
|
|
284
|
+
const absoluteEntrypoint = normalizeMetafilePath(path.resolve(process.cwd(), normalizedEntrypoint));
|
|
285
|
+
const routeId =
|
|
286
|
+
routeIdByEntrypoint.get(normalizedEntrypoint) ??
|
|
287
|
+
routeIdByEntrypoint.get(absoluteEntrypoint) ??
|
|
288
|
+
routeIdByEntryName.get(path.basename(normalizedEntrypoint));
|
|
289
|
+
if (!routeId) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
routeAssets[routeId] = {
|
|
294
|
+
script: toPublicBuildPath(options.publicPrefix, outputPath),
|
|
295
|
+
css: metadata.cssBundle ? [toPublicBuildPath(options.publicPrefix, metadata.cssBundle)] : [],
|
|
131
296
|
};
|
|
132
297
|
}
|
|
133
298
|
|
|
134
299
|
return routeAssets;
|
|
135
300
|
}
|
|
136
301
|
|
|
302
|
+
export function listBuildOutputFiles(metafile: Bun.BuildMetafile): string[] {
|
|
303
|
+
return Object.keys(metafile.outputs)
|
|
304
|
+
.map(normalizeMetafilePath)
|
|
305
|
+
.sort();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function createClientEntrySetSignature(entries: ClientEntryFile[]): string {
|
|
309
|
+
return stableHash(
|
|
310
|
+
entries
|
|
311
|
+
.map((entry) => normalizeSlashes(path.resolve(entry.entryFilePath)))
|
|
312
|
+
.sort()
|
|
313
|
+
.join('|'),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
137
317
|
export async function bundleClientEntries(options: {
|
|
138
318
|
entries: ClientEntryFile[];
|
|
139
319
|
outDir: string;
|
|
@@ -148,42 +328,64 @@ export async function bundleClientEntries(options: {
|
|
|
148
328
|
}
|
|
149
329
|
|
|
150
330
|
const result = await Bun.build({
|
|
151
|
-
entrypoints: entries.map(entry => entry.entryFilePath),
|
|
331
|
+
entrypoints: entries.map((entry) => entry.entryFilePath),
|
|
152
332
|
outdir: outDir,
|
|
153
|
-
target:
|
|
154
|
-
format:
|
|
155
|
-
|
|
156
|
-
|
|
333
|
+
target: 'browser',
|
|
334
|
+
format: 'esm',
|
|
335
|
+
metafile: true,
|
|
336
|
+
optimizeImports: BUILD_OPTIMIZE_IMPORTS,
|
|
337
|
+
splitting: true,
|
|
338
|
+
sourcemap: dev ? 'inline' : 'external',
|
|
157
339
|
minify: !dev,
|
|
158
|
-
naming: dev ?
|
|
340
|
+
naming: dev ? '[name].[ext]' : '[name]-[hash].[ext]',
|
|
159
341
|
});
|
|
160
342
|
|
|
161
343
|
if (!result.success) {
|
|
162
|
-
const messages = result.logs.map(log => log.message).join(
|
|
344
|
+
const messages = result.logs.map((log) => log.message).join('\n');
|
|
163
345
|
throw new Error(`Client bundle failed:\n${messages}`);
|
|
164
346
|
}
|
|
165
347
|
|
|
166
|
-
|
|
348
|
+
const routeAssetsFromMetafile = result.metafile
|
|
349
|
+
? mapBuildOutputsFromMetafile({
|
|
350
|
+
metafile: result.metafile,
|
|
351
|
+
entries,
|
|
352
|
+
publicPrefix,
|
|
353
|
+
})
|
|
354
|
+
: {};
|
|
355
|
+
|
|
356
|
+
if (Object.keys(routeAssetsFromMetafile).length === entries.length) {
|
|
357
|
+
return routeAssetsFromMetafile;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const routeAssetsFromPrefix = await mapBuildOutputsByPrefix({
|
|
167
361
|
outDir,
|
|
168
|
-
routeIds: entries.map(entry => entry.routeId),
|
|
362
|
+
routeIds: entries.map((entry) => entry.routeId),
|
|
169
363
|
publicPrefix,
|
|
170
364
|
});
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
...routeAssetsFromPrefix,
|
|
368
|
+
...routeAssetsFromMetafile,
|
|
369
|
+
};
|
|
171
370
|
}
|
|
172
371
|
|
|
173
372
|
export async function ensureCleanDirectory(dirPath: string): Promise<void> {
|
|
174
373
|
await ensureCleanDir(dirPath);
|
|
175
374
|
}
|
|
176
375
|
|
|
177
|
-
export async function copyDirRecursive(
|
|
376
|
+
export async function copyDirRecursive(
|
|
377
|
+
sourceDir: string,
|
|
378
|
+
destinationDir: string,
|
|
379
|
+
): Promise<void> {
|
|
178
380
|
if (!(await existsPath(sourceDir))) {
|
|
179
381
|
return;
|
|
180
382
|
}
|
|
181
383
|
|
|
182
384
|
await ensureDir(destinationDir);
|
|
183
385
|
|
|
184
|
-
const entries = await glob(
|
|
386
|
+
const entries = await glob('**/*', { cwd: sourceDir });
|
|
185
387
|
await Promise.all(
|
|
186
|
-
entries.map(async entry => {
|
|
388
|
+
entries.map(async (entry) => {
|
|
187
389
|
const from = path.join(sourceDir, entry);
|
|
188
390
|
const to = path.join(destinationDir, entry);
|
|
189
391
|
const fileStat = await statPath(from);
|
|
@@ -197,7 +399,9 @@ export async function copyDirRecursive(sourceDir: string, destinationDir: string
|
|
|
197
399
|
);
|
|
198
400
|
}
|
|
199
401
|
|
|
200
|
-
export function createBuildManifest(
|
|
402
|
+
export function createBuildManifest(
|
|
403
|
+
routeAssets: Record<string, BuildRouteAsset>,
|
|
404
|
+
): BuildManifest {
|
|
201
405
|
return {
|
|
202
406
|
version: stableHash(JSON.stringify(routeAssets)),
|
|
203
407
|
generatedAt: new Date().toISOString(),
|
|
@@ -205,30 +409,49 @@ export function createBuildManifest(routeAssets: Record<string, BuildRouteAsset>
|
|
|
205
409
|
};
|
|
206
410
|
}
|
|
207
411
|
|
|
208
|
-
export async function discoverFileSignature(
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
412
|
+
export async function discoverFileSignature(rootPath: string): Promise<string> {
|
|
413
|
+
const rootStat = await statPath(rootPath);
|
|
414
|
+
if (!rootStat) {
|
|
415
|
+
return stableHash("");
|
|
416
|
+
}
|
|
212
417
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return `${normalizeSlashes(filePath)}:${contentHash}`;
|
|
221
|
-
}),
|
|
222
|
-
)).filter((value): value is string => Boolean(value));
|
|
418
|
+
const files = (
|
|
419
|
+
rootStat.isFile()
|
|
420
|
+
? [rootPath]
|
|
421
|
+
: await walkFiles(rootPath)
|
|
422
|
+
)
|
|
423
|
+
.filter((file) => !normalizeSlashes(file).includes('/node_modules/'))
|
|
424
|
+
.sort();
|
|
223
425
|
|
|
224
|
-
|
|
426
|
+
const signatureBits = (
|
|
427
|
+
await Promise.all(
|
|
428
|
+
files.map(async (filePath) => {
|
|
429
|
+
const fileStat = await statPath(filePath);
|
|
430
|
+
if (!fileStat?.isFile()) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
const contentHash = stableHash(await Bun.file(filePath).bytes());
|
|
434
|
+
return `${normalizeSlashes(filePath)}:${contentHash}`;
|
|
435
|
+
}),
|
|
436
|
+
)
|
|
437
|
+
).filter((value): value is string => Boolean(value));
|
|
438
|
+
|
|
439
|
+
return stableHash(signatureBits.join('|'));
|
|
225
440
|
}
|
|
226
441
|
|
|
227
|
-
export async function buildRouteManifest(
|
|
442
|
+
export async function buildRouteManifest(
|
|
443
|
+
config: ResolvedConfig,
|
|
444
|
+
): Promise<RouteManifest> {
|
|
228
445
|
const adapter = await createBunRouteAdapter({
|
|
229
446
|
routesDir: config.routesDir,
|
|
230
|
-
generatedMarkdownRootDir: path.resolve(
|
|
231
|
-
|
|
447
|
+
generatedMarkdownRootDir: path.resolve(
|
|
448
|
+
config.cwd,
|
|
449
|
+
'.rbssr/generated/markdown-routes',
|
|
450
|
+
),
|
|
451
|
+
projectionRootDir: path.resolve(
|
|
452
|
+
config.cwd,
|
|
453
|
+
'.rbssr/generated/router-projection/build-manifest',
|
|
454
|
+
),
|
|
232
455
|
});
|
|
233
456
|
return adapter.manifest;
|
|
234
457
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { ensureDir, glob, removePath, writeTextIfChanged } from "./io";
|
|
3
|
+
import { matchRouteBySegments } from "./matcher";
|
|
3
4
|
import { scanRoutes } from "./route-scanner";
|
|
4
5
|
import type {
|
|
5
6
|
ApiRouteDefinition,
|
|
@@ -116,17 +117,30 @@ async function writeProjectionRoutes<T extends PageRouteDefinition | ApiRouteDef
|
|
|
116
117
|
}),
|
|
117
118
|
);
|
|
118
119
|
|
|
120
|
+
const expectedPaths = new Set(writes.map(({ projectedFilePath }) => path.resolve(projectedFilePath)));
|
|
121
|
+
const existingPaths = new Set(await glob(`**/*${extension}`, {
|
|
122
|
+
cwd: outDir,
|
|
123
|
+
absolute: true,
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
await Promise.all(
|
|
127
|
+
[...existingPaths]
|
|
128
|
+
.filter((projectedFilePath) => !expectedPaths.has(path.resolve(projectedFilePath)))
|
|
129
|
+
.map((projectedFilePath) => removePath(projectedFilePath)),
|
|
130
|
+
);
|
|
131
|
+
|
|
119
132
|
return byProjectedFilePath;
|
|
120
133
|
}
|
|
121
134
|
|
|
122
135
|
function toRouteMatch<T extends PageRouteDefinition | ApiRouteDefinition>(
|
|
136
|
+
orderedRoutes: T[],
|
|
123
137
|
routeByProjectedPath: Map<string, T>,
|
|
124
138
|
pathname: string,
|
|
125
139
|
router: Bun.FileSystemRouter,
|
|
126
140
|
): RouteMatch<T> | null {
|
|
127
141
|
const matched = router.match(pathname);
|
|
128
142
|
if (!matched) {
|
|
129
|
-
return
|
|
143
|
+
return matchRouteBySegments(orderedRoutes, pathname);
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
const matchedSource = normalizeRouteKey(
|
|
@@ -136,7 +150,7 @@ function toRouteMatch<T extends PageRouteDefinition | ApiRouteDefinition>(
|
|
|
136
150
|
);
|
|
137
151
|
const route = routeByProjectedPath.get(matchedSource);
|
|
138
152
|
if (!route) {
|
|
139
|
-
return
|
|
153
|
+
return matchRouteBySegments(orderedRoutes, pathname);
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
return {
|
|
@@ -153,8 +167,7 @@ export async function createBunRouteAdapter(options: {
|
|
|
153
167
|
const manifest = await scanRoutes(options.routesDir, {
|
|
154
168
|
generatedMarkdownRootDir: options.generatedMarkdownRootDir,
|
|
155
169
|
});
|
|
156
|
-
|
|
157
|
-
await ensureCleanDir(options.projectionRootDir);
|
|
170
|
+
await ensureDir(options.projectionRootDir);
|
|
158
171
|
|
|
159
172
|
const pagesProjectionDir = path.join(options.projectionRootDir, "pages");
|
|
160
173
|
const apiProjectionDir = path.join(options.projectionRootDir, "api");
|
|
@@ -191,10 +204,10 @@ export async function createBunRouteAdapter(options: {
|
|
|
191
204
|
return {
|
|
192
205
|
manifest,
|
|
193
206
|
matchPage(pathname) {
|
|
194
|
-
return toRouteMatch(pageRouteByProjectedPath, pathname, pageRouter);
|
|
207
|
+
return toRouteMatch(manifest.pages, pageRouteByProjectedPath, pathname, pageRouter);
|
|
195
208
|
},
|
|
196
209
|
matchApi(pathname) {
|
|
197
|
-
return toRouteMatch(apiRouteByProjectedPath, pathname, apiRouter);
|
|
210
|
+
return toRouteMatch(manifest.api, apiRouteByProjectedPath, pathname, apiRouter);
|
|
198
211
|
},
|
|
199
212
|
};
|
|
200
213
|
}
|