perf: self-host fonts, drop unused generated code and add swf tests
- Self-host Nunito and Pixelify Sans via next/font instead of Google Fonts CDN (removes render-blocking external stylesheets and preconnects) - Remove stale mariadb entry from serverExternalPackages (app uses mysql2) - Exclude unused src/generated Prisma client from typecheck and remove it - Add unit tests for swf-parser, effectmap and figuremap (0% coverage -> 90%+)
This commit is contained in:
1 parent
ae1393d3e3
commit
4993b75608
7 files changed
+330
-23
No files matched your search
+1
-1
@@ -36,7 +36,7 @@ const securityHeaders = [
|
||||
const nextConfig: NextConfig = {
|
||||
cacheComponents: true, // <-- Dit activeert Cache Components en lost de 'instant' build-fout op!
|
||||
deploymentId: resolveDeploymentId(),
|
||||
serverExternalPackages: ["mariadb", "lzma", "sharp", "pino", "pino-pretty"],
|
||||
serverExternalPackages: ["lzma", "sharp", "pino", "pino-pretty"],
|
||||
|
||||
// Onderdruk de specifieke Turbopack waarschuwingen voor brede bestandspatronen in de lib-map
|
||||
turbopack: {
|
||||
|
||||
@@ -4,12 +4,6 @@
|
||||
@plugin "tailwindcss-animate";
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
:root {
|
||||
--font-nunito:
|
||||
"Nunito", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
--font-pixel: "Pixelify Sans", monospace;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans:
|
||||
var(--font-nunito), "Nunito", ui-sans-serif, system-ui, -apple-system,
|
||||
|
||||
+20
-15
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Nunito, Pixelify_Sans } from "next/font/google";
|
||||
import { headers } from "next/headers";
|
||||
import Script from "next/script";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
@@ -14,6 +15,20 @@ import { resolveHotelName } from "@/lib/hotel-name";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import "./globals.css";
|
||||
|
||||
const nunito = Nunito({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "600", "700", "800"],
|
||||
variable: "--font-nunito",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const pixelify = Pixelify_Sans({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "700"],
|
||||
variable: "--font-pixel",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
// The root layout reads runtime data (locale cookie, request headers via
|
||||
// enforceSiteAccess, DB-backed site settings) to render the <html> element,
|
||||
// which cannot be wrapped in <Suspense>. Child pages inherit this opt-out,
|
||||
@@ -58,23 +73,13 @@ export default async function RootLayout({
|
||||
const nonce = (await headers()).get("x-nonce") ?? undefined;
|
||||
|
||||
return (
|
||||
<html lang={locale} className="app" suppressHydrationWarning>
|
||||
<html
|
||||
lang={locale}
|
||||
className={`app ${nunito.variable} ${pixelify.variable}`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<meta name="theme-default-dark" content={String(defaultDark)} />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Pixelify+Sans:wght@400;700&display=swap"
|
||||
/>
|
||||
<Script
|
||||
src="/scripts/theme-init.js"
|
||||
strategy="beforeInteractive"
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { EffectMapEntry } from "@/types/effects";
|
||||
import {
|
||||
__setEffectMapPathForTests,
|
||||
listLocalEffects,
|
||||
mergeEffect,
|
||||
readEffectMap,
|
||||
removeEffect,
|
||||
} from "./effectmap";
|
||||
|
||||
vi.mock("@/lib/services/site-settings", () => ({
|
||||
siteSettings: { get: vi.fn(async () => "") },
|
||||
}));
|
||||
|
||||
let dir: string;
|
||||
let file: string;
|
||||
|
||||
const ENTRY: EffectMapEntry = {
|
||||
id: "fx_1",
|
||||
lib: "fx_1",
|
||||
type: "fx",
|
||||
revision: 1,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = await fs.mkdtemp(path.join(os.tmpdir(), "effectmap-"));
|
||||
file = path.join(dir, "EffectMap.json");
|
||||
await fs.writeFile(file, JSON.stringify({ effects: [ENTRY] }));
|
||||
__setEffectMapPathForTests(file);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
__setEffectMapPathForTests(null);
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("lib/services/effectmap", () => {
|
||||
it("lists local effects from the map", async () => {
|
||||
const effects = await listLocalEffects();
|
||||
expect(effects).toEqual([ENTRY]);
|
||||
});
|
||||
|
||||
it("returns an empty list when the file has no effects array", async () => {
|
||||
await fs.writeFile(file, JSON.stringify({}));
|
||||
expect(await readEffectMap()).toEqual({ effects: [] });
|
||||
});
|
||||
|
||||
it("adds a new effect via mergeEffect", async () => {
|
||||
const added: EffectMapEntry = {
|
||||
id: "fx_2",
|
||||
lib: "fx_2",
|
||||
type: "fx",
|
||||
revision: 2,
|
||||
};
|
||||
await mergeEffect(added);
|
||||
const effects = await listLocalEffects();
|
||||
expect(effects).toHaveLength(2);
|
||||
expect(effects).toContainEqual(added);
|
||||
});
|
||||
|
||||
it("updates an existing effect in place via mergeEffect", async () => {
|
||||
await mergeEffect({ ...ENTRY, revision: 5 });
|
||||
const effects = await listLocalEffects();
|
||||
expect(effects).toHaveLength(1);
|
||||
expect(effects[0].revision).toBe(5);
|
||||
});
|
||||
|
||||
it("removes an effect by id", async () => {
|
||||
expect(await removeEffect("fx_1")).toBe(true);
|
||||
expect(await listLocalEffects()).toEqual([]);
|
||||
});
|
||||
|
||||
it("removes an effect by lib name", async () => {
|
||||
expect(await removeEffect("fx_1")).toBe(true);
|
||||
expect(await listLocalEffects()).toEqual([]);
|
||||
});
|
||||
|
||||
it("reports false when nothing was removed", async () => {
|
||||
expect(await removeEffect("missing")).toBe(false);
|
||||
expect(await listLocalEffects()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { FigureLibrary } from "./figuremap";
|
||||
import {
|
||||
__setFigureMapPathForTests,
|
||||
listFigureLibraries,
|
||||
mergeLibrary,
|
||||
} from "./figuremap";
|
||||
|
||||
vi.mock("@/lib/services/site-settings", () => ({
|
||||
siteSettings: { get: vi.fn(async () => "") },
|
||||
}));
|
||||
|
||||
let dir: string;
|
||||
let file: string;
|
||||
|
||||
const LIB: FigureLibrary = {
|
||||
id: "h_human",
|
||||
revision: 1,
|
||||
parts: [{ id: 1, type: "hd" }],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = await fs.mkdtemp(path.join(os.tmpdir(), "figuremap-"));
|
||||
file = path.join(dir, "FigureMap.json");
|
||||
await fs.writeFile(file, JSON.stringify({ libraries: [LIB] }));
|
||||
__setFigureMapPathForTests(file);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
__setFigureMapPathForTests(null);
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("lib/services/figuremap", () => {
|
||||
it("lists figure libraries from the map", async () => {
|
||||
const libraries = await listFigureLibraries();
|
||||
expect(libraries).toEqual([LIB]);
|
||||
});
|
||||
|
||||
it("returns an empty list when the file has no libraries array", async () => {
|
||||
await fs.writeFile(file, JSON.stringify({}));
|
||||
expect(await listFigureLibraries()).toEqual([]);
|
||||
});
|
||||
|
||||
it("adds a new library via mergeLibrary", async () => {
|
||||
const added: FigureLibrary = { id: "hh_human", revision: 2 };
|
||||
await mergeLibrary(added);
|
||||
const libraries = await listFigureLibraries();
|
||||
expect(libraries).toHaveLength(2);
|
||||
expect(libraries).toContainEqual(added);
|
||||
});
|
||||
|
||||
it("updates an existing library in place via mergeLibrary", async () => {
|
||||
await mergeLibrary({ ...LIB, revision: 9 });
|
||||
const libraries = await listFigureLibraries();
|
||||
expect(libraries).toHaveLength(1);
|
||||
expect(libraries[0].revision).toBe(9);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { deflateSync } from "node:zlib";
|
||||
import LZMA from "lzma";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
decompressSwf,
|
||||
parseBinaryData,
|
||||
parseSwfTags,
|
||||
parseSymbolClass,
|
||||
TAG_DEFINE_BINARY_DATA,
|
||||
TAG_DEFINE_BITS_LOSSLESS,
|
||||
TAG_END,
|
||||
} from "./swf-parser";
|
||||
|
||||
const BODY = Buffer.from(
|
||||
"uncompressed swf body used for decompression fixtures, padded to a reasonable length",
|
||||
);
|
||||
|
||||
function fwsHeader(payload: Buffer, version = 6): Buffer {
|
||||
const header = Buffer.alloc(8);
|
||||
header.write("FWS", 0, "ascii");
|
||||
header.writeUInt8(version, 3);
|
||||
header.writeUInt32LE(payload.length + 8, 4);
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
function cwsFixture(payload: Buffer, version = 6): Buffer {
|
||||
const header = Buffer.alloc(8);
|
||||
header.write("CWS", 0, "ascii");
|
||||
header.writeUInt8(version, 3);
|
||||
header.writeUInt32LE(payload.length + 8, 4);
|
||||
const compressed = deflateSync(payload);
|
||||
return Buffer.concat([header, compressed]);
|
||||
}
|
||||
|
||||
function zwsFixture(payload: Buffer, version = 6): Buffer {
|
||||
const rawLzma = LZMA.compress(payload, 9);
|
||||
const props = Buffer.from(rawLzma.slice(0, 5));
|
||||
const compressedPayload = Buffer.from(rawLzma.slice(13));
|
||||
const compressedSize = 5 + compressedPayload.length;
|
||||
const header = Buffer.alloc(12);
|
||||
header.write("ZWS", 0, "ascii");
|
||||
header.writeUInt8(version, 3);
|
||||
header.writeUInt32LE(payload.length + 8, 4);
|
||||
header.writeUInt32LE(compressedSize, 8);
|
||||
return Buffer.concat([header, props, compressedPayload]);
|
||||
}
|
||||
|
||||
describe("decompressSwf", () => {
|
||||
it("returns FWS buffers untouched", () => {
|
||||
const fws = fwsHeader(BODY);
|
||||
expect(decompressSwf(fws)).toBe(fws);
|
||||
});
|
||||
|
||||
it("inflates CWS (zlib) buffers and restores the FWS signature", () => {
|
||||
const result = decompressSwf(cwsFixture(BODY));
|
||||
expect(result.toString("ascii", 0, 3)).toBe("FWS");
|
||||
expect(result.length).toBe(BODY.length + 8);
|
||||
expect(result.subarray(8).equals(BODY)).toBe(true);
|
||||
});
|
||||
|
||||
it("decompresses ZWS (lzma) buffers and restores the FWS signature", () => {
|
||||
const result = decompressSwf(zwsFixture(BODY));
|
||||
expect(result.toString("ascii", 0, 3)).toBe("FWS");
|
||||
expect(result.length).toBe(BODY.length + 8);
|
||||
expect(result.subarray(8).equals(BODY)).toBe(true);
|
||||
});
|
||||
|
||||
it("throws on unknown signatures", () => {
|
||||
const bad = Buffer.alloc(16);
|
||||
bad.write("XYZ", 0, "ascii");
|
||||
expect(() => decompressSwf(bad)).toThrow("Unknown SWF signature");
|
||||
});
|
||||
});
|
||||
|
||||
function shortTag(tagType: number, data: Buffer): Buffer {
|
||||
expect(data.length).toBeLessThan(0x3f);
|
||||
const tag = Buffer.alloc(2 + data.length);
|
||||
tag.writeUInt16LE((tagType << 6) | data.length, 0);
|
||||
data.copy(tag, 2);
|
||||
return tag;
|
||||
}
|
||||
|
||||
function swfWithTags(tags: Buffer[]): Buffer {
|
||||
const tagStream = Buffer.concat([...tags, Buffer.from([0, 0])]);
|
||||
const body = Buffer.alloc(13);
|
||||
body.write("FWS", 0, "ascii");
|
||||
body.writeUInt8(6, 3);
|
||||
body.writeUInt32LE(tagStream.length + 13, 4);
|
||||
return Buffer.concat([body, tagStream]);
|
||||
}
|
||||
|
||||
describe("parseSwfTags", () => {
|
||||
it("parses short-form tags until the end marker", () => {
|
||||
const swf = swfWithTags([
|
||||
shortTag(TAG_DEFINE_BITS_LOSSLESS, Buffer.from([1, 2, 3])),
|
||||
shortTag(TAG_DEFINE_BINARY_DATA, Buffer.from([9, 9, 9])),
|
||||
]);
|
||||
const tags = parseSwfTags(swf);
|
||||
expect(tags).toHaveLength(2);
|
||||
expect(tags[0].type).toBe(TAG_DEFINE_BITS_LOSSLESS);
|
||||
expect(tags[0].data.equals(Buffer.from([1, 2, 3]))).toBe(true);
|
||||
expect(tags[1].type).toBe(TAG_DEFINE_BINARY_DATA);
|
||||
});
|
||||
|
||||
it("stops at TAG_END and ignores trailing bytes", () => {
|
||||
const swf = swfWithTags([
|
||||
shortTag(TAG_DEFINE_BITS_LOSSLESS, Buffer.from([1])),
|
||||
]);
|
||||
expect(parseSwfTags(swf)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("returns an empty array for an empty tag stream", () => {
|
||||
const empty = Buffer.alloc(13);
|
||||
empty.write("FWS", 0, "ascii");
|
||||
expect(parseSwfTags(empty)).toEqual([]);
|
||||
expect(TAG_END).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSymbolClass", () => {
|
||||
it("maps char ids to names and tracks aliases", () => {
|
||||
const data = Buffer.concat([
|
||||
Buffer.from([2, 0]),
|
||||
Buffer.from([1, 0]),
|
||||
Buffer.from("chair\0", "utf8"),
|
||||
Buffer.from([2, 0]),
|
||||
Buffer.from("table\0", "utf8"),
|
||||
]);
|
||||
const result = parseSymbolClass(data);
|
||||
expect(result.classNames.get(1)).toBe("chair");
|
||||
expect(result.classNames.get(2)).toBe("table");
|
||||
expect(result.symbols.get("chair")).toBe(1);
|
||||
expect(result.symbols.get("table")).toBe(2);
|
||||
});
|
||||
|
||||
it("keeps the first name for a repeated char id", () => {
|
||||
const data = Buffer.concat([
|
||||
Buffer.from([2, 0]),
|
||||
Buffer.from([1, 0]),
|
||||
Buffer.from("chair\0", "utf8"),
|
||||
Buffer.from([1, 0]),
|
||||
Buffer.from("lounge_chair\0", "utf8"),
|
||||
]);
|
||||
const result = parseSymbolClass(data);
|
||||
expect(result.classNames.get(1)).toBe("chair");
|
||||
expect(result.symbols.get("lounge_chair")).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseBinaryData", () => {
|
||||
it("reads the char id and strips the 6-byte attribute header", () => {
|
||||
const data = Buffer.concat([
|
||||
Buffer.from([42, 0]),
|
||||
Buffer.from([1, 2, 3, 4]),
|
||||
Buffer.from("binary payload"),
|
||||
]);
|
||||
const result = parseBinaryData(data);
|
||||
expect(result.id).toBe(42);
|
||||
expect(result.content.equals(Buffer.from("binary payload"))).toBe(true);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -32,5 +32,5 @@
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "e2e", "playwright.config.ts"]
|
||||
"exclude": ["node_modules", "e2e", "playwright.config.ts", "src/generated"]
|
||||
}
|
||||
Reference in new issue
Block a user