From a7904bdadd429227713cdaafcd6a5e8ffa3d7162 Mon Sep 17 00:00:00 2001 From: Jim Vogel Date: Fri, 13 Mar 2026 01:40:07 -0500 Subject: [PATCH 1/2] fix: always call initSyntax on mount to prevent lost highlighting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Syntax highlighting is lost when DiffView is unmounted and remounted with the same file content. The global File cache causes initRaw() → #syncSyntax() to copy highlighter metadata from cached File objects into a fresh DiffFile, making the syntax effect's guard condition evaluate to false and skip initSyntax() entirely. initSyntax() is already idempotent — when syntax is initialized with a matching highlighter, it efficiently re-syncs syntax line references without recomputing. Remove the guard condition and always call initSyntax() when highlighting is enabled. Fixes #63 --- packages/react/src/components/DiffView.tsx | 27 ++++++++++--------- packages/solid/src/components/DiffView.tsx | 16 +++-------- .../svelte/src/lib/components/DiffView.svelte | 20 ++++---------- packages/vue/src/components/DiffView.tsx | 20 ++++---------- 4 files changed, 27 insertions(+), 56 deletions(-) diff --git a/packages/react/src/components/DiffView.tsx b/packages/react/src/components/DiffView.tsx index 9fbf2be..8cfb89d 100644 --- a/packages/react/src/components/DiffView.tsx +++ b/packages/react/src/components/DiffView.tsx @@ -315,22 +315,23 @@ const DiffViewWithRef = ( if (!diffFile) return; if (props.diffViewHighlight) { + // Always call initSyntax when highlight is enabled. + // initSyntax() is idempotent — when syntax is already initialized with a + // matching highlighter, it efficiently re-syncs syntax line references + // from the underlying File objects without recomputing. + // + // Previously, this effect skipped initSyntax() when the highlighter + // name/type already matched. However, on remount the global File cache + // causes initRaw() → #syncSyntax() to copy highlighter metadata from + // cached File objects into the fresh DiffFile, making it appear that + // syntax was already set up when it wasn't. This left syntax lines null + // and highlighting was lost on subsequent renders. if (registerHighlighter) { - if ( - registerHighlighter.name !== diffFile._getHighlighterName() || - registerHighlighter.type !== diffFile._getHighlighterType() || - registerHighlighter.type !== "class" - ) { - diffFile.initSyntax({ registerHighlighter: registerHighlighter }); - diffFile.notifyAll(); - } - } else if ( - (!diffFile._getIsCloned() && diffFile._getHighlighterName() !== buildInHighlighter.name) || - diffFile._getHighlighterType() !== "class" - ) { + diffFile.initSyntax({ registerHighlighter: registerHighlighter }); + } else { diffFile.initSyntax(); - diffFile.notifyAll(); } + diffFile.notifyAll(); } }, [diffFile, props.diffViewHighlight, registerHighlighter, diffViewTheme]); diff --git a/packages/solid/src/components/DiffView.tsx b/packages/solid/src/components/DiffView.tsx index 00e8cd6..2340071 100644 --- a/packages/solid/src/components/DiffView.tsx +++ b/packages/solid/src/components/DiffView.tsx @@ -198,21 +198,11 @@ const InternalDiffView = (props: DiffViewProps) => { if (props.diffViewHighlight) { const registerHighlighter = props.registerHighlighter; if (registerHighlighter) { - if ( - registerHighlighter.name !== currentDiffFile._getHighlighterName() || - registerHighlighter.type !== currentDiffFile._getHighlighterType() || - registerHighlighter.type !== "class" - ) { - currentDiffFile.initSyntax({ registerHighlighter: registerHighlighter }); - currentDiffFile.notifyAll(); - } - } else if ( - (!currentDiffFile._getIsCloned() && currentDiffFile._getHighlighterName() !== buildInHighlighter.name) || - currentDiffFile._getHighlighterType() !== "class" - ) { + currentDiffFile.initSyntax({ registerHighlighter: registerHighlighter }); + } else { currentDiffFile.initSyntax(); - currentDiffFile.notifyAll(); } + currentDiffFile.notifyAll(); } } }; diff --git a/packages/svelte/src/lib/components/DiffView.svelte b/packages/svelte/src/lib/components/DiffView.svelte index ef055cf..24f5bf9 100644 --- a/packages/svelte/src/lib/components/DiffView.svelte +++ b/packages/svelte/src/lib/components/DiffView.svelte @@ -164,23 +164,13 @@ if (enableHighlight) { const registerHighlighter = props.registerHighlighter; if (registerHighlighter) { - if ( - registerHighlighter.name !== diffFile._getHighlighterName() || - registerHighlighter.type !== diffFile._getHighlighterType() || - registerHighlighter.type !== 'class' - ) { - diffFile.initSyntax({ - registerHighlighter: registerHighlighter - }); - diffFile.notifyAll(); - } - } else if ( - (!diffFile._getIsCloned() && diffFile._getHighlighterName() !== buildInHighlighter.name) || - diffFile._getHighlighterType() !== 'class' - ) { + diffFile.initSyntax({ + registerHighlighter: registerHighlighter + }); + } else { diffFile.initSyntax(); - diffFile.notifyAll(); } + diffFile.notifyAll(); } }; diff --git a/packages/vue/src/components/DiffView.tsx b/packages/vue/src/components/DiffView.tsx index df367bd..e287644 100644 --- a/packages/vue/src/components/DiffView.tsx +++ b/packages/vue/src/components/DiffView.tsx @@ -165,23 +165,13 @@ export const DiffView = defineComponent< if (enableHighlight.value) { const registerHighlighter = props.registerHighlighter; if (registerHighlighter) { - if ( - registerHighlighter.name !== instance._getHighlighterName() || - registerHighlighter.type !== instance._getHighlighterType() || - registerHighlighter.type !== "class" - ) { - instance.initSyntax({ - registerHighlighter: registerHighlighter, - }); - instance.notifyAll(); - } - } else if ( - (!instance._getIsCloned() && instance._getHighlighterName() !== buildInHighlighter.name) || - instance._getHighlighterType() !== "class" - ) { + instance.initSyntax({ + registerHighlighter: registerHighlighter, + }); + } else { instance.initSyntax({}); - instance.notifyAll(); } + instance.notifyAll(); } }; From e02392738ecdfb283988b25ec7e538b7592b81a1 Mon Sep 17 00:00:00 2001 From: Jim Vogel Date: Fri, 13 Mar 2026 01:53:15 -0500 Subject: [PATCH 2/2] fix: decouple core from lowlight to reduce bundle size Remove the hard dependency on @git-diff-view/lowlight from core, which was causing all 130+ highlight.js language grammars (~870KB minified) to be bundled even when users provide their own highlighter. Changes: - core/file.ts: Replace direct highlighter import with injectable default via setDefaultHighlighter() - core/index.ts: Replace `export * from "@git-diff-view/lowlight"` with type-only re-exports - lowlight/index.ts: Auto-register as the default highlighter when imported via setDefaultHighlighter() - core/package.json: Move @git-diff-view/lowlight from dependencies to optional peerDependencies - Framework packages: Remove unused buildInHighlighter imports Users who provide a custom registerHighlighter can now avoid bundling lowlight entirely. Users who rely on the built-in lowlight highlighter just need to ensure @git-diff-view/lowlight is installed (which it is by default via the framework packages). BREAKING CHANGE: `highlighter` and `processAST` are no longer re-exported from @git-diff-view/core. Import them from @git-diff-view/lowlight instead. Fixes #58 --- packages/core/package.json | 12 +++++-- packages/core/src/file.ts | 31 +++++++++++++++---- packages/core/src/index.ts | 5 ++- packages/lowlight/package.json | 3 ++ packages/lowlight/src/index.ts | 5 +++ packages/react/src/components/DiffView.tsx | 2 +- packages/solid/src/components/DiffView.tsx | 2 +- .../svelte/src/lib/components/DiffView.svelte | 3 +- packages/vue/src/components/DiffView.tsx | 2 +- 9 files changed, 50 insertions(+), 15 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 27f2232..fbf0759 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -53,12 +53,18 @@ "diff parse" ], "dependencies": { - "@git-diff-view/lowlight": "^0.1.2", - "highlight.js": "^11.11.0", - "lowlight": "^3.3.0", "fast-diff": "^1.3.0" }, + "peerDependencies": { + "@git-diff-view/lowlight": ">=0.1.2" + }, + "peerDependenciesMeta": { + "@git-diff-view/lowlight": { + "optional": true + } + }, "devDependencies": { + "@git-diff-view/lowlight": "^0.1.2", "@types/hast": "^3.0.0" } } diff --git a/packages/core/src/file.ts b/packages/core/src/file.ts index 945eb20..1e22454 100644 --- a/packages/core/src/file.ts +++ b/packages/core/src/file.ts @@ -1,10 +1,20 @@ -import { highlighter } from "@git-diff-view/lowlight"; - import { Cache } from "./cache"; import { getPlainLineTemplate, getSyntaxLineTemplate, processTransformForFile } from "./parse"; import type { DiffAST, DiffHighlighter, DiffHighlighterLang, SyntaxLine } from "@git-diff-view/lowlight"; +let _defaultHighlighter: Omit | null = null; + +/** + * Register a default highlighter to use when no `registerHighlighter` is + * provided to `DiffView`. This is called automatically by + * `@git-diff-view/lowlight` when it is imported, so most users don't need + * to call this directly. + */ +export function setDefaultHighlighter(h: Omit) { + _defaultHighlighter = h; +} + const map = new Cache(); const devKey = "@git-diff-cache"; @@ -109,7 +119,16 @@ export class File { }) { if (!this.raw) return; - const finalHighlighter = registerHighlighter || highlighter; + const finalHighlighter = registerHighlighter || _defaultHighlighter; + + if (!finalHighlighter) { + if (__DEV__) { + console.warn( + "[@git-diff-view/core] No syntax highlighter available. Import @git-diff-view/lowlight or provide a registerHighlighter." + ); + } + return; + } if (this.rawLength > finalHighlighter.maxLineToIgnoreSyntax) { if (__DEV__) { @@ -121,15 +140,15 @@ export class File { } // check current lang is support or not - // if it's a unsupported lang, fallback to use lowlightHighlighter + // if it's a unsupported lang, fallback to use the default highlighter let supportEngin = finalHighlighter; try { if (!finalHighlighter.hasRegisteredCurrentLang(this.lang)) { - supportEngin = highlighter; + supportEngin = _defaultHighlighter || finalHighlighter; } } catch { - supportEngin = highlighter; + supportEngin = _defaultHighlighter || finalHighlighter; } if ( diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2330807..3ca8fae 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,6 +5,9 @@ export * from "./diff-file"; export * from "./escape-html"; export * from "./diff-file-utils"; -export * from "@git-diff-view/lowlight"; +// Re-export only types from @git-diff-view/lowlight to avoid bundling +// all highlight.js languages. Users who want the built-in lowlight +// highlighter should import it from @git-diff-view/lowlight directly. +export type { DiffHighlighter, DiffHighlighterLang, DiffAST, SyntaxLine, SyntaxNode } from "@git-diff-view/lowlight"; export const versions = __VERSION__; diff --git a/packages/lowlight/package.json b/packages/lowlight/package.json index b2b82e5..aa49cd0 100644 --- a/packages/lowlight/package.json +++ b/packages/lowlight/package.json @@ -56,5 +56,8 @@ "@types/hast": "^3.0.0", "highlight.js": "^11.11.0", "lowlight": "^3.3.0" + }, + "peerDependencies": { + "@git-diff-view/core": ">=0.0.1" } } diff --git a/packages/lowlight/src/index.ts b/packages/lowlight/src/index.ts index 5329206..4e33ba2 100644 --- a/packages/lowlight/src/index.ts +++ b/packages/lowlight/src/index.ts @@ -1,4 +1,5 @@ import { processAST } from "@git-diff-view/utils"; +import { setDefaultHighlighter } from "@git-diff-view/core"; import { createLowlight, all } from "lowlight"; import type { _getAST } from "./lang"; @@ -151,4 +152,8 @@ export const versions = __VERSION__; export const highlighter: DiffHighlighter = instance as DiffHighlighter; +// Auto-register as the default highlighter in @git-diff-view/core +// so that syntax highlighting works out of the box when this package is imported. +setDefaultHighlighter(highlighter); + export * from "./lang"; diff --git a/packages/react/src/components/DiffView.tsx b/packages/react/src/components/DiffView.tsx index 8cfb89d..789f765 100644 --- a/packages/react/src/components/DiffView.tsx +++ b/packages/react/src/components/DiffView.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { DiffFile, _cacheMap, SplitSide, highlighter as buildInHighlighter } from "@git-diff-view/core"; +import { DiffFile, _cacheMap, SplitSide } from "@git-diff-view/core"; import { diffFontSizeName, DiffModeEnum } from "@git-diff-view/utils"; import { memo, useEffect, useMemo, forwardRef, useImperativeHandle, useRef } from "react"; import * as React from "react"; diff --git a/packages/solid/src/components/DiffView.tsx b/packages/solid/src/components/DiffView.tsx index 2340071..b9a627f 100644 --- a/packages/solid/src/components/DiffView.tsx +++ b/packages/solid/src/components/DiffView.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */ -import { _cacheMap, DiffFile, SplitSide, highlighter as buildInHighlighter } from "@git-diff-view/core"; +import { _cacheMap, DiffFile, SplitSide } from "@git-diff-view/core"; import { diffFontSizeName, DiffModeEnum } from "@git-diff-view/utils"; import { type JSXElement, type JSX, createSignal, createEffect, createMemo, onCleanup, Show } from "solid-js"; diff --git a/packages/svelte/src/lib/components/DiffView.svelte b/packages/svelte/src/lib/components/DiffView.svelte index 24f5bf9..9315f27 100644 --- a/packages/svelte/src/lib/components/DiffView.svelte +++ b/packages/svelte/src/lib/components/DiffView.svelte @@ -4,8 +4,7 @@ import { DiffFile, type DiffHighlighter, - SplitSide, - highlighter as buildInHighlighter + SplitSide } from '@git-diff-view/core'; import { onDestroy, type Snippet } from 'svelte'; import { useIsMounted } from '$lib/hooks/useIsMounted.svelte.js'; diff --git a/packages/vue/src/components/DiffView.tsx b/packages/vue/src/components/DiffView.tsx index e287644..28b7f53 100644 --- a/packages/vue/src/components/DiffView.tsx +++ b/packages/vue/src/components/DiffView.tsx @@ -1,4 +1,4 @@ -import { DiffFile, _cacheMap, SplitSide, highlighter as buildInHighlighter } from "@git-diff-view/core"; +import { DiffFile, _cacheMap, SplitSide } from "@git-diff-view/core"; import { diffFontSizeName, DiffModeEnum } from "@git-diff-view/utils"; import { defineComponent, provide, ref, watch, watchEffect, computed, onUnmounted } from "vue";