refactor(markdown-editor): make the barrel tiptap-free so lazy(MarkdownEditor) is real (L1)

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 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-03 11:49:38 +07:00
co-authored by Claude Opus 4.8
parent 996fcf49fa
commit 968d5f85d2
7 changed files with 44 additions and 30 deletions
@@ -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';
@@ -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]);
@@ -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 [
@@ -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;
};
@@ -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,
@@ -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({
+3 -1
View File
@@ -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({