@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.
@@ -1,14 +1,17 @@
1
1
  import {
2
2
  MAX_INTEGER_OPTION_VALUE,
3
+ MAX_TIMER_DURATION_MS,
3
4
  createExpressApp,
4
5
  parsePortValue,
5
- validateFiniteInteger
6
- } from "./chunk-VPFBKM2K.mjs";
6
+ validateFiniteInteger,
7
+ validateTimerDuration
8
+ } from "./chunk-QNRHWPTO.mjs";
7
9
 
8
10
  // src/cli-utils.ts
9
11
  import { pathToFileURL } from "url";
10
12
  import {
11
13
  dirname,
14
+ basename,
12
15
  resolve as pathResolve,
13
16
  extname,
14
17
  join as pathJoin,
@@ -75,6 +78,12 @@ function parseIntegerFlag(raw, name, min = 0, max = MAX_INTEGER_OPTION_VALUE) {
75
78
  }
76
79
  return validateFiniteInteger(Number(raw), { name, min, max });
77
80
  }
81
+ function parseTimerFlag(raw, name) {
82
+ if (!/^(0|[1-9]\d*)$/.test(raw)) {
83
+ throw new Error(`Invalid ${name}: ${raw}. Must be a finite integer in 0..${MAX_TIMER_DURATION_MS}.`);
84
+ }
85
+ return validateTimerDuration(Number(raw), name);
86
+ }
78
87
  function parsePortFlag(raw, name = "--port") {
79
88
  try {
80
89
  return parsePortValue(raw, name);
@@ -114,13 +123,13 @@ Dev options:
114
123
  --port <number> Port or named pipe (default: process.env.PORT or 8080)
115
124
  --host <hostname> Hostname to bind (default: process.env.HOST or 0.0.0.0)
116
125
  --no-signals Disable SIGINT/SIGTERM handler registration
117
- --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000)
126
+ --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000; 0..2147483647)
118
127
  --require <module> Module(s) to preload before app load (repeatable)
119
128
  --env <path> Env file(s) to load (repeatable; existing env vars are not overridden)
120
129
  --tsconfig <path> Tsconfig used by config-aware consumers for TS path resolution
121
130
  --watch <paths> Comma-separated paths to watch for restart (repeatable; dev only)
122
131
  --ext <extensions> Comma-separated extensions to watch (default: ts,js,mjs,cjs,json)
123
- --delay <ms> Debounce ms before restarting on change (default: 500)
132
+ --delay <ms> Debounce ms before restarting on change (default: 500; 0..2147483647)
124
133
 
125
134
  Build options:
126
135
  --init <path> Init hook module (default export, async function)
@@ -129,14 +138,14 @@ Build options:
129
138
  --out-name <name> Output filename without extension (default: app)
130
139
  --format <cjs|esm> Output format (default: cjs)
131
140
  --target <target> Compilation target (default: node22)
132
- --external <pkg> Mark package as external (repeatable; express always external)
141
+ --external <pkg> Mark package as external (repeatable; express and @web-ts-toolkit/express-runtime always external)
133
142
  --no-clean Don't clean the output directory before building
134
143
 
135
144
  Start options:
136
145
  --port <number> Port or named pipe (default: process.env.PORT or 8080)
137
146
  --host <hostname> Hostname to bind (default: process.env.HOST or 0.0.0.0)
138
147
  --no-signals Disable SIGINT/SIGTERM handler registration
139
- --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000)
148
+ --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000; 0..2147483647)
140
149
  --require <module> Module(s) to preload before app load (repeatable)
141
150
  --env <path> Env file(s) to load (repeatable; existing env vars are not overridden)
142
151
 
@@ -147,14 +156,14 @@ Build-serverless options:
147
156
  --out-name <name> Output filename without extension (default: handler)
148
157
  --format <cjs|esm> Output format (default: cjs)
149
158
  --target <target> Compilation target (default: node22)
150
- --external <pkg> Mark package as external (repeatable; express always external)
159
+ --external <pkg> Mark package as external (repeatable; express and @web-ts-toolkit/express-runtime always external)
151
160
  --no-clean Don't clean the output directory before building
152
161
 
153
162
  Start-serverless options:
154
163
  --port <number> Port or named pipe (default: process.env.PORT or 8080)
155
164
  --host <hostname> Hostname to bind (default: process.env.HOST or 0.0.0.0)
156
165
  --no-signals Disable SIGINT/SIGTERM handler registration
157
- --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000)
166
+ --shutdown-timeout <ms> Max ms to wait for in-flight requests (default: 5000; 0..2147483647)
158
167
  --max-body-bytes <bytes> Max request body bytes for adapter (default: 1048576; 0 disallows bodies)
159
168
  --require <module> Module(s) to preload before handler load (repeatable)
160
169
  --env <path> Env file(s) to load (repeatable; existing env vars are not overridden)
@@ -184,7 +193,7 @@ Notes:
184
193
  - --watch forks one child process running the same CLI without --watch. File changes
185
194
  are serialized into one restart at a time: SIGTERM, SIGKILL after 5000 ms if needed,
