From 968d5f85d26e3783226727cd830c93c48ed027e6 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Fri, 3 Jul 2026 11:49:38 +0700 Subject: [PATCH] refactor(markdown-editor): make the barrel tiptap-free so lazy(MarkdownEditor) is real (L1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit index.ts re-exported the heavy MarkdownEditor value, so any route statically importing a light util from the barrel (settings-prompt / template import EditorViewModeToggle + the variable probes) pulled the 527KB tiptap chunk into its eager graph — defeating their lazy(() => import(MarkdownEditor)). Extract the pure {{ }} helpers (VARIABLE_RE, variableProbe, findVariableUseRanges) into markdown-editor-variable-syntax.ts (no tiptap), export only light utils + the erased Handle type from the barrel, and import MarkdownEditor directly from its module (static in knowledge-form-controls, dynamic in the two lazy routes). Verified at build-artifact level: settings-prompt/template chunks now have ZERO static import of the tiptap/markdown-editor chunks — they reference the editor only via import("./markdown-editor-…"). tsc + lint + build + 831 vitest green. Co-Authored-By: Claude Opus 4.8 --- .../shared/markdown-editor/index.ts | 6 ++-- .../markdown-editor-highlight.test.ts | 2 +- .../markdown-editor-variable-highlight.ts | 27 ++---------------- .../markdown-editor-variable-syntax.ts | 28 +++++++++++++++++++ .../knowledges/knowledge-form-controls.tsx | 3 +- .../src/pages/settings/settings-prompt.tsx | 4 ++- frontend/src/pages/templates/template.tsx | 4 ++- 7 files changed, 44 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/shared/markdown-editor/markdown-editor-variable-syntax.ts diff --git a/frontend/src/components/shared/markdown-editor/index.ts b/frontend/src/components/shared/markdown-editor/index.ts index 4398f1d7..d8b2cab5 100644 --- a/frontend/src/components/shared/markdown-editor/index.ts +++ b/frontend/src/components/shared/markdown-editor/index.ts @@ -1,5 +1,7 @@ -export { MarkdownEditor } from './markdown-editor'; +// Deliberately does NOT re-export the heavy MarkdownEditor value — that would statically pull the tiptap chunk +// into any route importing a light util from here, defeating the lazy(MarkdownEditor) in settings-prompt / +// template. Import the component directly from './markdown-editor'. Only its type (erased) is safe to re-export. export type { MarkdownEditorHandle } from './markdown-editor'; -export { findVariableUseRanges, VARIABLE_RE, variableProbe } from './markdown-editor-variable-highlight'; +export { findVariableUseRanges, VARIABLE_RE, variableProbe } from './markdown-editor-variable-syntax'; export { EditorViewModeToggle } from './markdown-editor-view-mode'; export type { EditorViewMode } from './markdown-editor-view-mode'; diff --git a/frontend/src/components/shared/markdown-editor/markdown-editor-highlight.test.ts b/frontend/src/components/shared/markdown-editor/markdown-editor-highlight.test.ts index 7df4bc00..356bbdd4 100644 --- a/frontend/src/components/shared/markdown-editor/markdown-editor-highlight.test.ts +++ b/frontend/src/components/shared/markdown-editor/markdown-editor-highlight.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { TAG_RE } from './markdown-editor-tag-highlight'; -import { findVariableUseRanges, VARIABLE_RE, variableProbe } from './markdown-editor-variable-highlight'; +import { findVariableUseRanges, VARIABLE_RE, variableProbe } from './markdown-editor-variable-syntax'; const matches = (re: RegExp, text: string) => [...text.matchAll(re)].map((m) => m[0]); diff --git a/frontend/src/components/shared/markdown-editor/markdown-editor-variable-highlight.ts b/frontend/src/components/shared/markdown-editor/markdown-editor-variable-highlight.ts index 5707d270..86df51de 100644 --- a/frontend/src/components/shared/markdown-editor/markdown-editor-variable-highlight.ts +++ b/frontend/src/components/shared/markdown-editor/markdown-editor-variable-highlight.ts @@ -5,24 +5,16 @@ import { Plugin, PluginKey } from '@tiptap/pm/state'; import { Decoration, DecorationSet } from '@tiptap/pm/view'; import { collectInlineMatches } from './markdown-editor-inline-scan'; +import { VARIABLE_RE, variableProbe } from './markdown-editor-variable-syntax'; // Highlights Go-template actions ({{.Var}}, {{- if .X}}, {{end}}, {{.A | upper}}, …) as VIEW-ONLY // decorations. Decorations never touch the document, so getMarkdown() stays byte-identical, and {{ }} // already round-trips verbatim. A node/mark would gain nothing here and would break the variables // side-panel, which inserts {{.X}} as plain text and finds its uses by scanning, not by node identity. +// The pure `{{ }}` matching (VARIABLE_RE / variableProbe / findVariableUseRanges) lives in +// markdown-editor-variable-syntax.ts so tiptap-free consumers can use it without the decoration/PM code. const variableHighlightKey = new PluginKey('variableHighlight'); -// `[^{}]` keeps the scan linear (no catastrophic backtracking); Go actions never nest braces. -export const VARIABLE_RE = /\{\{[^{}]*\}\}/g; - -const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - -// Tests whether a single `{{ … }}` block references `variable` (`.Name` on a word boundary). Always used -// block-first — extract `{{ … }}` blocks with the linear VARIABLE_RE, THEN probe each — never a lazy -// `[^{}]*?` around the name, which backtracks O(n²) on an unclosed `{{`. Shared with settings-prompt.tsx -// (panel cycle + count) so the "used" badge and the editor cycle agree on what counts as a use. -export const variableProbe = (variable: string): RegExp => new RegExp(`\\.${escapeRegExp(variable)}\\b`); - const buildDecorations = (doc: PMNode): DecorationSet => DecorationSet.create( doc, @@ -39,19 +31,6 @@ export const findVariableOccurrences = (doc: PMNode, variable: string): { from: .map(({ from, to }) => ({ from, to })); }; -export const findVariableUseRanges = (value: string, variable: string): { index: number; length: number }[] => { - const probe = variableProbe(variable); - const ranges: { index: number; length: number }[] = []; - - for (const match of value.matchAll(VARIABLE_RE)) { - if (match.index !== undefined && probe.test(match[0])) { - ranges.push({ index: match.index, length: match[0].length }); - } - } - - return ranges; -}; - export const VariableHighlight = Extension.create({ addProseMirrorPlugins() { return [ diff --git a/frontend/src/components/shared/markdown-editor/markdown-editor-variable-syntax.ts b/frontend/src/components/shared/markdown-editor/markdown-editor-variable-syntax.ts new file mode 100644 index 00000000..ce181d0e --- /dev/null +++ b/frontend/src/components/shared/markdown-editor/markdown-editor-variable-syntax.ts @@ -0,0 +1,28 @@ +// Pure Go-template `{{ … }}` matching — no ProseMirror/tiptap imports, so consumers that only need the +// syntax helpers (settings-prompt's variable panel: cycle + "used" count) can pull them without dragging the +// editor (and its tiptap chunk) into their eager bundle. The DOM-facing decoration side lives in +// markdown-editor-variable-highlight.ts, which reuses VARIABLE_RE / variableProbe from here. + +// `[^{}]` keeps the scan linear (no catastrophic backtracking); Go actions never nest braces. +export const VARIABLE_RE = /\{\{[^{}]*\}\}/g; + +const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +// Tests whether a single `{{ … }}` block references `variable` (`.Name` on a word boundary). Always used +// block-first — extract `{{ … }}` blocks with the linear VARIABLE_RE, THEN probe each — never a lazy +// `[^{}]*?` around the name, which backtracks O(n²) on an unclosed `{{`. Shared with settings-prompt.tsx +// (panel cycle + count) so the "used" badge and the editor cycle agree on what counts as a use. +export const variableProbe = (variable: string): RegExp => new RegExp(`\\.${escapeRegExp(variable)}\\b`); + +export const findVariableUseRanges = (value: string, variable: string): { index: number; length: number }[] => { + const probe = variableProbe(variable); + const ranges: { index: number; length: number }[] = []; + + for (const match of value.matchAll(VARIABLE_RE)) { + if (match.index !== undefined && probe.test(match[0])) { + ranges.push({ index: match.index, length: match[0].length }); + } + } + + return ranges; +}; diff --git a/frontend/src/features/knowledges/knowledge-form-controls.tsx b/frontend/src/features/knowledges/knowledge-form-controls.tsx index f79c7865..b0c1f9eb 100644 --- a/frontend/src/features/knowledges/knowledge-form-controls.tsx +++ b/frontend/src/features/knowledges/knowledge-form-controls.tsx @@ -5,7 +5,8 @@ import type { KnowledgeGuideType as KnowledgeGuideTypeT, } from '@/graphql/types'; -import { type EditorViewMode, MarkdownEditor } from '@/components/shared/markdown-editor'; +import { type EditorViewMode } from '@/components/shared/markdown-editor'; +import { MarkdownEditor } from '@/components/shared/markdown-editor/markdown-editor'; import { Autocomplete, AutocompleteContent, diff --git a/frontend/src/pages/settings/settings-prompt.tsx b/frontend/src/pages/settings/settings-prompt.tsx index 95700032..f61a2545 100644 --- a/frontend/src/pages/settings/settings-prompt.tsx +++ b/frontend/src/pages/settings/settings-prompt.tsx @@ -98,7 +98,9 @@ import { cn } from '@/lib/utils'; // Dynamic-only import: a static import would merge the tiptap editor chunk into this route bundle. const MarkdownEditor = lazy(() => - import('@/components/shared/markdown-editor').then((module) => ({ default: module.MarkdownEditor })), + import('@/components/shared/markdown-editor/markdown-editor').then((module) => ({ + default: module.MarkdownEditor, + })), ); const systemFormSchema = z.object({ diff --git a/frontend/src/pages/templates/template.tsx b/frontend/src/pages/templates/template.tsx index 3657f8bd..cd25d5e8 100644 --- a/frontend/src/pages/templates/template.tsx +++ b/frontend/src/pages/templates/template.tsx @@ -46,7 +46,9 @@ import { cn } from '@/lib/utils'; import { type Template, useTemplates } from '@/providers/templates-provider'; const MarkdownEditor = lazy(() => - import('@/components/shared/markdown-editor').then((module) => ({ default: module.MarkdownEditor })), + import('@/components/shared/markdown-editor/markdown-editor').then((module) => ({ + default: module.MarkdownEditor, + })), ); const formSchema = z.object({