@web-ts-toolkit/express-runtime 0.43.0 → 0.44.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.js CHANGED
@@ -61,11 +61,15 @@ function parsePortValue(value, name) {
61
61
  }
62
62
  return value;
63
63
  }
64
- var MAX_INTEGER_OPTION_VALUE;
64
+ function validateTimerDuration(value, name) {
65
+ return validateFiniteInteger(value, { name, min: 0, max: MAX_TIMER_DURATION_MS });
66
+ }
67
+ var MAX_INTEGER_OPTION_VALUE, MAX_TIMER_DURATION_MS;
65
68
  var init_numeric_validation = __esm({
66
69
  "src/numeric-validation.ts"() {
67
70
  "use strict";
68
71
  MAX_INTEGER_OPTION_VALUE = Number.MAX_SAFE_INTEGER;
72
+ MAX_TIMER_DURATION_MS = 2147483647;
69
73
  }
70
74
  });
71
75
 
@@ -152,11 +156,7 @@ function startLocalServer(app, options = {}) {
152
156
  const logger = options.logger ?? defaultLogger;
153
157
  const port = normalizePort(options.port);
154
158
  const host = options.host ?? process.env.HOST ?? "0.0.0.0";
155
- const shutdownTimeout = validateFiniteInteger(options.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT, {
156
- name: "shutdownTimeout",
157
- min: 0,
158
- max: MAX_INTEGER_OPTION_VALUE
159
- });
159
+ const shutdownTimeout = validateTimerDuration(options.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT, "shutdownTimeout");
160
160
  const server = import_node_http.default.createServer(app);
161
161
  app.set("port", port);
162
162
  let state = "initializing";
@@ -328,6 +328,7 @@ function startLocalServer(app, options = {}) {
328
328
  done();
329
329
  }
330
330
  });
331
+ let shutdownFailed = false;
331
332
  let shutdownError;
