@bractjs/bractjs 0.1.16 → 0.1.17

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 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.16",
3
+ "version": "0.1.17",
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,16 @@ 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
- createServer({ port: 3000 });
20
+ // Load user lifecycle hooks if defined (e.g. app/lifecycle.ts)
21
+ let lifecycle: LifecycleHooks = {};
22
+ try {
23
+ const mod = await import("../../app/lifecycle.ts");
24
+ if (mod.default) lifecycle = mod.default;
25
+ } catch {
26
+ // No lifecycle file — that's fine
27
+ }
28
+
29
+ createServer({ port: 3000, ...lifecycle });
20
30
 
21
31
  watchApp("./app", async (file) => {
22
32
  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
+ }
@@ -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
- console.log(`[bract] Server running at http://localhost:${port}`);
212
+ console.log(`[bract] Server running at http://localhost:${port}`);
197
213
 
198
- return {
199
- stop() { adapter.stop(); },
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
- // Custom adapter: wire fetch handler in and call listen if available.
204
- if ("setHandler" in adapter && typeof (adapter as unknown as { setHandler: unknown }).setHandler === "function") {
205
- (adapter as unknown as { setHandler: (h: (r: Request) => Promise<Response>) => void }).setHandler(fetchHandler);
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
- console.log(`[bract] Server running at http://localhost:${port}`);
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;