186
195
  then respawn after the debounce delay. Shutdown closes owned watchers and signal handlers.
187
- - In build/build-serverless mode, express is always external. Add more externals with --external.
196
+ - In build/build-serverless mode, express and @web-ts-toolkit/express-runtime are always external. Add more externals with --external.
188
197
  - In start mode, the bundled app file must default-export an Express app (or export it as "app").
189
198
  If the bundle exports "init", it runs before the server starts listening.
190
199
  - In start-serverless mode, the bundled handler file must be a JS/CJS module whose
@@ -270,12 +279,12 @@ function parseDevArgs(argv) {
270
279
  continue;
271
280
  }
272
281
  if (arg === "--shutdown-timeout") {
273
- options.shutdownTimeout = parseIntegerFlag(readValue(argv, index, arg), "--shutdown-timeout");
282
+ options.shutdownTimeout = parseTimerFlag(readValue(argv, index, arg), "--shutdown-timeout");
274
283
  index += 1;
275
284
  continue;
276
285
  }
277
286
  if (arg.startsWith("--shutdown-timeout=")) {
278
- options.shutdownTimeout = parseIntegerFlag(
287
+ options.shutdownTimeout = parseTimerFlag(
279
288
  readInlineValue(arg, "--shutdown-timeout=", "--shutdown-timeout"),
280
289
  "--shutdown-timeout"
281
290
  );
@@ -325,12 +334,12 @@ function parseDevArgs(argv) {
325
334
  continue;
326
335
  }
327
336
  if (arg === "--delay") {
328
- watchDelay = parseIntegerFlag(readValue(argv, index, arg), "--delay");
337
+ watchDelay = parseTimerFlag(readValue(argv, index, arg), "--delay");
329
338
  index += 1;
330
339
  continue;
331
340
  }
332
341
  if (arg.startsWith("--delay=")) {
333
- watchDelay = parseIntegerFlag(readInlineValue(arg, "--delay=", "--delay"), "--delay");
342
+ watchDelay = parseTimerFlag(readInlineValue(arg, "--delay=", "--delay"), "--delay");
334
343
  continue;
335
344
  }
336
345
  if (!arg.startsWith("--")) {
@@ -625,12 +634,12 @@ function loadEnvFiles(paths) {
625
634
  }
626
635
  }
627
636
  }
628
- var moduleRequire = createRequire(
629
- pathToFileURL(pathResolve(process.cwd(), "__wtt_runtime_preload__.js"))
630
- );
631
637
  async function preloadModules(modules) {
638
+ const invocationRequire = createRequire(
639
+ pathToFileURL(pathResolve(process.cwd(), "__wtt_runtime_preload__.js"))
640
+ );
632
641
  for (const mod of modules) {
633
- moduleRequire(mod);
642
+ invocationRequire(mod);
634
643
  }
635
644
  }
636
645
  var DEFAULT_WATCH_KILL_TIMEOUT_MS = 5e3;
@@ -638,6 +647,12 @@ function toDiagnosticMessage(prefix, error) {
638
647
  const message = error instanceof Error ? error.message : String(error);
639
648
  return `${prefix}: ${message}`;
640
649
  }
650
+ function isChildGone(proc) {
651
+ const exitCode = proc.exitCode;
652
+ const signalCode = proc.signalCode;
653
+ if (exitCode != null || signalCode != null) return true;
654
+ return proc.pid === void 0;
655
+ }
641
656
  function createWatchSupervisor(args, deps = {}) {
642
657
  const forkImpl = deps.fork ?? fork;
643
658
  const watchImpl = deps.watch ?? watch;
@@ -646,6 +661,11 @@ function createWatchSupervisor(args, deps = {}) {
646
661
  const setTimeoutImpl = deps.setTimeout ?? setTimeout;
647
662
  const clearTimeoutImpl = deps.clearTimeout ?? clearTimeout;
648
663
  const killTimeoutMs = deps.killTimeoutMs ?? DEFAULT_WATCH_KILL_TIMEOUT_MS;
664
+ validateTimerDuration(args.watchDelay, "--delay");
665
+ validateTimerDuration(killTimeoutMs, "killTimeoutMs");
666
+ if (args.options.shutdownTimeout !== void 0) {
667
+ validateTimerDuration(args.options.shutdownTimeout, "--shutdown-timeout");
668
+ }
649
669
  const cliPath = process.argv[1];
650
670
  const childArgv = buildChildArgs(args);
651
671
  let child = null;
@@ -702,7 +722,7 @@ function createWatchSupervisor(args, deps = {}) {
702
722
  }
703
723
  child = nextChild;
704
724
  nextChild.once("error", (error) => {
705
- if (child === nextChild) {
725
+ if (isChildGone(nextChild) && child === nextChild) {
706
726
  child = null;
707
727
  }
708
728
  fail(toDiagnosticMessage("Watch child process error", error));
@@ -732,10 +752,15 @@ function createWatchSupervisor(args, deps = {}) {
732
752
  clearKillTimer();
733
753
  target.removeListener("exit", onExit);
734
754
  target.removeListener("error", onError);
735
- if (child === target) child = null;
736
- if (terminatingChild === target) terminatingChild = null;
737
- if (error) reject(error);
738
- else resolve();
755
+ if (error) {
756
+ if (terminatingChild === target) terminatingChild = null;
757
+ if (child === target && isChildGone(target)) child = null;
758
+ reject(error);
759
+ } else {
760
+ if (child === target) child = null;
761
+ if (terminatingChild === target) terminatingChild = null;
762
+ resolve();
763
+ }
739
764
  };
740
765
  const onExit = () => settle();
741
766
  const onError = (error) => settle(error);
@@ -864,7 +889,7 @@ function createWatchSupervisor(args, deps = {}) {
864
889
  };
865
890
  }
866
891
  function buildChildArgs(args) {
867
- const result = ["dev", args.appPath];
892
+ const result = ["dev"];
868
893
  if (args.options.port !== void 0) result.push("--port", String(args.options.port));
869
894
  if (args.options.host !== void 0) result.push("--host", args.options.host);
870
895
  if (args.options.signals === false) result.push("--no-signals");
@@ -873,17 +898,23 @@ function buildChildArgs(args) {
873
898
  if (args.tsconfigPath !== void 0) result.push("--tsconfig", args.tsconfigPath);
874
899
  for (const r of args.require) result.push("--require", r);
875
900
  for (const e of args.env) result.push("--env", e);
901
+ result.push("--", args.appPath);
876
902
  return result;
877
903
  }
878
904
  function runWithWatch(args, deps = {}) {
879
905
  const usingInjectedDeps = Object.keys(deps).length > 0;
880
906
  const installSignalHandlers = deps.installSignalHandlers ?? !usingInjectedDeps;
881
907
  const exitImpl = deps.exit ?? (usingInjectedDeps ? void 0 : (code) => process.exit(code));
908
+ let exited = false;
909
+ const exitOnce = (code) => {
910
+ if (exited) return;
911
+ exited = true;
912
+ exitImpl?.(code);
913
+ };
882
914
  const controller = createWatchSupervisor(args, {
883
915
  ...deps,
884
- exit: exitImpl
916
+ exit: exitOnce
885
917
  });
886
- let shutdownStarted = false;
887
918
  const ownedHandlers = [];
888
919
  const removeOwnedHandlers = () => {
889
920
  for (const [signal, handler] of ownedHandlers.splice(0)) {
@@ -891,8 +922,11 @@ function runWithWatch(args, deps = {}) {
891
922
  }
892
923
  };
893
924
  const shutdown = async () => {
894
- removeOwnedHandlers();
895
- await controller.shutdown();
925
+ try {
926
+ await controller.shutdown();
927
+ } finally {
928
+ removeOwnedHandlers();
929
+ }
896
930
  };
897
931
  const wrappedController = {
898
932
  shutdown,
@@ -901,14 +935,38 @@ function runWithWatch(args, deps = {}) {
901
935
  isShuttingDown: controller.isShuttingDown
902
936
  };
903
937
  if (installSignalHandlers) {
904
- const shutdownAndExit = () => {
905
- if (shutdownStarted) return;
906
- shutdownStarted = true;
907
- void shutdown().then(() => exitImpl?.(0));
938
+ let signalCount = 0;
939
+ const shutdownAndExit = (signal) => {
940
+ signalCount += 1;
941
+ if (signalCount === 1) {
942
+ void shutdown().then(
943
+ () => exitOnce(0),
944
+ () => exitOnce(1)
945
+ );
946
+ return;
947
+ }
948
+ if (signalCount === 2) {
949
+ try {
950
+ const live = controller.getChild();
951
+ if (live?.pid) {
952
+ try {
953
+ live.kill("SIGKILL");
954
+ } catch (_error) {
955
+ void _error;
956
+ }
957
+ }
958
+ } catch (_error) {
959
+ void _error;
960
+ }
961
+ void signal;
962
+ return;
963
+ }
908
964
  };
909
- ownedHandlers.push(["SIGINT", shutdownAndExit], ["SIGTERM", shutdownAndExit]);
910
- process.on("SIGINT", shutdownAndExit);
911
- process.on("SIGTERM", shutdownAndExit);
965
+ const onSigint = () => shutdownAndExit("SIGINT");
966
+ const onSigterm = () => shutdownAndExit("SIGTERM");
967
+ ownedHandlers.push(["SIGINT", onSigint], ["SIGTERM", onSigterm]);
968
+ process.on("SIGINT", onSigint);
969
+ process.on("SIGTERM", onSigterm);
912
970
  }
913
971
  return wrappedController;
914
972
  }
@@ -949,56 +1007,107 @@ function generateRuntimeEntry(appPath, initPath) {
949
1007
  function validateOutDirForClean(outDir, clean, appPath, initPath) {
950
1008
  if (!clean) return;
951
1009
  const cwd = process.cwd();
1010
+ const canonicalCwd = canonicalizeCwd(cwd);
952
1011
  const outAbs = pathResolve(cwd, outDir);
953
- const normalized = pathNormalize(outAbs);
954
- const root = pathParse(normalized).root;
955
- if (normalized === root) {
956
- throw new Error(`Refusing to clean filesystem root: ${outDir} resolves to ${normalized}`);
1012
+ const canonicalOut = canonicalizePhysicalPath(outAbs, "outDir");
1013
+ const root = pathParse(canonicalOut).root;
1014
+ if (canonicalOut === root) {
1015
+ throw new Error(`Refusing to clean filesystem root: ${outDir} resolves to ${canonicalOut}`);
957
1016
  }
958
- if (normalized === pathNormalize(cwd)) {
1017
+ if (canonicalOut === canonicalCwd) {
959
1018
  throw new Error(`Refusing to clean project directory: ${outDir} resolves to cwd ${cwd}`);
960
1019
  }
961
- if (cwd !== root && (cwd === normalized || cwd.startsWith(normalized + pathSep))) {
1020
+ if (canonicalCwd === canonicalOut || canonicalCwd.startsWith(canonicalOut + pathSep)) {
962
1021
  throw new Error(
963
- `Refusing to clean ancestor of project directory: ${outDir} resolves to ${normalized} which contains cwd ${cwd}`
1022
+ `Refusing to clean ancestor of project directory: ${outDir} resolves to ${canonicalOut} which contains cwd ${cwd}`
964
1023
  );
965
1024
  }
966
1025
  try {
967
- if (existsSync(outAbs)) {
968
- const st = lstatSync(outAbs);
969
- if (st.isSymbolicLink()) {
970
- throw new Error(`Refusing to clean symlinked outDir: ${outDir} resolves to symlink ${outAbs}`);
971
- }
1026
+ const st = lstatSync(outAbs);
1027
+ if (st.isSymbolicLink()) {
1028
+ throw new Error(`Refusing to clean symlinked outDir: ${outDir} resolves to symlink ${outAbs}`);
972
1029
  }
973
1030
  } catch (e) {
974
1031
  if (e.message.startsWith("Refusing to clean")) throw e;
1032
+ const code = e.code;
1033
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
1034
+ throw new Error(
1035
+ `Refusing to clean outDir with unresolvable state: ${outDir} (${outAbs}): ${e.message}`,
1036
+ { cause: e }
1037
+ );
1038
+ }
975
1039
  }
976
1040
  const checkOverlap = (inputPath, label) => {
977
1041
  if (!inputPath) return;
978
1042
  const inputAbs = pathResolve(cwd, inputPath);
979
- const inputNorm = pathNormalize(inputAbs);
980
- if (inputNorm === normalized) {
1043
+ const canonicalInput = canonicalizePhysicalPath(inputAbs, label);
1044
+ if (canonicalInput === canonicalOut) {
981
1045
  throw new Error(`Refusing to clean outDir that is the same as ${label}: ${outDir} == ${inputPath}`);
982
1046
  }
983
- if (inputNorm.startsWith(normalized + pathSep)) {
1047
+ if (canonicalInput.startsWith(canonicalOut + pathSep)) {
984
1048
  throw new Error(`Refusing to clean outDir that contains ${label}: ${outDir} contains ${inputPath}`);
985
1049
  }
1050
+ if (canonicalOut.startsWith(canonicalInput + pathSep)) {
1051
+ throw new Error(`Refusing to clean outDir inside ${label}: ${outDir} is inside ${inputPath}`);
1052
+ }
986
1053
  };
987
1054
  checkOverlap(appPath, "appPath");
988
1055
  checkOverlap(initPath, "initPath");
989
1056
  }
990
- function createUniqueStagingDir() {
1057
+ function canonicalizePhysicalPath(absPath, label) {
1058
+ const normalizedStart = pathNormalize(absPath);
1059
+ let current = normalizedStart;
1060
+ const suffixParts = [];
1061
+ while (true) {
1062
+ try {
1063
+ const real = realpathSync(current);
1064
+ if (suffixParts.length === 0) return pathNormalize(real);
1065
+ return pathNormalize(pathJoin(real, ...suffixParts.slice().reverse()));
1066
+ } catch (error) {
1067
+ const code = error.code;
1068
+ if (code === "ENOENT" || code === "ENOTDIR") {
1069
+ const parent = dirname(current);
1070
+ if (parent === current) {
1071
+ throw new Error(`Refusing to clean ${label}: unable to resolve existing ancestor of ${absPath}`, {
1072
+ cause: error
1073
+ });
1074
+ }
1075
+ suffixParts.push(basename(current));
1076
+ current = parent;
1077
+ continue;
1078
+ }
1079
+ throw new Error(`Refusing to clean ${label}: unable to resolve ${absPath}: ${error.message}`, {
1080
+ cause: error
1081
+ });
1082
+ }
1083
+ }
1084
+ }
1085
+ function canonicalizeCwd(cwd) {
1086
+ try {
1087
+ return pathNormalize(realpathSync(cwd));
1088
+ } catch (error) {
1089
+ throw new Error(`Refusing to clean: unable to resolve project directory ${cwd}: ${error.message}`, {
1090
+ cause: error
1091
+ });
1092
+ }
1093
+ }
1094
+ function createUniqueStagingDir(deps = {}) {
991
1095
  const cwd = process.cwd();
992
1096
  const prefix = pathJoin(cwd, STAGING_DIR_PREFIX);
993
- const dir = mkdtempSync(prefix);
1097
+ const mkdtemp = deps.mkdtempSyncImpl ?? mkdtempSync;
1098
+ const lstat = deps.lstatSyncImpl ?? lstatSync;
1099
+ const rm = deps.rmSyncImpl ?? rmSync;
1100
+ const dir = mkdtemp(prefix);
994
1101
  try {
995
- const st = lstatSync(dir);
1102
+ const st = lstat(dir);
996
1103
  if (st.isSymbolicLink()) {
997
- rmSync(dir, { recursive: true, force: true });
998
1104
  throw new Error(`Staging directory is a symlink: ${dir}`);
999
1105
  }
1000
1106
  } catch (e) {
1001
- if (e.message.includes("Staging directory is a symlink")) throw e;
1107
+ try {
1108
+ rm(dir, { recursive: true, force: true });
1109
+ } catch {
1110
+ }
1002
1111
  throw e;
1003
1112
  }
1004
1113
  try {
@@ -1007,41 +1116,58 @@ function createUniqueStagingDir() {
1007
1116
  }
1008
1117
  return dir;
1009
1118
  }
1010
- function writeStagingEntry(dir, content) {
1119
+ function writeStagingEntry(dir, content, deps = {}) {
1011
1120
  const entryPath = pathJoin(dir, "entry.ts");
1121
+ const lstat = deps.lstatSyncImpl ?? lstatSync;
1122
+ const writeFile = deps.writeFileSyncImpl ?? writeFileSync;
1123
+ const rm = deps.rmSyncImpl ?? rmSync;
1012
1124
  try {
1013
- if (existsSync(entryPath)) {
1014
- const st = lstatSync(entryPath);
1015
- if (st.isSymbolicLink()) {
1016
- throw new Error(`Refusing to overwrite symlink at staging path: ${entryPath}`);
1017
- }
1018
- throw new Error(`Staging file already exists: ${entryPath}`);
1125
+ const st = lstat(entryPath);
1126
+ if (st.isSymbolicLink()) {
1127
+ throw new Error(`Refusing to overwrite symlink at staging path: ${entryPath}`);
1019
1128
  }
1129
+ throw new Error(`Staging file already exists: ${entryPath}`);
1020
1130
  } catch (e) {
1021
- if (e.message.startsWith("Refusing to") || e.message.startsWith("Staging file already exists"))
1022
- throw e;
1131
+ const message = e.message;
1132
+ if (message.startsWith("Refusing to") || message.startsWith("Staging file already exists")) throw e;
1133
+ const code = e.code;
1134
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
1135
+ throw new Error(`Refusing to use staging path with unresolvable state: ${entryPath}: ${message}`, {
1136
+ cause: e
1137
+ });
1138
+ }
1023
1139
  }
1024
- writeFileSync(entryPath, content, { encoding: "utf8", flag: "wx", mode: 384 });
1140
+ writeFile(entryPath, content, { encoding: "utf8", flag: "wx", mode: 384 });
1025
1141
  try {
1026
- const st = lstatSync(entryPath);
1142
+ const st = lstat(entryPath);
1027
1143
  if (st.isSymbolicLink()) {
1028
- rmSync(entryPath, { force: true });
1144
+ try {
1145
+ rm(entryPath, { force: true });
1146
+ } catch {
1147
+ }
1029
1148
  throw new Error(`Staging file is a symlink after write: ${entryPath}`);
1030
1149
  }
1031
1150
  } catch (e) {
1032
1151
  if (e.message.includes("Staging file is a symlink")) throw e;
1152
+ try {
1153
+ rm(entryPath, { force: true });
1154
+ } catch {
1155
+ }
1156
+ throw new Error(`Refusing to use staging file with unresolvable state: ${entryPath}: ${e.message}`, {
1157
+ cause: e
1158
+ });
1033
1159
  }
1034
1160
  return entryPath;
1035
1161
  }
1036
- async function buildBundleFromEntryContent(args) {
1162
+ async function buildBundleFromEntryContent(args, deps = {}) {
1037
1163
  validateOutDirForClean(args.outDir, args.clean);
1038
- const tsupModule = await import("tsup");
1039
- const { build } = tsupModule;
1040
- const stagingDir = createUniqueStagingDir();
1041
- const tempEntryPath = writeStagingEntry(stagingDir, args.entryContent);
1042
- const absOutDir = pathResolve(process.cwd(), args.outDir);
1164
+ const buildImpl = deps.buildImpl ?? (await import("tsup")).build;
1165
+ const rm = deps.rmSyncImpl ?? rmSync;
1166
+ const stagingDir = createUniqueStagingDir(deps);
1043
1167
  try {
1044
- await build({
1168
+ const tempEntryPath = writeStagingEntry(stagingDir, args.entryContent, deps);
1169
+ const absOutDir = pathResolve(process.cwd(), args.outDir);
1170
+ await buildImpl({
1045
1171
  config: false,
1046
1172
  entry: { [args.outName]: tempEntryPath },
1047
1173
  tsconfig: args.tsconfigPath,
@@ -1055,7 +1181,10 @@ async function buildBundleFromEntryContent(args) {
1055
1181
  splitting: false
1056
1182
  });
1057
1183
  } finally {
1058
- rmSync(stagingDir, { recursive: true, force: true });
1184
+ try {
1185
+ rm(stagingDir, { recursive: true, force: true });
1186
+ } catch {
1187
+ }
1059
1188
  }
1060
1189
  }
1061
1190
  async function buildRuntime(args) {
@@ -1155,11 +1284,14 @@ function collectBody(req, maxBytes) {
1155
1284
  if (finished) return;
1156
1285
  finished = true;
1157
1286
  cleanup();
1287
+ let body;
1158
1288
  try {
1159
- resolve(Buffer.concat(chunks, total));
1289
+ body = Buffer.concat(chunks, total);
1160
1290
  } catch (e) {
1161
- fail(e);
1291
+ reject(e);
1292
+ return;
1162
1293
  }
1294
+ resolve(body);
1163
1295
  };
1164
1296
  const onError = (err) => {
1165
1297
  const e = err;
@@ -1183,13 +1315,13 @@ function collectBody(req, maxBytes) {
1183
1315
  }
1184
1316
  });
1185
1317
  }
1186
- function toServerlessEvent(method, url, headers, body) {
1187
- const parsedUrl = new URL(url, "http://localhost");
1188
- const { queryStringParameters, multiValueQueryStringParameters } = parseAwsRestQuery(parsedUrl.search);
1189
- const { singleValueHeaders, multiValueHeaders } = normalizeAwsRestHeaders(headers);
1318
+ function toServerlessEvent(method, url, headers, body, rawHeaders) {
1319
+ const { path, search } = splitRequestTarget(url);
1320
+ const { queryStringParameters, multiValueQueryStringParameters } = parseAwsRestQuery(search);
1321
+ const { singleValueHeaders, multiValueHeaders } = buildAwsRestHeaders(headers, rawHeaders);
1190
1322
  return {
1191
1323
  httpMethod: method,
1192
- path: parsedUrl.pathname,
1324
+ path,
1193
1325
  headers: singleValueHeaders,
1194
1326
  multiValueHeaders,
1195
1327
  queryStringParameters,
@@ -1204,12 +1336,49 @@ function toServerlessEvent(method, url, headers, body) {
1204
1336
  }
1205
1337
  };
1206
1338
  }
1339
+ function splitRequestTarget(target) {
1340
+ if (target === "") {
1341
+ return { path: "/", search: "" };
1342
+ }
1343
+ let remainder = target;
1344
+ const absoluteMatch = remainder.match(/^([A-Za-z][A-Za-z0-9+.-]*):\/\//);
1345
+ if (absoluteMatch) {
1346
+ const afterScheme = remainder.slice(absoluteMatch[0].length);
1347
+ const boundary = afterScheme.search(/[/?#]/);
1348
+ if (boundary === -1) {
1349
+ return { path: "/", search: "" };
1350
+ }
1351
+ remainder = afterScheme.slice(boundary);
1352
+ if (remainder.startsWith("?") || remainder.startsWith("#")) {
1353
+ remainder = `/${remainder}`;
1354
+ }
1355
+ } else if (remainder === "*" || remainder.startsWith("*?") || remainder.startsWith("*#")) {
1356
+ remainder = remainder.slice(1);
1357
+ if (remainder === "") {
1358
+ return { path: "*", search: "" };
1359
+ }
1360
+ const hashIndex2 = remainder.indexOf("#");
1361
+ const withoutFragment2 = hashIndex2 === -1 ? remainder : remainder.slice(0, hashIndex2);
1362
+ return { path: "*", search: withoutFragment2 };
1363
+ } else if (!remainder.startsWith("/")) {
1364
+ throw new Error(
1365
+ `Unsupported request target: ${JSON.stringify(target)}. Expected an origin-form path ("/path?query"), an absolute-form URI ("scheme://authority/path?query"), or "*"`
1366
+ );
1367
+ }
1368
+ const hashIndex = remainder.indexOf("#");
1369
+ const withoutFragment = hashIndex === -1 ? remainder : remainder.slice(0, hashIndex);
1370
+ const queryIndex = withoutFragment.indexOf("?");
1371
+ if (queryIndex === -1) {
1372
+ return { path: withoutFragment, search: "" };
1373
+ }
1374
+ return { path: withoutFragment.slice(0, queryIndex), search: withoutFragment.slice(queryIndex) };
1375
+ }
1207
1376
  function parseAwsRestQuery(search) {
1208
1377
  if (search === "" || search === "?") {
1209
1378
  return { queryStringParameters: null, multiValueQueryStringParameters: null };
1210
1379
  }
1211
- const single = {};
1212
- const multi = {};
1380
+ const single = /* @__PURE__ */ Object.create(null);
1381
+ const multi = /* @__PURE__ */ Object.create(null);
1213
1382
  const query = search.startsWith("?") ? search.slice(1) : search;
1214
1383
  for (const pair of query.split("&")) {
1215
1384
  if (pair === "") continue;
@@ -1219,7 +1388,11 @@ function parseAwsRestQuery(search) {
1219
1388
  const key = decodeQueryComponent(rawKey);
1220
1389
  const value = decodeQueryComponent(rawValue);
1221
1390
  single[key] = value;
1222
- (multi[key] ??= []).push(value);
1391
+ if (Object.prototype.hasOwnProperty.call(multi, key)) {
1392
+ multi[key].push(value);
1393
+ } else {
1394
+ multi[key] = [value];
1395
+ }
1223
1396
  }
1224
1397
  return {
1225
1398
  queryStringParameters: Object.keys(single).length > 0 ? single : null,
@@ -1235,8 +1408,8 @@ function decodeQueryComponent(value) {
1235
1408
  }
1236
1409
  }
1237
1410
  function normalizeAwsRestHeaders(headers) {
1238
- const singleValueHeaders = {};
1239
- const multiValueHeaders = {};
1411
+ const singleValueHeaders = /* @__PURE__ */ Object.create(null);
1412
+ const multiValueHeaders = /* @__PURE__ */ Object.create(null);
1240
1413
  for (const [key, value] of Object.entries(headers)) {
1241
1414
  if (value === void 0) continue;
1242
1415
  const values = Array.isArray(value) ? value.map(String) : [String(value)];
@@ -1245,23 +1418,92 @@ function normalizeAwsRestHeaders(headers) {
1245
1418
  }
1246
1419
  return { singleValueHeaders, multiValueHeaders };
1247
1420
  }
1421
+ function buildAwsRestHeaders(headers, rawHeaders) {
1422
+ const fromRaw = headersFromRawHeadersList(rawHeaders);
1423
+ if (fromRaw) return fromRaw;
1424
+ if (isPlainRecord(rawHeaders)) {
1425
+ return normalizeAwsRestHeaders(rawHeaders);
1426
+ }
1427
+ return normalizeAwsRestHeaders(headers);
1428
+ }
1429
+ function headersFromRawHeadersList(rawHeaders) {
1430
+ if (!Array.isArray(rawHeaders)) return null;
1431
+ if (rawHeaders.length % 2 !== 0) return null;
1432
+ for (const entry of rawHeaders) {
1433
+ if (typeof entry !== "string") return null;
1434
+ }
1435
+ const singleValueHeaders = /* @__PURE__ */ Object.create(null);
1436
+ const multiValueHeaders = /* @__PURE__ */ Object.create(null);
1437
+ for (let index = 0; index < rawHeaders.length; index += 2) {
1438
+ const name = rawHeaders[index].toLowerCase();
1439
+ const value = rawHeaders[index + 1];
1440
+ if (Object.prototype.hasOwnProperty.call(multiValueHeaders, name)) {
1441
+ multiValueHeaders[name].push(value);
1442
+ } else {
1443
+ multiValueHeaders[name] = [value];
1444
+ }
1445
+ }
1446
+ for (const key of Object.keys(multiValueHeaders)) {
1447
+ singleValueHeaders[key] = multiValueHeaders[key].join(", ");
1448
+ }
1449
+ return { singleValueHeaders, multiValueHeaders };
1450
+ }
1248
1451
  function applyServerlessResult(result, res) {
1249
1452
  const response = validateServerlessResult(result);
1250
1453
  res.status(response.statusCode);
1251
- for (const [key, value] of Object.entries(response.headers)) {
1252
- res.setHeader(key, value);
1454
+ let baseline;
1455
+ try {
1456
+ baseline = new Set(Object.keys(res.getHeaders()).map((name) => name.toLowerCase()));
1457
+ } catch (_e) {
1458
+ void _e;
1459
+ baseline = void 0;
1253
1460
  }
1254
- for (const [key, values] of Object.entries(response.multiValueHeaders)) {
1255
- if (key.toLowerCase() === "set-cookie") {
1256
- res.setHeader(key, values);
1461
+ try {
1462
+ for (const [key, value] of Object.entries(response.headers)) {
1463
+ res.setHeader(key, value);
1464
+ }
1465
+ for (const [key, values] of Object.entries(response.multiValueHeaders)) {
1466
+ if (key.toLowerCase() === "set-cookie") {
1467
+ res.setHeader(key, values);
1468
+ } else {
1469
+ res.setHeader(key, values.join(","));
1470
+ }
1471
+ }
1472
+ if (response.isBase64Encoded) {
1473
+ res.end(response.decodedBody);
1257
1474
  } else {
1258
- res.setHeader(key, values.join(","));
1475
+ res.end(response.body);
1259
1476
  }
1260
- }
1261
- if (response.isBase64Encoded) {
1262
- res.end(response.decodedBody);
1263
- } else {
1264
- res.end(response.body);
1477
+ } catch (error) {
1478
+ if (!res.headersSent) {
1479
+ try {
1480
+ if (baseline !== void 0) {
1481
+ for (const name of Object.keys(res.getHeaders())) {
1482
+ if (!baseline.has(name.toLowerCase())) {
1483
+ res.removeHeader(name);
1484
+ }
1485
+ }
1486
+ } else {
1487
+ for (const [key] of Object.entries(response.headers)) {
1488
+ try {
1489
+ res.removeHeader(key);
1490
+ } catch (_e) {
1491
+ void _e;
1492
+ }
1493
+ }
1494
+ for (const [key] of Object.entries(response.multiValueHeaders)) {
1495
+ try {
1496
+ res.removeHeader(key);
1497
+ } catch (_e) {
1498
+ void _e;
1499
+ }
1500
+ }
1501
+ }
1502
+ } catch (_e) {
1503
+ void _e;
1504
+ }
1505
+ }
1506
+ throw error;
1265
1507
  }
1266
1508
  }
1267
1509
  function validateServerlessResult(result) {
@@ -1313,7 +1555,7 @@ function validateSingleValueHeaders(value, name) {
1313
1555
  if (!isPlainRecord(value)) {
1314
1556
  throw new Error(`Invalid serverless result ${name}: expected an object of string header values.`);
1315
1557
  }
1316
- const headers = {};
1558
+ const headers = /* @__PURE__ */ Object.create(null);
1317
1559
  for (const [key, headerValue] of Object.entries(value)) {
1318
1560
  if (headerValue === void 0) continue;
1319
1561
  if (typeof headerValue !== "string") {
@@ -1329,12 +1571,16 @@ function validateMultiValueHeaders(value, name) {
1329
1571
  if (!isPlainRecord(value)) {
1330
1572
  throw new Error(`Invalid serverless result ${name}: expected an object of string-array header values.`);
1331
1573
  }
1332
- const headers = {};
1574
+ const headers = /* @__PURE__ */ Object.create(null);
1333
1575
  for (const [key, headerValue] of Object.entries(value)) {
1334
1576
  if (headerValue === void 0) continue;
1335
1577
  if (!Array.isArray(headerValue) || headerValue.some((entry) => typeof entry !== "string")) {
1336
1578
  throw new Error(`Invalid serverless result ${name}.${key}: expected an array of string header values.`);
1337
1579
  }
1580
+ if (headerValue.length === 0) {
1581
+ validateServerlessHeaderName(key, `${name}.${key}`);
1582
+ continue;
1583
+ }
1338
1584
  for (const entry of headerValue) {
1339
1585
  validateServerlessHeader(key, entry, `${name}.${key}`);
1340
1586
  }
@@ -1342,6 +1588,13 @@ function validateMultiValueHeaders(value, name) {
1342
1588
  }
1343
1589
  return headers;
1344
1590
  }
1591
+ function validateServerlessHeaderName(key, label) {
1592
+ try {
1593
+ validateHeaderName(key);
1594
+ } catch (error) {
1595
+ throw new Error(`Invalid serverless result header ${label}: ${error.message}`, { cause: error });
1596
+ }
1597
+ }
1345
1598
  function validateServerlessHeader(key, value, label) {
1346
1599
  try {
1347
1600
  validateHeaderName(key);
@@ -1400,18 +1653,42 @@ function createServerlessAdapterApp(handler, options = {}) {
1400
1653
  }
1401
1654
  let result;
1402
1655
  try {
1403
- const event = toServerlessEvent(req.method, req.url, req.headers, body);
1656
+ const raw = req.rawHeaders ?? req.headersDistinct;
1657
+ const event = toServerlessEvent(req.method, req.url, req.headers, body, raw);
1404
1658
  result = await handler(event, {});
1405
1659
  } catch (e) {
1406
1660
  console.error("Serverless adapter error:", e);
1407
1661
  if (!res.headersSent && !res.writableEnded) res.status(500).end("Internal server error");
1408
1662
  return;
1409
1663
  }
1664
+ let baselineHeaders;
1665
+ try {
1666
+ baselineHeaders = new Set(Object.keys(res.getHeaders()).map((name) => name.toLowerCase()));
1667
+ } catch (_e) {
1668
+ void _e;
1669
+ baselineHeaders = void 0;
1670
+ }
1410
1671
  try {
1411
1672
  applyServerlessResult(result, res);
1412
1673
  } catch (e) {
1413
1674
  console.error("Invalid serverless handler result:", e);
1414
- if (!res.headersSent && !res.writableEnded) res.status(500).end("Internal server error");
1675
+ if (!res.headersSent && !res.writableEnded) {
1676
+ try {
1677
+ for (const name of Object.keys(res.getHeaders())) {
1678
+ const lower = name.toLowerCase();
1679
+ if (lower === "content-length") continue;
1680
+ if (baselineHeaders !== void 0 && baselineHeaders.has(lower)) continue;
1681
+ try {
1682
+ res.removeHeader(name);
1683
+ } catch (_e) {
1684
+ void _e;
1685
+ }
1686
+ }
1687
+ } catch (_e) {
1688
+ void _e;
1689
+ }
1690
+ res.status(500).end("Internal server error");
1691
+ }
1415
1692
  }
1416
1693
  });
1417
1694
  },