332
333
  try {
333
334
  if (options.onShutdown) {
@@ -335,9 +336,10 @@ function startLocalServer(app, options = {}) {
335
336
  }
336
337
  } catch (err) {
337
338
  logger.error("onShutdown hook failed:", err);
339
+ shutdownFailed = true;
338
340
  shutdownError = err;
339
341
  }
340
- if (shutdownError) {
342
+ if (shutdownFailed) {
341
343
  state = "failed";
342
344
  if (options.exitAfterShutdown) {
343
345
  process.exit(1);
@@ -356,7 +358,7 @@ function startLocalServer(app, options = {}) {
356
358
  };
357
359
  if (options.signals !== false) {
358
360
  const list = options.signals === void 0 || options.signals === true ? DEFAULT_SIGNALS : options.signals;
359
- for (const sig of list) {
361
+ for (const sig of new Set(list)) {
360
362
  const handler = () => {
361
363
  void shutdown().catch(() => {
362
364
  });
@@ -508,6 +510,12 @@ function parseIntegerFlag(raw, name, min = 0, max = MAX_INTEGER_OPTION_VALUE) {
508
510
  }
509
511
  return validateFiniteInteger(Number(raw), { name, min, max });
510
512
  }
513
+ function parseTimerFlag(raw, name) {
514
+ if (!/^(0|[1-9]\d*)$/.test(raw)) {
515
+ throw new Error(`Invalid ${name}: ${raw}. Must be a finite integer in 0..${MAX_TIMER_DURATION_MS}.`);
516
+ }
517
+ return validateTimerDuration(Number(raw), name);
518
+ }
511
519
  function parsePortFlag(raw, name = "--port") {
512
520
  try {
513
521
  return parsePortValue(raw, name);
@@ -547,13 +555,13 @@ Dev options:
547
555
  --port <number> Port or named pipe (default: process.env.PORT or 8080)
548
556
  --host <hostname> Hostname to bind (default: process.env.HOST or 0.0.0.0)
549
557
  --no-signals Disable SIGINT/SIGTERM handler registration
550
- --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000)
558
+ --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000; 0..2147483647)
551
559
  --require <module> Module(s) to preload before app load (repeatable)
552
560
  --env <path> Env file(s) to load (repeatable; existing env vars are not overridden)
553
561
  --tsconfig <path> Tsconfig used by config-aware consumers for TS path resolution
554
562
  --watch <paths> Comma-separated paths to watch for restart (repeatable; dev only)
555
563
  --ext <extensions> Comma-separated extensions to watch (default: ts,js,mjs,cjs,json)
556
- --delay <ms> Debounce ms before restarting on change (default: 500)
564
+ --delay <ms> Debounce ms before restarting on change (default: 500; 0..2147483647)
557
565
 
558
566
  Build options:
559
567
  --init <path> Init hook module (default export, async function)
@@ -562,14 +570,14 @@ Build options:
562
570
  --out-name <name> Output filename without extension (default: app)
563
571
  --format <cjs|esm> Output format (default: cjs)
564
572
  --target <target> Compilation target (default: node22)
565
- --external <pkg> Mark package as external (repeatable; express always external)
573
+ --external <pkg> Mark package as external (repeatable; express and @web-ts-toolkit/express-runtime always external)
566
574
  --no-clean Don't clean the output directory before building
567
575
 
568
576
  Start options:
569
577
  --port <number> Port or named pipe (default: process.env.PORT or 8080)
570
578
  --host <hostname> Hostname to bind (default: process.env.HOST or 0.0.0.0)
571
579
  --no-signals Disable SIGINT/SIGTERM handler registration
572
- --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000)
580
+ --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000; 0..2147483647)
573
581
  --require <module> Module(s) to preload before app load (repeatable)
574
582
  --env <path> Env file(s) to load (repeatable; existing env vars are not overridden)
575
583
 
@@ -580,14 +588,14 @@ Build-serverless options:
580
588
  --out-name <name> Output filename without extension (default: handler)
581
589
  --format <cjs|esm> Output format (default: cjs)
582
590
  --target <target> Compilation target (default: node22)
583
- --external <pkg> Mark package as external (repeatable; express always external)
591
+ --external <pkg> Mark package as external (repeatable; express and @web-ts-toolkit/express-runtime always external)
584
592
  --no-clean Don't clean the output directory before building
585
593
 
586
594
  Start-serverless options:
587
595
  --port <number> Port or named pipe (default: process.env.PORT or 8080)
588
596
  --host <hostname> Hostname to bind (default: process.env.HOST or 0.0.0.0)
589
597
  --no-signals Disable SIGINT/SIGTERM handler registration
590
- --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000)
598
+ --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000; 0..2147483647)
591
599
  --max-body-bytes <bytes> Max request body bytes for adapter (default: 1048576; 0 disallows bodies)
592
600
  --require <module> Module(s) to preload before handler load (repeatable)
593
601
  --env <path> Env file(s) to load (repeatable; existing env vars are not overridden)
@@ -617,7 +625,7 @@ Notes:
617
625
  - --watch forks one child process running the same CLI without --watch. File changes
618
626
  are serialized into one restart at a time: SIGTERM, SIGKILL after 5000 ms if needed,
619
627
  then respawn after the debounce delay. Shutdown closes owned watchers and signal handlers.
620
- - In build/build-serverless mode, express is always external. Add more externals with --external.
628
+ - In build/build-serverless mode, express and @web-ts-toolkit/express-runtime are always external. Add more externals with --external.
621
629
  - In start mode, the bundled app file must default-export an Express app (or export it as "app").
622
630
  If the bundle exports "init", it runs before the server starts listening.
623
631
  - In start-serverless mode, the bundled handler file must be a JS/CJS module whose
@@ -701,12 +709,12 @@ function parseDevArgs(argv) {
701
709
  continue;
702
710
  }
703
711
  if (arg === "--shutdown-timeout") {
704
- options.shutdownTimeout = parseIntegerFlag(readValue(argv, index, arg), "--shutdown-timeout");
712
+ options.shutdownTimeout = parseTimerFlag(readValue(argv, index, arg), "--shutdown-timeout");
705
713
  index += 1;
706
714
  continue;
707
715
  }
708
716
  if (arg.startsWith("--shutdown-timeout=")) {
709
- options.shutdownTimeout = parseIntegerFlag(
717
+ options.shutdownTimeout = parseTimerFlag(
710
718
  readInlineValue(arg, "--shutdown-timeout=", "--shutdown-timeout"),
711
719
  "--shutdown-timeout"
712
720
  );
@@ -756,12 +764,12 @@ function parseDevArgs(argv) {
756
764
  continue;
757
765
  }
758
766
  if (arg === "--delay") {
759
- watchDelay = parseIntegerFlag(readValue(argv, index, arg), "--delay");
767
+ watchDelay = parseTimerFlag(readValue(argv, index, arg), "--delay");
760
768
  index += 1;
761
769
  continue;
762
770
  }
763
771
  if (arg.startsWith("--delay=")) {
764
- watchDelay = parseIntegerFlag(readInlineValue(arg, "--delay=", "--delay"), "--delay");
772
+ watchDelay = parseTimerFlag(readInlineValue(arg, "--delay=", "--delay"), "--delay");
765
773
  continue;
766
774
  }
767
775
  if (!arg.startsWith("--")) {
@@ -1057,14 +1065,23 @@ function loadEnvFiles(paths) {
1057
1065
  }
1058
1066
  }
1059
1067
  async function preloadModules(modules) {
1068
+ const invocationRequire = (0, import_node_module.createRequire)(
1069
+ (0, import_node_url.pathToFileURL)((0, import_node_path.resolve)(process.cwd(), "__wtt_runtime_preload__.js"))
1070
+ );
1060
1071
  for (const mod of modules) {
1061
- moduleRequire(mod);
1072
+ invocationRequire(mod);
1062
1073
  }
1063
1074
  }
1064
1075
  function toDiagnosticMessage(prefix, error) {
1065
1076
  const message = error instanceof Error ? error.message : String(error);
1066
1077
  return `${prefix}: ${message}`;
1067
1078
  }
1079
+ function isChildGone(proc) {
1080
+ const exitCode = proc.exitCode;
1081
+ const signalCode = proc.signalCode;
1082
+ if (exitCode != null || signalCode != null) return true;
1083
+ return proc.pid === void 0;
1084
+ }
1068
1085
  function createWatchSupervisor(args, deps = {}) {
1069
1086
  const forkImpl = deps.fork ?? import_node_child_process.fork;
1070
1087
  const watchImpl = deps.watch ?? import_node_fs.watch;
@@ -1073,6 +1090,11 @@ function createWatchSupervisor(args, deps = {}) {
1073
1090
  const setTimeoutImpl = deps.setTimeout ?? setTimeout;
1074
1091
  const clearTimeoutImpl = deps.clearTimeout ?? clearTimeout;
1075
1092
  const killTimeoutMs = deps.killTimeoutMs ?? DEFAULT_WATCH_KILL_TIMEOUT_MS;
1093
+ validateTimerDuration(args.watchDelay, "--delay");
1094
+ validateTimerDuration(killTimeoutMs, "killTimeoutMs");
1095
+ if (args.options.shutdownTimeout !== void 0) {
1096
+ validateTimerDuration(args.options.shutdownTimeout, "--shutdown-timeout");
1097
+ }
1076
1098
  const cliPath = process.argv[1];
1077
1099
  const childArgv = buildChildArgs(args);
1078
1100
  let child = null;
@@ -1129,7 +1151,7 @@ function createWatchSupervisor(args, deps = {}) {
1129
1151
  }
1130
1152
  child = nextChild;
1131
1153
  nextChild.once("error", (error) => {
1132
- if (child === nextChild) {
1154
+ if (isChildGone(nextChild) && child === nextChild) {
1133
1155
  child = null;
1134
1156
  }
1135
1157
  fail(toDiagnosticMessage("Watch child process error", error));
@@ -1159,10 +1181,15 @@ function createWatchSupervisor(args, deps = {}) {
1159
1181
  clearKillTimer();
1160
1182
  target.removeListener("exit", onExit);
1161
1183
  target.removeListener("error", onError);
1162
- if (child === target) child = null;
1163
- if (terminatingChild === target) terminatingChild = null;
1164
- if (error) reject(error);
1165
- else resolve();
1184
+ if (error) {
1185
+ if (terminatingChild === target) terminatingChild = null;
1186
+ if (child === target && isChildGone(target)) child = null;
1187
+ reject(error);
1188
+ } else {
1189
+ if (child === target) child = null;
1190
+ if (terminatingChild === target) terminatingChild = null;
1191
+ resolve();
1192
+ }
1166
1193
  };
1167
1194
  const onExit = () => settle();
1168
1195
  const onError = (error) => settle(error);
@@ -1291,7 +1318,7 @@ function createWatchSupervisor(args, deps = {}) {
1291
1318
  };
1292
1319
  }
1293
1320
  function buildChildArgs(args) {
1294
- const result = ["dev", args.appPath];
1321
+ const result = ["dev"];
1295
1322
  if (args.options.port !== void 0) result.push("--port", String(args.options.port));
1296
1323
  if (args.options.host !== void 0) result.push("--host", args.options.host);
1297
1324
  if (args.options.signals === false) result.push("--no-signals");
@@ -1300,17 +1327,23 @@ function buildChildArgs(args) {
1300
1327
  if (args.tsconfigPath !== void 0) result.push("--tsconfig", args.tsconfigPath);
1301
1328
  for (const r of args.require) result.push("--require", r);
1302
1329
  for (const e of args.env) result.push("--env", e);
1330
+ result.push("--", args.appPath);
1303
1331
  return result;
1304
1332
  }
1305
1333
  function runWithWatch(args, deps = {}) {
1306
1334
  const usingInjectedDeps = Object.keys(deps).length > 0;
1307
1335
  const installSignalHandlers = deps.installSignalHandlers ?? !usingInjectedDeps;
1308
1336
  const exitImpl = deps.exit ?? (usingInjectedDeps ? void 0 : (code) => process.exit(code));
1337
+ let exited = false;
1338
+ const exitOnce = (code) => {
1339
+ if (exited) return;
1340
+ exited = true;
1341
+ exitImpl?.(code);
1342
+ };
1309
1343
  const controller = createWatchSupervisor(args, {
1310
1344
  ...deps,
1311
- exit: exitImpl
1345
+ exit: exitOnce
1312
1346
  });
1313
- let shutdownStarted = false;
1314
1347
  const ownedHandlers = [];
1315
1348
  const removeOwnedHandlers = () => {
1316
1349
  for (const [signal, handler] of ownedHandlers.splice(0)) {
@@ -1318,8 +1351,11 @@ function runWithWatch(args, deps = {}) {
1318
1351
  }
1319
1352
  };
1320
1353
  const shutdown = async () => {
1321
- removeOwnedHandlers();
1322
- await controller.shutdown();
1354
+ try {
1355
+ await controller.shutdown();
1356
+ } finally {
1357
+ removeOwnedHandlers();
1358
+ }
1323
1359
  };
1324
1360
  const wrappedController = {
1325
1361
  shutdown,
@@ -1328,14 +1364,38 @@ function runWithWatch(args, deps = {}) {
1328
1364
  isShuttingDown: controller.isShuttingDown
1329
1365
  };
1330
1366
  if (installSignalHandlers) {
1331
- const shutdownAndExit = () => {
1332
- if (shutdownStarted) return;
1333
- shutdownStarted = true;
1334
- void shutdown().then(() => exitImpl?.(0));
1367
+ let signalCount = 0;
1368
+ const shutdownAndExit = (signal) => {
1369
+ signalCount += 1;
1370
+ if (signalCount === 1) {
1371
+ void shutdown().then(
1372
+ () => exitOnce(0),
1373
+ () => exitOnce(1)
1374
+ );
1375
+ return;
1376
+ }
1377
+ if (signalCount === 2) {
1378
+ try {
1379
+ const live = controller.getChild();
1380
+ if (live?.pid) {
1381
+ try {
1382
+ live.kill("SIGKILL");
1383
+ } catch (_error) {
1384
+ void _error;
1385
+ }
1386
+ }
1387
+ } catch (_error) {
1388
+ void _error;
1389
+ }
1390
+ void signal;
1391
+ return;
1392
+ }
1335
1393
  };
1336
- ownedHandlers.push(["SIGINT", shutdownAndExit], ["SIGTERM", shutdownAndExit]);
1337
- process.on("SIGINT", shutdownAndExit);
1338
- process.on("SIGTERM", shutdownAndExit);
1394
+ const onSigint = () => shutdownAndExit("SIGINT");
1395
+ const onSigterm = () => shutdownAndExit("SIGTERM");
1396
+ ownedHandlers.push(["SIGINT", onSigint], ["SIGTERM", onSigterm]);
1397
+ process.on("SIGINT", onSigint);
1398
+ process.on("SIGTERM", onSigterm);
1339
1399
  }
1340
1400
  return wrappedController;
1341
1401
  }
@@ -1373,56 +1433,107 @@ function generateRuntimeEntry(appPath, initPath) {
1373
1433
  function validateOutDirForClean(outDir, clean, appPath, initPath) {
1374
1434
  if (!clean) return;
1375
1435
  const cwd = process.cwd();
1436
+ const canonicalCwd = canonicalizeCwd(cwd);
1376
1437
  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}`);
1438
+ const canonicalOut = canonicalizePhysicalPath(outAbs, "outDir");
1439
+ const root = (0, import_node_path.parse)(canonicalOut).root;
1440
+ if (canonicalOut === root) {
1441
+ throw new Error(`Refusing to clean filesystem root: ${outDir} resolves to ${canonicalOut}`);
1381
1442
  }
1382
- if (normalized === (0, import_node_path.normalize)(cwd)) {
1443
+ if (canonicalOut === canonicalCwd) {
1383
1444
  throw new Error(`Refusing to clean project directory: ${outDir} resolves to cwd ${cwd}`);
1384
1445
  }
1385
- if (cwd !== root && (cwd === normalized || cwd.startsWith(normalized + import_node_path.sep))) {
1446
+ if (canonicalCwd === canonicalOut || canonicalCwd.startsWith(canonicalOut + import_node_path.sep)) {
1386
1447
  throw new Error(
1387
- `Refusing to clean ancestor of project directory: ${outDir} resolves to ${normalized} which contains cwd ${cwd}`
1448
+ `Refusing to clean ancestor of project directory: ${outDir} resolves to ${canonicalOut} which contains cwd ${cwd}`
1388
1449
  );
1389
1450
  }
1390
1451
  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
- }
1452
+ const st = (0, import_node_fs.lstatSync)(outAbs);
1453
+ if (st.isSymbolicLink()) {
1454
+ throw new Error(`Refusing to clean symlinked outDir: ${outDir} resolves to symlink ${outAbs}`);
1396
1455
  }
1397
1456
  } catch (e) {
1398
1457
  if (e.message.startsWith("Refusing to clean")) throw e;
1458
+ const code = e.code;
1459
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
1460
+ throw new Error(
1461
+ `Refusing to clean outDir with unresolvable state: ${outDir} (${outAbs}): ${e.message}`,
1462
+ { cause: e }
1463
+ );
1464
+ }
1399
1465
  }
1400
1466
  const checkOverlap = (inputPath, label) => {
1401
1467
  if (!inputPath) return;
1402
1468
  const inputAbs = (0, import_node_path.resolve)(cwd, inputPath);
1403
- const inputNorm = (0, import_node_path.normalize)(inputAbs);
1404
- if (inputNorm === normalized) {
1469
+ const canonicalInput = canonicalizePhysicalPath(inputAbs, label);
1470
+ if (canonicalInput === canonicalOut) {
1405
1471
  throw new Error(`Refusing to clean outDir that is the same as ${label}: ${outDir} == ${inputPath}`);
1406
1472
  }
1407
- if (inputNorm.startsWith(normalized + import_node_path.sep)) {
1473
+ if (canonicalInput.startsWith(canonicalOut + import_node_path.sep)) {
1408
1474
  throw new Error(`Refusing to clean outDir that contains ${label}: ${outDir} contains ${inputPath}`);
1409
1475
  }
1476
+ if (canonicalOut.startsWith(canonicalInput + import_node_path.sep)) {
1477
+ throw new Error(`Refusing to clean outDir inside ${label}: ${outDir} is inside ${inputPath}`);
1478
+ }
1410
1479
  };
1411
1480
  checkOverlap(appPath, "appPath");
1412
1481
  checkOverlap(initPath, "initPath");
1413
1482
  }
1414
- function createUniqueStagingDir() {
1483
+ function canonicalizePhysicalPath(absPath, label) {
1484
+ const normalizedStart = (0, import_node_path.normalize)(absPath);
1485
+ let current = normalizedStart;
1486
+ const suffixParts = [];
1487
+ while (true) {
1488
+ try {
1489
+ const real = (0, import_node_fs.realpathSync)(current);
1490
+ if (suffixParts.length === 0) return (0, import_node_path.normalize)(real);
1491
+ return (0, import_node_path.normalize)((0, import_node_path.join)(real, ...suffixParts.slice().reverse()));
1492
+ } catch (error) {
1493
+ const code = error.code;
1494
+ if (code === "ENOENT" || code === "ENOTDIR") {
1495
+ const parent = (0, import_node_path.dirname)(current);
1496
+ if (parent === current) {
1497
+ throw new Error(`Refusing to clean ${label}: unable to resolve existing ancestor of ${absPath}`, {
1498
+ cause: error
1499
+ });
1500
+ }
1501
+ suffixParts.push((0, import_node_path.basename)(current));
1502
+ current = parent;
1503
+ continue;
1504
+ }
1505
+ throw new Error(`Refusing to clean ${label}: unable to resolve ${absPath}: ${error.message}`, {
1506
+ cause: error
1507
+ });
1508
+ }
1509
+ }
1510
+ }
1511
+ function canonicalizeCwd(cwd) {
1512
+ try {
1513
+ return (0, import_node_path.normalize)((0, import_node_fs.realpathSync)(cwd));
1514
+ } catch (error) {
1515
+ throw new Error(`Refusing to clean: unable to resolve project directory ${cwd}: ${error.message}`, {
1516
+ cause: error
1517
+ });
1518
+ }
1519
+ }
1520
+ function createUniqueStagingDir(deps = {}) {
1415
1521
  const cwd = process.cwd();
1416
1522
  const prefix = (0, import_node_path.join)(cwd, STAGING_DIR_PREFIX);
1417
- const dir = (0, import_node_fs.mkdtempSync)(prefix);
1523
+ const mkdtemp = deps.mkdtempSyncImpl ?? import_node_fs.mkdtempSync;
1524
+ const lstat = deps.lstatSyncImpl ?? import_node_fs.lstatSync;
1525
+ const rm = deps.rmSyncImpl ?? import_node_fs.rmSync;
1526
+ const dir = mkdtemp(prefix);
1418
1527
  try {
1419
- const st = (0, import_node_fs.lstatSync)(dir);
1528
+ const st = lstat(dir);
1420
1529
  if (st.isSymbolicLink()) {
1421
- (0, import_node_fs.rmSync)(dir, { recursive: true, force: true });
1422
1530
  throw new Error(`Staging directory is a symlink: ${dir}`);
1423
1531
  }
1424
1532
  } catch (e) {
1425
- if (e.message.includes("Staging directory is a symlink")) throw e;
1533
+ try {
1534
+ rm(dir, { recursive: true, force: true });
1535
+ } catch {
1536
+ }
1426
1537
  throw e;
1427
1538
  }
1428
1539
  try {
@@ -1431,41 +1542,58 @@ function createUniqueStagingDir() {
1431
1542
  }
1432
1543
  return dir;
1433
1544
  }
1434
- function writeStagingEntry(dir, content) {
1545
+ function writeStagingEntry(dir, content, deps = {}) {
1435
1546
  const entryPath = (0, import_node_path.join)(dir, "entry.ts");
1547
+ const lstat = deps.lstatSyncImpl ?? import_node_fs.lstatSync;
1548
+ const writeFile = deps.writeFileSyncImpl ?? import_node_fs.writeFileSync;
1549
+ const rm = deps.rmSyncImpl ?? import_node_fs.rmSync;
1436
1550
  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}`);
1551
+ const st = lstat(entryPath);
1552
+ if (st.isSymbolicLink()) {
1553
+ throw new Error(`Refusing to overwrite symlink at staging path: ${entryPath}`);
1443
1554
  }
1555
+ throw new Error(`Staging file already exists: ${entryPath}`);
1444
1556
  } catch (e) {
1445
- if (e.message.startsWith("Refusing to") || e.message.startsWith("Staging file already exists"))
1446
- throw e;
1557
+ const message = e.message;
1558
+ if (message.startsWith("Refusing to") || message.startsWith("Staging file already exists")) throw e;
1559
+ const code = e.code;
1560
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
1561
+ throw new Error(`Refusing to use staging path with unresolvable state: ${entryPath}: ${message}`, {
1562
+ cause: e
1563
+ });
1564
+ }
1447
1565
  }
1448
- (0, import_node_fs.writeFileSync)(entryPath, content, { encoding: "utf8", flag: "wx", mode: 384 });
1566
+ writeFile(entryPath, content, { encoding: "utf8", flag: "wx", mode: 384 });
1449
1567
  try {
1450
- const st = (0, import_node_fs.lstatSync)(entryPath);
1568
+ const st = lstat(entryPath);
1451
1569
  if (st.isSymbolicLink()) {
1452
- (0, import_node_fs.rmSync)(entryPath, { force: true });
1570
+ try {
1571
+ rm(entryPath, { force: true });
1572
+ } catch {
1573
+ }
1453
1574
  throw new Error(`Staging file is a symlink after write: ${entryPath}`);
1454
1575
  }
1455
1576
  } catch (e) {
1456
1577
  if (e.message.includes("Staging file is a symlink")) throw e;
1578
+ try {
1579
+ rm(entryPath, { force: true });
1580
+ } catch {
1581
+ }
1582
+ throw new Error(`Refusing to use staging file with unresolvable state: ${entryPath}: ${e.message}`, {
1583
+ cause: e
1584
+ });
1457
1585
  }
1458
1586
  return entryPath;
1459
1587
  }
1460
- async function buildBundleFromEntryContent(args) {
1588
+ async function buildBundleFromEntryContent(args, deps = {}) {
1461
1589
  validateOutDirForClean(args.outDir, args.clean);
1462
- const tsupModule = await import("tsup");
1463
- const { build } = tsupModule;
1464
- const stagingDir = createUniqueStagingDir();
1465
- const tempEntryPath = writeStagingEntry(stagingDir, args.entryContent);
1466
- const absOutDir = (0, import_node_path.resolve)(process.cwd(), args.outDir);
1590
+ const buildImpl = deps.buildImpl ?? (await import("tsup")).build;
1591
+ const rm = deps.rmSyncImpl ?? import_node_fs.rmSync;
1592
+ const stagingDir = createUniqueStagingDir(deps);
1467
1593
  try {
1468
- await build({
1594
+ const tempEntryPath = writeStagingEntry(stagingDir, args.entryContent, deps);
1595
+ const absOutDir = (0, import_node_path.resolve)(process.cwd(), args.outDir);
1596
+ await buildImpl({
1469
1597
  config: false,
1470
1598
  entry: { [args.outName]: tempEntryPath },
1471
1599
  tsconfig: args.tsconfigPath,
@@ -1479,7 +1607,10 @@ async function buildBundleFromEntryContent(args) {
1479
1607
  splitting: false
1480
1608
  });
1481
1609
  } finally {
1482
- (0, import_node_fs.rmSync)(stagingDir, { recursive: true, force: true });
1610
+ try {
1611
+ rm(stagingDir, { recursive: true, force: true });
1612
+ } catch {
1613
+ }
1483
1614
  }
1484
1615
  }
1485
1616
  async function buildRuntime(args) {
@@ -1578,11 +1709,14 @@ function collectBody(req, maxBytes) {
1578
1709
  if (finished) return;
1579
1710
  finished = true;
1580
1711
  cleanup();
1712
+ let body;
1581
1713
  try {
1582
- resolve(Buffer.concat(chunks, total));
1714
+ body = Buffer.concat(chunks, total);
1583
1715
  } catch (e) {
1584
- fail(e);
1716
+ reject(e);
1717
+ return;
1585
1718
  }
1719
+ resolve(body);
1586
1720
  };
1587
1721
  const onError = (err) => {
1588
1722
  const e = err;
@@ -1606,13 +1740,13 @@ function collectBody(req, maxBytes) {
1606
1740
  }
1607
1741
  });
1608
1742
  }
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);
1743
+ function toServerlessEvent(method, url, headers, body, rawHeaders) {
1744
+ const { path, search } = splitRequestTarget(url);
1745
+ const { queryStringParameters, multiValueQueryStringParameters } = parseAwsRestQuery(search);
1746
+ const { singleValueHeaders, multiValueHeaders } = buildAwsRestHeaders(headers, rawHeaders);
1613
1747
  return {
1614
1748
  httpMethod: method,
1615
- path: parsedUrl.pathname,
1749
+ path,
1616
1750
  headers: singleValueHeaders,
1617
1751
  multiValueHeaders,
1618
1752
  queryStringParameters,
@@ -1627,12 +1761,49 @@ function toServerlessEvent(method, url, headers, body) {
1627
1761
  }
1628
1762
  };
1629
1763
  }
1764
+ function splitRequestTarget(target) {
1765
+ if (target === "") {
1766
+ return { path: "/", search: "" };
1767
+ }
1768
+ let remainder = target;
1769
+ const absoluteMatch = remainder.match(/^([A-Za-z][A-Za-z0-9+.-]*):\/\//);
1770
+ if (absoluteMatch) {
1771
+ const afterScheme = remainder.slice(absoluteMatch[0].length);
1772
+ const boundary = afterScheme.search(/[/?#]/);
1773
+ if (boundary === -1) {
1774
+ return { path: "/", search: "" };
1775
+ }
1776
+ remainder = afterScheme.slice(boundary);
1777
+ if (remainder.startsWith("?") || remainder.startsWith("#")) {
1778
+ remainder = `/${remainder}`;
1779
+ }
1780
+ } else if (remainder === "*" || remainder.startsWith("*?") || remainder.startsWith("*#")) {
1781
+ remainder = remainder.slice(1);
1782
+ if (remainder === "") {
1783
+ return { path: "*", search: "" };
1784
+ }
1785
+ const hashIndex2 = remainder.indexOf("#");
1786
+ const withoutFragment2 = hashIndex2 === -1 ? remainder : remainder.slice(0, hashIndex2);
1787
+ return { path: "*", search: withoutFragment2 };
1788
+ } else if (!remainder.startsWith("/")) {
1789
+ throw new Error(
1790
+ `Unsupported request target: ${JSON.stringify(target)}. Expected an origin-form path ("/path?query"), an absolute-form URI ("scheme://authority/path?query"), or "*"`
1791
+ );
1792
+ }
1793
+ const hashIndex = remainder.indexOf("#");
1794
+ const withoutFragment = hashIndex === -1 ? remainder : remainder.slice(0, hashIndex);
1795
+ const queryIndex = withoutFragment.indexOf("?");
1796
+ if (queryIndex === -1) {
1797
+ return { path: withoutFragment, search: "" };
1798
+ }
1799
+ return { path: withoutFragment.slice(0, queryIndex), search: withoutFragment.slice(queryIndex) };
1800
+ }
1630
1801
  function parseAwsRestQuery(search) {
1631
1802
  if (search === "" || search === "?") {
1632
1803
  return { queryStringParameters: null, multiValueQueryStringParameters: null };
1633
1804
  }
1634
- const single = {};
1635
- const multi = {};
1805
+ const single = /* @__PURE__ */ Object.create(null);
1806
+ const multi = /* @__PURE__ */ Object.create(null);
1636
1807
  const query = search.startsWith("?") ? search.slice(1) : search;
1637
1808
  for (const pair of query.split("&")) {
1638
1809
  if (pair === "") continue;
@@ -1642,7 +1813,11 @@ function parseAwsRestQuery(search) {
1642
1813
  const key = decodeQueryComponent(rawKey);
1643
1814
  const value = decodeQueryComponent(rawValue);
1644
1815
  single[key] = value;
1645
- (multi[key] ??= []).push(value);
1816
+ if (Object.prototype.hasOwnProperty.call(multi, key)) {
1817
+ multi[key].push(value);
1818
+ } else {
1819
+ multi[key] = [value];
1820
+ }
1646
1821
  }
1647
1822
  return {
1648
1823
  queryStringParameters: Object.keys(single).length > 0 ? single : null,
@@ -1658,8 +1833,8 @@ function decodeQueryComponent(value) {
1658
1833
  }
1659
1834
  }
1660
1835
  function normalizeAwsRestHeaders(headers) {
1661
- const singleValueHeaders = {};
1662
- const multiValueHeaders = {};
1836
+ const singleValueHeaders = /* @__PURE__ */ Object.create(null);
1837
+ const multiValueHeaders = /* @__PURE__ */ Object.create(null);
1663
1838
  for (const [key, value] of Object.entries(headers)) {
1664
1839
  if (value === void 0) continue;
1665
1840
  const values = Array.isArray(value) ? value.map(String) : [String(value)];
@@ -1668,23 +1843,92 @@ function normalizeAwsRestHeaders(headers) {
1668
1843
  }
1669
1844
  return { singleValueHeaders, multiValueHeaders };
1670
1845
  }
1846
+ function buildAwsRestHeaders(headers, rawHeaders) {
1847
+ const fromRaw = headersFromRawHeadersList(rawHeaders);
1848
+ if (fromRaw) return fromRaw;
1849
+ if (isPlainRecord(rawHeaders)) {
1850
+ return normalizeAwsRestHeaders(rawHeaders);
1851
+ }
1852
+ return normalizeAwsRestHeaders(headers);
1853
+ }
1854
+ function headersFromRawHeadersList(rawHeaders) {
1855
+ if (!Array.isArray(rawHeaders)) return null;
1856
+ if (rawHeaders.length % 2 !== 0) return null;
1857
+ for (const entry of rawHeaders) {
1858
+ if (typeof entry !== "string") return null;
1859
+ }
1860
+ const singleValueHeaders = /* @__PURE__ */ Object.create(null);
1861
+ const multiValueHeaders = /* @__PURE__ */ Object.create(null);
1862
+ for (let index = 0; index < rawHeaders.length; index += 2) {
1863
+ const name = rawHeaders[index].toLowerCase();
1864
+ const value = rawHeaders[index + 1];
1865
+ if (Object.prototype.hasOwnProperty.call(multiValueHeaders, name)) {
1866
+ multiValueHeaders[name].push(value);
1867
+ } else {
1868
+ multiValueHeaders[name] = [value];
1869
+ }
1870
+ }
1871
+ for (const key of Object.keys(multiValueHeaders)) {
1872
+ singleValueHeaders[key] = multiValueHeaders[key].join(", ");
1873
+ }
1874
+ return { singleValueHeaders, multiValueHeaders };
1875
+ }
1671
1876
  function applyServerlessResult(result, res) {
1672
1877
  const response = validateServerlessResult(result);
1673
1878
  res.status(response.statusCode);
1674
- for (const [key, value] of Object.entries(response.headers)) {
1675
- res.setHeader(key, value);
1879
+ let baseline;
1880
+ try {
1881
+ baseline = new Set(Object.keys(res.getHeaders()).map((name) => name.toLowerCase()));
1882
+ } catch (_e) {
1883
+ void _e;
1884
+ baseline = void 0;
1676
1885
  }
1677
- for (const [key, values] of Object.entries(response.multiValueHeaders)) {
1678
- if (key.toLowerCase() === "set-cookie") {
1679
- res.setHeader(key, values);
1886
+ try {
1887
+ for (const [key, value] of Object.entries(response.headers)) {
1888
+ res.setHeader(key, value);
1889
+ }
1890
+ for (const [key, values] of Object.entries(response.multiValueHeaders)) {
1891
+ if (key.toLowerCase() === "set-cookie") {
1892
+ res.setHeader(key, values);
1893
+ } else {
1894
+ res.setHeader(key, values.join(","));
1895
+ }
1896
+ }
1897
+ if (response.isBase64Encoded) {
1898
+ res.end(response.decodedBody);
1680
1899
  } else {
1681
- res.setHeader(key, values.join(","));
1900
+ res.end(response.body);
1682
1901
  }
1683
- }
1684
- if (response.isBase64Encoded) {
1685
- res.end(response.decodedBody);
1686
- } else {
1687
- res.end(response.body);
1902
+ } catch (error) {
1903
+ if (!res.headersSent) {
1904
+ try {
1905
+ if (baseline !== void 0) {
1906
+ for (const name of Object.keys(res.getHeaders())) {
1907
+ if (!baseline.has(name.toLowerCase())) {
1908
+ res.removeHeader(name);
1909
+ }
1910
+ }
1911
+ } else {
1912
+ for (const [key] of Object.entries(response.headers)) {
1913
+ try {
1914
+ res.removeHeader(key);
1915
+ } catch (_e) {
1916
+ void _e;
1917
+ }
1918
+ }
1919
+ for (const [key] of Object.entries(response.multiValueHeaders)) {
1920
+ try {
1921
+ res.removeHeader(key);
1922
+ } catch (_e) {
1923
+ void _e;
1924
+ }
1925
+ }
1926
+ }
1927
+ } catch (_e) {
1928
+ void _e;
1929
+ }
1930
+ }
1931
+ throw error;
1688
1932
  }
1689
1933
  }
1690
1934
  function validateServerlessResult(result) {
@@ -1736,7 +1980,7 @@ function validateSingleValueHeaders(value, name) {
1736
1980
  if (!isPlainRecord(value)) {
1737
1981
  throw new Error(`Invalid serverless result ${name}: expected an object of string header values.`);
1738
1982
  }
1739
- const headers = {};
1983
+ const headers = /* @__PURE__ */ Object.create(null);
1740
1984
  for (const [key, headerValue] of Object.entries(value)) {
1741
1985
  if (headerValue === void 0) continue;
1742
1986
  if (typeof headerValue !== "string") {
@@ -1752,12 +1996,16 @@ function validateMultiValueHeaders(value, name) {
1752
1996
  if (!isPlainRecord(value)) {
1753
1997
  throw new Error(`Invalid serverless result ${name}: expected an object of string-array header values.`);
1754
1998
  }
1755
- const headers = {};
1999
+ const headers = /* @__PURE__ */ Object.create(null);
1756
2000
  for (const [key, headerValue] of Object.entries(value)) {
1757
2001
  if (headerValue === void 0) continue;
1758
2002
  if (!Array.isArray(headerValue) || headerValue.some((entry) => typeof entry !== "string")) {
1759
2003
  throw new Error(`Invalid serverless result ${name}.${key}: expected an array of string header values.`);
1760
2004
  }
2005
+ if (headerValue.length === 0) {
2006
+ validateServerlessHeaderName(key, `${name}.${key}`);
2007
+ continue;
2008
+ }
1761
2009
  for (const entry of headerValue) {
1762
2010
  validateServerlessHeader(key, entry, `${name}.${key}`);
1763
2011
  }
@@ -1765,6 +2013,13 @@ function validateMultiValueHeaders(value, name) {
1765
2013
  }
1766
2014
  return headers;
1767
2015
  }
2016
+ function validateServerlessHeaderName(key, label) {
2017
+ try {
2018
+ (0, import_node_http2.validateHeaderName)(key);
2019
+ } catch (error) {
2020
+ throw new Error(`Invalid serverless result header ${label}: ${error.message}`, { cause: error });
2021
+ }
2022
+ }
1768
2023
  function validateServerlessHeader(key, value, label) {
1769
2024
  try {
1770
2025
  (0, import_node_http2.validateHeaderName)(key);
@@ -1823,18 +2078,42 @@ function createServerlessAdapterApp(handler, options = {}) {
1823
2078
  }
1824
2079
  let result;
1825
2080
  try {
1826
- const event = toServerlessEvent(req.method, req.url, req.headers, body);
2081
+ const raw = req.rawHeaders ?? req.headersDistinct;
2082
+ const event = toServerlessEvent(req.method, req.url, req.headers, body, raw);
1827
2083
  result = await handler(event, {});
1828
2084
  } catch (e) {
1829
2085
  console.error("Serverless adapter error:", e);
1830
2086
  if (!res.headersSent && !res.writableEnded) res.status(500).end("Internal server error");
1831
2087
  return;
1832
2088
  }
2089
+ let baselineHeaders;
2090
+ try {
2091
+ baselineHeaders = new Set(Object.keys(res.getHeaders()).map((name) => name.toLowerCase()));
2092
+ } catch (_e) {
2093
+ void _e;
2094
+ baselineHeaders = void 0;
2095
+ }
1833
2096
  try {
1834
2097
  applyServerlessResult(result, res);
1835
2098
  } catch (e) {
1836
2099
  console.error("Invalid serverless handler result:", e);
1837
- if (!res.headersSent && !res.writableEnded) res.status(500).end("Internal server error");
2100
+ if (!res.headersSent && !res.writableEnded) {
2101
+ try {
2102
+ for (const name of Object.keys(res.getHeaders())) {
2103
+ const lower = name.toLowerCase();
2104
+ if (lower === "content-length") continue;
2105
+ if (baselineHeaders !== void 0 && baselineHeaders.has(lower)) continue;
2106
+ try {
2107
+ res.removeHeader(name);
2108
+ } catch (_e) {
2109
+ void _e;
2110
+ }
2111
+ }
2112
+ } catch (_e) {
2113
+ void _e;
2114
+ }
2115
+ res.status(500).end("Internal server error");
2116
+ }
1838
2117
  }
1839
2118
  });
1840
2119
  },
@@ -1880,7 +2159,7 @@ async function loadHandler(handlerPath) {
1880
2159
  }
1881
2160
  return exported;
1882
2161
  }
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;
2162
+ 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, DEFAULT_WATCH_KILL_TIMEOUT_MS, TEMP_BUILD_ENTRY_FILENAME, TEMP_SERVERLESS_ENTRY_FILENAME, STAGING_DIR_PREFIX, DEFAULT_ADAPTER_MAX_BODY_BYTES;
1884
2163
  var init_cli_utils = __esm({
1885
2164
  "src/cli-utils.ts"() {
1886
2165
  "use strict";
@@ -1895,9 +2174,6 @@ var init_cli_utils = __esm({
1895
2174
  CLI_VERSION = resolveCliVersion();
1896
2175
  DEFAULT_WATCH_EXTENSIONS = ["ts", "js", "mjs", "cjs", "json"];
1897
2176
  DEFAULT_WATCH_DELAY = 500;
1898
- moduleRequire = (0, import_node_module.createRequire)(
1899
- (0, import_node_url.pathToFileURL)((0, import_node_path.resolve)(process.cwd(), "__wtt_runtime_preload__.js"))
1900
- );
1901
2177
  DEFAULT_WATCH_KILL_TIMEOUT_MS = 5e3;
1902
2178
  TEMP_BUILD_ENTRY_FILENAME = ".express-runtime-build-entry.ts";
1903
2179
  TEMP_SERVERLESS_ENTRY_FILENAME = ".express-runtime-build-serverless-entry.ts";
@@ -1960,7 +2236,7 @@ async function runExpressDevCommand(args) {
1960
2236
  await runDevCommand(args, {
1961
2237
  load: loadApp,
1962
2238
  start: (app, options) => {
1963
- startLocalServer(app, options);
2239
+ return startLocalServer(app, options);
1964
2240
  }
1965
2241
  });
1966
2242
  }