react-bun-ssr 0.2.0 → 0.3.1

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,16 +50,22 @@ 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
 
57
57
  `rbssr init` scaffolds a small Bun-first SSR app:
58
58
 
59
59
  ```text
60
+ package.json
61
+ tsconfig.json
62
+ .gitignore
60
63
  app/
61
64
  root.tsx
65
+ root.module.css
62
66
  middleware.ts
67
+ public/
68
+ favicon.svg
63
69
  routes/
64
70
  index.tsx
65
71
  api/
@@ -67,15 +73,20 @@ app/
67
73
  rbssr.config.ts
68
74
  ```
69
75
 
76
+ - `package.json`: Bun scripts and framework/runtime dependencies
77
+ - `tsconfig.json`: starter TypeScript config for Bun + JSX
78
+ - `.gitignore`: minimal app-level ignore rules
70
79
  - `app/root.tsx`: document shell and top-level layout
80
+ - `app/root.module.css`: starter CSS Module for layout and base presentation
71
81
  - `app/middleware.ts`: global request pipeline hook
82
+ - `app/public/favicon.svg`: starter public asset
72
83
  - `app/routes/index.tsx`: first SSR page route
73
84
  - `app/routes/api/health.ts`: first API route
74
85
  - `rbssr.config.ts`: runtime configuration entrypoint
75
86
 
76
87
  The quickest follow-up is:
77
88
 
78
- - https://react-bun-ssr.fly.dev/docs/start/quick-start
89
+ - https://react-bun-ssr.dev/docs/start/quick-start
79
90
 
80
91
  ## How it works
81
92
 
@@ -85,7 +96,7 @@ Routes live under `app/routes`. Page routes, API routes, dynamic params, and mar
85
96
 
86
97
  Read more:
87
98
 
88
- - https://react-bun-ssr.fly.dev/docs/routing/file-based-routing
99
+ - https://react-bun-ssr.dev/docs/routing/file-based-routing
89
100
 
90
101
  ### Request pipeline
91
102
 
@@ -93,8 +104,8 @@ For a page request, the framework resolves the matching route, runs global and n
93
104
 
94
105
  Read more:
95
106
 
96
- - https://react-bun-ssr.fly.dev/docs/routing/middleware
97
- - https://react-bun-ssr.fly.dev/docs/data/loaders
107
+ - https://react-bun-ssr.dev/docs/routing/middleware
108
+ - https://react-bun-ssr.dev/docs/data/loaders
98
109
 
99
110
  ### Rendering model
100
111
 
@@ -102,8 +113,8 @@ SSR is the default model. HTML responses stream, deferred loader data is support
102
113
 
103
114
  Read more:
104
115
 
105
- - https://react-bun-ssr.fly.dev/docs/rendering/streaming-deferred
106
- - https://react-bun-ssr.fly.dev/docs/routing/navigation
116
+ - https://react-bun-ssr.dev/docs/rendering/streaming-deferred
117
+ - https://react-bun-ssr.dev/docs/routing/navigation
107
118
 
108
119
  ### Bun-first runtime
109
120
 
@@ -111,7 +122,7 @@ Bun provides the runtime, server, bundler, markdown support, and file APIs that
111
122
 
112
123
  Read more:
113
124
 
114
- - https://react-bun-ssr.fly.dev/docs/api/bun-runtime-apis
125
+ - https://react-bun-ssr.dev/docs/api/bun-runtime-apis
115
126
 
116
127
  ## Core commands
117
128
 
@@ -132,7 +143,7 @@ Repository maintenance commands:
132
143
 
133
144
  CLI reference:
134
145
 
135
- - https://react-bun-ssr.fly.dev/docs/tooling/cli
146
+ - https://react-bun-ssr.dev/docs/tooling/cli
136
147
 
137
148
  ## Working on this repository
138
149
 
@@ -147,16 +158,24 @@ bun run docs:dev
147
158
 
148
159
  That starts the docs site locally using the framework itself.
149
160
 
161
+ Dependency ownership is split intentionally:
162
+
163
+ - the repo-root `package.json` is the published framework manifest
164
+ - [`app/package.json`](/Users/react-formation/code/my-app/app/package.json) owns docs-app runtime dependencies
165
+
166
+ 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.
167
+
150
168
  ## Project layout
151
169
 
152
170
  - `framework/`: runtime, renderer, route handling, build tooling, and CLI internals
153
171
  - `bin/rbssr.ts`: CLI entrypoint
154
172
  - `app/`: docs site routes, layouts, middleware, blog, and styles
