@web-ts-toolkit/express-runtime 0.7.0 → 0.9.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 +44 -14
  2. package/cli.js +251 -15
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -80,12 +80,12 @@ startLocalServer(app, {
80
80
 
81
81
  ### CLI — dev (local server)
82
82
 
83
- The `web-ts-toolkit-express-runtime dev` command runs any module that
83
+ The `wtt-express-runtime dev` command runs any module that
84
84
  default-exports an Express app (or an async function returning one) as a local
85
85
  dev server:
86
86
 
87
87
  ```sh
88
- npx web-ts-toolkit-express-runtime dev ./dist/app.js --port 3000 --host localhost
88
+ npx wtt-express-runtime dev ./dist/app.js --port 3000 --host localhost
89
89
  ```
90
90
 
91
91
  ```ts
@@ -103,6 +103,29 @@ For TypeScript app modules, run the CLI through `tsx`:
103
103
  npx tsx ./node_modules/@web-ts-toolkit/express-runtime/dist/cli.js dev ./src/app.ts
104
104
  ```
105
105
 
106
+ #### CLI — dev with env, require, and watch
107
+
108
+ `--env` loads `.env` files into `process.env` (existing vars are not
109
+ overridden). `--require` preloads modules (e.g. `tsconfig-paths/register` for
110
+ TS path aliases) before the app module is loaded. `--watch` forks a child
111
+ process running the server and restarts it on file changes:
112
+
113
+ ```sh
114
+ npx tsx ./node_modules/@web-ts-toolkit/express-runtime/dist/cli.js dev ./src/app.ts \
115
+ --env .env \
116
+ --require tsconfig-paths/register \
117
+ --watch ./src,./shared \
118
+ --ext ts,json
119
+ ```
120
+
121
+ > `--env` parses `KEY=VALUE` lines (supports `export` prefix, quoted values,
122
+ > `#` comments). For advanced dotenv features (multiline, variable expansion),
123
+ > use `--require dotenv/config` instead.
124
+ >
125
+ > `--watch` uses Node 20+'s `fs.watch` with `{ recursive: true }` and forks the
126
+ > same CLI as a child process. On file change, the child receives `SIGTERM`,
127
+ > waits for it to exit, and respawns after the debounce delay (`--delay`).
128
+
106
129
  > The `dev` command evaluates arbitrary code from `<app-module>` in the current
107
130
  > process and inherits its privileges. Init logic (e.g. DB connections) should
108
131
  > be placed at the top level of your app module since `dev` does not expose an
@@ -115,13 +138,13 @@ with `createServerlessHandler`, then bundles it with `tsup` into a
115
138
  deployment-ready file:
116
139
 
117
140
  ```sh
118
- npx web-ts-toolkit-express-runtime build ./src/app.ts --out-dir netlify/functions
141
+ npx wtt-express-runtime build ./src/app.ts --out-dir netlify/functions
119
142
  ```
120
143
 
121
144
  With an optional init hook (DB connections, cache warmup, etc.):
122
145
 
123
146
  ```sh
124
- npx web-ts-toolkit-express-runtime build ./src/app.ts --init ./src/init.ts --out-dir netlify/functions
147
+ npx wtt-express-runtime build ./src/app.ts --init ./src/init.ts --out-dir netlify/functions
125
148
  ```
126
149
 
127
150
  ```ts
@@ -146,8 +169,8 @@ responses — letting you smoke-test the exact `build` output without a
146
169
  serverless platform:
147
170
 
148
171
  ```sh
149
- npx web-ts-toolkit-express-runtime build ./src/app.ts --out-dir dist
150
- npx web-ts-toolkit-express-runtime start ./dist/handler.js --port 9000
172
+ npx wtt-express-runtime build ./src/app.ts --out-dir dist
173
+ npx wtt-express-runtime start ./dist/handler.js --port 9000 --env .env
151
174
  ```
152
175
 
153
176
  The handler module must export a `handler` function (named or default) that
@@ -272,7 +295,7 @@ interface Logger {
272
295
 
273
296
  ## CLI
274
297
 
275
- ### `web-ts-toolkit-express-runtime <command> <app-module> [options]`
298
+ ### `wtt-express-runtime <command> <app-module> [options]`
276
299
 
277
300
  Omitting `<command>` defaults to `dev` for backward compatibility.
278
301
 
@@ -291,6 +314,11 @@ Omitting `<command>` defaults to `dev` for backward compatibility.
291
314
  | `--host <hostname>` | Hostname to bind (default: `process.env.HOST` or `0.0.0.0`) |
292
315
  | `--no-signals` | Disable `SIGINT` / `SIGTERM` handler registration |
293
316
  | `--shutdown-timeout <ms>` | Max ms to wait for in-flight requests (default: `5000`) |
317
+ | `--require <module>` | Module(s) to preload before app load (repeatable; comma-separated values supported) |
318
+ | `--env <path>` | Env file(s) to load before app load (repeatable; existing env vars are not overridden) |
319
+ | `--watch <paths>` | Comma-separated paths to watch for restart (repeatable; forks a child process) |
320
+ | `--ext <extensions>` | Comma-separated extensions to watch (default: `ts,js,mjs,cjs,json`) |
321
+ | `--delay <ms>` | Debounce ms before restarting on change (default: `500`) |
294
322
 
295
323
  #### build options
296
324
 
@@ -307,13 +335,15 @@ Omitting `<command>` defaults to `dev` for backward compatibility.
307
335
 
308
336
  #### start options
309
337
 
310
- | Option | Description |
311
- | ------------------------- | --------------------------------------------------------------------------------- |
312
- | `<handler-module>` | JS/CJS module path exporting `handler` (named or default) — the output of `build` |
313
- | `--port <number>` | Port or named pipe (default: `process.env.PORT` or `8080`) |
314
- | `--host <hostname>` | Hostname to bind (default: `process.env.HOST` or `0.0.0.0`) |
315
- | `--no-signals` | Disable `SIGINT` / `SIGTERM` handler registration |
316
- | `--shutdown-timeout <ms>` | Max ms to wait for in-flight requests (default: `5000`) |
338
+ | Option | Description |
339
+ | ------------------------- | ------------------------------------------------------------------------------------------ |
340
+ | `<handler-module>` | JS/CJS module path exporting `handler` (named or default) — the output of `build` |
341
+ | `--port <number>` | Port or named pipe (default: `process.env.PORT` or `8080`) |
342
+ | `--host <hostname>` | Hostname to bind (default: `process.env.HOST` or `0.0.0.0`) |
343
+ | `--no-signals` | Disable `SIGINT` / `SIGTERM` handler registration |
344
+ | `--shutdown-timeout <ms>` | Max ms to wait for in-flight requests (default: `5000`) |
345
+ | `--require <module>` | Module(s) to preload before handler load (repeatable; comma-separated values supported) |
346
+ | `--env <path>` | Env file(s) to load before handler load (repeatable; existing env vars are not overridden) |
317
347
 
318
348
  #### global options
319
349
 
package/cli.js CHANGED
@@ -27,6 +27,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var import_node_url = require("url");
28
28
  var import_node_path = require("path");
29
29
  var import_node_fs = require("fs");
30
+ var import_node_module = require("module");
31
+ var import_node_child_process = require("child_process");
30
32
 
31
33
  // src/index.ts
32
34
  var import_node_http = __toESM(require("http"));
@@ -194,6 +196,7 @@ function startLocalServer(app, options = {}) {
194
196
  }
195
197
 
196
198
  // src/cli-utils.ts
199
+ var import_meta = {};
197
200
  var CLI_VERSION = "0.0.0-PLACEHOLDER";
198
201
  function readValue(argv, index, name) {
199
202
  const value = argv[index + 1];
@@ -203,13 +206,13 @@ function readValue(argv, index, name) {
203
206
  return value;
204
207
  }
205
208
  function printHelp() {
206
- console.log(`web-ts-toolkit-express-runtime
209
+ console.log(`wtt-express-runtime
207
210
 
208
211
  Run an Express app locally, bundle it as a serverless handler, or run the bundle.
209
212
 
210
213
  Usage:
211
- web-ts-toolkit-express-runtime <command> <app-module> [options]
212
- web-ts-toolkit-express-runtime <app-module> [options] (alias for dev)
214
+ wtt-express-runtime <command> <app-module> [options]
215
+ wtt-express-runtime <app-module> [options] (alias for dev)
213
216
 
214
217
  Commands:
215
218
  dev Run the Express app as a local dev server
@@ -221,6 +224,11 @@ Dev options:
221
224
  --host <hostname> Hostname to bind (default: process.env.HOST or 0.0.0.0)
222
225
  --no-signals Disable SIGINT/SIGTERM handler registration
223
226
  --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000)
227
+ --require <module> Module(s) to preload before app load (repeatable)
228
+ --env <path> Env file(s) to load (repeatable; existing env vars are not overridden)
229
+ --watch <paths> Comma-separated paths to watch for restart (repeatable; dev only)
230
+ --ext <extensions> Comma-separated extensions to watch (default: ts,js,mjs,cjs,json)
231
+ --delay <ms> Debounce ms before restarting on change (default: 500)
224
232
 
225
233
  Build options:
226
234
  --init <path> Init hook module (default export, async function)
@@ -236,23 +244,31 @@ Start options:
236
244
  --host <hostname> Hostname to bind (default: process.env.HOST or 0.0.0.0)
237
245
  --no-signals Disable SIGINT/SIGTERM handler registration
238
246
  --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000)
247
+ --require <module> Module(s) to preload before handler load (repeatable)
248
+ --env <path> Env file(s) to load (repeatable; existing env vars are not overridden)
239
249
 
240
250
  Global options:
241
251
  -V, --version Show version
242
252
  -h, --help Show this help message
243
253
 
244
254
  Examples:
245
- web-ts-toolkit-express-runtime dev ./dist/app.js
246
- web-ts-toolkit-express-runtime dev ./dist/app.js --port 3000 --host localhost
247
- web-ts-toolkit-express-runtime build ./src/app.ts --out-dir netlify/functions
248
- web-ts-toolkit-express-runtime build ./src/app.ts --init ./src/init.ts --format esm
249
- web-ts-toolkit-express-runtime start ./netlify/functions/handler.js --port 9000
250
- web-ts-toolkit-express-runtime build ./src/app.ts && web-ts-toolkit-express-runtime start ./dist/handler.js
255
+ wtt-express-runtime dev ./dist/app.js
256
+ wtt-express-runtime dev ./dist/app.js --port 3000 --host localhost
257
+ 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
251
262
 
252
263
  Notes:
253
264
  - In dev mode, the CLI evaluates arbitrary code from <app-module> in the current process.
254
265
  - TypeScript app modules in dev mode require a TS loader. Run via tsx:
255
266
  npx tsx ./node_modules/@web-ts-toolkit/express-runtime/dist/cli.js dev ./src/app.ts
267
+ Or use --require with a TS-aware loader module.
268
+ - --env files are parsed as KEY=VALUE; existing process.env entries are never overridden.
269
+ For advanced dotenv features (multiline, expansion), --require dotenv/config instead.
270
+ - --watch forks a child process running the same CLI without --watch. On file change,
271
+ the child is killed (SIGTERM) and respawned after the debounce delay.
256
272
  - In build mode, tsup is required: pnpm add -D tsup
257
273
  - In start mode, the bundled handler file must be a JS/CJS module whose "handler"
258
274
  export (or default export) is a function: (event, context) => Promise<result>.
@@ -268,8 +284,23 @@ function isHelp(arg) {
268
284
  function isSubcommand(arg) {
269
285
  return arg === "dev" || arg === "build" || arg === "start";
270
286
  }
287
+ var DEFAULT_WATCH_EXTENSIONS = ["ts", "js", "mjs", "cjs", "json"];
288
+ var DEFAULT_WATCH_DELAY = 500;
289
+ function parseRepeatable(argv, index, arg, list) {
290
+ const value = readValue(argv, index, arg);
291
+ for (const part of value.split(",")) {
292
+ const trimmed = part.trim();
293
+ if (trimmed) list.push(trimmed);
294
+ }
295
+ return index + 1;
296
+ }
271
297
  function parseDevArgs(argv) {
272
298
  const options = {};
299
+ const requireModules = [];
300
+ const envFiles = [];
301
+ const watchPaths = [];
302
+ let watchExt;
303
+ let watchDelay;
273
304
  let appPath;
274
305
  for (let index = 0; index < argv.length; index += 1) {
275
306
  const arg = argv[index];
@@ -314,6 +345,61 @@ function parseDevArgs(argv) {
314
345
  options.shutdownTimeout = Number(arg.slice("--shutdown-timeout=".length));
315
346
  continue;
316
347
  }
348
+ if (arg === "--require") {
349
+ index = parseRepeatable(argv, index, arg, requireModules);
350
+ continue;
351
+ }
352
+ if (arg.startsWith("--require=")) {
353
+ for (const part of arg.slice("--require=".length).split(",")) {
354
+ const trimmed = part.trim();
355
+ if (trimmed) requireModules.push(trimmed);
356
+ }
357
+ continue;
358
+ }
359
+ if (arg === "--env") {
360
+ index = parseRepeatable(argv, index, arg, envFiles);
361
+ continue;
362
+ }
363
+ if (arg.startsWith("--env=")) {
364
+ for (const part of arg.slice("--env=".length).split(",")) {
365
+ const trimmed = part.trim();
366
+ if (trimmed) envFiles.push(trimmed);
367
+ }
368
+ continue;
369
+ }
370
+ if (arg === "--watch") {
371
+ index = parseRepeatable(argv, index, arg, watchPaths);
372
+ continue;
373
+ }
374
+ if (arg.startsWith("--watch=")) {
375
+ for (const part of arg.slice("--watch=".length).split(",")) {
376
+ const trimmed = part.trim();
377
+ if (trimmed) watchPaths.push(trimmed);
378
+ }
379
+ continue;
380
+ }
381
+ if (arg === "--ext") {
382
+ watchExt = [];
383
+ index = parseRepeatable(argv, index, arg, watchExt);
384
+ continue;
385
+ }
386
+ if (arg.startsWith("--ext=")) {
387
+ watchExt = [];
388
+ for (const part of arg.slice("--ext=".length).split(",")) {
389
+ const trimmed = part.trim();
390
+ if (trimmed) watchExt.push(trimmed);
391
+ }
392
+ continue;
393
+ }
394
+ if (arg === "--delay") {
395
+ watchDelay = Number(readValue(argv, index, arg));
396
+ index += 1;
397
+ continue;
398
+ }
399
+ if (arg.startsWith("--delay=")) {
400
+ watchDelay = Number(arg.slice("--delay=".length));
401
+ continue;
402
+ }
317
403
  if (!arg.startsWith("--")) {
318
404
  if (appPath) {
319
405
  throw new Error(`Unexpected positional argument: ${arg}. App module already set to ${appPath}`);
@@ -327,11 +413,29 @@ function parseDevArgs(argv) {
327
413
  printHelp();
328
414
  throw new Error("Missing required argument: <app-module>");
329
415
  }
330
- return { appPath, options };
416
+ return {
417
+ appPath,
418
+ options,
419
+ require: requireModules,
420
+ env: envFiles,
421
+ watch: watchPaths,
422
+ watchExt: watchExt ?? DEFAULT_WATCH_EXTENSIONS,
423
+ watchDelay: watchDelay ?? DEFAULT_WATCH_DELAY
424
+ };
331
425
  }
332
426
  function parseStartArgs(argv) {
427
+ for (const arg of argv) {
428
+ 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`);
430
+ }
431
+ }
333
432
  const result = parseDevArgs(argv);
