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 CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  `react-bun-ssr` is a Bun-native SSR React framework with file-based routing, loaders, actions, middleware, streaming, soft navigation, and first-class markdown routes.
4
4
 
5
- - Documentation: https://react-bun-ssr.fly.dev/docs
6
- - API reference: https://react-bun-ssr.fly.dev/docs/api/overview
7
- - Blog: https://react-bun-ssr.fly.dev/blog
5
+ - Documentation: https://react-bun-ssr.dev/docs
6
+ - API reference: https://react-bun-ssr.dev/docs/api/overview
7
+ - Blog: https://react-bun-ssr.dev/blog
8
8
  - Repository: https://github.com/react-formation/react-bun-ssr
9
9
 
10
10
  ## Why react-bun-ssr?
@@ -50,7 +50,7 @@ http://127.0.0.1:3000
50
50
 
51
51
  For the full setup walkthrough, read the installation guide:
52
52
 
53
- - https://react-bun-ssr.fly.dev/docs/start/installation
53
+ - https://react-bun-ssr.dev/docs/start/installation
54
54
 
55
55
  ## What `rbssr init` gives you
56
56
 
@@ -75,7 +75,7 @@ rbssr.config.ts
75
75
 
76
76
  The quickest follow-up is:
77
77
 
78
- - https://react-bun-ssr.fly.dev/docs/start/quick-start
78
+ - https://react-bun-ssr.dev/docs/start/quick-start
79
79
 
80
80
  ## How it works
81
81
 
@@ -85,7 +85,7 @@ Routes live under `app/routes`. Page routes, API routes, dynamic params, and mar
85
85
 
86
86
  Read more:
87
87
 
88
- - https://react-bun-ssr.fly.dev/docs/routing/file-based-routing
88
+ - https://react-bun-ssr.dev/docs/routing/file-based-routing
89
89
 
90
90
  ### Request pipeline
91
91
 
@@ -93,8 +93,8 @@ For a page request, the framework resolves the matching route, runs global and n
93
93
 
94
94
  Read more:
95
95
 
96
- - https://react-bun-ssr.fly.dev/docs/routing/middleware
97
- - https://react-bun-ssr.fly.dev/docs/data/loaders
96
+ - https://react-bun-ssr.dev/docs/routing/middleware
97
+ - https://react-bun-ssr.dev/docs/data/loaders
98
98
 
99
99
  ### Rendering model
100
100
 
@@ -102,8 +102,8 @@ SSR is the default model. HTML responses stream, deferred loader data is support
102
102
 
103
103
  Read more:
104
104
 
105
- - https://react-bun-ssr.fly.dev/docs/rendering/streaming-deferred
106
- - https://react-bun-ssr.fly.dev/docs/routing/navigation
105
+ - https://react-bun-ssr.dev/docs/rendering/streaming-deferred
106
+ - https://react-bun-ssr.dev/docs/routing/navigation
107
107
 
108
108
  ### Bun-first runtime
109
109
 
@@ -111,7 +111,7 @@ Bun provides the runtime, server, bundler, markdown support, and file APIs that
111
111
 
112
112
  Read more:
113
113
 
114
- - https://react-bun-ssr.fly.dev/docs/api/bun-runtime-apis
114
+ - https://react-bun-ssr.dev/docs/api/bun-runtime-apis
115
115
 
116
116
  ## Core commands
117
117
 
@@ -132,7 +132,7 @@ Repository maintenance commands:
132
132
 
133
133
  CLI reference:
134
134
 
135
- - https://react-bun-ssr.fly.dev/docs/tooling/cli
135
+ - https://react-bun-ssr.dev/docs/tooling/cli
136
136
 
137
137
  ## Working on this repository
138
138
 
@@ -147,16 +147,24 @@ bun run docs:dev
147
147
 
148
148
  That starts the docs site locally using the framework itself.
149
149
 
150
+ Dependency ownership is split intentionally:
151
+
152
+ - the repo-root `package.json` is the published framework manifest
153
+ - [`app/package.json`](/Users/react-formation/code/my-app/app/package.json) owns docs-app runtime dependencies
154
+
155
+ Contributors should still use the repo-root commands; the workspace split is there to keep npm package metadata accurate, not to change the day-to-day workflow.
156
+
150
157
  ## Project layout
151
158
 
152
159
  - `framework/`: runtime, renderer, route handling, build tooling, and CLI internals
153
160
  - `bin/rbssr.ts`: CLI entrypoint
