@bractjs/bractjs 0.1.16 → 0.1.18
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 +41 -0
- package/package.json +1 -1
- package/src/dev/server.ts +12 -1
- package/src/index.ts +2 -0
- package/src/server/lifecycle.ts +9 -0
- package/src/server/serve.ts +51 -15
- package/types/config.d.ts +4 -0
- package/types/index.d.ts +5 -0
package/README.md
CHANGED
|
@@ -440,6 +440,44 @@ export const db = new Database(Bun.env.DATABASE_URL);
|
|
|
440
440
|
|
|
441
441
|
---
|
|
442
442
|
|
|
443
|
+
## Server Lifecycle Hooks
|
|
444
|
+
|
|
445
|
+
Use `defineLifecycle()` in `app/lifecycle.ts` to run code when the server starts or shuts down. The shutdown hook runs on **any** exit signal (`SIGTERM`, `SIGINT`, `SIGUSR2`, `beforeExit`, and uncaught exceptions), so database connections are always closed cleanly.
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
// app/lifecycle.ts
|
|
449
|
+
import { defineLifecycle } from "@bractjs/bractjs";
|
|
450
|
+
import { db } from "./db.server.ts";
|
|
451
|
+
|
|
452
|
+
export default defineLifecycle({
|
|
453
|
+
async onStart() {
|
|
454
|
+
await db.connect();
|
|
455
|
+
console.log("Database connected");
|
|
456
|
+
},
|
|
457
|
+
async onShutdown() {
|
|
458
|
+
await db.disconnect();
|
|
459
|
+
console.log("Database disconnected");
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
BractJS picks up `app/lifecycle.ts` automatically in dev mode. In production, spread the hooks into `createServer()`:
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
// server.ts (production entry)
|
|
468
|
+
import { createServer } from "@bractjs/bractjs";
|
|
469
|
+
import lifecycle from "./app/lifecycle.ts";
|
|
470
|
+
|
|
471
|
+
createServer({ port: 3000, ...lifecycle });
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
| Hook | When it runs |
|
|
475
|
+
|------|-------------|
|
|
476
|
+
| `onStart` | Once, after the server begins accepting requests |
|
|
477
|
+
| `onShutdown` | Before process exit — any signal or uncaught exception |
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
443
481
|
## Configuration Reference
|
|
444
482
|
|
|
445
483
|
All fields are optional. BractJS works with zero configuration.
|
|
@@ -454,6 +492,8 @@ All fields are optional. BractJS works with zero configuration.
|
|
|
454
492
|
| `sourcemap` | `string` | `"external"` | `"none"` \| `"inline"` \| `"external"` |
|
|
455
493
|
| `minify` | `boolean` | `true` | Minify client bundles |
|
|
456
494
|
| `clientEnv` | `string[]` | `[]` | `process.env` keys exposed to the client |
|
|
495
|
+
| `onStart` | `() => void \| Promise<void>` | — | Called once after the server starts listening |
|
|
496
|
+
| `onShutdown` | `() => void \| Promise<void>` | — | Called before process exit on any signal |
|
|
457
497
|
|
|
458
498
|
---
|
|
459
499
|
|
|
@@ -475,6 +515,7 @@ All fields are optional. BractJS works with zero configuration.
|
|
|
475
515
|
my-app/
|
|
476
516
|
├── app/
|
|
477
517
|
│ ├── root.tsx # required — <html> shell
|
|
518
|
+
│ ├── lifecycle.ts # optional — onStart / onShutdown hooks
|
|
478
519
|
│ ├── route-types.gen.ts # generated by bractjs codegen
|
|
479
520
|
│ ├── actions.ts # "use server" actions
|
|
480
521
|
│ └── routes/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bractjs/bractjs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Production-grade SSR framework for Bun + React 19. File-based routing, streaming SSR, server actions, typed routes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/bractjs/bractjs#readme",
|
package/src/dev/server.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { watchApp } from "./watcher.ts";
|
|
|
5
5
|
import { rebuildClient } from "./rebuilder.ts";
|
|
6
6
|
import { filePathToPattern } from "../server/scanner.ts";
|
|
7
7
|
import { basename, extname } from "node:path";
|
|
8
|
+
import type { LifecycleHooks } from "../server/lifecycle.ts";
|
|
8
9
|
|
|
9
10
|
// Must precede any user-code import so SSR-time isDevRuntime() checks
|
|
10
11
|
// (e.g. inside <LiveReload>) observe the dev mode.
|
|
@@ -16,7 +17,17 @@ const hmr = createHmrServer(3001);
|
|
|
16
17
|
const { duration: initialMs } = await rebuildClient();
|
|
17
18
|
console.log(`[bractjs] initial client build in ${initialMs}ms`);
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
// Load user lifecycle hooks if defined (e.g. app/lifecycle.ts)
|
|
21
|
+
let lifecycle: LifecycleHooks = {};
|
|
22
|
+
try {
|
|
23
|
+
const lifecyclePath = `${process.cwd()}/app/lifecycle.ts`;
|
|
24
|
+
const mod = await import(lifecyclePath);
|
|
25
|
+
if (mod.default) lifecycle = mod.default;
|
|
26
|
+
} catch {
|
|
27
|
+
// No lifecycle file — that's fine
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
createServer({ port: 3000, ...lifecycle });
|
|
20
31
|
|
|
21
32
|
watchApp("./app", async (file) => {
|
|
22
33
|
const { duration } = await rebuildClient();
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ export { cssModulesPlugin, transformCssModule } from "./build/plugins/css-module
|
|
|
19
19
|
// Client RPC
|
|
20
20
|
export { createClient } from "./client/rpc.ts";
|
|
21
21
|
export type { BractJSConfig, RenderOptions, ServerManifest } from "./server/index.ts";
|
|
22
|
+
export { defineLifecycle } from "./server/lifecycle.ts";
|
|
23
|
+
export type { LifecycleHooks } from "./server/lifecycle.ts";
|
|
22
24
|
|
|
23
25
|
// Shared types
|
|
24
26
|
export type {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface LifecycleHooks {
|
|
2
|
+
onStart?: () => Promise<void> | void;
|
|
3
|
+
onShutdown?: () => Promise<void> | void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Type-safe helper for declaring server lifecycle hooks in app/lifecycle.ts. */
|
|
7
|
+
export function defineLifecycle(hooks: LifecycleHooks): LifecycleHooks {
|
|
8
|
+
return hooks;
|
|
9
|
+
}
|
package/src/server/serve.ts
CHANGED
|
@@ -32,6 +32,10 @@ export interface BractJSConfig {
|
|
|
32
32
|
buildDir?: string;
|
|
33
33
|
/** Directory for transformed image cache. Defaults to .bract-image-cache */
|
|
34
34
|
imageCacheDir?: string;
|
|
35
|
+
/** Called once after the server starts listening. Use to open DB connections, warm caches, etc. */
|
|
36
|
+
onStart?: () => Promise<void> | void;
|
|
37
|
+
/** Called before the process exits (any signal or uncaught error). Use to close DB connections, flush queues, etc. */
|
|
38
|
+
onShutdown?: () => Promise<void> | void;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
const DEFAULT_MANIFEST: ServerManifest = {
|
|
@@ -175,6 +179,11 @@ async function warnIfStaleBuild(buildDir: string): Promise<void> {
|
|
|
175
179
|
}
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
// Module-level guards so signal handlers are registered exactly once across
|
|
183
|
+
// HMR restarts and multiple createServer() calls in the same process.
|
|
184
|
+
let signalsRegistered = false;
|
|
185
|
+
let isShuttingDown = false;
|
|
186
|
+
|
|
178
187
|
export function createServer(config?: Partial<BractJSConfig>): {
|
|
179
188
|
stop(): void;
|
|
180
189
|
} {
|
|
@@ -192,28 +201,55 @@ export function createServer(config?: Partial<BractJSConfig>): {
|
|
|
192
201
|
if (adapter instanceof BunAdapter) {
|
|
193
202
|
adapter.setHandler(fetchHandler);
|
|
194
203
|
adapter.listen(port);
|
|
204
|
+
} else {
|
|
205
|
+
// Custom adapter: wire fetch handler in and call listen if available.
|
|
206
|
+
if ("setHandler" in adapter && typeof (adapter as unknown as { setHandler: unknown }).setHandler === "function") {
|
|
207
|
+
(adapter as unknown as { setHandler: (h: (r: Request) => Promise<Response>) => void }).setHandler(fetchHandler);
|
|
208
|
+
}
|
|
209
|
+
adapter.listen?.(port);
|
|
210
|
+
}
|
|
195
211
|
|
|
196
|
-
|
|
212
|
+
console.log(`[bract] Server running at http://localhost:${port}`);
|
|
197
213
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
214
|
+
const stopAdapter = () => {
|
|
215
|
+
if (adapter instanceof BunAdapter) {
|
|
216
|
+
adapter.stop();
|
|
217
|
+
} else if ("stop" in adapter && typeof (adapter as unknown as { stop: unknown }).stop === "function") {
|
|
218
|
+
(adapter as unknown as { stop: () => void }).stop();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const gracefulShutdown = async (signal?: string) => {
|
|
223
|
+
if (isShuttingDown) return;
|
|
224
|
+
isShuttingDown = true;
|
|
225
|
+
if (signal) console.log(`\n[bract] Received ${signal}, shutting down…`);
|
|
226
|
+
try {
|
|
227
|
+
await config?.onShutdown?.();
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error("[bract] onShutdown error:", err);
|
|
230
|
+
}
|
|
231
|
+
stopAdapter();
|
|
232
|
+
process.exit(0);
|
|
233
|
+
};
|
|
202
234
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
(
|
|
235
|
+
if (!signalsRegistered) {
|
|
236
|
+
signalsRegistered = true;
|
|
237
|
+
process.on("SIGTERM", () => void gracefulShutdown("SIGTERM"));
|
|
238
|
+
process.on("SIGINT", () => void gracefulShutdown("SIGINT"));
|
|
239
|
+
process.on("SIGUSR2", () => void gracefulShutdown("SIGUSR2"));
|
|
240
|
+
process.on("beforeExit", () => void gracefulShutdown());
|
|
241
|
+
process.on("uncaughtException", (err) => {
|
|
242
|
+
console.error("[bract] Uncaught exception:", err);
|
|
243
|
+
void gracefulShutdown("uncaughtException");
|
|
244
|
+
});
|
|
206
245
|
}
|
|
207
|
-
adapter.listen?.(port);
|
|
208
246
|
|
|
209
|
-
|
|
247
|
+
void Promise.resolve(config?.onStart?.()).catch((err) => {
|
|
248
|
+
console.error("[bract] onStart error:", err);
|
|
249
|
+
});
|
|
210
250
|
|
|
211
251
|
return {
|
|
212
|
-
stop() {
|
|
213
|
-
if ("stop" in adapter && typeof (adapter as unknown as { stop: unknown }).stop === "function") {
|
|
214
|
-
(adapter as unknown as { stop: () => void }).stop();
|
|
215
|
-
}
|
|
216
|
-
},
|
|
252
|
+
stop() { void gracefulShutdown(); },
|
|
217
253
|
};
|
|
218
254
|
}
|
|
219
255
|
|
package/types/config.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface BractJSConfig {
|
|
|
15
15
|
minify?: boolean;
|
|
16
16
|
/** process.env keys allowed to be inlined into client bundles. */
|
|
17
17
|
clientEnv?: string[];
|
|
18
|
+
/** Called once after the server starts listening. Use to open DB connections, warm caches, etc. */
|
|
19
|
+
onStart?: () => Promise<void> | void;
|
|
20
|
+
/** Called before the process exits (any signal or uncaught error). Use to close DB connections, flush queues, etc. */
|
|
21
|
+
onShutdown?: () => Promise<void> | void;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
export interface ServerManifest {
|
package/types/index.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export interface RenderOptions {
|
|
|
22
22
|
status?: number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface LifecycleHooks {
|
|
26
|
+
onStart?: () => Promise<void> | void;
|
|
27
|
+
onShutdown?: () => Promise<void> | void;
|
|
28
|
+
}
|
|
29
|
+
export declare function defineLifecycle(hooks: LifecycleHooks): LifecycleHooks;
|
|
25
30
|
export declare function createServer(config?: Partial<BractJSConfig>): { stop(): void };
|
|
26
31
|
export declare function renderRoute(options: RenderOptions): Promise<Response>;
|
|
27
32
|
export declare function redirect(url: string, status?: number): Response;
|