react-bun-ssr 0.1.1 → 0.2.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/framework/cli/commands.ts +59 -223
- package/framework/cli/dev-client-watch.ts +281 -0
- package/framework/cli/dev-route-table.ts +71 -0
- package/framework/cli/dev-runtime.ts +382 -0
- package/framework/cli/internal.ts +138 -0
- package/framework/cli/main.ts +27 -31
- package/framework/runtime/build-tools.ts +131 -12
- package/framework/runtime/bun-route-adapter.ts +20 -7
- package/framework/runtime/client-runtime.tsx +73 -132
- package/framework/runtime/client-transition-core.ts +159 -0
- package/framework/runtime/markdown-routes.ts +1 -14
- package/framework/runtime/matcher.ts +11 -11
- package/framework/runtime/module-loader.ts +62 -24
- package/framework/runtime/render.tsx +56 -20
- package/framework/runtime/server.ts +49 -106
- package/framework/runtime/types.ts +12 -1
- package/framework/runtime/utils.ts +3 -0
- package/package.json +2 -1
|
@@ -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,
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
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,12 +25,6 @@ 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
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
28
|
async function getConfig(cwd: string): Promise<{ userConfig: FrameworkConfig; resolved: ResolvedConfig }> {
|
|
30
29
|
const userConfig = await loadUserConfig(cwd);
|
|
31
30
|
const resolved = resolveConfig(userConfig, cwd);
|
|
@@ -37,29 +36,7 @@ async function writeProductionServerEntrypoint(options: { distDir: string }): Pr
|
|
|
37
36
|
await ensureDir(serverDir);
|
|
38
37
|
|
|
39
38
|
const serverEntryPath = path.join(serverDir, "server.mjs");
|
|
40
|
-
|
|
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);
|
|
39
|
+
await writeText(serverEntryPath, createProductionServerEntrypointSource());
|
|
63
40
|
}
|
|
64
41
|
|
|
65
42
|
export async function runInit(args: string[], cwd = process.cwd()): Promise<void> {
|
|
@@ -109,200 +86,64 @@ export async function runBuild(cwd = process.cwd()): Promise<void> {
|
|
|
109
86
|
}
|
|
110
87
|
|
|
111
88
|
export async function runDev(cwd = process.cwd()): Promise<void> {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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(":");
|
|
89
|
+
await getConfig(cwd);
|
|
90
|
+
|
|
91
|
+
const generatedDevDir = path.resolve(cwd, ".rbssr/generated/dev");
|
|
92
|
+
const generatedEntryPath = path.join(generatedDevDir, "entry.ts");
|
|
93
|
+
await ensureDir(generatedDevDir);
|
|
94
|
+
await writeTextIfChanged(generatedEntryPath, createDevHotEntrypointSource({
|
|
95
|
+
cwd,
|
|
96
|
+
runtimeModulePath: path.resolve(import.meta.dir, "dev-runtime.ts"),
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
let activeChild: Bun.Subprocess<"inherit", "inherit", "inherit"> | null = null;
|
|
100
|
+
let shuttingDown = false;
|
|
101
|
+
|
|
102
|
+
const forwardSignal = (signal: NodeJS.Signals): void => {
|
|
103
|
+
shuttingDown = true;
|
|
104
|
+
activeChild?.kill(signal);
|
|
142
105
|
};
|
|
143
106
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
});
|
|
107
|
+
process.once("SIGINT", () => {
|
|
108
|
+
forwardSignal("SIGINT");
|
|
109
|
+
});
|
|
196
110
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
};
|
|
111
|
+
process.once("SIGTERM", () => {
|
|
112
|
+
forwardSignal("SIGTERM");
|
|
113
|
+
});
|
|
214
114
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
115
|
+
while (true) {
|
|
116
|
+
activeChild = Bun.spawn({
|
|
117
|
+
cmd: ["bun", "--hot", generatedEntryPath],
|
|
118
|
+
cwd,
|
|
119
|
+
stdin: "inherit",
|
|
120
|
+
stdout: "inherit",
|
|
121
|
+
stderr: "inherit",
|
|
122
|
+
env: {
|
|
123
|
+
...process.env,
|
|
124
|
+
RBSSR_DEV_LAUNCHER: "1",
|
|
125
|
+
RBSSR_DEV_CHILD: "1",
|
|
126
|
+
},
|
|
221
127
|
});
|
|
222
|
-
return task;
|
|
223
|
-
};
|
|
224
128
|
|
|
225
|
-
|
|
129
|
+
const exitCode = await activeChild.exited;
|
|
130
|
+
activeChild = null;
|
|
226
131
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (rebuildTimer) {
|
|
230
|
-
clearTimeout(rebuildTimer);
|
|
132
|
+
if (shuttingDown) {
|
|
133
|
+
return;
|
|
231
134
|
}
|
|
232
135
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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`);
|
|
136
|
+
if (exitCode === RBSSR_DEV_RESTART_EXIT_CODE) {
|
|
137
|
+
log("restarting dev child after config change");
|
|
138
|
+
continue;
|
|
248
139
|
}
|
|
249
|
-
}
|
|
250
140
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
clearTimeout(rebuildTimer);
|
|
254
|
-
rebuildTimer = undefined;
|
|
255
|
-
}
|
|
256
|
-
for (const watcher of watchers) {
|
|
257
|
-
watcher.close();
|
|
141
|
+
if (exitCode !== 0) {
|
|
142
|
+
process.exit(exitCode);
|
|
258
143
|
}
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
process.once("SIGINT", () => {
|
|
262
|
-
cleanup();
|
|
263
|
-
process.exit(0);
|
|
264
|
-
});
|
|
265
144
|
|
|
266
|
-
|
|
267
|
-
|
|
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}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
306
147
|
}
|
|
307
148
|
|
|
308
149
|
export async function runStart(cwd = process.cwd()): Promise<void> {
|
|
@@ -328,16 +169,11 @@ function runSubprocess(cmd: string[]): void {
|
|
|
328
169
|
}
|
|
329
170
|
|
|
330
171
|
export async function runTypecheck(): Promise<void> {
|
|
331
|
-
runSubprocess(
|
|
172
|
+
runSubprocess(createTypecheckCommand());
|
|
332
173
|
}
|
|
333
174
|
|
|
334
175
|
export async function runTest(extraArgs: string[]): Promise<void> {
|
|
335
|
-
|
|
336
|
-
runSubprocess(
|
|
337
|
-
return;
|
|
176
|
+
for (const cmd of createTestCommands(extraArgs)) {
|
|
177
|
+
runSubprocess(cmd);
|
|
338
178
|
}
|
|
339
|
-
|
|
340
|
-
runSubprocess(["bun", "test", "./tests/unit"]);
|
|
341
|
-
runSubprocess(["bun", "test", "./tests/integration"]);
|
|
342
|
-
runSubprocess(["bun", "x", "playwright", "test"]);
|
|
343
179
|
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
createClientEntrySetSignature,
|
|
4
|
+
listBuildOutputFiles,
|
|
5
|
+
mapBuildOutputsFromMetafile,
|
|
6
|
+
type ClientEntryFile,
|
|
7
|
+
} from "../runtime/build-tools";
|
|
8
|
+
import { ensureDir, readText, removePath, statPath } from "../runtime/io";
|
|
9
|
+
import type { BuildRouteAsset } from "../runtime/types";
|
|
10
|
+
|
|
11
|
+
export interface DevClientBuildSnapshot {
|
|
12
|
+
entrySetSignature: string;
|
|
13
|
+
routeAssets: Record<string, BuildRouteAsset>;
|
|
14
|
+
outputFiles: string[];
|
|
15
|
+
buildCount: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DevClientWatchHandle {
|
|
19
|
+
syncEntries(entries: ClientEntryFile[]): Promise<{ restarted: boolean; entrySetSignature: string }>;
|
|
20
|
+
getBuildCount(): number;
|
|
21
|
+
waitForBuildAfter(buildCount: number): Promise<void>;
|
|
22
|
+
waitUntilReady(): Promise<void>;
|
|
23
|
+
stop(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DevClientWatchState {
|
|
27
|
+
buildCount: number;
|
|
28
|
+
entrySetSignature: string;
|
|
29
|
+
entries: ClientEntryFile[];
|
|
30
|
+
readyPromise: Promise<void>;
|
|
31
|
+
resolveReady: () => void;
|
|
32
|
+
rejectReady: (error: unknown) => void;
|
|
33
|
+
outputFiles: Set<string>;
|
|
34
|
+
process: Bun.Subprocess<"ignore", "inherit", "inherit"> | null;
|
|
35
|
+
stopped: boolean;
|
|
36
|
+
waiters: Array<{
|
|
37
|
+
minBuildCount: number;
|
|
38
|
+
resolve: () => void;
|
|
39
|
+
reject: (error: unknown) => void;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createDeferred(): {
|
|
44
|
+
promise: Promise<void>;
|
|
45
|
+
resolve: () => void;
|
|
46
|
+
reject: (error: unknown) => void;
|
|
47
|
+
} {
|
|
48
|
+
let resolve!: () => void;
|
|
49
|
+
let reject!: (error: unknown) => void;
|
|
50
|
+
const promise = new Promise<void>((res, rej) => {
|
|
51
|
+
resolve = res;
|
|
52
|
+
reject = rej;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
promise,
|
|
57
|
+
resolve,
|
|
58
|
+
reject,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createDevClientWatch(options: {
|
|
63
|
+
cwd: string;
|
|
64
|
+
outDir: string;
|
|
65
|
+
metafilePath: string;
|
|
66
|
+
entries: ClientEntryFile[];
|
|
67
|
+
publicPrefix: string;
|
|
68
|
+
onBuild: (snapshot: DevClientBuildSnapshot) => void | Promise<void>;
|
|
69
|
+
onLog?: (message: string) => void;
|
|
70
|
+
}): DevClientWatchHandle {
|
|
71
|
+
const state: DevClientWatchState = {
|
|
72
|
+
buildCount: 0,
|
|
73
|
+
entrySetSignature: createClientEntrySetSignature(options.entries),
|
|
74
|
+
entries: [...options.entries],
|
|
75
|
+
...(() => {
|
|
76
|
+
const deferred = createDeferred();
|
|
77
|
+
return {
|
|
78
|
+
readyPromise: deferred.promise,
|
|
79
|
+
resolveReady: deferred.resolve,
|
|
80
|
+
rejectReady: deferred.reject,
|
|
81
|
+
};
|
|
82
|
+
})(),
|
|
83
|
+
outputFiles: new Set<string>(),
|
|
84
|
+
process: null,
|
|
85
|
+
stopped: false,
|
|
86
|
+
waiters: [],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
let metafilePoller: ReturnType<typeof setInterval> | undefined;
|
|
90
|
+
let lastMetafileMtime = "";
|
|
91
|
+
|
|
92
|
+
const stopPolling = (): void => {
|
|
93
|
+
if (metafilePoller) {
|
|
94
|
+
clearInterval(metafilePoller);
|
|
95
|
+
metafilePoller = undefined;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const parseMetafile = async (): Promise<void> => {
|
|
100
|
+
const stat = await statPath(options.metafilePath);
|
|
101
|
+
if (!stat?.isFile()) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const nextMtime = stat.mtime.toISOString();
|
|
106
|
+
if (nextMtime === lastMetafileMtime) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let metafile: Bun.BuildMetafile;
|
|
111
|
+
try {
|
|
112
|
+
metafile = JSON.parse(await readText(options.metafilePath)) as Bun.BuildMetafile;
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
lastMetafileMtime = nextMtime;
|
|
118
|
+
|
|
119
|
+
const nextOutputFiles = new Set(listBuildOutputFiles(metafile));
|
|
120
|
+
const staleOutputFiles = [...state.outputFiles].filter((filePath) => !nextOutputFiles.has(filePath));
|
|
121
|
+
|
|
122
|
+
await Promise.all(
|
|
123
|
+
staleOutputFiles.map((filePath) => removePath(path.resolve(options.cwd, filePath))),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
state.outputFiles = nextOutputFiles;
|
|
127
|
+
state.buildCount += 1;
|
|
128
|
+
|
|
129
|
+
const readyWaiters = state.waiters.filter((waiter) => state.buildCount > waiter.minBuildCount);
|
|
130
|
+
state.waiters = state.waiters.filter((waiter) => state.buildCount <= waiter.minBuildCount);
|
|
131
|
+
for (const waiter of readyWaiters) {
|
|
132
|
+
waiter.resolve();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await options.onBuild({
|
|
136
|
+
entrySetSignature: state.entrySetSignature,
|
|
137
|
+
routeAssets: mapBuildOutputsFromMetafile({
|
|
138
|
+
metafile,
|
|
139
|
+
entries: state.entries,
|
|
140
|
+
publicPrefix: options.publicPrefix,
|
|
141
|
+
}),
|
|
142
|
+
outputFiles: [...nextOutputFiles].sort(),
|
|
143
|
+
buildCount: state.buildCount,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
state.resolveReady();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const startProcess = async (): Promise<void> => {
|
|
150
|
+
await ensureDir(path.dirname(options.metafilePath));
|
|
151
|
+
await ensureDir(options.outDir);
|
|
152
|
+
await removePath(options.metafilePath);
|
|
153
|
+
|
|
154
|
+
const previousOutputFiles = [...state.outputFiles];
|
|
155
|
+
const deferred = createDeferred();
|
|
156
|
+
state.readyPromise = deferred.promise;
|
|
157
|
+
state.resolveReady = deferred.resolve;
|
|
158
|
+
state.rejectReady = deferred.reject;
|
|
159
|
+
state.buildCount = 0;
|
|
160
|
+
state.outputFiles = new Set<string>();
|
|
161
|
+
lastMetafileMtime = "";
|
|
162
|
+
|
|
163
|
+
if (state.entries.length === 0) {
|
|
164
|
+
await Promise.all(
|
|
165
|
+
previousOutputFiles.map((filePath) => removePath(path.resolve(options.cwd, filePath))),
|
|
166
|
+
);
|
|
167
|
+
state.resolveReady();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const cmd = [
|
|
172
|
+
"bun",
|
|
173
|
+
"build",
|
|
174
|
+
"--watch",
|
|
175
|
+
"--no-clear-screen",
|
|
176
|
+
"--target=browser",
|
|
177
|
+
"--format=esm",
|
|
178
|
+
"--splitting",
|
|
179
|
+
"--sourcemap=inline",
|
|
180
|
+
"--outdir",
|
|
181
|
+
options.outDir,
|
|
182
|
+
`--metafile=${options.metafilePath}`,
|
|
183
|
+
"--entry-naming",
|
|
184
|
+
"[name].[ext]",
|
|
185
|
+
"--chunk-naming",
|
|
186
|
+
"[name]-[hash].[ext]",
|
|
187
|
+
"--asset-naming",
|
|
188
|
+
"[name]-[hash].[ext]",
|
|
189
|
+
...state.entries.map((entry) => entry.entryFilePath),
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
state.process = Bun.spawn({
|
|
193
|
+
cmd,
|
|
194
|
+
cwd: options.cwd,
|
|
195
|
+
stdin: "ignore",
|
|
196
|
+
stdout: "inherit",
|
|
197
|
+
stderr: "inherit",
|
|
198
|
+
env: process.env,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
void state.process.exited.then((exitCode) => {
|
|
202
|
+
if (state.stopped || exitCode === 0) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const waiter of state.waiters) {
|
|
206
|
+
waiter.reject(new Error(`bun build --watch exited with code ${exitCode}`));
|
|
207
|
+
}
|
|
208
|
+
state.waiters = [];
|
|
209
|
+
state.rejectReady(new Error(`bun build --watch exited with code ${exitCode}`));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
metafilePoller = setInterval(() => {
|
|
213
|
+
void parseMetafile();
|
|
214
|
+
}, 75);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const stopProcess = async (): Promise<void> => {
|
|
218
|
+
stopPolling();
|
|
219
|
+
|
|
220
|
+
if (!state.process) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const active = state.process;
|
|
225
|
+
state.process = null;
|
|
226
|
+
active.kill();
|
|
227
|
+
await active.exited;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
void startProcess();
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
async syncEntries(entries) {
|
|
234
|
+
const nextEntrySetSignature = createClientEntrySetSignature(entries);
|
|
235
|
+
const restarted = nextEntrySetSignature !== state.entrySetSignature;
|
|
236
|
+
state.entries = [...entries];
|
|
237
|
+
|
|
238
|
+
if (!restarted) {
|
|
239
|
+
options.onLog?.("kept Bun client watch hot");
|
|
240
|
+
return {
|
|
241
|
+
restarted: false,
|
|
242
|
+
entrySetSignature: state.entrySetSignature,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
state.entrySetSignature = nextEntrySetSignature;
|
|
247
|
+
await stopProcess();
|
|
248
|
+
await startProcess();
|
|
249
|
+
options.onLog?.("restarted Bun client watch after entry set change");
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
restarted: true,
|
|
253
|
+
entrySetSignature: state.entrySetSignature,
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
getBuildCount() {
|
|
257
|
+
return state.buildCount;
|
|
258
|
+
},
|
|
259
|
+
waitForBuildAfter(buildCount) {
|
|
260
|
+
if (state.buildCount > buildCount || state.entries.length === 0) {
|
|
261
|
+
return Promise.resolve();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return new Promise<void>((resolve, reject) => {
|
|
265
|
+
state.waiters.push({
|
|
266
|
+
minBuildCount: buildCount,
|
|
267
|
+
resolve,
|
|
268
|
+
reject,
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
waitUntilReady() {
|
|
273
|
+
return state.readyPromise;
|
|
274
|
+
},
|
|
275
|
+
async stop() {
|
|
276
|
+
state.stopped = true;
|
|
277
|
+
stopPolling();
|
|
278
|
+
await stopProcess();
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { statPath } from "../runtime/io";
|
|
2
|
+
import type { RouteManifest } from "../runtime/types";
|
|
3
|
+
import { ensureWithin } from "../runtime/utils";
|
|
4
|
+
|
|
5
|
+
export const RBSSR_DEV_RELOAD_TOPIC = "rbssr:reload";
|
|
6
|
+
export const RBSSR_DEV_WS_PATH = "/__rbssr/ws";
|
|
7
|
+
|
|
8
|
+
export type DevReloadReason =
|
|
9
|
+
| "client-build"
|
|
10
|
+
| "route-structure"
|
|
11
|
+
| "server-runtime"
|
|
12
|
+
| "markdown-route"
|
|
13
|
+
| "config-restart";
|
|
14
|
+
|
|
15
|
+
export interface DevReloadMessage {
|
|
16
|
+
token: number;
|
|
17
|
+
reason: DevReloadReason;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeApiRoutePath(routePath: string): string {
|
|
21
|
+
return routePath.replace(/\*[A-Za-z0-9_]+/g, "*");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function serveDevClientAsset(devClientDir: string, request: Request): Promise<Response> {
|
|
25
|
+
const url = new URL(request.url);
|
|
26
|
+
const relativePath = url.pathname.replace(/^\/__rbssr\/client\//, "");
|
|
27
|
+
const resolvedPath = ensureWithin(devClientDir, relativePath);
|
|
28
|
+
if (!resolvedPath) {
|
|
29
|
+
return new Response("Not found", { status: 404 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stat = await statPath(resolvedPath);
|
|
33
|
+
if (!stat?.isFile()) {
|
|
34
|
+
return new Response("Not found", { status: 404 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return new Response(Bun.file(resolvedPath), {
|
|
38
|
+
headers: {
|
|
39
|
+
"cache-control": "no-store",
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createDevRouteTable(options: {
|
|
45
|
+
devClientDir: string;
|
|
46
|
+
manifest: RouteManifest;
|
|
47
|
+
handleFrameworkFetch: (request: Request) => Promise<Response>;
|
|
48
|
+
}): Record<string, Response | ((request: Request, server: Bun.Server<undefined>) => Response | Promise<Response> | void)> {
|
|
49
|
+
const routes: Record<
|
|
50
|
+
string,
|
|
51
|
+
Response | ((request: Request, server: Bun.Server<undefined>) => Response | Promise<Response> | void)
|
|
52
|
+
> = {
|
|
53
|
+
[RBSSR_DEV_WS_PATH]: (request, server) => {
|
|
54
|
+
const upgraded = server.upgrade(request);
|
|
55
|
+
if (!upgraded) {
|
|
56
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"/__rbssr/client/*": (request) => {
|
|
60
|
+
return serveDevClientAsset(options.devClientDir, request);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const apiRoute of options.manifest.api) {
|
|
65
|
+
routes[normalizeApiRoutePath(apiRoute.routePath)] = (request) => {
|
|
66
|
+
return options.handleFrameworkFetch(request);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return routes;
|
|
71
|
+
}
|