@vesk/adapter 0.2.65 → 0.2.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/image-pipeline.d.ts +2 -1
- package/dist/image-pipeline.d.ts.map +1 -1
- package/dist/image-pipeline.js +45 -9
- package/dist/image-pipeline.test-helpers.d.ts +13 -0
- package/dist/image-pipeline.test-helpers.d.ts.map +1 -0
- package/dist/image-pipeline.test-helpers.js +15 -0
- package/dist/prod-server.d.ts.map +1 -1
- package/dist/prod-server.js +51 -4
- package/package.json +4 -4
package/dist/image-pipeline.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { ImageResult } from '@vesk/adapter/src/types';
|
|
1
|
+
import type { ImageRef, ImageResult } from '@vesk/adapter/src/types';
|
|
2
|
+
export declare function collectImageRefs(appDir: string): ImageRef[];
|
|
2
3
|
export declare function optimizeImages(appDir: string, outDir: string): Promise<ImageResult[]>;
|
|
3
4
|
//# sourceMappingURL=image-pipeline.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-pipeline.d.ts","sourceRoot":"","sources":["../src/image-pipeline.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"image-pipeline.d.ts","sourceRoot":"","sources":["../src/image-pipeline.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAoKrE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,EAAE,CA6B3D;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAoE3F"}
|
package/dist/image-pipeline.js
CHANGED
|
@@ -99,22 +99,42 @@ async function ensureSharp() {
|
|
|
99
99
|
async function processImage(srcPath, outDir, baseName) {
|
|
100
100
|
const image = sharpFn ? sharpFn(srcPath) : null;
|
|
101
101
|
if (!image) {
|
|
102
|
+
// Copy-only fallback. The emitted file MUST keep the source extension:
|
|
103
|
+
// `<Image>`'s srcset is `${base}-${w}w${ext}`, so writing `hero-640w` with
|
|
104
|
+
// no extension left every copy-only URL 404ing.
|
|
102
105
|
const original = readFileSync(srcPath);
|
|
106
|
+
const ext = extname(srcPath);
|
|
107
|
+
const written = [];
|
|
103
108
|
for (const w of OUTPUT_WIDTHS) {
|
|
104
|
-
const outputPath = resolve(outDir, `${baseName}-${w}w`);
|
|
109
|
+
const outputPath = resolve(outDir, `${baseName}-${w}w${ext}`);
|
|
105
110
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
106
111
|
writeFileSync(outputPath, original);
|
|
112
|
+
written.push(outputPath);
|
|
107
113
|
}
|
|
108
|
-
return
|
|
114
|
+
return written;
|
|
109
115
|
}
|
|
110
116
|
const meta = await image.metadata();
|
|
111
117
|
const originalWidth = meta.width || 1920;
|
|
112
118
|
const generated = [];
|
|
119
|
+
// `<Image>` emits a srcset entry for EVERY configured width, whatever the
|
|
120
|
+
// source's real size — it has no way to know what was generated. Skipping
|
|
121
|
+
// widths above the source's width therefore left real URLs 404ing (a 400px
|
|
122
|
+
// photo produced none of the 640w+ entries the browser would pick).
|
|
123
|
+
//
|
|
124
|
+
// So emit every configured width, and for any width at or above the
|
|
125
|
+
// source's own, emit the ORIGINAL bytes rather than enlarging: the URL
|
|
126
|
+
// resolves, and the browser gets a smaller image than it asked for instead
|
|
127
|
+
// of a 404.
|
|
113
128
|
for (const w of OUTPUT_WIDTHS) {
|
|
114
|
-
|
|
129
|
+
const base = `${baseName}-${w}w`;
|
|
130
|
+
if (w >= originalWidth) {
|
|
131
|
+
const copyPath = resolve(outDir, `${base}${extname(srcPath)}`);
|
|
132
|
+
mkdirSync(dirname(copyPath), { recursive: true });
|
|
133
|
+
writeFileSync(copyPath, readFileSync(srcPath));
|
|
134
|
+
generated.push(copyPath);
|
|
115
135
|
continue;
|
|
136
|
+
}
|
|
116
137
|
const resized = image.clone().resize({ width: w, withoutEnlargement: true });
|
|
117
|
-
const base = `${baseName}-${w}w`;
|
|
118
138
|
const jpgPath = resolve(outDir, `${base}${extname(srcPath)}`);
|
|
119
139
|
mkdirSync(dirname(jpgPath), { recursive: true });
|
|
120
140
|
await resized.toFile(jpgPath);
|
|
@@ -127,7 +147,7 @@ async function processImage(srcPath, outDir, baseName) {
|
|
|
127
147
|
}
|
|
128
148
|
return generated;
|
|
129
149
|
}
|
|
130
|
-
function collectImageRefs(appDir) {
|
|
150
|
+
export function collectImageRefs(appDir) {
|
|
131
151
|
const refs = [];
|
|
132
152
|
function walk(dir) {
|
|
133
153
|
let entries;
|
|
@@ -145,9 +165,13 @@ function collectImageRefs(appDir) {
|
|
|
145
165
|
continue;
|
|
146
166
|
walk(full);
|
|
147
167
|
}
|
|
148
|
-
else if (entry
|
|
168
|
+
else if (entry.endsWith('.vsk')) {
|
|
169
|
+
// Scan EVERY .vsk file, not just page.vsk. An `<Image>` living in a
|
|
170
|
+
// shared component or a layout is just as real a reference, and
|
|
171
|
+
// restricting the walk to page.vsk meant those images were never
|
|
172
|
+
// processed — their srcsets 404'd with no build-time complaint.
|
|
149
173
|
const src = readFileSync(full, 'utf-8');
|
|
150
|
-
const imgRegex = /<Image\s
|
|
174
|
+
const imgRegex = /<Image\s[^>]*src=["']([^"']+)["']/g;
|
|
151
175
|
let m;
|
|
152
176
|
while ((m = imgRegex.exec(src)) !== null) {
|
|
153
177
|
refs.push({ source: full, src: m[1] });
|
|
@@ -159,8 +183,15 @@ function collectImageRefs(appDir) {
|
|
|
159
183
|
return refs;
|
|
160
184
|
}
|
|
161
185
|
export async function optimizeImages(appDir, outDir) {
|
|
162
|
-
|
|
163
|
-
|
|
186
|
+
// Variants must land where the emitted URLs actually resolve.
|
|
187
|
+
//
|
|
188
|
+
// `<Image src="/hero.png" />` renders `srcset="/hero-640w.png 640w, …"` —
|
|
189
|
+
// site-root-relative, with no directory prefix. The prod server serves the
|
|
190
|
+
// site root from `<outDir>/static/public`, so that is exactly where each
|
|
191
|
+
// variant belongs. They used to be written to `<outDir>/static/images`,
|
|
192
|
+
// which nothing serves: every `<Image>` srcset URL 404'd in a real build
|
|
193
|
+
// while the build itself reported success.
|
|
194
|
+
mkdirSync(resolve(outDir, 'static', 'public'), { recursive: true });
|
|
164
195
|
const refs = collectImageRefs(appDir);
|
|
165
196
|
if (refs.length === 0) {
|
|
166
197
|
console.error('vesk images: no <Image> refs found');
|
|
@@ -198,6 +229,11 @@ export async function optimizeImages(appDir, outDir) {
|
|
|
198
229
|
continue;
|
|
199
230
|
}
|
|
200
231
|
const baseName = ref.src.replace(/^\//, '').replace(extname(ref.src), '');
|
|
232
|
+
// `baseName` already carries the source's subdirectory (`/photos/hero.png`
|
|
233
|
+
// → `photos/hero`), and processImage mkdir's the parent of each output —
|
|
234
|
+
// so the destination stays the site root and the nesting is not doubled.
|
|
235
|
+
const imageOutDir = resolve(outDir, 'static', 'public');
|
|
236
|
+
mkdirSync(imageOutDir, { recursive: true });
|
|
201
237
|
const files = await processImage(srcPath, imageOutDir, baseName);
|
|
202
238
|
results.push({ src: ref.src, baseName, files, widths: OUTPUT_WIDTHS });
|
|
203
239
|
console.error(`vesk images: ${ref.src} → ${files.length} variants`);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helper exposing the srcset URL list that `collectImageRefs` /
|
|
3
|
+
* `optimizeImages` must make reachable.
|
|
4
|
+
*
|
|
5
|
+
* `<Image>`'s `generateSrcset` lives inside the runtime package's private
|
|
6
|
+
* scope, so the pipeline tests derive the same URLs here rather than
|
|
7
|
+
* duplicating the rule by hand in every assertion.
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateSrcsetForTest(src: string, widths: number[]): Array<{
|
|
10
|
+
url: string;
|
|
11
|
+
descriptor: string;
|
|
12
|
+
}>;
|
|
13
|
+
//# sourceMappingURL=image-pipeline.test-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-pipeline.test-helpers.d.ts","sourceRoot":"","sources":["../src/image-pipeline.test-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACpC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EAAE,GACd,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAK5C"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helper exposing the srcset URL list that `collectImageRefs` /
|
|
3
|
+
* `optimizeImages` must make reachable.
|
|
4
|
+
*
|
|
5
|
+
* `<Image>`'s `generateSrcset` lives inside the runtime package's private
|
|
6
|
+
* scope, so the pipeline tests derive the same URLs here rather than
|
|
7
|
+
* duplicating the rule by hand in every assertion.
|
|
8
|
+
*/
|
|
9
|
+
export function generateSrcsetForTest(src, widths) {
|
|
10
|
+
if (!src || widths.length === 0)
|
|
11
|
+
return [];
|
|
12
|
+
const ext = src.lastIndexOf('.') > src.lastIndexOf('/') ? src.slice(src.lastIndexOf('.')) : '';
|
|
13
|
+
const base = ext ? src.slice(0, -ext.length) : src;
|
|
14
|
+
return widths.map(w => ({ url: `${base}-${w}w${ext}`, descriptor: `${w}w` }));
|
|
15
|
+
}
|
|
@@ -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;AAkDjG,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AAkGD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,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;AAkDjG,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AAkGD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAkiBxI"}
|
package/dist/prod-server.js
CHANGED
|
@@ -281,6 +281,52 @@ export async function startProdServer(outDir, options) {
|
|
|
281
281
|
}
|
|
282
282
|
return origWriteHead(statusCode, headers);
|
|
283
283
|
});
|
|
284
|
+
// Per-request security overrides (`request.setCsp()`, `.setSecurityHeader()`,
|
|
285
|
+
// `.setRateLimit()`, `.setCsrf()`) accumulate in the runtime's
|
|
286
|
+
// AsyncLocalStorage bag. The server cannot import the runtime directly (it
|
|
287
|
+
// is injected into generated code as a string), so it reads the bridge it
|
|
288
|
+
// registers on globalThis. Without this every one of those setters was a
|
|
289
|
+
// silent no-op.
|
|
290
|
+
const securityBridge = globalThis.__vesk_security;
|
|
291
|
+
// Rate limiting is enforced BEFORE the handler runs, so a per-request
|
|
292
|
+
// limit set inside the handler cannot apply to this same request. It is
|
|
293
|
+
// surfaced here for the next request on the connection-independent bag;
|
|
294
|
+
// the global limit configured in vesk.config is what gates this one.
|
|
295
|
+
// Wrap a handler call so overrides it records land in a fresh bag that is
|
|
296
|
+
// still readable when the response is written. Falls back to a pass-through
|
|
297
|
+
// when the runtime has not registered its bridge.
|
|
298
|
+
const securityRun = securityBridge?.run;
|
|
299
|
+
const withSecurityScope = securityRun
|
|
300
|
+
? (fn) => securityRun(fn)
|
|
301
|
+
: (fn) => fn();
|
|
302
|
+
const applyOverrideHeaders = (headers) => {
|
|
303
|
+
const overrides = securityBridge?.get?.();
|
|
304
|
+
if (!overrides)
|
|
305
|
+
return headers;
|
|
306
|
+
const next = { ...headers };
|
|
307
|
+
if (typeof overrides.contentSecurityPolicy === 'string') {
|
|
308
|
+
next['Content-Security-Policy'] = overrides.contentSecurityPolicy;
|
|
309
|
+
}
|
|
310
|
+
const custom = overrides.customHeaders;
|
|
311
|
+
if (custom) {
|
|
312
|
+
for (const [name, value] of Object.entries(custom)) {
|
|
313
|
+
if (value === false)
|
|
314
|
+
delete next[name];
|
|
315
|
+
else
|
|
316
|
+
next[name] = value;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return next;
|
|
320
|
+
};
|
|
321
|
+
const origWriteHead2 = res.writeHead.bind(res);
|
|
322
|
+
let overridesApplied = false;
|
|
323
|
+
res.writeHead = ((statusCode, headers) => {
|
|
324
|
+
if (!overridesApplied) {
|
|
325
|
+
overridesApplied = true;
|
|
326
|
+
headers = applyOverrideHeaders((headers || {}));
|
|
327
|
+
}
|
|
328
|
+
return origWriteHead2(statusCode, headers);
|
|
329
|
+
});
|
|
284
330
|
const publicDir = resolve(staticDir, 'public');
|
|
285
331
|
const rootFile = url.pathname.length > 1 ? resolveWithin(publicDir, url.pathname.slice(1)) : null;
|
|
286
332
|
if (rootFile && existsSync(rootFile) && statSync(rootFile).isFile()) {
|
|
@@ -427,7 +473,8 @@ export async function startProdServer(outDir, options) {
|
|
|
427
473
|
}
|
|
428
474
|
try {
|
|
429
475
|
const webRequest = makeWebRequest(req, url.href, maxBodyBytes);
|
|
430
|
-
const
|
|
476
|
+
const handleAction = mod.handleAction.bind(mod);
|
|
477
|
+
const response = await withSecurityScope(() => handleAction(webRequest, actionId));
|
|
431
478
|
await deliverResponse(res, response);
|
|
432
479
|
}
|
|
433
480
|
catch (e) {
|
|
@@ -449,7 +496,7 @@ export async function startProdServer(outDir, options) {
|
|
|
449
496
|
if (mod) {
|
|
450
497
|
try {
|
|
451
498
|
const webRequest = makeWebRequest(req, url.href, maxBodyBytes);
|
|
452
|
-
const response = await mod.handle(webRequest);
|
|
499
|
+
const response = await withSecurityScope(() => mod.handle(webRequest));
|
|
453
500
|
await deliverResponse(res, response);
|
|
454
501
|
return;
|
|
455
502
|
}
|
|
@@ -492,7 +539,7 @@ export async function startProdServer(outDir, options) {
|
|
|
492
539
|
if (route.revalidate && route.revalidate > 0) {
|
|
493
540
|
const { pageIsr } = await import('@vesk/runtime/src/index-server');
|
|
494
541
|
cachedResult = await pageIsr(url.pathname, async () => {
|
|
495
|
-
const response = await mod.handle(webRequest);
|
|
542
|
+
const response = await withSecurityScope(() => mod.handle(webRequest));
|
|
496
543
|
return { html: await response.text(), headers: Object.fromEntries(response.headers) };
|
|
497
544
|
}, { revalidate: route.revalidate, tags: route.tags || [] });
|
|
498
545
|
}
|
|
@@ -501,7 +548,7 @@ export async function startProdServer(outDir, options) {
|
|
|
501
548
|
res.end(cachedResult.html);
|
|
502
549
|
return;
|
|
503
550
|
}
|
|
504
|
-
const response = await mod.handle(webRequest);
|
|
551
|
+
const response = await withSecurityScope(() => mod.handle(webRequest));
|
|
505
552
|
const headers = Object.fromEntries(response.headers);
|
|
506
553
|
if (!headers['content-type'] && !headers['Content-Type'])
|
|
507
554
|
headers['Content-Type'] = 'text/html';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vesk/adapter",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.67",
|
|
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.67",
|
|
27
|
+
"@vesk/runtime": "^0.2.67",
|
|
28
|
+
"@vesk/types": "^0.2.67"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/ws": "^8.18.1",
|