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.
@@ -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
- `{ ...Layout${index}Module, default: Layout${index}Default }`,
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: { ...RootModule, default: RootDefault },
105
+ root: ${buildClientModuleProjectionSource('RootDefault', 'RootModule')},
83
106
  layouts: [${layoutModuleRefs.join(', ')}],
84
- route: { ...RouteModule, default: RouteDefault },
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
- routeId: route.id,
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(rootDir: string): Promise<string> {
303
- const files = (await walkFiles(rootDir))
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 { ensureCleanDir, ensureDir, writeTextIfChanged } from "./io";
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 null;
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 null;
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
  }