154
161
  - `app/`: docs site routes, layouts, middleware, blog, and styles
162
+ - `app/package.json`: private docs-app dependency manifest
155
163
  - `app/routes/docs/**/*.md`: authored documentation pages
156
164
  - `app/routes/blog/*.md`: authored blog posts
157
165
  - `scripts/`: generators and validation scripts
158
- - `tests/`: unit and integration tests
159
- - `e2e/`: Playwright end-to-end tests
166
+ - `tests/framework/`: framework runtime, CLI, build, unit/integration, and framework Playwright tests
167
+ - `tests/docs-app/`: docs site, blog, analytics, and docs-app Playwright tests
160
168
 
161
169
  ## Contributing
162
170
 
@@ -184,6 +192,6 @@ fly deploy
184
192
 
185
193
  Full deployment docs:
186
194
 
187
- - https://react-bun-ssr.fly.dev/docs/deployment/bun-deployment
188
- - https://react-bun-ssr.fly.dev/docs/deployment/configuration
189
- - https://react-bun-ssr.fly.dev/docs/deployment/troubleshooting
195
+ - https://react-bun-ssr.dev/docs/deployment/bun-deployment
196
+ - https://react-bun-ssr.dev/docs/deployment/configuration
197
+ - https://react-bun-ssr.dev/docs/deployment/troubleshooting
@@ -1,18 +1,23 @@
1
- import { watch, type FSWatcher } from "node:fs";
2
1
  import path from "node:path";
3
2
  import {
4
3
  buildRouteManifest,
5
4
  bundleClientEntries,
6
5
  copyDirRecursive,
7
6
  createBuildManifest,
8
- discoverFileSignature,
9
7
  ensureCleanDirectory,
10
8
  generateClientEntries,
11
9
  } from "../runtime/build-tools";
12
10
  import { loadUserConfig, resolveConfig } from "../runtime/config";
13
- import { ensureDir, existsPath, listEntries, removePath, writeText } from "../runtime/io";
14
- import { createServer } from "../runtime/server";
15
- import type { BuildRouteAsset, FrameworkConfig, ResolvedConfig } from "../runtime/types";
11
+ import { ensureDir, existsPath, writeText, writeTextIfChanged } from "../runtime/io";
12
+ import type { FrameworkConfig, ResolvedConfig } from "../runtime/types";
13
+ import {
14
+ createDevHotEntrypointSource,
15
+ createProductionServerEntrypointSource,
16
+ createTestCommands,
17
+ createTypecheckCommand,
18
+ parseFlags,
19
+ RBSSR_DEV_RESTART_EXIT_CODE,
20
+ } from "./internal";
16
21
  import { scaffoldApp } from "./scaffold";
17
22
 
