react-bun-ssr 0.1.0 → 0.1.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 +116 -132
- package/framework/runtime/build-tools.ts +152 -48
- package/framework/runtime/client-runtime.tsx +1277 -15
- package/framework/runtime/config.ts +4 -1
- package/framework/runtime/index.ts +6 -0
- package/framework/runtime/io.ts +1 -1
- package/framework/runtime/link.tsx +205 -0
- package/framework/runtime/markdown-headings.ts +54 -0
- package/framework/runtime/markdown-routes.ts +8 -26
- package/framework/runtime/module-loader.ts +172 -47
- package/framework/runtime/navigation-api.ts +223 -0
- package/framework/runtime/render.tsx +56 -92
- package/framework/runtime/route-api.ts +6 -0
- package/framework/runtime/route-errors.ts +166 -0
- package/framework/runtime/router.ts +80 -0
- package/framework/runtime/runtime-constants.ts +4 -0
- package/framework/runtime/server.ts +696 -71
- package/framework/runtime/tree.tsx +171 -3
- package/framework/runtime/types.ts +70 -3
- package/framework/runtime/utils.ts +6 -5
- package/package.json +18 -5
package/README.md
CHANGED
|
@@ -1,48 +1,45 @@
|
|
|
1
1
|
# react-bun-ssr
|
|
2
2
|
|
|
3
|
-
`react-bun-ssr` is a
|
|
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
|
|
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
|
|
8
|
+
- Repository: https://github.com/react-formation/react-bun-ssr
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
- The framework implementation (`framework/**`, `bin/**`).
|
|
9
|
-
- The official documentation site built with the framework itself (`app/**`).
|
|
10
|
+
## Why react-bun-ssr?
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
`react-bun-ssr` exists for teams that want server-rendered React without starting from Node-first assumptions.
|
|
12
13
|
|
|
13
|
-
|
|
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.
|
|
14
|
+
It is designed around Bun's runtime, server, bundler, markdown support, and file APIs instead of treating Bun as a compatibility layer. The goal is to stay small enough to understand, but complete enough to use seriously for real SSR applications. The documentation site in this repository is built with the framework itself, so the framework is continuously exercised by its own product surface.
|
|
22
15
|
|
|
23
|
-
##
|
|
16
|
+
## What it includes
|
|
24
17
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
18
|
+
- [File-based routing](/docs/routing/file-based-routing) for pages, APIs, dynamic params, and markdown routes
|
|
19
|
+
- [Layouts, route groups, and middleware](/docs/routing/layouts-and-groups) with a dedicated [middleware pipeline](/docs/routing/middleware)
|
|
20
|
+
- [Loaders and actions](/docs/data/loaders) for explicit data fetching and mutation flow
|
|
21
|
+
- [Streaming SSR and deferred data](/docs/rendering/streaming-deferred)
|
|
22
|
+
- Soft client transitions with [`Link` and `useRouter`](/docs/routing/navigation)
|
|
23
|
+
- [Bun-first runtime, build, and deployment model](/docs/deployment/bun-deployment)
|
|
24
|
+
- [CSS Modules and static asset support](/docs/styling/css-modules)
|
|
25
|
+
- [Response header config and static caching defaults](/docs/deployment/configuration)
|
|
27
26
|
|
|
28
|
-
##
|
|
27
|
+
## Installation
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
- `node:path` is intentionally retained for robust path resolution behavior.
|
|
32
|
-
- `node:fs` is retained only for `watch` usage in dev mode.
|
|
29
|
+
Prerequisites:
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
- Bun `>= 1.3.10`
|
|
32
|
+
- `rbssr` available on PATH in the workflow you use to start a new app
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
Minimal setup:
|
|
37
35
|
|
|
38
36
|
```bash
|
|
37
|
+
bun --version
|
|
38
|
+
mkdir my-app
|
|
39
|
+
cd my-app
|
|
40
|
+
rbssr init
|
|
39
41
|
bun install
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Run docs site in development:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
bun run docs:dev
|
|
42
|
+
bun run dev
|
|
46
43
|
```
|
|
47
44
|
|
|
48
45
|
Open:
|
|
@@ -51,155 +48,142 @@ Open:
|
|
|
51
48
|
http://127.0.0.1:3000
|
|
52
49
|
```
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
bun run docs:build
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Start production server:
|
|
51
|
+
For the full setup walkthrough, read the installation guide:
|
|
61
52
|
|
|
62
|
-
|
|
63
|
-
bun run docs:preview
|
|
64
|
-
```
|
|
53
|
+
- https://react-bun-ssr.fly.dev/docs/start/installation
|
|
65
54
|
|
|
66
|
-
##
|
|
55
|
+
## What `rbssr init` gives you
|
|
67
56
|
|
|
68
|
-
|
|
57
|
+
`rbssr init` scaffolds a small Bun-first SSR app:
|
|
69
58
|
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
```text
|
|
60
|
+
app/
|
|
61
|
+
root.tsx
|
|
62
|
+
middleware.ts
|
|
63
|
+
routes/
|
|
64
|
+
index.tsx
|
|
65
|
+
api/
|
|
66
|
+
health.ts
|
|
67
|
+
rbssr.config.ts
|
|
76
68
|
```
|
|
77
69
|
|
|
78
|
-
|
|
70
|
+
- `app/root.tsx`: document shell and top-level layout
|
|
71
|
+
- `app/middleware.ts`: global request pipeline hook
|
|
72
|
+
- `app/routes/index.tsx`: first SSR page route
|
|
73
|
+
- `app/routes/api/health.ts`: first API route
|
|
74
|
+
- `rbssr.config.ts`: runtime configuration entrypoint
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
bun run docs:dev
|
|
82
|
-
bun run docs:check
|
|
83
|
-
bun run docs:build
|
|
84
|
-
bun run docs:preview
|
|
85
|
-
```
|
|
76
|
+
The quickest follow-up is:
|
|
86
77
|
|
|
87
|
-
|
|
78
|
+
- https://react-bun-ssr.fly.dev/docs/start/quick-start
|
|
88
79
|
|
|
89
|
-
|
|
90
|
-
- `app/routes/docs/api/*.md`
|
|
91
|
-
- `app/routes/docs/search-index.json`
|
|
80
|
+
## How it works
|
|
92
81
|
|
|
93
|
-
|
|
94
|
-
- `bun run scripts/generate-api-docs.ts`
|
|
95
|
-
- `bun run scripts/build-search-index.ts`
|
|
96
|
-
- or automatically via `bun run docs:build`
|
|
82
|
+
### File-based routing
|
|
97
83
|
|
|
98
|
-
|
|
84
|
+
Routes live under `app/routes`. Page routes, API routes, dynamic params, and markdown routes all share one route tree. Files like `_layout` and `_middleware` participate in routing and request flow without becoming public URL segments.
|
|
99
85
|
|
|
100
|
-
|
|
86
|
+
Read more:
|
|
101
87
|
|
|
102
|
-
|
|
103
|
-
git checkout -b feat/<short-description>
|
|
104
|
-
```
|
|
88
|
+
- https://react-bun-ssr.fly.dev/docs/routing/file-based-routing
|
|
105
89
|
|
|
106
|
-
###
|
|
90
|
+
### Request pipeline
|
|
107
91
|
|
|
108
|
-
-
|
|
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.
|
|
92
|
+
For a page request, the framework resolves the matching route, runs global and nested middleware, executes the matched loader or action, and then renders an HTML response or returns a direct `Response` when the route short-circuits. API routes use the same route tree and middleware model, but return handler responses instead of page HTML.
|
|
112
93
|
|
|
113
|
-
|
|
94
|
+
Read more:
|
|
114
95
|
|
|
115
|
-
|
|
116
|
-
bun
|
|
117
|
-
bun run docs:check
|
|
118
|
-
bun run docs:build
|
|
119
|
-
```
|
|
96
|
+
- https://react-bun-ssr.fly.dev/docs/routing/middleware
|
|
97
|
+
- https://react-bun-ssr.fly.dev/docs/data/loaders
|
|
120
98
|
|
|
121
|
-
###
|
|
99
|
+
### Rendering model
|
|
122
100
|
|
|
123
|
-
|
|
101
|
+
SSR is the default model. HTML responses stream, deferred loader data is supported, and soft client transitions are handled through `Link` and `useRouter`. The docs site in this repository uses the same routing, rendering, markdown, and transition model that framework users get.
|
|
124
102
|
|
|
125
|
-
|
|
103
|
+
Read more:
|
|
126
104
|
|
|
127
|
-
|
|
128
|
-
-
|
|
129
|
-
- Approach and tradeoffs.
|
|
130
|
-
- Testing performed.
|
|
131
|
-
- Screenshots/GIF for docs UI changes (if relevant).
|
|
105
|
+
- https://react-bun-ssr.fly.dev/docs/rendering/streaming-deferred
|
|
106
|
+
- https://react-bun-ssr.fly.dev/docs/routing/navigation
|
|
132
107
|
|
|
133
|
-
|
|
108
|
+
### Bun-first runtime
|
|
134
109
|
|
|
135
|
-
|
|
136
|
-
- `bun run test:unit`
|
|
137
|
-
- `bun run test:integration`
|
|
138
|
-
- `bun run docs:check`
|
|
139
|
-
- `bun run docs:build`
|
|
110
|
+
Bun provides the runtime, server, bundler, markdown support, and file APIs that the framework is built around. `react-bun-ssr` is designed to use those primitives directly instead of layering itself on top of a Node-first base.
|
|
140
111
|
|
|
141
|
-
|
|
112
|
+
Read more:
|
|
142
113
|
|
|
143
|
-
|
|
114
|
+
- https://react-bun-ssr.fly.dev/docs/api/bun-runtime-apis
|
|
144
115
|
|
|
145
|
-
##
|
|
116
|
+
## Core commands
|
|
146
117
|
|
|
147
|
-
|
|
118
|
+
Framework commands:
|
|
148
119
|
|
|
149
|
-
-
|
|
150
|
-
-
|
|
120
|
+
- `rbssr init`: scaffold a new app in the current directory
|
|
121
|
+
- `rbssr dev`: start the Bun dev server
|
|
122
|
+
- `rbssr build`: create production output in `dist/`
|
|
123
|
+
- `rbssr start`: run the built app in production mode
|
|
151
124
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
125
|
+
Repository maintenance commands:
|
|
126
|
+
|
|
127
|
+
- `bun run docs:dev`
|
|
128
|
+
- `bun run docs:check`
|
|
129
|
+
- `bun run docs:build`
|
|
130
|
+
- `bun run test`
|
|
131
|
+
- `bun run typecheck`
|
|
155
132
|
|
|
156
|
-
|
|
133
|
+
CLI reference:
|
|
157
134
|
|
|
158
|
-
|
|
135
|
+
- https://react-bun-ssr.fly.dev/docs/tooling/cli
|
|
159
136
|
|
|
160
|
-
|
|
137
|
+
## Working on this repository
|
|
161
138
|
|
|
162
|
-
|
|
139
|
+
This repository contains both the framework and the official docs site built with it.
|
|
163
140
|
|
|
164
141
|
```bash
|
|
165
|
-
|
|
142
|
+
git clone git@github.com:react-formation/react-bun-ssr.git
|
|
143
|
+
cd react-bun-ssr
|
|
144
|
+
bun install
|
|
145
|
+
bun run docs:dev
|
|
166
146
|
```
|
|
167
147
|
|
|
168
|
-
|
|
148
|
+
That starts the docs site locally using the framework itself.
|
|
169
149
|
|
|
170
|
-
|
|
171
|
-
fly deploy
|
|
172
|
-
```
|
|
150
|
+
## Project layout
|
|
173
151
|
|
|
174
|
-
|
|
152
|
+
- `framework/`: runtime, renderer, route handling, build tooling, and CLI internals
|
|
153
|
+
- `bin/rbssr.ts`: CLI entrypoint
|
|
154
|
+
- `app/`: docs site routes, layouts, middleware, blog, and styles
|
|
155
|
+
- `app/routes/docs/**/*.md`: authored documentation pages
|
|
156
|
+
- `app/routes/blog/*.md`: authored blog posts
|
|
157
|
+
- `scripts/`: generators and validation scripts
|
|
158
|
+
- `tests/`: unit and integration tests
|
|
159
|
+
- `e2e/`: Playwright end-to-end tests
|
|
175
160
|
|
|
176
|
-
|
|
177
|
-
fly status
|
|
178
|
-
fly logs
|
|
179
|
-
```
|
|
161
|
+
## Contributing
|
|
180
162
|
|
|
181
|
-
|
|
163
|
+
Contributions should keep framework behavior, docs, tests, and generated artifacts aligned. For local setup, workflow, validation requirements, and generated-file policy, read [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
182
164
|
|
|
183
|
-
|
|
184
|
-
https://<your-fly-app>.fly.dev/docs/getting-started/introduction
|
|
185
|
-
```
|
|
165
|
+
## Release and deploy
|
|
186
166
|
|
|
187
|
-
|
|
167
|
+
- Pushes to `main` run the main-branch CI gate and deploy automatically to Fly.io.
|
|
168
|
+
- Tags like `v0.1.1-rc.0` publish prereleases to npm under `rc`.
|
|
169
|
+
- Tags like `v0.1.1` publish stable releases to npm under `latest`.
|
|
170
|
+
- The release workflow derives the published package version from the Git tag and rewrites `package.json` in the release job before publishing.
|
|
171
|
+
- npm publishing uses trusted publishing with GitHub OIDC instead of an `NPM_TOKEN`.
|
|
172
|
+
- npm package settings must have a trusted publisher configured for `react-formation / react-bun-ssr / release.yml`.
|
|
188
173
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
```
|
|
174
|
+
## Deploying
|
|
175
|
+
|
|
176
|
+
Fly.io deployment support is already documented and used by this project.
|
|
193
177
|
|
|
194
|
-
|
|
178
|
+
Happy path:
|
|
195
179
|
|
|
196
180
|
```bash
|
|
197
|
-
fly
|
|
181
|
+
fly auth login
|
|
182
|
+
fly deploy
|
|
198
183
|
```
|
|
199
184
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
The workflow includes an optional `deploy-fly` job on `main` push.
|
|
203
|
-
Set this repository secret before enabling production deploys:
|
|
185
|
+
Full deployment docs:
|
|
204
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import { createBunRouteAdapter } from
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createBunRouteAdapter } from './bun-route-adapter';
|
|
3
3
|
import {
|
|
4
4
|
ensureCleanDir,
|
|
5
5
|
ensureDir,
|
|
@@ -7,15 +7,23 @@ import {
|
|
|
7
7
|
glob,
|
|
8
8
|
statPath,
|
|
9
9
|
writeTextIfChanged,
|
|
10
|
-
} from
|
|
10
|
+
} from './io';
|
|
11
11
|
import type {
|
|
12
12
|
BuildManifest,
|
|
13
13
|
BuildRouteAsset,
|
|
14
14
|
PageRouteDefinition,
|
|
15
15
|
ResolvedConfig,
|
|
16
16
|
RouteManifest,
|
|
17
|
-
} from
|
|
18
|
-
import { normalizeSlashes, stableHash, toImportPath } from
|
|
17
|
+
} from './types';
|
|
18
|
+
import { normalizeSlashes, stableHash, toImportPath } from './utils';
|
|
19
|
+
|
|
20
|
+
const BUILD_OPTIMIZE_IMPORTS = [
|
|
21
|
+
'react-bun-ssr',
|
|
22
|
+
'react-bun-ssr/route',
|
|
23
|
+
'react',
|
|
24
|
+
'react-dom',
|
|
25
|
+
'@datadog/browser-rum-react',
|
|
26
|
+
];
|
|
19
27
|
|
|
20
28
|
interface ClientEntryFile {
|
|
21
29
|
routeId: string;
|
|
@@ -28,7 +36,7 @@ async function walkFiles(rootDir: string): Promise<string[]> {
|
|
|
28
36
|
return [];
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
return glob(
|
|
39
|
+
return glob('**/*', { cwd: rootDir, absolute: true });
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
function buildClientEntrySource(options: {
|
|
@@ -41,11 +49,13 @@ function buildClientEntrySource(options: {
|
|
|
41
49
|
|
|
42
50
|
const imports: string[] = [];
|
|
43
51
|
|
|
44
|
-
const runtimeImport =
|
|
52
|
+
const runtimeImport = normalizeSlashes(path.resolve(runtimeClientFile));
|
|
45
53
|
const rootImport = toImportPath(generatedDir, rootModulePath);
|
|
46
54
|
const routeImport = toImportPath(generatedDir, route.filePath);
|
|
47
55
|
|
|
48
|
-
imports.push(
|
|
56
|
+
imports.push(
|
|
57
|
+
`import { hydrateInitialRoute, registerRouteModules } from "${runtimeImport}";`,
|
|
58
|
+
);
|
|
49
59
|
|
|
50
60
|
imports.push(`import RootDefault from "${rootImport}";`);
|
|
51
61
|
imports.push(`import * as RootModule from "${rootImport}";`);
|
|
@@ -58,19 +68,24 @@ function buildClientEntrySource(options: {
|
|
|
58
68
|
const layoutFilePath = route.layoutFiles[index]!;
|
|
59
69
|
const layoutImportPath = toImportPath(generatedDir, layoutFilePath);
|
|
60
70
|
imports.push(`import Layout${index}Default from "${layoutImportPath}";`);
|
|
61
|
-
imports.push(
|
|
62
|
-
|
|
71
|
+
imports.push(
|
|
72
|
+
`import * as Layout${index}Module from "${layoutImportPath}";`,
|
|
73
|
+
);
|
|
74
|
+
layoutModuleRefs.push(
|
|
75
|
+
`{ ...Layout${index}Module, default: Layout${index}Default }`,
|
|
76
|
+
);
|
|
63
77
|
}
|
|
64
78
|
|
|
65
|
-
return `${imports.join(
|
|
79
|
+
return `${imports.join('\n')}
|
|
66
80
|
|
|
67
81
|
const modules = {
|
|
68
82
|
root: { ...RootModule, default: RootDefault },
|
|
69
|
-
layouts: [${layoutModuleRefs.join(
|
|
83
|
+
layouts: [${layoutModuleRefs.join(', ')}],
|
|
70
84
|
route: { ...RouteModule, default: RouteDefault },
|
|
71
85
|
};
|
|
72
86
|
|
|
73
|
-
|
|
87
|
+
registerRouteModules(${JSON.stringify(route.id)}, modules);
|
|
88
|
+
hydrateInitialRoute(${JSON.stringify(route.id)});
|
|
74
89
|
`;
|
|
75
90
|
}
|
|
76
91
|
|
|
@@ -82,10 +97,10 @@ export async function generateClientEntries(options: {
|
|
|
82
97
|
const { config, manifest, generatedDir } = options;
|
|
83
98
|
await ensureDir(generatedDir);
|
|
84
99
|
|
|
85
|
-
const runtimeClientFile = path.resolve(
|
|
100
|
+
const runtimeClientFile = path.resolve(import.meta.dir, 'client-runtime.tsx');
|
|
86
101
|
|
|
87
102
|
return Promise.all(
|
|
88
|
-
manifest.pages.map(async route => {
|
|
103
|
+
manifest.pages.map(async (route) => {
|
|
89
104
|
const entryName = `route__${route.id}.tsx`;
|
|
90
105
|
const entryFilePath = path.join(generatedDir, entryName);
|
|
91
106
|
const source = buildClientEntrySource({
|
|
@@ -112,14 +127,20 @@ async function mapBuildOutputsByPrefix(options: {
|
|
|
112
127
|
publicPrefix: string;
|
|
113
128
|
}): Promise<Record<string, BuildRouteAsset>> {
|
|
114
129
|
const { outDir, routeIds, publicPrefix } = options;
|
|
115
|
-
const files = (await walkFiles(outDir)).map(filePath =>
|
|
130
|
+
const files = (await walkFiles(outDir)).map((filePath) =>
|
|
131
|
+
normalizeSlashes(path.relative(outDir, filePath)),
|
|
132
|
+
);
|
|
116
133
|
|
|
117
134
|
const routeAssets: Record<string, BuildRouteAsset> = {};
|
|
118
135
|
|
|
119
136
|
for (const routeId of routeIds) {
|
|
120
137
|
const base = `route__${routeId}`;
|
|
121
|
-
const script = files.find(
|
|
122
|
-
|
|
138
|
+
const script = files.find(
|
|
139
|
+
(file) => file.startsWith(base) && file.endsWith('.js'),
|
|
140
|
+
);
|
|
141
|
+
const css = files.filter(
|
|
142
|
+
(file) => file.startsWith(base) && file.endsWith('.css'),
|
|
143
|
+
);
|
|
123
144
|
|
|
124
145
|
if (!script) {
|
|
125
146
|
continue;
|
|
@@ -127,7 +148,56 @@ async function mapBuildOutputsByPrefix(options: {
|
|
|
127
148
|
|
|
128
149
|
routeAssets[routeId] = {
|
|
129
150
|
script: `${publicPrefix}${script}`,
|
|
130
|
-
css: css.map(file => `${publicPrefix}${file}`),
|
|
151
|
+
css: css.map((file) => `${publicPrefix}${file}`),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return routeAssets;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizeMetafilePath(filePath: string): string {
|
|
159
|
+
return normalizeSlashes(filePath).replace(/^\.\//, "");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function toPublicBuildPath(publicPrefix: string, filePath: string): string {
|
|
163
|
+
return `${publicPrefix}${normalizeMetafilePath(filePath)}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function mapBuildOutputsFromMetafile(options: {
|
|
167
|
+
metafile: Bun.BuildMetafile;
|
|
168
|
+
entries: ClientEntryFile[];
|
|
169
|
+
publicPrefix: string;
|
|
170
|
+
}): Record<string, BuildRouteAsset> {
|
|
171
|
+
const routeIdByEntrypoint = new Map<string, string>();
|
|
172
|
+
const routeIdByEntryName = new Map<string, string>();
|
|
173
|
+
for (const entry of options.entries) {
|
|
174
|
+
const absoluteEntrypoint = normalizeMetafilePath(path.resolve(entry.entryFilePath));
|
|
175
|
+
const relativeEntrypoint = normalizeMetafilePath(path.relative(process.cwd(), entry.entryFilePath));
|
|
176
|
+
routeIdByEntrypoint.set(absoluteEntrypoint, entry.routeId);
|
|
177
|
+
routeIdByEntrypoint.set(relativeEntrypoint, entry.routeId);
|
|
178
|
+
routeIdByEntryName.set(path.basename(entry.entryFilePath), entry.routeId);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const routeAssets: Record<string, BuildRouteAsset> = {};
|
|
182
|
+
|
|
183
|
+
for (const [outputPath, metadata] of Object.entries(options.metafile.outputs)) {
|
|
184
|
+
if (!outputPath.endsWith(".js") || !metadata.entryPoint) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const normalizedEntrypoint = normalizeMetafilePath(metadata.entryPoint);
|
|
189
|
+
const absoluteEntrypoint = normalizeMetafilePath(path.resolve(process.cwd(), normalizedEntrypoint));
|
|
190
|
+
const routeId =
|
|
191
|
+
routeIdByEntrypoint.get(normalizedEntrypoint) ??
|
|
192
|
+
routeIdByEntrypoint.get(absoluteEntrypoint) ??
|
|
193
|
+
routeIdByEntryName.get(path.basename(normalizedEntrypoint));
|
|
194
|
+
if (!routeId) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
routeAssets[routeId] = {
|
|
199
|
+
script: toPublicBuildPath(options.publicPrefix, outputPath),
|
|
200
|
+
css: metadata.cssBundle ? [toPublicBuildPath(options.publicPrefix, metadata.cssBundle)] : [],
|
|
131
201
|
};
|
|
132
202
|
}
|
|
133
203
|
|
|
@@ -148,42 +218,64 @@ export async function bundleClientEntries(options: {
|
|
|
148
218
|
}
|
|
149
219
|
|
|
150
220
|
const result = await Bun.build({
|
|
151
|
-
entrypoints: entries.map(entry => entry.entryFilePath),
|
|
221
|
+
entrypoints: entries.map((entry) => entry.entryFilePath),
|
|
152
222
|
outdir: outDir,
|
|
153
|
-
target:
|
|
154
|
-
format:
|
|
155
|
-
|
|
156
|
-
|
|
223
|
+
target: 'browser',
|
|
224
|
+
format: 'esm',
|
|
225
|
+
metafile: true,
|
|
226
|
+
optimizeImports: BUILD_OPTIMIZE_IMPORTS,
|
|
227
|
+
splitting: true,
|
|
228
|
+
sourcemap: dev ? 'inline' : 'external',
|
|
157
229
|
minify: !dev,
|
|
158
|
-
naming: dev ?
|
|
230
|
+
naming: dev ? '[name].[ext]' : '[name]-[hash].[ext]',
|
|
159
231
|
});
|
|
160
232
|
|
|
161
233
|
if (!result.success) {
|
|
162
|
-
const messages = result.logs.map(log => log.message).join(
|
|
234
|
+
const messages = result.logs.map((log) => log.message).join('\n');
|
|
163
235
|
throw new Error(`Client bundle failed:\n${messages}`);
|
|
164
236
|
}
|
|
165
237
|
|
|
166
|
-
|
|
238
|
+
const routeAssetsFromMetafile = result.metafile
|
|
239
|
+
? mapBuildOutputsFromMetafile({
|
|
240
|
+
metafile: result.metafile,
|
|
241
|
+
entries,
|
|
242
|
+
publicPrefix,
|
|
243
|
+
})
|
|
244
|
+
: {};
|
|
245
|
+
|
|
246
|
+
if (Object.keys(routeAssetsFromMetafile).length === entries.length) {
|
|
247
|
+
return routeAssetsFromMetafile;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const routeAssetsFromPrefix = await mapBuildOutputsByPrefix({
|
|
167
251
|
outDir,
|
|
168
|
-
routeIds: entries.map(entry => entry.routeId),
|
|
252
|
+
routeIds: entries.map((entry) => entry.routeId),
|
|
169
253
|
publicPrefix,
|
|
170
254
|
});
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
...routeAssetsFromPrefix,
|
|
258
|
+
...routeAssetsFromMetafile,
|
|
259
|
+
};
|
|
171
260
|
}
|
|
172
261
|
|
|
173
262
|
export async function ensureCleanDirectory(dirPath: string): Promise<void> {
|
|
174
263
|
await ensureCleanDir(dirPath);
|
|
175
264
|
}
|
|
176
265
|
|
|
177
|
-
export async function copyDirRecursive(
|
|
266
|
+
export async function copyDirRecursive(
|
|
267
|
+
sourceDir: string,
|
|
268
|
+
destinationDir: string,
|
|
269
|
+
): Promise<void> {
|
|
178
270
|
if (!(await existsPath(sourceDir))) {
|
|
179
271
|
return;
|
|
180
272
|
}
|
|
181
273
|
|
|
182
274
|
await ensureDir(destinationDir);
|
|
183
275
|
|
|
184
|
-
const entries = await glob(
|
|
276
|
+
const entries = await glob('**/*', { cwd: sourceDir });
|
|
185
277
|
await Promise.all(
|
|
186
|
-
entries.map(async entry => {
|
|
278
|
+
entries.map(async (entry) => {
|
|
187
279
|
const from = path.join(sourceDir, entry);
|
|
188
280
|
const to = path.join(destinationDir, entry);
|
|
189
281
|
const fileStat = await statPath(from);
|
|
@@ -197,7 +289,9 @@ export async function copyDirRecursive(sourceDir: string, destinationDir: string
|
|
|
197
289
|
);
|
|
198
290
|
}
|
|
199
291
|
|
|
200
|
-
export function createBuildManifest(
|
|
292
|
+
export function createBuildManifest(
|
|
293
|
+
routeAssets: Record<string, BuildRouteAsset>,
|
|
294
|
+
): BuildManifest {
|
|
201
295
|
return {
|
|
202
296
|
version: stableHash(JSON.stringify(routeAssets)),
|
|
203
297
|
generatedAt: new Date().toISOString(),
|
|
@@ -207,28 +301,38 @@ export function createBuildManifest(routeAssets: Record<string, BuildRouteAsset>
|
|
|
207
301
|
|
|
208
302
|
export async function discoverFileSignature(rootDir: string): Promise<string> {
|
|
209
303
|
const files = (await walkFiles(rootDir))
|
|
210
|
-
.filter(file => !normalizeSlashes(file).includes(
|
|
304
|
+
.filter((file) => !normalizeSlashes(file).includes('/node_modules/'))
|
|
211
305
|
.sort();
|
|
212
306
|
|
|
213
|
-
const signatureBits = (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
307
|
+
const signatureBits = (
|
|
308
|
+
await Promise.all(
|
|
309
|
+
files.map(async (filePath) => {
|
|
310
|
+
const fileStat = await statPath(filePath);
|
|
311
|
+
if (!fileStat?.isFile()) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const contentHash = stableHash(await Bun.file(filePath).bytes());
|
|
315
|
+
return `${normalizeSlashes(filePath)}:${contentHash}`;
|
|
316
|
+
}),
|
|
317
|
+
)
|
|
318
|
+
).filter((value): value is string => Boolean(value));
|
|
319
|
+
|
|
320
|
+
return stableHash(signatureBits.join('|'));
|
|
225
321
|
}
|
|
226
322
|
|
|
227
|
-
export async function buildRouteManifest(
|
|
323
|
+
export async function buildRouteManifest(
|
|
324
|
+
config: ResolvedConfig,
|
|
325
|
+
): Promise<RouteManifest> {
|
|
228
326
|
const adapter = await createBunRouteAdapter({
|
|
229
327
|
routesDir: config.routesDir,
|
|
230
|
-
generatedMarkdownRootDir: path.resolve(
|
|
231
|
-
|
|
328
|
+
generatedMarkdownRootDir: path.resolve(
|
|
329
|
+
config.cwd,
|
|
330
|
+
'.rbssr/generated/markdown-routes',
|
|
331
|
+
),
|
|
332
|
+
projectionRootDir: path.resolve(
|
|
333
|
+
config.cwd,
|
|
334
|
+
'.rbssr/generated/router-projection/build-manifest',
|
|
335
|
+
),
|
|
232
336
|
});
|
|
233
337
|
return adapter.manifest;
|
|
234
338
|
}
|