mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
66ce8172f3
* fix(build): skip unhelpful precompressed variants * fix(build): require wire-beneficial precompression * fix(server): ignore precompression temp artifacts * fix(server): scope temp exclusion to hashed assets * test(server): keep range fixture wire-beneficial --------- Co-authored-by: James <james@eli.cx>
423 lines
17 KiB
TypeScript
423 lines
17 KiB
TypeScript
/**
|
|
* Tests for the startup metadata cache used by the production server.
|
|
*
|
|
* StaticFileCache walks dist/client/ once at server startup, caches file
|
|
* metadata (path, size, content-type, cache-control, etag, precompressed
|
|
* variant paths), and serves lookups from memory with zero filesystem calls.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from "vite-plus/test";
|
|
import fsp from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { toSlash } from "pathslash";
|
|
import os from "node:os";
|
|
import zlib from "node:zlib";
|
|
import { StaticFileCache } from "../packages/vinext/src/server/static-file-cache.js";
|
|
|
|
/** Create a temp directory that mimics dist/client/ structure. */
|
|
async function setupClientDir(): Promise<string> {
|
|
const dir = path.join(
|
|
os.tmpdir(),
|
|
`vinext-cache-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
);
|
|
await fsp.mkdir(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
async function writeFile(
|
|
clientDir: string,
|
|
relativePath: string,
|
|
content: string | Buffer,
|
|
): Promise<void> {
|
|
const fullPath = path.join(clientDir, relativePath);
|
|
await fsp.mkdir(path.dirname(fullPath), { recursive: true });
|
|
await fsp.writeFile(fullPath, content);
|
|
}
|
|
|
|
describe("StaticFileCache", () => {
|
|
let clientDir: string;
|
|
|
|
beforeEach(async () => {
|
|
clientDir = await setupClientDir();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fsp.rm(clientDir, { recursive: true, force: true });
|
|
});
|
|
|
|
// ── Creation and scanning ──────────────────────────────────────
|
|
|
|
it("creates a cache by scanning the client directory", async () => {
|
|
await writeFile(clientDir, "_next/static/app-abc123.js", "const x = 1;");
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
|
|
expect(cache).toBeDefined();
|
|
});
|
|
|
|
it("handles empty client directory", async () => {
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
|
|
expect(cache.lookup("/_next/static/nope.js")).toBeUndefined();
|
|
});
|
|
|
|
it("handles non-existent client directory gracefully", async () => {
|
|
const cache = await StaticFileCache.create(path.join(clientDir, "does-not-exist"));
|
|
|
|
expect(cache.lookup("/anything")).toBeUndefined();
|
|
});
|
|
|
|
// ── Lookup ─────────────────────────────────────────────────────
|
|
|
|
it("returns cached metadata for an existing file", async () => {
|
|
await writeFile(clientDir, "_next/static/index-abc123.js", "const x = 1;");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/index-abc123.js");
|
|
|
|
expect(entry).toBeDefined();
|
|
expect(entry!.original.headers["Content-Type"]).toBe("application/javascript; charset=utf-8");
|
|
expect(entry!.original.headers["Content-Length"]).toBe("12"); // "const x = 1;"
|
|
expect(Date.parse(entry!.original.headers["Last-Modified"])).not.toBeNaN();
|
|
expect(entry!.original.path).toBe(
|
|
toSlash(path.join(clientDir, "_next/static/index-abc123.js")),
|
|
);
|
|
});
|
|
|
|
it("returns undefined for non-existent files", async () => {
|
|
await writeFile(clientDir, "_next/static/real-abc123.js", "x");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
|
|
expect(cache.lookup("/_next/static/missing-xyz789.js")).toBeUndefined();
|
|
});
|
|
|
|
it("ignores precompressed variants that are not smaller than the original", async () => {
|
|
await writeFile(clientDir, "_next/static/app-abc123.js", "small original");
|
|
await writeFile(clientDir, "_next/static/app-abc123.js.br", "larger brotli representation");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/app-abc123.js");
|
|
|
|
expect(entry?.br).toBeUndefined();
|
|
expect(entry?.original.headers.Vary).toBeUndefined();
|
|
});
|
|
|
|
it("ignores variants whose savings do not cover response-header overhead", async () => {
|
|
await writeFile(clientDir, "_next/static/app-abc123.js", "x".repeat(1000));
|
|
await writeFile(clientDir, "_next/static/app-abc123.js.br", "y".repeat(955));
|
|
await writeFile(clientDir, "_next/static/app-abc123.js.gz", "z".repeat(953));
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/app-abc123.js");
|
|
|
|
expect(entry?.br).toBeUndefined();
|
|
expect(entry?.gz).toBeUndefined();
|
|
expect(entry?.original.headers.Vary).toBeUndefined();
|
|
});
|
|
|
|
it("does not serve temporary files left by interrupted precompression", async () => {
|
|
const basename = "app-abc123.js.br.123.123e4567-e89b-42d3-a456-426614174000.tmp";
|
|
const temporaryPath = `_next/static/${basename}`;
|
|
await writeFile(clientDir, temporaryPath, "partial compressed content");
|
|
await writeFile(clientDir, `downloads/${basename}`, "legitimate public content");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
|
|
expect(cache.lookup("/" + temporaryPath)).toBeUndefined();
|
|
expect(cache.lookup(`/downloads/${basename}`)?.original.buffer?.toString()).toBe(
|
|
"legitimate public content",
|
|
);
|
|
});
|
|
|
|
it("sets immutable cache-control for hashed assets under /assets/", async () => {
|
|
await writeFile(clientDir, "_next/static/bundle-abc123.js", "x".repeat(100));
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/bundle-abc123.js");
|
|
|
|
expect(entry!.original.headers["Cache-Control"]).toBe("public, max-age=31536000, immutable");
|
|
});
|
|
|
|
it("sets short cache-control for non-hashed files", async () => {
|
|
await writeFile(clientDir, "favicon.ico", "icon-data");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/favicon.ico");
|
|
|
|
expect(entry!.original.headers["Cache-Control"]).toBe("public, max-age=3600");
|
|
});
|
|
|
|
it("generates weak etag from filename hash for hashed assets", async () => {
|
|
await writeFile(clientDir, "_next/static/app-abc123.css", ".body { margin: 0; }");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/app-abc123.css");
|
|
|
|
// Hashed assets use the content hash from the filename
|
|
expect(entry!.etag).toBe('W/"abc123"');
|
|
});
|
|
|
|
it("generates stable weak etag for dot-delimited managed image hashes", async () => {
|
|
await writeFile(clientDir, "_next/static/media/photo.0123abcd.png", "image bytes");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/media/photo.0123abcd.png");
|
|
|
|
expect(entry!.etag).toBe('W/"0123abcd"');
|
|
});
|
|
|
|
it("falls back to mtime etag for non-hashed files", async () => {
|
|
await writeFile(clientDir, "favicon.ico", "icon");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/favicon.ico");
|
|
|
|
expect(entry!.etag).toMatch(/^W\/"\d+-\d+"$/);
|
|
});
|
|
|
|
it("falls back to mtime etag for assets without hash suffix", async () => {
|
|
await writeFile(clientDir, "_next/static/logo.svg", "<svg></svg>");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/logo.svg");
|
|
|
|
// No hash in filename — falls back to mtime-based ETag
|
|
expect(entry!.etag).toMatch(/^W\/"\d+-\d+"$/);
|
|
});
|
|
|
|
it("does not treat non-hash suffixes as hashed asset etags", async () => {
|
|
await writeFile(clientDir, "_next/static/my-library-v2.0.0.js", "export {};");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/my-library-v2.0.0.js");
|
|
|
|
expect(entry!.etag).toMatch(/^W\/"\d+-\d+"$/);
|
|
});
|
|
|
|
it("does not treat arbitrary dot suffixes as managed image hashes", async () => {
|
|
await writeFile(clientDir, "_next/static/media/photo.deadbeefg.png", "image bytes");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/media/photo.deadbeefg.png");
|
|
|
|
expect(entry!.etag).toMatch(/^W\/"\d+-\d+"$/);
|
|
});
|
|
|
|
it("does not trust exact dot-hash suffixes outside managed media", async () => {
|
|
await writeFile(clientDir, "_next/static/config.deadbeef.json", '{"version":1}');
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/config.deadbeef.json");
|
|
|
|
expect(entry!.etag).toMatch(/^W\/"\d+-\d+"$/);
|
|
expect(entry!.etag).not.toBe('W/"deadbeef"');
|
|
expect(entry!.original.headers["Cache-Control"]).toBe("public, max-age=31536000, immutable");
|
|
});
|
|
|
|
// ── Precompressed variants ─────────────────────────────────────
|
|
|
|
it("detects brotli precompressed variant", async () => {
|
|
const content = "const x = 1;\n".repeat(200);
|
|
await writeFile(clientDir, "_next/static/app-abc123.js", content);
|
|
// Simulate build-time precompression
|
|
const brContent = zlib.brotliCompressSync(Buffer.from(content));
|
|
await writeFile(clientDir, "_next/static/app-abc123.js.br", brContent);
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/app-abc123.js");
|
|
|
|
expect(entry!.br?.path).toBe(toSlash(path.join(clientDir, "_next/static/app-abc123.js.br")));
|
|
expect(entry!.br?.headers["Content-Length"]).toBe(String(brContent.length));
|
|
});
|
|
|
|
it("detects gzip precompressed variant", async () => {
|
|
const content = "body { margin: 0; }\n".repeat(200);
|
|
await writeFile(clientDir, "_next/static/styles-def456.css", content);
|
|
const gzContent = zlib.gzipSync(Buffer.from(content));
|
|
await writeFile(clientDir, "_next/static/styles-def456.css.gz", gzContent);
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/styles-def456.css");
|
|
|
|
expect(entry!.gz?.path).toBe(
|
|
toSlash(path.join(clientDir, "_next/static/styles-def456.css.gz")),
|
|
);
|
|
expect(entry!.gz?.headers["Content-Length"]).toBe(String(gzContent.length));
|
|
});
|
|
|
|
it("detects zstandard precompressed variant", async () => {
|
|
const content = "const zstd = true;\n".repeat(200);
|
|
await writeFile(clientDir, "_next/static/app-zstd.js", content);
|
|
const zstdContent = zlib.zstdCompressSync(Buffer.from(content));
|
|
await writeFile(clientDir, "_next/static/app-zstd.js.zst", zstdContent);
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/app-zstd.js");
|
|
|
|
expect(entry!.zst?.path).toBe(toSlash(path.join(clientDir, "_next/static/app-zstd.js.zst")));
|
|
expect(entry!.zst?.headers["Content-Length"]).toBe(String(zstdContent.length));
|
|
});
|
|
|
|
it("sets Vary: Accept-Encoding on original variant when compressed siblings exist", async () => {
|
|
const content = "const x = 1;\n".repeat(200);
|
|
await writeFile(clientDir, "_next/static/app-abc123.js", content);
|
|
await writeFile(
|
|
clientDir,
|
|
"_next/static/app-abc123.js.br",
|
|
zlib.brotliCompressSync(Buffer.from(content)),
|
|
);
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/_next/static/app-abc123.js");
|
|
|
|
expect(entry!.original.headers["Vary"]).toBe("Accept-Encoding");
|
|
});
|
|
|
|
it("omits Vary on original variant when no compressed siblings exist", async () => {
|
|
await writeFile(clientDir, "favicon.ico", "icon-data");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/favicon.ico");
|
|
|
|
expect(entry!.original.headers["Vary"]).toBeUndefined();
|
|
});
|
|
|
|
it("does not expose .br/.gz/.zst files as standalone entries", async () => {
|
|
const content = "const x = 1;\n".repeat(200);
|
|
await writeFile(clientDir, "_next/static/app-abc123.js", content);
|
|
await writeFile(
|
|
clientDir,
|
|
"_next/static/app-abc123.js.br",
|
|
zlib.brotliCompressSync(Buffer.from(content)),
|
|
);
|
|
await writeFile(
|
|
clientDir,
|
|
"_next/static/app-abc123.js.gz",
|
|
zlib.gzipSync(Buffer.from(content)),
|
|
);
|
|
await writeFile(
|
|
clientDir,
|
|
"_next/static/app-abc123.js.zst",
|
|
zlib.zstdCompressSync(Buffer.from(content)),
|
|
);
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
|
|
// .br, .gz, .zst should not be independently servable
|
|
expect(cache.lookup("/_next/static/app-abc123.js.br")).toBeUndefined();
|
|
expect(cache.lookup("/_next/static/app-abc123.js.gz")).toBeUndefined();
|
|
expect(cache.lookup("/_next/static/app-abc123.js.zst")).toBeUndefined();
|
|
});
|
|
|
|
// ── HTML fallbacks ─────────────────────────────────────────────
|
|
|
|
it("resolves .html extension fallback for prerendered pages", async () => {
|
|
await writeFile(clientDir, "about.html", "<html>About</html>");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/about");
|
|
|
|
expect(entry).toBeDefined();
|
|
expect(entry!.original.path).toBe(toSlash(path.join(clientDir, "about.html")));
|
|
expect(entry!.original.headers["Content-Type"]).toBe("text/html; charset=utf-8");
|
|
});
|
|
|
|
it("resolves /index.html fallback for directory paths", async () => {
|
|
await writeFile(clientDir, "blog/index.html", "<html>Blog</html>");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
const entry = cache.lookup("/blog");
|
|
|
|
expect(entry).toBeDefined();
|
|
expect(entry!.original.path).toBe(toSlash(path.join(clientDir, "blog/index.html")));
|
|
});
|
|
|
|
// ── Directory traversal protection ─────────────────────────────
|
|
|
|
it("blocks .vite/ internal directory access", async () => {
|
|
await writeFile(clientDir, ".vite/manifest.json", "{}");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
|
|
expect(cache.lookup("/.vite/manifest.json")).toBeUndefined();
|
|
});
|
|
|
|
it("skips root / path", async () => {
|
|
await writeFile(clientDir, "index.html", "<html>Root</html>");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
|
|
// Root index.html is served by SSR/RSC, not static serving
|
|
expect(cache.lookup("/")).toBeUndefined();
|
|
});
|
|
|
|
// ── Content type detection ─────────────────────────────────────
|
|
|
|
it("detects content types from file extensions", async () => {
|
|
await writeFile(clientDir, "_next/static/style-aaa.css", "body{}");
|
|
await writeFile(clientDir, "_next/static/data-bbb.json", "{}");
|
|
await writeFile(clientDir, "logo.svg", "<svg/>");
|
|
await writeFile(clientDir, "photo.webp", "webp-data");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
|
|
expect(cache.lookup("/_next/static/style-aaa.css")!.original.headers["Content-Type"]).toBe(
|
|
"text/css; charset=utf-8",
|
|
);
|
|
expect(cache.lookup("/_next/static/data-bbb.json")!.original.headers["Content-Type"]).toBe(
|
|
"application/json; charset=utf-8",
|
|
);
|
|
expect(cache.lookup("/logo.svg")!.original.headers["Content-Type"]).toBe("image/svg+xml");
|
|
expect(cache.lookup("/photo.webp")!.original.headers["Content-Type"]).toBe("image/webp");
|
|
});
|
|
|
|
it("falls back to application/octet-stream for unknown extensions", async () => {
|
|
await writeFile(clientDir, "_next/static/data-ccc.xyz", "unknown-data");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
|
|
expect(cache.lookup("/_next/static/data-ccc.xyz")!.original.headers["Content-Type"]).toBe(
|
|
"application/octet-stream",
|
|
);
|
|
});
|
|
|
|
it("serves common web assets with their standard content types", async () => {
|
|
await Promise.all([
|
|
writeFile(clientDir, "module.wasm", "wasm"),
|
|
writeFile(clientDir, "movie.mp4", "video"),
|
|
writeFile(clientDir, "document.pdf", "pdf"),
|
|
writeFile(clientDir, "site.webmanifest", "{}"),
|
|
writeFile(clientDir, "feed.xml", "<feed />"),
|
|
]);
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
|
|
expect(cache.lookup("/module.wasm")!.original.headers["Content-Type"]).toBe("application/wasm");
|
|
expect(cache.lookup("/movie.mp4")!.original.headers["Content-Type"]).toBe("video/mp4");
|
|
expect(cache.lookup("/document.pdf")!.original.headers["Content-Type"]).toBe("application/pdf");
|
|
expect(cache.lookup("/site.webmanifest")!.original.headers["Content-Type"]).toBe(
|
|
"application/manifest+json",
|
|
);
|
|
expect(cache.lookup("/feed.xml")!.original.headers["Content-Type"]).toBe("application/xml");
|
|
});
|
|
|
|
it("matches file extensions case-insensitively", async () => {
|
|
await writeFile(clientDir, "logo.SVG", "<svg />");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
|
|
expect(cache.lookup("/logo.SVG")!.original.headers["Content-Type"]).toBe("image/svg+xml");
|
|
});
|
|
|
|
// ── Nested directory scanning ──────────────────────────────────
|
|
|
|
it("scans nested directories recursively", async () => {
|
|
await writeFile(clientDir, "_next/static/chunks/vendor-aaa.js", "vendor code");
|
|
await writeFile(clientDir, "_next/static/chunks/lazy/page-bbb.js", "page code");
|
|
|
|
const cache = await StaticFileCache.create(clientDir);
|
|
|
|
expect(cache.lookup("/_next/static/chunks/vendor-aaa.js")).toBeDefined();
|
|
expect(cache.lookup("/_next/static/chunks/lazy/page-bbb.js")).toBeDefined();
|
|
});
|
|
});
|