173
+ - `app/package.json`: private docs-app dependency manifest
155
174
  - `app/routes/docs/**/*.md`: authored documentation pages
156
175
  - `app/routes/blog/*.md`: authored blog posts
157
176
  - `scripts/`: generators and validation scripts
158
- - `tests/`: unit and integration tests
159
- - `e2e/`: Playwright end-to-end tests
177
+ - `tests/framework/`: framework runtime, CLI, build, unit/integration, and framework Playwright tests
178
+ - `tests/docs-app/`: docs site, blog, analytics, and docs-app Playwright tests
160
179
 
161
180
  ## Contributing
162
181
 
@@ -184,6 +203,6 @@ fly deploy
184
203
 
185
204
  Full deployment docs:
186
205
 
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
206
+ - https://react-bun-ssr.dev/docs/deployment/bun-deployment
207
+ - https://react-bun-ssr.dev/docs/deployment/configuration
208
+ - https://react-bun-ssr.dev/docs/deployment/troubleshooting
@@ -25,6 +25,21 @@ function log(message: string): void {
25
25
  console.log(`[rbssr] ${message}`);
26
26
  }
27
27
 
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
+ }
41
+ }
42
+
28
43
  async function getConfig(cwd: string): Promise<{ userConfig: FrameworkConfig; resolved: ResolvedConfig }> {
29
44
  const userConfig = await loadUserConfig(cwd);
30
45
  const resolved = resolveConfig(userConfig, cwd);
@@ -48,41 +63,47 @@ export async function runInit(args: string[], cwd = process.cwd()): Promise<void
48
63
  }
49
64
 
50
65
  export async function runBuild(cwd = process.cwd()): Promise<void> {
51
- const { resolved } = await getConfig(cwd);
52
-
53
- const distClientDir = path.join(resolved.distDir, "client");
54
- const generatedDir = path.resolve(cwd, ".rbssr/generated/client-entries");
55
-
56
- await Promise.all([
57
- ensureCleanDirectory(resolved.distDir),
58
- ensureCleanDirectory(generatedDir),
59
- ]);
60
-
61
- const routeManifest = await buildRouteManifest(resolved);
62
- const entries = await generateClientEntries({
63
- config: resolved,
64
- manifest: routeManifest,
65
- generatedDir,
66
- });
66
+ await withNodeEnv("production", async () => {
67
+ const userConfig = await loadUserConfig(cwd);
68
+ const resolved = resolveConfig({
69
+ ...userConfig,
70
+ mode: "production",
71
+ }, cwd);
72
+
73
+ const distClientDir = path.join(resolved.distDir, "client");
74
+ const generatedDir = path.resolve(cwd, ".rbssr/generated/client-entries");
75
+
76
+ await Promise.all([
77
+ ensureCleanDirectory(resolved.distDir),
78
+ ensureCleanDirectory(generatedDir),
79
+ ]);
80
+
81
+ const routeManifest = await buildRouteManifest(resolved);
82
+ const entries = await generateClientEntries({
83
+ config: resolved,
84
+ manifest: routeManifest,
85
+ generatedDir,
86
+ });
67
87
 
68
- const routeAssets = await bundleClientEntries({
69
- entries,
70
- outDir: distClientDir,
71
- dev: false,
72
- publicPrefix: "/client/",
73
- });
88
+ const routeAssets = await bundleClientEntries({
89
+ entries,
90
+ outDir: distClientDir,
91
+ dev: false,
92
+ publicPrefix: "/client/",
93
+ });
74
94
 
75
- await copyDirRecursive(resolved.publicDir, distClientDir);
95
+ await copyDirRecursive(resolved.publicDir, distClientDir);
76
96
 
77
- const buildManifest = createBuildManifest(routeAssets);
78
- await writeText(
79
- path.join(resolved.distDir, "manifest.json"),
80
- JSON.stringify(buildManifest, null, 2),
81
- );
97
+ const buildManifest = createBuildManifest(routeAssets);
98
+ await writeText(
99
+ path.join(resolved.distDir, "manifest.json"),
100
+ JSON.stringify(buildManifest, null, 2),
101
+ );
82
102
 
83
- await writeProductionServerEntrypoint({ distDir: resolved.distDir });
103
+ await writeProductionServerEntrypoint({ distDir: resolved.distDir });
84
104
 
85
- log(`build complete: ${resolved.distDir}`);
105
+ log(`build complete: ${resolved.distDir}`);
106
+ });
86
107
  }
87
108
 