18
23
  function log(message: string): void {
@@ -20,10 +25,19 @@ function log(message: string): void {
20
25
  console.log(`[rbssr] ${message}`);
21
26
  }
22
27
 
23
- function parseFlags(args: string[]): { force: boolean } {
24
- return {
25
- force: args.includes("--force"),
26
- };
28
+ async function withNodeEnv<T>(nodeEnv: "development" | "production", run: () => Promise<T>): Promise<T> {
29
+ const previousNodeEnv = process.env.NODE_ENV;
30
+ process.env.NODE_ENV = nodeEnv;
31
+
32
+ try {
33
+ return await run();
34
+ } finally {
35
+ if (previousNodeEnv === undefined) {
36
+ delete process.env.NODE_ENV;
37
+ } else {
38
+ process.env.NODE_ENV = previousNodeEnv;
39
+ }
40
+ }
27
41
  }
28
42
 
29
43
  async function getConfig(cwd: string): Promise<{ userConfig: FrameworkConfig; resolved: ResolvedConfig }> {
@@ -37,29 +51,7 @@ async function writeProductionServerEntrypoint(options: { distDir: string }): Pr
37
51
  await ensureDir(serverDir);
38
52
 
39
53
  const serverEntryPath = path.join(serverDir, "server.mjs");
40
- const content = `import path from "node:path";
41
- import config from "../../rbssr.config.ts";
42
- import { startHttpServer } from "../../framework/runtime/index.ts";
43
-
44
- const rootDir = path.resolve(path.dirname(Bun.fileURLToPath(import.meta.url)), "../..");
45
- process.chdir(rootDir);
46
-
47
- const manifestPath = path.resolve(rootDir, "dist/manifest.json");
48
- const manifest = await Bun.file(manifestPath).json();
49
-
50
- startHttpServer({
51
- config: {
52
- ...(config ?? {}),
53
- mode: "production",
54
- },
55
- runtimeOptions: {
56
- dev: false,
57
- buildManifest: manifest,
58
- },
59
- });
60
- `;
61
-
62
- await writeText(serverEntryPath, content);
54
+ await writeText(serverEntryPath, createProductionServerEntrypointSource());
63
55
  }
64
56
 
65
57
  export async function runInit(args: string[], cwd = process.cwd()): Promise<void> {
@@ -71,238 +63,109 @@ export async function runInit(args: string[], cwd = process.cwd()): Promise<void
71
63
  }
72
64
 
73
65
  export async function runBuild(cwd = process.cwd()): Promise<void> {
74
- const { resolved } = await getConfig(cwd);
75
-
76
- const distClientDir = path.join(resolved.distDir, "client");
77
- const generatedDir = path.resolve(cwd, ".rbssr/generated/client-entries");
78
-
79
- await Promise.all([
80
- ensureCleanDirectory(resolved.distDir),
81
- ensureCleanDirectory(generatedDir),
82
- ]);
83
-
84
- const routeManifest = await buildRouteManifest(resolved);
85
- const entries = await generateClientEntries({
86
- config: resolved,
87
- manifest: routeManifest,
88
- generatedDir,
89
- });
90
-
91
- const routeAssets = await bundleClientEntries({
92
- entries,
93
- outDir: distClientDir,
94
- dev: false,
95
- publicPrefix: "/client/",
96
- });
97
-
98
- await copyDirRecursive(resolved.publicDir, distClientDir);
99
-
100
- const buildManifest = createBuildManifest(routeAssets);
101
- await writeText(
102
- path.join(resolved.distDir, "manifest.json"),
103
- JSON.stringify(buildManifest, null, 2),
104
- );
105
-
106
- await writeProductionServerEntrypoint({ distDir: resolved.distDir });
107
-
108
- log(`build complete: ${resolved.distDir}`);
109
- }
110
-
111
- export async function runDev(cwd = process.cwd()): Promise<void> {
112
- const { userConfig, resolved } = await getConfig(cwd);
113
- const generatedDir = path.resolve(cwd, ".rbssr/generated/client-entries");
114
- const devClientDir = path.resolve(cwd, ".rbssr/dev/client");
115
- const serverSnapshotsRoot = path.resolve(cwd, ".rbssr/dev/server-snapshots");
116
- const docsSourceDir = path.resolve(cwd, "docs");
117
- const docsSnapshotDir = path.join(serverSnapshotsRoot, "docs");
118
-
119
- await Promise.all([
120
- ensureDir(generatedDir),
121
- ensureDir(devClientDir),
122
- ensureCleanDirectory(serverSnapshotsRoot),
123
- ]);
124
-
125
- let routeAssets: Record<string, BuildRouteAsset> = {};
126
- let signature = "";
127
- let version = 0;
128
- let currentServerSnapshotDir = resolved.appDir;
129
- const reloadListeners = new Set<(nextVersion: number) => void>();
130
- const docsDir = path.resolve(cwd, "docs");
131
-
132
- const watchedRoots = [resolved.appDir];
133
- if (await existsPath(docsDir)) {
134
- watchedRoots.push(docsDir);
135
- }
136
-
137
- const getSourceSignature = async (): Promise<string> => {
138
- const signatures = await Promise.all(
139
- watchedRoots.map(root => discoverFileSignature(root)),
140
- );
141
- return signatures.join(":");
142
- };
143
-
144
- const notifyReload = (): void => {
145
- for (const listener of reloadListeners) {
146
- listener(version);
147
- }
148
- };
149
-
150
- const rebuildIfNeeded = async (force = false): Promise<void> => {
151
- const nextSignature = await getSourceSignature();
152
- if (!force && nextSignature === signature) {
153
- return;
154
- }
66
+ await withNodeEnv("production", async () => {
67
+ const userConfig = await loadUserConfig(cwd);
68
+ const resolved = resolveConfig({
69
+ ...userConfig,
70
+ mode: "production",
71
+ }, cwd);
155
72
 
156
- signature = nextSignature;
73
+ const distClientDir = path.join(resolved.distDir, "client");
74
+ const generatedDir = path.resolve(cwd, ".rbssr/generated/client-entries");
157
75
 
158
- const snapshotDir = path.join(serverSnapshotsRoot, `v${version + 1}`);
159
- const hasDocsSourceDir = await existsPath(docsSourceDir);
160
76
  await Promise.all([
161
- (async () => {
162
- await ensureCleanDirectory(snapshotDir);
163
- await copyDirRecursive(resolved.appDir, snapshotDir);
164
- })(),
165
- hasDocsSourceDir
166
- ? (async () => {
167
- await ensureCleanDirectory(docsSnapshotDir);
168
- await copyDirRecursive(docsSourceDir, docsSnapshotDir);
169
- })()
170
- : removePath(docsSnapshotDir),
77
+ ensureCleanDirectory(resolved.distDir),
78
+ ensureCleanDirectory(generatedDir),
171
79
  ]);
172
80
 
173
- const snapshotConfig: ResolvedConfig = {
174
- ...resolved,
175
- appDir: snapshotDir,
176
- routesDir: path.join(snapshotDir, "routes"),
177
- rootModule: path.join(snapshotDir, "root.tsx"),
178
- middlewareFile: path.join(snapshotDir, "middleware.ts"),
179
- };
180
-
181
- const manifest = await buildRouteManifest(snapshotConfig);
81
+ const routeManifest = await buildRouteManifest(resolved);
182
82
  const entries = await generateClientEntries({
183
- config: snapshotConfig,
184
- manifest,
83
+ config: resolved,
84
+ manifest: routeManifest,
185
85
  generatedDir,
186
86
  });
187
87
 
188
- await ensureCleanDirectory(devClientDir);
189
-
190
- routeAssets = await bundleClientEntries({
88
+ const routeAssets = await bundleClientEntries({
191
89
  entries,
192
- outDir: devClientDir,
193
- dev: true,
194
- publicPrefix: "/__rbssr/client/",
90
+ outDir: distClientDir,
91
+ dev: false,
92
+ publicPrefix: "/client/",
195
93
  });
196
94
 
197
- currentServerSnapshotDir = snapshotDir;
198
-
199
- const staleVersions = (await listEntries(serverSnapshotsRoot))
200
- .filter(entry => entry.isDirectory && /^v\d+$/.test(entry.name))
201
- .map(entry => entry.name)
202
- .sort((a, b) => {
203
- const aNum = Number(a.slice(1));
204
- const bNum = Number(b.slice(1));
205
- return bNum - aNum;
206
- })
207
- .slice(3);
208
- await Promise.all(staleVersions.map(stale => removePath(path.join(serverSnapshotsRoot, stale))));
209
-
210
- version += 1;
211
- notifyReload();
212
- log(`rebuilt client assets (version ${version})`);
213
- };
214
-
215
- let rebuildQueue: Promise<void> = Promise.resolve();
216
- const enqueueRebuild = (force = false): Promise<void> => {
217
- const task = rebuildQueue.then(() => rebuildIfNeeded(force));
218
- rebuildQueue = task.catch(error => {
219
- // eslint-disable-next-line no-console
220
- console.error("[rbssr] rebuild failed", error);
221
- });
222
- return task;
223
- };
95
+ await copyDirRecursive(resolved.publicDir, distClientDir);
224
96
 
225
- await enqueueRebuild(true);
226
-
227
- let rebuildTimer: ReturnType<typeof setTimeout> | undefined;
228
- const scheduleRebuild = (): void => {
229
- if (rebuildTimer) {
230
- clearTimeout(rebuildTimer);
231
- }
97
+ const buildManifest = createBuildManifest(routeAssets);
98
+ await writeText(
99
+ path.join(resolved.distDir, "manifest.json"),
100
+ JSON.stringify(buildManifest, null, 2),
101
+ );
232
102
 
233
- rebuildTimer = setTimeout(() => {
234
- rebuildTimer = undefined;
235
- void enqueueRebuild(false);
236
- }, 75);
237
- };
103
+ await writeProductionServerEntrypoint({ distDir: resolved.distDir });
238
104
 
239
- const watchers: FSWatcher[] = [];
240
- for (const root of watchedRoots) {
241
- try {
242
- const watcher = watch(root, { recursive: true }, () => {
243
- scheduleRebuild();
244
- });
245
- watchers.push(watcher);
246
- } catch {
247
- log(`recursive file watching unavailable for ${root}; relying on request-time rebuild checks`);
248
- }
249
- }
105
+ log(`build complete: ${resolved.distDir}`);
106
+ });
107
+ }
250
108
 
251
- const cleanup = (): void => {
252
- if (rebuildTimer) {
253
- clearTimeout(rebuildTimer);
254
- rebuildTimer = undefined;
255
- }
256
- for (const watcher of watchers) {
257
- watcher.close();
258
- }
109
+ export async function runDev(cwd = process.cwd()): Promise<void> {
110
+ await getConfig(cwd);
111
+
112
+ const generatedDevDir = path.resolve(cwd, ".rbssr/generated/dev");
113
+ const generatedEntryPath = path.join(generatedDevDir, "entry.ts");
114
+ await ensureDir(generatedDevDir);
115
+ await writeTextIfChanged(generatedEntryPath, createDevHotEntrypointSource({
116
+ cwd,
117
+ runtimeModulePath: path.resolve(import.meta.dir, "dev-runtime.ts"),
118
+ }));
119
+
120
+ let activeChild: Bun.Subprocess<"inherit", "inherit", "inherit"> | null = null;
121
+ let shuttingDown = false;
122
+
123
+ const forwardSignal = (signal: NodeJS.Signals): void => {
124
+ shuttingDown = true;
125
+ activeChild?.kill(signal);
259
126
  };
260
127
 
261
128
  process.once("SIGINT", () => {
262
- cleanup();
263
- process.exit(0);
129
+ forwardSignal("SIGINT");
264
130
  });
265
131
 
266
132
  process.once("SIGTERM", () => {
267
- cleanup();
268
- process.exit(0);
133
+ forwardSignal("SIGTERM");
269
134
  });
270
135
 
271
- const server = createServer(
272
- {
273
- ...userConfig,
274
- mode: "development",
275
- },
276
- {
277
- dev: true,
278
- getDevAssets: () => routeAssets,
279
- reloadVersion: () => version,
280
- subscribeReload: listener => {
281
- reloadListeners.add(listener);
282
- return () => {
283
- reloadListeners.delete(listener);
284
- };
136
+ while (true) {
137
+ activeChild = Bun.spawn({
138
+ cmd: ["bun", "--hot", generatedEntryPath],
139
+ cwd,
140
+ stdin: "inherit",
141
+ stdout: "inherit",
142
+ stderr: "inherit",
143
+ env: {
144
+ ...process.env,
145
+ NODE_ENV: "development",
146
+ RBSSR_DEV_LAUNCHER: "1",
147
+ RBSSR_DEV_CHILD: "1",
285
148
  },
286
- resolvePaths: () => ({
287
- appDir: currentServerSnapshotDir,
288
- routesDir: path.join(currentServerSnapshotDir, "routes"),
289
- rootModule: path.join(currentServerSnapshotDir, "root.tsx"),
290
- middlewareFile: path.join(currentServerSnapshotDir, "middleware.ts"),
291
- }),
292
- onBeforeRequest: () => {
293
- return enqueueRebuild(false);
294
- },
295
- },
296
- );
297
-
298
- const bunServer = Bun.serve({
299
- hostname: resolved.host,
300
- port: resolved.port,
301
- fetch: server.fetch,
302
- development: true,
303
- });
149
+ });
150
+
151
+ const exitCode = await activeChild.exited;
152
+ activeChild = null;
304
153
 
305
- log(`dev server listening on ${bunServer.url}`);
154
+ if (shuttingDown) {
155
+ return;
156
+ }
157
+
158
+ if (exitCode === RBSSR_DEV_RESTART_EXIT_CODE) {
159
+ log("restarting dev child after config change");
160
+ continue;
161
+ }
162
+
163
+ if (exitCode !== 0) {
164
+ process.exit(exitCode);
165
+ }
166
+
167
+ return;
168
+ }
306
169
  }
307
170
 
308
171
  export async function runStart(cwd = process.cwd()): Promise<void> {
@@ -328,16 +191,11 @@ function runSubprocess(cmd: string[]): void {
328
191
  }
329
192
 
330
193
  export async function runTypecheck(): Promise<void> {
331
- runSubprocess(["bun", "x", "tsc", "--noEmit"]);
194
+ runSubprocess(createTypecheckCommand());
332
195
  }
333
196
 
334
197
  export async function runTest(extraArgs: string[]): Promise<void> {
335
- if (extraArgs.length > 0) {
336
- runSubprocess(["bun", "test", ...extraArgs]);
337
- return;
198
+ for (const cmd of createTestCommands(extraArgs)) {
199
+ runSubprocess(cmd);
338
200
  }
339
-
340
- runSubprocess(["bun", "test", "./tests/unit"]);
341
- runSubprocess(["bun", "test", "./tests/integration"]);
342
- runSubprocess(["bun", "x", "playwright", "test"]);
343
201
  }