334
- return { handlerPath: result.appPath, options: result.options };
433
+ return {
434
+ handlerPath: result.appPath,
435
+ options: result.options,
436
+ require: result.require,
437
+ env: result.env
438
+ };
335
439
  }
336
440
  function parseBuildArgs(argv) {
337
441
  let appPath;
@@ -494,6 +598,124 @@ async function loadApp(appPath) {
494
598
  }
495
599
  return resolveExport(exported, appPath);
496
600
  }
601
+ function parseEnvFile(content) {
602
+ const result = {};
603
+ for (const line of content.split("\n")) {
604
+ let trimmed = line.trim();
605
+ if (!trimmed || trimmed.startsWith("#")) continue;
606
+ if (trimmed.startsWith("export ")) trimmed = trimmed.slice("export ".length).trim();
607
+ const eqIndex = trimmed.indexOf("=");
608
+ if (eqIndex === -1) continue;
609
+ const key = trimmed.slice(0, eqIndex).trim();
610
+ let value = trimmed.slice(eqIndex + 1).trim();
611
+ if (value.length >= 2) {
612
+ const first = value[0];
613
+ const last = value[value.length - 1];
614
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
615
+ value = value.slice(1, -1);
616
+ }
617
+ }
618
+ result[key] = value;
619
+ }
620
+ return result;
621
+ }
622
+ function loadEnvFiles(paths) {
623
+ for (const p of paths) {
624
+ const absPath = (0, import_node_path.resolve)(process.cwd(), p);
625
+ if (!(0, import_node_fs.existsSync)(absPath)) {
626
+ throw new Error(`Env file not found: ${p}`);
627
+ }
628
+ const content = (0, import_node_fs.readFileSync)(absPath, "utf8");
629
+ const parsed = parseEnvFile(content);
630
+ for (const [key, value] of Object.entries(parsed)) {
631
+ if (process.env[key] === void 0) {
632
+ process.env[key] = value;
633
+ }
634
+ }
635
+ }
636
+ }
637
+ var moduleRequire = (0, import_node_module.createRequire)(
638
+ typeof import_meta !== "undefined" && import_meta.url || (0, import_node_path.resolve)(process.cwd(), "x").replace(/x$/, "")
639
+ );
640
+ async function preloadModules(modules) {
641
+ for (const mod of modules) {
642
+ moduleRequire(mod);
643
+ }
644
+ }
645
+ function buildChildArgs(args) {
646
+ const result = ["dev", args.appPath];
647
+ if (args.options.port !== void 0) result.push("--port", String(args.options.port));
648
+ if (args.options.host !== void 0) result.push("--host", args.options.host);
649
+ if (args.options.signals === false) result.push("--no-signals");
650
+ if (args.options.shutdownTimeout !== void 0)
651
+ result.push("--shutdown-timeout", String(args.options.shutdownTimeout));
652
+ for (const r of args.require) result.push("--require", r);
653
+ for (const e of args.env) result.push("--env", e);
654
+ return result;
655
+ }
656
+ function runWithWatch(args) {
657
+ const cliPath = process.argv[1];
658
+ const childArgv = buildChildArgs(args);
659
+ let child = null;
660
+ let restartTimer = null;
661
+ let isShuttingDown = false;
662
+ const restartDelay = args.watchDelay;
663
+ const spawnChild = () => {
664
+ child = (0, import_node_child_process.fork)(cliPath, childArgv, { stdio: "inherit" });
665
+ child.on("exit", (code) => {
666
+ child = null;
667
+ if (!isShuttingDown && code !== null && code !== 0) {
668
+ }
669
+ });
670
+ };
671
+ const killChild = () => {
672
+ return new Promise((resolve) => {
673
+ if (!child || !child.pid) {
674
+ resolve();
675
+ return;
676
+ }
677
+ child.once("exit", () => resolve());
678
+ child.kill("SIGTERM");
679
+ });
680
+ };
681
+ const restart = async () => {
682
+ await killChild();
683
+ spawnChild();
684
+ };
685
+ const debouncedRestart = () => {
686
+ if (restartTimer) clearTimeout(restartTimer);
687
+ restartTimer = setTimeout(() => {
688
+ restartTimer = null;
689
+ void restart();
690
+ }, restartDelay);
691
+ };
692
+ for (const watchPath of args.watch) {
693
+ const absPath = (0, import_node_path.resolve)(process.cwd(), watchPath);
694
+ if (!(0, import_node_fs.existsSync)(absPath)) {
695
+ throw new Error(`Watch path not found: ${watchPath}`);
696
+ }
697
+ (0, import_node_fs.watch)(absPath, { recursive: true }, (_eventType, filename) => {
698
+ if (!filename) return;
699
+ const ext = (0, import_node_path.extname)(filename).slice(1).toLowerCase();
700
+ if (args.watchExt.includes(ext)) {
701
+ debouncedRestart();
702
+ }
703
+ });
704
+ }
705
+ const shutdown = () => {
706
+ isShuttingDown = true;
707
+ if (restartTimer) clearTimeout(restartTimer);
708
+ if (child && child.pid) {
709
+ child.once("exit", () => process.exit(0));
710
+ child.kill("SIGTERM");
711
+ } else {
712
+ process.exit(0);
713
+ }
714
+ };
715
+ process.on("SIGINT", shutdown);
716
+ process.on("SIGTERM", shutdown);
717
+ spawnChild();
718
+ }
497
719
  var TEMP_ENTRY_FILENAME = ".express-runtime-build-entry.ts";
