@reckona/mreact-router 0.0.65 → 0.0.67
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/package.json +13 -12
- package/src/actions.ts +1130 -0
- package/src/adapters/aws-lambda.ts +993 -0
- package/src/adapters/cloudflare.ts +1286 -0
- package/src/adapters/devtools.ts +5 -0
- package/src/adapters/edge.ts +70 -0
- package/src/adapters/node.ts +126 -0
- package/src/adapters/static.ts +61 -0
- package/src/app-router-globals.ts +19 -0
- package/src/assets.ts +113 -0
- package/src/build.ts +2948 -0
- package/src/bundle-pipeline.ts +496 -0
- package/src/cache-config.ts +35 -0
- package/src/cache-stats.ts +54 -0
- package/src/cache.ts +418 -0
- package/src/cli-options.ts +296 -0
- package/src/cli.ts +94 -0
- package/src/client.ts +3398 -0
- package/src/config.ts +146 -0
- package/src/cookies.ts +113 -0
- package/src/csp.ts +103 -0
- package/src/csrf.ts +132 -0
- package/src/deferred.ts +52 -0
- package/src/dev-server.ts +262 -0
- package/src/file-conventions.ts +88 -0
- package/src/http.ts +128 -0
- package/src/i18n.ts +98 -0
- package/src/import-policy.ts +261 -0
- package/src/index.ts +221 -0
- package/src/link.ts +47 -0
- package/src/logger.ts +149 -0
- package/src/module-runner.ts +554 -0
- package/src/multipart.ts +577 -0
- package/src/native-escape.ts +53 -0
- package/src/native-route-matcher.ts +173 -0
- package/src/navigation-state.ts +98 -0
- package/src/navigation.ts +215 -0
- package/src/prerender-store.ts +233 -0
- package/src/render.ts +5187 -0
- package/src/route-path.ts +5 -0
- package/src/route-shells.ts +47 -0
- package/src/route-source.ts +224 -0
- package/src/route-styles.ts +91 -0
- package/src/routes.ts +362 -0
- package/src/runtime-cache.ts +8 -0
- package/src/runtime-state.ts +37 -0
- package/src/security-headers.ts +134 -0
- package/src/serve.ts +1265 -0
- package/src/session.ts +238 -0
- package/src/source-jsx.ts +30 -0
- package/src/source-modules.ts +69 -0
- package/src/stream-list.ts +45 -0
- package/src/trace.ts +114 -0
- package/src/types.ts +155 -0
- package/src/upgrade.ts +8 -0
- package/src/vite-config.ts +63 -0
- package/src/vite-plugin-cache-key.ts +53 -0
- package/src/vite.ts +690 -0
- package/src/workspace-packages.ts +67 -0
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { access, lstat, mkdir, readFile, readlink, symlink } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import type { AppRouterServerActionOptions } from "../actions.js";
|
|
7
|
+
import type { AppRouterCache } from "../cache.js";
|
|
8
|
+
import type { AppRouterImportPolicy } from "../import-policy.js";
|
|
9
|
+
import {
|
|
10
|
+
emitRouterLog,
|
|
11
|
+
logDurationMs,
|
|
12
|
+
logError,
|
|
13
|
+
logNow,
|
|
14
|
+
requestLogFields,
|
|
15
|
+
type AppRouterLogger,
|
|
16
|
+
type RouterRequestLogFields,
|
|
17
|
+
} from "../logger.js";
|
|
18
|
+
import type { AppRouterResponseHook } from "../render.js";
|
|
19
|
+
import type { RouterInstrumentation } from "../trace.js";
|
|
20
|
+
|
|
21
|
+
export type { RouterInstrumentation } from "../trace.js";
|
|
22
|
+
export type { AppRouterImportPolicy } from "../import-policy.js";
|
|
23
|
+
import {
|
|
24
|
+
preloadBuiltAppRuntime,
|
|
25
|
+
renderBuiltAppRequest,
|
|
26
|
+
resolveRequestHost,
|
|
27
|
+
warnIfImplicitHostTrust,
|
|
28
|
+
type BuiltAppRuntimePreloadStrategy,
|
|
29
|
+
type AppRouterPrerenderStore,
|
|
30
|
+
type RequestHostPolicy,
|
|
31
|
+
type ResponseSinkStrategy,
|
|
32
|
+
} from "../serve.js";
|
|
33
|
+
|
|
34
|
+
export interface AwsLambdaHttpEventV2 {
|
|
35
|
+
body?: string | undefined;
|
|
36
|
+
cookies?: string[] | undefined;
|
|
37
|
+
headers?: Record<string, string | undefined> | undefined;
|
|
38
|
+
isBase64Encoded?: boolean | undefined;
|
|
39
|
+
rawPath?: string | undefined;
|
|
40
|
+
rawQueryString?: string | undefined;
|
|
41
|
+
requestContext?: {
|
|
42
|
+
http?: {
|
|
43
|
+
method?: string | undefined;
|
|
44
|
+
protocol?: string | undefined;
|
|
45
|
+
} | undefined;
|
|
46
|
+
} | undefined;
|
|
47
|
+
version?: "2.0" | string | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AwsLambdaHttpResultV2 {
|
|
51
|
+
body: string;
|
|
52
|
+
cookies?: string[] | undefined;
|
|
53
|
+
headers?: Record<string, string> | undefined;
|
|
54
|
+
isBase64Encoded: boolean;
|
|
55
|
+
statusCode: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AwsLambdaStreamingResponseMetadata {
|
|
59
|
+
cookies?: string[] | undefined;
|
|
60
|
+
headers: Record<string, string>;
|
|
61
|
+
statusCode: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface AwsLambdaStreamingResponseStream {
|
|
65
|
+
destroy?: ((error?: unknown) => void) | undefined;
|
|
66
|
+
end(): void;
|
|
67
|
+
once?: ((event: "drain", listener: () => void) => unknown) | undefined;
|
|
68
|
+
write(chunk: string | Uint8Array): boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface AwsLambdaRequestHandlerOptions {
|
|
72
|
+
allowedHosts?: readonly string[] | undefined;
|
|
73
|
+
errorHandler?:
|
|
74
|
+
| ((error: unknown) => {
|
|
75
|
+
body: string;
|
|
76
|
+
headers?: Record<string, string>;
|
|
77
|
+
status: number;
|
|
78
|
+
})
|
|
79
|
+
| undefined;
|
|
80
|
+
hostPolicy?: RequestHostPolicy | undefined;
|
|
81
|
+
hostname?: string | undefined;
|
|
82
|
+
importPolicy?: AwsLambdaImportPolicy | undefined;
|
|
83
|
+
instrumentation?: RouterInstrumentation | undefined;
|
|
84
|
+
logger?: AppRouterLogger | undefined;
|
|
85
|
+
onResponse?: AppRouterResponseHook | undefined;
|
|
86
|
+
outDir: string;
|
|
87
|
+
preload?: AwsLambdaPreloadStrategy | undefined;
|
|
88
|
+
prerenderStore?: AppRouterPrerenderStore | undefined;
|
|
89
|
+
routeCache?: AppRouterCache | undefined;
|
|
90
|
+
runtimeDir?: string | undefined;
|
|
91
|
+
serverActions?: AppRouterServerActionOptions | undefined;
|
|
92
|
+
sinkStrategy?: ResponseSinkStrategy | undefined;
|
|
93
|
+
timings?: boolean | undefined;
|
|
94
|
+
trustForwardedProto?: boolean | undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type AwsLambdaImportPolicy =
|
|
98
|
+
| AppRouterImportPolicy
|
|
99
|
+
| "generated"
|
|
100
|
+
| { fromManifest: true };
|
|
101
|
+
|
|
102
|
+
export type AwsLambdaPreloadStrategy =
|
|
103
|
+
| "all"
|
|
104
|
+
| "hot-route-requests"
|
|
105
|
+
| "middleware"
|
|
106
|
+
| "none"
|
|
107
|
+
| {
|
|
108
|
+
mode: "all" | "hot-route-requests" | "hot-routes" | "middleware" | "none";
|
|
109
|
+
routes?: readonly string[] | undefined;
|
|
110
|
+
wait?: "background" | "before-render" | "first-request" | undefined;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
type NormalizedAwsLambdaPreloadStrategy = BuiltAppRuntimePreloadStrategy & {
|
|
114
|
+
wait: "background" | "before-render" | "first-request";
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
type AwsLambdaDefaultPreloadMode = "all" | "middleware";
|
|
118
|
+
|
|
119
|
+
export type AwsLambdaRequestHandler = (
|
|
120
|
+
event: AwsLambdaHttpEventV2,
|
|
121
|
+
) => Promise<AwsLambdaHttpResultV2>;
|
|
122
|
+
|
|
123
|
+
export type AwsLambdaStreamingRequestHandler<TContext = unknown> = (
|
|
124
|
+
event: AwsLambdaHttpEventV2,
|
|
125
|
+
responseStream: AwsLambdaStreamingResponseStream,
|
|
126
|
+
context: TContext,
|
|
127
|
+
) => Promise<void>;
|
|
128
|
+
|
|
129
|
+
export function createAwsLambdaRequestHandler(
|
|
130
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
131
|
+
): AwsLambdaRequestHandler {
|
|
132
|
+
warnIfImplicitHostTrust(options);
|
|
133
|
+
const runtimeDirPromise = prepareAwsLambdaRuntimeDir(options);
|
|
134
|
+
const runtimePreloadPromise = startAwsLambdaRuntimePreload(
|
|
135
|
+
options,
|
|
136
|
+
runtimeDirPromise,
|
|
137
|
+
"middleware",
|
|
138
|
+
);
|
|
139
|
+
void runtimePreloadPromise?.catch(() => {});
|
|
140
|
+
|
|
141
|
+
return createAwsLambdaRequestHandlerFromRuntime(
|
|
142
|
+
options,
|
|
143
|
+
runtimeDirPromise,
|
|
144
|
+
runtimePreloadPromise,
|
|
145
|
+
"middleware",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function createPreloadedAwsLambdaRequestHandler(
|
|
150
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
151
|
+
): Promise<AwsLambdaRequestHandler> {
|
|
152
|
+
warnIfImplicitHostTrust(options);
|
|
153
|
+
const runtimeDir = await prepareAwsLambdaRuntimeDir(options);
|
|
154
|
+
await preloadAwsLambdaRuntime(options, runtimeDir);
|
|
155
|
+
|
|
156
|
+
return createAwsLambdaRequestHandlerFromRuntime(options, Promise.resolve(runtimeDir));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function createAwsLambdaRequestHandlerFromRuntime(
|
|
160
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
161
|
+
runtimeDirPromise: Promise<string>,
|
|
162
|
+
runtimePreloadPromise?: Promise<void> | undefined,
|
|
163
|
+
defaultPreloadMode: AwsLambdaDefaultPreloadMode = "all",
|
|
164
|
+
): AwsLambdaRequestHandler {
|
|
165
|
+
return async (event) => {
|
|
166
|
+
const startedAt = logNow();
|
|
167
|
+
const phases = createAwsLambdaTimingPhases(options);
|
|
168
|
+
const eventToRequestStartedAt = phaseStartedAt(phases);
|
|
169
|
+
const request = eventToRequest(event, options);
|
|
170
|
+
finishPhase(phases, eventToRequestStartedAt, "eventToRequestMs");
|
|
171
|
+
const logFields = requestLogFields(request, "aws-lambda");
|
|
172
|
+
emitRouterLog(options.logger, "info", {
|
|
173
|
+
...logFields,
|
|
174
|
+
type: "router:request:start",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const runtimeDirStartedAt = phaseStartedAt(phases);
|
|
179
|
+
const runtimeDir = await runtimeDirPromise;
|
|
180
|
+
finishPhase(phases, runtimeDirStartedAt, "runtimeDirMs");
|
|
181
|
+
await waitForAwsLambdaRuntimePreload(
|
|
182
|
+
options,
|
|
183
|
+
runtimePreloadPromise,
|
|
184
|
+
phases,
|
|
185
|
+
defaultPreloadMode,
|
|
186
|
+
);
|
|
187
|
+
const importPolicy = await resolveAwsLambdaImportPolicy(options);
|
|
188
|
+
const renderStartedAt = phaseStartedAt(phases);
|
|
189
|
+
const preload = awsLambdaRenderPreload(
|
|
190
|
+
options,
|
|
191
|
+
runtimePreloadPromise,
|
|
192
|
+
defaultPreloadMode,
|
|
193
|
+
);
|
|
194
|
+
const response = await renderBuiltAppRequest({
|
|
195
|
+
outDir: options.outDir,
|
|
196
|
+
importPolicy,
|
|
197
|
+
instrumentation: options.instrumentation,
|
|
198
|
+
logger: awsLambdaRenderLogger(options),
|
|
199
|
+
onResponse: options.onResponse,
|
|
200
|
+
...(preload === undefined ? {} : { preload }),
|
|
201
|
+
prerenderStore: options.prerenderStore,
|
|
202
|
+
request,
|
|
203
|
+
routeCache: options.routeCache,
|
|
204
|
+
runtimeDir,
|
|
205
|
+
serverActions: options.serverActions,
|
|
206
|
+
...(options.sinkStrategy === undefined ? {} : { sinkStrategy: options.sinkStrategy }),
|
|
207
|
+
});
|
|
208
|
+
finishPhase(phases, renderStartedAt, "renderMs");
|
|
209
|
+
const responseSerializationStartedAt = phaseStartedAt(phases);
|
|
210
|
+
const result = await responseToLambdaResult(response, phases);
|
|
211
|
+
finishPhase(phases, responseSerializationStartedAt, "responseSerializationMs");
|
|
212
|
+
emitRouterLog(options.logger, "info", {
|
|
213
|
+
...logFields,
|
|
214
|
+
durationMs: logDurationMs(startedAt),
|
|
215
|
+
status: result.statusCode,
|
|
216
|
+
type: "router:request:end",
|
|
217
|
+
});
|
|
218
|
+
emitAwsLambdaTiming(options, logFields, result.statusCode, startedAt, phases);
|
|
219
|
+
|
|
220
|
+
return result;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
emitRouterLog(options.logger, "error", {
|
|
223
|
+
...logFields,
|
|
224
|
+
durationMs: logDurationMs(startedAt),
|
|
225
|
+
error: logError(error),
|
|
226
|
+
type: "router:request:error",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const payload = options.errorHandler
|
|
230
|
+
? options.errorHandler(error)
|
|
231
|
+
: { body: "Internal Server Error", status: 500 };
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
body: payload.body,
|
|
235
|
+
headers: {
|
|
236
|
+
"content-type": "text/plain; charset=utf-8",
|
|
237
|
+
...payload.headers,
|
|
238
|
+
},
|
|
239
|
+
isBase64Encoded: false,
|
|
240
|
+
statusCode: payload.status,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function createAwsLambdaStreamingRequestHandler<TContext = unknown>(
|
|
247
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
248
|
+
): AwsLambdaStreamingRequestHandler<TContext> {
|
|
249
|
+
warnIfImplicitHostTrust(options);
|
|
250
|
+
const runtime = awsLambdaRuntime();
|
|
251
|
+
const runtimeDirPromise = prepareAwsLambdaRuntimeDir(options);
|
|
252
|
+
const runtimePreloadPromise = startAwsLambdaRuntimePreload(
|
|
253
|
+
options,
|
|
254
|
+
runtimeDirPromise,
|
|
255
|
+
"middleware",
|
|
256
|
+
);
|
|
257
|
+
void runtimePreloadPromise?.catch(() => {});
|
|
258
|
+
|
|
259
|
+
return createAwsLambdaStreamingRequestHandlerFromRuntime(
|
|
260
|
+
options,
|
|
261
|
+
runtime,
|
|
262
|
+
runtimeDirPromise,
|
|
263
|
+
runtimePreloadPromise,
|
|
264
|
+
"middleware",
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function createPreloadedAwsLambdaStreamingRequestHandler<TContext = unknown>(
|
|
269
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
270
|
+
): Promise<AwsLambdaStreamingRequestHandler<TContext>> {
|
|
271
|
+
warnIfImplicitHostTrust(options);
|
|
272
|
+
const runtime = awsLambdaRuntime();
|
|
273
|
+
const runtimeDir = await prepareAwsLambdaRuntimeDir(options);
|
|
274
|
+
await preloadAwsLambdaRuntime(options, runtimeDir);
|
|
275
|
+
|
|
276
|
+
return createAwsLambdaStreamingRequestHandlerFromRuntime<TContext>(
|
|
277
|
+
options,
|
|
278
|
+
runtime,
|
|
279
|
+
Promise.resolve(runtimeDir),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function startAwsLambdaRuntimePreload(
|
|
284
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
285
|
+
runtimeDirPromise: Promise<string>,
|
|
286
|
+
defaultMode: AwsLambdaDefaultPreloadMode = "all",
|
|
287
|
+
): Promise<void> | undefined {
|
|
288
|
+
const preload = normalizeAwsLambdaPreload(options.preload, defaultMode);
|
|
289
|
+
if (preload.mode === "none") {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return runtimeDirPromise.then((runtimeDir) =>
|
|
294
|
+
preloadAwsLambdaRuntime(options, runtimeDir, defaultMode),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function preloadAwsLambdaRuntime(
|
|
299
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
300
|
+
runtimeDir: string,
|
|
301
|
+
defaultMode: AwsLambdaDefaultPreloadMode = "all",
|
|
302
|
+
): Promise<void> {
|
|
303
|
+
const preload = normalizeAwsLambdaPreload(options.preload, defaultMode);
|
|
304
|
+
if (preload.mode === "none") {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const importPolicy = await resolveAwsLambdaImportPolicy(options);
|
|
309
|
+
await preloadBuiltAppRuntime({
|
|
310
|
+
importPolicy,
|
|
311
|
+
outDir: options.outDir,
|
|
312
|
+
preload,
|
|
313
|
+
runtimeDir,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function waitForAwsLambdaRuntimePreload(
|
|
318
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
319
|
+
runtimePreloadPromise: Promise<void> | undefined,
|
|
320
|
+
phases: Record<string, number> | undefined,
|
|
321
|
+
defaultMode: AwsLambdaDefaultPreloadMode,
|
|
322
|
+
): Promise<void> {
|
|
323
|
+
const preload = normalizeAwsLambdaPreload(options.preload, defaultMode);
|
|
324
|
+
if (preload.wait !== "first-request" || runtimePreloadPromise === undefined) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const preloadWaitStartedAt = phaseStartedAt(phases);
|
|
329
|
+
try {
|
|
330
|
+
await runtimePreloadPromise;
|
|
331
|
+
} finally {
|
|
332
|
+
finishPhase(phases, preloadWaitStartedAt, "preloadWaitMs");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function awsLambdaRenderPreload(
|
|
337
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
338
|
+
runtimePreloadPromise: Promise<void> | undefined,
|
|
339
|
+
defaultMode: AwsLambdaDefaultPreloadMode,
|
|
340
|
+
): { promise: Promise<void>; wait: "before-render" } | undefined {
|
|
341
|
+
if (
|
|
342
|
+
runtimePreloadPromise === undefined ||
|
|
343
|
+
normalizeAwsLambdaPreload(options.preload, defaultMode).wait !== "before-render"
|
|
344
|
+
) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
promise: runtimePreloadPromise,
|
|
350
|
+
wait: "before-render",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const generatedImportPolicyCache = new Map<string, Promise<AppRouterImportPolicy>>();
|
|
355
|
+
|
|
356
|
+
async function resolveAwsLambdaImportPolicy(
|
|
357
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
358
|
+
): Promise<AppRouterImportPolicy | undefined> {
|
|
359
|
+
const policy = options.importPolicy;
|
|
360
|
+
|
|
361
|
+
if (policy === undefined) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (policy === "generated") {
|
|
366
|
+
return await readGeneratedAwsLambdaImportPolicy(options.outDir);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (isGeneratedAwsLambdaImportPolicyReference(policy) && policy.fromManifest === true) {
|
|
370
|
+
return await readGeneratedAwsLambdaImportPolicy(options.outDir);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const configuredPolicy = policy as AppRouterImportPolicy;
|
|
374
|
+
return {
|
|
375
|
+
...(configuredPolicy.allowedPackages === undefined
|
|
376
|
+
? {}
|
|
377
|
+
: { allowedPackages: configuredPolicy.allowedPackages }),
|
|
378
|
+
...(configuredPolicy.allowedSourceDirs === undefined
|
|
379
|
+
? {}
|
|
380
|
+
: { allowedSourceDirs: configuredPolicy.allowedSourceDirs }),
|
|
381
|
+
...(configuredPolicy.projectRoot === undefined
|
|
382
|
+
? {}
|
|
383
|
+
: { projectRoot: configuredPolicy.projectRoot }),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isGeneratedAwsLambdaImportPolicyReference(
|
|
388
|
+
policy: AwsLambdaImportPolicy,
|
|
389
|
+
): policy is { fromManifest: true } {
|
|
390
|
+
return typeof policy === "object" && policy !== null && "fromManifest" in policy;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function readGeneratedAwsLambdaImportPolicy(outDir: string): Promise<AppRouterImportPolicy> {
|
|
394
|
+
const cached = generatedImportPolicyCache.get(outDir);
|
|
395
|
+
if (cached !== undefined) {
|
|
396
|
+
return await cached;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const promise = readGeneratedAwsLambdaImportPolicyInner(outDir);
|
|
400
|
+
generatedImportPolicyCache.set(outDir, promise);
|
|
401
|
+
|
|
402
|
+
return await promise;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function readGeneratedAwsLambdaImportPolicyInner(outDir: string): Promise<AppRouterImportPolicy> {
|
|
406
|
+
const policy = JSON.parse(await readFile(join(outDir, "server", "import-policy.json"), "utf8")) as {
|
|
407
|
+
runtimePackages?: readonly string[] | undefined;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
allowedPackages: policy.runtimePackages ?? [],
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function normalizeAwsLambdaPreload(
|
|
416
|
+
strategy: AwsLambdaPreloadStrategy | undefined,
|
|
417
|
+
defaultMode: AwsLambdaDefaultPreloadMode = "all",
|
|
418
|
+
): NormalizedAwsLambdaPreloadStrategy {
|
|
419
|
+
if (strategy === undefined) {
|
|
420
|
+
return { mode: defaultMode, wait: "background" };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (typeof strategy === "string") {
|
|
424
|
+
return { mode: strategy, wait: "background" };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
mode: strategy.mode,
|
|
429
|
+
...(strategy.routes === undefined ? {} : { routes: strategy.routes }),
|
|
430
|
+
wait: strategy.wait ?? "background",
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function createAwsLambdaStreamingRequestHandlerFromRuntime<TContext = unknown>(
|
|
435
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
436
|
+
runtime: AwsLambdaRuntime,
|
|
437
|
+
runtimeDirPromise: Promise<string>,
|
|
438
|
+
runtimePreloadPromise?: Promise<void> | undefined,
|
|
439
|
+
defaultPreloadMode: AwsLambdaDefaultPreloadMode = "all",
|
|
440
|
+
): AwsLambdaStreamingRequestHandler<TContext> {
|
|
441
|
+
return runtime.streamifyResponse(async (event, responseStream, _context) => {
|
|
442
|
+
const startedAt = logNow();
|
|
443
|
+
const phases = createAwsLambdaTimingPhases(options);
|
|
444
|
+
const eventToRequestStartedAt = phaseStartedAt(phases);
|
|
445
|
+
const request = eventToRequest(event, options);
|
|
446
|
+
finishPhase(phases, eventToRequestStartedAt, "eventToRequestMs");
|
|
447
|
+
const logFields = requestLogFields(request, "aws-lambda");
|
|
448
|
+
emitRouterLog(options.logger, "info", {
|
|
449
|
+
...logFields,
|
|
450
|
+
type: "router:request:start",
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
const runtimeDirStartedAt = phaseStartedAt(phases);
|
|
455
|
+
const runtimeDir = await runtimeDirPromise;
|
|
456
|
+
finishPhase(phases, runtimeDirStartedAt, "runtimeDirMs");
|
|
457
|
+
await waitForAwsLambdaRuntimePreload(
|
|
458
|
+
options,
|
|
459
|
+
runtimePreloadPromise,
|
|
460
|
+
phases,
|
|
461
|
+
defaultPreloadMode,
|
|
462
|
+
);
|
|
463
|
+
const importPolicy = await resolveAwsLambdaImportPolicy(options);
|
|
464
|
+
const renderStartedAt = phaseStartedAt(phases);
|
|
465
|
+
const preload = awsLambdaRenderPreload(
|
|
466
|
+
options,
|
|
467
|
+
runtimePreloadPromise,
|
|
468
|
+
defaultPreloadMode,
|
|
469
|
+
);
|
|
470
|
+
const response = await renderBuiltAppRequest({
|
|
471
|
+
outDir: options.outDir,
|
|
472
|
+
importPolicy,
|
|
473
|
+
instrumentation: options.instrumentation,
|
|
474
|
+
logger: awsLambdaRenderLogger(options),
|
|
475
|
+
onResponse: options.onResponse,
|
|
476
|
+
...(preload === undefined ? {} : { preload }),
|
|
477
|
+
prerenderStore: options.prerenderStore,
|
|
478
|
+
request,
|
|
479
|
+
routeCache: options.routeCache,
|
|
480
|
+
runtimeDir,
|
|
481
|
+
serverActions: options.serverActions,
|
|
482
|
+
...(options.sinkStrategy === undefined ? {} : { sinkStrategy: options.sinkStrategy }),
|
|
483
|
+
});
|
|
484
|
+
finishPhase(phases, renderStartedAt, "renderMs");
|
|
485
|
+
const responseStreamingStartedAt = phaseStartedAt(phases);
|
|
486
|
+
await streamResponseToLambda(response, responseStream, runtime, phases);
|
|
487
|
+
finishPhase(phases, responseStreamingStartedAt, "responseStreamingMs");
|
|
488
|
+
emitRouterLog(options.logger, "info", {
|
|
489
|
+
...logFields,
|
|
490
|
+
durationMs: logDurationMs(startedAt),
|
|
491
|
+
status: response.status,
|
|
492
|
+
type: "router:request:end",
|
|
493
|
+
});
|
|
494
|
+
emitAwsLambdaTiming(options, logFields, response.status, startedAt, phases);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
emitRouterLog(options.logger, "error", {
|
|
497
|
+
...logFields,
|
|
498
|
+
durationMs: logDurationMs(startedAt),
|
|
499
|
+
error: logError(error),
|
|
500
|
+
type: "router:request:error",
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const payload = options.errorHandler
|
|
504
|
+
? options.errorHandler(error)
|
|
505
|
+
: { body: "Internal Server Error", status: 500 };
|
|
506
|
+
const response = new Response(payload.body, {
|
|
507
|
+
headers: {
|
|
508
|
+
"content-type": "text/plain; charset=utf-8",
|
|
509
|
+
...payload.headers,
|
|
510
|
+
},
|
|
511
|
+
status: payload.status,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
await streamResponseToLambda(response, responseStream, runtime, phases);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function prepareAwsLambdaRuntimeDir(options: {
|
|
520
|
+
outDir: string;
|
|
521
|
+
runtimeDir?: string | undefined;
|
|
522
|
+
}): Promise<string> {
|
|
523
|
+
const runtimeDir = options.runtimeDir ?? defaultAwsLambdaRuntimeDir(options.outDir);
|
|
524
|
+
|
|
525
|
+
await mkdir(runtimeDir, { recursive: true });
|
|
526
|
+
await linkAwsLambdaNodeModules({
|
|
527
|
+
outDir: options.outDir,
|
|
528
|
+
runtimeDir,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
return runtimeDir;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function linkAwsLambdaNodeModules(options: {
|
|
535
|
+
outDir: string;
|
|
536
|
+
runtimeDir: string;
|
|
537
|
+
}): Promise<void> {
|
|
538
|
+
const source = join(dirname(options.outDir), "node_modules");
|
|
539
|
+
const target = join(options.runtimeDir, "node_modules");
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
await access(source);
|
|
543
|
+
} catch {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
const stats = await lstat(target);
|
|
549
|
+
|
|
550
|
+
if (!stats.isSymbolicLink()) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if ((await readlink(target)) === source) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return;
|
|
559
|
+
} catch {
|
|
560
|
+
// Missing target: create it below.
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
await symlink(source, target, "dir");
|
|
565
|
+
} catch (error) {
|
|
566
|
+
if (!isNodeErrorCode(error, "EEXIST")) {
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function defaultAwsLambdaRuntimeDir(outDir: string): string {
|
|
573
|
+
return join(tmpdir(), "mreact-router", hashText(outDir), "runtime");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function hashText(value: string): string {
|
|
577
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function isNodeErrorCode(error: unknown, code: string): boolean {
|
|
581
|
+
return (
|
|
582
|
+
typeof error === "object" &&
|
|
583
|
+
error !== null &&
|
|
584
|
+
"code" in error &&
|
|
585
|
+
(error as { code?: unknown }).code === code
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function eventToRequest(
|
|
590
|
+
event: AwsLambdaHttpEventV2,
|
|
591
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
592
|
+
): Request {
|
|
593
|
+
const headers = eventHeaders(event);
|
|
594
|
+
const rawHost = lambdaRequestHost(headers, options);
|
|
595
|
+
const host = resolveRequestHost({
|
|
596
|
+
allowedHosts: options.allowedHosts,
|
|
597
|
+
fallbackHost: options.hostname ?? "lambda.local",
|
|
598
|
+
hostPolicy: lambdaHostPolicy(options),
|
|
599
|
+
rawHost: rawHost ?? undefined,
|
|
600
|
+
});
|
|
601
|
+
const protocol = lambdaRequestProtocol(event, headers, options);
|
|
602
|
+
const rawPath = event.rawPath === undefined || event.rawPath === "" ? "/" : event.rawPath;
|
|
603
|
+
const rawQueryString =
|
|
604
|
+
event.rawQueryString === undefined || event.rawQueryString === ""
|
|
605
|
+
? ""
|
|
606
|
+
: `?${event.rawQueryString}`;
|
|
607
|
+
const method = event.requestContext?.http?.method ?? "GET";
|
|
608
|
+
const init: RequestInit = {
|
|
609
|
+
headers,
|
|
610
|
+
method,
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
if (method !== "GET" && method !== "HEAD" && event.body !== undefined) {
|
|
614
|
+
init.body =
|
|
615
|
+
event.isBase64Encoded === true ? Buffer.from(event.body, "base64") : event.body;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return new Request(`${protocol}://${host}${rawPath}${rawQueryString}`, init);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function lambdaRequestHost(
|
|
622
|
+
headers: Headers,
|
|
623
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
624
|
+
): string | null {
|
|
625
|
+
if (options.hostPolicy === "trusted-proxy") {
|
|
626
|
+
return firstForwardedValue(headers.get("x-forwarded-host")) ?? headers.get("host");
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return headers.get("host");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function lambdaHostPolicy(
|
|
633
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
634
|
+
): RequestHostPolicy | undefined {
|
|
635
|
+
return options.hostPolicy ?? (process.env.NODE_ENV === "production" ? "strict" : undefined);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function lambdaRequestProtocol(
|
|
639
|
+
event: AwsLambdaHttpEventV2,
|
|
640
|
+
headers: Headers,
|
|
641
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
642
|
+
): "http" | "https" {
|
|
643
|
+
if (options.trustForwardedProto === true) {
|
|
644
|
+
return normalizeRequestProtocol(firstForwardedValue(headers.get("x-forwarded-proto")));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return normalizeRequestProtocol(event.requestContext?.http?.protocol);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function normalizeRequestProtocol(value: string | undefined): "http" | "https" {
|
|
651
|
+
if (value === undefined || value === "") {
|
|
652
|
+
return "https";
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const normalized = value.toLowerCase();
|
|
656
|
+
return normalized === "http" || normalized.startsWith("http/") ? "http" : "https";
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function eventHeaders(event: AwsLambdaHttpEventV2): Headers {
|
|
660
|
+
const headers = new Headers();
|
|
661
|
+
|
|
662
|
+
for (const [name, value] of Object.entries(event.headers ?? {})) {
|
|
663
|
+
if (value !== undefined) {
|
|
664
|
+
headers.set(name, value);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (event.cookies !== undefined && event.cookies.length > 0 && !headers.has("cookie")) {
|
|
669
|
+
headers.set("cookie", event.cookies.join("; "));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return headers;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function responseToLambdaResult(
|
|
676
|
+
response: Response,
|
|
677
|
+
phases?: Record<string, number> | undefined,
|
|
678
|
+
): Promise<AwsLambdaHttpResultV2> {
|
|
679
|
+
const headers: Record<string, string> = {};
|
|
680
|
+
response.headers.forEach((value, key) => {
|
|
681
|
+
if (key !== "set-cookie") {
|
|
682
|
+
headers[key] = value;
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const cookies = responseCookies(response.headers);
|
|
687
|
+
|
|
688
|
+
if (response.body === null) {
|
|
689
|
+
return {
|
|
690
|
+
body: "",
|
|
691
|
+
...(cookies.length === 0 ? {} : { cookies }),
|
|
692
|
+
headers,
|
|
693
|
+
isBase64Encoded: false,
|
|
694
|
+
statusCode: response.status,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const streamDrainStartedAt = phaseStartedAt(phases);
|
|
699
|
+
const bytes = await drainBufferedResponseBody(response.body, phases);
|
|
700
|
+
addPhaseDuration(phases, streamDrainStartedAt, "streamDrainMs");
|
|
701
|
+
|
|
702
|
+
const bodyEncodeStartedAt = phaseStartedAt(phases);
|
|
703
|
+
const contentType = response.headers.get("content-type");
|
|
704
|
+
const text = isTextContentType(contentType);
|
|
705
|
+
const result = {
|
|
706
|
+
body: text ? bytes.toString("utf8") : bytes.toString("base64"),
|
|
707
|
+
...(cookies.length === 0 ? {} : { cookies }),
|
|
708
|
+
headers,
|
|
709
|
+
isBase64Encoded: !text,
|
|
710
|
+
statusCode: response.status,
|
|
711
|
+
};
|
|
712
|
+
addPhaseDuration(phases, bodyEncodeStartedAt, "bodyEncodeMs");
|
|
713
|
+
|
|
714
|
+
return result;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async function drainBufferedResponseBody(
|
|
718
|
+
body: ReadableStream<Uint8Array>,
|
|
719
|
+
phases?: Record<string, number> | undefined,
|
|
720
|
+
): Promise<Buffer> {
|
|
721
|
+
const reader = body.getReader();
|
|
722
|
+
const chunks: Uint8Array[] = [];
|
|
723
|
+
let byteLength = 0;
|
|
724
|
+
|
|
725
|
+
while (true) {
|
|
726
|
+
const readStartedAt = phaseStartedAt(phases);
|
|
727
|
+
const result = await reader.read();
|
|
728
|
+
addPhaseDuration(phases, readStartedAt, "streamReadMs");
|
|
729
|
+
|
|
730
|
+
if (result.done) {
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
chunks.push(result.value);
|
|
735
|
+
byteLength += result.value.byteLength;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const concatStartedAt = phaseStartedAt(phases);
|
|
739
|
+
const bytes = concatUint8ArrayChunks(chunks, byteLength);
|
|
740
|
+
addPhaseDuration(phases, concatStartedAt, "streamConcatMs");
|
|
741
|
+
|
|
742
|
+
return bytes;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function concatUint8ArrayChunks(chunks: readonly Uint8Array[], byteLength: number): Buffer {
|
|
746
|
+
if (chunks.length === 0 || byteLength === 0) {
|
|
747
|
+
return Buffer.alloc(0);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (chunks.length === 1) {
|
|
751
|
+
const [chunk] = chunks;
|
|
752
|
+
if (chunk === undefined) {
|
|
753
|
+
return Buffer.alloc(0);
|
|
754
|
+
}
|
|
755
|
+
return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const bytes = Buffer.allocUnsafe(byteLength);
|
|
759
|
+
let offset = 0;
|
|
760
|
+
|
|
761
|
+
for (const chunk of chunks) {
|
|
762
|
+
bytes.set(chunk, offset);
|
|
763
|
+
offset += chunk.byteLength;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return bytes;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function createAwsLambdaTimingPhases(
|
|
770
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
771
|
+
): Record<string, number> | undefined {
|
|
772
|
+
return options.timings === true ? {} : undefined;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function awsLambdaRenderLogger(
|
|
776
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
777
|
+
): AppRouterLogger | undefined {
|
|
778
|
+
return options.timings === true ? options.logger : undefined;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function phaseStartedAt(phases: Record<string, number> | undefined): number | undefined {
|
|
782
|
+
return phases === undefined ? undefined : logNow();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function finishPhase(
|
|
786
|
+
phases: Record<string, number> | undefined,
|
|
787
|
+
startedAt: number | undefined,
|
|
788
|
+
name: string,
|
|
789
|
+
): void {
|
|
790
|
+
if (phases !== undefined && startedAt !== undefined) {
|
|
791
|
+
phases[name] = logDurationMs(startedAt);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function addPhaseDuration(
|
|
796
|
+
phases: Record<string, number> | undefined,
|
|
797
|
+
startedAt: number | undefined,
|
|
798
|
+
name: string,
|
|
799
|
+
): void {
|
|
800
|
+
if (phases !== undefined && startedAt !== undefined) {
|
|
801
|
+
phases[name] = (phases[name] ?? 0) + logDurationMs(startedAt);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function emitAwsLambdaTiming(
|
|
806
|
+
options: AwsLambdaRequestHandlerOptions,
|
|
807
|
+
logFields: RouterRequestLogFields,
|
|
808
|
+
status: number,
|
|
809
|
+
startedAt: number,
|
|
810
|
+
phases: Record<string, number> | undefined,
|
|
811
|
+
): void {
|
|
812
|
+
if (phases === undefined) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
emitRouterLog(options.logger, "debug", {
|
|
817
|
+
...logFields,
|
|
818
|
+
durationMs: logDurationMs(startedAt),
|
|
819
|
+
phases,
|
|
820
|
+
status,
|
|
821
|
+
type: "router:request:timing",
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async function streamResponseToLambda(
|
|
826
|
+
response: Response,
|
|
827
|
+
responseStream: AwsLambdaStreamingResponseStream,
|
|
828
|
+
runtime: AwsLambdaRuntime,
|
|
829
|
+
phases?: Record<string, number> | undefined,
|
|
830
|
+
): Promise<void> {
|
|
831
|
+
const stream = runtime.HttpResponseStream.from(
|
|
832
|
+
responseStream,
|
|
833
|
+
responseStreamingMetadata(response),
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
try {
|
|
837
|
+
if (response.body === null) {
|
|
838
|
+
stream.end();
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const reader = response.body.getReader();
|
|
843
|
+
|
|
844
|
+
while (true) {
|
|
845
|
+
const streamWaitStartedAt = phaseStartedAt(phases);
|
|
846
|
+
const result = await reader.read();
|
|
847
|
+
addPhaseDuration(phases, streamWaitStartedAt, "streamWaitMs");
|
|
848
|
+
|
|
849
|
+
if (result.done) {
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const streamWriteStartedAt = phaseStartedAt(phases);
|
|
854
|
+
await writeStreamingChunk(stream, result.value);
|
|
855
|
+
addPhaseDuration(phases, streamWriteStartedAt, "streamWriteMs");
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
stream.end();
|
|
859
|
+
} catch (error) {
|
|
860
|
+
if (typeof stream.destroy === "function") {
|
|
861
|
+
stream.destroy(error);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
throw error;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function responseStreamingMetadata(
|
|
870
|
+
response: Response,
|
|
871
|
+
): AwsLambdaStreamingResponseMetadata {
|
|
872
|
+
const headers: Record<string, string> = {};
|
|
873
|
+
response.headers.forEach((value, key) => {
|
|
874
|
+
if (key !== "set-cookie") {
|
|
875
|
+
headers[key] = value;
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
const cookies = responseCookies(response.headers);
|
|
880
|
+
|
|
881
|
+
return {
|
|
882
|
+
...(cookies.length === 0 ? {} : { cookies }),
|
|
883
|
+
headers,
|
|
884
|
+
statusCode: response.status,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async function writeStreamingChunk(
|
|
889
|
+
stream: AwsLambdaStreamingResponseStream,
|
|
890
|
+
chunk: Uint8Array,
|
|
891
|
+
): Promise<void> {
|
|
892
|
+
if (stream.write(chunk) !== false) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (typeof stream.once !== "function") {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
await new Promise<void>((resolve) => {
|
|
901
|
+
stream.once?.("drain", resolve);
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
interface AwsLambdaRuntime {
|
|
906
|
+
HttpResponseStream: {
|
|
907
|
+
from(
|
|
908
|
+
responseStream: AwsLambdaStreamingResponseStream,
|
|
909
|
+
metadata: AwsLambdaStreamingResponseMetadata,
|
|
910
|
+
): AwsLambdaStreamingResponseStream;
|
|
911
|
+
};
|
|
912
|
+
streamifyResponse<TContext>(
|
|
913
|
+
handler: AwsLambdaStreamingRequestHandler<TContext>,
|
|
914
|
+
): AwsLambdaStreamingRequestHandler<TContext>;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function awsLambdaRuntime(): AwsLambdaRuntime {
|
|
918
|
+
const runtime = (globalThis as { awslambda?: Partial<AwsLambdaRuntime> | undefined })
|
|
919
|
+
.awslambda;
|
|
920
|
+
|
|
921
|
+
if (
|
|
922
|
+
typeof runtime?.streamifyResponse !== "function" ||
|
|
923
|
+
typeof runtime.HttpResponseStream?.from !== "function"
|
|
924
|
+
) {
|
|
925
|
+
throw new Error(
|
|
926
|
+
"AWS Lambda response streaming requires the Node.js Lambda runtime awslambda.streamifyResponse() and awslambda.HttpResponseStream.from().",
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return runtime as AwsLambdaRuntime;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function firstForwardedValue(value: string | null): string | undefined {
|
|
934
|
+
const first = value?.split(",")[0]?.trim();
|
|
935
|
+
return first === undefined || first === "" ? undefined : first;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function responseCookies(headers: Headers): string[] {
|
|
939
|
+
const maybeHeaders = headers as Headers & { getSetCookie?: () => string[] };
|
|
940
|
+
if (typeof maybeHeaders.getSetCookie === "function") {
|
|
941
|
+
return maybeHeaders.getSetCookie();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const raw = headers.get("set-cookie");
|
|
945
|
+
return raw === null ? [] : splitSetCookieHeader(raw);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function splitSetCookieHeader(header: string): string[] {
|
|
949
|
+
const cookies: string[] = [];
|
|
950
|
+
let start = 0;
|
|
951
|
+
let inExpires = false;
|
|
952
|
+
|
|
953
|
+
for (let index = 0; index < header.length; index += 1) {
|
|
954
|
+
const remaining = header.slice(index).toLowerCase();
|
|
955
|
+
if (remaining.startsWith("expires=")) {
|
|
956
|
+
inExpires = true;
|
|
957
|
+
index += "expires=".length - 1;
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const char = header[index];
|
|
962
|
+
if (char === ";") {
|
|
963
|
+
inExpires = false;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (char === "," && !inExpires) {
|
|
968
|
+
cookies.push(header.slice(start, index).trim());
|
|
969
|
+
start = index + 1;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
cookies.push(header.slice(start).trim());
|
|
974
|
+
return cookies.filter((cookie) => cookie.length > 0);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function isTextContentType(contentType: string | null): boolean {
|
|
978
|
+
if (contentType === null) {
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const type = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
983
|
+
return (
|
|
984
|
+
type.startsWith("text/") ||
|
|
985
|
+
type === "application/json" ||
|
|
986
|
+
type === "application/javascript" ||
|
|
987
|
+
type === "application/x-javascript" ||
|
|
988
|
+
type === "application/xml" ||
|
|
989
|
+
type === "application/x-www-form-urlencoded" ||
|
|
990
|
+
type.endsWith("+json") ||
|
|
991
|
+
type.endsWith("+xml")
|
|
992
|
+
);
|
|
993
|
+
}
|