@vesk/adapter 0.2.67 → 0.2.68
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/dist/prod-server.d.ts +4 -4
- package/dist/prod-server.d.ts.map +1 -1
- package/dist/prod-server.js +118 -22
- package/dist/response-pipeline.d.ts +54 -0
- package/dist/response-pipeline.d.ts.map +1 -0
- package/dist/response-pipeline.js +422 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/prod-server.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { type Server } from 'node:http';
|
|
2
|
+
import type { CompressionConfig, ProdServerOptions } from '@vesk/adapter/src/types';
|
|
2
3
|
export declare function bodyTooLarge(maxBytes: number): Error & {
|
|
3
4
|
status: number;
|
|
4
5
|
};
|
|
5
|
-
export declare function startProdServer(outDir: string, options?: {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
maxBodyBytes?: number;
|
|
6
|
+
export declare function startProdServer(outDir: string, options?: ProdServerOptions & {
|
|
7
|
+
compression?: CompressionConfig | false;
|
|
8
|
+
healthCheck?: boolean;
|
|
9
9
|
}): Promise<Server>;
|
|
10
10
|
//# sourceMappingURL=prod-server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prod-server.d.ts","sourceRoot":"","sources":["../src/prod-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2D,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"prod-server.d.ts","sourceRoot":"","sources":["../src/prod-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2D,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAajG,OAAO,KAAK,EAAkB,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAuFpG,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AA0FD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG;IAAE,WAAW,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqlBvK"}
|
package/dist/prod-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
2
|
-
import { resolve,
|
|
2
|
+
import { resolve, dirname, basename } from 'node:path';
|
|
3
3
|
import { createServer } from 'node:http';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { createRateLimiter, resolveComponentName, safeJsonForScript, getClientProtocol, DEFAULT_MAX_BODY_BYTES } from '@vesk/compiler/src/server-codegen';
|
|
@@ -11,6 +11,8 @@ import { resolveWithin, installMdReadHook } from '@vesk/adapter/src/paths';
|
|
|
11
11
|
import { resolveCssUrls, hasBuiltGlobalCss } from '@vesk/adapter/src/css';
|
|
12
12
|
import { loadVeskConfig } from '@vesk/adapter/src/load-config';
|
|
13
13
|
import { renderDefaultNotFoundPage, renderDefaultErrorPage } from '@vesk/adapter/src/default-pages';
|
|
14
|
+
import { installResponsePipeline } from '@vesk/adapter/src/response-pipeline';
|
|
15
|
+
import { mimeFor } from '@vesk/adapter/src/platform-output';
|
|
14
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
17
|
async function readBody(req, maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
16
18
|
const chunks = [];
|
|
@@ -23,15 +25,68 @@ async function readBody(req, maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
|
23
25
|
}
|
|
24
26
|
return Buffer.concat(chunks);
|
|
25
27
|
}
|
|
28
|
+
/** Open-ended content types. Buffering one would hold the response until the
|
|
29
|
+
* stream closed — which, for a never-ending event stream, is never. */
|
|
30
|
+
const STREAMING_TYPES = new Set(['text/event-stream', 'application/x-ndjson']);
|
|
31
|
+
/** Above this, a "buffered" body is streamed instead rather than held whole. */
|
|
32
|
+
const DELIVER_BUFFER_LIMIT = 1024 * 1024;
|
|
26
33
|
/**
|
|
27
|
-
* Writes a handler Response to the socket
|
|
28
|
-
*
|
|
34
|
+
* Writes a handler Response to the socket.
|
|
35
|
+
*
|
|
36
|
+
* A finite body is delivered as ONE `end(buffer)`, not chunk-by-chunk, and that
|
|
37
|
+
* is load-bearing: the response pipeline can only tag a body and revalidate it
|
|
38
|
+
* against `If-None-Match` — and only apply content-encoding — once it can see
|
|
39
|
+
* the whole body. Streaming a 2 KB JSON body would silently opt every API
|
|
40
|
+
* response out of both. A response that turns out to exceed the buffer limit,
|
|
41
|
+
* or that declares an open-ended content type, falls back to streaming so the
|
|
42
|
+
* memory and time-to-first-byte cost of a genuinely large body is unchanged.
|
|
29
43
|
*/
|
|
30
44
|
async function deliverResponse(res, response) {
|
|
31
|
-
|
|
45
|
+
const headers = Object.fromEntries(response.headers);
|
|
32
46
|
const body = response.body;
|
|
33
|
-
if (body
|
|
34
|
-
|
|
47
|
+
if (!body || typeof body.getReader !== 'function') {
|
|
48
|
+
res.writeHead(response.status, headers);
|
|
49
|
+
res.end(await response.text());
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const contentType = String(response.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
|
|
53
|
+
const reader = body.getReader();
|
|
54
|
+
if (STREAMING_TYPES.has(contentType)) {
|
|
55
|
+
res.writeHead(response.status, headers);
|
|
56
|
+
pumpRemaining(res, reader);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const chunks = [];
|
|
60
|
+
let size = 0;
|
|
61
|
+
let overflow = false;
|
|
62
|
+
try {
|
|
63
|
+
for (;;) {
|
|
64
|
+
const { done, value } = await reader.read();
|
|
65
|
+
if (done)
|
|
66
|
+
break;
|
|
67
|
+
chunks.push(Buffer.from(value));
|
|
68
|
+
size += value.byteLength;
|
|
69
|
+
if (size > DELIVER_BUFFER_LIMIT) {
|
|
70
|
+
overflow = true;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* stream aborted by the client */
|
|
77
|
+
}
|
|
78
|
+
res.writeHead(response.status, headers);
|
|
79
|
+
if (overflow) {
|
|
80
|
+
for (const chunk of chunks)
|
|
81
|
+
res.write(chunk);
|
|
82
|
+
pumpRemaining(res, reader);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
res.end(Buffer.concat(chunks));
|
|
86
|
+
}
|
|
87
|
+
/** Drain an already-started reader onto the socket, then end it. */
|
|
88
|
+
function pumpRemaining(res, reader) {
|
|
89
|
+
void (async () => {
|
|
35
90
|
try {
|
|
36
91
|
for (;;) {
|
|
37
92
|
const { done, value } = await reader.read();
|
|
@@ -44,9 +99,7 @@ async function deliverResponse(res, response) {
|
|
|
44
99
|
/* stream aborted by the client */
|
|
45
100
|
}
|
|
46
101
|
res.end();
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
res.end(await response.text());
|
|
102
|
+
})();
|
|
50
103
|
}
|
|
51
104
|
export function bodyTooLarge(maxBytes) {
|
|
52
105
|
const err = new Error(`Request body exceeds limit (${maxBytes} bytes)`);
|
|
@@ -111,13 +164,6 @@ function makeWebRequest(nodeReq, url, maxBodyBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
|
111
164
|
webRequest.query = query;
|
|
112
165
|
return webRequest;
|
|
113
166
|
}
|
|
114
|
-
const MIME = {
|
|
115
|
-
'.svg': 'image/svg+xml', '.css': 'text/css', '.js': 'application/javascript',
|
|
116
|
-
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
117
|
-
'.ico': 'image/x-icon', '.html': 'text/html', '.json': 'application/json',
|
|
118
|
-
'.webmanifest': 'application/manifest+json',
|
|
119
|
-
'.woff': 'font/woff', '.woff2': 'font/woff2', '.wasm': 'application/wasm',
|
|
120
|
-
};
|
|
121
167
|
export async function startProdServer(outDir, options) {
|
|
122
168
|
const port = options?.port || 3000;
|
|
123
169
|
const host = options?.host || '127.0.0.1';
|
|
@@ -142,11 +188,16 @@ export async function startProdServer(outDir, options) {
|
|
|
142
188
|
const projectDir = resolve(outDir, '..');
|
|
143
189
|
let securityConfig = {};
|
|
144
190
|
let mdConfig;
|
|
191
|
+
let compressionConfig = {};
|
|
192
|
+
let healthCheckEnabled = true;
|
|
145
193
|
try {
|
|
146
194
|
const config = await loadVeskConfig(projectDir);
|
|
147
195
|
if (config) {
|
|
148
196
|
securityConfig = { security: config.security };
|
|
149
197
|
mdConfig = config.md;
|
|
198
|
+
// `compression: false` disables encoding entirely; an object tunes it.
|
|
199
|
+
compressionConfig = config.compression === false ? { enabled: false } : (config.compression || {});
|
|
200
|
+
healthCheckEnabled = config.healthCheck !== false;
|
|
150
201
|
}
|
|
151
202
|
}
|
|
152
203
|
catch (e) {
|
|
@@ -154,6 +205,13 @@ export async function startProdServer(outDir, options) {
|
|
|
154
205
|
// config must never silently disable the app's chosen security policy.
|
|
155
206
|
console.error('vesk start: failed to load vesk.config — falling back to secure defaults:', e instanceof Error ? e.message : e);
|
|
156
207
|
}
|
|
208
|
+
// Explicit server options win over vesk.config (the CLI passes nothing here,
|
|
209
|
+
// so this is the programmatic path — how tests and embedders configure it).
|
|
210
|
+
if (options?.compression !== undefined) {
|
|
211
|
+
compressionConfig = options.compression === false ? { enabled: false } : options.compression;
|
|
212
|
+
}
|
|
213
|
+
if (options?.healthCheck !== undefined)
|
|
214
|
+
healthCheckEnabled = options.healthCheck !== false;
|
|
157
215
|
if (mdConfig) {
|
|
158
216
|
try {
|
|
159
217
|
const { configureMd } = await import('@vesk/runtime/src/md');
|
|
@@ -262,6 +320,23 @@ export async function startProdServer(outDir, options) {
|
|
|
262
320
|
? 'https'
|
|
263
321
|
: getClientProtocol({ headers: req.headers }, !!securityConfig?.trustProxy);
|
|
264
322
|
globalThis.__vesk_ssr_base_url = `${proto}://${reqHost}`;
|
|
323
|
+
// Liveness probe. Deliberately the FIRST thing in the handler, ahead of the
|
|
324
|
+
// rate limiter, middleware and the pipeline below:
|
|
325
|
+
// - a probe that can be answered 429 is indistinguishable from a dead
|
|
326
|
+
// server to an orchestrator, which then restarts a healthy pod;
|
|
327
|
+
// - it must not be rewritten or authed by user middleware;
|
|
328
|
+
// - it must never be encoded or tagged — a load balancer revalidating a
|
|
329
|
+
// cached "ok" is testing its own cache, not this process.
|
|
330
|
+
if (healthCheckEnabled && (url.pathname === '/healthz' || url.pathname === '/_vesk/healthz')) {
|
|
331
|
+
const probe = JSON.stringify({ status: 'ok', uptime: Math.floor(process.uptime()) });
|
|
332
|
+
res.writeHead(200, {
|
|
333
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
334
|
+
'Content-Length': String(Buffer.byteLength(probe)),
|
|
335
|
+
'Cache-Control': 'no-store',
|
|
336
|
+
});
|
|
337
|
+
res.end(probe);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
265
340
|
if (rateLimiter) {
|
|
266
341
|
const clientIp = getClientIpFromReq(req);
|
|
267
342
|
if (!rateLimiter.check(clientIp)) {
|
|
@@ -327,11 +402,22 @@ export async function startProdServer(outDir, options) {
|
|
|
327
402
|
}
|
|
328
403
|
return origWriteHead2(statusCode, headers);
|
|
329
404
|
});
|
|
405
|
+
// Installed LAST so it wraps the two header decorators above: its captured
|
|
406
|
+
// `writeHead` is their wrapper, so security headers and per-request
|
|
407
|
+
// overrides are still merged in — once, at flush time. One install covers
|
|
408
|
+
// every response the handler produces (static files, SSR HTML, API JSON,
|
|
409
|
+
// middleware short-circuits), so no call site can be missed.
|
|
410
|
+
installResponsePipeline(res, req, {
|
|
411
|
+
compress: compressionConfig.enabled !== false,
|
|
412
|
+
compressThreshold: compressionConfig.threshold,
|
|
413
|
+
brotliQuality: compressionConfig.brotliQuality,
|
|
414
|
+
gzipLevel: compressionConfig.gzipLevel,
|
|
415
|
+
etag: compressionConfig.etag !== false,
|
|
416
|
+
});
|
|
330
417
|
const publicDir = resolve(staticDir, 'public');
|
|
331
418
|
const rootFile = url.pathname.length > 1 ? resolveWithin(publicDir, url.pathname.slice(1)) : null;
|
|
332
419
|
if (rootFile && existsSync(rootFile) && statSync(rootFile).isFile()) {
|
|
333
|
-
|
|
334
|
-
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
|
|
420
|
+
res.writeHead(200, { 'Content-Type': mimeFor(rootFile) });
|
|
335
421
|
res.end(readFileSync(rootFile));
|
|
336
422
|
return;
|
|
337
423
|
}
|
|
@@ -387,9 +473,8 @@ export async function startProdServer(outDir, options) {
|
|
|
387
473
|
return;
|
|
388
474
|
}
|
|
389
475
|
if (existsSync(staticPath) && statSync(staticPath).isFile()) {
|
|
390
|
-
const ext = extname(staticPath);
|
|
391
476
|
res.writeHead(200, {
|
|
392
|
-
'Content-Type':
|
|
477
|
+
'Content-Type': mimeFor(staticPath),
|
|
393
478
|
// A content-hashed name can never change meaning, so it is immutable
|
|
394
479
|
// for a year; anything unhashed must be revalidated or a deploy would
|
|
395
480
|
// serve stale JS forever.
|
|
@@ -651,8 +736,19 @@ export async function startProdServer(outDir, options) {
|
|
|
651
736
|
server.close(() => process.exit(0));
|
|
652
737
|
setTimeout(() => process.exit(0), 3000).unref();
|
|
653
738
|
}
|
|
654
|
-
|
|
655
|
-
process
|
|
739
|
+
// Named handlers so they can be detached on close. Without this every
|
|
740
|
+
// start/stop cycle leaks two process listeners, which is a hard crash
|
|
741
|
+
// (MaxListenersExceededWarning → throw) once a host restarts the server more
|
|
742
|
+
// than five times — the shape every test harness and hot-restarting dev
|
|
743
|
+
// wrapper uses.
|
|
744
|
+
const onSigint = () => shutdown('SIGINT');
|
|
745
|
+
const onSigterm = () => shutdown('SIGTERM');
|
|
746
|
+
process.on('SIGINT', onSigint);
|
|
747
|
+
process.on('SIGTERM', onSigterm);
|
|
748
|
+
server.once('close', () => {
|
|
749
|
+
process.off('SIGINT', onSigint);
|
|
750
|
+
process.off('SIGTERM', onSigterm);
|
|
751
|
+
});
|
|
656
752
|
server.listen(port, host, () => {
|
|
657
753
|
console.error(`vesk production server at http://localhost:${port} (listening on ${host})`);
|
|
658
754
|
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
export type ContentEncoding = 'br' | 'gzip';
|
|
3
|
+
/**
|
|
4
|
+
* Pick a content coding from `Accept-Encoding`.
|
|
5
|
+
*
|
|
6
|
+
* Quality values are honoured because a client is entitled to say "I accept
|
|
7
|
+
* gzip but not brotli" (`br;q=0`) or "gzip only at half priority". `*` applies
|
|
8
|
+
* to codings the header does not name. Ties go to brotli: every browser that
|
|
9
|
+
* sends both lists them equally, and brotli is smaller.
|
|
10
|
+
*/
|
|
11
|
+
export declare function negotiateEncoding(header: string | string[] | undefined): ContentEncoding | null;
|
|
12
|
+
/**
|
|
13
|
+
* Would compressing this content type actually pay?
|
|
14
|
+
*
|
|
15
|
+
* The rule is "text-like, not already compressed". Images, fonts, video and
|
|
16
|
+
* archives are already compressed — a second pass costs CPU and usually makes
|
|
17
|
+
* the body *larger*, which is why the size check in `installResponsePipeline`
|
|
18
|
+
* is kept as a second line of defence rather than the only one.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isCompressibleType(contentType: string | undefined): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* A validator for these exact bytes.
|
|
23
|
+
*
|
|
24
|
+
* sha1 rather than sha256 because this is a cache validator, not a security
|
|
25
|
+
* primitive (RFC 9110 requires "a function of the representation", and nginx
|
|
26
|
+
* ships md5 here), and every request hashes its body. Truncated to 20 bytes of
|
|
27
|
+
* base64 — 27 characters — which is far beyond collision range for a validator.
|
|
28
|
+
*/
|
|
29
|
+
export declare function etagFor(body: Buffer): string;
|
|
30
|
+
/**
|
|
31
|
+
* `If-None-Match` satisfaction, weak comparison as RFC 9110 §13.1.2 requires
|
|
32
|
+
* for GET. Weak comparison ignores the `W/` prefix on either side, which is what
|
|
33
|
+
* makes a `W/"x"` from a proxy match our strong `"x"`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function ifNoneMatchSatisfied(header: string | string[] | undefined, etag: string): boolean;
|
|
36
|
+
export interface ResponsePipelineOptions {
|
|
37
|
+
/** Set false to disable content-coding negotiation. Default true. */
|
|
38
|
+
compress?: boolean;
|
|
39
|
+
/** Smallest body worth compressing, in bytes. Default 1024. */
|
|
40
|
+
compressThreshold?: number;
|
|
41
|
+
/** Set false to disable ETag/304. Default true. */
|
|
42
|
+
etag?: boolean;
|
|
43
|
+
/** brotli quality 0-11. Default 5 — 11 is several hundred ms per megabyte. */
|
|
44
|
+
brotliQuality?: number;
|
|
45
|
+
/** gzip level 1-9. Default 6. */
|
|
46
|
+
gzipLevel?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Wrap `res` so every buffered response is tagged with an ETag, revalidated
|
|
50
|
+
* against `If-None-Match`, and encoded — in that order, because a 304 must be
|
|
51
|
+
* decided on the *unencoded* bytes.
|
|
52
|
+
*/
|
|
53
|
+
export declare function installResponsePipeline(res: ServerResponse, req: IncomingMessage, options?: ResponsePipelineOptions): void;
|
|
54
|
+
//# sourceMappingURL=response-pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-pipeline.d.ts","sourceRoot":"","sources":["../src/response-pipeline.ts"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAKjE,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,MAAM,CAAC;AAE5C;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,eAAe,GAAG,IAAI,CAgC/F;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAwB3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAWjG;AAED,MAAM,WAAW,uBAAuB;IACvC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AA8BD;;;;GAIG;AACH,wBAAgB,uBAAuB,CACtC,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,EACpB,OAAO,GAAE,uBAA4B,GACnC,IAAI,CA2ON"}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-level response pipeline: conditional GET (ETag → 304) and
|
|
3
|
+
* content-encoding negotiation (brotli / gzip).
|
|
4
|
+
*
|
|
5
|
+
* Both live in one wrapper because they share the same constraint: the decision
|
|
6
|
+
* can only be made once the whole body is known. `res.writeHead` sends headers
|
|
7
|
+
* to the socket immediately, so a wrapper that compressed or tagged at
|
|
8
|
+
* `writeHead` time would either have to buffer every response in the server (a
|
|
9
|
+
* second copy of every page) or get the answer wrong for streamed bodies.
|
|
10
|
+
* Instead this captures `writeHead` and defers it until the body is either
|
|
11
|
+
* complete (`end`) or discovered to be a stream (`write`).
|
|
12
|
+
*
|
|
13
|
+
* Consequences worth knowing:
|
|
14
|
+
*
|
|
15
|
+
* - **One install covers every call site.** The prod server writes headers in
|
|
16
|
+
* ~15 places; wrapping `res` once means a new one cannot be forgotten. A
|
|
17
|
+
* static file, an SSR page, a JSON API body and a middleware short-circuit
|
|
18
|
+
* all pass through the same path.
|
|
19
|
+
* - **The first `write()` opts the response out of everything.** A stream
|
|
20
|
+
* (SSE, a chunked `renderPageStream`) cannot be re-encoded after the first
|
|
21
|
+
* bytes are out, so `write()` flushes the captured headers verbatim and every
|
|
22
|
+
* later `write`/`end` is a passthrough. No `Content-Encoding` is ever
|
|
23
|
+
* announced for a body that was not encoded.
|
|
24
|
+
* - **The wrapper composes with header decorators.** Installing *after* another
|
|
25
|
+
* `writeHead` wrapper means this layer's captured reference IS that wrapper,
|
|
26
|
+
* so security headers and per-request overrides are still applied — at flush
|
|
27
|
+
* time, exactly once.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately not here: range requests, `Last-Modified`/`If-Modified-Since`
|
|
30
|
+
* (the ETag covers the same revalidation need and cannot disagree with itself),
|
|
31
|
+
* and compression of already-encoded bodies.
|
|
32
|
+
*/
|
|
33
|
+
import { createHash } from 'node:crypto';
|
|
34
|
+
import { brotliCompress, constants as zlibConstants, createBrotliCompress, createGzip, gzip, } from 'node:zlib';
|
|
35
|
+
import { promisify } from 'node:util';
|
|
36
|
+
const brotliCompressAsync = promisify(brotliCompress);
|
|
37
|
+
const gzipAsync = promisify(gzip);
|
|
38
|
+
/**
|
|
39
|
+
* Pick a content coding from `Accept-Encoding`.
|
|
40
|
+
*
|
|
41
|
+
* Quality values are honoured because a client is entitled to say "I accept
|
|
42
|
+
* gzip but not brotli" (`br;q=0`) or "gzip only at half priority". `*` applies
|
|
43
|
+
* to codings the header does not name. Ties go to brotli: every browser that
|
|
44
|
+
* sends both lists them equally, and brotli is smaller.
|
|
45
|
+
*/
|
|
46
|
+
export function negotiateEncoding(header) {
|
|
47
|
+
if (!header)
|
|
48
|
+
return null;
|
|
49
|
+
// `header` is `string | string[]` per Node's typings, but a duplicated
|
|
50
|
+
// header arrives joined by ", " in practice; both shapes fold to one list.
|
|
51
|
+
const value = Array.isArray(header) ? header.join(',') : header;
|
|
52
|
+
const q = {};
|
|
53
|
+
for (const part of value.split(',')) {
|
|
54
|
+
const seg = part.trim();
|
|
55
|
+
if (!seg)
|
|
56
|
+
continue;
|
|
57
|
+
const semi = seg.indexOf(';');
|
|
58
|
+
const token = (semi === -1 ? seg : seg.slice(0, semi)).trim().toLowerCase();
|
|
59
|
+
if (!token)
|
|
60
|
+
continue;
|
|
61
|
+
let weight = 1;
|
|
62
|
+
if (semi !== -1) {
|
|
63
|
+
for (const param of seg.slice(semi + 1).split(';')) {
|
|
64
|
+
const eq = param.indexOf('=');
|
|
65
|
+
if (eq === -1)
|
|
66
|
+
continue;
|
|
67
|
+
if (param.slice(0, eq).trim().toLowerCase() !== 'q')
|
|
68
|
+
continue;
|
|
69
|
+
const parsed = Number.parseFloat(param.slice(eq + 1).trim());
|
|
70
|
+
if (Number.isFinite(parsed))
|
|
71
|
+
weight = parsed;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// First occurrence wins, per RFC 9110 — a client cannot raise its own
|
|
75
|
+
// preference by listing a coding twice.
|
|
76
|
+
if (!(token in q))
|
|
77
|
+
q[token] = weight;
|
|
78
|
+
}
|
|
79
|
+
const star = q['*'];
|
|
80
|
+
const br = q['br'] !== undefined ? q['br'] : star;
|
|
81
|
+
const gz = q['gzip'] !== undefined ? q['gzip'] : star;
|
|
82
|
+
if (br !== undefined && br > 0 && (gz === undefined || gz <= 0 || br >= gz))
|
|
83
|
+
return 'br';
|
|
84
|
+
if (gz !== undefined && gz > 0)
|
|
85
|
+
return 'gzip';
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Would compressing this content type actually pay?
|
|
90
|
+
*
|
|
91
|
+
* The rule is "text-like, not already compressed". Images, fonts, video and
|
|
92
|
+
* archives are already compressed — a second pass costs CPU and usually makes
|
|
93
|
+
* the body *larger*, which is why the size check in `installResponsePipeline`
|
|
94
|
+
* is kept as a second line of defence rather than the only one.
|
|
95
|
+
*/
|
|
96
|
+
export function isCompressibleType(contentType) {
|
|
97
|
+
if (!contentType)
|
|
98
|
+
return false;
|
|
99
|
+
const type = (contentType.split(';')[0] || '').trim().toLowerCase();
|
|
100
|
+
if (!type)
|
|
101
|
+
return false;
|
|
102
|
+
if (type.startsWith('text/'))
|
|
103
|
+
return true;
|
|
104
|
+
if (type === 'image/svg+xml' || type === 'image/x-icon' || type === 'image/bmp')
|
|
105
|
+
return true;
|
|
106
|
+
if (type.endsWith('+json') || type.endsWith('+xml'))
|
|
107
|
+
return true;
|
|
108
|
+
switch (type) {
|
|
109
|
+
case 'application/javascript':
|
|
110
|
+
case 'application/x-javascript':
|
|
111
|
+
case 'application/ecmascript':
|
|
112
|
+
case 'application/json':
|
|
113
|
+
case 'application/manifest+json':
|
|
114
|
+
case 'application/ld+json':
|
|
115
|
+
case 'application/graphql':
|
|
116
|
+
case 'application/x-ndjson':
|
|
117
|
+
case 'application/xml':
|
|
118
|
+
case 'application/xhtml+xml':
|
|
119
|
+
case 'application/rss+xml':
|
|
120
|
+
case 'application/atom+xml':
|
|
121
|
+
return true;
|
|
122
|
+
default:
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A validator for these exact bytes.
|
|
128
|
+
*
|
|
129
|
+
* sha1 rather than sha256 because this is a cache validator, not a security
|
|
130
|
+
* primitive (RFC 9110 requires "a function of the representation", and nginx
|
|
131
|
+
* ships md5 here), and every request hashes its body. Truncated to 20 bytes of
|
|
132
|
+
* base64 — 27 characters — which is far beyond collision range for a validator.
|
|
133
|
+
*/
|
|
134
|
+
export function etagFor(body) {
|
|
135
|
+
return `"${createHash('sha1').update(body).digest('base64').slice(0, 27)}"`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* `If-None-Match` satisfaction, weak comparison as RFC 9110 §13.1.2 requires
|
|
139
|
+
* for GET. Weak comparison ignores the `W/` prefix on either side, which is what
|
|
140
|
+
* makes a `W/"x"` from a proxy match our strong `"x"`.
|
|
141
|
+
*/
|
|
142
|
+
export function ifNoneMatchSatisfied(header, etag) {
|
|
143
|
+
if (!header)
|
|
144
|
+
return false;
|
|
145
|
+
const value = Array.isArray(header) ? header.join(',') : header;
|
|
146
|
+
const candidates = value.split(',').map(s => s.trim()).filter(Boolean);
|
|
147
|
+
if (candidates.includes('*'))
|
|
148
|
+
return true;
|
|
149
|
+
const strip = (s) => (s.startsWith('W/') ? s.slice(2) : s).trim();
|
|
150
|
+
const target = strip(etag);
|
|
151
|
+
for (const candidate of candidates) {
|
|
152
|
+
if (strip(candidate) === target)
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
function getHeader(headers, name) {
|
|
158
|
+
const wanted = name.toLowerCase();
|
|
159
|
+
for (const key of Object.keys(headers)) {
|
|
160
|
+
if (key.toLowerCase() === wanted) {
|
|
161
|
+
const v = headers[key];
|
|
162
|
+
if (v === undefined)
|
|
163
|
+
return undefined;
|
|
164
|
+
return Array.isArray(v) ? v.join(', ') : String(v);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
function hasNoStore(cacheControl) {
|
|
170
|
+
if (!cacheControl)
|
|
171
|
+
return false;
|
|
172
|
+
return cacheControl.split(',').some(part => part.trim().toLowerCase() === 'no-store');
|
|
173
|
+
}
|
|
174
|
+
/** Add `value` to `Vary` without duplicating an entry already there. */
|
|
175
|
+
function withVary(current, value) {
|
|
176
|
+
if (!current)
|
|
177
|
+
return value;
|
|
178
|
+
const existing = current.split(',').map(s => s.trim().toLowerCase());
|
|
179
|
+
if (existing.includes(value.toLowerCase()))
|
|
180
|
+
return current;
|
|
181
|
+
if (existing.includes('*'))
|
|
182
|
+
return current;
|
|
183
|
+
return `${current}, ${value}`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Wrap `res` so every buffered response is tagged with an ETag, revalidated
|
|
187
|
+
* against `If-None-Match`, and encoded — in that order, because a 304 must be
|
|
188
|
+
* decided on the *unencoded* bytes.
|
|
189
|
+
*/
|
|
190
|
+
export function installResponsePipeline(res, req, options = {}) {
|
|
191
|
+
const compressEnabled = options.compress !== false;
|
|
192
|
+
const etagEnabled = options.etag !== false;
|
|
193
|
+
const threshold = options.compressThreshold ?? 1024;
|
|
194
|
+
const brotliQuality = options.brotliQuality ?? 5;
|
|
195
|
+
const gzipLevel = options.gzipLevel ?? 6;
|
|
196
|
+
const realWriteHead = res.writeHead.bind(res);
|
|
197
|
+
const realWrite = res.write.bind(res);
|
|
198
|
+
const realEnd = res.end.bind(res);
|
|
199
|
+
const realFlushHeaders = res.flushHeaders.bind(res);
|
|
200
|
+
let pendingStatus = 200;
|
|
201
|
+
let pendingHeaders = {};
|
|
202
|
+
let flushed = false;
|
|
203
|
+
let streaming = false;
|
|
204
|
+
let streamCompressor = null;
|
|
205
|
+
/** Emit the captured status+headers exactly once, merging in `extra`. */
|
|
206
|
+
function flush(extra) {
|
|
207
|
+
if (flushed)
|
|
208
|
+
return;
|
|
209
|
+
flushed = true;
|
|
210
|
+
const headers = {};
|
|
211
|
+
for (const [key, value] of Object.entries(pendingHeaders)) {
|
|
212
|
+
if (value !== undefined)
|
|
213
|
+
headers[key] = value;
|
|
214
|
+
}
|
|
215
|
+
for (const [key, value] of Object.entries(extra || {})) {
|
|
216
|
+
if (value === undefined)
|
|
217
|
+
delete headers[key];
|
|
218
|
+
else
|
|
219
|
+
headers[key] = value;
|
|
220
|
+
}
|
|
221
|
+
realWriteHead(pendingStatus, headers);
|
|
222
|
+
}
|
|
223
|
+
res.writeHead = ((statusCode, headers) => {
|
|
224
|
+
if (flushed || streaming)
|
|
225
|
+
return realWriteHead(statusCode, headers);
|
|
226
|
+
pendingStatus = statusCode;
|
|
227
|
+
pendingHeaders = { ...(headers || {}) };
|
|
228
|
+
return res;
|
|
229
|
+
});
|
|
230
|
+
/**
|
|
231
|
+
* Would this response's bytes differ if the client sent a different
|
|
232
|
+
* `Accept-Encoding`? If yes it must be announced in `Vary` whether or not
|
|
233
|
+
* an encoding was actually applied — a cache that stored the identity copy
|
|
234
|
+
* without seeing `Vary` would then serve it to a client that can decode br.
|
|
235
|
+
*/
|
|
236
|
+
function encodingIsVariable() {
|
|
237
|
+
return compressEnabled
|
|
238
|
+
&& !getHeader(pendingHeaders, 'content-encoding')
|
|
239
|
+
&& isCompressibleType(getHeader(pendingHeaders, 'content-type'));
|
|
240
|
+
}
|
|
241
|
+
function startStreamCompression(encoding) {
|
|
242
|
+
const compressor = encoding === 'br'
|
|
243
|
+
? createBrotliCompress({ params: { [zlibConstants.BROTLI_PARAM_QUALITY]: brotliQuality } })
|
|
244
|
+
: createGzip({ level: gzipLevel });
|
|
245
|
+
// The compressor owns the socket from here: its output is what the client
|
|
246
|
+
// receives, and its `end` is what actually finishes the response. This is
|
|
247
|
+
// what lets a *streamed* body still be encoded — buffering it to compute a
|
|
248
|
+
// validator would defeat the streaming in the first place.
|
|
249
|
+
compressor.on('data', (chunk) => { realWrite(chunk); });
|
|
250
|
+
compressor.on('end', () => { realEnd(); });
|
|
251
|
+
compressor.on('error', () => {
|
|
252
|
+
// A compressor failure must still produce a complete response object;
|
|
253
|
+
// the bytes already written are the unencoded prefix, which is why the
|
|
254
|
+
// socket has to be closed rather than left open.
|
|
255
|
+
try {
|
|
256
|
+
realEnd();
|
|
257
|
+
}
|
|
258
|
+
catch { /* already finished */ }
|
|
259
|
+
});
|
|
260
|
+
streamCompressor = compressor;
|
|
261
|
+
}
|
|
262
|
+
res.write = ((chunk, encoding) => {
|
|
263
|
+
if (!flushed && !streaming) {
|
|
264
|
+
streaming = true;
|
|
265
|
+
const vary = withVary(getHeader(pendingHeaders, 'vary'), 'Accept-Encoding');
|
|
266
|
+
const encodingChosen = encodingIsVariable() ? negotiateEncoding(req.headers['accept-encoding']) : null;
|
|
267
|
+
if (encodingChosen) {
|
|
268
|
+
startStreamCompression(encodingChosen);
|
|
269
|
+
// Content-Length cannot survive: the encoded stream's length is
|
|
270
|
+
// unknown until it ends, and a wrong one truncates the response.
|
|
271
|
+
flush({ 'Content-Encoding': encodingChosen, Vary: vary, 'Content-Length': undefined });
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
flush({ Vary: encodingIsVariable() ? vary : undefined });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (streamCompressor) {
|
|
278
|
+
streamCompressor.write(toBuffer(chunk, encoding) || Buffer.alloc(0));
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
return typeof encoding === 'undefined'
|
|
282
|
+
? realWrite(chunk)
|
|
283
|
+
: realWrite(chunk, encoding);
|
|
284
|
+
});
|
|
285
|
+
res.flushHeaders = (() => {
|
|
286
|
+
if (!flushed) {
|
|
287
|
+
streaming = true;
|
|
288
|
+
flush();
|
|
289
|
+
}
|
|
290
|
+
realFlushHeaders.call(res);
|
|
291
|
+
});
|
|
292
|
+
function toBuffer(chunk, encoding) {
|
|
293
|
+
if (chunk === undefined || chunk === null)
|
|
294
|
+
return null;
|
|
295
|
+
if (Buffer.isBuffer(chunk))
|
|
296
|
+
return chunk;
|
|
297
|
+
if (typeof chunk === 'string') {
|
|
298
|
+
return Buffer.from(chunk, typeof encoding === 'string' ? encoding : 'utf8');
|
|
299
|
+
}
|
|
300
|
+
if (chunk instanceof Uint8Array)
|
|
301
|
+
return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
function finish(body, endArgs) {
|
|
305
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
306
|
+
// Nothing to compress, tag or revalidate. Empty bodies include 204 and
|
|
307
|
+
// 304 responses, which must not carry a Content-Length anyway.
|
|
308
|
+
if (!body || body.length === 0 || streaming || flushed) {
|
|
309
|
+
flush();
|
|
310
|
+
realEnd(...endArgs);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const cacheable = method === 'GET' || method === 'HEAD';
|
|
314
|
+
const existingEtag = getHeader(pendingHeaders, 'etag');
|
|
315
|
+
// `no-store` means the bytes are not reusable, so a validator would only
|
|
316
|
+
// produce a 304 that hands the client back a body the server has already
|
|
317
|
+
// consumed. This is load-bearing for the single-use `/ssr-data.js` and
|
|
318
|
+
// `/store-init.js` payloads, which are deleted as they are read.
|
|
319
|
+
const canValidate = cacheable && pendingStatus === 200 && !hasNoStore(getHeader(pendingHeaders, 'cache-control'));
|
|
320
|
+
if (!canValidate) {
|
|
321
|
+
maybeCompress(body, endArgs);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const etag = existingEtag || (etagEnabled ? etagFor(body) : undefined);
|
|
325
|
+
if (etag && ifNoneMatchSatisfied(req.headers['if-none-match'], etag)) {
|
|
326
|
+
// A 304 carries the validator and the caching directives and nothing
|
|
327
|
+
// else — no body, and no Content-Type/Content-Length describing one.
|
|
328
|
+
const headers304 = { ETag: etag };
|
|
329
|
+
const cc = getHeader(pendingHeaders, 'cache-control');
|
|
330
|
+
if (cc)
|
|
331
|
+
headers304['Cache-Control'] = cc;
|
|
332
|
+
headers304.Vary = withVary(getHeader(pendingHeaders, 'vary'), 'Accept-Encoding');
|
|
333
|
+
flushed = true;
|
|
334
|
+
realWriteHead(304, headers304);
|
|
335
|
+
realEnd();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (etag && !existingEtag) {
|
|
339
|
+
pendingHeaders = { ...pendingHeaders, ETag: etag };
|
|
340
|
+
}
|
|
341
|
+
maybeCompress(body, endArgs);
|
|
342
|
+
}
|
|
343
|
+
function maybeCompress(body, endArgs) {
|
|
344
|
+
// Below the threshold the bytes are identical whatever the client asks
|
|
345
|
+
// for, so no `Vary` is owed — announcing one would fragment caches for
|
|
346
|
+
// nothing. At or above it, the representation does vary by encoding and
|
|
347
|
+
// `Vary` is announced whether or not a coding was chosen.
|
|
348
|
+
if (compressEnabled
|
|
349
|
+
&& body.length < threshold
|
|
350
|
+
|| !encodingIsVariable()) {
|
|
351
|
+
flush();
|
|
352
|
+
realEnd(...endArgs);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const encoding = negotiateEncoding(req.headers['accept-encoding']);
|
|
356
|
+
if (!encoding) {
|
|
357
|
+
flush({ Vary: withVary(getHeader(pendingHeaders, 'vary'), 'Accept-Encoding') });
|
|
358
|
+
realEnd(...endArgs);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const compress = encoding === 'br' ? brotliCompressAsync : gzipAsync;
|
|
362
|
+
const params = encoding === 'br'
|
|
363
|
+
? { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: brotliQuality } }
|
|
364
|
+
: { level: gzipLevel };
|
|
365
|
+
void Promise.resolve()
|
|
366
|
+
.then(() => compress(body, params))
|
|
367
|
+
.then((encoded) => {
|
|
368
|
+
// A body that did not get smaller is not worth the encoding: it
|
|
369
|
+
// costs the client CPU on every fetch for nothing.
|
|
370
|
+
if (flushed || streaming) {
|
|
371
|
+
flush();
|
|
372
|
+
realEnd(...endArgs);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (encoded.length >= body.length) {
|
|
376
|
+
flush({ Vary: withVary(getHeader(pendingHeaders, 'vary'), 'Accept-Encoding') });
|
|
377
|
+
realEnd(...endArgs);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
flush({
|
|
381
|
+
'Content-Encoding': encoding,
|
|
382
|
+
Vary: withVary(getHeader(pendingHeaders, 'vary'), 'Accept-Encoding'),
|
|
383
|
+
'Content-Length': String(encoded.length),
|
|
384
|
+
});
|
|
385
|
+
realEnd(encoded);
|
|
386
|
+
})
|
|
387
|
+
.catch(() => {
|
|
388
|
+
// Compression must never be the reason a response is lost.
|
|
389
|
+
flush({ Vary: withVary(getHeader(pendingHeaders, 'vary'), 'Accept-Encoding') });
|
|
390
|
+
realEnd(...endArgs);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
res.end = ((chunk, encoding, callback) => {
|
|
394
|
+
if (streamCompressor) {
|
|
395
|
+
// Ending the compressor is what ends the response — it flushes its
|
|
396
|
+
// own buffered tail first, so the client gets a complete stream.
|
|
397
|
+
if (typeof callback === 'function')
|
|
398
|
+
res.once('finish', callback);
|
|
399
|
+
const tail = toBuffer(chunk, encoding);
|
|
400
|
+
if (tail)
|
|
401
|
+
streamCompressor.end(tail);
|
|
402
|
+
else
|
|
403
|
+
streamCompressor.end();
|
|
404
|
+
return res;
|
|
405
|
+
}
|
|
406
|
+
if (streaming || flushed) {
|
|
407
|
+
return realEnd(chunk, encoding, callback);
|
|
408
|
+
}
|
|
409
|
+
// Node's end() has four shapes — (), (cb), (data), (data, enc),
|
|
410
|
+
// (data, cb), (data, enc, cb). Forward only the arguments that were
|
|
411
|
+
// actually supplied: `end(body, undefined, undefined)` is NOT the same
|
|
412
|
+
// as `end(body)` for a callback-taking overload.
|
|
413
|
+
const endArgs = chunk === undefined
|
|
414
|
+
? (callback === undefined ? [] : [callback])
|
|
415
|
+
: encoding === undefined
|
|
416
|
+
? (callback === undefined ? [chunk] : [chunk, callback])
|
|
417
|
+
: (callback === undefined ? [chunk, encoding] : [chunk, encoding, callback]);
|
|
418
|
+
const body = toBuffer(chunk, encoding);
|
|
419
|
+
finish(body, endArgs);
|
|
420
|
+
return res;
|
|
421
|
+
});
|
|
422
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { RouteNode, ApiRouteNode, VeskPlugin, ServerEventContext, VeskEventHandlers, VeskSecurity, VeskCors, VeskRateLimit, VeskSecurityPreset, SecurityConfig, MiddlewareChainItem, MiddlewareExtractResult, AncestorLayout, BuildOptions, DevServerOptions, ProdServerOptions, SsrFunctionOptions, ApiFunctionOptions, ClientBundleOptions, ClientBundleFileEntry, ClientBundleChunkSpec, ClientBundleCache, ChunkEntry, ClientBundleResult, MonolithicBundleParts, Manifest, ManifestRouteEntry, ManifestActionEntry, ManifestPrerenderedEntry, SsgRouteResult, BuildResult, ImageRef, ImageResult, SeoCheckIssue, SeoAuditResult, CombinedPageInfo, Tracked, Resource, UseFetchOptions, UseFetchStreamOptions, VeskRequest, VeskResponse, SetCookieOptions, CookieStore, ServerRequest, SsrDataSink, MdProps, MdConfig, MdHtmlMode, MarkdownOptions, MdHtmlWarning, VeskLeafComponent, VeskPrimitiveComponent, VeskTaggedLeafComponent, VeskComponentRegistry, VeskScope, } from '@vesk/types';
|
|
1
|
+
export type { RouteNode, ApiRouteNode, VeskPlugin, ServerEventContext, VeskEventHandlers, VeskSecurity, VeskCors, VeskRateLimit, VeskSecurityPreset, SecurityConfig, MiddlewareChainItem, MiddlewareExtractResult, AncestorLayout, BuildOptions, DevServerOptions, ProdServerOptions, SsrFunctionOptions, ApiFunctionOptions, ClientBundleOptions, ClientBundleFileEntry, ClientBundleChunkSpec, ClientBundleCache, ChunkEntry, ClientBundleResult, MonolithicBundleParts, Manifest, ManifestRouteEntry, ManifestActionEntry, ManifestPrerenderedEntry, SsgRouteResult, BuildResult, ImageRef, ImageResult, SeoCheckIssue, SeoAuditResult, CombinedPageInfo, Tracked, Resource, UseFetchOptions, UseFetchStreamOptions, VeskRequest, VeskResponse, SetCookieOptions, CookieStore, ServerRequest, SsrDataSink, MdProps, MdConfig, MdHtmlMode, MarkdownOptions, CompressionConfig, MdHtmlWarning, VeskLeafComponent, VeskPrimitiveComponent, VeskTaggedLeafComponent, VeskComponentRegistry, VeskScope, } from '@vesk/types';
|
|
2
2
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,SAAS,EACT,YAAY,EACZ,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,UAAU,EACV,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,EACR,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,EACd,WAAW,EACX,QAAQ,EACR,WAAW,EACX,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,OAAO,EACP,QAAQ,EACR,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,WAAW,EACX,OAAO,EACP,QAAQ,EACR,UAAU,EACV,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,SAAS,GACV,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,SAAS,EACT,YAAY,EACZ,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,UAAU,EACV,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,EACR,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,EACd,WAAW,EACX,QAAQ,EACR,WAAW,EACX,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,OAAO,EACP,QAAQ,EACR,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,WAAW,EACX,OAAO,EACP,QAAQ,EACR,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,SAAS,GACV,MAAM,aAAa,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vesk/adapter",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.68",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vesk adapter — builds deployable .vesk/ output for Deno and Node serverless",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"sharp": "^0.35.3"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@vesk/compiler": "^0.2.
|
|
27
|
-
"@vesk/runtime": "^0.2.
|
|
28
|
-
"@vesk/types": "^0.2.
|
|
26
|
+
"@vesk/compiler": "^0.2.68",
|
|
27
|
+
"@vesk/runtime": "^0.2.68",
|
|
28
|
+
"@vesk/types": "^0.2.68"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/ws": "^8.18.1",
|