498
720
  function generateServerlessEntry(appPath, initPath) {
499
721
  const absAppPath = (0, import_node_path.resolve)(process.cwd(), appPath);
@@ -619,12 +841,26 @@ async function main() {
619
841
  return;
620
842
  }
621
843
  if (parsedArgs.subcommand === "dev") {
622
- const app = await loadApp(parsedArgs.dev.appPath);
623
- startLocalServer(app, { ...parsedArgs.dev.options, exitAfterShutdown: true });
844
+ const { dev } = parsedArgs;
845
+ if (dev.watch.length > 0) {
846
+ runWithWatch(dev);
847
+ return;
848
+ }
849
+ if (dev.env.length > 0) {
850
+ loadEnvFiles(dev.env);
851
+ }
852
+ await preloadModules(dev.require);
853
+ const app = await loadApp(dev.appPath);
854
+ startLocalServer(app, { ...dev.options, exitAfterShutdown: true });
624
855
  } else if (parsedArgs.subcommand === "start") {
625
- const handler = await loadHandler(parsedArgs.start.handlerPath);
856
+ const { start } = parsedArgs;
857
+ if (start.env.length > 0) {
858
+ loadEnvFiles(start.env);
859
+ }
860
+ await preloadModules(start.require);
861
+ const handler = await loadHandler(start.handlerPath);
626
862
  const app = createServerlessAdapterApp(handler);
627
- startLocalServer(app, { ...parsedArgs.start.options, exitAfterShutdown: true });
863
+ startLocalServer(app, { ...start.options, exitAfterShutdown: true });
628
864
  } else {
629
865
  await buildServerless(parsedArgs.build);
630
866
  }
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.7.0",
5
+ "version": "0.9.0",
6
6
  "sideEffects": false,
7
7
  "keywords": [
8
8
  "express",
@@ -16,7 +16,7 @@
16
16
  "module": "./index.mjs",
17
17
  "types": "./index.d.ts",
18
18
  "bin": {
19
- "web-ts-toolkit-express-runtime": "./cli.js"
19
+ "wtt-express-runtime": "./cli.js"
20
20
  },
21
21
  "exports": {
22
22
  ".": {