Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"private": true,
"scripts": {
"dev": "vite --config vite.dev.config.ts --configLoader runner",
"build": "vite build --logLevel warn",
"build": "vite build --logLevel warn && node ../../scripts/precompress-app-dist.mjs dist",
"preview": "vite preview",
"clean": "rimraf dist",
"storybook": "ladle serve",
Expand Down
119 changes: 119 additions & 0 deletions apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readFile, stat } from "node:fs/promises";
import { performance } from "node:perf_hooks";
import { extname, join, resolve } from "node:path";
import { Hono } from "hono";
import { compress } from "hono/compress";
import { cors } from "hono/cors";
import {
buildLocalAppOrigins,
Expand Down Expand Up @@ -100,6 +101,8 @@ interface CreateAppOptions {
}

interface StaticResponseHeadersArgs {
contentEncoding?: string;
contentLength?: number;
contentType: string;
urlPath: string;
}
Expand All @@ -112,6 +115,10 @@ const WEB_SOCKET_SHUTDOWN_REASON = "server-shutdown";
const SLOW_API_REQUEST_LOG_THRESHOLD_MS = 1_000;
const THREAD_EVENT_WAIT_PATH_PATTERN =
/^\/api\/v1\/threads\/[^/]+\/events\/wait$/u;
const PRECOMPRESSED_STATIC_FILES = [
{ encoding: "br", extension: ".br" },
{ encoding: "gzip", extension: ".gz" },
] as const;

interface ShouldLogSlowApiRequestArgs {
durationMs: number;
Expand All @@ -135,9 +142,103 @@ function createStaticResponseHeaders(args: StaticResponseHeadersArgs): Headers {
? STATIC_ASSET_CACHE_CONTROL
: STATIC_INDEX_CACHE_CONTROL,
);
if (args.contentEncoding !== undefined) {
headers.set("content-encoding", args.contentEncoding);
headers.set("vary", "Accept-Encoding");
}
if (args.contentLength !== undefined) {
headers.set("content-length", String(args.contentLength));
}
return headers;
}

function acceptedEncodingQuality(
acceptEncodingHeader: string | undefined,
encoding: string,
): number {
if (acceptEncodingHeader === undefined) {
return 0;
}
let wildcardQuality = 0;
for (const part of acceptEncodingHeader.split(",")) {
const [rawName, ...rawParams] = part.trim().split(";");
const name = rawName?.trim().toLowerCase();
const qParam = rawParams
.map((param) => param.trim().toLowerCase())
.find((param) => param.startsWith("q="));
const quality =
qParam === undefined
? 1
: Number.isNaN(Number(qParam.slice(2)))
? 1
: Number(qParam.slice(2));
if (name === encoding) {
return quality;
}
if (name === "*") {
wildcardQuality = quality;
}
}
return wildcardQuality;
}

function canServePrecompressedStaticFile(contentType: string): boolean {
return (
contentType.startsWith("text/") ||
contentType === "application/javascript" ||
contentType === "application/json" ||
contentType === "application/manifest+json" ||
contentType === "application/wasm" ||
contentType === "application/xml" ||
contentType === "image/svg+xml"
);
}

async function findPrecompressedStaticFile(args: {
acceptEncodingHeader: string | undefined;
contentType: string;
filePath: string;
}): Promise<{
contentLength: number;
encoding: string;
filePath: string;
} | null> {
if (!canServePrecompressedStaticFile(args.contentType)) {
return null;
}

const candidates = PRECOMPRESSED_STATIC_FILES.map((candidate, index) => ({
...candidate,
index,
quality: acceptedEncodingQuality(
args.acceptEncodingHeader,
candidate.encoding,
),
}))
.filter((candidate) => candidate.quality > 0)
.sort(
(left, right) => right.quality - left.quality || left.index - right.index,
);

for (const candidate of candidates) {
const encodedFilePath = `${args.filePath}${candidate.extension}`;
try {
const encodedStat = await stat(encodedFilePath);
if (encodedStat.isFile()) {
return {
contentLength: encodedStat.size,
encoding: candidate.encoding,
filePath: encodedFilePath,
};
}
} catch {
// Sidecar missing — try the next acceptable encoding.
}
}

return null;
}

function buildAllowedCorsOrigins(deps: AppDeps): Set<string> {
const originArgs: BuildLocalAppOriginsArgs = {
serverPort: deps.config.serverPort,
Expand Down Expand Up @@ -204,6 +305,7 @@ export function createApp(
},
}),
);
app.use("*", compress());
app.onError((error) => errorToResponse(error, deps.logger));
app.get("/health", (context) => context.json({ ok: true }));
app.use("/api/v1/*", async (context, next) => {
Expand Down Expand Up @@ -375,6 +477,7 @@ export function createApp(
".woff": "font/woff",
".woff2": "font/woff2",
".webp": "image/webp",
".webmanifest": "application/manifest+json",
".map": "application/json",
};

Expand Down Expand Up @@ -403,6 +506,22 @@ export function createApp(
headers: createStaticResponseHeaders({ contentType, urlPath }),
});
}
const precompressedFile = await findPrecompressedStaticFile({
acceptEncodingHeader: context.req.header("accept-encoding"),
contentType,
filePath,
});
if (precompressedFile !== null) {
const content = await readFile(precompressedFile.filePath);
return new Response(content, {
headers: createStaticResponseHeaders({
contentEncoding: precompressedFile.encoding,
contentLength: precompressedFile.contentLength,
contentType,
urlPath,
}),
});
}
const content = await readFile(filePath);
return new Response(content, {
headers: createStaticResponseHeaders({ contentType, urlPath }),
Expand Down
58 changes: 56 additions & 2 deletions apps/server/test/app/static-cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { brotliCompressSync, gzipSync } from "node:zlib";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
Expand All @@ -13,9 +14,16 @@ describe("production static cache headers", () => {
join(staticDir, "index.html"),
'<!doctype html><script type="module" src="/assets/index-test.js"></script>',
);
const bundleBody = `console.log('${"fresh bundle ".repeat(600)}');`;
const bundlePath = join(staticDir, "assets", "index-test.js");
const brotliBundle = brotliCompressSync(Buffer.from(bundleBody));
const gzipBundle = gzipSync(Buffer.from(bundleBody));
await writeFile(bundlePath, bundleBody);
await writeFile(`${bundlePath}.br`, brotliBundle);
await writeFile(`${bundlePath}.gz`, gzipBundle);
await writeFile(
join(staticDir, "assets", "index-test.js"),
"console.log('fresh bundle');",
join(staticDir, "assets", "dynamic-only.js"),
`console.log('${"dynamic bundle ".repeat(600)}');`,
);

const harness = await createTestAppHarness();
Expand All @@ -34,6 +42,52 @@ describe("production static cache headers", () => {
"public, max-age=31536000, immutable",
);

const brotliAssetResponse = await serverApp.app.request(
"/assets/index-test.js",
{ headers: { "accept-encoding": "br, gzip" } },
);
expect(brotliAssetResponse.headers.get("content-encoding")).toBe("br");
expect(
brotliAssetResponse.headers
.get("vary")
?.split(",")
.map((value) => value.trim()),
).toContain("Accept-Encoding");
expect(brotliAssetResponse.headers.get("content-length")).toBe(
String(brotliBundle.length),
);
expect((await brotliAssetResponse.arrayBuffer()).byteLength).toBe(
brotliBundle.length,
);

const gzipAssetResponse = await serverApp.app.request(
"/assets/index-test.js",
{ headers: { "accept-encoding": "gzip" } },
);
expect(gzipAssetResponse.headers.get("content-encoding")).toBe("gzip");
expect(gzipAssetResponse.headers.get("content-length")).toBe(
String(gzipBundle.length),
);

const gzipPreferredAssetResponse = await serverApp.app.request(
"/assets/index-test.js",
{ headers: { "accept-encoding": "br;q=0, gzip;q=1" } },
);
expect(gzipPreferredAssetResponse.headers.get("content-encoding")).toBe(
"gzip",
);

const dynamicCompressedAssetResponse = await serverApp.app.request(
"/assets/dynamic-only.js",
{ headers: { "accept-encoding": "gzip" } },
);
expect(
dynamicCompressedAssetResponse.headers.get("content-encoding"),
).toBe("gzip");
expect(dynamicCompressedAssetResponse.headers.has("content-length")).toBe(
false,
);

const apiMissResponse = await serverApp.app.request(
"/api/v1/does-not-exist.js",
);
Expand Down
118 changes: 118 additions & 0 deletions scripts/precompress-app-dist.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env node
import {
constants as zlibConstants,
brotliCompressSync,
gzipSync,
} from "node:zlib";
import {
existsSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from "node:fs";
import { extname, join, resolve } from "node:path";

const DEFAULT_DIST_DIR = "apps/app/dist";
const MIN_COMPRESS_BYTES = 1024;
const COMPRESSIBLE_EXTENSIONS = new Set([
".css",
".js",
".json",
".mjs",
".svg",
".txt",
".wasm",
".webmanifest",
".xml",
]);

function usage() {
console.error("Usage: node scripts/precompress-app-dist.mjs [dist-dir]");
}

function walkFiles(dir) {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const filePath = join(dir, entry.name);
return entry.isDirectory() ? walkFiles(filePath) : [filePath];
});
}

function shouldPrecompress(filePath) {
if (filePath.endsWith(".br") || filePath.endsWith(".gz")) {
return false;
}
return COMPRESSIBLE_EXTENSIONS.has(extname(filePath));
}

function writeIfSmaller(args) {
if (args.compressed.length >= args.rawLength) {
return false;
}
writeFileSync(args.outputPath, args.compressed);
return true;
}

const args = process.argv.slice(2);
if (args.includes("-h") || args.includes("--help")) {
usage();
process.exit(0);
}
if (args.length > 1) {
usage();
process.exit(1);
}

const distDir = resolve(args[0] ?? DEFAULT_DIST_DIR);
if (!existsSync(distDir)) {
console.error(
`Missing ${distDir}. Run: pnpm exec turbo run build --filter=@bb/app`,
);
process.exit(1);
}

let sourceFiles = 0;
let brotliFiles = 0;
let gzipFiles = 0;

for (const filePath of walkFiles(distDir)) {
if (!shouldPrecompress(filePath)) {
continue;
}
const fileStat = statSync(filePath);
if (!fileStat.isFile() || fileStat.size < MIN_COMPRESS_BYTES) {
continue;
}

sourceFiles += 1;
const body = readFileSync(filePath);
const brotli = brotliCompressSync(body, {
params: {
[zlibConstants.BROTLI_PARAM_QUALITY]: 10,
},
});
const gzip = gzipSync(body, { level: 9 });

if (
writeIfSmaller({
compressed: brotli,
outputPath: `${filePath}.br`,
rawLength: body.length,
})
) {
brotliFiles += 1;
}
if (
writeIfSmaller({
compressed: gzip,
outputPath: `${filePath}.gz`,
rawLength: body.length,
})
) {
gzipFiles += 1;
}
}

console.log(
`precompressed ${sourceFiles} files (${brotliFiles} br, ${gzipFiles} gzip) in ${distDir}`,
);
Loading