88
109
  export async function runDev(cwd = process.cwd()): Promise<void> {
@@ -121,6 +142,7 @@ export async function runDev(cwd = process.cwd()): Promise<void> {
121
142
  stderr: "inherit",
122
143
  env: {
123
144
  ...process.env,
145
+ NODE_ENV: "development",
124
146
  RBSSR_DEV_LAUNCHER: "1",
125
147
  RBSSR_DEV_CHILD: "1",
126
148
  },
@@ -89,6 +89,13 @@ export function createDevClientWatch(options: {
89
89
  let metafilePoller: ReturnType<typeof setInterval> | undefined;
90
90
  let lastMetafileMtime = "";
91
91
 
92
+ const resetReadyState = (): void => {
93
+ const deferred = createDeferred();
94
+ state.readyPromise = deferred.promise;
95
+ state.resolveReady = deferred.resolve;
96
+ state.rejectReady = deferred.reject;
97
+ };
98
+
92
99
  const stopPolling = (): void => {
93
100
  if (metafilePoller) {
94
101
  clearInterval(metafilePoller);
@@ -152,10 +159,6 @@ export function createDevClientWatch(options: {
152
159
  await removePath(options.metafilePath);
153
160
 
154
161
  const previousOutputFiles = [...state.outputFiles];
155
- const deferred = createDeferred();
156
- state.readyPromise = deferred.promise;
157
- state.resolveReady = deferred.resolve;
158
- state.rejectReady = deferred.reject;
159
162
  state.buildCount = 0;
160
163
  state.outputFiles = new Set<string>();
161
164
  lastMetafileMtime = "";
@@ -195,7 +198,10 @@ export function createDevClientWatch(options: {
195
198
  stdin: "ignore",
196
199
  stdout: "inherit",
197
200
  stderr: "inherit",
198
- env: process.env,
201
+ env: {
202
+ ...process.env,
203
+ NODE_ENV: "development",
204
+ },
199
205
  });
200
206
 
201
207
  void state.process.exited.then((exitCode) => {
@@ -244,6 +250,7 @@ export function createDevClientWatch(options: {
244
250
  }
245
251
 
246
252
  state.entrySetSignature = nextEntrySetSignature;
253
+ resetReadyState();
247
254
  await stopProcess();
248
255
  await startProcess();
249
256
  options.onLog?.("restarted Bun client watch after entry set change");
@@ -22,11 +22,14 @@ export function parseFlags(args: string[]): CliFlags {
22
22
 
23
23
  export function createProductionServerEntrypointSource(): string {
24
24
  return `import path from "node:path";
25
- import config from "../../rbssr.config.ts";
26
25
  import { startHttpServer } from "react-bun-ssr";
27
26
 
27
+ process.env.NODE_ENV = "production";
28
+
28
29
  const rootDir = path.resolve(path.dirname(Bun.fileURLToPath(import.meta.url)), "../..");
29
30
  process.chdir(rootDir);
31
+ const configModule = await import("../../rbssr.config.ts");
32
+ const config = configModule.default;
30
33
 
31
34
  const manifestPath = path.resolve(rootDir, "dist/manifest.json");
32
35
  const manifest = await Bun.file(manifestPath).json();
@@ -53,6 +56,7 @@ export function createDevHotEntrypointSource(options: {
53
56
 
54
57
  return `import { runHotDevChild } from ${runtimeModulePath};
55
58
 
59
+ process.env.NODE_ENV = "development";
56
60
  process.chdir(${cwd});
57
61
  await runHotDevChild({
58
62
  cwd: ${cwd},
@@ -93,8 +97,8 @@ export function createTestCommands(extraArgs: string[]): string[][] {
93
97
  }
94
98
 
95
99
  return [
96
- ["bun", "test", "./tests/unit"],
97
- ["bun", "test", "./tests/integration"],
100
+ ["bun", "test", "./tests/framework/unit", "./tests/docs-app/unit"],
101
+ ["bun", "test", "./tests/framework/integration", "./tests/docs-app/integration"],
98
102
  ["bun", "x", "playwright", "test"],
99
103
  ];
100
104
  }
@@ -6,6 +6,23 @@ interface ScaffoldFile {
6
6
  content: string;
7
7
  }
8
8
 
9
+ interface FrameworkPackageManifest {
10
+ version?: string;
11
+ devDependencies?: Record<string, string>;
12
+ }
13
+
14
+ const DEFAULT_FRAMEWORK_VERSION = "0.0.0";
15
+ const DEFAULT_TYPESCRIPT_VERSION = "^5";
16
+ const DEFAULT_BUN_TYPES_VERSION = "latest";
17
+ const DEFAULT_REACT_TYPES_VERSION = "^19";
18
+ const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
19
+ <rect width="96" height="96" rx="20" fill="#111827"/>
20
+ <path d="M25 28h20c13.807 0 25 11.193 25 25S58.807 78 45 78H25V28Zm18 40c8.837 0 16-7.163 16-16s-7.163-16-16-16h-8v32h8Z" fill="#f9fafb"/>
21
+ </svg>
22
+ `;
23
+
24
+ let frameworkPackageManifestPromise: Promise<FrameworkPackageManifest> | null = null;
25
+
9
26
  async function writeIfMissing(filePath: string, content: string, force: boolean): Promise<void> {
10
27
  if (!force && await existsPath(filePath)) {
11
28
  return;
@@ -14,8 +31,115 @@ async function writeIfMissing(filePath: string, content: string, force: boolean)
14
31
  await writeText(filePath, content);
15
32
  }
16
33
 
17
- function templateFiles(cwd: string): ScaffoldFile[] {
34
+ function getFrameworkPackageManifest(): Promise<FrameworkPackageManifest> {
35
+ if (!frameworkPackageManifestPromise) {
36
+ const packageJsonPath = path.resolve(import.meta.dir, "../../package.json");
37
+ frameworkPackageManifestPromise = Bun.file(packageJsonPath).json() as Promise<FrameworkPackageManifest>;
38
+ }
39
+
40
+ return frameworkPackageManifestPromise;
41
+ }
42
+
43
+ function normalizePackageName(cwd: string): string {
44
+ const baseName = path.basename(path.resolve(cwd));
45
+ const normalized = baseName
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9._-]+/g, "-")
48
+ .replace(/[._-]{2,}/g, "-")
49
+ .replace(/^[._-]+|[._-]+$/g, "");
50
+
51
+ return normalized || "rbssr-app";
52
+ }
53
+
54
+ function createPackageJsonContent(options: {
55
+ cwd: string;
56
+ frameworkVersion: string;
57
+ typescriptVersion: string;
58
+ bunTypesVersion: string;
59
+ reactTypesVersion: string;
60
+ reactDomTypesVersion: string;
61
+ }): string {
62
+ const packageJson = {
63
+ name: normalizePackageName(options.cwd),
64
+ version: "0.0.0",
65
+ private: true,
66
+ type: "module",
67
+ scripts: {
68
+ dev: "rbssr dev",
69
+ build: "rbssr build",
70
+ start: "rbssr start",
71
+ typecheck: "bunx tsc --noEmit",
72
+ },
73
+ dependencies: {
74
+ "react-bun-ssr": options.frameworkVersion,
75
+ react: "^19",
76
+ "react-dom": "^19",
77
+ },
78
+ devDependencies: {
79
+ "@types/react": options.reactTypesVersion,
80
+ "@types/react-dom": options.reactDomTypesVersion,
81
+ "bun-types": options.bunTypesVersion,
82
+ typescript: options.typescriptVersion,
83
+ },
84
+ };
85
+
86
+ return `${JSON.stringify(packageJson, null, 2)}\n`;
87
+ }
88
+
89
+ function createTsconfigContent(): string {
90
+ const tsconfig = {
91
+ compilerOptions: {
92
+ target: "ESNext",
93
+ module: "Preserve",
94
+ moduleResolution: "Bundler",
95
+ jsx: "react-jsx",
96
+ strict: true,
97
+ types: ["bun-types"],
98
+ },
99
+ include: ["app", "rbssr.config.ts"],
100
+ };
101
+
102
+ return `${JSON.stringify(tsconfig, null, 2)}\n`;
103
+ }
104
+
105
+ function createGitignoreContent(): string {
106
+ return `node_modules
107
+ dist
108
+ .rbssr
109
+ .env
110
+ .env.local
111
+ .DS_Store
112
+ `;
113
+ }
114
+
115
+ async function templateFiles(cwd: string): Promise<ScaffoldFile[]> {
116
+ const frameworkPackage = await getFrameworkPackageManifest();
117
+ const frameworkVersion = frameworkPackage.version ?? DEFAULT_FRAMEWORK_VERSION;
118
+ const typescriptVersion = frameworkPackage.devDependencies?.typescript ?? DEFAULT_TYPESCRIPT_VERSION;
119
+ const bunTypesVersion = frameworkPackage.devDependencies?.["bun-types"] ?? DEFAULT_BUN_TYPES_VERSION;
120
+ const reactTypesVersion = frameworkPackage.devDependencies?.["@types/react"] ?? DEFAULT_REACT_TYPES_VERSION;
121
+ const reactDomTypesVersion = frameworkPackage.devDependencies?.["@types/react-dom"] ?? DEFAULT_REACT_TYPES_VERSION;
122
+
18
123
  return [
124
+ {
125
+ filePath: path.join(cwd, "package.json"),
126
+ content: createPackageJsonContent({
127
+ cwd,
128
+ frameworkVersion,
129
+ typescriptVersion,
130
+ bunTypesVersion,
131
+ reactTypesVersion,
132
+ reactDomTypesVersion,
133
+ }),
134
+ },
135
+ {
136
+ filePath: path.join(cwd, "tsconfig.json"),
137
+ content: createTsconfigContent(),
138
+ },
139
+ {
140
+ filePath: path.join(cwd, ".gitignore"),
141
+ content: createGitignoreContent(),
142
+ },
19
143
  {
20
144
  filePath: path.join(cwd, "rbssr.config.ts"),
21
145
  content: `import { defineConfig } from "react-bun-ssr";
@@ -29,14 +153,17 @@ export default defineConfig({
29
153
  {
30
154
  filePath: path.join(cwd, "app/root.tsx"),
31
155
  content: `import { Outlet } from "react-bun-ssr/route";
156
+ import styles from "./root.module.css";
32
157
 
33
158
  export default function RootLayout() {
34
159
  return (
35
- <main className="shell">
36
- <header className="top">
160
+ <main className={styles.shell}>
161
+ <header className={styles.top}>
37
162
  <h1>react-bun-ssr</h1>
38
163
  </header>
39
- <Outlet />
164
+ <section className={styles.content}>
165
+ <Outlet />
166
+ </section>
40
167
  </main>
41
168
  );
42
169
  }
@@ -44,6 +171,53 @@ export default function RootLayout() {
44
171
  export function head() {
45
172
  return <title>react-bun-ssr app</title>;
46
173
  }
174
+ `,
175
+ },
176
+ {
177
+ filePath: path.join(cwd, "app/root.module.css"),
178
+ content: `:global(*) {
179
+ box-sizing: border-box;
180
+ }
181
+
182
+ :global(html) {
183
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
184
+ background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
185
+ color: #0f172a;
186
+ }
187
+
188
+ :global(body) {
189
+ margin: 0;
190
+ }
191
+
192
+ .shell {
193
+ min-height: 100vh;
194
+ width: min(100%, 72rem);
195
+ margin: 0 auto;
196
+ padding: 3rem 1.5rem 4rem;
197
+ }
198
+
199
+ .top {
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: space-between;
203
+ margin-bottom: 2rem;
204
+ }
205
+
206
+ .top h1 {
207
+ margin: 0;
208
+ font-size: clamp(2rem, 5vw, 3.5rem);
209
+ line-height: 1;
210
+ letter-spacing: -0.04em;
211
+ }
212
+
213
+ .content {
214
+ padding: 1.5rem;
215
+ border: 1px solid rgba(15, 23, 42, 0.08);
216
+ border-radius: 1.5rem;
217
+ background: rgba(255, 255, 255, 0.84);
218
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.12);
219
+ backdrop-filter: blur(18px);
220
+ }
47
221
  `,
48
222
  },
49
223
  {
@@ -87,11 +261,15 @@ export const middleware: Middleware = async (ctx, next) => {
87
261
  };
88
262
  `,
89
263
  },
264
+ {
265
+ filePath: path.join(cwd, "app/public/favicon.svg"),
266
+ content: FAVICON_SVG,
267
+ },
90
268
  ];
91
269
  }
92
270
 
93
271
  export async function scaffoldApp(cwd: string, options: { force: boolean }): Promise<void> {
94
- for (const file of templateFiles(cwd)) {
272
+ for (const file of await templateFiles(cwd)) {
95
273
  await writeIfMissing(file.filePath, file.content, options.force);
96
274
  }
97
275
  }
@@ -23,7 +23,6 @@ const BUILD_OPTIMIZE_IMPORTS = [
23
23
  'react-bun-ssr/route',
24
24
  'react',
25
25
  'react-dom',
26
- '@datadog/browser-rum-react',
27
26
  ];
28
27
 
29
28
  export interface ClientEntryFile {
@@ -338,6 +337,9 @@ export async function bundleClientEntries(options: {
338
337
  sourcemap: dev ? 'inline' : 'external',
339
338
  minify: !dev,
340
339
  naming: dev ? '[name].[ext]' : '[name]-[hash].[ext]',
340
+ define: {
341
+ "process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
342
+ },
341
343
  });
342
344
 
343
345
  if (!result.success) {