1234567import { existsSync, readFileSync } from "node:fs";8import { resolve } from "node:path";9import { describe, expect, it } from "vitest";1011type ContentScript = {12 matches?: string[];13 js?: string[];14 run_at?: string;15 all_frames?: boolean;16};1718type ExtensionManifest = {19 permissions?: string[];20 host_permissions?: string[];21 content_security_policy?: Record<string, string>;22 content_scripts?: ContentScript[];23 background?: {24 service_worker?: string;25 scripts?: string[];26 type?: string;27 };28};293031const CRXJS_ADDED_KEYS = ["web_accessible_resources"];3233const sourcePath = (target: "chrome" | "firefox") =>34 resolve(process.cwd(), target === "chrome" ? "manifest.json" : "manifest.firefox.json");35const builtPath = (target: "chrome" | "firefox") =>36 resolve(process.cwd(), `dist-${target}`, "manifest.json");3738function read(path: string): ExtensionManifest {39 return JSON.parse(readFileSync(path, "utf8")) as ExtensionManifest;40}4142for (const target of ["chrome", "firefox"] as const) {43 describe.skipIf(!existsSync(builtPath(target)))(44 `built ${target} manifest matches the reviewed source manifest`,45 () => {46 it("adds no permission and no host permission", () => {47 const source = read(sourcePath(target));48 const built = read(builtPath(target));4950 expect(built.permissions ?? []).toEqual(source.permissions ?? []);51 expect(built.host_permissions ?? []).toEqual(source.host_permissions ?? []);52 });5354 it("keeps the content security policy byte-identical", () => {55 const source = read(sourcePath(target));56 const built = read(builtPath(target));5758 expect(built.content_security_policy).toEqual(source.content_security_policy);59 });6061 it("keeps content_scripts matches, run_at and frame scope identical", () => {62 const source = read(sourcePath(target));63 const built = read(builtPath(target));6465 const shape = (scripts: ContentScript[] | undefined) =>66 (scripts ?? []).map((script) => ({67 matches: script.matches,68 run_at: script.run_at,69 all_frames: script.all_frames,70 scriptCount: script.js?.length ?? 0,71 }));7273 expect(shape(built.content_scripts)).toEqual(shape(source.content_scripts));74 });7576 it("keeps the same background kind and module type", () => {77 const source = read(sourcePath(target));78 const built = read(builtPath(target));7980 expect(Object.keys(built.background ?? {}).sort()).toEqual(81 Object.keys(source.background ?? {}).sort(),82 );83 expect(built.background?.type).toBe(source.background?.type);84 });8586 it("introduces no top-level key beyond the known crxjs addition", () => {87 const source = read(sourcePath(target));88 const built = JSON.parse(readFileSync(builtPath(target), "utf8")) as Record<89 string,90 unknown91 >;9293 const added = Object.keys(built).filter(94 (key) => !(key in source) && !CRXJS_ADDED_KEYS.includes(key),95 );96 expect(added).toEqual([]);97 });98 },99 );100}