react-bun-ssr 0.1.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 +205 -0
- package/bin/rbssr.ts +2 -0
- package/framework/cli/commands.ts +343 -0
- package/framework/cli/main.ts +63 -0
- package/framework/cli/scaffold.ts +97 -0
- package/framework/index.ts +1 -0
- package/framework/runtime/build-tools.ts +234 -0
- package/framework/runtime/bun-route-adapter.ts +200 -0
- package/framework/runtime/client-runtime.tsx +62 -0
- package/framework/runtime/config.ts +142 -0
- package/framework/runtime/deferred.ts +115 -0
- package/framework/runtime/helpers.ts +40 -0
- package/framework/runtime/index.ts +24 -0
- package/framework/runtime/io.ts +146 -0
- package/framework/runtime/markdown-routes.ts +319 -0
- package/framework/runtime/matcher.ts +89 -0
- package/framework/runtime/middleware.ts +26 -0
- package/framework/runtime/module-loader.ts +192 -0
- package/framework/runtime/render.tsx +370 -0
- package/framework/runtime/route-api.ts +17 -0
- package/framework/runtime/route-scanner.ts +242 -0
- package/framework/runtime/server.ts +744 -0
- package/framework/runtime/tree.tsx +77 -0
- package/framework/runtime/types.ts +213 -0
- package/framework/runtime/utils.ts +115 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# react-bun-ssr
|
|
2
|
+
|
|
3
|
+
`react-bun-ssr` is a TypeScript-first SSR React framework built for Bun.
|
|
4
|
+
|
|
5
|
+
Documentation: https://react-bun-ssr.fly.dev/docs/getting-started/introduction
|
|
6
|
+
|
|
7
|
+
This repository contains both:
|
|
8
|
+
- The framework implementation (`framework/**`, `bin/**`).
|
|
9
|
+
- The official documentation site built with the framework itself (`app/**`).
|
|
10
|
+
|
|
11
|
+
## Project layout
|
|
12
|
+
|
|
13
|
+
- `framework/`: runtime, router, renderer, build tooling, and CLI internals.
|
|
14
|
+
- `bin/rbssr.ts`: CLI entrypoint.
|
|
15
|
+
- `app/`: docs web app routes, layouts, middleware, and styles.
|
|
16
|
+
- `app/routes/docs/**/*.md`: hand-authored markdown docs routes.
|
|
17
|
+
- `app/routes/docs/_sidebar.ts`: docs navigation source of truth.
|
|
18
|
+
- `app/routes/docs/api/*.md`: generated API docs.
|
|
19
|
+
- `app/routes/docs/search-index.json`: generated search index.
|
|
20
|
+
- `tests/`: unit + integration tests.
|
|
21
|
+
- `e2e/`: Playwright end-to-end tests.
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- Bun `>= 1.3.9`
|
|
26
|
+
- macOS/Linux (or equivalent Bun-supported environment)
|
|
27
|
+
|
|
28
|
+
## Runtime API policy
|
|
29
|
+
|
|
30
|
+
- Prefer Bun APIs for file content I/O, hashing, build, and runtime operations.
|
|
31
|
+
- `node:path` is intentionally retained for robust path resolution behavior.
|
|
32
|
+
- `node:fs` is retained only for `watch` usage in dev mode.
|
|
33
|
+
|
|
34
|
+
## Run locally
|
|
35
|
+
|
|
36
|
+
Install dependencies:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bun install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Run docs site in development:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bun run docs:dev
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Open:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
http://127.0.0.1:3000
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Build production artifacts:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
bun run docs:build
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Start production server:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bun run docs:preview
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Useful commands
|
|
67
|
+
|
|
68
|
+
Framework:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bun run dev
|
|
72
|
+
bun run build
|
|
73
|
+
bun run start
|
|
74
|
+
bun run typecheck
|
|
75
|
+
bun run test
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Docs:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bun run docs:dev
|
|
82
|
+
bun run docs:check
|
|
83
|
+
bun run docs:build
|
|
84
|
+
bun run docs:preview
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Generated files
|
|
88
|
+
|
|
89
|
+
Do not hand-edit generated docs artifacts:
|
|
90
|
+
- `app/routes/docs/api/*.md`
|
|
91
|
+
- `app/routes/docs/search-index.json`
|
|
92
|
+
|
|
93
|
+
They are regenerated by:
|
|
94
|
+
- `bun run scripts/generate-api-docs.ts`
|
|
95
|
+
- `bun run scripts/build-search-index.ts`
|
|
96
|
+
- or automatically via `bun run docs:build`
|
|
97
|
+
|
|
98
|
+
## Collaboration guide
|
|
99
|
+
|
|
100
|
+
### 1. Create a branch
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git checkout -b feat/<short-description>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 2. Make changes
|
|
107
|
+
|
|
108
|
+
- Framework code: update `framework/**`.
|
|
109
|
+
- Docs content: update `app/routes/docs/**/*.md`.
|
|
110
|
+
- Nav changes: update `app/routes/docs/_sidebar.ts`.
|
|
111
|
+
- If exports change, regenerate API docs/search index.
|
|
112
|
+
|
|
113
|
+
### 3. Validate before pushing
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
bun run test
|
|
117
|
+
bun run docs:check
|
|
118
|
+
bun run docs:build
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 4. Commit with generated artifacts when needed
|
|
122
|
+
|
|
123
|
+
If your changes affect API docs or search corpus, commit updated files under `app/routes/docs/api/*.md` and `app/routes/docs/search-index.json`.
|
|
124
|
+
|
|
125
|
+
### 5. Open a PR
|
|
126
|
+
|
|
127
|
+
Include:
|
|
128
|
+
- Problem statement.
|
|
129
|
+
- Approach and tradeoffs.
|
|
130
|
+
- Testing performed.
|
|
131
|
+
- Screenshots/GIF for docs UI changes (if relevant).
|
|
132
|
+
|
|
133
|
+
## CI contract
|
|
134
|
+
|
|
135
|
+
CI runs:
|
|
136
|
+
- `bun run test:unit`
|
|
137
|
+
- `bun run test:integration`
|
|
138
|
+
- `bun run docs:check`
|
|
139
|
+
- `bun run docs:build`
|
|
140
|
+
|
|
141
|
+
`docs:check` fails when docs metadata, links, or generated artifacts are stale.
|
|
142
|
+
|
|
143
|
+
E2E (`bun run test:e2e`) runs on `push` to `main` only.
|
|
144
|
+
|
|
145
|
+
## Deploy to Fly.io
|
|
146
|
+
|
|
147
|
+
### Prerequisites
|
|
148
|
+
|
|
149
|
+
- Install `flyctl`: https://fly.io/docs/hands-on/install-flyctl/
|
|
150
|
+
- Login:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
fly auth login
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### One-time setup
|
|
157
|
+
|
|
158
|
+
`fly.toml` is included in this repository. Update the `app` name if needed:
|
|
159
|
+
|
|
160
|
+
- `fly.toml`
|
|
161
|
+
|
|
162
|
+
Initialize without deploying (optional):
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
fly launch --no-deploy
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Deploy
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
fly deploy
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Validate deployment
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
fly status
|
|
178
|
+
fly logs
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Open:
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
https://<your-fly-app>.fly.dev/docs/getting-started/introduction
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Rollback
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
fly releases
|
|
191
|
+
fly deploy --image <image-ref-from-release>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Scaling (optional)
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
fly scale vm shared-cpu-1x
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Optional CI deploy
|
|
201
|
+
|
|
202
|
+
The workflow includes an optional `deploy-fly` job on `main` push.
|
|
203
|
+
Set this repository secret before enabling production deploys:
|
|
204
|
+
|
|
205
|
+
- `FLY_API_TOKEN`
|
package/bin/rbssr.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { watch, type FSWatcher } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
buildRouteManifest,
|
|
5
|
+
bundleClientEntries,
|
|
6
|
+
copyDirRecursive,
|
|
7
|
+
createBuildManifest,
|
|
8
|
+
discoverFileSignature,
|
|
9
|
+
ensureCleanDirectory,
|
|
10
|
+
generateClientEntries,
|
|
11
|
+
} from "../runtime/build-tools";
|
|
12
|
+
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";
|
|
16
|
+
import { scaffoldApp } from "./scaffold";
|
|
17
|
+
|
|
18
|
+
function log(message: string): void {
|
|
19
|
+
// eslint-disable-next-line no-console
|
|
20
|
+
console.log(`[rbssr] ${message}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseFlags(args: string[]): { force: boolean } {
|
|
24
|
+
return {
|
|
25
|
+
force: args.includes("--force"),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function getConfig(cwd: string): Promise<{ userConfig: FrameworkConfig; resolved: ResolvedConfig }> {
|
|
30
|
+
const userConfig = await loadUserConfig(cwd);
|
|
31
|
+
const resolved = resolveConfig(userConfig, cwd);
|
|
32
|
+
return { userConfig, resolved };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function writeProductionServerEntrypoint(options: { distDir: string }): Promise<void> {
|
|
36
|
+
const serverDir = path.join(options.distDir, "server");
|
|
37
|
+
await ensureDir(serverDir);
|
|
38
|
+
|
|
39
|
+
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);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function runInit(args: string[], cwd = process.cwd()): Promise<void> {
|
|
66
|
+
const flags = parseFlags(args);
|
|
67
|
+
await scaffoldApp(cwd, {
|
|
68
|
+
force: flags.force,
|
|
69
|
+
});
|
|
70
|
+
log("project scaffolded");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
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
|
+
}
|
|
155
|
+
|
|
156
|
+
signature = nextSignature;
|
|
157
|
+
|
|
158
|
+
const snapshotDir = path.join(serverSnapshotsRoot, `v${version + 1}`);
|
|
159
|
+
const hasDocsSourceDir = await existsPath(docsSourceDir);
|
|
160
|
+
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),
|
|
171
|
+
]);
|
|
172
|
+
|
|
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);
|
|
182
|
+
const entries = await generateClientEntries({
|
|
183
|
+
config: snapshotConfig,
|
|
184
|
+
manifest,
|
|
185
|
+
generatedDir,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await ensureCleanDirectory(devClientDir);
|
|
189
|
+
|
|
190
|
+
routeAssets = await bundleClientEntries({
|
|
191
|
+
entries,
|
|
192
|
+
outDir: devClientDir,
|
|
193
|
+
dev: true,
|
|
194
|
+
publicPrefix: "/__rbssr/client/",
|
|
195
|
+
});
|
|
196
|
+
|
|
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
|
+
};
|
|
224
|
+
|
|
225
|
+
await enqueueRebuild(true);
|
|
226
|
+
|
|
227
|
+
let rebuildTimer: ReturnType<typeof setTimeout> | undefined;
|
|
228
|
+
const scheduleRebuild = (): void => {
|
|
229
|
+
if (rebuildTimer) {
|
|
230
|
+
clearTimeout(rebuildTimer);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
rebuildTimer = setTimeout(() => {
|
|
234
|
+
rebuildTimer = undefined;
|
|
235
|
+
void enqueueRebuild(false);
|
|
236
|
+
}, 75);
|
|
237
|
+
};
|
|
238
|
+
|
|
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
|
+
}
|
|
250
|
+
|
|
251
|
+
const cleanup = (): void => {
|
|
252
|
+
if (rebuildTimer) {
|
|
253
|
+
clearTimeout(rebuildTimer);
|
|
254
|
+
rebuildTimer = undefined;
|
|
255
|
+
}
|
|
256
|
+
for (const watcher of watchers) {
|
|
257
|
+
watcher.close();
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
process.once("SIGINT", () => {
|
|
262
|
+
cleanup();
|
|
263
|
+
process.exit(0);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
process.once("SIGTERM", () => {
|
|
267
|
+
cleanup();
|
|
268
|
+
process.exit(0);
|
|
269
|
+
});
|
|
270
|
+
|
|
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
|
+
};
|
|
285
|
+
},
|
|
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
|
+
});
|
|
304
|
+
|
|
305
|
+
log(`dev server listening on ${bunServer.url}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function runStart(cwd = process.cwd()): Promise<void> {
|
|
309
|
+
const { resolved } = await getConfig(cwd);
|
|
310
|
+
const serverEntry = path.join(resolved.distDir, "server", "server.mjs");
|
|
311
|
+
if (!(await existsPath(serverEntry))) {
|
|
312
|
+
throw new Error("Missing dist/server/server.mjs. Run `rbssr build` first.");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
runSubprocess(["bun", serverEntry]);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function runSubprocess(cmd: string[]): void {
|
|
319
|
+
const subprocess = Bun.spawnSync({
|
|
320
|
+
cmd,
|
|
321
|
+
stdout: "inherit",
|
|
322
|
+
stderr: "inherit",
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (subprocess.exitCode !== 0) {
|
|
326
|
+
process.exit(subprocess.exitCode);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function runTypecheck(): Promise<void> {
|
|
331
|
+
runSubprocess(["bun", "x", "tsc", "--noEmit"]);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function runTest(extraArgs: string[]): Promise<void> {
|
|
335
|
+
if (extraArgs.length > 0) {
|
|
336
|
+
runSubprocess(["bun", "test", ...extraArgs]);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
runSubprocess(["bun", "test", "./tests/unit"]);
|
|
341
|
+
runSubprocess(["bun", "test", "./tests/integration"]);
|
|
342
|
+
runSubprocess(["bun", "x", "playwright", "test"]);
|
|
343
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
runBuild,
|
|
5
|
+
runDev,
|
|
6
|
+
runInit,
|
|
7
|
+
runStart,
|
|
8
|
+
runTest,
|
|
9
|
+
runTypecheck,
|
|
10
|
+
} from "./commands";
|
|
11
|
+
|
|
12
|
+
function printHelp(): void {
|
|
13
|
+
// eslint-disable-next-line no-console
|
|
14
|
+
console.log(`rbssr commands:
|
|
15
|
+
rbssr init [--force]
|
|
16
|
+
rbssr dev
|
|
17
|
+
rbssr build
|
|
18
|
+
rbssr start
|
|
19
|
+
rbssr typecheck
|
|
20
|
+
rbssr test [bun-test-args]
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function main(argv: string[]): Promise<void> {
|
|
25
|
+
const [command = "help", ...rest] = argv;
|
|
26
|
+
|
|
27
|
+
switch (command) {
|
|
28
|
+
case "init":
|
|
29
|
+
await runInit(rest);
|
|
30
|
+
return;
|
|
31
|
+
case "dev":
|
|
32
|
+
await runDev();
|
|
33
|
+
return;
|
|
34
|
+
case "build":
|
|
35
|
+
await runBuild();
|
|
36
|
+
return;
|
|
37
|
+
case "start":
|
|
38
|
+
await runStart();
|
|
39
|
+
return;
|
|
40
|
+
case "typecheck":
|
|
41
|
+
await runTypecheck();
|
|
42
|
+
return;
|
|
43
|
+
case "test":
|
|
44
|
+
await runTest(rest);
|
|
45
|
+
return;
|
|
46
|
+
case "help":
|
|
47
|
+
case "--help":
|
|
48
|
+
case "-h":
|
|
49
|
+
printHelp();
|
|
50
|
+
return;
|
|
51
|
+
default:
|
|
52
|
+
// eslint-disable-next-line no-console
|
|
53
|
+
console.error(`Unknown command: ${command}`);
|
|
54
|
+
printHelp();
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
main(process.argv.slice(2)).catch(error => {
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.error(error);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { existsPath, writeText } from "../runtime/io";
|
|
3
|
+
|
|
4
|
+
interface ScaffoldFile {
|
|
5
|
+
filePath: string;
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function writeIfMissing(filePath: string, content: string, force: boolean): Promise<void> {
|
|
10
|
+
if (!force && await existsPath(filePath)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
await writeText(filePath, content);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function templateFiles(cwd: string): ScaffoldFile[] {
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
filePath: path.join(cwd, "rbssr.config.ts"),
|
|
21
|
+
content: `import { defineConfig } from "react-bun-ssr";
|
|
22
|
+
|
|
23
|
+
export default defineConfig({
|
|
24
|
+
appDir: "app",
|
|
25
|
+
port: 3000,
|
|
26
|
+
});
|
|
27
|
+
`,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
filePath: path.join(cwd, "app/root.tsx"),
|
|
31
|
+
content: `import { Outlet } from "react-bun-ssr/route";
|
|
32
|
+
|
|
33
|
+
export default function RootLayout() {
|
|
34
|
+
return (
|
|
35
|
+
<main className="shell">
|
|
36
|
+
<header className="top">
|
|
37
|
+
<h1>react-bun-ssr</h1>
|
|
38
|
+
</header>
|
|
39
|
+
<Outlet />
|
|
40
|
+
</main>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function head() {
|
|
45
|
+
return <title>react-bun-ssr app</title>;
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
filePath: path.join(cwd, "app/routes/index.tsx"),
|
|
51
|
+
content: `import { useLoaderData } from "react-bun-ssr/route";
|
|
52
|
+
|
|
53
|
+
export function loader() {
|
|
54
|
+
return {
|
|
55
|
+
message: "Hello from SSR",
|
|
56
|
+
now: new Date().toISOString(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default function IndexRoute() {
|
|
61
|
+
const data = useLoaderData<{ message: string; now: string }>();
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<section>
|
|
65
|
+
<p>{data.message}</p>
|
|
66
|
+
<small>{data.now}</small>
|
|
67
|
+
</section>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
`,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
filePath: path.join(cwd, "app/routes/api/health.ts"),
|
|
74
|
+
content: `export function GET() {
|
|
75
|
+
return Response.json({ status: "ok" });
|
|
76
|
+
}
|
|
77
|
+
`,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
filePath: path.join(cwd, "app/middleware.ts"),
|
|
81
|
+
content: `import type { Middleware } from "react-bun-ssr/route";
|
|
82
|
+
|
|
83
|
+
export const middleware: Middleware = async (ctx, next) => {
|
|
84
|
+
const response = await next();
|
|
85
|
+
response.headers.set("x-powered-by", "react-bun-ssr");
|
|
86
|
+
return response;
|
|
87
|
+
};
|
|
88
|
+
`,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function scaffoldApp(cwd: string, options: { force: boolean }): Promise<void> {
|
|
94
|
+
for (const file of templateFiles(cwd)) {
|
|
95
|
+
await writeIfMissing(file.filePath, file.content, options.force);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./runtime";
|