@web-ts-toolkit/express-runtime 0.40.1 → 0.41.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.
package/cli-api.js CHANGED
@@ -30,6 +30,45 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // src/numeric-validation.ts
34
+ function validateFiniteInteger(value, options) {
35
+ const min = options.min ?? Number.MIN_SAFE_INTEGER;
36
+ const max = options.max ?? MAX_INTEGER_OPTION_VALUE;
37
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < min || value > max) {
38
+ throw new Error(`Invalid ${options.name}: ${String(value)}. Must be a finite integer in ${min}..${max}.`);
39
+ }
40
+ return value;
41
+ }
42
+ function parsePortValue(value, name) {
43
+ if (typeof value === "number") {
44
+ return validateFiniteInteger(value, { name, min: 0, max: 65535 });
45
+ }
46
+ if (value.trim() === "") {
47
+ throw new Error(
48
+ `Invalid ${name}: ${JSON.stringify(value)}. Must be a port number in 0..65535 or a named pipe path.`
49
+ );
50
+ }
51
+ if (value.trim() !== value) {
52
+ throw new Error(
53
+ `Invalid ${name}: ${JSON.stringify(value)}. Numeric ports must not contain surrounding whitespace.`
54
+ );
55
+ }
56
+ if (/^(0|[1-9]\d*)$/.test(value)) {
57
+ return validateFiniteInteger(Number(value), { name, min: 0, max: 65535 });
58
+ }
59
+ if (/^[+-]?(?:\d+|\d*\.\d+)(?:e[+-]?\d+)?$/i.test(value) || /^[+-]?(?:infinity|nan)$/i.test(value)) {
60
+ throw new Error(`Invalid ${name}: ${value}. Numeric ports must be canonical decimal integers in 0..65535.`);
61
+ }
62
+ return value;
63
+ }
64
+ var MAX_INTEGER_OPTION_VALUE;
65
+ var init_numeric_validation = __esm({
66
+ "src/numeric-validation.ts"() {
67
+ "use strict";
68
+ MAX_INTEGER_OPTION_VALUE = Number.MAX_SAFE_INTEGER;
69
+ }
70
+ });
71
+
33
72
  // src/index.ts
