lambder 6.0.2 → 7.0.1
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/CHANGELOG.md +2316 -0
- package/README.md +60 -33
- package/dist/api/LambderApiAnswer.d.ts +40 -0
- package/dist/api/LambderApiAnswer.js +19 -0
- package/dist/api/LambderApiCallContext.d.ts +38 -0
- package/dist/api/LambderApiCallContext.js +13 -0
- package/dist/api/LambderApiDefinition.d.ts +18 -0
- package/dist/api/LambderApiDefinition.js +1 -0
- package/dist/api/LambderApiEnvelope.d.ts +67 -0
- package/dist/api/LambderApiEnvelope.js +180 -0
- package/dist/api/LambderApiGuards.d.ts +302 -0
- package/dist/api/LambderApiGuards.js +134 -0
- package/dist/api/LambderApiIdempotency.d.ts +122 -0
- package/dist/api/LambderApiIdempotency.js +330 -0
- package/dist/api/LambderApiPipeline.d.ts +134 -0
- package/dist/api/LambderApiPipeline.js +221 -0
- package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
- package/dist/api/LambderApiPolicyEngine.js +77 -0
- package/dist/api/LambderApiRateLimits.d.ts +206 -0
- package/dist/api/LambderApiRateLimits.js +239 -0
- package/dist/api/LambderApiRequest.d.ts +101 -0
- package/dist/api/LambderApiRequest.js +129 -0
- package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
- package/dist/api/LambderApiValidationRefusal.js +40 -0
- package/dist/client/LambderCaller.d.ts +62 -55
- package/dist/client/LambderCaller.js +147 -90
- package/dist/client/lambderFetchTransport.d.ts +9 -0
- package/dist/client/lambderFetchTransport.js +71 -0
- package/dist/client.d.ts +20 -10
- package/dist/client.js +11 -5
- package/dist/core/Lambder.d.ts +117 -253
- package/dist/core/Lambder.js +374 -341
- package/dist/core/LambderContext.d.ts +54 -44
- package/dist/core/LambderContext.js +41 -110
- package/dist/core/LambderCreateOptions.d.ts +285 -0
- package/dist/core/LambderCreateOptions.js +44 -0
- package/dist/core/LambderFiles.d.ts +1 -45
- package/dist/core/LambderFiles.js +18 -38
- package/dist/core/LambderIndexHtml.d.ts +37 -0
- package/dist/core/LambderIndexHtml.js +87 -0
- package/dist/core/LambderPolicyBuilders.d.ts +17 -0
- package/dist/core/LambderPolicyBuilders.js +16 -0
- package/dist/core/LambderPublicFiles.d.ts +5 -2
- package/dist/core/LambderPublicFiles.js +7 -2
- package/dist/core/LambderResolver.d.ts +8 -6
- package/dist/core/LambderResponse.d.ts +29 -11
- package/dist/core/LambderResponse.js +96 -49
- package/dist/core/LambderResponseBuilder.d.ts +18 -14
- package/dist/core/LambderResponseBuilder.js +19 -25
- package/dist/core/LambderRouting.d.ts +18 -7
- package/dist/core/LambderRouting.js +17 -7
- package/dist/core/LambderTemplatingEngine.d.ts +0 -62
- package/dist/core/LambderTemplatingEngine.js +7 -3
- package/dist/index.d.ts +85 -32
- package/dist/index.js +44 -16
- package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
- package/dist/invoke/LambderInvokeCaller.js +140 -335
- package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
- package/dist/invoke/LambderInvokeOutcome.js +129 -0
- package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
- package/dist/invoke/LambderLambdaEvent.js +187 -0
- package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
- package/dist/invoke/lambderHandlerTransport.js +89 -0
- package/dist/mock/LambderMockApp.d.ts +352 -0
- package/dist/mock/LambderMockApp.js +815 -0
- package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
- package/dist/mock/LambderMockBrowserCookies.js +76 -0
- package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
- package/dist/mock/LambderMockCallRecorder.js +183 -0
- package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
- package/dist/mock/LambderMockCreateOptions.js +9 -0
- package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
- package/dist/mock/LambderMockEntryRegistry.js +126 -0
- package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
- package/dist/mock/LambderMockFailureInjector.js +138 -0
- package/dist/mock/LambderMockTypes.d.ts +421 -0
- package/dist/mock/LambderMockTypes.js +8 -0
- package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
- package/dist/mock/lambderMockConsoleLogger.js +35 -0
- package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
- package/dist/mock/lambderMockInvokeTransport.js +52 -0
- package/dist/mock/lambderMockMswHandler.d.ts +99 -0
- package/dist/mock/lambderMockMswHandler.js +126 -0
- package/dist/mock.d.ts +34 -0
- package/dist/mock.js +27 -0
- package/dist/session/LambderSessionController.d.ts +199 -30
- package/dist/session/LambderSessionController.js +396 -82
- package/dist/session/LambderSessionCrypto.d.ts +66 -0
- package/dist/session/LambderSessionCrypto.js +101 -0
- package/dist/session/LambderSessionManager.d.ts +118 -80
- package/dist/session/LambderSessionManager.js +212 -184
- package/dist/shared/LambderI18n.d.ts +6 -6
- package/dist/shared/LambderI18n.js +1 -1
- package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
- package/dist/shared/contracts/LambderFileSource.js +19 -0
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
- package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
- package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
- package/dist/shared/contracts/LambderRateLimiter.js +24 -0
- package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
- package/dist/shared/contracts/LambderSessionStore.js +13 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
- package/dist/shared/transport/LambderApiTransport.js +65 -0
- package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
- package/dist/shared/transport/LambderCookieJar.js +246 -0
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
- package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
- package/dist/shared/util/LambderBase64.d.ts +10 -0
- package/dist/shared/util/LambderBase64.js +27 -0
- package/dist/shared/util/LambderCallAbort.d.ts +62 -0
- package/dist/shared/util/LambderCallAbort.js +80 -0
- package/dist/shared/util/LambderClientIp.d.ts +32 -0
- package/dist/shared/util/LambderClientIp.js +56 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
- package/dist/shared/util/LambderExpiringMap.js +217 -0
- package/dist/shared/util/LambderKeyFields.d.ts +32 -0
- package/dist/shared/util/LambderKeyFields.js +34 -0
- package/dist/shared/util/LambderNodeModules.d.ts +9 -0
- package/dist/shared/util/LambderNodeModules.js +39 -0
- package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
- package/dist/shared/util/LambderOptionChecks.js +33 -0
- package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
- package/dist/shared/util/LambderResponseBrand.js +18 -0
- package/dist/shared/util/LambderTextDigest.d.ts +17 -0
- package/dist/shared/util/LambderTextDigest.js +34 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
- package/dist/shared/util/LambderTypeUtilities.js +8 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
- package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
- package/dist/shared/wire/LambderApiContract.d.ts +129 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
- package/dist/shared/wire/LambderApiOptionValues.js +11 -0
- package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
- package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
- package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
- package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
- package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
- package/dist/shared/wire/LambderCallOptions.js +17 -0
- package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
- package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
- package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
- package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
- package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
- package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
- package/dist/shared/wire/LambderHttpStatus.js +1 -0
- package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
- package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
- package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
- package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
- package/dist/stores/LambderDdbCache.d.ts +12 -9
- package/dist/stores/LambderDdbCache.js +56 -47
- package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
- package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
- package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
- package/dist/stores/LambderDdbRateLimiter.js +47 -45
- package/dist/stores/LambderDdbSdk.d.ts +83 -6
- package/dist/stores/LambderDdbSdk.js +83 -2
- package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
- package/dist/stores/LambderDdbSessionStore.js +161 -0
- package/dist/stores/LambderHttpFileSource.d.ts +1 -1
- package/dist/stores/LambderHttpFileSource.js +10 -1
- package/dist/stores/LambderLocalFileSource.d.ts +15 -0
- package/dist/stores/LambderLocalFileSource.js +28 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
- package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
- package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
- package/dist/stores/LambderMemoryRateLimiter.js +64 -0
- package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
- package/dist/stores/LambderMemorySessionStore.js +74 -0
- package/dist/stores/LambderS3FileSource.d.ts +1 -1
- package/dist/stores/LambderS3FileSource.js +1 -1
- package/package.json +21 -19
- package/dist/client/LambderMSW.d.ts +0 -69
- package/dist/client/LambderMSW.js +0 -121
- package/dist/policies/LambderApiGuards.d.ts +0 -256
- package/dist/policies/LambderApiGuards.js +0 -94
- package/dist/policies/LambderApiIdempotency.d.ts +0 -58
- package/dist/policies/LambderApiIdempotency.js +0 -219
- package/dist/policies/LambderApiPolicies.d.ts +0 -42
- package/dist/policies/LambderApiPolicies.js +0 -52
- package/dist/policies/LambderApiRateLimits.d.ts +0 -132
- package/dist/policies/LambderApiRateLimits.js +0 -119
- package/dist/shared/LambderApiContract.d.ts +0 -57
- package/dist/shared/LambderApiOutcome.d.ts +0 -69
- package/dist/shared/LambderCallOptions.d.ts +0 -71
- package/dist/shared/LambderCallOptions.js +0 -16
- package/dist/shared/node-polyfills.d.ts +0 -4
- package/dist/shared/node-polyfills.js +0 -58
- package/dist/stores/LambderDdbIdempotency.js +0 -229
- package/dist/testing.d.ts +0 -9
- package/dist/testing.js +0 -8
- /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { LAMBDER_RESPONSE_BRAND } from "../shared/util/LambderResponseBrand.js";
|
|
2
|
+
import { bytesToBase64 } from "../shared/util/LambderBase64.js";
|
|
3
|
+
import { getCrypto } from "../shared/util/LambderNodeModules.js";
|
|
4
|
+
import { compressText } from "../shared/wire/LambderCompressionCodec.js";
|
|
5
|
+
import { getAnswerHeader, setAnswerHeader, addAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
|
|
6
|
+
const normalizeHeaders = (headers) => Object.fromEntries(Object.entries(headers ?? {}).map(([k, v]) => [k, Array.isArray(v) ? [...v] : [v]]));
|
|
4
7
|
/**
|
|
5
8
|
* Intermediate response object returned by all response builder methods and by
|
|
6
9
|
* route/api handlers. Bodies stay uncompressed and un-encoded so hooks can
|
|
@@ -11,6 +14,7 @@ export const normalizeHeaders = (headers) => Object.fromEntries(Object.entries(h
|
|
|
11
14
|
* the request: the thrown response becomes the response.
|
|
12
15
|
*/
|
|
13
16
|
export class LambderResponse {
|
|
17
|
+
[LAMBDER_RESPONSE_BRAND] = true;
|
|
14
18
|
statusCode;
|
|
15
19
|
headers;
|
|
16
20
|
body;
|
|
@@ -25,36 +29,63 @@ export class LambderResponse {
|
|
|
25
29
|
this.compress = init.compress ?? "auto";
|
|
26
30
|
this.etag = init.etag ?? "auto";
|
|
27
31
|
}
|
|
32
|
+
// The three header methods are the core's own header helpers over this
|
|
33
|
+
// response's map: the case-insensitive lookup, the replace-under-any-casing
|
|
34
|
+
// and the append-under-the-existing-casing rules are one implementation,
|
|
35
|
+
// not a copy per class, so an answer and a response can never disagree
|
|
36
|
+
// about what setting a header means.
|
|
28
37
|
getHeader(key) {
|
|
29
|
-
|
|
30
|
-
for (const [k, v] of Object.entries(this.headers)) {
|
|
31
|
-
if (k.toLowerCase() === lower)
|
|
32
|
-
return v;
|
|
33
|
-
}
|
|
34
|
-
return undefined;
|
|
38
|
+
return getAnswerHeader(this.headers, key);
|
|
35
39
|
}
|
|
36
40
|
setHeader(key, value) {
|
|
37
|
-
|
|
38
|
-
for (const k of Object.keys(this.headers)) {
|
|
39
|
-
if (k.toLowerCase() === lower)
|
|
40
|
-
delete this.headers[k];
|
|
41
|
-
}
|
|
42
|
-
this.headers[key] = Array.isArray(value) ? [...value] : [value];
|
|
41
|
+
setAnswerHeader(this.headers, key, value);
|
|
43
42
|
return this;
|
|
44
43
|
}
|
|
45
44
|
addHeader(key, value) {
|
|
46
|
-
|
|
47
|
-
const existingKey = Object.keys(this.headers).find((k) => k.toLowerCase() === lower);
|
|
48
|
-
if (existingKey) {
|
|
49
|
-
this.headers[existingKey].push(value);
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
this.headers[key] = [value];
|
|
53
|
-
}
|
|
45
|
+
addAnswerHeader(this.headers, key, value);
|
|
54
46
|
return this;
|
|
55
47
|
}
|
|
56
48
|
}
|
|
57
|
-
|
|
49
|
+
/**
|
|
50
|
+
* A handler's response as a core answer: what the API pipeline stores,
|
|
51
|
+
* replays and hands back. A Buffer body travels base64-encoded and marked
|
|
52
|
+
* as such, so the idempotency engine never caches it and finalization
|
|
53
|
+
* passes it through untouched; the compress and etag flags ride along so
|
|
54
|
+
* nothing a handler asked for is lost on the way through the core.
|
|
55
|
+
*/
|
|
56
|
+
export const answerFromResponse = (response) => {
|
|
57
|
+
const binary = Buffer.isBuffer(response.body);
|
|
58
|
+
return {
|
|
59
|
+
statusCode: response.statusCode,
|
|
60
|
+
headers: normalizeHeaders(response.headers),
|
|
61
|
+
body: response.body === null ? "" : binary ? bytesToBase64(response.body) : String(response.body),
|
|
62
|
+
isBodyBase64: response.isBodyBase64 || binary,
|
|
63
|
+
compress: response.compress,
|
|
64
|
+
etag: response.etag,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* An answer's status as the response model spells statuses.
|
|
69
|
+
*
|
|
70
|
+
* LambderHttpStatusCode is an authoring surface: it exists so `res.status(...)`
|
|
71
|
+
* offers the codes an app writes and catches the typo'd one. An answer is
|
|
72
|
+
* plain data that already left that surface (a replay the idempotency store
|
|
73
|
+
* persisted, a mock's answer, a third adapter's), so its status is a number
|
|
74
|
+
* and a code outside the union is not a reason to refuse a request the app
|
|
75
|
+
* has already answered. Stated once here rather than as a bare cast at the
|
|
76
|
+
* call site, so the widening is a decision a reader can see.
|
|
77
|
+
*/
|
|
78
|
+
const httpStatusOfAnswer = (statusCode) => statusCode;
|
|
79
|
+
/** A core answer as the response hooks, CORS and finalization work on. */
|
|
80
|
+
export const responseFromAnswer = (answer) => new LambderResponse({
|
|
81
|
+
statusCode: httpStatusOfAnswer(answer.statusCode),
|
|
82
|
+
headers: answer.headers,
|
|
83
|
+
body: answer.body,
|
|
84
|
+
isBodyBase64: answer.isBodyBase64 ?? false,
|
|
85
|
+
compress: answer.compress ?? "auto",
|
|
86
|
+
etag: answer.etag ?? "auto",
|
|
87
|
+
});
|
|
88
|
+
const isCompressibleContentType = (contentType) => {
|
|
58
89
|
if (!contentType)
|
|
59
90
|
return false;
|
|
60
91
|
const mime = (contentType.split(";")[0] ?? "").trim().toLowerCase();
|
|
@@ -72,7 +103,7 @@ export const isCompressibleContentType = (contentType) => {
|
|
|
72
103
|
"application/lambder-json-stream",
|
|
73
104
|
].includes(mime);
|
|
74
105
|
};
|
|
75
|
-
|
|
106
|
+
const acceptsEncoding = (acceptEncoding, encoding) => {
|
|
76
107
|
if (!acceptEncoding)
|
|
77
108
|
return false;
|
|
78
109
|
return acceptEncoding.split(",").some((part) => {
|
|
@@ -102,18 +133,25 @@ export const DEFAULT_FINALIZE_OPTIONS = {
|
|
|
102
133
|
etag: true,
|
|
103
134
|
maxResponseBytes: 5_500_000,
|
|
104
135
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
136
|
+
/**
|
|
137
|
+
* The headers a 304 leaves behind: they describe a body, and a 304 carries
|
|
138
|
+
* none. Everything else goes with it.
|
|
139
|
+
*
|
|
140
|
+
* A keep-list of cache headers instead of this drop-list would quietly make a
|
|
141
|
+
* revalidation the one exit of the request where the call's headers do not
|
|
142
|
+
* belong to the call: a cacheable GET that also slides a session cookie would
|
|
143
|
+
* stop refreshing it the moment the browser held the ETag, and a cross-origin
|
|
144
|
+
* revalidation would lose Access-Control-Allow-Origin, so the browser would
|
|
145
|
+
* refuse the 304 it had asked for.
|
|
146
|
+
*/
|
|
147
|
+
const HEADERS_DROPPED_ON_NOT_MODIFIED = ["content-type", "content-length", "content-encoding"];
|
|
148
|
+
/**
|
|
149
|
+
* Emit the format-specific Lambda response shape. Exported because the
|
|
150
|
+
* last-resort crash path has to emit without finalizing (finalization may be
|
|
151
|
+
* what failed) and must still get the shape right; hand-writing it there left
|
|
152
|
+
* the v1/v2 split in four places.
|
|
153
|
+
*/
|
|
154
|
+
export const emitResponse = (format, statusCode, headers, body, isBase64Encoded) => {
|
|
117
155
|
if (format === "v2") {
|
|
118
156
|
// Payload v2 has no multiValueHeaders: multi-values are comma-joined,
|
|
119
157
|
// except Set-Cookie which uses the dedicated cookies array.
|
|
@@ -135,7 +173,11 @@ const emitResponse = (format, statusCode, headers, body, isBase64Encoded) => {
|
|
|
135
173
|
* encoding, HEAD body stripping, and Lambda payload size guard. Emits the v1
|
|
136
174
|
* (REST API) or v2 (HTTP API / Function URL) response shape.
|
|
137
175
|
*/
|
|
138
|
-
export const finalizeResponse = async (
|
|
176
|
+
export const finalizeResponse = async (
|
|
177
|
+
// ctx.header rather than ctx.headers: the context already carries the
|
|
178
|
+
// case-insensitive lookup, and taking the raw map meant a second
|
|
179
|
+
// implementation of it lived here for the two headers this reads.
|
|
180
|
+
ctx, response, options, format = "v1") => {
|
|
139
181
|
const method = (ctx?.method ?? "GET").toUpperCase();
|
|
140
182
|
if (response.body === null) {
|
|
141
183
|
return emitResponse(format, response.statusCode, response.headers, "", false);
|
|
@@ -164,7 +206,7 @@ export const finalizeResponse = async (ctx, response, options, format = "v1") =>
|
|
|
164
206
|
// compress: true forces compression even with it globally off, so
|
|
165
207
|
// the settings fall back to the defaults rather than being absent.
|
|
166
208
|
const settings = options.compression ?? DEFAULT_RESPONSE_COMPRESSION_SETTINGS;
|
|
167
|
-
const acceptEncoding =
|
|
209
|
+
const acceptEncoding = ctx?.header("accept-encoding");
|
|
168
210
|
const encoding = settings.encodings.find((candidate) => acceptsEncoding(acceptEncoding, candidate));
|
|
169
211
|
if (encoding) {
|
|
170
212
|
// The same codec, quality and TEXT mode a stored record gets.
|
|
@@ -173,7 +215,7 @@ export const finalizeResponse = async (ctx, response, options, format = "v1") =>
|
|
|
173
215
|
}
|
|
174
216
|
}
|
|
175
217
|
if (Buffer.isBuffer(response.body) || response.getHeader("Content-Encoding")) {
|
|
176
|
-
outBody = bodyBuffer
|
|
218
|
+
outBody = bytesToBase64(bodyBuffer);
|
|
177
219
|
isBase64 = true;
|
|
178
220
|
}
|
|
179
221
|
else {
|
|
@@ -189,23 +231,28 @@ export const finalizeResponse = async (ctx, response, options, format = "v1") =>
|
|
|
189
231
|
if (crypto) {
|
|
190
232
|
const etagValue = `"${crypto.createHash("sha256").update(outBody).digest("hex").slice(0, 32)}"`;
|
|
191
233
|
response.setHeader("ETag", etagValue);
|
|
192
|
-
const ifNoneMatch =
|
|
234
|
+
const ifNoneMatch = ctx?.header("if-none-match");
|
|
193
235
|
if (ifNoneMatch && ifNoneMatch.split(",").map((s) => s.trim()).includes(etagValue)) {
|
|
194
|
-
const
|
|
195
|
-
for (const key
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
preservedHeaders[key] = value;
|
|
236
|
+
const notModifiedHeaders = {};
|
|
237
|
+
for (const [key, values] of Object.entries(response.headers)) {
|
|
238
|
+
if (!HEADERS_DROPPED_ON_NOT_MODIFIED.includes(key.toLowerCase()))
|
|
239
|
+
notModifiedHeaders[key] = values;
|
|
199
240
|
}
|
|
200
|
-
return emitResponse(format, 304,
|
|
241
|
+
return emitResponse(format, 304, notModifiedHeaders, "", false);
|
|
201
242
|
}
|
|
202
243
|
}
|
|
203
244
|
}
|
|
204
245
|
if (method === "HEAD") {
|
|
205
246
|
return emitResponse(format, response.statusCode, response.headers, "", false);
|
|
206
247
|
}
|
|
207
|
-
|
|
208
|
-
|
|
248
|
+
// What Lambda weighs is bytes. A base64 body is ASCII, so its length is
|
|
249
|
+
// its byte count; a plain UTF-8 one is not, and counting its UTF-16 code
|
|
250
|
+
// units under-reported a non-ASCII response by up to 3x, which is the one
|
|
251
|
+
// way this guard could pass a body Lambda then refuses with an opaque
|
|
252
|
+
// payload-size error and no envelope.
|
|
253
|
+
const outBytes = isBase64 ? outBody.length : Buffer.byteLength(outBody, "utf8");
|
|
254
|
+
if (outBytes > options.maxResponseBytes) {
|
|
255
|
+
throw new Error(`Lambder: final response body is ${outBytes} bytes which exceeds the configured ` +
|
|
209
256
|
`maxResponseBytes (${options.maxResponseBytes}). Lambda caps proxy responses at ~6MB. ` +
|
|
210
257
|
`Consider pagination or enabling compression.`);
|
|
211
258
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { LambderRenderContext } from "./LambderContext.js";
|
|
2
|
-
import { type LambderCookieOptions, type LambderClearCookieOptions } from "
|
|
2
|
+
import { type LambderCookieOptions, type LambderClearCookieOptions } from "../shared/wire/LambderCookie.js";
|
|
3
3
|
import type { LambderFiles } from "./LambderFiles.js";
|
|
4
|
-
import { LambderResponse, type
|
|
4
|
+
import { LambderResponse, type LambderHeadersInput } from "./LambderResponse.js";
|
|
5
|
+
import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
|
|
5
6
|
import { LambderSafeHtml } from "../shared/LambderHtml.js";
|
|
6
7
|
import type { LambderTemplateData } from "./LambderTemplatingEngine.js";
|
|
7
|
-
import type { LambderApiResponseConfig } from "../shared/LambderApiContract.js";
|
|
8
|
-
export type {
|
|
8
|
+
import type { LambderApiResponseConfig, LambderApiNullAnswerConfig } from "../shared/wire/LambderApiContract.js";
|
|
9
|
+
export type { LambderApiEnvelopeBody, LambderApiResponseConfig } from "../shared/wire/LambderApiContract.js";
|
|
9
10
|
export type LambderResponseOptions = {
|
|
10
|
-
statusCode?:
|
|
11
|
+
statusCode?: LambderHttpStatusCode;
|
|
11
12
|
headers?: LambderHeadersInput;
|
|
12
13
|
/** Shorthand for the Cache-Control header. */
|
|
13
14
|
cacheControl?: string;
|
|
@@ -22,14 +23,15 @@ export type LambderResponseOptions = {
|
|
|
22
23
|
* `message`). A bare `res.api(null)` compiles only when the output type
|
|
23
24
|
* itself allows null, so a success payload is always the declared output,
|
|
24
25
|
* which is what lets a typed caller (LambderInvokeCaller.api) promise it.
|
|
25
|
-
* Untyped resolvers (`TOutput = any`) accept anything, as before.
|
|
26
|
+
* Untyped resolvers (`TOutput = any`) accept anything, as before. This is
|
|
27
|
+
* the resolver's method type; the core's answer type is LambderApiAnswer.
|
|
26
28
|
*/
|
|
27
|
-
export type
|
|
29
|
+
export type LambderResolverApiMethod<TOutput, TResult> = {
|
|
28
30
|
(payload: TOutput, config?: LambderApiResponseConfig, options?: LambderResponseOptions): TResult;
|
|
29
|
-
(payload: null, config:
|
|
31
|
+
(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): TResult;
|
|
30
32
|
};
|
|
31
33
|
export type LambderRawResponseInit = {
|
|
32
|
-
statusCode:
|
|
34
|
+
statusCode: LambderHttpStatusCode;
|
|
33
35
|
headers?: LambderHeadersInput;
|
|
34
36
|
body: string | Buffer | null;
|
|
35
37
|
/** True when body is already a base64-encoded string. */
|
|
@@ -49,7 +51,9 @@ export default class LambderResponseBuilder<TResponse = any> {
|
|
|
49
51
|
private buildResponse;
|
|
50
52
|
/** The instance's file reader, which res.file and res.templateFile need. */
|
|
51
53
|
private requireFiles;
|
|
54
|
+
/** Appends a response header; applied onto the response once the handler has one, in call order. */
|
|
52
55
|
addHeader(key: string, value: string): void;
|
|
56
|
+
/** Replaces a response header; applied onto the response once the handler has one, in call order. */
|
|
53
57
|
setHeader(key: string, value: string | string[]): void;
|
|
54
58
|
/**
|
|
55
59
|
* Adds a Set-Cookie header. A function-form `domain` is resolved against
|
|
@@ -64,15 +68,15 @@ export default class LambderResponseBuilder<TResponse = any> {
|
|
|
64
68
|
* different cookie and deletes nothing.
|
|
65
69
|
*/
|
|
66
70
|
clearCookie(name: string, options?: LambderClearCookieOptions): void;
|
|
67
|
-
logToApiResponse(input:
|
|
71
|
+
logToApiResponse(input: unknown): void;
|
|
68
72
|
raw(init: LambderRawResponseInit): LambderResponse;
|
|
69
73
|
json(data: Record<string, any>, options?: LambderResponseOptions): LambderResponse;
|
|
70
74
|
text(data: string, options?: LambderResponseOptions): LambderResponse;
|
|
71
75
|
xml(data: string | LambderSafeHtml, options?: LambderResponseOptions): LambderResponse;
|
|
72
76
|
html(data: string | LambderSafeHtml, options?: LambderResponseOptions): LambderResponse;
|
|
73
|
-
status(statusCode:
|
|
77
|
+
status(statusCode: LambderHttpStatusCode, body?: string, options?: LambderResponseOptions): LambderResponse;
|
|
74
78
|
status404(data: string, options?: LambderResponseOptions): LambderResponse;
|
|
75
|
-
redirect(url: string, statusCode?:
|
|
79
|
+
redirect(url: string, statusCode?: LambderHttpStatusCode, options?: LambderResponseOptions): LambderResponse;
|
|
76
80
|
versionExpired(options?: LambderResponseOptions): LambderResponse;
|
|
77
81
|
fileBase64(fileBase64: string, mimeType: string, options?: LambderResponseOptions): LambderResponse;
|
|
78
82
|
/** A file from the files source as a response; 404 when there is none. */
|
|
@@ -89,8 +93,8 @@ export default class LambderResponseBuilder<TResponse = any> {
|
|
|
89
93
|
htmlVirtualSlots?: boolean;
|
|
90
94
|
}): Promise<LambderResponse>;
|
|
91
95
|
api(payload: TResponse, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
92
|
-
api(payload: null, config:
|
|
96
|
+
api(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): LambderResponse;
|
|
93
97
|
/** Same as api() but forces compression of the response body. */
|
|
94
98
|
apiBinary(payload: TResponse, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
95
|
-
apiBinary(payload: null, config:
|
|
99
|
+
apiBinary(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): LambderResponse;
|
|
96
100
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { serializeCookie, serializeClearCookie } from "
|
|
1
|
+
import { serializeCookie, serializeClearCookie } from "../shared/wire/LambderCookie.js";
|
|
2
2
|
import { LambderResponse } from "./LambderResponse.js";
|
|
3
|
+
import { buildApiEnvelope } from "../api/LambderApiEnvelope.js";
|
|
3
4
|
export default class LambderResponseBuilder {
|
|
4
5
|
files;
|
|
5
6
|
apiVersion;
|
|
@@ -29,21 +30,21 @@ export default class LambderResponseBuilder {
|
|
|
29
30
|
/** The instance's file reader, which res.file and res.templateFile need. */
|
|
30
31
|
requireFiles(method) {
|
|
31
32
|
if (!this.files)
|
|
32
|
-
throw new Error(
|
|
33
|
+
throw new Error(`Lambder: ${method} requires the files option at creation (e.g. files: new LambderLocalFileSource({ root }))`);
|
|
33
34
|
return this.files;
|
|
34
35
|
}
|
|
36
|
+
/** Appends a response header; applied onto the response once the handler has one, in call order. */
|
|
35
37
|
addHeader(key, value) {
|
|
36
38
|
if (!this.ctx)
|
|
37
|
-
throw new Error(".addHeader
|
|
38
|
-
this.ctx.
|
|
39
|
+
throw new Error("Lambder: res.addHeader needs the request context, and this response builder was created without one.");
|
|
40
|
+
this.ctx.responseHeaders.add(key, value);
|
|
39
41
|
}
|
|
40
42
|
;
|
|
43
|
+
/** Replaces a response header; applied onto the response once the handler has one, in call order. */
|
|
41
44
|
setHeader(key, value) {
|
|
42
45
|
if (!this.ctx)
|
|
43
|
-
throw new Error(".setHeader
|
|
44
|
-
this.ctx.
|
|
45
|
-
.filter((header) => header.key !== key);
|
|
46
|
-
this.ctx._otherInternal.setHeaderFnAccumulator.push({ key, value });
|
|
46
|
+
throw new Error("Lambder: res.setHeader needs the request context, and this response builder was created without one.");
|
|
47
|
+
this.ctx.responseHeaders.set(key, value);
|
|
47
48
|
}
|
|
48
49
|
;
|
|
49
50
|
/**
|
|
@@ -53,7 +54,7 @@ export default class LambderResponseBuilder {
|
|
|
53
54
|
*/
|
|
54
55
|
setCookie(name, value, options) {
|
|
55
56
|
if (!this.ctx)
|
|
56
|
-
throw new Error(".setCookie
|
|
57
|
+
throw new Error("Lambder: res.setCookie needs the request context, and this response builder was created without one.");
|
|
57
58
|
this.addHeader("Set-Cookie", serializeCookie(name, value, options, this.ctx.host));
|
|
58
59
|
}
|
|
59
60
|
;
|
|
@@ -65,14 +66,14 @@ export default class LambderResponseBuilder {
|
|
|
65
66
|
*/
|
|
66
67
|
clearCookie(name, options) {
|
|
67
68
|
if (!this.ctx)
|
|
68
|
-
throw new Error(".clearCookie
|
|
69
|
+
throw new Error("Lambder: res.clearCookie needs the request context, and this response builder was created without one.");
|
|
69
70
|
this.addHeader("Set-Cookie", serializeClearCookie(name, options, this.ctx.host));
|
|
70
71
|
}
|
|
71
72
|
;
|
|
72
73
|
logToApiResponse(input) {
|
|
73
74
|
if (!this.ctx)
|
|
74
|
-
throw new Error(".logToApiResponse
|
|
75
|
-
this.ctx.
|
|
75
|
+
throw new Error("Lambder: res.logToApiResponse needs the request context, and this response builder was created without one.");
|
|
76
|
+
this.ctx.logList.push(input);
|
|
76
77
|
}
|
|
77
78
|
;
|
|
78
79
|
raw(init) {
|
|
@@ -157,19 +158,12 @@ export default class LambderResponseBuilder {
|
|
|
157
158
|
return this.buildResponse(200, "text/html; charset=utf-8", template.render(data), options);
|
|
158
159
|
}
|
|
159
160
|
;
|
|
160
|
-
api(payload,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
...(sessionExpired ? { sessionExpired } : {}),
|
|
167
|
-
...(notAuthorized ? { notAuthorized } : {}),
|
|
168
|
-
...(message ? { message } : {}),
|
|
169
|
-
...(errorMessage ? { errorMessage } : {}),
|
|
170
|
-
...(crash ? { crash } : {}),
|
|
171
|
-
...(finalLogList?.length ? { logList: finalLogList } : {}),
|
|
172
|
-
}, options);
|
|
161
|
+
api(payload, config = {}, options) {
|
|
162
|
+
// The envelope is the core's (one writer for both the server and the
|
|
163
|
+
// mock runtime); the logList channel is what this request accumulated
|
|
164
|
+
// unless the config names its own.
|
|
165
|
+
const envelope = buildApiEnvelope(this.apiVersion, payload, { ...config, logList: config.logList || this.ctx?.logList });
|
|
166
|
+
return this.json(envelope, options);
|
|
173
167
|
}
|
|
174
168
|
;
|
|
175
169
|
apiBinary(payload, config = {}, options) {
|
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
import type { LambderRenderContext } from "./LambderContext.js";
|
|
2
|
-
|
|
2
|
+
/** A route path as an app writes it: absolute, so a matcher and the index-HTML layer agree on the shape. */
|
|
3
|
+
export type LambderRoutePath = `/${string}`;
|
|
3
4
|
type CutAt<S extends string, D extends string> = S extends `${infer Head}${D}${string}` ? Head : S;
|
|
4
5
|
type ParamNameFrom<S extends string> = CutAt<CutAt<CutAt<CutAt<CutAt<CutAt<CutAt<S, "/">, ".">, "(">, "?">, "+">, "*">, "-">;
|
|
5
6
|
type PathParamNames<T extends string> = T extends `${string}:${infer Rest}` ? (ParamNameFrom<Rest> extends "" ? never : ParamNameFrom<Rest>) | PathParamNames<Rest> : never;
|
|
6
|
-
export type
|
|
7
|
+
export type LambderPathParamsOf<T extends string> = string extends T ? Record<string, string> : T extends `${string}(${string}` ? Record<string, string> : [PathParamNames<T>] extends [never] ? Record<string, string> : {
|
|
7
8
|
[K in PathParamNames<T>]: string;
|
|
8
9
|
};
|
|
9
|
-
export type
|
|
10
|
+
export type LambderRouteConditionFn = (ctx: LambderRenderContext) => boolean;
|
|
10
11
|
/** Structured route matcher: all provided fields must match. */
|
|
11
12
|
export type LambderRouteMatcher = {
|
|
12
|
-
path?:
|
|
13
|
+
path?: LambderRoutePath | RegExp;
|
|
13
14
|
host?: string | RegExp;
|
|
14
15
|
/** One or more HTTP methods; HEAD requests also match GET routes. */
|
|
15
16
|
method?: string | string[];
|
|
16
|
-
condition?:
|
|
17
|
+
condition?: LambderRouteConditionFn;
|
|
17
18
|
};
|
|
18
|
-
export type
|
|
19
|
+
export type LambderRouteCondition = LambderRoutePath | RegExp | LambderRouteConditionFn | LambderRouteMatcher;
|
|
19
20
|
/** Returns matched path params, or false when the route doesn't match. */
|
|
20
21
|
export type CompiledMatcher = (ctx: LambderRenderContext) => false | Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a request method is one the slot accepts, with HEAD folded into GET
|
|
24
|
+
* unless the list names HEAD itself: a HEAD is a GET whose body finalization
|
|
25
|
+
* strips, so an app that narrowed a slot to ["GET"] did not mean to 404 it.
|
|
26
|
+
*
|
|
27
|
+
* The three places that gate on a method (a route matcher's `method`,
|
|
28
|
+
* servePublicFiles and serveIndexHtml) share this one rule, so neighbouring
|
|
29
|
+
* slots cannot disagree about what a method means.
|
|
30
|
+
*/
|
|
31
|
+
export declare const allowsRequestMethod: (methods: ReadonlySet<string>, requestMethod: string) => boolean;
|
|
21
32
|
/** Compile a route condition once at registration time. */
|
|
22
|
-
export declare const compileRouteMatcher: (condition:
|
|
33
|
+
export declare const compileRouteMatcher: (condition: LambderRouteCondition) => CompiledMatcher;
|
|
23
34
|
export {};
|
|
@@ -27,6 +27,21 @@ const compilePathMatcher = (path) => {
|
|
|
27
27
|
return params;
|
|
28
28
|
};
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Whether a request method is one the slot accepts, with HEAD folded into GET
|
|
32
|
+
* unless the list names HEAD itself: a HEAD is a GET whose body finalization
|
|
33
|
+
* strips, so an app that narrowed a slot to ["GET"] did not mean to 404 it.
|
|
34
|
+
*
|
|
35
|
+
* The three places that gate on a method (a route matcher's `method`,
|
|
36
|
+
* servePublicFiles and serveIndexHtml) share this one rule, so neighbouring
|
|
37
|
+
* slots cannot disagree about what a method means.
|
|
38
|
+
*/
|
|
39
|
+
export const allowsRequestMethod = (methods, requestMethod) => {
|
|
40
|
+
const method = requestMethod.toUpperCase();
|
|
41
|
+
if (methods.has(method))
|
|
42
|
+
return true;
|
|
43
|
+
return method === "HEAD" && methods.has("GET");
|
|
44
|
+
};
|
|
30
45
|
/** Compile a route condition once at registration time. */
|
|
31
46
|
export const compileRouteMatcher = (condition) => {
|
|
32
47
|
if (typeof condition === "string" || condition instanceof RegExp) {
|
|
@@ -42,13 +57,8 @@ export const compileRouteMatcher = (condition) => {
|
|
|
42
57
|
? new Set((Array.isArray(matcher.method) ? matcher.method : [matcher.method]).map((m) => m.toUpperCase()))
|
|
43
58
|
: null;
|
|
44
59
|
return (ctx) => {
|
|
45
|
-
if (methods)
|
|
46
|
-
|
|
47
|
-
if (requestMethod === "HEAD" && !methods.has("HEAD"))
|
|
48
|
-
requestMethod = "GET";
|
|
49
|
-
if (!methods.has(requestMethod))
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
60
|
+
if (methods && !allowsRequestMethod(methods, ctx.method))
|
|
61
|
+
return false;
|
|
52
62
|
if (matcher.host !== undefined) {
|
|
53
63
|
if (typeof matcher.host === "string") {
|
|
54
64
|
if (ctx.host.toLowerCase() !== matcher.host.toLowerCase())
|
|
@@ -1,66 +1,4 @@
|
|
|
1
1
|
import { type LambderHtmlValue } from "../shared/LambderHtml.js";
|
|
2
|
-
/**
|
|
3
|
-
* LambderTemplatingEngine: a comment-only HTML template engine.
|
|
4
|
-
*
|
|
5
|
-
* Fully standalone: it has no dependency on Lambder routing or file serving,
|
|
6
|
-
* and can template any HTML: app shells, emails, error pages. res.templateFile
|
|
7
|
-
* uses it internally to render HTML files from the files source per request.
|
|
8
|
-
*
|
|
9
|
-
* Every construct is an HTML comment. That is the whole point: templates
|
|
10
|
-
* survive HTML build pipelines (e.g. Vite) untouched, and are invisible in the
|
|
11
|
-
* browser during frontend development, where the default content between the
|
|
12
|
-
* markers renders as-is.
|
|
13
|
-
*
|
|
14
|
-
* Syntax:
|
|
15
|
-
*
|
|
16
|
-
* <!--slot:name-->default content<!--/slot:name--> replaceable region;
|
|
17
|
-
* default kept when the
|
|
18
|
-
* data omits "name"
|
|
19
|
-
* <!--slot:name/--> insert-only point
|
|
20
|
-
* <!--if:name--> ... <!--else--> ... <!--/if:name--> conditional block,
|
|
21
|
-
* shown when data.name
|
|
22
|
-
* is truthy
|
|
23
|
-
* <!--if:!name--> ... <!--/if:!name--> negated conditional
|
|
24
|
-
*
|
|
25
|
-
* Blocks nest freely (ifs in slots, slots in ifs). There are intentionally no
|
|
26
|
-
* loops or inline expressions: dynamic lists are built server-side with the
|
|
27
|
-
* html`...` tagged template and passed in as a slot value. Attribute-position
|
|
28
|
-
* values (e.g. <html lang="...">) are handled with if/else around whole-tag
|
|
29
|
-
* variants.
|
|
30
|
-
*
|
|
31
|
-
* Data is dynamically typed: one Record<string, LambderHtmlValue> shared by
|
|
32
|
-
* slots and conditions.
|
|
33
|
-
* - strings/numbers are HTML-escaped on insertion (XSS-safe by default)
|
|
34
|
-
* - html`...` / raw() / jsonScript() values are inserted verbatim
|
|
35
|
-
* - arrays are flattened; null/undefined/false render the slot default
|
|
36
|
-
* - unknown data keys are ignored, so one data object can serve several
|
|
37
|
-
* templates with different slots
|
|
38
|
-
*
|
|
39
|
-
* Templates are parsed once (construction throws on unclosed or mismatched
|
|
40
|
-
* blocks with a descriptive message); render() is a cheap tree walk, safe to
|
|
41
|
-
* call per request. Discovered names are exposed on `slotNames` and
|
|
42
|
-
* `conditionNames` for runtime validation.
|
|
43
|
-
*
|
|
44
|
-
* @example
|
|
45
|
-
* ```typescript
|
|
46
|
-
* import { LambderTemplatingEngine, html } from "lambder";
|
|
47
|
-
*
|
|
48
|
-
* const template = new LambderTemplatingEngine(`
|
|
49
|
-
* <title><!--slot:title-->My Site<!--/slot:title--></title>
|
|
50
|
-
* <!--if:isBeta--><meta name="robots" content="noindex" /><!--/if:isBeta-->
|
|
51
|
-
* <!--slot:head/-->
|
|
52
|
-
* `);
|
|
53
|
-
*
|
|
54
|
-
* template.render({
|
|
55
|
-
* title: userInput, // escaped
|
|
56
|
-
* isBeta: stage === "beta",
|
|
57
|
-
* head: html`<link rel="canonical" href="${canonical}" />`, // verbatim
|
|
58
|
-
* });
|
|
59
|
-
*
|
|
60
|
-
* // Or load from disk (compile once, render many times):
|
|
61
|
-
* const emailTemplate = await LambderTemplatingEngine.fromFile("./templates/welcome.html");
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
2
|
export type LambderTemplateData = Record<string, LambderHtmlValue>;
|
|
65
3
|
export type LambderTemplatingEngineOptions = {
|
|
66
4
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getFS } from "../shared/
|
|
1
|
+
import { getFS } from "../shared/util/LambderNodeModules.js";
|
|
2
2
|
import { renderHtmlValue } from "../shared/LambderHtml.js";
|
|
3
3
|
const TOKEN_PATTERN = /<!--\s*(?:(slot:([\w-]+)\s*\/)|(slot:([\w-]+))|(\/slot:([\w-]+))|(if:(!?)([\w-]+))|(else)|(\/if:(!?)([\w-]+)))\s*-->/g;
|
|
4
4
|
/**
|
|
@@ -92,7 +92,11 @@ const renderNodes = (nodes, data) => {
|
|
|
92
92
|
out += value === undefined ? renderNodes(node.defaultNodes, data) : renderHtmlValue(value);
|
|
93
93
|
}
|
|
94
94
|
else {
|
|
95
|
-
|
|
95
|
+
// Own properties only, the same lookup the slot branch uses:
|
|
96
|
+
// through the prototype, <!--if:toString--> was unconditionally
|
|
97
|
+
// true on every render.
|
|
98
|
+
const value = Object.prototype.hasOwnProperty.call(data, node.name) ? data[node.name] : undefined;
|
|
99
|
+
const condition = !!value !== node.negated;
|
|
96
100
|
out += renderNodes(condition ? node.thenNodes : node.elseNodes, data);
|
|
97
101
|
}
|
|
98
102
|
}
|
|
@@ -130,7 +134,7 @@ export class LambderTemplatingEngine {
|
|
|
130
134
|
static async fromFile(filePath, options = {}) {
|
|
131
135
|
const fs = await getFS();
|
|
132
136
|
if (!fs)
|
|
133
|
-
throw new Error("LambderTemplatingEngine.fromFile requires a Node.js environment.");
|
|
137
|
+
throw new Error("Lambder: LambderTemplatingEngine.fromFile requires a Node.js environment.");
|
|
134
138
|
const source = await fs.promises.readFile(filePath, "utf8");
|
|
135
139
|
return new LambderTemplatingEngine(source, options);
|
|
136
140
|
}
|