@web-ts-toolkit/express-runtime 0.24.0 → 0.26.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.
Files changed (3) hide show
  1. package/README.md +96 -41
  2. package/cli.js +121 -26
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,10 +23,12 @@ pnpm add @web-ts-toolkit/express-runtime express
23
23
  - `startLocalServer()` — `http.createServer` + `listen` with friendly
24
24
  `EADDRINUSE` / `EACCES` errors, optional graceful `SIGINT` / `SIGTERM`
25
25
  shutdown that drains in-flight requests, and a configurable timeout.
26
- - CLI binary with three subcommands:
26
+ - CLI binary with five subcommands:
27
27
  - `dev` — run an Express app as a local dev server
28
- - `build` — bundle the app as a serverless handler
29
- - `start` — smoke-test the bundled handler locally by translating HTTP ↔ serverless events
28
+ - `build` — bundle the app as a local runtime module
29
+ - `start` — start the bundled local app module
30
+ - `build-serverless` — bundle the app as a serverless handler
31
+ - `start-serverless` — smoke-test the bundled handler locally by translating HTTP ↔ serverless events
30
32
 
31
33
  ## Quick Start
32
34
 
@@ -131,19 +133,19 @@ npx tsx ./node_modules/@web-ts-toolkit/express-runtime/dist/cli.js dev ./src/app
131
133
  > be placed at the top level of your app module since `dev` does not expose an
132
134
  > `init` hook.
133
135
 
134
- ### CLI — build (serverless bundle)
136
+ ### CLI — build (local runtime bundle)
135
137
 
136
- The `build` command generates a temporary serverless entry that wraps the app
137
- with `createServerlessHandler`, then bundles it into a deployment-ready file:
138
+ The `build` command generates a temporary runtime entry that re-exports the app
139
+ and optional `init` hook, then bundles it into a local runtime file:
138
140
 
139
141
  ```sh
140
- npx wtt-express-runtime build ./src/app.ts --out-dir netlify/functions
142
+ npx wtt-express-runtime build ./src/app.ts --out-dir dist
141
143
  ```
142
144
 
143
145
  With an optional init hook (DB connections, cache warmup, etc.):
144
146
 
145
147
  ```sh
146
- npx wtt-express-runtime build ./src/app.ts --init ./src/init.ts --out-dir netlify/functions
148
+ npx wtt-express-runtime build ./src/app.ts --init ./src/init.ts --out-dir dist
147
149
  ```
148
150
 
149
151
  ```ts
@@ -153,29 +155,54 @@ export default async () => {
153
155
  };
154
156
  ```
155
157
 
156
- This produces `netlify/functions/handler.js` (configurable via `--out-name`)
157
- that exports a `handler` function compatible with Netlify, Vercel, AWS Lambda,
158
- and any platform that calls `(event, context)`.
158
+ This produces `dist/app.js` (configurable via `--out-name`) that default-exports
159
+ the Express app and, when `--init` is used, also exports `init` for the `start`
160
+ command to run before listening.
159
161
 
160
162
  > `express` is always external; additional externals can be added via
161
163
  > `--external`.
162
164
 
163
- ### CLI — start (run a bundled handler locally)
165
+ ### CLI — start (run a bundled app locally)
164
166
 
165
- The `start` command runs a bundled serverless handler locally by translating
166
- HTTP requests into serverless events and the handler's results back into HTTP
167
- responses — letting you smoke-test the exact `build` output without a
168
- serverless platform:
167
+ The `start` command runs the `build` output locally with `startLocalServer()`:
169
168
 
170
169
  ```sh
171
170
  npx wtt-express-runtime build ./src/app.ts --out-dir dist
172
- npx wtt-express-runtime start ./dist/handler.js --port 9000 --env .env
171
+ npx wtt-express-runtime start ./dist/app.js --port 9000 --env .env
172
+ ```
173
+
174
+ The bundled app module must default-export an Express app (or export it as
175
+ `app`). If it exports `init`, that hook runs once before the server starts
176
+ listening.
177
+
178
+ ### CLI — build-serverless (serverless bundle)
179
+
180
+ The `build-serverless` command preserves the previous serverless bundling flow:
181
+
182
+ ```sh
183
+ npx wtt-express-runtime build-serverless ./src/app.ts --out-dir netlify/functions
173
184
  ```
