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
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { watch, type FSWatcher } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { buildRouteManifest, syncClientEntries, type ClientEntryFile } from "../runtime/build-tools";
|
|
4
|
+
import { loadUserConfig, resolveConfig } from "../runtime/config";
|
|
5
|
+
import { compileMarkdownRouteModule } from "../runtime/markdown-routes";
|
|
6
|
+
import { ensureDir, existsPath } from "../runtime/io";
|
|
7
|
+
import { createServer } from "../runtime/server";
|
|
8
|
+
import type { BuildRouteAsset, FrameworkConfig, RouteManifest } from "../runtime/types";
|
|
9
|
+
import { normalizeSlashes, stableHash } from "../runtime/utils";
|
|
10
|
+
import { RBSSR_DEV_RESTART_EXIT_CODE } from "./internal";
|
|
11
|
+
import { createDevClientWatch, type DevClientWatchHandle } from "./dev-client-watch";
|
|
12
|
+
import {
|
|
13
|
+
createDevRouteTable,
|
|
14
|
+
RBSSR_DEV_RELOAD_TOPIC,
|
|
15
|
+
type DevReloadMessage,
|
|
16
|
+
type DevReloadReason,
|
|
17
|
+
} from "./dev-route-table";
|
|
18
|
+
|
|
19
|
+
function log(message: string): void {
|
|
20
|
+
// eslint-disable-next-line no-console
|
|
21
|
+
console.log(`[rbssr] ${message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isConfigFileName(fileName: string): boolean {
|
|
25
|
+
return fileName === "rbssr.config.ts" || fileName === "rbssr.config.js" || fileName === "rbssr.config.mjs";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isTopLevelAppRuntimeFile(relativePath: string): boolean {
|
|
29
|
+
return /^root\.(tsx|jsx|ts|js)$/.test(relativePath) || /^middleware\.(tsx|jsx|ts|js)$/.test(relativePath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isMarkdownRouteFile(relativePath: string): boolean {
|
|
33
|
+
return /^routes\/.+\.md$/.test(relativePath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isStructuralAppPath(relativePath: string): boolean {
|
|
37
|
+
return relativePath === "routes"
|
|
38
|
+
|| relativePath.startsWith("routes/")
|
|
39
|
+
|| isTopLevelAppRuntimeFile(relativePath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toAbsoluteAppPath(appDir: string, relativePath: string): string {
|
|
43
|
+
return path.join(appDir, relativePath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface DevHotData {
|
|
47
|
+
bunServer?: Bun.Server<undefined>;
|
|
48
|
+
reloadToken?: number;
|
|
49
|
+
routeManifestVersion?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function runHotDevChild(options: {
|
|
53
|
+
cwd: string;
|
|
54
|
+
}): Promise<void> {
|
|
55
|
+
const userConfig = await loadUserConfig(options.cwd);
|
|
56
|
+
const resolved = resolveConfig({
|
|
57
|
+
...userConfig,
|
|
58
|
+
mode: "development",
|
|
59
|
+
}, options.cwd);
|
|
60
|
+
|
|
61
|
+
const generatedClientEntriesDir = path.resolve(options.cwd, ".rbssr/generated/client-entries");
|
|
62
|
+
const generatedMarkdownRootDir = path.resolve(options.cwd, ".rbssr/generated/markdown-routes");
|
|
63
|
+
const devClientDir = path.resolve(options.cwd, ".rbssr/dev/client");
|
|
64
|
+
const clientMetafilePath = path.resolve(options.cwd, ".rbssr/dev/client-metafile.json");
|
|
65
|
+
|
|
66
|
+
await Promise.all([
|
|
67
|
+
ensureDir(generatedClientEntriesDir),
|
|
68
|
+
ensureDir(generatedMarkdownRootDir),
|
|
69
|
+
ensureDir(devClientDir),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const hotData = (import.meta.hot?.data ?? {}) as DevHotData;
|
|
73
|
+
let bunServer = hotData.bunServer;
|
|
74
|
+
let reloadToken = hotData.reloadToken ?? 0;
|
|
75
|
+
let routeManifestVersion = hotData.routeManifestVersion ?? 0;
|
|
76
|
+
let manifest: RouteManifest = {
|
|
77
|
+
pages: [],
|
|
78
|
+
api: [],
|
|
79
|
+
};
|
|
80
|
+
let routeAssets: Record<string, BuildRouteAsset> = {};
|
|
81
|
+
let clientWatch: DevClientWatchHandle | null = null;
|
|
82
|
+
let frameworkServer = createFrameworkServer(userConfig, {
|
|
83
|
+
routeAssets,
|
|
84
|
+
reloadToken: () => reloadToken,
|
|
85
|
+
routeManifestVersion: () => routeManifestVersion,
|
|
86
|
+
});
|
|
87
|
+
let suppressNextClientReload = true;
|
|
88
|
+
let nextClientBuildReason: DevReloadReason | null = null;
|
|
89
|
+
let stopping = false;
|
|
90
|
+
|
|
91
|
+
const watchers: FSWatcher[] = [];
|
|
92
|
+
let structuralSyncTimer: ReturnType<typeof setTimeout> | undefined;
|
|
93
|
+
let structuralSyncQueue: Promise<void> = Promise.resolve();
|
|
94
|
+
|
|
95
|
+
const publishReload = (reason: DevReloadReason): void => {
|
|
96
|
+
reloadToken += 1;
|
|
97
|
+
if (bunServer) {
|
|
98
|
+
const message: DevReloadMessage = {
|
|
99
|
+
token: reloadToken,
|
|
100
|
+
reason,
|
|
101
|
+
};
|
|
102
|
+
bunServer.publish(RBSSR_DEV_RELOAD_TOPIC, JSON.stringify(message));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const onClientBuild = async (nextRouteAssets: Record<string, BuildRouteAsset>): Promise<void> => {
|
|
107
|
+
routeAssets = nextRouteAssets;
|
|
108
|
+
frameworkServer = createFrameworkServer(userConfig, {
|
|
109
|
+
routeAssets,
|
|
110
|
+
reloadToken: () => reloadToken,
|
|
111
|
+
routeManifestVersion: () => routeManifestVersion,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (suppressNextClientReload) {
|
|
115
|
+
suppressNextClientReload = false;
|
|
116
|
+
nextClientBuildReason = null;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
publishReload(nextClientBuildReason ?? "client-build");
|
|
121
|
+
nextClientBuildReason = null;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const buildServeOptions = (): Bun.Serve.Options<undefined, string> => {
|
|
125
|
+
return {
|
|
126
|
+
id: `rbssr-dev:${stableHash(normalizeSlashes(resolved.cwd))}`,
|
|
127
|
+
hostname: resolved.host,
|
|
128
|
+
port: resolved.port,
|
|
129
|
+
development: true,
|
|
130
|
+
routes: createDevRouteTable({
|
|
131
|
+
devClientDir,
|
|
132
|
+
manifest,
|
|
133
|
+
handleFrameworkFetch: frameworkServer.fetch,
|
|
134
|
+
}),
|
|
135
|
+
fetch: frameworkServer.fetch,
|
|
136
|
+
websocket: {
|
|
137
|
+
open(ws) {
|
|
138
|
+
ws.subscribe(RBSSR_DEV_RELOAD_TOPIC);
|
|
139
|
+
const message: DevReloadMessage = {
|
|
140
|
+
token: reloadToken,
|
|
141
|
+
reason: "server-runtime",
|
|
142
|
+
};
|
|
143
|
+
ws.send(JSON.stringify(message));
|
|
144
|
+
},
|
|
145
|
+
message() {
|
|
146
|
+
// noop
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const ensureClientWatch = async (entries: ClientEntryFile[]): Promise<void> => {
|
|
153
|
+
if (clientWatch) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
clientWatch = createDevClientWatch({
|
|
158
|
+
cwd: options.cwd,
|
|
159
|
+
outDir: devClientDir,
|
|
160
|
+
metafilePath: clientMetafilePath,
|
|
161
|
+
entries,
|
|
162
|
+
publicPrefix: "/__rbssr/client/",
|
|
163
|
+
onBuild: async (snapshot) => {
|
|
164
|
+
await onClientBuild(snapshot.routeAssets);
|
|
165
|
+
},
|
|
166
|
+
onLog: (message) => {
|
|
167
|
+
log(message);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const performStructuralSync = async (mode: "bootstrap" | "update"): Promise<void> => {
|
|
173
|
+
const nextManifest = await buildRouteManifest(resolved);
|
|
174
|
+
const syncResult = await syncClientEntries({
|
|
175
|
+
config: resolved,
|
|
176
|
+
manifest: nextManifest,
|
|
177
|
+
generatedDir: generatedClientEntriesDir,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const previousBuildCount = clientWatch?.getBuildCount() ?? 0;
|
|
181
|
+
suppressNextClientReload = true;
|
|
182
|
+
nextClientBuildReason = null;
|
|
183
|
+
|
|
184
|
+
await ensureClientWatch(syncResult.entries);
|
|
185
|
+
|
|
186
|
+
if (!clientWatch) {
|
|
187
|
+
throw new Error("client watch failed to initialize");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const watchSync = await clientWatch.syncEntries(syncResult.entries);
|
|
191
|
+
const shouldAwaitBuild = watchSync.restarted
|
|
192
|
+
|| syncResult.addedEntryPaths.length > 0
|
|
193
|
+
|| syncResult.changedEntryPaths.length > 0
|
|
194
|
+
|| syncResult.removedEntryPaths.length > 0;
|
|
195
|
+
|
|
196
|
+
if (watchSync.restarted) {
|
|
197
|
+
await clientWatch.waitUntilReady();
|
|
198
|
+
} else if (shouldAwaitBuild) {
|
|
199
|
+
await clientWatch.waitForBuildAfter(previousBuildCount);
|
|
200
|
+
} else {
|
|
201
|
+
await clientWatch.waitUntilReady();
|
|
202
|
+
suppressNextClientReload = false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
manifest = nextManifest;
|
|
206
|
+
routeManifestVersion += 1;
|
|
207
|
+
frameworkServer = createFrameworkServer(userConfig, {
|
|
208
|
+
routeAssets,
|
|
209
|
+
reloadToken: () => reloadToken,
|
|
210
|
+
routeManifestVersion: () => routeManifestVersion,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const nextServeOptions = buildServeOptions();
|
|
214
|
+
if (bunServer) {
|
|
215
|
+
bunServer.reload(nextServeOptions);
|
|
216
|
+
} else {
|
|
217
|
+
bunServer = Bun.serve(nextServeOptions);
|
|
218
|
+
log(`dev server listening on ${bunServer.url}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (mode === "update") {
|
|
222
|
+
publishReload("route-structure");
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const enqueueStructuralSync = (mode: "bootstrap" | "update"): Promise<void> => {
|
|
227
|
+
const task = structuralSyncQueue.then(() => performStructuralSync(mode));
|
|
228
|
+
structuralSyncQueue = task.catch((error) => {
|
|
229
|
+
// eslint-disable-next-line no-console
|
|
230
|
+
console.error("[rbssr] structural sync failed", error);
|
|
231
|
+
});
|
|
232
|
+
return task;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const scheduleStructuralSync = (): void => {
|
|
236
|
+
if (structuralSyncTimer) {
|
|
237
|
+
clearTimeout(structuralSyncTimer);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
structuralSyncTimer = setTimeout(() => {
|
|
241
|
+
structuralSyncTimer = undefined;
|
|
242
|
+
void enqueueStructuralSync("update");
|
|
243
|
+
}, 75);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const restartForConfigChange = async (): Promise<void> => {
|
|
247
|
+
if (stopping) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
stopping = true;
|
|
251
|
+
publishReload("config-restart");
|
|
252
|
+
await cleanup({ preserveServer: false });
|
|
253
|
+
process.exit(RBSSR_DEV_RESTART_EXIT_CODE);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const handleAppEvent = (eventType: string, fileName?: string | Buffer | null): void => {
|
|
257
|
+
const relativePath = typeof fileName === "string"
|
|
258
|
+
? normalizeSlashes(fileName)
|
|
259
|
+
: "";
|
|
260
|
+
|
|
261
|
+
if (!relativePath) {
|
|
262
|
+
scheduleStructuralSync();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (eventType === "rename" && isStructuralAppPath(relativePath)) {
|
|
267
|
+
scheduleStructuralSync();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (eventType !== "change" || !isMarkdownRouteFile(relativePath)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const sourceFilePath = toAbsoluteAppPath(resolved.appDir, relativePath);
|
|
276
|
+
void (async () => {
|
|
277
|
+
if (!(await existsPath(sourceFilePath))) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
await compileMarkdownRouteModule({
|
|
281
|
+
routesDir: resolved.routesDir,
|
|
282
|
+
sourceFilePath,
|
|
283
|
+
generatedMarkdownRootDir,
|
|
284
|
+
});
|
|
285
|
+
nextClientBuildReason = "markdown-route";
|
|
286
|
+
})().catch((error) => {
|
|
287
|
+
// eslint-disable-next-line no-console
|
|
288
|
+
console.error("[rbssr] markdown rebuild failed", error);
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const addWatcher = (watcher: FSWatcher | null): void => {
|
|
293
|
+
if (watcher) {
|
|
294
|
+
watchers.push(watcher);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const cleanup = async (options: {
|
|
299
|
+
preserveServer: boolean;
|
|
300
|
+
}): Promise<void> => {
|
|
301
|
+
if (structuralSyncTimer) {
|
|
302
|
+
clearTimeout(structuralSyncTimer);
|
|
303
|
+
structuralSyncTimer = undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const watcher of watchers.splice(0, watchers.length)) {
|
|
307
|
+
watcher.close();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (clientWatch) {
|
|
311
|
+
await clientWatch.stop();
|
|
312
|
+
clientWatch = null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!options.preserveServer && bunServer) {
|
|
316
|
+
await bunServer.stop(true);
|
|
317
|
+
bunServer = undefined;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
addWatcher(
|
|
323
|
+
watch(resolved.appDir, { recursive: true }, (eventType, fileName) => {
|
|
324
|
+
handleAppEvent(eventType, fileName);
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
} catch {
|
|
328
|
+
log(`recursive file watching unavailable for ${resolved.appDir}; dev route topology updates may require a restart`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
addWatcher(
|
|
333
|
+
watch(options.cwd, (eventType, fileName) => {
|
|
334
|
+
if (typeof fileName !== "string" || !isConfigFileName(fileName)) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (eventType === "rename" || eventType === "change") {
|
|
338
|
+
void restartForConfigChange();
|
|
339
|
+
}
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
} catch {
|
|
343
|
+
log(`config file watching unavailable for ${options.cwd}; config changes may require a manual restart`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (import.meta.hot) {
|
|
347
|
+
import.meta.hot.dispose(async (data: DevHotData) => {
|
|
348
|
+
data.bunServer = bunServer;
|
|
349
|
+
data.reloadToken = reloadToken;
|
|
350
|
+
data.routeManifestVersion = routeManifestVersion;
|
|
351
|
+
await cleanup({ preserveServer: true });
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
await enqueueStructuralSync("bootstrap");
|
|
356
|
+
|
|
357
|
+
if (hotData.bunServer && bunServer) {
|
|
358
|
+
publishReload("server-runtime");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function createFrameworkServer(
|
|
363
|
+
userConfig: FrameworkConfig,
|
|
364
|
+
options: {
|
|
365
|
+
routeAssets: Record<string, BuildRouteAsset>;
|
|
366
|
+
reloadToken: () => number;
|
|
367
|
+
routeManifestVersion: () => number;
|
|
368
|
+
},
|
|
369
|
+
): ReturnType<typeof createServer> {
|
|
370
|
+
return createServer(
|
|
371
|
+
{
|
|
372
|
+
...userConfig,
|
|
373
|
+
mode: "development",
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
dev: true,
|
|
377
|
+
getDevAssets: () => options.routeAssets,
|
|
378
|
+
reloadVersion: options.reloadToken,
|
|
379
|
+
routeManifestVersion: options.routeManifestVersion,
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { statPath, type FileEntry } from "../runtime/io";
|
|
3
|
+
|
|
4
|
+
export interface CliFlags {
|
|
5
|
+
force: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type CliCommand = "init" | "dev" | "build" | "start" | "typecheck" | "test";
|
|
9
|
+
|
|
10
|
+
export type CliInvocation =
|
|
11
|
+
| { kind: "command"; command: CliCommand; args: string[] }
|
|
12
|
+
| { kind: "help" }
|
|
13
|
+
| { kind: "unknown"; command: string };
|
|
14
|
+
|
|
15
|
+
export const RBSSR_DEV_RESTART_EXIT_CODE = 75;
|
|
16
|
+
|
|
17
|
+
export function parseFlags(args: string[]): CliFlags {
|
|
18
|
+
return {
|
|
19
|
+
force: args.includes("--force"),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createProductionServerEntrypointSource(): string {
|
|
24
|
+
return `import path from "node:path";
|
|
25
|
+
import config from "../../rbssr.config.ts";
|
|
26
|
+
import { startHttpServer } from "react-bun-ssr";
|
|
27
|
+
|
|
28
|
+
const rootDir = path.resolve(path.dirname(Bun.fileURLToPath(import.meta.url)), "../..");
|
|
29
|
+
process.chdir(rootDir);
|
|
30
|
+
|
|
31
|
+
const manifestPath = path.resolve(rootDir, "dist/manifest.json");
|
|
32
|
+
const manifest = await Bun.file(manifestPath).json();
|
|
33
|
+
|
|
34
|
+
startHttpServer({
|
|
35
|
+
config: {
|
|
36
|
+
...(config ?? {}),
|
|
37
|
+
mode: "production",
|
|
38
|
+
},
|
|
39
|
+
runtimeOptions: {
|
|
40
|
+
dev: false,
|
|
41
|
+
buildManifest: manifest,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createDevHotEntrypointSource(options: {
|
|
48
|
+
cwd: string;
|
|
49
|
+
runtimeModulePath: string;
|
|
50
|
+
}): string {
|
|
51
|
+
const cwd = JSON.stringify(path.resolve(options.cwd));
|
|
52
|
+
const runtimeModulePath = JSON.stringify(path.resolve(options.runtimeModulePath));
|
|
53
|
+
|
|
54
|
+
return `import { runHotDevChild } from ${runtimeModulePath};
|
|
55
|
+
|
|
56
|
+
process.chdir(${cwd});
|
|
57
|
+
await runHotDevChild({
|
|
58
|
+
cwd: ${cwd},
|
|
59
|
+
});
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function readProjectEntries(rootDir: string): Promise<FileEntry[]> {
|
|
64
|
+
const entries: FileEntry[] = [];
|
|
65
|
+
|
|
66
|
+
for await (const name of new Bun.Glob("*").scan({
|
|
67
|
+
cwd: rootDir,
|
|
68
|
+
dot: true,
|
|
69
|
+
onlyFiles: false,
|
|
70
|
+
})) {
|
|
71
|
+
const entryStat = await statPath(path.join(rootDir, name));
|
|
72
|
+
if (!entryStat) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
entries.push({
|
|
77
|
+
name,
|
|
78
|
+
isDirectory: entryStat.isDirectory(),
|
|
79
|
+
isFile: entryStat.isFile(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createTypecheckCommand(): string[] {
|
|
87
|
+
return ["bun", "x", "tsc", "--noEmit"];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createTestCommands(extraArgs: string[]): string[][] {
|
|
91
|
+
if (extraArgs.length > 0) {
|
|
92
|
+
return [["bun", "test", ...extraArgs]];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
["bun", "test", "./tests/unit"],
|
|
97
|
+
["bun", "test", "./tests/integration"],
|
|
98
|
+
["bun", "x", "playwright", "test"],
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatCliHelp(): string {
|
|
103
|
+
return `rbssr commands:
|
|
104
|
+
rbssr init [--force]
|
|
105
|
+
rbssr dev
|
|
106
|
+
rbssr build
|
|
107
|
+
rbssr start
|
|
108
|
+
rbssr typecheck
|
|
109
|
+
rbssr test [bun-test-args]
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resolveCliInvocation(argv: string[]): CliInvocation {
|
|
114
|
+
const [command = "help", ...rest] = argv;
|
|
115
|
+
|
|
116
|
+
switch (command) {
|
|
117
|
+
case "init":
|
|
118
|
+
case "dev":
|
|
119
|
+
case "build":
|
|
120
|
+
case "start":
|
|
121
|
+
case "typecheck":
|
|
122
|
+
case "test":
|
|
123
|
+
return {
|
|
124
|
+
kind: "command",
|
|
125
|
+
command,
|
|
126
|
+
args: rest,
|
|
127
|
+
};
|
|
128
|
+
case "help":
|
|
129
|
+
case "--help":
|
|
130
|
+
case "-h":
|
|
131
|
+
return { kind: "help" };
|
|
132
|
+
default:
|
|
133
|
+
return {
|
|
134
|
+
kind: "unknown",
|
|
135
|
+
command,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
package/framework/cli/main.ts
CHANGED
|
@@ -8,49 +8,45 @@ import {
|
|
|
8
8
|
runTest,
|
|
9
9
|
runTypecheck,
|
|
10
10
|
} from "./commands";
|
|
11
|
+
import { formatCliHelp, resolveCliInvocation } from "./internal";
|
|
11
12
|
|
|
12
13
|
function printHelp(): void {
|
|
13
14
|
// eslint-disable-next-line no-console
|
|
14
|
-
console.log(
|
|
15
|
-
rbssr init [--force]
|
|
16
|
-
rbssr dev
|
|
17
|
-
rbssr build
|
|
18
|
-
rbssr start
|
|
19
|
-
rbssr typecheck
|
|
20
|
-
rbssr test [bun-test-args]
|
|
21
|
-
`);
|
|
15
|
+
console.log(formatCliHelp());
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
async function main(argv: string[]): Promise<void> {
|
|
25
|
-
const
|
|
19
|
+
const invocation = resolveCliInvocation(argv);
|
|
26
20
|
|
|
27
|
-
switch (
|
|
28
|
-
case "
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
21
|
+
switch (invocation.kind) {
|
|
22
|
+
case "command":
|
|
23
|
+
switch (invocation.command) {
|
|
24
|
+
case "init":
|
|
25
|
+
await runInit(invocation.args);
|
|
26
|
+
return;
|
|
27
|
+
case "dev":
|
|
28
|
+
await runDev();
|
|
29
|
+
return;
|
|
30
|
+
case "build":
|
|
31
|
+
await runBuild();
|
|
32
|
+
return;
|
|
33
|
+
case "start":
|
|
34
|
+
await runStart();
|
|
35
|
+
return;
|
|
36
|
+
case "typecheck":
|
|
37
|
+
await runTypecheck();
|
|
38
|
+
return;
|
|
39
|
+
case "test":
|
|
40
|
+
await runTest(invocation.args);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
45
43
|
return;
|
|
46
44
|
case "help":
|
|
47
|
-
case "--help":
|
|
48
|
-
case "-h":
|
|
49
45
|
printHelp();
|
|
50
46
|
return;
|
|
51
|
-
|
|
47
|
+
case "unknown":
|
|
52
48
|
// eslint-disable-next-line no-console
|
|
53
|
-
console.error(`Unknown command: ${command}`);
|
|
49
|
+
console.error(`Unknown command: ${invocation.command}`);
|
|
54
50
|
printHelp();
|
|
55
51
|
process.exit(1);
|
|
56
52
|
}
|