react-bun-ssr 0.1.1 → 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/README.md +25 -17
- package/framework/cli/commands.ts +104 -246
- package/framework/cli/dev-client-watch.ts +288 -0
- package/framework/cli/dev-route-table.ts +71 -0
- package/framework/cli/dev-runtime.ts +382 -0
- package/framework/cli/internal.ts +142 -0
- package/framework/cli/main.ts +27 -31
- package/framework/runtime/build-tools.ts +134 -13
- package/framework/runtime/bun-route-adapter.ts +20 -7
- package/framework/runtime/client-runtime.tsx +150 -159
- package/framework/runtime/client-transition-core.ts +159 -0
- package/framework/runtime/config.ts +7 -2
- package/framework/runtime/index.ts +1 -1
- package/framework/runtime/link.tsx +3 -11
- package/framework/runtime/markdown-routes.ts +1 -14
- package/framework/runtime/matcher.ts +11 -11
- package/framework/runtime/module-loader.ts +75 -25
- package/framework/runtime/render.tsx +150 -22
- package/framework/runtime/route-api.ts +1 -1
- package/framework/runtime/router.ts +75 -4
- package/framework/runtime/server.ts +57 -106
- package/framework/runtime/tree.tsx +24 -2
- package/framework/runtime/types.ts +13 -2
- package/framework/runtime/utils.ts +3 -0
- package/package.json +13 -7
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ensureDir,
|
|
6
6
|
existsPath,
|
|
7
7
|
glob,
|
|
8
|
+
removePath,
|
|
8
9
|
statPath,
|
|
9
10
|
writeTextIfChanged,
|
|
10
11
|
} from './io';
|
|
@@ -22,15 +23,22 @@ const BUILD_OPTIMIZE_IMPORTS = [
|
|
|
22
23
|
'react-bun-ssr/route',
|
|
23
24
|
'react',
|
|
24
25
|
'react-dom',
|
|
25
|
-
'@datadog/browser-rum-react',
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
-
interface ClientEntryFile {
|
|
28
|
+
export interface ClientEntryFile {
|
|
29
29
|
routeId: string;
|
|
30
30
|
entryFilePath: string;
|
|
31
31
|
route: PageRouteDefinition;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export interface ClientEntrySyncResult {
|
|
35
|
+
entries: ClientEntryFile[];
|
|
36
|
+
addedEntryPaths: string[];
|
|
37
|
+
changedEntryPaths: string[];
|
|
38
|
+
removedEntryPaths: string[];
|
|
39
|
+
entrySetChanged: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
async function walkFiles(rootDir: string): Promise<string[]> {
|
|
35
43
|
if (!(await existsPath(rootDir))) {
|
|
36
44
|
return [];
|
|
@@ -39,6 +47,21 @@ async function walkFiles(rootDir: string): Promise<string[]> {
|
|
|
39
47
|
return glob('**/*', { cwd: rootDir, absolute: true });
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
function buildClientModuleProjectionSource(defaultRef: string, moduleRef: string): string {
|
|
51
|
+
return `projectClientModule(${defaultRef}, ${moduleRef})`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toClientEntryFile(options: {
|
|
55
|
+
route: PageRouteDefinition;
|
|
56
|
+
entryFilePath: string;
|
|
57
|
+
}): ClientEntryFile {
|
|
58
|
+
return {
|
|
59
|
+
routeId: options.route.id,
|
|
60
|
+
entryFilePath: options.entryFilePath,
|
|
61
|
+
route: options.route,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
function buildClientEntrySource(options: {
|
|
43
66
|
generatedDir: string;
|
|
44
67
|
route: PageRouteDefinition;
|
|
@@ -54,7 +77,7 @@ function buildClientEntrySource(options: {
|
|
|
54
77
|
const routeImport = toImportPath(generatedDir, route.filePath);
|
|
55
78
|
|
|
56
79
|
imports.push(
|
|
57
|
-
`import { hydrateInitialRoute, registerRouteModules } from "${runtimeImport}";`,
|
|
80
|
+
`import { hydrateInitialRoute, projectClientModule, registerRouteModules } from "${runtimeImport}";`,
|
|
58
81
|
);
|
|
59
82
|
|
|
60
83
|
imports.push(`import RootDefault from "${rootImport}";`);
|
|
@@ -72,16 +95,16 @@ function buildClientEntrySource(options: {
|
|
|
72
95
|
`import * as Layout${index}Module from "${layoutImportPath}";`,
|
|
73
96
|
);
|
|
74
97
|
layoutModuleRefs.push(
|
|
75
|
-
`
|
|
98
|
+
buildClientModuleProjectionSource(`Layout${index}Default`, `Layout${index}Module`),
|
|
76
99
|
);
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
return `${imports.join('\n')}
|
|
80
103
|
|
|
81
104
|
const modules = {
|
|
82
|
-
root: {
|
|
105
|
+
root: ${buildClientModuleProjectionSource('RootDefault', 'RootModule')},
|
|
83
106
|
layouts: [${layoutModuleRefs.join(', ')}],
|
|
84
|
-
route: {
|
|
107
|
+
route: ${buildClientModuleProjectionSource('RouteDefault', 'RouteModule')},
|
|
85
108
|
};
|
|
86
109
|
|
|
87
110
|
registerRouteModules(${JSON.stringify(route.id)}, modules);
|
|
@@ -112,13 +135,84 @@ export async function generateClientEntries(options: {
|
|
|
112
135
|
|
|
113
136
|
await writeTextIfChanged(entryFilePath, source);
|
|
114
137
|
|
|
115
|
-
return {
|
|
116
|
-
|
|
138
|
+
return toClientEntryFile({
|
|
139
|
+
route,
|
|
117
140
|
entryFilePath,
|
|
141
|
+
});
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function syncClientEntries(options: {
|
|
147
|
+
config: ResolvedConfig;
|
|
148
|
+
manifest: RouteManifest;
|
|
149
|
+
generatedDir: string;
|
|
150
|
+
}): Promise<ClientEntrySyncResult> {
|
|
151
|
+
const { config, manifest, generatedDir } = options;
|
|
152
|
+
await ensureDir(generatedDir);
|
|
153
|
+
|
|
154
|
+
const runtimeClientFile = path.resolve(import.meta.dir, 'client-runtime.tsx');
|
|
155
|
+
const desiredEntries = new Map<string, { route: PageRouteDefinition; entryFilePath: string }>();
|
|
156
|
+
for (const route of manifest.pages) {
|
|
157
|
+
const entryFilePath = path.join(generatedDir, `route__${route.id}.tsx`);
|
|
158
|
+
desiredEntries.set(entryFilePath, {
|
|
159
|
+
route,
|
|
160
|
+
entryFilePath,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const existingEntryPaths = new Set(
|
|
165
|
+
(await glob('route__*.tsx', {
|
|
166
|
+
cwd: generatedDir,
|
|
167
|
+
absolute: true,
|
|
168
|
+
}))
|
|
169
|
+
.map((filePath) => path.resolve(filePath)),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const addedEntryPaths: string[] = [];
|
|
173
|
+
const changedEntryPaths: string[] = [];
|
|
174
|
+
const removedEntryPaths: string[] = [];
|
|
175
|
+
|
|
176
|
+
await Promise.all(
|
|
177
|
+
[...desiredEntries.values()].map(async ({ route, entryFilePath }) => {
|
|
178
|
+
const source = buildClientEntrySource({
|
|
179
|
+
generatedDir,
|
|
118
180
|
route,
|
|
119
|
-
|
|
181
|
+
rootModulePath: config.rootModule,
|
|
182
|
+
runtimeClientFile,
|
|
183
|
+
});
|
|
184
|
+
const existed = existingEntryPaths.has(entryFilePath);
|
|
185
|
+
const changed = await writeTextIfChanged(entryFilePath, source);
|
|
186
|
+
if (!existed) {
|
|
187
|
+
addedEntryPaths.push(entryFilePath);
|
|
188
|
+
} else if (changed) {
|
|
189
|
+
changedEntryPaths.push(entryFilePath);
|
|
190
|
+
}
|
|
191
|
+
existingEntryPaths.delete(entryFilePath);
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
await Promise.all(
|
|
196
|
+
[...existingEntryPaths].map(async (entryFilePath) => {
|
|
197
|
+
removedEntryPaths.push(entryFilePath);
|
|
198
|
+
await removePath(entryFilePath);
|
|
120
199
|
}),
|
|
121
200
|
);
|
|
201
|
+
|
|
202
|
+
const entries = manifest.pages.map((route) => {
|
|
203
|
+
return toClientEntryFile({
|
|
204
|
+
route,
|
|
205
|
+
entryFilePath: path.join(generatedDir, `route__${route.id}.tsx`),
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
entries,
|
|
211
|
+
addedEntryPaths: addedEntryPaths.sort(),
|
|
212
|
+
changedEntryPaths: changedEntryPaths.sort(),
|
|
213
|
+
removedEntryPaths: removedEntryPaths.sort(),
|
|
214
|
+
entrySetChanged: addedEntryPaths.length > 0 || removedEntryPaths.length > 0,
|
|
215
|
+
};
|
|
122
216
|
}
|
|
123
217
|
|
|
124
218
|
async function mapBuildOutputsByPrefix(options: {
|
|
@@ -155,7 +249,7 @@ async function mapBuildOutputsByPrefix(options: {
|
|
|
155
249
|
return routeAssets;
|
|
156
250
|
}
|
|
157
251
|
|
|
158
|
-
function normalizeMetafilePath(filePath: string): string {
|
|
252
|
+
export function normalizeMetafilePath(filePath: string): string {
|
|
159
253
|
return normalizeSlashes(filePath).replace(/^\.\//, "");
|
|
160
254
|
}
|
|
161
255
|
|
|
@@ -163,7 +257,7 @@ function toPublicBuildPath(publicPrefix: string, filePath: string): string {
|
|
|
163
257
|
return `${publicPrefix}${normalizeMetafilePath(filePath)}`;
|
|
164
258
|
}
|
|
165
259
|
|
|
166
|
-
function mapBuildOutputsFromMetafile(options: {
|
|
260
|
+
export function mapBuildOutputsFromMetafile(options: {
|
|
167
261
|
metafile: Bun.BuildMetafile;
|
|
168
262
|
entries: ClientEntryFile[];
|
|
169
263
|
publicPrefix: string;
|
|
@@ -204,6 +298,21 @@ function mapBuildOutputsFromMetafile(options: {
|
|
|
204
298
|
return routeAssets;
|
|
205
299
|
}
|
|
206
300
|
|
|
301
|
+
export function listBuildOutputFiles(metafile: Bun.BuildMetafile): string[] {
|
|
302
|
+
return Object.keys(metafile.outputs)
|
|
303
|
+
.map(normalizeMetafilePath)
|
|
304
|
+
.sort();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function createClientEntrySetSignature(entries: ClientEntryFile[]): string {
|
|
308
|
+
return stableHash(
|
|
309
|
+
entries
|
|
310
|
+
.map((entry) => normalizeSlashes(path.resolve(entry.entryFilePath)))
|
|
311
|
+
.sort()
|
|
312
|
+
.join('|'),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
207
316
|
export async function bundleClientEntries(options: {
|
|
208
317
|
entries: ClientEntryFile[];
|
|
209
318
|
outDir: string;
|
|
@@ -228,6 +337,9 @@ export async function bundleClientEntries(options: {
|
|
|
228
337
|
sourcemap: dev ? 'inline' : 'external',
|
|
229
338
|
minify: !dev,
|
|
230
339
|
naming: dev ? '[name].[ext]' : '[name]-[hash].[ext]',
|
|
340
|
+
define: {
|
|
341
|
+
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
|
342
|
+
},
|
|
231
343
|
});
|
|
232
344
|
|
|
233
345
|
if (!result.success) {
|
|
@@ -299,8 +411,17 @@ export function createBuildManifest(
|
|
|
299
411
|
};
|
|
300
412
|
}
|
|
301
413
|
|
|
302
|
-
export async function discoverFileSignature(
|
|
303
|
-
const
|
|
414
|
+
export async function discoverFileSignature(rootPath: string): Promise<string> {
|
|
415
|
+
const rootStat = await statPath(rootPath);
|
|
416
|
+
if (!rootStat) {
|
|
417
|
+
return stableHash("");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const files = (
|
|
421
|
+
rootStat.isFile()
|
|
422
|
+
? [rootPath]
|
|
423
|
+
: await walkFiles(rootPath)
|
|
424
|
+
)
|
|
304
425
|
.filter((file) => !normalizeSlashes(file).includes('/node_modules/'))
|
|
305
426
|
.sort();
|
|
306
427
|
|
|
@@ -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
|
}
|