174
185
 
175
- The handler module must export a `handler` function (named or default) that
176
- accepts `(event, context)` and returns a result with `statusCode`, `headers`,
177
- and `body` — the same shape produced by `createServerlessHandler` via
178
- `serverless-http`.
186
+ With an optional init hook:
187
+
188
+ ```sh
189
+ npx wtt-express-runtime build-serverless ./src/app.ts --init ./src/init.ts --out-dir netlify/functions
190
+ ```
191
+
192
+ This produces `netlify/functions/handler.js` (configurable via `--out-name`)
193
+ that exports a `handler` function compatible with Netlify, Vercel, AWS Lambda,
194
+ and any platform that calls `(event, context)`.
195
+
196
+ ### CLI — start-serverless (run a bundled handler locally)
197
+
198
+ The `start-serverless` command runs a bundled serverless handler locally by
199
+ translating HTTP requests into serverless events and the handler's results back
200
+ into HTTP responses:
201
+
202
+ ```sh
203
+ npx wtt-express-runtime build-serverless ./src/app.ts --out-dir dist
204
+ npx wtt-express-runtime start-serverless ./dist/handler.js --port 9000 --env .env
205
+ ```
179
206
 
180
207
  > The adapter passes the raw request body as a Buffer (no body parsing) so the
181
208
  > handler's request hook, including the serverless-http #305 workaround, runs
