@bractjs/bractjs 0.1.22 → 0.1.23
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 +137 -6
- package/bin/cli.ts +93 -5
- package/package.json +1 -1
- package/src/__tests__/action-registry.test.ts +54 -14
- package/src/__tests__/layout-registry.test.ts +95 -0
- package/src/__tests__/module-registry.test.ts +178 -0
- package/src/__tests__/prebuilt-handler.test.ts +94 -0
- package/src/__tests__/security.test.ts +12 -4
- package/src/__tests__/static-embedded.test.ts +74 -0
- package/src/build/bundler.ts +2 -2
- package/src/build/directives.ts +60 -21
- package/src/client/ClientRouter.tsx +4 -1
- package/src/codegen/module-registry.ts +312 -0
- package/src/dev/hmr-module-handler.ts +2 -2
- package/src/dev/rebuilder.ts +2 -2
- package/src/index.ts +30 -0
- package/src/server/action-registry.ts +41 -4
- package/src/server/layout.ts +74 -1
- package/src/server/lifecycle.ts +14 -0
- package/src/server/loader.ts +10 -5
- package/src/server/request-handler.ts +17 -6
- package/src/server/serve.ts +66 -17
- package/src/server/static.ts +27 -7
- package/templates/new-app/app/server.ts +30 -0
- package/templates/new-app/package.json +2 -1
- package/types/config.d.ts +18 -0
- package/types/index.d.ts +33 -0
- package/types/route.d.ts +8 -0
- package/src/__tests__/.tmp-security-action/routes/_index.tsx +0 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createElement } from "react";
|
|
2
2
|
import type { TrieNode } from "./matcher.ts";
|
|
3
3
|
import { matchRoute } from "./matcher.ts";
|
|
4
|
-
import { resolveRouteChain } from "./layout.ts";
|
|
4
|
+
import { resolveRouteChain, type ModuleRegistry } from "./layout.ts";
|
|
5
5
|
import { runLoaders, runAction, buildLoaderArgs, runRouteContext, runBeforeLoad } from "./loader.ts";
|
|
6
6
|
import { renderRoute, type ServerManifest } from "./render.ts";
|
|
7
7
|
import { resolveMeta } from "./meta.ts";
|
|
@@ -11,11 +11,19 @@ import { isExplicitDev } from "./env.ts";
|
|
|
11
11
|
import { pipeline, type MiddlewareContext } from "./middleware.ts";
|
|
12
12
|
import { BractJSProvider } from "../shared/context.ts";
|
|
13
13
|
import { isAllowedMutation } from "./csrf.ts";
|
|
14
|
+
import { fireOnError, type OnErrorHook } from "./lifecycle.ts";
|
|
14
15
|
|
|
15
16
|
export interface HandlerConfig {
|
|
16
17
|
appDir: string;
|
|
17
18
|
publicDir: string;
|
|
18
19
|
manifest: ServerManifest;
|
|
20
|
+
onError?: OnErrorHook;
|
|
21
|
+
/**
|
|
22
|
+
* Pre-loaded route/layout/root modules keyed by appDir-relative path.
|
|
23
|
+
* Provided by codegen (`_generated/routes.ts`) for compiled binaries
|
|
24
|
+
* where dynamic `import(absPath)` is unavailable. Falsy in dev mode.
|
|
25
|
+
*/
|
|
26
|
+
moduleRegistry?: ModuleRegistry;
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
const MUTATING_METHODS = new Set(["POST", "PUT", "DELETE", "PATCH"]);
|
|
@@ -45,7 +53,7 @@ async function route(
|
|
|
45
53
|
config: HandlerConfig,
|
|
46
54
|
context: Record<string, unknown>,
|
|
47
55
|
): Promise<Response> {
|
|
48
|
-
const { appDir, manifest } = config;
|
|
56
|
+
const { appDir, manifest, onError, moduleRegistry } = config;
|
|
49
57
|
const url = new URL(request.url);
|
|
50
58
|
const { pathname, searchParams } = url;
|
|
51
59
|
|
|
@@ -68,7 +76,7 @@ async function route(
|
|
|
68
76
|
if (!match) return json({ error: "Not Found" }, { status: 404 });
|
|
69
77
|
|
|
70
78
|
try {
|
|
71
|
-
const chain = await resolveRouteChain(match.routeFile, appDir);
|
|
79
|
+
const chain = await resolveRouteChain(match.routeFile, appDir, moduleRegistry);
|
|
72
80
|
// Reconstruct a Request that carries the original search params so loaders
|
|
73
81
|
// can access them via request.url / new URL(request.url).searchParams.
|
|
74
82
|
const targetUrl = new URL(request.url);
|
|
@@ -90,12 +98,13 @@ async function route(
|
|
|
90
98
|
const args = buildLoaderArgs(loaderRequest, match.params, routeContext);
|
|
91
99
|
const beforeLoadResponse = await runBeforeLoad(chain.route, args);
|
|
92
100
|
if (beforeLoadResponse) return beforeLoadResponse;
|
|
93
|
-
const results = await runLoaders(chain, args);
|
|
101
|
+
const results = await runLoaders(chain, args, onError);
|
|
94
102
|
return json({ root: results.root, layouts: results.layouts, route: results.route, params: match.params });
|
|
95
103
|
} catch (err) {
|
|
96
104
|
if (isRedirect(err)) return err as Response;
|
|
97
105
|
if (isHttpError(err)) return json({ error: err.message }, { status: err.status });
|
|
98
106
|
console.error("[bractjs] /_data error:", err);
|
|
107
|
+
await fireOnError(onError, err, request);
|
|
99
108
|
return json({ error: "Internal Server Error" }, { status: 500 });
|
|
100
109
|
}
|
|
101
110
|
}
|
|
@@ -104,7 +113,7 @@ async function route(
|
|
|
104
113
|
const match = matchRoute(pathname, trie);
|
|
105
114
|
if (!match) return error("Not Found", 404);
|
|
106
115
|
|
|
107
|
-
const chain = await resolveRouteChain(match.routeFile, appDir);
|
|
116
|
+
const chain = await resolveRouteChain(match.routeFile, appDir, moduleRegistry);
|
|
108
117
|
// Run per-route context factory (defineContext export) before loaders.
|
|
109
118
|
const routeContext = await runRouteContext(
|
|
110
119
|
chain.route as Parameters<typeof runRouteContext>[0],
|
|
@@ -138,6 +147,7 @@ async function route(
|
|
|
138
147
|
} catch (err) {
|
|
139
148
|
if (isRedirect(err)) return err as Response;
|
|
140
149
|
if (isHttpError(err)) return error(err.message, err.status);
|
|
150
|
+
await fireOnError(onError, err, request);
|
|
141
151
|
if (isExplicitDev()) return error(err instanceof Error ? err.message : String(err), 500);
|
|
142
152
|
return error("Internal Server Error", 500);
|
|
143
153
|
}
|
|
@@ -151,10 +161,11 @@ async function route(
|
|
|
151
161
|
// ── Loaders ───────────────────────────────────────────────────────────
|
|
152
162
|
let loaderResults;
|
|
153
163
|
try {
|
|
154
|
-
loaderResults = await runLoaders(chain, args);
|
|
164
|
+
loaderResults = await runLoaders(chain, args, onError);
|
|
155
165
|
} catch (err) {
|
|
156
166
|
if (isRedirect(err)) return err as Response;
|
|
157
167
|
if (isHttpError(err)) return error(err.message, err.status);
|
|
168
|
+
await fireOnError(onError, err, request);
|
|
158
169
|
if (isExplicitDev()) return error(err instanceof Error ? err.message : String(err), 500);
|
|
159
170
|
return error("Internal Server Error", 500);
|
|
160
171
|
}
|
package/src/server/serve.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { scanRoutes } from "./scanner.ts";
|
|
1
|
+
import { scanRoutes, type RouteFile } from "./scanner.ts";
|
|
2
2
|
import { buildTrie } from "./matcher.ts";
|
|
3
3
|
import { handleRequest, type HandlerConfig } from "./request-handler.ts";
|
|
4
4
|
import { type ServerManifest } from "./render.ts";
|
|
@@ -6,10 +6,12 @@ import { isDevRuntime, isExplicitDev } from "./env.ts";
|
|
|
6
6
|
import { loadManifest } from "../build/manifest.ts";
|
|
7
7
|
import { serveStatic } from "./static.ts";
|
|
8
8
|
import { handleImageRequest } from "../image/handler.ts";
|
|
9
|
-
import { loadServerActions } from "./action-registry.ts";
|
|
9
|
+
import { loadServerActions, loadServerActionsFromRegistry } from "./action-registry.ts";
|
|
10
10
|
import { handleActionRequest } from "./action-handler.ts";
|
|
11
11
|
import { BunAdapter, type BractAdapter } from "./adapter.ts";
|
|
12
|
+
import type { ModuleRegistry } from "./layout.ts";
|
|
12
13
|
import { resolve, join } from "node:path";
|
|
14
|
+
import { fireOnError, type OnErrorHook } from "./lifecycle.ts";
|
|
13
15
|
|
|
14
16
|
export interface I18nConfig {
|
|
15
17
|
locales: string[];
|
|
@@ -38,6 +40,27 @@ export interface BractJSConfig {
|
|
|
38
40
|
onStart?: () => Promise<void> | void;
|
|
39
41
|
/** Called before the process exits (any signal or uncaught error). Use to close DB connections, flush queues, etc. */
|
|
40
42
|
onShutdown?: () => Promise<void> | void;
|
|
43
|
+
/** Called for every unexpected error: loader failures, action throws, and uncaught process exceptions. Redirects and HttpErrors are intentional control flow and are NOT reported here. The request is undefined for process-level exceptions. */
|
|
44
|
+
onError?: OnErrorHook;
|
|
45
|
+
/**
|
|
46
|
+
* Pre-scanned route list (typically exported from `app/_generated/routes.ts`).
|
|
47
|
+
* When provided, skips the startup `Bun.Glob` scan of `appDir`. Required for
|
|
48
|
+
* `bun build --compile` binaries where the embedded filesystem has no
|
|
49
|
+
* scannable routes/ directory.
|
|
50
|
+
*/
|
|
51
|
+
routeFiles?: RouteFile[];
|
|
52
|
+
/**
|
|
53
|
+
* Pre-loaded route/layout/root modules keyed by appDir-relative path.
|
|
54
|
+
* Required alongside `routeFiles` for compiled binaries — `resolveRouteChain`
|
|
55
|
+
* uses this map instead of `import(absPath)` at request time.
|
|
56
|
+
*/
|
|
57
|
+
moduleRegistry?: ModuleRegistry;
|
|
58
|
+
/**
|
|
59
|
+
* Pre-imported server-action modules (typically `app/_generated/actions.ts`).
|
|
60
|
+
* When provided, skips the startup `Bun.Glob` scan + dynamic import that
|
|
61
|
+
* `loadServerActions` does.
|
|
62
|
+
*/
|
|
63
|
+
actionModules?: Array<{ relPath: string; mod: Record<string, unknown> }>;
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
const DEFAULT_MANIFEST: ServerManifest = {
|
|
@@ -84,8 +107,18 @@ export function buildFetchHandler(config: Partial<BractJSConfig>) {
|
|
|
84
107
|
}))
|
|
85
108
|
: Promise.resolve(config.manifest ?? DEFAULT_MANIFEST);
|
|
86
109
|
|
|
87
|
-
|
|
88
|
-
|
|
110
|
+
// Codegen / compiled-binary path: when the caller supplies pre-scanned
|
|
111
|
+
// routes, skip the runtime `Bun.Glob` scan that `bun build --compile`
|
|
112
|
+
// can't satisfy (the routes/ directory isn't on the filesystem in a
|
|
113
|
+
// single-binary deployment). Same idea for server actions.
|
|
114
|
+
const trieReady = config.routeFiles
|
|
115
|
+
? Promise.resolve(buildTrie(config.routeFiles))
|
|
116
|
+
: scanRoutes(appDir).then(buildTrie);
|
|
117
|
+
const actionsReady = config.actionModules
|
|
118
|
+
? loadServerActionsFromRegistry(config.actionModules)
|
|
119
|
+
: loadServerActions(appDir);
|
|
120
|
+
const moduleRegistry = config.moduleRegistry;
|
|
121
|
+
const onError = config.onError;
|
|
89
122
|
|
|
90
123
|
return async function fetch(request: Request): Promise<Response> {
|
|
91
124
|
const url = new URL(request.url);
|
|
@@ -154,7 +187,7 @@ export function buildFetchHandler(config: Partial<BractJSConfig>) {
|
|
|
154
187
|
|
|
155
188
|
const trie = await trieReady;
|
|
156
189
|
const manifest = isDevRuntime() ? await readDevManifest(buildDir) : await manifestReady;
|
|
157
|
-
const handlerConfig: HandlerConfig = { appDir, publicDir, manifest };
|
|
190
|
+
const handlerConfig: HandlerConfig = { appDir, publicDir, manifest, onError, moduleRegistry };
|
|
158
191
|
return handleRequest(request, trie, handlerConfig);
|
|
159
192
|
};
|
|
160
193
|
}
|
|
@@ -186,6 +219,7 @@ async function warnIfStaleBuild(buildDir: string): Promise<void> {
|
|
|
186
219
|
let signalsRegistered = false;
|
|
187
220
|
let isShuttingDown = false;
|
|
188
221
|
let activeOnShutdown: (() => Promise<void> | void) | undefined;
|
|
222
|
+
let activeOnError: OnErrorHook | undefined;
|
|
189
223
|
|
|
190
224
|
export function createServer(config?: Partial<BractJSConfig>): {
|
|
191
225
|
stop(): void;
|
|
@@ -213,6 +247,7 @@ export function createServer(config?: Partial<BractJSConfig>): {
|
|
|
213
247
|
}
|
|
214
248
|
|
|
215
249
|
activeOnShutdown = config?.onShutdown;
|
|
250
|
+
activeOnError = config?.onError;
|
|
216
251
|
|
|
217
252
|
console.log(`[bract] Server running at http://localhost:${port}`);
|
|
218
253
|
|
|
@@ -224,37 +259,46 @@ export function createServer(config?: Partial<BractJSConfig>): {
|
|
|
224
259
|
}
|
|
225
260
|
};
|
|
226
261
|
|
|
227
|
-
|
|
262
|
+
// Programmatic / beforeExit path — runs the user hook, stops the adapter,
|
|
263
|
+
// and returns. Does NOT call process.exit() so callers (tests, parent
|
|
264
|
+
// supervisors) can keep running. `gracefulShutdown` below wraps this and
|
|
265
|
+
// adds an explicit exit for signal handlers, where termination is the
|
|
266
|
+
// whole point.
|
|
267
|
+
const shutdownOnce = async (signal?: string): Promise<void> => {
|
|
228
268
|
if (isShuttingDown) return;
|
|
229
269
|
isShuttingDown = true;
|
|
230
270
|
if (signal) console.log(`\n[bract] Received ${signal}, shutting down…`);
|
|
231
271
|
try {
|
|
232
272
|
const result = activeOnShutdown?.();
|
|
233
273
|
if (result instanceof Promise) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
process.exit(0);
|
|
237
|
-
});
|
|
238
|
-
} else {
|
|
239
|
-
stopAdapter();
|
|
240
|
-
process.exit(0);
|
|
274
|
+
try { await result; }
|
|
275
|
+
catch (err) { console.error("[bract] onShutdown error:", err); }
|
|
241
276
|
}
|
|
242
277
|
} catch (err) {
|
|
243
278
|
console.error("[bract] onShutdown error:", err);
|
|
279
|
+
} finally {
|
|
244
280
|
stopAdapter();
|
|
245
|
-
process.exit(1);
|
|
246
281
|
}
|
|
247
282
|
};
|
|
248
283
|
|
|
284
|
+
const gracefulShutdown = (signal?: string, exitCode = 0): void => {
|
|
285
|
+
void shutdownOnce(signal).finally(() => process.exit(exitCode));
|
|
286
|
+
};
|
|
287
|
+
|
|
249
288
|
if (!signalsRegistered) {
|
|
250
289
|
signalsRegistered = true;
|
|
251
290
|
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
252
291
|
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
253
292
|
process.on("SIGUSR2", () => gracefulShutdown("SIGUSR2"));
|
|
254
|
-
|
|
293
|
+
// `beforeExit` fires when the event loop is naturally draining — we
|
|
294
|
+
// already shut down the adapter at that point, but we must NOT call
|
|
295
|
+
// process.exit(). Doing so re-enters the lifecycle and prevents test
|
|
296
|
+
// runners (and any parent process supervising us) from observing a
|
|
297
|
+
// clean exit code. Just run the user hook + stop the listener.
|
|
298
|
+
process.on("beforeExit", () => { void shutdownOnce(); });
|
|
255
299
|
process.on("uncaughtException", (err) => {
|
|
256
300
|
console.error("[bract] Uncaught exception:", err);
|
|
257
|
-
gracefulShutdown("uncaughtException");
|
|
301
|
+
void fireOnError(activeOnError, err).then(() => gracefulShutdown("uncaughtException", 1));
|
|
258
302
|
});
|
|
259
303
|
}
|
|
260
304
|
|
|
@@ -263,7 +307,12 @@ export function createServer(config?: Partial<BractJSConfig>): {
|
|
|
263
307
|
});
|
|
264
308
|
|
|
265
309
|
return {
|
|
266
|
-
stop
|
|
310
|
+
// Programmatic stop — runs `onShutdown`, then closes the listener. Does
|
|
311
|
+
// NOT call `process.exit()`. Tests rely on this so the runner can print
|
|
312
|
+
// its summary; long-running supervisors rely on it so a stop() doesn't
|
|
313
|
+
// tear down the whole worker. Use SIGTERM/SIGINT if you actually want
|
|
314
|
+
// the process to exit.
|
|
315
|
+
stop() { void shutdownOnce(); },
|
|
267
316
|
};
|
|
268
317
|
}
|
|
269
318
|
|
package/src/server/static.ts
CHANGED
|
@@ -4,20 +4,40 @@ import { realpath } from "node:fs/promises";
|
|
|
4
4
|
const IMMUTABLE = "public, max-age=31536000, immutable";
|
|
5
5
|
const NO_CACHE = "no-cache";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Resolve to a canonical path that follows symlinks, with a fallback for
|
|
9
|
+
* `bun build --compile` binaries.
|
|
10
|
+
*
|
|
11
|
+
* Three outcomes:
|
|
12
|
+
* 1. realpath succeeds + stays inside `root` → return resolved path
|
|
13
|
+
* 2. realpath throws AND `Bun.file(candidate)` exists → return the candidate
|
|
14
|
+
* path. This is the embedded-asset case: `bun --compile` exposes assets
|
|
15
|
+
* through virtual paths that don't appear in the filesystem, so realpath
|
|
16
|
+
* errors with ENOENT/EINVAL but `Bun.file()` still reads them.
|
|
17
|
+
* 3. otherwise → null (escape, ENOENT, etc.)
|
|
18
|
+
*
|
|
19
|
+
* The structural `startsWith(root + sep)` check at the top runs before any
|
|
20
|
+
* I/O and is the authoritative traversal guard — the realpath check is
|
|
21
|
+
* defense-in-depth against symlink escape, which can't happen inside an
|
|
22
|
+
* embedded virtual filesystem.
|
|
23
|
+
*/
|
|
9
24
|
async function safeRealpath(root: string, requested: string): Promise<string | null> {
|
|
10
25
|
const candidate = resolve(join(root, requested));
|
|
11
|
-
// Cheap structural reject before touching the FS.
|
|
26
|
+
// Cheap structural reject before touching the FS. This blocks `..` escapes
|
|
27
|
+
// unconditionally, in both the normal-FS and embedded-binary paths.
|
|
12
28
|
if (!candidate.startsWith(root + sep) && candidate !== root) return null;
|
|
13
|
-
let real: string;
|
|
14
29
|
try {
|
|
15
|
-
real = await realpath(candidate);
|
|
30
|
+
const real = await realpath(candidate);
|
|
31
|
+
if (!real.startsWith(root + sep) && real !== root) return null;
|
|
32
|
+
return real;
|
|
16
33
|
} catch {
|
|
34
|
+
// realpath fails for paths embedded by `bun build --compile --asset`.
|
|
35
|
+
// The structural check above already prevented traversal, so the only
|
|
36
|
+
// remaining concern is whether the asset actually exists — defer to
|
|
37
|
+
// Bun.file() which reads from the embed table.
|
|
38
|
+
if (await Bun.file(candidate).exists()) return candidate;
|
|
17
39
|
return null;
|
|
18
40
|
}
|
|
19
|
-
if (!real.startsWith(root + sep) && real !== root) return null;
|
|
20
|
-
return real;
|
|
21
41
|
}
|
|
22
42
|
|
|
23
43
|
/**
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Entry point for `bun build --compile`.
|
|
2
|
+
//
|
|
3
|
+
// Pre-generated registries below come from `bractjs codegen:registry` and
|
|
4
|
+
// `bractjs codegen:manifest`. They turn every runtime fs scan / dynamic
|
|
5
|
+
// import the framework would normally do into static, traceable imports so
|
|
6
|
+
// `bun build --compile` can produce a single executable.
|
|
7
|
+
//
|
|
8
|
+
// Generate-then-compile:
|
|
9
|
+
// bractjs codegen:registry # writes routes.ts + actions.ts
|
|
10
|
+
// bractjs build # writes build/client/* + manifest
|
|
11
|
+
// bractjs codegen:manifest # snapshots manifest into TS
|
|
12
|
+
// bun build --compile app/server.ts --outfile myapp [--asset build/client/]
|
|
13
|
+
//
|
|
14
|
+
// Or run all of the above in one shot:
|
|
15
|
+
// bractjs compile
|
|
16
|
+
|
|
17
|
+
import { createServer } from "@bractjs/bractjs";
|
|
18
|
+
import { routeFiles, moduleRegistry } from "./_generated/routes.ts";
|
|
19
|
+
import { actionModules } from "./_generated/actions.ts";
|
|
20
|
+
import { manifest } from "./_generated/manifest.ts";
|
|
21
|
+
|
|
22
|
+
createServer({
|
|
23
|
+
port: Number(process.env.PORT ?? 3000),
|
|
24
|
+
appDir: "./app",
|
|
25
|
+
publicDir: "./public",
|
|
26
|
+
manifest,
|
|
27
|
+
routeFiles,
|
|
28
|
+
moduleRegistry,
|
|
29
|
+
actionModules,
|
|
30
|
+
});
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "bractjs dev",
|
|
7
7
|
"build": "NODE_ENV=production bractjs build",
|
|
8
|
-
"start": "NODE_ENV=production bractjs start"
|
|
8
|
+
"start": "NODE_ENV=production bractjs start",
|
|
9
|
+
"compile": "NODE_ENV=production bractjs compile"
|
|
9
10
|
},
|
|
10
11
|
"dependencies": {
|
|
11
12
|
"@bractjs/bractjs": "latest",
|
package/types/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BunPlugin } from "bun";
|
|
2
|
+
import type { RouteFile, RouteModule } from "./route.d.ts";
|
|
2
3
|
|
|
3
4
|
export interface BractJSConfig {
|
|
4
5
|
/** TCP port to listen on. Default: 3000. */
|
|
@@ -23,6 +24,23 @@ export interface BractJSConfig {
|
|
|
23
24
|
onStart?: () => Promise<void> | void;
|
|
24
25
|
/** Called before the process exits (any signal or uncaught error). Use to close DB connections, flush queues, etc. */
|
|
25
26
|
onShutdown?: () => Promise<void> | void;
|
|
27
|
+
/**
|
|
28
|
+
* Pre-scanned route list. Typically imported from `app/_generated/routes.ts`.
|
|
29
|
+
* Required for `bun build --compile` binaries where the routes/ directory
|
|
30
|
+
* isn't on a scannable filesystem.
|
|
31
|
+
*/
|
|
32
|
+
routeFiles?: RouteFile[];
|
|
33
|
+
/**
|
|
34
|
+
* Pre-loaded route/layout/root modules keyed by appDir-relative path.
|
|
35
|
+
* Typically imported from `app/_generated/routes.ts`.
|
|
36
|
+
*/
|
|
37
|
+
moduleRegistry?: Record<string, RouteModule | Record<string, unknown>>;
|
|
38
|
+
/**
|
|
39
|
+
* Pre-imported server-action modules. Typically imported from
|
|
40
|
+
* `app/_generated/actions.ts`. Each `relPath` MUST match what the client
|
|
41
|
+
* proxy plugin hashed during the client build.
|
|
42
|
+
*/
|
|
43
|
+
actionModules?: Array<{ relPath: string; mod: Record<string, unknown> }>;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
export interface ServerManifest {
|
package/types/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ReactNode, Context } from "react";
|
|
|
4
4
|
export type {
|
|
5
5
|
LoaderArgs, ActionArgs, MetaDescriptor, MetaArgs,
|
|
6
6
|
LoaderFunction, ActionFunction, MetaFunction, RouteModule,
|
|
7
|
+
RouteFile, Segment,
|
|
7
8
|
} from "./route.d.ts";
|
|
8
9
|
|
|
9
10
|
// ── Config + Server ───────────────────────────────────────────────────────
|
|
@@ -22,9 +23,13 @@ export interface RenderOptions {
|
|
|
22
23
|
status?: number;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
export type OnErrorHook = (err: unknown, request?: Request) => Promise<void> | void;
|
|
27
|
+
|
|
25
28
|
export interface LifecycleHooks {
|
|
26
29
|
onStart?: () => Promise<void> | void;
|
|
27
30
|
onShutdown?: () => Promise<void> | void;
|
|
31
|
+
/** Called for every unexpected error: loader failures, action throws, and uncaught process exceptions. Redirects and HttpErrors are NOT reported here. The request is undefined for process-level exceptions. */
|
|
32
|
+
onError?: OnErrorHook;
|
|
28
33
|
}
|
|
29
34
|
export declare function defineLifecycle(hooks: LifecycleHooks): LifecycleHooks;
|
|
30
35
|
export declare function createServer(config?: Partial<BractJSConfig>): { stop(): void };
|
|
@@ -203,6 +208,34 @@ export declare function makeCloudflareHandler(
|
|
|
203
208
|
export declare const cssModulesPlugin: unknown; // BunPlugin
|
|
204
209
|
export declare function transformCssModule(filePath: string): Promise<{ map: Record<string, string>; css: string }>;
|
|
205
210
|
|
|
211
|
+
// ── Build plugins (required for native `bun build --compile` workflow) ───
|
|
212
|
+
//
|
|
213
|
+
// Apply all of these on the relevant bundle or face crashes / secret leaks:
|
|
214
|
+
// server bundle → useClientStubPlugin
|
|
215
|
+
// client bundle → createUseServerProxyPlugin(appDir), serverOnlyPlugin,
|
|
216
|
+
// clientEnvPlugin(allowedKeys, env), cssModulesPlugin
|
|
217
|
+
export declare const useClientStubPlugin: unknown; // BunPlugin
|
|
218
|
+
export declare function createUseServerProxyPlugin(appDir?: string): unknown; // BunPlugin
|
|
219
|
+
export declare const useServerProxyPlugin: unknown; // BunPlugin (legacy — uses absolute paths)
|
|
220
|
+
export declare const serverOnlyPlugin: unknown; // BunPlugin
|
|
221
|
+
export declare function clientEnvPlugin(
|
|
222
|
+
allowedKeys: string[],
|
|
223
|
+
envValues: Record<string, string>,
|
|
224
|
+
): unknown; // BunPlugin
|
|
225
|
+
|
|
226
|
+
// ── Module-registry codegen (drives `bun build --compile`) ────────────────
|
|
227
|
+
export interface CodegenResult {
|
|
228
|
+
routesPath: string;
|
|
229
|
+
actionsPath: string;
|
|
230
|
+
}
|
|
231
|
+
/** Scan appDir; write `<appDir>/_generated/routes.ts` and `actions.ts`. */
|
|
232
|
+
export declare function writeModuleRegistries(appDir: string): Promise<CodegenResult>;
|
|
233
|
+
/** Read `<buildDir>/route-manifest.json`; write `<appDir>/_generated/manifest.ts`. */
|
|
234
|
+
export declare function writeManifestModule(appDir: string, buildDir: string): Promise<string>;
|
|
235
|
+
|
|
236
|
+
/** Pre-loaded route/layout/root modules keyed by appDir-relative path. */
|
|
237
|
+
export type ModuleRegistry = Record<string, import("./route.d.ts").RouteModule | Record<string, unknown>>;
|
|
238
|
+
|
|
206
239
|
// ── buildFetchHandler (D1) ───────────────────────────────────────────────
|
|
207
240
|
export declare function buildFetchHandler(config: Partial<import("./config.d.ts").BractJSConfig>): (request: Request) => Promise<Response>;
|
|
208
241
|
|
package/types/route.d.ts
CHANGED
|
@@ -50,3 +50,11 @@ export interface RouteModule<TLoader = unknown, TAction = unknown> {
|
|
|
50
50
|
ErrorBoundary?: ComponentType<{ error: unknown }>;
|
|
51
51
|
default?: ComponentType;
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
export type Segment = string | { param: string } | { catchAll: string };
|
|
55
|
+
|
|
56
|
+
export interface RouteFile {
|
|
57
|
+
filePath: string;
|
|
58
|
+
urlPattern: string;
|
|
59
|
+
segments: Segment[];
|
|
60
|
+
}
|