fix(ui): prevent shiki template-call OOM on backtick JS

Neutralize the catastrophic TextMate template-call lookahead when loading
JS/TS grammars in the markdown Shiki worker, and terminate hung highlight
requests after 5s so unbounded Oniguruma WASM matching cannot OOM the
renderer (openchamber/openchamber#2587).

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-08-04 11:35:42 +00:00
co-authored by Serhii Dziupin
parent f47110c66f
commit 2b67be0a07
6 changed files with 197 additions and 11 deletions
@@ -0,0 +1,45 @@
/**
* Neutralize the JavaScript/TypeScript TextMate `template-call` rule.
*
* Upstream grammars use a triple-nested `{()[]}` lookahead to detect tagged
* templates with type arguments (`foo<T>\`...\``). On the Oniguruma WASM engine
* shipped with Shiki — which does not expose `setRetryLimit` / match-stack
* limits — that pattern can enter exponential backtracking on ordinary
* backtick template literals, grow the WASM heap without bound, and OOM the
* renderer (openchamber/openchamber#2587).
*
* Clearing `template-call` is safe: the plain `#template` rule still highlights
* backticks and simple tagged templates. Only the rare `ident<TypeArgs>\`...\``
* form loses its specialized type-argument coloring and falls through to
* normal tokenization.
*/
type GrammarRepository = Record<string, { patterns?: unknown[] } | undefined>;
export type TemplateCallGrammar = {
name?: string;
repository?: GrammarRepository;
};
const TEMPLATE_CALL_KEY = 'template-call';
export const hasCatastrophicTemplateCall = (grammar: TemplateCallGrammar): boolean => {
const patterns = grammar.repository?.[TEMPLATE_CALL_KEY]?.patterns;
return Array.isArray(patterns) && patterns.length > 0;
};
export const sanitizeTemplateCallGrammar = <T extends TemplateCallGrammar>(grammar: T): T => {
if (!hasCatastrophicTemplateCall(grammar)) return grammar;
const repository = { ...grammar.repository };
repository[TEMPLATE_CALL_KEY] = { patterns: [] };
return { ...grammar, repository };
};
/** Language ids whose bundled grammars ship the catastrophic `template-call` rule. */
export const TEMPLATE_CALL_LANGUAGE_IDS = ['javascript', 'typescript', 'jsx', 'tsx'] as const;
export type TemplateCallLanguageId = (typeof TEMPLATE_CALL_LANGUAGE_IDS)[number];
export const isTemplateCallLanguageId = (lang: string): lang is TemplateCallLanguageId =>
(TEMPLATE_CALL_LANGUAGE_IDS as readonly string[]).includes(lang);