@@ -298,11 +325,13 @@ interface Logger {
298
325
 
299
326
  Omitting `<command>` defaults to `dev` for backward compatibility.
300
327
 
301
- | Command | Description |
302
- | ------- | ----------------------------------------------------------------------------------- |
303
- | `dev` | Run the Express app as a local dev server (`http.createServer` + graceful shutdown) |
304
- | `build` | Bundle the Express app as a serverless handler |
305
- | `start` | Run a bundled serverless handler locally (HTTP ↔ serverless event adapter) |
328
+ | Command | Description |
329
+ | ------------------ | ----------------------------------------------------------------------------------- |
330
+ | `dev` | Run the Express app as a local dev server (`http.createServer` + graceful shutdown) |
331
+ | `build` | Bundle the Express app as a local runtime module |
332
+ | `start` | Run a bundled local app module with `startLocalServer()` |
333
+ | `build-serverless` | Bundle the Express app as a serverless handler |
334
+ | `start-serverless` | Run a bundled serverless handler locally (HTTP ↔ serverless event adapter) |
306
335
 
307
336
  #### dev options
308
337
 
@@ -326,7 +355,7 @@ Omitting `<command>` defaults to `dev` for backward compatibility.
326
355
  | `<app-module>` | Module path whose **default export** is an Express app (sync, not async factory) |
327
356
  | `--init <path>` | Init hook module (default export, async function) called once per cold start |
328
357
  | `--out-dir <path>` | Output directory (default: `dist`) |
329
- | `--out-name <name>` | Output filename without extension (default: `handler`) |
358
+ | `--out-name <name>` | Output filename without extension (default: `app`) |
330
359
  | `--format <cjs\|esm>` | Output format (default: `cjs`) |
331
360
  | `--target <target>` | Compilation target (default: `node22`) |
332
361
  | `--external <pkg>` | Mark package as external (repeatable; `express` is always external) |
@@ -334,15 +363,40 @@ Omitting `<command>` defaults to `dev` for backward compatibility.
334
363
 
335
364
  #### start options
336
365
 
337
- | Option | Description |
338
- | ------------------------- | ------------------------------------------------------------------------------------------ |
339
- | `<handler-module>` | JS/CJS module path exporting `handler` (named or default) — the output of `build` |
340
- | `--port <number>` | Port or named pipe (default: `process.env.PORT` or `8080`) |
341
- | `--host <hostname>` | Hostname to bind (default: `process.env.HOST` or `0.0.0.0`) |
342
- | `--no-signals` | Disable `SIGINT` / `SIGTERM` handler registration |
343
- | `--shutdown-timeout <ms>` | Max ms to wait for in-flight requests (default: `5000`) |
344
- | `--require <module>` | Module(s) to preload before handler load (repeatable; comma-separated values supported) |
345
- | `--env <path>` | Env file(s) to load before handler load (repeatable; existing env vars are not overridden) |
366
+ | Option | Description |
367
+ | ------------------------- | ------------------------------------------------------------------------------------------------ |
368
+ | `<app-module>` | JS/CJS module path default-exporting an Express app (or exporting `app`) — the output of `build` |
369
+ | `--port <number>` | Port or named pipe (default: `process.env.PORT` or `8080`) |
370
+ | `--host <hostname>` | Hostname to bind (default: `process.env.HOST` or `0.0.0.0`) |
371
+ | `--no-signals` | Disable `SIGINT` / `SIGTERM` handler registration |
372
+ | `--shutdown-timeout <ms>` | Max ms to wait for in-flight requests (default: `5000`) |
373
+ | `--require <module>` | Module(s) to preload before app load (repeatable; comma-separated values supported) |
374
+ | `--env <path>` | Env file(s) to load before app load (repeatable; existing env vars are not overridden) |
375
+
376
+ #### build-serverless options
377
+
378
+ | Option | Description |
379
+ | --------------------- | -------------------------------------------------------------------------------- |
380
+ | `<app-module>` | Module path whose **default export** is an Express app (sync, not async factory) |
381
+ | `--init <path>` | Init hook module (default export, async function) called once per cold start |
382
+ | `--out-dir <path>` | Output directory (default: `dist`) |
383
+ | `--out-name <name>` | Output filename without extension (default: `handler`) |
384
+ | `--format <cjs\|esm>` | Output format (default: `cjs`) |
385
+ | `--target <target>` | Compilation target (default: `node22`) |
386
+ | `--external <pkg>` | Mark package as external (repeatable; `express` is always external) |
387
+ | `--no-clean` | Don't clean the output directory before building |
388
+
389
+ #### start-serverless options
390
+
391
+ | Option | Description |
392
+ | ------------------------- | -------------------------------------------------------------------------------------------- |
393
+ | `<handler-module>` | JS/CJS module path exporting `handler` (named or default) — the output of `build-serverless` |
394
+ | `--port <number>` | Port or named pipe (default: `process.env.PORT` or `8080`) |
395
+ | `--host <hostname>` | Hostname to bind (default: `process.env.HOST` or `0.0.0.0`) |
396
+ | `--no-signals` | Disable `SIGINT` / `SIGTERM` handler registration |
397
+ | `--shutdown-timeout <ms>` | Max ms to wait for in-flight requests (default: `5000`) |
398
+ | `--require <module>` | Module(s) to preload before handler load (repeatable; comma-separated values supported) |
399
+ | `--env <path>` | Env file(s) to load before handler load (repeatable; existing env vars are not overridden) |
346
400
 
347
401
  #### global options
348
402
 
@@ -355,11 +409,12 @@ The `dev` command sets `exitAfterShutdown: true` so `SIGINT` / `SIGTERM` cleanly
355
409
  exit the process after the server drains. TypeScript app modules require a TS
356
410
  loader (see the Quick Start CLI section for a `tsx` invocation).
357
411
 
358
- The `build` command generates a temporary entry file that imports the app
359
- module and wraps it with `createServerlessHandler`, then produces a
360
- self-contained bundle. `express` is always external; all other dependencies
361
- (including `@web-ts-toolkit/express-runtime` and `serverless-http`) are
362
- bundled into the output unless marked external via `--external`.
412
+ The `build` command generates a temporary entry file that re-exports the app
413
+ module and optional `init` hook, then produces a local runtime bundle. The
414
+ `build-serverless` command instead wraps the app with `createServerlessHandler`
415
+ and bundles the serverless runtime. `express` is always external; all other
416
+ dependencies are bundled into the output unless marked external via
417
+ `--external`.
363
418
 
364
419
  ## License
365
420
 
package/cli.js CHANGED
@@ -208,7 +208,7 @@ function readValue(argv, index, name) {
208
208
  function printHelp() {
209
209
  console.log(`wtt-express-runtime
210
210
 
211
- Run an Express app locally, bundle it as a serverless handler, or run the bundle.
211
+ Run an Express app locally, bundle it for local or serverless runtimes, or run the bundle.
212
212
 
213
213
  Usage:
214
214
  wtt-express-runtime <command> <app-module> [options]
@@ -216,8 +216,10 @@ Usage:
216
216
 
217
217
  Commands:
218
218
  dev Run the Express app as a local dev server
219
- build Bundle the Express app as a serverless handler
220
- start Run a bundled serverless handler locally
219
+ build Bundle the Express app as a local app module
220
+ start Run a bundled local app module
221
+ build-serverless Bundle the Express app as a serverless handler
222
+ start-serverless Run a bundled serverless handler locally
221
223
 
222
224
  Dev options:
223
225
  --port <number> Port or named pipe (default: process.env.PORT or 8080)
@@ -233,13 +235,30 @@ Dev options:
233
235
  Build options:
234
236
  --init <path> Init hook module (default export, async function)
235
237
  --out-dir <path> Output directory (default: dist)
236
- --out-name <name> Output filename without extension (default: handler)
238
+ --out-name <name> Output filename without extension (default: app)
237
239
  --format <cjs|esm> Output format (default: cjs)
238
240
  --target <target> Compilation target (default: node22)
239
241
  --external <pkg> Mark package as external (repeatable; express always external)
240
242
  --no-clean Don't clean the output directory before building
241
243
 
242
244
  Start options:
245
+ --port <number> Port or named pipe (default: process.env.PORT or 8080)
246
+ --host <hostname> Hostname to bind (default: process.env.HOST or 0.0.0.0)
247
+ --no-signals Disable SIGINT/SIGTERM handler registration
248
+ --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000)
249
+ --require <module> Module(s) to preload before app load (repeatable)
250
+ --env <path> Env file(s) to load (repeatable; existing env vars are not overridden)
251
+
252
+ Build-serverless options:
253
+ --init <path> Init hook module (default export, async function)
254
+ --out-dir <path> Output directory (default: dist)
255
+ --out-name <name> Output filename without extension (default: handler)
256
+ --format <cjs|esm> Output format (default: cjs)
257
+ --target <target> Compilation target (default: node22)
258
+ --external <pkg> Mark package as external (repeatable; express always external)
259
+ --no-clean Don't clean the output directory before building
260
+
261
+ Start-serverless options:
243
262
  --port <number> Port or named pipe (default: process.env.PORT or 8080)
244
263
  --host <hostname> Hostname to bind (default: process.env.HOST or 0.0.0.0)
245
264
  --no-signals Disable SIGINT/SIGTERM handler registration
@@ -255,10 +274,12 @@ Examples:
255
274
  wtt-express-runtime dev ./dist/app.js
256
275
  wtt-express-runtime dev ./dist/app.js --port 3000 --host localhost
257
276
  wtt-express-runtime dev ./src/app.ts --env .env --require tsconfig-paths/register --watch ./src,./shared
258
- wtt-express-runtime build ./src/app.ts --out-dir netlify/functions
259
- wtt-express-runtime build ./src/app.ts --init ./src/init.ts --format esm
260
- wtt-express-runtime start ./netlify/functions/handler.js --port 9000 --env .env
261
- wtt-express-runtime build ./src/app.ts && wtt-express-runtime start ./dist/handler.js
277
+ wtt-express-runtime build ./src/app.ts --out-dir dist
278
+ wtt-express-runtime start ./dist/app.js --port 3000 --env .env
279
+ wtt-express-runtime build-serverless ./src/app.ts --out-dir netlify/functions
280
+ wtt-express-runtime build-serverless ./src/app.ts --init ./src/init.ts --format esm
281
+ wtt-express-runtime start-serverless ./netlify/functions/handler.js --port 9000 --env .env
282
+ wtt-express-runtime build-serverless ./src/app.ts && wtt-express-runtime start-serverless ./dist/handler.js
262
283
 
263
284
  Notes:
264
285
  - In dev mode, the CLI evaluates arbitrary code from <app-module> in the current process.
@@ -269,9 +290,11 @@ Notes:
269
290
  For advanced dotenv features (multiline, expansion), --require dotenv/config instead.
270
291
  - --watch forks a child process running the same CLI without --watch. On file change,
271
292
  the child is killed (SIGTERM) and respawned after the debounce delay.
272
- - In build mode, express is always external. Add more externals with --external.
273
- - In start mode, the bundled handler file must be a JS/CJS module whose "handler"
274
- export (or default export) is a function: (event, context) => Promise<result>.
293
+ - In build/build-serverless mode, express is always external. Add more externals with --external.
294
+ - In start mode, the bundled app file must default-export an Express app (or export it as "app").
295
+ If the bundle exports "init", it runs before the server starts listening.
296
+ - In start-serverless mode, the bundled handler file must be a JS/CJS module whose
297
+ "handler" export (or default export) is a function: (event, context) => Promise<result>.
275
298
  - Init logic for dev mode (DB connections, etc.): add at the top level of your app module.
276
299
  `);
277
300
  }
@@ -282,7 +305,7 @@ function isHelp(arg) {
282
305
  return arg === "-h" || arg === "--help";
283
306
  }
284
307
  function isSubcommand(arg) {
285
- return arg === "dev" || arg === "build" || arg === "start";
308
+ return arg === "dev" || arg === "build" || arg === "start" || arg === "build-serverless" || arg === "start-serverless";
286
309
  }
287
310
  var DEFAULT_WATCH_EXTENSIONS = ["ts", "js", "mjs", "cjs", "json"];
288
311
  var DEFAULT_WATCH_DELAY = 500;
@@ -423,13 +446,25 @@ function parseDevArgs(argv) {
423
446
  watchDelay: watchDelay ?? DEFAULT_WATCH_DELAY
424
447
  };
425
448
  }
426
- function parseStartArgs(argv) {
449
+ function parseStartLikeArgs(argv, subcommandName) {
427
450
  for (const arg of argv) {
428
451
  if (arg === "--watch" || arg.startsWith("--watch=") || arg === "--ext" || arg.startsWith("--ext=") || arg === "--delay" || arg.startsWith("--delay=")) {
429
- throw new Error(`--watch/--ext/--delay are not supported with the start subcommand`);
452
+ throw new Error(`--watch/--ext/--delay are not supported with the ${subcommandName} subcommand`);
430
453
  }
431
454
  }
432
- const result = parseDevArgs(argv);
455
+ return parseDevArgs(argv);
456
+ }
457
+ function parseStartArgs(argv) {
458
+ const result = parseStartLikeArgs(argv, "start");
459
+ return {
460
+ appPath: result.appPath,
461
+ options: result.options,
462
+ require: result.require,
463
+ env: result.env
464
+ };
465
+ }
466
+ function parseStartServerlessArgs(argv) {
467
+ const result = parseStartLikeArgs(argv, "start-serverless");
433
468
  return {
434
469
  handlerPath: result.appPath,
435
470
  options: result.options,
@@ -437,13 +472,13 @@ function parseStartArgs(argv) {
437
472
  env: result.env
438
473
  };
439
474
  }
440
- function parseBuildArgs(argv) {
475
+ function parseBuildArgs(argv, outNameDefault) {
441
476
  let appPath;
442
477
  const external = [];
443
478
  const result = {
444
479
  initPath: void 0,
445
480
  outDir: "dist",
446
- outName: "handler",
481
+ outName: outNameDefault,
447
482
  format: "cjs",
448
483
  target: "node22",
449
484
  external,
@@ -538,6 +573,12 @@ function parseBuildArgs(argv) {
538
573
  }
539
574
  return { appPath, ...result };
540
575
  }
576
+ function parseLocalBuildArgs(argv) {
577
+ return parseBuildArgs(argv, "app");
578
+ }
579
+ function parseBuildServerlessArgs(argv) {
580
+ return parseBuildArgs(argv, "handler");
581
+ }
541
582
  function parseArgs(argv) {
542
583
  if (argv.length === 0) {
543
584
  printHelp();
@@ -557,10 +598,16 @@ function parseArgs(argv) {
557
598
  if (first === "dev") {
558
599
  return { subcommand: "dev", dev: parseDevArgs(rest) };
559
600
  }
601
+ if (first === "build") {
602
+ return { subcommand: "build", build: parseLocalBuildArgs(rest) };
603
+ }
560
604
  if (first === "start") {
561
605
  return { subcommand: "start", start: parseStartArgs(rest) };
562
606
  }
563
- return { subcommand: "build", build: parseBuildArgs(rest) };
607
+ if (first === "build-serverless") {
608
+ return { subcommand: "build-serverless", buildServerless: parseBuildServerlessArgs(rest) };
609
+ }
610
+ return { subcommand: "start-serverless", startServerless: parseStartServerlessArgs(rest) };
564
611
  }
565
612
  return { subcommand: "dev", dev: parseDevArgs(argv) };
566
613
  }
@@ -716,7 +763,8 @@ function runWithWatch(args) {
716
763
  process.on("SIGTERM", shutdown);
717
764
  spawnChild();
718
765
  }
719
- var TEMP_ENTRY_FILENAME = ".express-runtime-build-entry.ts";
766
+ var TEMP_BUILD_ENTRY_FILENAME = ".express-runtime-build-entry.ts";
767
+ var TEMP_SERVERLESS_ENTRY_FILENAME = ".express-runtime-build-serverless-entry.ts";
720
768
  function generateServerlessEntry(appPath, initPath) {
721
769
  const absAppPath = (0, import_node_path.resolve)(process.cwd(), appPath);
722
770
  const absInitPath = initPath ? (0, import_node_path.resolve)(process.cwd(), initPath) : void 0;
@@ -734,16 +782,28 @@ function generateServerlessEntry(appPath, initPath) {
734
782
  lines.push(`export { handler };`);
735
783
  return lines.join("\n") + "\n";
736
784
  }
737
- async function buildServerless(args) {
785
+ function generateRuntimeEntry(appPath, initPath) {
786
+ const absAppPath = (0, import_node_path.resolve)(process.cwd(), appPath);
787
+ const absInitPath = initPath ? (0, import_node_path.resolve)(process.cwd(), initPath) : void 0;
788
+ const lines = [
789
+ "// Auto-generated by @web-ts-toolkit/express-runtime CLI \u2014 do not edit.",
790
+ `import app from ${JSON.stringify(absAppPath)};`,
791
+ "export default app;",
792
+ "export { app };"
793
+ ];
794
+ if (absInitPath) {
795
+ lines.push(`export { default as init } from ${JSON.stringify(absInitPath)};`);
796
+ }
797
+ return lines.join("\n") + "\n";
798
+ }
799
+ async function buildBundle(args, tempEntryFilename, entryContent) {
738
800
  const tsupModule = await import("tsup");
739
801
  const { build } = tsupModule;
740
- const entryContent = generateServerlessEntry(args.appPath, args.initPath);
741
- const tempEntryPath = (0, import_node_path.resolve)(process.cwd(), TEMP_ENTRY_FILENAME);
802
+ const tempEntryPath = (0, import_node_path.resolve)(process.cwd(), tempEntryFilename);
742
803
  (0, import_node_fs.writeFileSync)(tempEntryPath, entryContent, "utf8");
743
804
  try {
744
805
  await build({
745
806
  config: false,
746
- // Don't auto-load tsup.config.ts from cwd
747
807
  entry: { [args.outName]: tempEntryPath },
748
808
  format: [args.format],
749
809
  target: args.target,
@@ -758,6 +818,12 @@ async function buildServerless(args) {
758
818
  (0, import_node_fs.rmSync)(tempEntryPath, { force: true });
759
819
  }
760
820
  }
821
+ async function buildRuntime(args) {
822
+ await buildBundle(args, TEMP_BUILD_ENTRY_FILENAME, generateRuntimeEntry(args.appPath, args.initPath));
823
+ }
824
+ async function buildServerless(args) {
825
+ await buildBundle(args, TEMP_SERVERLESS_ENTRY_FILENAME, generateServerlessEntry(args.appPath, args.initPath));
826
+ }
761
827
  function collectBody(req) {
762
828
  return new Promise((resolve, reject) => {
763
829
  const chunks = [];
@@ -816,6 +882,25 @@ function createServerlessAdapterApp(handler) {
816
882
  }
817
883
  });
818
884
  }
885
+ async function loadBuiltApp(appPath) {
886
+ const fullPath = (0, import_node_path.resolve)(process.cwd(), appPath);
887
+ const moduleUrl = (0, import_node_url.pathToFileURL)(fullPath).href;
888
+ const mod = await import(moduleUrl);
889
+ const exported = extractExport(mod);
890
+ if (!exported) {
891
+ throw new Error(
892
+ `Module "${appPath}" must default-export an Express app or export it as "app". Exports: ${Object.keys(mod).join(", ")}`
893
+ );
894
+ }
895
+ const init = mod.init;
896
+ if (init !== void 0 && typeof init !== "function") {
897
+ throw new Error(`Module "${appPath}" must export "init" as a function when present.`);
898
+ }
899
+ return {
900
+ app: await resolveExport(exported, appPath),
901
+ init
902
+ };
903
+ }
819
904
  async function loadHandler(handlerPath) {
820
905
  const fullPath = (0, import_node_path.resolve)(process.cwd(), handlerPath);
821
906
  const moduleUrl = (0, import_node_url.pathToFileURL)(fullPath).href;
@@ -853,11 +938,21 @@ async function main() {
853
938
  loadEnvFiles(start.env);
854
939
  }
855
940
  await preloadModules(start.require);
856
- const handler = await loadHandler(start.handlerPath);
941
+ const { app, init } = await loadBuiltApp(start.appPath);
942
+ startLocalServer(app, { ...start.options, init, exitAfterShutdown: true });
943
+ } else if (parsedArgs.subcommand === "build") {
944
+ await buildRuntime(parsedArgs.build);
945
+ } else if (parsedArgs.subcommand === "start-serverless") {
946
+ const { startServerless } = parsedArgs;
947
+ if (startServerless.env.length > 0) {
948
+ loadEnvFiles(startServerless.env);
949
+ }
950
+ await preloadModules(startServerless.require);
951
+ const handler = await loadHandler(startServerless.handlerPath);
857
952
  const app = createServerlessAdapterApp(handler);
858
- startLocalServer(app, { ...start.options, exitAfterShutdown: true });
953
+ startLocalServer(app, { ...startServerless.options, exitAfterShutdown: true });
859
954
  } else {
860
- await buildServerless(parsedArgs.build);
955
+ await buildServerless(parsedArgs.buildServerless);
861
956
  }
862
957
  }
863
958
  main().catch((error) => {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@web-ts-toolkit/express-runtime",
3
3
  "description": "Express app factory plus serverless handler and local dev server helpers",
4
4
  "homepage": "https://web-ts-toolkit.pages.dev/docs/packages/express-runtime",
5
- "version": "0.24.0",
5
+ "version": "0.26.0",
6
6
  "sideEffects": false,
7
7
  "keywords": [
8
8
  "express",