lambder 4.4.1 → 4.5.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/Readme.md +23 -15
- package/dist/core/Lambder.d.ts +15 -7
- package/dist/core/Lambder.js +14 -12
- package/dist/core/LambderFiles.d.ts +85 -0
- package/dist/core/LambderFiles.js +116 -0
- package/dist/core/LambderPublicFiles.d.ts +8 -48
- package/dist/core/LambderPublicFiles.js +9 -83
- package/dist/core/LambderResponseBuilder.d.ts +15 -13
- package/dist/core/LambderResponseBuilder.js +19 -54
- package/dist/core/LambderTemplatingEngine.d.ts +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/stores/LambderS3FileSource.d.ts +3 -3
- package/package.json +1 -1
package/Readme.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Lambder is a highly opinionated dynamic serverless framework designed to facilitate the management and implementation of routes and APIs within AWS Lambda functions, specifically tailored for TypeScript projects. It provides a streamlined approach to handling HTTP requests, managing sessions, and defining API routes, making serverless application development more intuitive and structured.
|
|
4
4
|
|
|
5
|
+
**New in 4.5:**
|
|
6
|
+
|
|
7
|
+
- **`files` at creation replaces `publicPath`** (and `servePublicFiles({ source })`): one `LambderFileSource` configured once, `files: new LambderLocalFileSource({ root: path.resolve("./public") })` for the folder bundled with the deployment, `new LambderS3FileSource({...})` for S3 or R2, or your own `{ read(relativePath) }`. The instance owns one reader over it (`lambder.files`): path rule, in-memory file cache and compiled-template cache in one place, shared by `servePublicFiles`, `serveIndexHtml`, `res.file` and `res.templateFile`, so a build hosted from a bucket serves its index.html and templates from the bucket too, cached the same way as its assets. The cache is tuned or disabled beside the source, `files: { source, memoryCache }`, and `memoryCache` leaves `servePublicFiles`; `res.file` loses its SPA-era `fallback` option (the fallback chain replaced it).
|
|
8
|
+
|
|
5
9
|
**New in 4.4:**
|
|
6
10
|
|
|
7
11
|
- **`guardInputsProvider`** on `LambderCaller`: supply guardInput-mode guard values for every call from one place (the organization the UI is on, a device token) instead of at each call site; per-call `guardInputs` merge on top. Name the covered guards in the caller's second type parameter, `new LambderCaller<Contract, "orgPermission">({ guardInputsProvider, ... })`: calls to APIs whose guardInput guards are all covered no longer require the options argument, uncovered ones (a Turnstile token) still do, and naming guards makes the provider itself mandatory.
|
|
@@ -92,7 +96,7 @@ would silently widen the inferred policy types, which is why the curried
|
|
|
92
96
|
creator is the canonical entry.
|
|
93
97
|
|
|
94
98
|
```typescript
|
|
95
|
-
import { initLambder } from 'lambder';
|
|
99
|
+
import { initLambder, LambderLocalFileSource } from 'lambder';
|
|
96
100
|
import { z } from 'zod';
|
|
97
101
|
import * as path from 'path';
|
|
98
102
|
|
|
@@ -100,7 +104,7 @@ interface SessionData { userId: string; }
|
|
|
100
104
|
|
|
101
105
|
const lambder = initLambder<SessionData>().create({
|
|
102
106
|
apiPath: "/api",
|
|
103
|
-
|
|
107
|
+
files: new LambderLocalFileSource({ root: path.resolve(`./public`) }),
|
|
104
108
|
session: {
|
|
105
109
|
tableName: "website-session",
|
|
106
110
|
tableRegion: "us-east-1",
|
|
@@ -166,9 +170,9 @@ lambder
|
|
|
166
170
|
.addRoute({ path: "/stripe-webhook", method: "POST" }, (ctx, res) => {
|
|
167
171
|
return res.json({ received: true });
|
|
168
172
|
})
|
|
169
|
-
// Serve real files
|
|
170
|
-
// below
|
|
171
|
-
//
|
|
173
|
+
// Serve real files from the files source (see "Public file sources"
|
|
174
|
+
// below). This is a terminal fallback slot, NOT a catch-all route, so it
|
|
175
|
+
// can never shadow routes registered after it.
|
|
172
176
|
.servePublicFiles()
|
|
173
177
|
// Serve the app shell for GET/HEAD page requests nothing else handled
|
|
174
178
|
// (see "Hosting a frontend build" below).
|
|
@@ -216,7 +220,7 @@ For larger applications, split your APIs into separate modules:
|
|
|
216
220
|
```typescript
|
|
217
221
|
// user-api.ts
|
|
218
222
|
import { z } from "zod";
|
|
219
|
-
import Lambder from "lambder";
|
|
223
|
+
import Lambder, { LambderLocalFileSource } from "lambder";
|
|
220
224
|
|
|
221
225
|
export const userApi = <T>(l: Lambder<T>) => {
|
|
222
226
|
return l
|
|
@@ -237,7 +241,7 @@ export const userApi = <T>(l: Lambder<T>) => {
|
|
|
237
241
|
// index.ts
|
|
238
242
|
import { userApi } from "./user-api";
|
|
239
243
|
|
|
240
|
-
const lambder = new Lambder({
|
|
244
|
+
const lambder = new Lambder({ files: new LambderLocalFileSource({ root: './public' }) })
|
|
241
245
|
.use(userApi);
|
|
242
246
|
|
|
243
247
|
export type ApiContractType = typeof lambder.ApiContract;
|
|
@@ -443,27 +447,31 @@ Lambder has no SPA-specific machinery; hosting a frontend build is a recipe buil
|
|
|
443
447
|
|
|
444
448
|
#### Public file sources
|
|
445
449
|
|
|
446
|
-
`
|
|
450
|
+
The `files` option at creation is a `LambderFileSource`, an object with one method, `read(relativePath)`, returning `{ body, mimeType? }` or `null`. The instance owns one reader over it, `lambder.files`, and `servePublicFiles`, `serveIndexHtml`, `res.file` and `res.templateFile` all go through that reader, which does everything else for every source: traversal check, in-memory file cache for warm invocations (default 32MB, 2MB per file), compiled-template cache, mime fallback from the extension. Cache-Control (immutable for content-hashed names), ETag and compression are applied by the serving slot and the response pipeline. Built in:
|
|
447
451
|
|
|
448
452
|
```typescript
|
|
449
|
-
//
|
|
450
|
-
|
|
453
|
+
// A folder, typically the build output bundled with the deployment.
|
|
454
|
+
initLambder().create({ files: new LambderLocalFileSource({ root: path.resolve("./public") }) });
|
|
451
455
|
|
|
452
456
|
// S3. @aws-sdk/client-s3 is an optional peer dependency, loaded on first read.
|
|
453
|
-
|
|
454
|
-
|
|
457
|
+
initLambder().create({
|
|
458
|
+
files: new LambderS3FileSource({ bucket: "myapp-web", prefix: "v42/", clientConfig: { region: "eu-central-1" } }),
|
|
455
459
|
});
|
|
456
460
|
|
|
457
461
|
// Cloudflare R2, or any S3-compatible store: point the client at its endpoint.
|
|
458
|
-
|
|
459
|
-
|
|
462
|
+
initLambder().create({
|
|
463
|
+
files: new LambderS3FileSource({
|
|
460
464
|
bucket: "myapp-web",
|
|
461
465
|
clientConfig: { region: "auto", endpoint: "https://<account>.r2.cloudflarestorage.com", credentials: { accessKeyId, secretAccessKey } },
|
|
462
466
|
}),
|
|
463
467
|
});
|
|
464
468
|
|
|
465
469
|
// Anything else: implement read().
|
|
466
|
-
|
|
470
|
+
initLambder().create({ files: { read: async (relativePath) => myStore.get(relativePath) } });
|
|
471
|
+
|
|
472
|
+
// The in-memory file cache, tuned or off, beside any source.
|
|
473
|
+
initLambder().create({ files: { source: new LambderS3FileSource({ bucket: "myapp-web" }), memoryCache: { maxBytes: 64_000_000, maxFileBytes: 4_000_000 } } });
|
|
474
|
+
initLambder().create({ files: { source: new LambderLocalFileSource({ root }), memoryCache: false } });
|
|
467
475
|
```
|
|
468
476
|
|
|
469
477
|
A missing S3 object reads as null; grant `s3:ListBucket` besides `s3:GetObject`, otherwise S3 answers a missing key with AccessDenied, which propagates as an error instead of falling through. The object's Content-Type is used unless it is a generic octet-stream, in which case the extension decides. Lambda's ~6MB response cap still applies to anything proxied this way: redirect large downloads to the bucket or CDN URL instead of serving them.
|
package/dist/core/Lambder.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { type LambderSessionDataRefreshConfig } from "../session/LambderSessionM
|
|
|
9
9
|
import type { LambderCompressionOption } from "../stores/LambderDdbCompression.js";
|
|
10
10
|
import LambderSessionController, { type LambderSessionCookieOptions } from "../session/LambderSessionController.js";
|
|
11
11
|
import { type LambderPublicFilesOptions } from "./LambderPublicFiles.js";
|
|
12
|
+
import { LambderFiles, type LambderFilesOption } from "./LambderFiles.js";
|
|
12
13
|
import type { LambderApiGuard, LambderGuardMetaMap, LambderGuardsOption, LambderGuardDataOf, LambderGuardInputsOf } from "../policies/LambderApiGuards.js";
|
|
13
14
|
import type { LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderRateLimitOption } from "../policies/LambderApiRateLimits.js";
|
|
14
15
|
import type { LambderApiIdempotencyConfig } from "../policies/LambderApiIdempotency.js";
|
|
@@ -107,7 +108,14 @@ export type LambderSessionOptions<TSessionData = any> = {
|
|
|
107
108
|
* instance type ever needs a name.
|
|
108
109
|
*/
|
|
109
110
|
export type LambderCreateOptions<TSessionData = any> = {
|
|
110
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Where the app's files come from, for servePublicFiles, serveIndexHtml,
|
|
113
|
+
* res.file and res.templateFile: a LambderLocalFileSource over a folder
|
|
114
|
+
* (the build output bundled with the deployment), a LambderS3FileSource
|
|
115
|
+
* (S3, R2), or any LambderFileSource; or `{ source, memoryCache }` to
|
|
116
|
+
* tune or disable the in-memory file cache. Required by those features.
|
|
117
|
+
*/
|
|
118
|
+
files?: LambderFilesOption;
|
|
111
119
|
apiPath?: string;
|
|
112
120
|
apiVersion?: string;
|
|
113
121
|
/** Automatic gzip for compressible responses. `true` (the default) is `{ minBytes: 860 }`; `false` disables it. */
|
|
@@ -153,7 +161,8 @@ export type LambderCreateOptions<TSessionData = any> = {
|
|
|
153
161
|
export default class Lambder<TSessionData = any, _TContract extends Record<string, any> = {}, _TRateLimitPolicies extends Record<string, LambderApiRateLimitPolicyConfig> = {}, _TGuards extends Record<string, any> = {}, _TIdempotencyEnabled extends boolean = false> {
|
|
154
162
|
apiPath: string;
|
|
155
163
|
apiVersion: null | string;
|
|
156
|
-
|
|
164
|
+
/** The instance's file reader (source + caches), or null without the files option. */
|
|
165
|
+
files: LambderFiles | null;
|
|
157
166
|
/**
|
|
158
167
|
* Type property for extracting the API contract
|
|
159
168
|
* Use this to export your API types to the frontend
|
|
@@ -194,9 +203,8 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
194
203
|
setSessionExpiredRouteHandler(handler: FallbackHandlerFunction): this;
|
|
195
204
|
/**
|
|
196
205
|
* Terminal public-file layer. Runs only when no route matched, so it can
|
|
197
|
-
* never shadow routes registered after it. Serves files from `
|
|
198
|
-
*
|
|
199
|
-
* R2, or any LambderPublicFileSource), traversal-safe, mime-typed,
|
|
206
|
+
* never shadow routes registered after it. Serves files from the `files`
|
|
207
|
+
* source configured at creation, traversal-safe, mime-typed,
|
|
200
208
|
* memory-cached, with the immutable-cache heuristic for content-hashed
|
|
201
209
|
* assets; when the source has no such file the request falls through to
|
|
202
210
|
* setRouteFallbackHandler, where the app decides what remains (e.g.
|
|
@@ -209,8 +217,8 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
209
217
|
* gone; everything left is an app route (option `skipFilePaths` opts back
|
|
210
218
|
* into 404ing dotted paths). Only configured methods reach it, default
|
|
211
219
|
* GET/HEAD. Gated-out requests fall through to setRouteFallbackHandler.
|
|
212
|
-
* Without a handler,
|
|
213
|
-
* (markers optional) with no-cache.
|
|
220
|
+
* Without a handler, index.html from the files source is served via
|
|
221
|
+
* res.templateFile (markers optional) with no-cache.
|
|
214
222
|
*/
|
|
215
223
|
serveIndexHtml(handler?: FallbackHandlerFunction, options?: LambderIndexHtmlOptions): this;
|
|
216
224
|
/** Apply the serveIndexHtml gates; null means fall through. */
|
package/dist/core/Lambder.js
CHANGED
|
@@ -5,7 +5,8 @@ import { compileRouteMatcher } from "./LambderRouting.js";
|
|
|
5
5
|
import { applyCorsHeaders } from "./LambderCors.js";
|
|
6
6
|
import LambderSessionManager from "../session/LambderSessionManager.js";
|
|
7
7
|
import LambderSessionController from "../session/LambderSessionController.js";
|
|
8
|
-
import { LambderPublicFilesHandler
|
|
8
|
+
import { LambderPublicFilesHandler } from "./LambderPublicFiles.js";
|
|
9
|
+
import { LambderFiles } from "./LambderFiles.js";
|
|
9
10
|
import { isLambderApiError, LAMBDER_REFUSAL_CODES } from "../shared/LambderApiError.js";
|
|
10
11
|
import { LambderApiPolicyEngine } from "../policies/LambderApiPolicies.js";
|
|
11
12
|
import { createContext, isV2HttpEvent } from "./LambderContext.js";
|
|
@@ -33,7 +34,8 @@ import { createContext, isV2HttpEvent } from "./LambderContext.js";
|
|
|
33
34
|
export default class Lambder {
|
|
34
35
|
apiPath;
|
|
35
36
|
apiVersion;
|
|
36
|
-
|
|
37
|
+
/** The instance's file reader (source + caches), or null without the files option. */
|
|
38
|
+
files;
|
|
37
39
|
/**
|
|
38
40
|
* Type property for extracting the API contract
|
|
39
41
|
* Use this to export your API types to the frontend
|
|
@@ -66,7 +68,7 @@ export default class Lambder {
|
|
|
66
68
|
sessionTokenCookieKey = "LMDRSESSIONTKID";
|
|
67
69
|
sessionCsrfCookieKey = "LMDRSESSIONCSTK";
|
|
68
70
|
constructor(options = {}) {
|
|
69
|
-
this.
|
|
71
|
+
this.files = options.files ? new LambderFiles(options.files) : null;
|
|
70
72
|
this.apiPath = options.apiPath ?? "/api";
|
|
71
73
|
this.apiVersion = options.apiVersion ?? null;
|
|
72
74
|
this.finalizeOptions = {
|
|
@@ -129,17 +131,17 @@ export default class Lambder {
|
|
|
129
131
|
}
|
|
130
132
|
/**
|
|
131
133
|
* Terminal public-file layer. Runs only when no route matched, so it can
|
|
132
|
-
* never shadow routes registered after it. Serves files from `
|
|
133
|
-
*
|
|
134
|
-
* R2, or any LambderPublicFileSource), traversal-safe, mime-typed,
|
|
134
|
+
* never shadow routes registered after it. Serves files from the `files`
|
|
135
|
+
* source configured at creation, traversal-safe, mime-typed,
|
|
135
136
|
* memory-cached, with the immutable-cache heuristic for content-hashed
|
|
136
137
|
* assets; when the source has no such file the request falls through to
|
|
137
138
|
* setRouteFallbackHandler, where the app decides what remains (e.g.
|
|
138
139
|
* render an app shell with res.templateFile).
|
|
139
140
|
*/
|
|
140
141
|
servePublicFiles(options = {}) {
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
if (!this.files)
|
|
143
|
+
throw new Error("servePublicFiles requires the files option at creation (e.g. files: new LambderLocalFileSource({ root }))");
|
|
144
|
+
this.publicFilesHandler = new LambderPublicFilesHandler(this.files, options);
|
|
143
145
|
return this;
|
|
144
146
|
}
|
|
145
147
|
/**
|
|
@@ -148,8 +150,8 @@ export default class Lambder {
|
|
|
148
150
|
* gone; everything left is an app route (option `skipFilePaths` opts back
|
|
149
151
|
* into 404ing dotted paths). Only configured methods reach it, default
|
|
150
152
|
* GET/HEAD. Gated-out requests fall through to setRouteFallbackHandler.
|
|
151
|
-
* Without a handler,
|
|
152
|
-
* (markers optional) with no-cache.
|
|
153
|
+
* Without a handler, index.html from the files source is served via
|
|
154
|
+
* res.templateFile (markers optional) with no-cache.
|
|
153
155
|
*/
|
|
154
156
|
serveIndexHtml(handler, options = {}) {
|
|
155
157
|
this.indexHtmlConfig = { handler: handler ?? null, options };
|
|
@@ -331,7 +333,7 @@ export default class Lambder {
|
|
|
331
333
|
}
|
|
332
334
|
getResponseBuilder(ctx) {
|
|
333
335
|
return new LambderResponseBuilder({
|
|
334
|
-
|
|
336
|
+
files: this.files,
|
|
335
337
|
apiVersion: this.apiVersion,
|
|
336
338
|
ctx,
|
|
337
339
|
});
|
|
@@ -339,7 +341,7 @@ export default class Lambder {
|
|
|
339
341
|
;
|
|
340
342
|
getResolver(ctx) {
|
|
341
343
|
return new LambderResolver({
|
|
342
|
-
|
|
344
|
+
files: this.files,
|
|
343
345
|
apiVersion: this.apiVersion,
|
|
344
346
|
ctx,
|
|
345
347
|
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { LambderTemplatingEngine } from "./LambderTemplatingEngine.js";
|
|
2
|
+
/** A file a source serves: its bytes, and its mime type when the source knows it (otherwise resolved from the extension). */
|
|
3
|
+
export type LambderFile = {
|
|
4
|
+
body: Buffer;
|
|
5
|
+
mimeType?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Where an app's files come from: the `files` option at creation, read by
|
|
9
|
+
* servePublicFiles, serveIndexHtml, res.file and res.templateFile alike,
|
|
10
|
+
* through the instance's one reader (LambderFiles). Implement `read` over
|
|
11
|
+
* any backing store: LambderLocalFileSource (a folder), LambderS3FileSource
|
|
12
|
+
* (S3, or R2 and other S3-compatible stores), or your own. The reader does
|
|
13
|
+
* the rest for every source: traversal check, memory cache, mime fallback
|
|
14
|
+
* from the extension.
|
|
15
|
+
*/
|
|
16
|
+
export interface LambderFileSource {
|
|
17
|
+
/**
|
|
18
|
+
* The file at a relative path (no leading slash, no ".." segments: the
|
|
19
|
+
* reader rejects those before calling), or null when there is no such
|
|
20
|
+
* file, which lets a request fall through to the route fallback.
|
|
21
|
+
*/
|
|
22
|
+
read(relativePath: string): Promise<LambderFile | null>;
|
|
23
|
+
}
|
|
24
|
+
/** In-memory cache of files for warm invocations. Default: { maxBytes: 32MB, maxFileBytes: 2MB }. false disables it. */
|
|
25
|
+
export type LambderFileMemoryCacheOption = false | {
|
|
26
|
+
maxBytes?: number;
|
|
27
|
+
maxFileBytes?: number;
|
|
28
|
+
};
|
|
29
|
+
/** The `files` option at creation: a source, or a source with its memory cache tuned or off. */
|
|
30
|
+
export type LambderFilesOption = LambderFileSource | {
|
|
31
|
+
source: LambderFileSource;
|
|
32
|
+
memoryCache?: LambderFileMemoryCacheOption;
|
|
33
|
+
};
|
|
34
|
+
/** A file as the reader hands it out: path normalized, mime type resolved. */
|
|
35
|
+
export type LambderReadFile = {
|
|
36
|
+
body: Buffer;
|
|
37
|
+
mimeType: string;
|
|
38
|
+
relativePath: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Files from a folder on the Lambda's filesystem, typically the build output
|
|
42
|
+
* bundled into the deployment package. Reads stay under root.
|
|
43
|
+
*/
|
|
44
|
+
export declare class LambderLocalFileSource implements LambderFileSource {
|
|
45
|
+
private root;
|
|
46
|
+
constructor({ root }: {
|
|
47
|
+
root: string;
|
|
48
|
+
});
|
|
49
|
+
read(relativePath: string): Promise<LambderFile | null>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The path a source is asked for: leading slash stripped, traversal
|
|
53
|
+
* rejected; null for a path that names no file (empty, or a directory).
|
|
54
|
+
*/
|
|
55
|
+
export declare const toRelativePath: (target: string) => string | null;
|
|
56
|
+
/**
|
|
57
|
+
* The app's file reader, owned by the Lambder instance: one source, one
|
|
58
|
+
* path rule, one memory cache and one compiled-template cache, shared by
|
|
59
|
+
* every feature that reads files. Both caches live as long as the instance,
|
|
60
|
+
* i.e. across warm invocations.
|
|
61
|
+
*/
|
|
62
|
+
export declare class LambderFiles {
|
|
63
|
+
private source;
|
|
64
|
+
private cache;
|
|
65
|
+
private cacheBytes;
|
|
66
|
+
private maxBytes;
|
|
67
|
+
private maxFileBytes;
|
|
68
|
+
private templates;
|
|
69
|
+
constructor(option: LambderFilesOption);
|
|
70
|
+
/**
|
|
71
|
+
* The file at a request or handler path (leading slash optional), mime
|
|
72
|
+
* type resolved; null when the path is invalid or the source has none.
|
|
73
|
+
*/
|
|
74
|
+
read(path: string): Promise<LambderReadFile | null>;
|
|
75
|
+
/**
|
|
76
|
+
* The compiled template for an HTML file, compiled once per instance.
|
|
77
|
+
* A missing file throws: it is a server-side configuration error, not a
|
|
78
|
+
* client 404.
|
|
79
|
+
*/
|
|
80
|
+
template(path: string, options?: {
|
|
81
|
+
htmlVirtualSlots?: boolean;
|
|
82
|
+
}): Promise<LambderTemplatingEngine>;
|
|
83
|
+
/** Cache small files within the byte budget, evicting the oldest entries first. */
|
|
84
|
+
private remember;
|
|
85
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import mimeTypeResolver from "mime-types";
|
|
2
|
+
import { getFS, getPath } from "../shared/node-polyfills.js";
|
|
3
|
+
import { LambderTemplatingEngine } from "./LambderTemplatingEngine.js";
|
|
4
|
+
const DEFAULT_MEMORY_CACHE_MAX_BYTES = 32 * 1024 * 1024;
|
|
5
|
+
const DEFAULT_MEMORY_CACHE_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* Files from a folder on the Lambda's filesystem, typically the build output
|
|
8
|
+
* bundled into the deployment package. Reads stay under root.
|
|
9
|
+
*/
|
|
10
|
+
export class LambderLocalFileSource {
|
|
11
|
+
root;
|
|
12
|
+
constructor({ root }) {
|
|
13
|
+
this.root = root;
|
|
14
|
+
}
|
|
15
|
+
async read(relativePath) {
|
|
16
|
+
const fs = await getFS();
|
|
17
|
+
const path = await getPath();
|
|
18
|
+
if (!fs || !path)
|
|
19
|
+
throw new Error("LambderLocalFileSource requires a Node.js environment.");
|
|
20
|
+
const base = path.resolve(this.root);
|
|
21
|
+
const absolute = path.resolve(base, relativePath);
|
|
22
|
+
if (absolute !== base && !absolute.startsWith(base + path.sep))
|
|
23
|
+
return null;
|
|
24
|
+
const stat = await fs.promises.stat(absolute).catch(() => null);
|
|
25
|
+
if (!stat?.isFile())
|
|
26
|
+
return null;
|
|
27
|
+
return { body: await fs.promises.readFile(absolute) };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The path a source is asked for: leading slash stripped, traversal
|
|
32
|
+
* rejected; null for a path that names no file (empty, or a directory).
|
|
33
|
+
*/
|
|
34
|
+
export const toRelativePath = (target) => {
|
|
35
|
+
if (target.split("/").some((segment) => segment === ".."))
|
|
36
|
+
return null;
|
|
37
|
+
const relative = target.startsWith("/") ? target.slice(1) : target;
|
|
38
|
+
if (relative === "" || relative.endsWith("/"))
|
|
39
|
+
return null;
|
|
40
|
+
return relative;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The app's file reader, owned by the Lambder instance: one source, one
|
|
44
|
+
* path rule, one memory cache and one compiled-template cache, shared by
|
|
45
|
+
* every feature that reads files. Both caches live as long as the instance,
|
|
46
|
+
* i.e. across warm invocations.
|
|
47
|
+
*/
|
|
48
|
+
export class LambderFiles {
|
|
49
|
+
source;
|
|
50
|
+
cache;
|
|
51
|
+
cacheBytes = 0;
|
|
52
|
+
maxBytes;
|
|
53
|
+
maxFileBytes;
|
|
54
|
+
templates = new Map();
|
|
55
|
+
constructor(option) {
|
|
56
|
+
const { source, memoryCache } = "source" in option ? option : { source: option, memoryCache: undefined };
|
|
57
|
+
this.source = source;
|
|
58
|
+
this.cache = memoryCache === false ? null : new Map();
|
|
59
|
+
this.maxBytes = memoryCache === false ? 0 : (memoryCache?.maxBytes ?? DEFAULT_MEMORY_CACHE_MAX_BYTES);
|
|
60
|
+
this.maxFileBytes = memoryCache === false ? 0 : (memoryCache?.maxFileBytes ?? DEFAULT_MEMORY_CACHE_MAX_FILE_BYTES);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The file at a request or handler path (leading slash optional), mime
|
|
64
|
+
* type resolved; null when the path is invalid or the source has none.
|
|
65
|
+
*/
|
|
66
|
+
async read(path) {
|
|
67
|
+
const relativePath = toRelativePath(path);
|
|
68
|
+
if (relativePath === null)
|
|
69
|
+
return null;
|
|
70
|
+
const cached = this.cache?.get(relativePath);
|
|
71
|
+
if (cached)
|
|
72
|
+
return cached;
|
|
73
|
+
const file = await this.source.read(relativePath);
|
|
74
|
+
if (!file)
|
|
75
|
+
return null;
|
|
76
|
+
const entry = {
|
|
77
|
+
body: file.body,
|
|
78
|
+
mimeType: file.mimeType || mimeTypeResolver.lookup(relativePath) || "application/octet-stream",
|
|
79
|
+
relativePath,
|
|
80
|
+
};
|
|
81
|
+
this.remember(entry);
|
|
82
|
+
return entry;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The compiled template for an HTML file, compiled once per instance.
|
|
86
|
+
* A missing file throws: it is a server-side configuration error, not a
|
|
87
|
+
* client 404.
|
|
88
|
+
*/
|
|
89
|
+
async template(path, options = {}) {
|
|
90
|
+
const key = `${toRelativePath(path)}|${options.htmlVirtualSlots ? "v" : ""}`;
|
|
91
|
+
const cached = this.templates.get(key);
|
|
92
|
+
if (cached)
|
|
93
|
+
return cached;
|
|
94
|
+
const file = await this.read(path);
|
|
95
|
+
if (!file)
|
|
96
|
+
throw new Error(`templateFile: file not found in the files source: ${path}`);
|
|
97
|
+
const template = new LambderTemplatingEngine(file.body.toString("utf8"), { htmlVirtualSlots: options.htmlVirtualSlots });
|
|
98
|
+
this.templates.set(key, template);
|
|
99
|
+
return template;
|
|
100
|
+
}
|
|
101
|
+
/** Cache small files within the byte budget, evicting the oldest entries first. */
|
|
102
|
+
remember(entry) {
|
|
103
|
+
if (!this.cache || entry.body.length > this.maxFileBytes)
|
|
104
|
+
return;
|
|
105
|
+
for (const [key, value] of this.cache) {
|
|
106
|
+
if (this.cacheBytes + entry.body.length <= this.maxBytes)
|
|
107
|
+
break;
|
|
108
|
+
this.cache.delete(key);
|
|
109
|
+
this.cacheBytes -= value.body.length;
|
|
110
|
+
}
|
|
111
|
+
if (this.cacheBytes + entry.body.length <= this.maxBytes) {
|
|
112
|
+
this.cache.set(entry.relativePath, entry);
|
|
113
|
+
this.cacheBytes += entry.body.length;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -1,28 +1,8 @@
|
|
|
1
1
|
import type { LambderRenderContext } from "./LambderContext.js";
|
|
2
|
+
import type { LambderFiles } from "./LambderFiles.js";
|
|
2
3
|
import { LambderResponse } from "./LambderResponse.js";
|
|
3
|
-
/**
|
|
4
|
-
export type LambderPublicFile = {
|
|
5
|
-
body: Buffer;
|
|
6
|
-
mimeType?: string;
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Where servePublicFiles gets its files. Implement `read` over any backing
|
|
10
|
-
* store: LambderLocalFileSource (a folder, the default), LambderS3FileSource
|
|
11
|
-
* (S3, or R2 and other S3-compatible stores), or your own. The handler does
|
|
12
|
-
* the rest for every source: traversal check, memory cache, mime fallback
|
|
13
|
-
* from the extension, Cache-Control, ETag and compression.
|
|
14
|
-
*/
|
|
15
|
-
export interface LambderPublicFileSource {
|
|
16
|
-
/**
|
|
17
|
-
* The file at a relative path (no leading slash, no ".." segments: the
|
|
18
|
-
* handler rejects those before calling), or null when there is no such
|
|
19
|
-
* file, which lets the request fall through to the route fallback.
|
|
20
|
-
*/
|
|
21
|
-
read(relativePath: string): Promise<LambderPublicFile | null>;
|
|
22
|
-
}
|
|
4
|
+
/** Per-registration policy of servePublicFiles: how a request maps to a file and how the response is cached. */
|
|
23
5
|
export type LambderPublicFilesOptions = {
|
|
24
|
-
/** Where files come from. Default: LambderLocalFileSource over publicPath. */
|
|
25
|
-
source?: LambderPublicFileSource;
|
|
26
6
|
/**
|
|
27
7
|
* Map the request to a file path (app-owned logic, e.g. per-tenant
|
|
28
8
|
* roots: (ctx) => `${brand(ctx.host)}${ctx.path}`). Return
|
|
@@ -35,45 +15,25 @@ export type LambderPublicFilesOptions = {
|
|
|
35
15
|
immutablePattern?: RegExp | false;
|
|
36
16
|
/** Default: "public, max-age=31536000, immutable". */
|
|
37
17
|
immutableCacheControl?: string;
|
|
38
|
-
/** In-memory cache of files for warm invocations. Default: { maxBytes: 32MB, maxFileBytes: 2MB }. Set false to disable. */
|
|
39
|
-
memoryCache?: false | {
|
|
40
|
-
maxBytes?: number;
|
|
41
|
-
maxFileBytes?: number;
|
|
42
|
-
};
|
|
43
18
|
/**
|
|
44
19
|
* Compression per file: "auto" (default: compressible mime + size threshold),
|
|
45
20
|
* true/false, or a function, e.g. (ctx) => /\.(css|js|svg)$/.test(ctx.path).
|
|
46
21
|
*/
|
|
47
22
|
compress?: boolean | "auto" | ((ctx: LambderRenderContext) => boolean | "auto");
|
|
48
23
|
};
|
|
49
|
-
/**
|
|
50
|
-
* Files from a folder on the Lambda's filesystem, typically the build output
|
|
51
|
-
* bundled into the deployment package. Reads stay under root. The default
|
|
52
|
-
* source of servePublicFiles, over publicPath.
|
|
53
|
-
*/
|
|
54
|
-
export declare class LambderLocalFileSource implements LambderPublicFileSource {
|
|
55
|
-
private root;
|
|
56
|
-
constructor({ root }: {
|
|
57
|
-
root: string;
|
|
58
|
-
});
|
|
59
|
-
read(relativePath: string): Promise<LambderPublicFile | null>;
|
|
60
|
-
}
|
|
61
24
|
/**
|
|
62
25
|
* Terminal public-file handler registered via lambder.servePublicFiles().
|
|
63
26
|
* Runs only when no route matched, so it can never shadow routes registered
|
|
64
|
-
* after it. Serves files
|
|
65
|
-
* memory-cached
|
|
66
|
-
* falls through to the route fallback when the
|
|
27
|
+
* after it. Serves files through the instance's reader (traversal-safe,
|
|
28
|
+
* mime-typed, memory-cached) with the immutable-cache heuristic for
|
|
29
|
+
* content-hashed assets, and falls through to the route fallback when the
|
|
30
|
+
* source has no such file.
|
|
67
31
|
*/
|
|
68
32
|
export declare class LambderPublicFilesHandler {
|
|
69
|
-
private
|
|
33
|
+
private files;
|
|
70
34
|
private options;
|
|
71
|
-
|
|
72
|
-
private fileCacheBytes;
|
|
73
|
-
constructor(source: LambderPublicFileSource, options: LambderPublicFilesOptions);
|
|
35
|
+
constructor(files: LambderFiles, options: LambderPublicFilesOptions);
|
|
74
36
|
/** Serve the mapped file, or return null to fall through. */
|
|
75
37
|
handle(ctx: LambderRenderContext): Promise<LambderResponse | null>;
|
|
76
|
-
/** Read from the source, caching small files in memory for warm invocations. */
|
|
77
|
-
private readCached;
|
|
78
38
|
private cacheControlFor;
|
|
79
39
|
}
|
|
@@ -1,61 +1,22 @@
|
|
|
1
|
-
import mimeTypeResolver from "mime-types";
|
|
2
|
-
import { getFS, getPath } from "../shared/node-polyfills.js";
|
|
3
1
|
import { LambderResponse } from "./LambderResponse.js";
|
|
4
2
|
// Content-hashed build outputs (Vite/webpack/Rollup): a [-.] separated run of
|
|
5
3
|
// 8+ hash chars containing at least one digit, before the extension.
|
|
6
4
|
const DEFAULT_IMMUTABLE_PATTERN = /[-.](?=[A-Za-z0-9_-]*\d)[A-Za-z0-9_-]{8,}\.[A-Za-z0-9]+$/;
|
|
7
5
|
const DEFAULT_IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
8
6
|
const DEFAULT_CACHE_CONTROL = "public, max-age=3600";
|
|
9
|
-
const DEFAULT_MEMORY_CACHE_MAX_BYTES = 32 * 1024 * 1024;
|
|
10
|
-
const DEFAULT_MEMORY_CACHE_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
11
|
-
/**
|
|
12
|
-
* Files from a folder on the Lambda's filesystem, typically the build output
|
|
13
|
-
* bundled into the deployment package. Reads stay under root. The default
|
|
14
|
-
* source of servePublicFiles, over publicPath.
|
|
15
|
-
*/
|
|
16
|
-
export class LambderLocalFileSource {
|
|
17
|
-
root;
|
|
18
|
-
constructor({ root }) {
|
|
19
|
-
this.root = root;
|
|
20
|
-
}
|
|
21
|
-
async read(relativePath) {
|
|
22
|
-
const fs = await getFS();
|
|
23
|
-
const path = await getPath();
|
|
24
|
-
if (!fs || !path)
|
|
25
|
-
throw new Error("LambderLocalFileSource requires a Node.js environment.");
|
|
26
|
-
const base = path.resolve(this.root);
|
|
27
|
-
const absolute = path.resolve(base, relativePath);
|
|
28
|
-
if (absolute !== base && !absolute.startsWith(base + path.sep))
|
|
29
|
-
return null;
|
|
30
|
-
const stat = await fs.promises.stat(absolute).catch(() => null);
|
|
31
|
-
if (!stat?.isFile())
|
|
32
|
-
return null;
|
|
33
|
-
return { body: await fs.promises.readFile(absolute) };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/** Strip the leading slash and reject traversal; null for a path that names no file (empty, or a directory). */
|
|
37
|
-
const toRelativePath = (target) => {
|
|
38
|
-
if (target.split("/").some((segment) => segment === ".."))
|
|
39
|
-
return null;
|
|
40
|
-
const relative = target.startsWith("/") ? target.slice(1) : target;
|
|
41
|
-
if (relative === "" || relative.endsWith("/"))
|
|
42
|
-
return null;
|
|
43
|
-
return relative;
|
|
44
|
-
};
|
|
45
7
|
/**
|
|
46
8
|
* Terminal public-file handler registered via lambder.servePublicFiles().
|
|
47
9
|
* Runs only when no route matched, so it can never shadow routes registered
|
|
48
|
-
* after it. Serves files
|
|
49
|
-
* memory-cached
|
|
50
|
-
* falls through to the route fallback when the
|
|
10
|
+
* after it. Serves files through the instance's reader (traversal-safe,
|
|
11
|
+
* mime-typed, memory-cached) with the immutable-cache heuristic for
|
|
12
|
+
* content-hashed assets, and falls through to the route fallback when the
|
|
13
|
+
* source has no such file.
|
|
51
14
|
*/
|
|
52
15
|
export class LambderPublicFilesHandler {
|
|
53
|
-
|
|
16
|
+
files;
|
|
54
17
|
options;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
constructor(source, options) {
|
|
58
|
-
this.source = source;
|
|
18
|
+
constructor(files, options) {
|
|
19
|
+
this.files = files;
|
|
59
20
|
this.options = options;
|
|
60
21
|
}
|
|
61
22
|
/** Serve the mapped file, or return null to fall through. */
|
|
@@ -63,10 +24,7 @@ export class LambderPublicFilesHandler {
|
|
|
63
24
|
const mappedPath = this.options.path ? this.options.path(ctx) : ctx.path;
|
|
64
25
|
if (!mappedPath)
|
|
65
26
|
return null;
|
|
66
|
-
const
|
|
67
|
-
if (relativePath === null)
|
|
68
|
-
return null;
|
|
69
|
-
const file = await this.readCached(relativePath);
|
|
27
|
+
const file = await this.files.read(mappedPath);
|
|
70
28
|
if (!file)
|
|
71
29
|
return null;
|
|
72
30
|
const compressOption = this.options.compress;
|
|
@@ -75,44 +33,12 @@ export class LambderPublicFilesHandler {
|
|
|
75
33
|
statusCode: 200,
|
|
76
34
|
headers: {
|
|
77
35
|
"Content-Type": file.mimeType,
|
|
78
|
-
"Cache-Control": this.cacheControlFor(ctx, relativePath),
|
|
36
|
+
"Cache-Control": this.cacheControlFor(ctx, file.relativePath),
|
|
79
37
|
},
|
|
80
38
|
body: file.body,
|
|
81
39
|
compress,
|
|
82
40
|
});
|
|
83
41
|
}
|
|
84
|
-
/** Read from the source, caching small files in memory for warm invocations. */
|
|
85
|
-
async readCached(relativePath) {
|
|
86
|
-
const cached = this.fileCache.get(relativePath);
|
|
87
|
-
if (cached)
|
|
88
|
-
return cached;
|
|
89
|
-
const file = await this.source.read(relativePath);
|
|
90
|
-
if (!file)
|
|
91
|
-
return null;
|
|
92
|
-
const entry = {
|
|
93
|
-
body: file.body,
|
|
94
|
-
mimeType: file.mimeType || mimeTypeResolver.lookup(relativePath) || "application/octet-stream",
|
|
95
|
-
};
|
|
96
|
-
const cacheConfig = this.options.memoryCache;
|
|
97
|
-
if (cacheConfig !== false) {
|
|
98
|
-
const maxBytes = cacheConfig?.maxBytes ?? DEFAULT_MEMORY_CACHE_MAX_BYTES;
|
|
99
|
-
const maxFileBytes = cacheConfig?.maxFileBytes ?? DEFAULT_MEMORY_CACHE_MAX_FILE_BYTES;
|
|
100
|
-
if (entry.body.length <= maxFileBytes) {
|
|
101
|
-
// Evict oldest entries until the new file fits the budget.
|
|
102
|
-
for (const [key, value] of this.fileCache) {
|
|
103
|
-
if (this.fileCacheBytes + entry.body.length <= maxBytes)
|
|
104
|
-
break;
|
|
105
|
-
this.fileCache.delete(key);
|
|
106
|
-
this.fileCacheBytes -= value.body.length;
|
|
107
|
-
}
|
|
108
|
-
if (this.fileCacheBytes + entry.body.length <= maxBytes) {
|
|
109
|
-
this.fileCache.set(relativePath, entry);
|
|
110
|
-
this.fileCacheBytes += entry.body.length;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return entry;
|
|
115
|
-
}
|
|
116
42
|
cacheControlFor(ctx, relativePath) {
|
|
117
43
|
const cacheOption = this.options.cacheControl;
|
|
118
44
|
if (typeof cacheOption === "function")
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { LambderRenderContext } from "./LambderContext.js";
|
|
2
|
+
import type { LambderFiles } from "./LambderFiles.js";
|
|
2
3
|
import { LambderResponse, type HttpStatusCode, type LambderHeadersInput } from "./LambderResponse.js";
|
|
3
4
|
import { LambderSafeHtml } from "../shared/LambderHtml.js";
|
|
4
|
-
import {
|
|
5
|
+
import type { LambderTemplateData } from "./LambderTemplatingEngine.js";
|
|
5
6
|
import type { LambderApiResponseConfig } from "../shared/LambderApiContract.js";
|
|
6
7
|
export type { LambderApiResponse, LambderApiResponseConfig } from "../shared/LambderApiContract.js";
|
|
7
8
|
export type LambderResponseOptions = {
|
|
@@ -24,16 +25,17 @@ export type LambderRawResponseInit = {
|
|
|
24
25
|
etag?: boolean | "auto";
|
|
25
26
|
};
|
|
26
27
|
export default class LambderResponseBuilder<TResponse = any> {
|
|
27
|
-
protected
|
|
28
|
+
protected files: LambderFiles | null;
|
|
28
29
|
protected apiVersion: string | null;
|
|
29
30
|
protected ctx?: LambderRenderContext;
|
|
30
|
-
constructor({
|
|
31
|
-
|
|
31
|
+
constructor({ files, apiVersion, ctx }: {
|
|
32
|
+
files?: LambderFiles | null;
|
|
32
33
|
apiVersion?: string | null;
|
|
33
34
|
ctx?: LambderRenderContext;
|
|
34
35
|
});
|
|
35
36
|
private buildResponse;
|
|
36
|
-
|
|
37
|
+
/** The instance's file reader, which res.file and res.templateFile need. */
|
|
38
|
+
private requireFiles;
|
|
37
39
|
addHeader(key: string, value: string): void;
|
|
38
40
|
setHeader(key: string, value: string | string[]): void;
|
|
39
41
|
logToApiResponse(input: any): void;
|
|
@@ -47,15 +49,15 @@ export default class LambderResponseBuilder<TResponse = any> {
|
|
|
47
49
|
redirect(url: string, statusCode?: HttpStatusCode, options?: LambderResponseOptions): LambderResponse;
|
|
48
50
|
versionExpired(options?: LambderResponseOptions): LambderResponse;
|
|
49
51
|
fileBase64(fileBase64: string, mimeType: string, options?: LambderResponseOptions): LambderResponse;
|
|
50
|
-
file
|
|
51
|
-
|
|
52
|
-
}): Promise<LambderResponse>;
|
|
52
|
+
/** A file from the files source as a response; 404 when there is none. */
|
|
53
|
+
file(filePath: string, options?: LambderResponseOptions): Promise<LambderResponse>;
|
|
53
54
|
/**
|
|
54
|
-
* Render an HTML file
|
|
55
|
-
* (comment-based slots/conditionals) and return
|
|
56
|
-
* The compiled template is cached
|
|
57
|
-
*
|
|
58
|
-
*
|
|
55
|
+
* Render an HTML file from the files source through
|
|
56
|
+
* LambderTemplatingEngine (comment-based slots/conditionals) and return
|
|
57
|
+
* it as an HTML response. The compiled template is cached on the
|
|
58
|
+
* instance across warm invocations; a missing file throws (it is a
|
|
59
|
+
* server-side configuration error, not a client 404). Set
|
|
60
|
+
* htmlVirtualSlots to expose "title"/"head" slots on marker-less files.
|
|
59
61
|
*/
|
|
60
62
|
templateFile(filePath: string, data?: LambderTemplateData, options?: LambderResponseOptions & {
|
|
61
63
|
htmlVirtualSlots?: boolean;
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import mimeTypeResolver from "mime-types";
|
|
2
|
-
import { getFS, getPath } from "../shared/node-polyfills.js";
|
|
3
1
|
import { LambderResponse } from "./LambderResponse.js";
|
|
4
|
-
import { LambderTemplatingEngine } from "./LambderTemplatingEngine.js";
|
|
5
|
-
// Compiled templates survive across requests (builder instances are per-request).
|
|
6
|
-
const templateFileCache = new Map();
|
|
7
2
|
export default class LambderResponseBuilder {
|
|
8
|
-
|
|
3
|
+
files;
|
|
9
4
|
apiVersion;
|
|
10
5
|
ctx;
|
|
11
|
-
constructor({
|
|
12
|
-
this.
|
|
6
|
+
constructor({ files, apiVersion, ctx }) {
|
|
7
|
+
this.files = files ?? null;
|
|
13
8
|
this.apiVersion = apiVersion ?? null;
|
|
14
9
|
this.ctx = ctx;
|
|
15
10
|
}
|
|
@@ -30,25 +25,12 @@ export default class LambderResponseBuilder {
|
|
|
30
25
|
response.setHeader("Cache-Control", options.cacheControl);
|
|
31
26
|
return response;
|
|
32
27
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const publicPath = path.resolve(this.publicPath);
|
|
39
|
-
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
40
|
-
const absolutePath = path.resolve(publicPath, normalizedFilePath);
|
|
41
|
-
if (absolutePath !== publicPath && !absolutePath.startsWith(publicPath + path.sep))
|
|
42
|
-
return null;
|
|
43
|
-
try {
|
|
44
|
-
const stat = await fs.promises.stat(absolutePath);
|
|
45
|
-
return stat.isFile() ? absolutePath : null;
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
28
|
+
/** The instance's file reader, which res.file and res.templateFile need. */
|
|
29
|
+
requireFiles(method) {
|
|
30
|
+
if (!this.files)
|
|
31
|
+
throw new Error(`${method} requires the files option at creation (e.g. files: new LambderLocalFileSource({ root }))`);
|
|
32
|
+
return this.files;
|
|
50
33
|
}
|
|
51
|
-
;
|
|
52
34
|
addHeader(key, value) {
|
|
53
35
|
if (!this.ctx)
|
|
54
36
|
throw new Error(".addHeader function is not available within this hook");
|
|
@@ -130,41 +112,24 @@ export default class LambderResponseBuilder {
|
|
|
130
112
|
return response;
|
|
131
113
|
}
|
|
132
114
|
;
|
|
115
|
+
/** A file from the files source as a response; 404 when there is none. */
|
|
133
116
|
async file(filePath, options) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (!resolvedPath && options?.fallback) {
|
|
137
|
-
resolvedPath = await this.resolvePublicFilePath(options.fallback);
|
|
138
|
-
effectivePath = options.fallback;
|
|
139
|
-
}
|
|
140
|
-
if (!resolvedPath) {
|
|
117
|
+
const file = await this.requireFiles("res.file").read(filePath);
|
|
118
|
+
if (!file)
|
|
141
119
|
return this.status404("File not found", { etag: false });
|
|
142
|
-
|
|
143
|
-
const fs = await getFS();
|
|
144
|
-
if (!fs)
|
|
145
|
-
return this.status404("File not found", { etag: false });
|
|
146
|
-
const body = await fs.promises.readFile(resolvedPath);
|
|
147
|
-
const mimeType = mimeTypeResolver.lookup(effectivePath) || "application/octet-stream";
|
|
148
|
-
return this.buildResponse(200, mimeType, body, options);
|
|
120
|
+
return this.buildResponse(200, file.mimeType, file.body, options);
|
|
149
121
|
}
|
|
150
122
|
;
|
|
151
123
|
/**
|
|
152
|
-
* Render an HTML file
|
|
153
|
-
* (comment-based slots/conditionals) and return
|
|
154
|
-
* The compiled template is cached
|
|
155
|
-
*
|
|
156
|
-
*
|
|
124
|
+
* Render an HTML file from the files source through
|
|
125
|
+
* LambderTemplatingEngine (comment-based slots/conditionals) and return
|
|
126
|
+
* it as an HTML response. The compiled template is cached on the
|
|
127
|
+
* instance across warm invocations; a missing file throws (it is a
|
|
128
|
+
* server-side configuration error, not a client 404). Set
|
|
129
|
+
* htmlVirtualSlots to expose "title"/"head" slots on marker-less files.
|
|
157
130
|
*/
|
|
158
131
|
async templateFile(filePath, data, options) {
|
|
159
|
-
const
|
|
160
|
-
if (!resolvedPath)
|
|
161
|
-
throw new Error(`templateFile: file not found under publicPath: ${filePath}`);
|
|
162
|
-
const cacheKey = `${resolvedPath}|${options?.htmlVirtualSlots ? "v" : ""}`;
|
|
163
|
-
let template = templateFileCache.get(cacheKey);
|
|
164
|
-
if (!template) {
|
|
165
|
-
template = await LambderTemplatingEngine.fromFile(resolvedPath, { htmlVirtualSlots: options?.htmlVirtualSlots });
|
|
166
|
-
templateFileCache.set(cacheKey, template);
|
|
167
|
-
}
|
|
132
|
+
const template = await this.requireFiles("res.templateFile").template(filePath, { htmlVirtualSlots: options?.htmlVirtualSlots });
|
|
168
133
|
return this.buildResponse(200, "text/html; charset=utf-8", template.render(data), options);
|
|
169
134
|
}
|
|
170
135
|
;
|
|
@@ -4,7 +4,7 @@ import { type LambderHtmlValue } from "../shared/LambderHtml.js";
|
|
|
4
4
|
*
|
|
5
5
|
* Fully standalone: it has no dependency on Lambder routing or file serving,
|
|
6
6
|
* and can template any HTML: app shells, emails, error pages. res.templateFile
|
|
7
|
-
* uses it internally to render HTML files from
|
|
7
|
+
* uses it internally to render HTML files from the files source per request.
|
|
8
8
|
*
|
|
9
9
|
* Every construct is an HTML comment. That is the whole point: templates
|
|
10
10
|
* survive HTML build pipelines (e.g. Vite) untouched, and are invisible in the
|
package/dist/index.d.ts
CHANGED
|
@@ -15,8 +15,10 @@ export { LambderTemplatingEngine } from "./core/LambderTemplatingEngine.js";
|
|
|
15
15
|
export type { LambderTemplateData, LambderTemplatingEngineOptions } from "./core/LambderTemplatingEngine.js";
|
|
16
16
|
export type { LambderResponseOptions, LambderRawResponseInit, } from "./core/LambderResponseBuilder.js";
|
|
17
17
|
export type { LambderRouteMatcher, LambderCorsConfig, LambderCreateOptions, LambderSessionOptions, ConditionFunction, RouteCondition, PathParamsOf, LambderActionTools, LambderHandler, LambderIndexHtmlOptions, } from "./core/Lambder.js";
|
|
18
|
-
export { LambderPublicFilesHandler
|
|
19
|
-
export type { LambderPublicFilesOptions
|
|
18
|
+
export { LambderPublicFilesHandler } from "./core/LambderPublicFiles.js";
|
|
19
|
+
export type { LambderPublicFilesOptions } from "./core/LambderPublicFiles.js";
|
|
20
|
+
export { LambderFiles, LambderLocalFileSource } from "./core/LambderFiles.js";
|
|
21
|
+
export type { LambderFileSource, LambderFile, LambderFilesOption, LambderFileMemoryCacheOption, LambderReadFile } from "./core/LambderFiles.js";
|
|
20
22
|
export { LambderS3FileSource } from "./stores/LambderS3FileSource.js";
|
|
21
23
|
export type { LambderS3FileSourceOptions } from "./stores/LambderS3FileSource.js";
|
|
22
24
|
export type { LambderSessionCookieOptions } from "./session/LambderSessionController.js";
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,8 @@ export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtm
|
|
|
15
15
|
// Comment-based HTML templating engine (build-pipeline-safe slots and conditionals, standalone)
|
|
16
16
|
export { LambderTemplatingEngine } from "./core/LambderTemplatingEngine.js";
|
|
17
17
|
// Public file serving
|
|
18
|
-
export { LambderPublicFilesHandler
|
|
18
|
+
export { LambderPublicFilesHandler } from "./core/LambderPublicFiles.js";
|
|
19
|
+
export { LambderFiles, LambderLocalFileSource } from "./core/LambderFiles.js";
|
|
19
20
|
export { LambderS3FileSource } from "./stores/LambderS3FileSource.js";
|
|
20
21
|
export { LambderSessionDataRefreshError, LambderSessionReadError } from "./session/LambderSessionManager.js";
|
|
21
22
|
// DynamoDB-backed compressed cache (standalone, server-only)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { S3Client, S3ClientConfig } from "@aws-sdk/client-s3";
|
|
2
|
-
import type {
|
|
2
|
+
import type { LambderFile, LambderFileSource } from "../core/LambderFiles.js";
|
|
3
3
|
export type LambderS3FileSourceOptions = {
|
|
4
4
|
bucket: string;
|
|
5
5
|
/** Literal key prefix the relative path is appended to, so include the trailing slash: "web/v42/". Default: none. */
|
|
@@ -23,7 +23,7 @@ export type LambderS3FileSourceOptions = {
|
|
|
23
23
|
* An object's Content-Type is used unless it is a generic octet-stream, in
|
|
24
24
|
* which case the extension decides, as for local files.
|
|
25
25
|
*/
|
|
26
|
-
export declare class LambderS3FileSource implements
|
|
26
|
+
export declare class LambderS3FileSource implements LambderFileSource {
|
|
27
27
|
private readonly bucket;
|
|
28
28
|
private readonly prefix;
|
|
29
29
|
private readonly clientConfig;
|
|
@@ -31,5 +31,5 @@ export declare class LambderS3FileSource implements LambderPublicFileSource {
|
|
|
31
31
|
private sdk;
|
|
32
32
|
constructor({ bucket, prefix, client, clientConfig }: LambderS3FileSourceOptions);
|
|
33
33
|
private loadSdk;
|
|
34
|
-
read(relativePath: string): Promise<
|
|
34
|
+
read(relativePath: string): Promise<LambderFile | null>;
|
|
35
35
|
}
|