@web-ts-toolkit/express-runtime 0.42.2 → 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/README.md +80 -50
- package/{chunk-UPFG3S34.mjs → chunk-KBNC4WIR.mjs} +378 -101
- package/{chunk-VPFBKM2K.mjs → chunk-QNRHWPTO.mjs} +13 -10
- package/cli-api.d.mts +166 -18
- package/cli-api.d.ts +166 -18
- package/cli-api.js +385 -109
- package/cli-api.mjs +4 -4
- package/{cli-utils-4POUMJN7.mjs → cli-utils-IN67EHOM.mjs} +2 -2
- package/cli.js +385 -109
- package/index.d.mts +16 -6
- package/index.d.ts +16 -6
- package/index.js +11 -10
- package/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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"
|
|
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:
|
|
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
|
-
|
|
895
|
-
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
|
954
|
-
const root = pathParse(
|
|
955
|
-
if (
|
|
956
|
-
throw new Error(`Refusing to clean filesystem root: ${outDir} resolves to ${
|
|
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 (
|
|
1017
|
+
if (canonicalOut === canonicalCwd) {
|
|
959
1018
|
throw new Error(`Refusing to clean project directory: ${outDir} resolves to cwd ${cwd}`);
|
|
960
1019
|
}
|
|
961
|
-
if (
|
|
1020
|
+
if (canonicalCwd === canonicalOut || canonicalCwd.startsWith(canonicalOut + pathSep)) {
|
|
962
1021
|
throw new Error(
|
|
963
|
-
`Refusing to clean ancestor of project directory: ${outDir} resolves to ${
|
|
1022
|
+
`Refusing to clean ancestor of project directory: ${outDir} resolves to ${canonicalOut} which contains cwd ${cwd}`
|
|
964
1023
|
);
|
|
965
1024
|
}
|
|
966
1025
|
try {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
|
980
|
-
if (
|
|
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 (
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
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
|
-
|
|
1140
|
+
writeFile(entryPath, content, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
1025
1141
|
try {
|
|
1026
|
-
const st =
|
|
1142
|
+
const st = lstat(entryPath);
|
|
1027
1143
|
if (st.isSymbolicLink()) {
|
|
1028
|
-
|
|
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
|
|
1039
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1289
|
+
body = Buffer.concat(chunks, total);
|
|
1160
1290
|
} catch (e) {
|
|
1161
|
-
|
|
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
|
|
1188
|
-
const { queryStringParameters, multiValueQueryStringParameters } = parseAwsRestQuery(
|
|
1189
|
-
const { singleValueHeaders, multiValueHeaders } =
|
|
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
|
|
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
|
|
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
|
-
|
|
1252
|
-
|
|
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
|
-
|
|
1255
|
-
|
|
1256
|
-
res.setHeader(key,
|
|
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.
|
|
1475
|
+
res.end(response.body);
|
|
1259
1476
|
}
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
|
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)
|
|
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
|
},
|