34
73
  function applySettings(app, options) {
35
74
  if (options.disablePoweredBy !== false) {
@@ -54,8 +93,15 @@ function applyRouters(app, options) {
54
93
  app.use(path, mount.handler);
55
94
  }
56
95
  }
96
+ function createDefaultErrorHandler(logger) {
97
+ return (error, _req, _res, next) => {
98
+ logger.error("Unhandled Express error:", error);
99
+ next(error);
100
+ };
101
+ }
57
102
  function createExpressApp(options = {}) {
58
103
  const app = (0, import_express.default)();
104
+ const logger = options.logger ?? defaultLogger;
59
105
  applySettings(app, options);
60
106
  applyMiddlewareList(app, options.preMiddleware);
61
107
  if (options.json !== false) {
@@ -72,10 +118,12 @@ function createExpressApp(options = {}) {
72
118
  }
73
119
  if (options.errorHandler) {
74
120
  app.use(options.errorHandler);
121
+ } else {
122
+ app.use(createDefaultErrorHandler(logger));
75
123
  }
76
124
  return app;
77
125
  }
78
- function normalizePort(val) {
126
+ function normalizePort(val, name = "port") {
79
127
  if (val === void 0 || val === "") {
80
128
  const envPort = process.env.PORT;
81
129
  if (envPort === void 0 || envPort === "") {
@@ -83,17 +131,7 @@ function normalizePort(val) {
83
131
  }
84
132
  val = envPort;
85
133
  }
86
- if (typeof val === "string") {
87
- const parsed = Number(val);
88
- if (Number.isNaN(parsed)) {
89
- return val;
90
- }
91
- val = parsed;
92
- }
93
- if (!Number.isFinite(val) || val < 0 || val > 65535) {
94
- throw new Error(`Invalid port: ${String(val)}`);
95
- }
96
- return val;
134
+ return parsePortValue(val, name);
97
135
  }
98
136
  function defaultOnError(error, port, logger) {
99
137
  if (error.syscall !== "listen") {
@@ -114,54 +152,217 @@ function startLocalServer(app, options = {}) {
114
152
  const logger = options.logger ?? defaultLogger;
115
153
  const port = normalizePort(options.port);
116
154
  const host = options.host ?? process.env.HOST ?? "0.0.0.0";
117
- const shutdownTimeout = options.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT;
155
+ const shutdownTimeout = validateFiniteInteger(options.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT, {
156
+ name: "shutdownTimeout",
157
+ min: 0,
158
+ max: MAX_INTEGER_OPTION_VALUE
159
+ });
118
160
  const server = import_node_http.default.createServer(app);
119
161
  app.set("port", port);
120
- const onError = (error) => {
162
+ let state = "initializing";
163
+ let shutdownPromise = null;
164
+ let readySettled = false;
165
+ let readyResolve;
166
+ let readyReject;
167
+ const ready = new Promise((resolve, reject) => {
168
+ readyResolve = () => {
169
+ if (!readySettled) {
170
+ readySettled = true;
171
+ resolve();
172
+ }
173
+ };
174
+ readyReject = (err) => {
175
+ if (!readySettled) {
176
+ readySettled = true;
177
+ reject(err);
178
+ }
179
+ };
180
+ });
181
+ ready.catch(() => {
182
+ });
183
+ const ownedSignalHandlers = /* @__PURE__ */ new Map();
184
+ const cleanupSignalHandlers = () => {
185
+ for (const [sig, handler] of ownedSignalHandlers.entries()) {
186
+ process.removeListener(sig, handler);
187
+ }
188
+ ownedSignalHandlers.clear();
189
+ };
190
+ const handleListenError = (error) => {
191
+ if (state === "stopping" || state === "stopped" || state === "failed") {
192
+ logger.error("Server error after terminal state:", error);
193
+ return;
194
+ }
195
+ if (state === "initializing") {
196
+ state = "failed";
197
+ cleanupSignalHandlers();
198
+ readyReject(error);
199
+ if (options.onError) {
200
+ try {
201
+ options.onError(error);
202
+ } catch (e) {
203
+ logger.error(e);
204
+ }
205
+ } else {
206
+ try {
207
+ defaultOnError(error, port, logger);
208
+ } catch (e) {
209
+ logger.error(e);
210
+ }
211
+ }
212
+ try {
213
+ server.close();
214
+ } catch (_err) {
215
+ void _err;
216
+ }
217
+ return;
218
+ }
121
219
  if (options.onError) {
122
- options.onError(error);
220
+ try {
221
+ options.onError(error);
222
+ } catch (e) {
223
+ logger.error(e);
224
+ }
123
225
  } else {
124
- defaultOnError(error, port, logger);
226
+ try {
227
+ defaultOnError(error, port, logger);
228
+ } catch (e) {
229
+ logger.error(e);
230
+ }
125
231
  }
126
232
  };
127
233
  const onListening = () => {
234
+ if (state === "stopping" || state === "stopped" || state === "failed") {
235
+ try {
236
+ server.close();
237
+ } catch (_err) {
238
+ void _err;
239
+ }
240
+ return;
241
+ }
242
+ state = "listening";
128
243
  const addr = server.address();
129
244
  const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr?.port}`;
130
- logger.log(`Server running at http://${host}:${port}/ (${bind})`);
131
- options.onListening?.();
245
+ const actualPort = typeof addr === "object" && addr !== null ? addr.port : port;
246
+ if (typeof addr === "object" && addr !== null) {
247
+ logger.log(`Server running at http://${host}:${actualPort}/ (${bind})`);
248
+ } else if (typeof addr === "string") {
249
+ logger.log(`Server running at pipe ${addr} (${bind})`);
250
+ } else {
251
+ logger.log(`Server running at http://${host}:${port}/ (${bind})`);
252
+ }
253
+ try {
254
+ options.onListening?.();
255
+ } catch (e) {
256
+ logger.error("onListening hook failed:", e);
257
+ }
258
+ readyResolve();
132
259
  };
133
- server.on("error", onError);
260
+ const handleClose = () => {
261
+ if (state === "stopping") {
262
+ return;
263
+ }
264
+ if (state === "listening") {
265
+ state = "stopped";
266
+ cleanupSignalHandlers();
267
+ return;
268
+ }
269
+ if (state === "initializing" && !readySettled) {
270
+ state = "stopped";
271
+ cleanupSignalHandlers();
272
+ readyReject(new Error("Server closed before listening"));
273
+ return;
274
+ }
275
+ if (state === "initializing") {
276
+ state = "stopped";
277
+ cleanupSignalHandlers();
278
+ }
279
+ };
280
+ server.on("error", handleListenError);
134
281
  server.on("listening", onListening);
135
- const shutdown = async () => {
282
+ server.on("close", handleClose);
283
+ const doShutdown = async () => {
284
+ if (state === "stopping" || state === "stopped") {
285
+ return;
286
+ }
287
+ if (state === "failed") {
288
+ state = "stopped";
289
+ cleanupSignalHandlers();
290
+ return;
291
+ }
292
+ state = "stopping";
293
+ if (!readySettled) {
294
+ readyReject(new Error("Server shutdown before listening"));
295
+ }
296
+ cleanupSignalHandlers();
136
297
  logger.log("Shutting down...");
298
+ await new Promise((resolve) => {
299
+ if (!server.listening) {
300
+ resolve();
301
+ return;
302
+ }
303
+ let settled = false;
304
+ const done = () => {
305
+ if (settled) return;
306
+ settled = true;
307
+ clearTimeout(timer);
308
+ resolve();
309
+ };
310
+ const timer = setTimeout(() => {
311
+ try {
312
+ server.closeAllConnections?.();
313
+ } catch (_err) {
314
+ void _err;
315
+ }
316
+ done();
317
+ }, shutdownTimeout);
318
+ timer.unref?.();
319
+ try {
320
+ server.close((err) => {
321
+ if (err) {
322
+ logger.error("Server close error:", err);
323
+ }
324
+ done();
325
+ });
326
+ } catch (err) {
327
+ logger.error("Server close error:", err);
328
+ done();
329
+ }
330
+ });
331
+ let shutdownError;
137
332
  try {
138
333
  if (options.onShutdown) {
139
334
  await options.onShutdown();
140
335
  }
141
336
  } catch (err) {
142
337
  logger.error("onShutdown hook failed:", err);
338
+ shutdownError = err;
143
339
  }
144
- await new Promise((resolve) => {
145
- const timer = setTimeout(() => {
146
- server.closeAllConnections?.();
147
- resolve();
148
- }, shutdownTimeout);
149
- server.close((err) => {
150
- clearTimeout(timer);
151
- if (err) {
152
- logger.error("Server close error:", err);
153
- }
154
- resolve();
155
- });
156
- });
340
+ if (shutdownError) {
341
+ state = "failed";
342
+ if (options.exitAfterShutdown) {
343
+ process.exit(1);
344
+ }
345
+ throw shutdownError;
346
+ }
347
+ state = "stopped";
157
348
  if (options.exitAfterShutdown) {
158
349
  process.exit(0);
159
350
  }
160
351
  };
352
+ const shutdown = () => {
353
+ if (shutdownPromise) return shutdownPromise;
354
+ shutdownPromise = doShutdown();
355
+ return shutdownPromise;
356
+ };
161
357
  if (options.signals !== false) {
162
358
  const list = options.signals === void 0 || options.signals === true ? DEFAULT_SIGNALS : options.signals;
163
359
  for (const sig of list) {
164
- process.once(sig, () => void shutdown());
360
+ const handler = () => {
361
+ void shutdown().catch(() => {
362
+ });
363
+ };
364
+ ownedSignalHandlers.set(sig, handler);
365
+ process.once(sig, handler);
165
366
  }
166
367
  }
167
368
  const start = async () => {
@@ -169,19 +370,44 @@ function startLocalServer(app, options = {}) {
169
370
  if (options.init) {
170
371
  await options.init();
171
372
  }
373
+ if (state === "stopping" || state === "stopped" || state === "failed") {
374
+ return;
375
+ }
172
376
  if (typeof port === "number") {
173
377
  server.listen(port, host);
174
378
  } else {
175
379
  server.listen(port);
176
380
  }
177
381
  } catch (err) {
178
- server.emit("error", err);
382
+ if (state === "stopping" || state === "stopped") {
383
+ logger.error("Init failed after shutdown started:", err);
384
+ return;
385
+ }
386
+ state = "failed";
387
+ cleanupSignalHandlers();
388
+ const error = err;
389
+ readyReject(error);
390
+ if (options.onError) {
391
+ try {
392
+ options.onError(error);
393
+ } catch (e) {
394
+ logger.error(e);
395
+ }
396
+ } else {
397
+ logger.error("Init failed:", error);
398
+ }
399
+ try {
400
+ server.close();
401
+ } catch (_err) {
402
+ void _err;
403
+ }
179
404
  }
180
405
  };
181
406
  void start();
182
407
  return {
183
408
  server,
184
- shutdown
409
+ shutdown,
410
+ ready
185
411
  };
186
412
  }
187
413
  var import_node_http, import_express, import_serverless_http, defaultLogger, DEFAULT_SIGNALS, DEFAULT_SHUTDOWN_TIMEOUT;
@@ -191,6 +417,7 @@ var init_index = __esm({
191
417
  import_node_http = __toESM(require("http"));
192
418
  import_express = __toESM(require("express"));
193
419
  import_serverless_http = __toESM(require("serverless-http"));
420
+ init_numeric_validation();
194
421
  defaultLogger = {
195
422
  log: (...args) => console.log(...args),
196
423
  error: (...args) => console.error(...args),
@@ -202,13 +429,104 @@ var init_index = __esm({
202
429
  });
203
430
 
204
431
  // src/cli-utils.ts
432
+ var cli_utils_exports = {};
433
+ __export(cli_utils_exports, {
434
+ CLI_VERSION: () => CLI_VERSION,
435
+ DEFAULT_ADAPTER_MAX_BODY_BYTES: () => DEFAULT_ADAPTER_MAX_BODY_BYTES,
436
+ DEFAULT_WATCH_KILL_TIMEOUT_MS: () => DEFAULT_WATCH_KILL_TIMEOUT_MS,
437
+ TEMP_BUILD_ENTRY_FILENAME: () => TEMP_BUILD_ENTRY_FILENAME,
438
+ TEMP_SERVERLESS_ENTRY_FILENAME: () => TEMP_SERVERLESS_ENTRY_FILENAME,
439
+ applyServerlessResult: () => applyServerlessResult,
440
+ buildBundleFromEntryContent: () => buildBundleFromEntryContent,
441
+ buildChildArgs: () => buildChildArgs,
442
+ buildRuntime: () => buildRuntime,
443
+ buildServerless: () => buildServerless,
444
+ collectBody: () => collectBody,
445
+ createServerlessAdapterApp: () => createServerlessAdapterApp,
446
+ createWatchSupervisor: () => createWatchSupervisor,
447
+ extractExport: () => extractExport,
448
+ generateRuntimeEntry: () => generateRuntimeEntry,
449
+ generateServerlessEntry: () => generateServerlessEntry,
450
+ isExpressApp: () => isExpressApp,
451
+ loadApp: () => loadApp,
452
+ loadBuiltApp: () => loadBuiltApp,
453
+ loadEnvFiles: () => loadEnvFiles,
454
+ loadHandler: () => loadHandler,
455
+ parseArgs: () => parseArgs,
456
+ parseEnvFile: () => parseEnvFile,
457
+ preloadModules: () => preloadModules,
458
+ printHelp: () => printHelp,
459
+ readValue: () => readValue,
460
+ resolveCliVersion: () => resolveCliVersion,
461
+ resolveExport: () => resolveExport,
462
+ runWithWatch: () => runWithWatch,
463
+ toServerlessEvent: () => toServerlessEvent,
464
+ validateMaxBodyBytes: () => validateMaxBodyBytes,
465
+ validateOutDirForClean: () => validateOutDirForClean
466
+ });
467
+ function readPackageVersion(candidatePath) {
468
+ try {
469
+ const manifest = JSON.parse((0, import_node_fs.readFileSync)(candidatePath, "utf8"));
470
+ return typeof manifest.version === "string" && manifest.version.length > 0 ? manifest.version : void 0;
471
+ } catch (_error) {
472
+ void _error;
473
+ return void 0;
474
+ }
475
+ }
476
+ function resolveCliVersion(executablePath = process.argv[1]) {
477
+ let resolvedExecutable;
478
+ try {
479
+ resolvedExecutable = executablePath ? (0, import_node_fs.realpathSync)(executablePath) : void 0;
480
+ } catch (_error) {
481
+ void _error;
482
+ }
483
+ const executableDir = resolvedExecutable ? (0, import_node_path.dirname)(resolvedExecutable) : void 0;
484
+ const candidates = executableDir ? [(0, import_node_path.join)(executableDir, "package.json"), (0, import_node_path.join)(executableDir, "..", "package.json")] : [];
485
+ for (const candidate of candidates) {
486
+ const version = readPackageVersion(candidate);
487
+ if (version) return version;
488
+ }
489
+ return "0.0.0-dev";
490
+ }
205
491
  function readValue(argv, index, name) {
206
492
  const value = argv[index + 1];
207
- if (value === void 0 || value.startsWith("--")) {
493
+ if (value === void 0 || value === "" || value.startsWith("--")) {
494
+ throw new Error(`Missing value for argument: ${name}`);
495
+ }
496
+ return value;
497
+ }
498
+ function readInlineValue(arg, prefix, name) {
499
+ const value = arg.slice(prefix.length);
500
+ if (value === "") {
208
501
  throw new Error(`Missing value for argument: ${name}`);
209
502
  }
210
503
  return value;
211
504
  }
505
+ function parseIntegerFlag(raw, name, min = 0, max = MAX_INTEGER_OPTION_VALUE) {
506
+ if (!/^(0|[1-9]\d*)$/.test(raw)) {
507
+ throw new Error(`Invalid ${name}: ${raw}. Must be a finite integer in ${min}..${max}.`);
508
+ }
509
+ return validateFiniteInteger(Number(raw), { name, min, max });
510
+ }
511
+ function parsePortFlag(raw, name = "--port") {
512
+ try {
513
+ return parsePortValue(raw, name);
514
+ } catch (error) {
515
+ if (error instanceof Error && error.message.startsWith(`Invalid ${name}:`)) {
516
+ throw error;
517
+ }
518
+ throw new Error(`Invalid ${name}: ${raw}. Must be a port number in 0..65535 or a named pipe path.`, {
519
+ cause: error
520
+ });
521
+ }
522
+ }
523
+ function parseCsvFlagValue(raw, name) {
524
+ const values = raw.split(",").map((part) => part.trim()).filter(Boolean);
525
+ if (values.length === 0) {
526
+ throw new Error(`Missing value for argument: ${name}`);
527
+ }
528
+ return values;
529
+ }
212
530
  function printHelp() {
213
531
  console.log(`wtt-express-runtime
214
532
 
@@ -270,6 +588,7 @@ Start-serverless options:
270
588
  --host <hostname> Hostname to bind (default: process.env.HOST or 0.0.0.0)
271
589
  --no-signals Disable SIGINT/SIGTERM handler registration
272
590
  --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000)
591
+ --max-body-bytes <bytes> Max request body bytes for adapter (default: 1048576; 0 disallows bodies)
273
592
  --require <module> Module(s) to preload before handler load (repeatable)
274
593
  --env <path> Env file(s) to load (repeatable; existing env vars are not overridden)
275
594
 
@@ -291,17 +610,22 @@ Examples:
291
610
  Notes:
292
611
  - In dev mode, the CLI evaluates arbitrary code from <app-module> in the current process.
293
612
  - TypeScript app modules in dev mode require a TS loader. Run via tsx:
294
- npx tsx ./node_modules/@web-ts-toolkit/express-runtime/dist/cli.js dev ./src/app.ts
613
+ npx tsx ./node_modules/@web-ts-toolkit/express-runtime/cli.js dev ./src/app.ts
295
614
  Or use --require with a TS-aware loader module.
296
615
  - --env files are parsed as KEY=VALUE; existing process.env entries are never overridden.
297
616
  For advanced dotenv features (multiline, expansion), --require dotenv/config instead.
298
- - --watch forks a child process running the same CLI without --watch. On file change,
299
- the child is killed (SIGTERM) and respawned after the debounce delay.
617
+ - --watch forks one child process running the same CLI without --watch. File changes
618
+ are serialized into one restart at a time: SIGTERM, SIGKILL after 5000 ms if needed,
619
+ then respawn after the debounce delay. Shutdown closes owned watchers and signal handlers.
300
620
  - In build/build-serverless mode, express is always external. Add more externals with --external.
301
621
  - In start mode, the bundled app file must default-export an Express app (or export it as "app").
302
622
  If the bundle exports "init", it runs before the server starts listening.
303
623
  - In start-serverless mode, the bundled handler file must be a JS/CJS module whose
304
624
  "handler" export (or default export) is a function: (event, context) => Promise<result>.
625
+ - The start-serverless adapter buffers at most --max-body-bytes per request (default 1 MiB, 0 = empty bodies only);
626
+ larger declared Content-Length or chunked bodies receive 413 without invoking the handler.
627
+ - Use -- before a positional path that starts with a dash, e.g. dev -- --app.js.
628
+ - Numeric flag values are validated before env/preload/app loading, watching, or binding.
305
629
  - Init logic for dev mode (DB connections, etc.): add at the top level of your app module.
306
630
  `);
307
631
  }
@@ -315,13 +639,25 @@ function isSubcommand(arg) {
315
639
  return arg === "dev" || arg === "build" || arg === "start" || arg === "build-serverless" || arg === "start-serverless";
316
640
  }
317
641
  function parseRepeatable(argv, index, arg, list) {
318
- const value = readValue(argv, index, arg);
319
- for (const part of value.split(",")) {
320
- const trimmed = part.trim();
321
- if (trimmed) list.push(trimmed);
322
- }
642
+ list.push(...parseCsvFlagValue(readValue(argv, index, arg), arg));
323
643
  return index + 1;
324
644
  }
645
+ function addPositional(arg, current, label) {
646
+ if (current) {
647
+ throw new Error(`Unexpected positional argument: ${arg}. ${label} already set to ${current}`);
648
+ }
649
+ return arg;
650
+ }
651
+ function setTsconfigPath(current, next) {
652
+ if (current !== void 0 && current !== next) {
653
+ throw new Error(`Conflicting --tsconfig values: ${current} and ${next}`);
654
+ }
655
+ return next;
656
+ }
657
+ function optionArgs(argv) {
658
+ const terminator = argv.indexOf("--");
659
+ return terminator === -1 ? argv : argv.slice(0, terminator);
660
+ }
325
661
  function parseDevArgs(argv) {
326
662
  const options = {};
327
663
  const requireModules = [];
@@ -334,22 +670,21 @@ function parseDevArgs(argv) {
334
670
  for (let index = 0; index < argv.length; index += 1) {
335
671
  const arg = argv[index];
336
672
  if (arg === "--") {
337
- continue;
673
+ for (const positional of argv.slice(index + 1)) {
674
+ appPath = addPositional(positional, appPath, "App module");
675
+ }
676
+ break;
338
677
  }
339
678
  if (isHelp(arg) || isVersion(arg)) {
340
679
  continue;
341
680
  }
342
681
  if (arg === "--port") {
343
- const port = readValue(argv, index, arg);
344
- const portNum = Number(port);
345
- options.port = Number.isNaN(portNum) ? port : portNum;
682
+ options.port = parsePortFlag(readValue(argv, index, arg));
346
683
  index += 1;
347
684
  continue;
348
685
  }
349
686
  if (arg.startsWith("--port=")) {
350
- const port = arg.slice("--port=".length);
351
- const portNum = Number(port);
352
- options.port = Number.isNaN(portNum) ? port : portNum;
687
+ options.port = parsePortFlag(readInlineValue(arg, "--port=", "--port"));
353
688
  continue;
354
689
  }
355
690
  if (arg === "--host") {
@@ -358,7 +693,7 @@ function parseDevArgs(argv) {
358
693
  continue;
359
694
  }
360
695
  if (arg.startsWith("--host=")) {
361
- options.host = arg.slice("--host=".length);
696
+ options.host = readInlineValue(arg, "--host=", "--host");
362
697
  continue;
363
698
  }
364
699
  if (arg === "--no-signals") {
@@ -366,12 +701,15 @@ function parseDevArgs(argv) {
366
701
  continue;
367
702
  }
368
703
  if (arg === "--shutdown-timeout") {
369
- options.shutdownTimeout = Number(readValue(argv, index, arg));
704
+ options.shutdownTimeout = parseIntegerFlag(readValue(argv, index, arg), "--shutdown-timeout");
370
705
  index += 1;
371
706
  continue;
372
707
  }
373
708
  if (arg.startsWith("--shutdown-timeout=")) {
374
- options.shutdownTimeout = Number(arg.slice("--shutdown-timeout=".length));
709
+ options.shutdownTimeout = parseIntegerFlag(
710
+ readInlineValue(arg, "--shutdown-timeout=", "--shutdown-timeout"),
711
+ "--shutdown-timeout"
712
+ );
375
713
  continue;
376
714
  }
377
715
  if (arg === "--require") {
@@ -379,10 +717,7 @@ function parseDevArgs(argv) {
379
717
  continue;
380
718
  }
381
719
  if (arg.startsWith("--require=")) {
382
- for (const part of arg.slice("--require=".length).split(",")) {
383
- const trimmed = part.trim();
384
- if (trimmed) requireModules.push(trimmed);
385
- }
720
+ requireModules.push(...parseCsvFlagValue(readInlineValue(arg, "--require=", "--require"), "--require"));
386
721
  continue;
387
722
  }
388
723
  if (arg === "--env") {
@@ -390,19 +725,16 @@ function parseDevArgs(argv) {
390
725
  continue;
391
726
  }
392
727
  if (arg.startsWith("--env=")) {
393
- for (const part of arg.slice("--env=".length).split(",")) {
394
- const trimmed = part.trim();
395
- if (trimmed) envFiles.push(trimmed);
396
- }
728
+ envFiles.push(...parseCsvFlagValue(readInlineValue(arg, "--env=", "--env"), "--env"));
397
729
  continue;
398
730
  }
399
731
  if (arg === "--tsconfig") {
400
- tsconfigPath = readValue(argv, index, arg);
732
+ tsconfigPath = setTsconfigPath(tsconfigPath, readValue(argv, index, arg));
401
733
  index += 1;
402
734
  continue;
403
735
  }
404
736
  if (arg.startsWith("--tsconfig=")) {
405
- tsconfigPath = arg.slice("--tsconfig=".length);
737
+ tsconfigPath = setTsconfigPath(tsconfigPath, readInlineValue(arg, "--tsconfig=", "--tsconfig"));
406
738
  continue;
407
739
  }
408
740
  if (arg === "--watch") {
@@ -410,10 +742,7 @@ function parseDevArgs(argv) {
410
742
  continue;
411
743
  }
412
744
  if (arg.startsWith("--watch=")) {
413
- for (const part of arg.slice("--watch=".length).split(",")) {
414
- const trimmed = part.trim();
415
- if (trimmed) watchPaths.push(trimmed);
416
- }
745
+ watchPaths.push(...parseCsvFlagValue(readInlineValue(arg, "--watch=", "--watch"), "--watch"));
417
746
  continue;
418
747
  }
419
748
  if (arg === "--ext") {
@@ -423,26 +752,20 @@ function parseDevArgs(argv) {
423
752
  }
424
753
  if (arg.startsWith("--ext=")) {
425
754
  watchExt = [];
426
- for (const part of arg.slice("--ext=".length).split(",")) {
427
- const trimmed = part.trim();
428
- if (trimmed) watchExt.push(trimmed);
429
- }
755
+ watchExt.push(...parseCsvFlagValue(readInlineValue(arg, "--ext=", "--ext"), "--ext"));
430
756
  continue;
431
757
  }
432
758
  if (arg === "--delay") {
433
- watchDelay = Number(readValue(argv, index, arg));
759
+ watchDelay = parseIntegerFlag(readValue(argv, index, arg), "--delay");
434
760
  index += 1;
435
761
  continue;
436
762
  }
437
763
  if (arg.startsWith("--delay=")) {
438
- watchDelay = Number(arg.slice("--delay=".length));
764
+ watchDelay = parseIntegerFlag(readInlineValue(arg, "--delay=", "--delay"), "--delay");
439
765
  continue;
440
766
  }
441
767
  if (!arg.startsWith("--")) {
442
- if (appPath) {
443
- throw new Error(`Unexpected positional argument: ${arg}. App module already set to ${appPath}`);
444
- }
445
- appPath = arg;
768
+ appPath = addPositional(arg, appPath, "App module");
446
769
  continue;
447
770
  }
448
771
  throw new Error(`Unknown argument: ${arg}`);
@@ -463,7 +786,7 @@ function parseDevArgs(argv) {
463
786
  };
464
787
  }
465
788
  function parseStartLikeArgs(argv, subcommandName) {
466
- for (const arg of argv) {
789
+ for (const arg of optionArgs(argv)) {
467
790
  if (arg === "--watch" || arg.startsWith("--watch=") || arg === "--tsconfig" || arg.startsWith("--tsconfig=") || arg === "--ext" || arg.startsWith("--ext=") || arg === "--delay" || arg.startsWith("--delay=")) {
468
791
  throw new Error(`--watch/--tsconfig/--ext/--delay are not supported with the ${subcommandName} subcommand`);
469
792
  }
@@ -480,10 +803,35 @@ function parseStartArgs(argv) {
480
803
  };
481
804
  }
482
805
  function parseStartServerlessArgs(argv) {
483
- const result = parseStartLikeArgs(argv, "start-serverless");
806
+ let maxBodyBytes;
807
+ const filtered = [];
808
+ for (let i = 0; i < argv.length; i += 1) {
809
+ const arg = argv[i];
810
+ if (arg === "--") {
811
+ filtered.push(...argv.slice(i));
812
+ break;
813
+ }
814
+ if (arg === "--max-body-bytes") {
815
+ maxBodyBytes = parseIntegerFlag(readValue(argv, i, arg), "--max-body-bytes");
816
+ validateMaxBodyBytes(maxBodyBytes);
817
+ i += 1;
818
+ continue;
819
+ }
820
+ if (arg.startsWith("--max-body-bytes=")) {
821
+ maxBodyBytes = parseIntegerFlag(
822
+ readInlineValue(arg, "--max-body-bytes=", "--max-body-bytes"),
823
+ "--max-body-bytes"
824
+ );
825
+ validateMaxBodyBytes(maxBodyBytes);
826
+ continue;
827
+ }
828
+ filtered.push(arg);
829
+ }
830
+ const result = parseStartLikeArgs(filtered, "start-serverless");
484
831
  return {
485
832
  handlerPath: result.appPath,
486
833
  options: result.options,
834
+ maxBodyBytes,
487
835
  require: result.require,
488
836
  env: result.env
489
837
  };
@@ -504,7 +852,10 @@ function parseBuildArgs(argv, outNameDefault) {
504
852
  for (let index = 0; index < argv.length; index += 1) {
505
853
  const arg = argv[index];
506
854
  if (arg === "--") {
507
- continue;
855
+ for (const positional of argv.slice(index + 1)) {
856
+ appPath = addPositional(positional, appPath, "App module");
857
+ }
858
+ break;
508
859
  }
509
860
  if (isHelp(arg) || isVersion(arg)) {
510
861
  continue;
@@ -515,16 +866,16 @@ function parseBuildArgs(argv, outNameDefault) {
515
866
  continue;
516
867
  }
517
868
  if (arg.startsWith("--init=")) {
518
- result.initPath = arg.slice("--init=".length);
869
+ result.initPath = readInlineValue(arg, "--init=", "--init");
519
870
  continue;
520
871
  }
521
872
  if (arg === "--tsconfig") {
522
- result.tsconfigPath = readValue(argv, index, arg);
873
+ result.tsconfigPath = setTsconfigPath(result.tsconfigPath, readValue(argv, index, arg));
523
874
  index += 1;
524
875
  continue;
525
876
  }
526
877
  if (arg.startsWith("--tsconfig=")) {
527
- result.tsconfigPath = arg.slice("--tsconfig=".length);
878
+ result.tsconfigPath = setTsconfigPath(result.tsconfigPath, readInlineValue(arg, "--tsconfig=", "--tsconfig"));
528
879
  continue;
529
880
  }
530
881
  if (arg === "--out-dir") {
@@ -533,7 +884,7 @@ function parseBuildArgs(argv, outNameDefault) {
533
884
  continue;
534
885
  }
535
886
  if (arg.startsWith("--out-dir=")) {
536
- result.outDir = arg.slice("--out-dir=".length);
887
+ result.outDir = readInlineValue(arg, "--out-dir=", "--out-dir");
537
888
  continue;
538
889
  }
539
890
  if (arg === "--out-name") {
@@ -542,7 +893,7 @@ function parseBuildArgs(argv, outNameDefault) {
542
893
  continue;
543
894
  }
544
895
  if (arg.startsWith("--out-name=")) {
545
- result.outName = arg.slice("--out-name=".length);
896
+ result.outName = readInlineValue(arg, "--out-name=", "--out-name");
546
897
  continue;
547
898
  }
548
899
  if (arg === "--format") {
@@ -555,7 +906,7 @@ function parseBuildArgs(argv, outNameDefault) {
555
906
  continue;
556
907
  }
557
908
  if (arg.startsWith("--format=")) {
558
- const fmt = arg.slice("--format=".length);
909
+ const fmt = readInlineValue(arg, "--format=", "--format");
559
910
  if (fmt !== "cjs" && fmt !== "esm") {
560
911
  throw new Error(`Invalid --format: ${fmt}. Must be 'cjs' or 'esm'.`);
561
912
  }
@@ -568,7 +919,7 @@ function parseBuildArgs(argv, outNameDefault) {
568
919
  continue;
569
920
  }
570
921
  if (arg.startsWith("--target=")) {
571
- result.target = arg.slice("--target=".length);
922
+ result.target = readInlineValue(arg, "--target=", "--target");
572
923
  continue;
573
924
  }
574
925
  if (arg === "--external") {
@@ -577,7 +928,7 @@ function parseBuildArgs(argv, outNameDefault) {
577
928
  continue;
578
929
  }
579
930
  if (arg.startsWith("--external=")) {
580
- external.push(arg.slice("--external=".length));
931
+ external.push(readInlineValue(arg, "--external=", "--external"));
581
932
  continue;
582
933
  }
583
934
  if (arg === "--no-clean") {
@@ -585,10 +936,7 @@ function parseBuildArgs(argv, outNameDefault) {
585
936
  continue;
586
937
  }
587
938
  if (!arg.startsWith("--")) {
588
- if (appPath) {
589
- throw new Error(`Unexpected positional argument: ${arg}. App module already set to ${appPath}`);
590
- }
591
- appPath = arg;
939
+ appPath = addPositional(arg, appPath, "App module");
592
940
  continue;
593
941
  }
594
942
  throw new Error(`Unknown argument: ${arg}`);
@@ -610,11 +958,12 @@ function parseArgs(argv) {
610
958
  printHelp();
611
959
  return null;
612
960
  }
613
- if (argv.some((a) => isHelp(a))) {
961
+ const globalArgs = optionArgs(argv);
962
+ if (globalArgs.some((a) => isHelp(a))) {
614
963
  printHelp();
615
964
  return null;
616
965
  }
617
- if (argv.some((a) => isVersion(a))) {
966
+ if (globalArgs.some((a) => isVersion(a))) {
618
967
  console.log(CLI_VERSION);
619
968
  return null;
620
969
  }
@@ -712,80 +1061,283 @@ async function preloadModules(modules) {
712
1061
  moduleRequire(mod);
713
1062
  }
714
1063
  }
715
- function buildChildArgs(args) {
716
- const result = ["dev", args.appPath];
717
- if (args.options.port !== void 0) result.push("--port", String(args.options.port));
718
- if (args.options.host !== void 0) result.push("--host", args.options.host);
719
- if (args.options.signals === false) result.push("--no-signals");
720
- if (args.options.shutdownTimeout !== void 0)
721
- result.push("--shutdown-timeout", String(args.options.shutdownTimeout));
722
- if (args.tsconfigPath !== void 0) result.push("--tsconfig", args.tsconfigPath);
723
- for (const r of args.require) result.push("--require", r);
724
- for (const e of args.env) result.push("--env", e);
725
- return result;
1064
+ function toDiagnosticMessage(prefix, error) {
1065
+ const message = error instanceof Error ? error.message : String(error);
1066
+ return `${prefix}: ${message}`;
726
1067
  }
727
- function runWithWatch(args) {
1068
+ function createWatchSupervisor(args, deps = {}) {
1069
+ const forkImpl = deps.fork ?? import_node_child_process.fork;
1070
+ const watchImpl = deps.watch ?? import_node_fs.watch;
1071
+ const existsSyncImpl = deps.existsSync ?? import_node_fs.existsSync;
1072
+ const logger = deps.logger ?? console;
1073
+ const setTimeoutImpl = deps.setTimeout ?? setTimeout;
1074
+ const clearTimeoutImpl = deps.clearTimeout ?? clearTimeout;
1075
+ const killTimeoutMs = deps.killTimeoutMs ?? DEFAULT_WATCH_KILL_TIMEOUT_MS;
728
1076
  const cliPath = process.argv[1];
729
1077
  const childArgv = buildChildArgs(args);
730
1078
  let child = null;
731
1079
  let restartTimer = null;
1080
+ let killTimer = null;
732
1081
  let isShuttingDown = false;
1082
+ let shutdownPromise = null;
1083
+ let restartInFlight = null;
1084
+ let terminatingChild = null;
1085
+ let failureHandled = false;
733
1086
  const restartDelay = args.watchDelay;
1087
+ const watchers = [];
1088
+ const clearRestartTimer = () => {
1089
+ if (restartTimer) {
1090
+ clearTimeoutImpl(restartTimer);
1091
+ restartTimer = null;
1092
+ }
1093
+ };
1094
+ const clearKillTimer = () => {
1095
+ if (killTimer) {
1096
+ clearTimeoutImpl(killTimer);
1097
+ killTimer = null;
1098
+ }
1099
+ };
1100
+ const closeWatchers = () => {
1101
+ for (const watcher of watchers.splice(0)) {
1102
+ try {
1103
+ watcher.close();
1104
+ } catch (_error) {
1105
+ void _error;
1106
+ }
1107
+ }
1108
+ };
1109
+ const completeWithExit = async (code) => {
1110
+ await shutdown();
1111
+ deps.exit?.(code);
1112
+ };
1113
+ const fail = (message, code = 1) => {
1114
+ if (failureHandled) return;
1115
+ failureHandled = true;
1116
+ logger.error(message);
1117
+ void completeWithExit(code).catch(() => {
1118
+ deps.exit?.(code);
1119
+ });
1120
+ };
734
1121
  const spawnChild = () => {
735
- child = (0, import_node_child_process.fork)(cliPath, childArgv, { stdio: "inherit" });
736
- child.on("exit", (code) => {
737
- child = null;
738
- if (!isShuttingDown && code !== null && code !== 0) {
1122
+ if (isShuttingDown) return;
1123
+ let nextChild;
1124
+ try {
1125
+ nextChild = forkImpl(cliPath, childArgv, { stdio: "inherit" });
1126
+ } catch (error) {
1127
+ fail(toDiagnosticMessage("Watch child failed to spawn", error));
1128
+ return;
1129
+ }
1130
+ child = nextChild;
1131
+ nextChild.once("error", (error) => {
1132
+ if (child === nextChild) {
1133
+ child = null;
1134
+ }
1135
+ fail(toDiagnosticMessage("Watch child process error", error));
1136
+ });
1137
+ nextChild.once("exit", (code, signal) => {
1138
+ if (child === nextChild) {
1139
+ child = null;
1140
+ }
1141
+ if (terminatingChild === nextChild || isShuttingDown || failureHandled) {
1142
+ return;
739
1143
  }
1144
+ const exitCode = typeof code === "number" && code > 0 ? code : 1;
1145
+ fail(`Watch child exited unexpectedly${signal ? ` from ${signal}` : ` with code ${String(code)}`}`, exitCode);
740
1146
  });
741
1147
  };
742
- const killChild = () => {
743
- return new Promise((resolve) => {
744
- if (!child || !child.pid) {
745
- resolve();
1148
+ const killChild = async (target = child) => {
1149
+ if (!target || !target.pid) {
1150
+ if (target && child === target) child = null;
1151
+ return;
1152
+ }
1153
+ terminatingChild = target;
1154
+ await new Promise((resolve, reject) => {
1155
+ let settled = false;
1156
+ const settle = (error) => {
1157
+ if (settled) return;
1158
+ settled = true;
1159
+ clearKillTimer();
1160
+ target.removeListener("exit", onExit);
1161
+ target.removeListener("error", onError);
1162
+ if (child === target) child = null;
1163
+ if (terminatingChild === target) terminatingChild = null;
1164
+ if (error) reject(error);
1165
+ else resolve();
1166
+ };
1167
+ const onExit = () => settle();
1168
+ const onError = (error) => settle(error);
1169
+ target.once("exit", onExit);
1170
+ target.once("error", onError);
1171
+ try {
1172
+ const signaled = target.kill("SIGTERM");
1173
+ if (!signaled) {
1174
+ throw new Error('child.kill("SIGTERM") returned false');
1175
+ }
1176
+ } catch (error) {
1177
+ settle(error instanceof Error ? error : new Error(String(error)));
746
1178
  return;
747
1179
  }
748
- child.once("exit", () => resolve());
749
- child.kill("SIGTERM");
1180
+ if (!settled) {
1181
+ killTimer = setTimeoutImpl(() => {
1182
+ try {
1183
+ const signaled = target.kill("SIGKILL");
1184
+ if (!signaled) {
1185
+ settle(new Error('child.kill("SIGKILL") returned false'));
1186
+ }
1187
+ } catch (error) {
1188
+ settle(error instanceof Error ? error : new Error(String(error)));
1189
+ }
1190
+ }, killTimeoutMs);
1191
+ }
750
1192
  });
751
1193
  };
752
1194
  const restart = async () => {
753
- await killChild();
754
- spawnChild();
1195
+ if (isShuttingDown) return;
1196
+ if (restartInFlight) {
1197
+ await restartInFlight;
1198
+ return;
1199
+ }
1200
+ restartInFlight = (async () => {
1201
+ if (isShuttingDown) return;
1202
+ await killChild();
1203
+ if (isShuttingDown) return;
1204
+ spawnChild();
1205
+ })();
1206
+ try {
1207
+ await restartInFlight;
1208
+ } finally {
1209
+ restartInFlight = null;
1210
+ }
755
1211
  };
756
1212
  const debouncedRestart = () => {
757
- if (restartTimer) clearTimeout(restartTimer);
758
- restartTimer = setTimeout(() => {
1213
+ if (isShuttingDown) return;
1214
+ clearRestartTimer();
1215
+ restartTimer = setTimeoutImpl(() => {
759
1216
  restartTimer = null;
760
- void restart();
1217
+ void restart().catch((error) => {
1218
+ fail(toDiagnosticMessage("Watch restart failed", error));
1219
+ });
761
1220
  }, restartDelay);
762
1221
  };
763
1222
  for (const watchPath of args.watch) {
764
1223
  const absPath = (0, import_node_path.resolve)(process.cwd(), watchPath);
765
- if (!(0, import_node_fs.existsSync)(absPath)) {
1224
+ if (!existsSyncImpl(absPath)) {
766
1225
  throw new Error(`Watch path not found: ${watchPath}`);
767
1226
  }
768
- (0, import_node_fs.watch)(absPath, { recursive: true }, (_eventType, filename) => {
769
- if (!filename) return;
770
- const ext = (0, import_node_path.extname)(filename).slice(1).toLowerCase();
771
- if (args.watchExt.includes(ext)) {
772
- debouncedRestart();
773
- }
774
- });
775
1227
  }
776
- const shutdown = () => {
1228
+ const shutdown = async () => {
1229
+ if (shutdownPromise) return shutdownPromise;
777
1230
  isShuttingDown = true;
778
- if (restartTimer) clearTimeout(restartTimer);
779
- if (child && child.pid) {
780
- child.once("exit", () => process.exit(0));
781
- child.kill("SIGTERM");
782
- } else {
783
- process.exit(0);
1231
+ clearRestartTimer();
1232
+ closeWatchers();
1233
+ shutdownPromise = (async () => {
1234
+ const activeRestart = restartInFlight;
1235
+ if (activeRestart) {
1236
+ await activeRestart.catch(() => void 0);
1237
+ }
1238
+ const activeChild = child;
1239
+ if (activeChild) {
1240
+ await killChild(activeChild).catch((error) => {
1241
+ if (!failureHandled) {
1242
+ fail(toDiagnosticMessage("Watch child termination failed", error));
1243
+ }
1244
+ });
1245
+ }
1246
+ })();
1247
+ return shutdownPromise;
1248
+ };
1249
+ try {
1250
+ for (const watchPath of args.watch) {
1251
+ const absPath = (0, import_node_path.resolve)(process.cwd(), watchPath);
1252
+ const watchListener = (_eventType, filename) => {
1253
+ if (isShuttingDown) return;
1254
+ if (!filename) return;
1255
+ const ext = (0, import_node_path.extname)(filename).slice(1).toLowerCase();
1256
+ if (args.watchExt.includes(ext)) {
1257
+ debouncedRestart();
1258
+ }
1259
+ };
1260
+ let watcher;
1261
+ try {
1262
+ watcher = watchImpl(absPath, { recursive: true }, watchListener);
1263
+ } catch (error) {
1264
+ if (error.code !== "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM") {
1265
+ throw error;
1266
+ }
1267
+ watcher = watchImpl(absPath, watchListener);
1268
+ }
1269
+ watcher.on?.(
1270
+ "error",
1271
+ (error) => {
1272
+ fail(toDiagnosticMessage("Watch path runtime error", error));
1273
+ }
1274
+ );
1275
+ watchers.push(watcher);
784
1276
  }
1277
+ spawnChild();
1278
+ } catch (error) {
1279
+ clearRestartTimer();
1280
+ closeWatchers();
1281
+ if (child) {
1282
+ void killChild(child).catch(() => void 0);
1283
+ }
1284
+ throw error;
1285
+ }
1286
+ return {
1287
+ shutdown,
1288
+ getChild: () => child,
1289
+ getWatchers: () => [...watchers],
1290
+ isShuttingDown: () => isShuttingDown
785
1291
  };
786
- process.on("SIGINT", shutdown);
787
- process.on("SIGTERM", shutdown);
788
- spawnChild();
1292
+ }
1293
+ function buildChildArgs(args) {
1294
+ const result = ["dev", args.appPath];
1295
+ if (args.options.port !== void 0) result.push("--port", String(args.options.port));
1296
+ if (args.options.host !== void 0) result.push("--host", args.options.host);
1297
+ if (args.options.signals === false) result.push("--no-signals");
1298
+ if (args.options.shutdownTimeout !== void 0)
1299
+ result.push("--shutdown-timeout", String(args.options.shutdownTimeout));
1300
+ if (args.tsconfigPath !== void 0) result.push("--tsconfig", args.tsconfigPath);
1301
+ for (const r of args.require) result.push("--require", r);
1302
+ for (const e of args.env) result.push("--env", e);
1303
+ return result;
1304
+ }
1305
+ function runWithWatch(args, deps = {}) {
1306
+ const usingInjectedDeps = Object.keys(deps).length > 0;
1307
+ const installSignalHandlers = deps.installSignalHandlers ?? !usingInjectedDeps;
1308
+ const exitImpl = deps.exit ?? (usingInjectedDeps ? void 0 : (code) => process.exit(code));
1309
+ const controller = createWatchSupervisor(args, {
1310
+ ...deps,
1311
+ exit: exitImpl
1312
+ });
1313
+ let shutdownStarted = false;
1314
+ const ownedHandlers = [];
1315
+ const removeOwnedHandlers = () => {
1316
+ for (const [signal, handler] of ownedHandlers.splice(0)) {
1317
+ process.removeListener(signal, handler);
1318
+ }
1319
+ };
1320
+ const shutdown = async () => {
1321
+ removeOwnedHandlers();
1322
+ await controller.shutdown();
1323
+ };
1324
+ const wrappedController = {
1325
+ shutdown,
1326
+ getChild: controller.getChild,
1327
+ getWatchers: controller.getWatchers,
1328
+ isShuttingDown: controller.isShuttingDown
1329
+ };
1330
+ if (installSignalHandlers) {
1331
+ const shutdownAndExit = () => {
1332
+ if (shutdownStarted) return;
1333
+ shutdownStarted = true;
1334
+ void shutdown().then(() => exitImpl?.(0));
1335
+ };
1336
+ ownedHandlers.push(["SIGINT", shutdownAndExit], ["SIGTERM", shutdownAndExit]);
1337
+ process.on("SIGINT", shutdownAndExit);
1338
+ process.on("SIGTERM", shutdownAndExit);
1339
+ }
1340
+ return wrappedController;
789
1341
  }
790
1342
  function generateServerlessEntry(appPath, initPath) {
791
1343
  const absAppPath = (0, import_node_path.resolve)(process.cwd(), appPath);
@@ -818,11 +1370,100 @@ function generateRuntimeEntry(appPath, initPath) {
818
1370
  }
819
1371
  return lines.join("\n") + "\n";
820
1372
  }
1373
+ function validateOutDirForClean(outDir, clean, appPath, initPath) {
1374
+ if (!clean) return;
1375
+ const cwd = process.cwd();
1376
+ const outAbs = (0, import_node_path.resolve)(cwd, outDir);
1377
+ const normalized = (0, import_node_path.normalize)(outAbs);
1378
+ const root = (0, import_node_path.parse)(normalized).root;
1379
+ if (normalized === root) {
1380
+ throw new Error(`Refusing to clean filesystem root: ${outDir} resolves to ${normalized}`);
1381
+ }
1382
+ if (normalized === (0, import_node_path.normalize)(cwd)) {
1383
+ throw new Error(`Refusing to clean project directory: ${outDir} resolves to cwd ${cwd}`);
1384
+ }
1385
+ if (cwd !== root && (cwd === normalized || cwd.startsWith(normalized + import_node_path.sep))) {
1386
+ throw new Error(
1387
+ `Refusing to clean ancestor of project directory: ${outDir} resolves to ${normalized} which contains cwd ${cwd}`
1388
+ );
1389
+ }
1390
+ try {
1391
+ if ((0, import_node_fs.existsSync)(outAbs)) {
1392
+ const st = (0, import_node_fs.lstatSync)(outAbs);
1393
+ if (st.isSymbolicLink()) {
1394
+ throw new Error(`Refusing to clean symlinked outDir: ${outDir} resolves to symlink ${outAbs}`);
1395
+ }
1396
+ }
1397
+ } catch (e) {
1398
+ if (e.message.startsWith("Refusing to clean")) throw e;
1399
+ }
1400
+ const checkOverlap = (inputPath, label) => {
1401
+ if (!inputPath) return;
1402
+ const inputAbs = (0, import_node_path.resolve)(cwd, inputPath);
1403
+ const inputNorm = (0, import_node_path.normalize)(inputAbs);
1404
+ if (inputNorm === normalized) {
1405
+ throw new Error(`Refusing to clean outDir that is the same as ${label}: ${outDir} == ${inputPath}`);
1406
+ }
1407
+ if (inputNorm.startsWith(normalized + import_node_path.sep)) {
1408
+ throw new Error(`Refusing to clean outDir that contains ${label}: ${outDir} contains ${inputPath}`);
1409
+ }
1410
+ };
1411
+ checkOverlap(appPath, "appPath");
1412
+ checkOverlap(initPath, "initPath");
1413
+ }
1414
+ function createUniqueStagingDir() {
1415
+ const cwd = process.cwd();
1416
+ const prefix = (0, import_node_path.join)(cwd, STAGING_DIR_PREFIX);
1417
+ const dir = (0, import_node_fs.mkdtempSync)(prefix);
1418
+ try {
1419
+ const st = (0, import_node_fs.lstatSync)(dir);
1420
+ if (st.isSymbolicLink()) {
1421
+ (0, import_node_fs.rmSync)(dir, { recursive: true, force: true });
1422
+ throw new Error(`Staging directory is a symlink: ${dir}`);
1423
+ }
1424
+ } catch (e) {
1425
+ if (e.message.includes("Staging directory is a symlink")) throw e;
1426
+ throw e;
1427
+ }
1428
+ try {
1429
+ (0, import_node_fs.chmodSync)(dir, 448);
1430
+ } catch {
1431
+ }
1432
+ return dir;
1433
+ }
1434
+ function writeStagingEntry(dir, content) {
1435
+ const entryPath = (0, import_node_path.join)(dir, "entry.ts");
1436
+ try {
1437
+ if ((0, import_node_fs.existsSync)(entryPath)) {
1438
+ const st = (0, import_node_fs.lstatSync)(entryPath);
1439
+ if (st.isSymbolicLink()) {
1440
+ throw new Error(`Refusing to overwrite symlink at staging path: ${entryPath}`);
1441
+ }
1442
+ throw new Error(`Staging file already exists: ${entryPath}`);
1443
+ }
1444
+ } catch (e) {
1445
+ if (e.message.startsWith("Refusing to") || e.message.startsWith("Staging file already exists"))
1446
+ throw e;
1447
+ }
1448
+ (0, import_node_fs.writeFileSync)(entryPath, content, { encoding: "utf8", flag: "wx", mode: 384 });
1449
+ try {
1450
+ const st = (0, import_node_fs.lstatSync)(entryPath);
1451
+ if (st.isSymbolicLink()) {
1452
+ (0, import_node_fs.rmSync)(entryPath, { force: true });
1453
+ throw new Error(`Staging file is a symlink after write: ${entryPath}`);
1454
+ }
1455
+ } catch (e) {
1456
+ if (e.message.includes("Staging file is a symlink")) throw e;
1457
+ }
1458
+ return entryPath;
1459
+ }
821
1460
  async function buildBundleFromEntryContent(args) {
1461
+ validateOutDirForClean(args.outDir, args.clean);
822
1462
  const tsupModule = await import("tsup");
823
1463
  const { build } = tsupModule;
824
- const tempEntryPath = (0, import_node_path.resolve)(process.cwd(), args.tempEntryFilename);
825
- (0, import_node_fs.writeFileSync)(tempEntryPath, args.entryContent, "utf8");
1464
+ const stagingDir = createUniqueStagingDir();
1465
+ const tempEntryPath = writeStagingEntry(stagingDir, args.entryContent);
1466
+ const absOutDir = (0, import_node_path.resolve)(process.cwd(), args.outDir);
826
1467
  try {
827
1468
  await build({
828
1469
  config: false,
@@ -830,85 +1471,371 @@ async function buildBundleFromEntryContent(args) {
830
1471
  tsconfig: args.tsconfigPath,
831
1472
  format: [args.format],
832
1473
  target: args.target,
833
- outDir: args.outDir,
1474
+ outDir: absOutDir,
834
1475
  clean: args.clean,
835
- external: ["express", ...args.external],
1476
+ external: ["express", "@web-ts-toolkit/express-runtime", ...args.external],
836
1477
  sourcemap: false,
837
1478
  dts: false,
838
1479
  splitting: false
839
1480
  });
840
1481
  } finally {
841
- (0, import_node_fs.rmSync)(tempEntryPath, { force: true });
1482
+ (0, import_node_fs.rmSync)(stagingDir, { recursive: true, force: true });
842
1483
  }
843
1484
  }
844
1485
  async function buildRuntime(args) {
845
1486
  const { runBuildEntryCommand: runBuildEntryCommand2 } = await Promise.resolve().then(() => (init_cli_api(), cli_api_exports));
846
1487
  await runBuildEntryCommand2(args, {
847
- generateEntry: generateRuntimeEntry,
848
- tempEntryFilename: TEMP_BUILD_ENTRY_FILENAME
1488
+ generateEntry: generateRuntimeEntry
849
1489
  });
850
1490
  }
851
1491
  async function buildServerless(args) {
852
1492
  const { runBuildEntryCommand: runBuildEntryCommand2 } = await Promise.resolve().then(() => (init_cli_api(), cli_api_exports));
853
1493
  await runBuildEntryCommand2(args, {
854
- generateEntry: generateServerlessEntry,
855
- tempEntryFilename: TEMP_SERVERLESS_ENTRY_FILENAME
1494
+ generateEntry: generateServerlessEntry
856
1495
  });
857
1496
  }
858
- function collectBody(req) {
1497
+ function validateMaxBodyBytes(value) {
1498
+ try {
1499
+ return validateFiniteInteger(value, { name: "--max-body-bytes", min: 0, max: MAX_INTEGER_OPTION_VALUE });
1500
+ } catch (_error) {
1501
+ void _error;
1502
+ throw new Error(
1503
+ `Invalid --max-body-bytes: ${String(value)}. Must be a finite integer in 0..${MAX_INTEGER_OPTION_VALUE}. Use 0 to disallow bodies (empty bodies only).`,
1504
+ { cause: _error }
1505
+ );
1506
+ }
1507
+ }
1508
+ function collectBody(req, maxBytes) {
1509
+ validateMaxBodyBytes(maxBytes);
859
1510
  return new Promise((resolve, reject) => {
1511
+ const rawLength = req.headers["content-length"];
1512
+ if (rawLength !== void 0) {
1513
+ const raw = Array.isArray(rawLength) ? rawLength[0] : rawLength;
1514
+ const parsed = Number(raw);
1515
+ if (Number.isFinite(parsed) && Number.isInteger(parsed) && parsed >= 0) {
1516
+ if (parsed > maxBytes) {
1517
+ try {
1518
+ req.resume?.();
1519
+ } catch (_e) {
1520
+ void _e;
1521
+ }
1522
+ const err = new Error(
1523
+ `Request body too large: Content-Length ${parsed} exceeds limit ${maxBytes}`
1524
+ );
1525
+ err.code = "LIMIT_EXCEEDED";
1526
+ err.statusCode = 413;
1527
+ reject(err);
1528
+ return;
1529
+ }
1530
+ }
1531
+ }
860
1532
  const chunks = [];
861
- req.on("data", (chunk) => chunks.push(chunk));
862
- req.on("end", () => resolve(Buffer.concat(chunks)));
863
- req.on("error", reject);
1533
+ let total = 0;
1534
+ let finished = false;
1535
+ const cleanup = () => {
1536
+ req.removeListener("data", onData);
1537
+ req.removeListener("end", onEnd);
1538
+ req.removeListener("error", onError);
1539
+ req.removeListener("close", onClose);
1540
+ try {
1541
+ req.removeListener?.(
1542
+ "aborted",
1543
+ onClose
1544
+ );
1545
+ } catch (_e) {
1546
+ void _e;
1547
+ }
1548
+ };
1549
+ const fail = (err) => {
1550
+ if (finished) return;
1551
+ finished = true;
1552
+ cleanup();
1553
+ if (err.code === "LIMIT_EXCEEDED") {
1554
+ try {
1555
+ req.resume?.();
1556
+ } catch (_e) {
1557
+ void _e;
1558
+ }
1559
+ }
1560
+ reject(err);
1561
+ };
1562
+ const onData = (chunk) => {
1563
+ if (finished) return;
1564
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1565
+ total += buf.length;
1566
+ if (total > maxBytes) {
1567
+ const err = new Error(
1568
+ `Request body too large: received ${total} bytes exceeds limit ${maxBytes}`
1569
+ );
1570
+ err.code = "LIMIT_EXCEEDED";
1571
+ err.statusCode = 413;
1572
+ fail(err);
1573
+ return;
1574
+ }
1575
+ chunks.push(buf);
1576
+ };
1577
+ const onEnd = () => {
1578
+ if (finished) return;
1579
+ finished = true;
1580
+ cleanup();
1581
+ try {
1582
+ resolve(Buffer.concat(chunks, total));
1583
+ } catch (e) {
1584
+ fail(e);
1585
+ }
1586
+ };
1587
+ const onError = (err) => {
1588
+ const e = err;
1589
+ if (!e.code) e.code = "STREAM_ERROR";
1590
+ fail(e);
1591
+ };
1592
+ const onClose = () => {
1593
+ if (finished) return;
1594
+ const e = new Error("Request aborted by client");
1595
+ e.code = "CLIENT_ABORT";
1596
+ fail(e);
1597
+ };
1598
+ req.on("data", onData);
1599
+ req.on("end", onEnd);
1600
+ req.on("error", onError);
1601
+ req.on("close", onClose);
1602
+ try {
1603
+ req.on?.("aborted", onClose);
1604
+ } catch (_e) {
1605
+ void _e;
1606
+ }
864
1607
  });
865
1608
  }
866
1609
  function toServerlessEvent(method, url, headers, body) {
1610
+ const parsedUrl = new URL(url, "http://localhost");
1611
+ const { queryStringParameters, multiValueQueryStringParameters } = parseAwsRestQuery(parsedUrl.search);
1612
+ const { singleValueHeaders, multiValueHeaders } = normalizeAwsRestHeaders(headers);
867
1613
  return {
868
1614
  httpMethod: method,
869
- path: url,
870
- headers,
871
- body: body.length > 0 ? body : void 0
1615
+ path: parsedUrl.pathname,
1616
+ headers: singleValueHeaders,
1617
+ multiValueHeaders,
1618
+ queryStringParameters,
1619
+ multiValueQueryStringParameters,
1620
+ body: body.length > 0 ? body.toString("base64") : "",
1621
+ isBase64Encoded: body.length > 0,
1622
+ requestContext: {
1623
+ identity: {
1624
+ // Minimal field required by serverless-http's AWS v1 request adapter.
1625
+ sourceIp: ""
1626
+ }
1627
+ }
1628
+ };
1629
+ }
1630
+ function parseAwsRestQuery(search) {
1631
+ if (search === "" || search === "?") {
1632
+ return { queryStringParameters: null, multiValueQueryStringParameters: null };
1633
+ }
1634
+ const single = {};
1635
+ const multi = {};
1636
+ const query = search.startsWith("?") ? search.slice(1) : search;
1637
+ for (const pair of query.split("&")) {
1638
+ if (pair === "") continue;
1639
+ const separator = pair.indexOf("=");
1640
+ const rawKey = separator === -1 ? pair : pair.slice(0, separator);
1641
+ const rawValue = separator === -1 ? "" : pair.slice(separator + 1);
1642
+ const key = decodeQueryComponent(rawKey);
1643
+ const value = decodeQueryComponent(rawValue);
1644
+ single[key] = value;
1645
+ (multi[key] ??= []).push(value);
1646
+ }
1647
+ return {
1648
+ queryStringParameters: Object.keys(single).length > 0 ? single : null,
1649
+ multiValueQueryStringParameters: Object.keys(multi).length > 0 ? multi : null
872
1650
  };
873
1651
  }
1652
+ function decodeQueryComponent(value) {
1653
+ try {
1654
+ return decodeURIComponent(value);
1655
+ } catch (_error) {
1656
+ void _error;
1657
+ return value;
1658
+ }
1659
+ }
1660
+ function normalizeAwsRestHeaders(headers) {
1661
+ const singleValueHeaders = {};
1662
+ const multiValueHeaders = {};
1663
+ for (const [key, value] of Object.entries(headers)) {
1664
+ if (value === void 0) continue;
1665
+ const values = Array.isArray(value) ? value.map(String) : [String(value)];
1666
+ multiValueHeaders[key] = values;
1667
+ singleValueHeaders[key] = values.join(", ");
1668
+ }
1669
+ return { singleValueHeaders, multiValueHeaders };
1670
+ }
874
1671
  function applyServerlessResult(result, res) {
875
- if (result === null || result === void 0) {
876
- res.status(200).end();
877
- return;
1672
+ const response = validateServerlessResult(result);
1673
+ res.status(response.statusCode);
1674
+ for (const [key, value] of Object.entries(response.headers)) {
1675
+ res.setHeader(key, value);
878
1676
  }
879
- const r = result;
880
- if (typeof r.statusCode === "number") {
881
- res.status(r.statusCode);
882
- }
883
- if (r.headers && typeof r.headers === "object") {
884
- for (const [key, value] of Object.entries(r.headers)) {
885
- if (value !== void 0) {
886
- if (Array.isArray(value) && key.toLowerCase() === "set-cookie") {
887
- res.setHeader(key, value);
888
- } else {
889
- res.setHeader(key, Array.isArray(value) ? value.join(",") : value);
890
- }
891
- }
1677
+ for (const [key, values] of Object.entries(response.multiValueHeaders)) {
1678
+ if (key.toLowerCase() === "set-cookie") {
1679
+ res.setHeader(key, values);
1680
+ } else {
1681
+ res.setHeader(key, values.join(","));
892
1682
  }
893
1683
  }
894
- if (r.isBase64Encoded && typeof r.body === "string") {
895
- res.end(Buffer.from(r.body, "base64"));
896
- } else if (typeof r.body === "string") {
897
- res.end(r.body);
1684
+ if (response.isBase64Encoded) {
1685
+ res.end(response.decodedBody);
898
1686
  } else {
899
- res.end();
1687
+ res.end(response.body);
900
1688
  }
901
1689
  }
902
- function createServerlessAdapterApp(handler) {
1690
+ function validateServerlessResult(result) {
1691
+ if (!isPlainRecord(result)) {
1692
+ throw new Error(
1693
+ "Invalid serverless result: expected an object with an optional statusCode, headers, multiValueHeaders, body, and isBase64Encoded."
1694
+ );
1695
+ }
1696
+ const rawStatus = result.statusCode;
1697
+ if (rawStatus !== void 0 && (typeof rawStatus !== "number" || !Number.isInteger(rawStatus) || rawStatus < 100 || rawStatus > 599)) {
1698
+ throw new Error(`Invalid serverless result statusCode: ${String(rawStatus)}. Expected an integer in 100..599.`);
1699
+ }
1700
+ const rawIsBase64Encoded = result.isBase64Encoded;
1701
+ if (rawIsBase64Encoded !== void 0 && typeof rawIsBase64Encoded !== "boolean") {
1702
+ throw new Error("Invalid serverless result isBase64Encoded: expected a boolean when provided.");
1703
+ }
1704
+ const rawBody = result.body;
1705
+ if (rawBody !== void 0 && typeof rawBody !== "string") {
1706
+ throw new Error(`Invalid serverless result body: expected a string when provided, received ${typeof rawBody}.`);
1707
+ }
1708
+ const headers = validateSingleValueHeaders(result.headers, "headers");
1709
+ const multiValueHeaders = validateMultiValueHeaders(result.multiValueHeaders, "multiValueHeaders");
1710
+ const multiHeaderKeys = new Set(Object.keys(multiValueHeaders).map((key) => key.toLowerCase()));
1711
+ for (const key of Object.keys(headers)) {
1712
+ if (multiHeaderKeys.has(key.toLowerCase())) {
1713
+ delete headers[key];
1714
+ }
1715
+ }
1716
+ const isBase64Encoded = rawIsBase64Encoded ?? false;
1717
+ const body = rawBody ?? "";
1718
+ let decodedBody;
1719
+ if (isBase64Encoded) {
1720
+ if (!isValidBase64(body)) {
1721
+ throw new Error("Invalid serverless result body: isBase64Encoded is true but body is not valid standard base64.");
1722
+ }
1723
+ decodedBody = Buffer.from(body, "base64");
1724
+ }
1725
+ return {
1726
+ statusCode: rawStatus ?? 200,
1727
+ headers,
1728
+ multiValueHeaders,
1729
+ body,
1730
+ decodedBody,
1731
+ isBase64Encoded
1732
+ };
1733
+ }
1734
+ function validateSingleValueHeaders(value, name) {
1735
+ if (value === void 0) return {};
1736
+ if (!isPlainRecord(value)) {
1737
+ throw new Error(`Invalid serverless result ${name}: expected an object of string header values.`);
1738
+ }
1739
+ const headers = {};
1740
+ for (const [key, headerValue] of Object.entries(value)) {
1741
+ if (headerValue === void 0) continue;
1742
+ if (typeof headerValue !== "string") {
1743
+ throw new Error(`Invalid serverless result ${name}.${key}: expected a string header value.`);
1744
+ }
1745
+ validateServerlessHeader(key, headerValue, `${name}.${key}`);
1746
+ headers[key] = headerValue;
1747
+ }
1748
+ return headers;
1749
+ }
1750
+ function validateMultiValueHeaders(value, name) {
1751
+ if (value === void 0) return {};
1752
+ if (!isPlainRecord(value)) {
1753
+ throw new Error(`Invalid serverless result ${name}: expected an object of string-array header values.`);
1754
+ }
1755
+ const headers = {};
1756
+ for (const [key, headerValue] of Object.entries(value)) {
1757
+ if (headerValue === void 0) continue;
1758
+ if (!Array.isArray(headerValue) || headerValue.some((entry) => typeof entry !== "string")) {
1759
+ throw new Error(`Invalid serverless result ${name}.${key}: expected an array of string header values.`);
1760
+ }
1761
+ for (const entry of headerValue) {
1762
+ validateServerlessHeader(key, entry, `${name}.${key}`);
1763
+ }
1764
+ headers[key] = headerValue;
1765
+ }
1766
+ return headers;
1767
+ }
1768
+ function validateServerlessHeader(key, value, label) {
1769
+ try {
1770
+ (0, import_node_http2.validateHeaderName)(key);
1771
+ (0, import_node_http2.validateHeaderValue)(key, value);
1772
+ } catch (error) {
1773
+ throw new Error(`Invalid serverless result header ${label}: ${error.message}`, { cause: error });
1774
+ }
1775
+ }
1776
+ function isValidBase64(value) {
1777
+ if (value === "") return true;
1778
+ if (value.length % 4 !== 0) return false;
1779
+ return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
1780
+ }
1781
+ function isPlainRecord(value) {
1782
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1783
+ }
1784
+ function createServerlessAdapterApp(handler, options = {}) {
1785
+ const maxBytes = options.maxBodyBytes ?? DEFAULT_ADAPTER_MAX_BODY_BYTES;
1786
+ validateMaxBodyBytes(maxBytes);
903
1787
  return createExpressApp({
904
1788
  json: false,
905
1789
  urlencoded: false,
906
1790
  finalize: (app) => {
907
1791
  app.use(async (req, res) => {
908
- const body = await collectBody(req);
909
- const event = toServerlessEvent(req.method, req.url, req.headers, body);
910
- const result = await handler(event, {});
911
- applyServerlessResult(result, res);
1792
+ let body;
1793
+ try {
1794
+ body = await collectBody(req, maxBytes);
1795
+ } catch (err) {
1796
+ const e = err;
1797
+ if (e?.code === "LIMIT_EXCEEDED" || e?.statusCode === 413) {
1798
+ if (!res.headersSent && !res.writableEnded) {
1799
+ res.status(413).end("Payload Too Large");
1800
+ } else {
1801
+ try {
1802
+ res.end();
1803
+ } catch (_e) {
1804
+ void _e;
1805
+ }
1806
+ }
1807
+ return;
1808
+ }
1809
+ if (e?.code === "CLIENT_ABORT") {
1810
+ return;
1811
+ }
1812
+ console.error("Serverless adapter error:", e);
1813
+ if (!res.headersSent && !res.writableEnded) {
1814
+ res.status(500).end("Internal server error");
1815
+ } else {
1816
+ try {
1817
+ res.end();
1818
+ } catch (_e) {
1819
+ void _e;
1820
+ }
1821
+ }
1822
+ return;
1823
+ }
1824
+ let result;
1825
+ try {
1826
+ const event = toServerlessEvent(req.method, req.url, req.headers, body);
1827
+ result = await handler(event, {});
1828
+ } catch (e) {
1829
+ console.error("Serverless adapter error:", e);
1830
+ if (!res.headersSent && !res.writableEnded) res.status(500).end("Internal server error");
1831
+ return;
1832
+ }
1833
+ try {
1834
+ applyServerlessResult(result, res);
1835
+ } catch (e) {
1836
+ console.error("Invalid serverless handler result:", e);
1837
+ if (!res.headersSent && !res.writableEnded) res.status(500).end("Internal server error");
1838
+ }
912
1839
  });
913
1840
  },
914
1841
  errorHandler: (error, _req, res, _next) => {
@@ -921,7 +1848,7 @@ async function loadBuiltApp(appPath) {
921
1848
  const fullPath = (0, import_node_path.resolve)(process.cwd(), appPath);
922
1849
  const moduleUrl = (0, import_node_url.pathToFileURL)(fullPath).href;
923
1850
  const mod = await import(moduleUrl);
924
- const exported = extractExport(mod);
1851
+ const exported = mod.app ?? mod.default;
925
1852
  if (!exported) {
926
1853
  throw new Error(
927
1854
  `Module "${appPath}" must default-export an Express app or export it as "app". Exports: ${Object.keys(mod).join(", ")}`
@@ -931,9 +1858,14 @@ async function loadBuiltApp(appPath) {
931
1858
  if (init !== void 0 && typeof init !== "function") {
932
1859
  throw new Error(`Module "${appPath}" must export "init" as a function when present.`);
933
1860
  }
1861
+ const shutdown = mod.shutdown;
1862
+ if (shutdown !== void 0 && typeof shutdown !== "function") {
1863
+ throw new Error(`Module "${appPath}" must export "shutdown" as a function when present.`);
1864
+ }
934
1865
  return {
935
1866
  app: await resolveExport(exported, appPath),
936
- init
1867
+ init,
1868
+ shutdown
937
1869
  };
938
1870
  }
939
1871
  async function loadHandler(handlerPath) {
@@ -948,7 +1880,7 @@ async function loadHandler(handlerPath) {
948
1880
  }
949
1881
  return exported;
950
1882
  }
951
- var import_node_url, import_node_path, import_node_fs, import_node_module, import_node_child_process, CLI_VERSION, DEFAULT_WATCH_EXTENSIONS, DEFAULT_WATCH_DELAY, moduleRequire, TEMP_BUILD_ENTRY_FILENAME, TEMP_SERVERLESS_ENTRY_FILENAME;
1883
+ var import_node_url, import_node_path, import_node_fs, import_node_module, import_node_child_process, import_node_http2, CLI_VERSION, DEFAULT_WATCH_EXTENSIONS, DEFAULT_WATCH_DELAY, moduleRequire, DEFAULT_WATCH_KILL_TIMEOUT_MS, TEMP_BUILD_ENTRY_FILENAME, TEMP_SERVERLESS_ENTRY_FILENAME, STAGING_DIR_PREFIX, DEFAULT_ADAPTER_MAX_BODY_BYTES;
952
1884
  var init_cli_utils = __esm({
953
1885
  "src/cli-utils.ts"() {
954
1886
  "use strict";
@@ -957,15 +1889,20 @@ var init_cli_utils = __esm({
957
1889
  import_node_fs = require("fs");
958
1890
  import_node_module = require("module");
959
1891
  import_node_child_process = require("child_process");
1892
+ import_node_http2 = require("http");
960
1893
  init_index();
961
- CLI_VERSION = "0.0.0-PLACEHOLDER";
1894
+ init_numeric_validation();
1895
+ CLI_VERSION = resolveCliVersion();
962
1896
  DEFAULT_WATCH_EXTENSIONS = ["ts", "js", "mjs", "cjs", "json"];
963
1897
  DEFAULT_WATCH_DELAY = 500;
964
1898
  moduleRequire = (0, import_node_module.createRequire)(
965
1899
  (0, import_node_url.pathToFileURL)((0, import_node_path.resolve)(process.cwd(), "__wtt_runtime_preload__.js"))
966
1900
  );
1901
+ DEFAULT_WATCH_KILL_TIMEOUT_MS = 5e3;
967
1902
  TEMP_BUILD_ENTRY_FILENAME = ".express-runtime-build-entry.ts";
968
1903
  TEMP_SERVERLESS_ENTRY_FILENAME = ".express-runtime-build-serverless-entry.ts";
1904
+ STAGING_DIR_PREFIX = ".wtt-build-";
1905
+ DEFAULT_ADAPTER_MAX_BODY_BYTES = 1024 * 1024;
969
1906
  }
970
1907
  });
971
1908
 
@@ -973,11 +1910,15 @@ var init_cli_utils = __esm({
973
1910
  var cli_api_exports = {};
974
1911
  __export(cli_api_exports, {
975
1912
  CLI_VERSION: () => CLI_VERSION,
1913
+ DEFAULT_ADAPTER_MAX_BODY_BYTES: () => DEFAULT_ADAPTER_MAX_BODY_BYTES,
1914
+ TEMP_BUILD_ENTRY_FILENAME: () => TEMP_BUILD_ENTRY_FILENAME,
1915
+ TEMP_SERVERLESS_ENTRY_FILENAME: () => TEMP_SERVERLESS_ENTRY_FILENAME,
976
1916
  applyServerlessResult: () => applyServerlessResult,
977
1917
  buildBundleFromEntryContent: () => buildBundleFromEntryContent,
978
1918
  buildChildArgs: () => buildChildArgs,
979
1919
  buildRuntime: () => buildRuntime,
980
1920
  buildServerless: () => buildServerless,
1921
+ collectBody: () => collectBody,
981
1922
  createServerlessAdapterApp: () => createServerlessAdapterApp,
982
1923
  extractExport: () => extractExport,
983
1924
  generateRuntimeEntry: () => generateRuntimeEntry,
@@ -998,7 +1939,9 @@ __export(cli_api_exports, {
998
1939
  runDevCommand: () => runDevCommand,
999
1940
  runExpressDevCommand: () => runExpressDevCommand,
1000
1941
  runWithWatch: () => runWithWatch,
1001
- toServerlessEvent: () => toServerlessEvent
1942
+ toServerlessEvent: () => toServerlessEvent,
1943
+ validateMaxBodyBytes: () => validateMaxBodyBytes,
1944
+ validateOutDirForClean: () => validateOutDirForClean
1002
1945
  });
1003
1946
  module.exports = __toCommonJS(cli_api_exports);
1004
1947
  async function runDevCommand(args, runner) {
@@ -1011,7 +1954,8 @@ async function runDevCommand(args, runner) {
1011
1954
  }
1012
1955
  await preloadModules(args.require);
1013
1956
  const loaded = await runner.load(args.appPath);
1014
- runner.start(loaded, { ...args.options, exitAfterShutdown: true });
1957
+ const server = runner.start(loaded, { ...args.options, exitAfterShutdown: true });
1958
+ await server?.ready;
1015
1959
  }
1016
1960
  async function runExpressDevCommand(args) {
1017
1961
  await runDevCommand(args, {
@@ -1025,9 +1969,10 @@ async function runBuildEntryCommand(args, options) {
1025
1969
  if (options.allowInit === false && args.initPath) {
1026
1970
  throw new Error(options.initErrorMessage ?? "This build command manages init automatically. Remove --init.");
1027
1971
  }
1972
+ const { validateOutDirForClean: validateOutDirForClean2 } = await Promise.resolve().then(() => (init_cli_utils(), cli_utils_exports));
1973
+ validateOutDirForClean2(args.outDir, args.clean, args.appPath, args.initPath);
1028
1974
  await buildBundleFromEntryContent({
1029
1975
  entryContent: options.generateEntry(args.appPath, args.initPath),
1030
- tempEntryFilename: options.tempEntryFilename,
1031
1976
  tsconfigPath: args.tsconfigPath,
1032
1977
  outDir: args.outDir,
1033
1978
  outName: args.outName,
@@ -1048,14 +1993,17 @@ async function runCliCommand(parsedArgs) {
1048
1993
  loadEnvFiles(start.env);
1049
1994
  }
1050
1995
  await preloadModules(start.require);
1051
- const { app, init } = await loadBuiltApp(start.appPath);
1052
- startLocalServer(app, {
1996
+ const { app, init, shutdown } = await loadBuiltApp(start.appPath);
1997
+ await startLocalServer(app, {
1053
1998
  ...start.options,
1054
1999
  init: init ? async () => {
1055
2000
  await init();
1056
2001
  } : void 0,
2002
+ onShutdown: shutdown ? async () => {
2003
+ await shutdown();
2004
+ } : void 0,
1057
2005
  exitAfterShutdown: true
1058
- });
2006
+ }).ready;
1059
2007
  return;
1060
2008
  }
1061
2009
  if (parsedArgs.subcommand === "build") {
@@ -1069,8 +2017,8 @@ async function runCliCommand(parsedArgs) {
1069
2017
  }
1070
2018
  await preloadModules(startServerless.require);
1071
2019
  const handler = await loadHandler(startServerless.handlerPath);
1072
- const app = createServerlessAdapterApp(handler);
1073
- startLocalServer(app, { ...startServerless.options, exitAfterShutdown: true });
2020
+ const app = createServerlessAdapterApp(handler, { maxBodyBytes: startServerless.maxBodyBytes });
2021
+ await startLocalServer(app, { ...startServerless.options, exitAfterShutdown: true }).ready;
1074
2022
  return;
1075
2023
  }
1076
2024
  await buildServerless(parsedArgs.buildServerless);
@@ -1085,11 +2033,15 @@ init_cli_api();
1085
2033
  // Annotate the CommonJS export names for ESM import in node:
1086
2034
  0 && (module.exports = {
1087
2035
  CLI_VERSION,
2036
+ DEFAULT_ADAPTER_MAX_BODY_BYTES,
2037
+ TEMP_BUILD_ENTRY_FILENAME,
2038
+ TEMP_SERVERLESS_ENTRY_FILENAME,
1088
2039
  applyServerlessResult,
1089
2040
  buildBundleFromEntryContent,
1090
2041
  buildChildArgs,
1091
2042
  buildRuntime,
1092
2043
  buildServerless,
2044
+ collectBody,
1093
2045
  createServerlessAdapterApp,
1094
2046
  extractExport,
1095
2047
  generateRuntimeEntry,
@@ -1110,5 +2062,7 @@ init_cli_api();
1110
2062
  runDevCommand,
1111
2063
  runExpressDevCommand,
1112
2064
  runWithWatch,
1113
- toServerlessEvent
2065
+ toServerlessEvent,
2066
+ validateMaxBodyBytes,
2067
+ validateOutDirForClean
1114
2068
  });