diff --git a/extensions/diffs/src/pierre-themes.ts b/extensions/diffs/src/pierre-themes.ts index c2b61f293a6..fffa65bdc18 100644 --- a/extensions/diffs/src/pierre-themes.ts +++ b/extensions/diffs/src/pierre-themes.ts @@ -1,33 +1,58 @@ import fs from "node:fs/promises"; import { createRequire } from "node:module"; +import type { ThemeRegistrationResolved } from "@pierre/diffs"; import { RegisteredCustomThemes, ResolvedThemes, ResolvingThemes } from "@pierre/diffs"; -type RegisteredThemeLoader = NonNullable>; -type RegisteredTheme = Awaited>; -const require = createRequire(import.meta.url); +type PierreThemeName = "pierre-dark" | "pierre-light"; +const diffsRequire = createRequire(import.meta.resolve("@pierre/diffs")); +const PIERRE_THEME_SPECS = [ + ["pierre-dark", "@pierre/theme/themes/pierre-dark.json"], + ["pierre-light", "@pierre/theme/themes/pierre-light.json"], +] as const satisfies ReadonlyArray; -async function loadPierreTheme( +function createThemeLoader( + themeName: PierreThemeName, themeSpecifier: string, - themeName: string, -): Promise { - const themePath = require.resolve(themeSpecifier); - return { - ...(JSON.parse(await fs.readFile(themePath, "utf8")) as Record), - name: themeName, +): () => Promise { + let cachedTheme: ThemeRegistrationResolved | undefined; + return async () => { + if (cachedTheme) { + return cachedTheme; + } + const themePath = diffsRequire.resolve(themeSpecifier); + cachedTheme = { + ...(JSON.parse(await fs.readFile(themePath, "utf8")) as Record), + name: themeName, + } as ThemeRegistrationResolved; + return cachedTheme; }; } -export async function ensurePierreThemesRegistered(): Promise { - // Always overwrite the upstream loaders so the Node-safe path wins even if - // @pierre/diffs registered its JSON-import loaders earlier in this process. - RegisteredCustomThemes.set("pierre-light", () => - loadPierreTheme("@pierre/theme/themes/pierre-light.json", "pierre-light"), - ); - RegisteredCustomThemes.set("pierre-dark", () => - loadPierreTheme("@pierre/theme/themes/pierre-dark.json", "pierre-dark"), - ); - ResolvedThemes.delete("pierre-light"); - ResolvedThemes.delete("pierre-dark"); - ResolvingThemes.delete("pierre-light"); - ResolvingThemes.delete("pierre-dark"); +const PIERRE_THEME_LOADERS = new Map( + PIERRE_THEME_SPECS.map(([themeName, themeSpecifier]) => [ + themeName, + createThemeLoader(themeName, themeSpecifier), + ]), +); + +export function ensurePierreThemesRegistered(): void { + let replacedThemeLoader = false; + + for (const [themeName, loader] of PIERRE_THEME_LOADERS) { + if (RegisteredCustomThemes.get(themeName) !== loader) { + RegisteredCustomThemes.set(themeName, loader); + replacedThemeLoader = true; + } + } + + if (!replacedThemeLoader) { + return; + } + + // If another path swapped these loaders, clear the resolver caches so the + // next render rehydrates the highlighter with the Node-safe theme source. + for (const [themeName] of PIERRE_THEME_LOADERS) { + ResolvedThemes.delete(themeName); + ResolvingThemes.delete(themeName); + } } diff --git a/extensions/diffs/src/render.ts b/extensions/diffs/src/render.ts index 730ba1942d4..3788a31f382 100644 --- a/extensions/diffs/src/render.ts +++ b/extensions/diffs/src/render.ts @@ -1,12 +1,5 @@ -import fs from "node:fs/promises"; -import { createRequire } from "node:module"; -import type { - FileContents, - FileDiffMetadata, - SupportedLanguages, - ThemeRegistrationResolved, -} from "@pierre/diffs"; -import { RegisteredCustomThemes, parsePatchFiles } from "@pierre/diffs"; +import type { FileContents, FileDiffMetadata, SupportedLanguages } from "@pierre/diffs"; +import { parsePatchFiles } from "@pierre/diffs"; import { preloadFileDiff, preloadMultiFileDiff } from "@pierre/diffs/ssr"; import { ensurePierreThemesRegistered } from "./pierre-themes.js"; import type { @@ -21,45 +14,7 @@ import { VIEWER_LOADER_PATH } from "./viewer-assets.js"; const DEFAULT_FILE_NAME = "diff.txt"; const MAX_PATCH_FILE_COUNT = 128; const MAX_PATCH_TOTAL_LINES = 120_000; -const diffsRequire = createRequire(import.meta.resolve("@pierre/diffs")); - -let pierreThemesPatched = false; - -function createThemeLoader( - themeName: "pierre-dark" | "pierre-light", - themePath: string, -): () => Promise { - let cachedTheme: ThemeRegistrationResolved | undefined; - return async () => { - if (cachedTheme) { - return cachedTheme; - } - const raw = await fs.readFile(themePath, "utf8"); - const parsed = JSON.parse(raw) as Record; - cachedTheme = { - ...parsed, - name: themeName, - } as ThemeRegistrationResolved; - return cachedTheme; - }; -} - -function patchPierreThemeLoadersForNode24(): void { - if (pierreThemesPatched) { - return; - } - try { - const darkThemePath = diffsRequire.resolve("@pierre/theme/themes/pierre-dark.json"); - const lightThemePath = diffsRequire.resolve("@pierre/theme/themes/pierre-light.json"); - RegisteredCustomThemes.set("pierre-dark", createThemeLoader("pierre-dark", darkThemePath)); - RegisteredCustomThemes.set("pierre-light", createThemeLoader("pierre-light", lightThemePath)); - pierreThemesPatched = true; - } catch { - // Keep upstream loaders if theme files cannot be resolved. - } -} - -patchPierreThemeLoadersForNode24(); +ensurePierreThemesRegistered(); function escapeCssString(value: string): string { return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"'); @@ -376,7 +331,7 @@ async function renderBeforeAfterDiff( input: Extract, options: DiffRenderOptions, ): Promise<{ viewerBodyHtml: string; imageBodyHtml: string; fileCount: number }> { - await ensurePierreThemesRegistered(); + ensurePierreThemesRegistered(); const fileName = resolveBeforeAfterFileName(input); const lang = normalizeSupportedLanguage(input.lang); @@ -436,7 +391,7 @@ async function renderPatchDiff( input: Extract, options: DiffRenderOptions, ): Promise<{ viewerBodyHtml: string; imageBodyHtml: string; fileCount: number }> { - await ensurePierreThemesRegistered(); + ensurePierreThemesRegistered(); const files = parsePatchFiles(input.patch).flatMap((entry) => entry.files ?? []); if (files.length === 0) {