[bug][export] Экспорт (Markdown/HTML) падает «Export failed:undefined» на страницах с комментариями: comment.renderHTML отдаёт чужой jsdom-узел при серверной сериализации (hap… #298
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Симптом
При экспорте страницы (формат по умолчанию — Markdown) в UI появляется уведомление:
Файл не скачивается. В логах сервера:
Затронуто
jsonToHtml→generateHTML).comment(комментарии/выделения). Страницы без комментариев экспортируются нормально — поэтому баг выглядит «плавающим».Первопричина
POST /api/pages/export→ExportService.exportPage()→jsonToHtml()→generateHTML()→getHTMLFromFragment()создаёт локальный happy-domWindowи сериализует документ черезDOMSerializer.serializeFragment(doc, { document: happyDomDocument }).comment(packages/editor-ext/src/lib/comment/comment.ts,renderHTML, строки ~171–211) при наличии глобальногоdocumentидёт по «браузерной» ветке и создаёт живой DOM-узел:document.createElement("span")+addEventListener("click", …)— и возвращает этот узел.documentприсутствует, потому что in-process MCP-модуль (packages/mcp/src/lib/collaboration.ts, строки ~43–45) при загрузке делает:McpModuleвapps/server/src/app.module.ts.typeof window === "undefined" || typeof document === "undefined"вcomment.renderHTMLне срабатывает (из-за jsdom-глобалов) → создаётся jsdom-узел<span>.DOMSerializer(работает поверх happy-dom-документа) вызываетhappyDomParagraph.appendChild(jsdomSpan). Внутри happy-domNodeUtility.isInclusiveAncestor(newNode, self)читаетnewNode[PropertySymbol.nodeArray].length, но у чужого (jsdom) узла нет happy-dom-свойстваnodeArray→Cannot read properties of undefined (reading 'length').Итог: в одно DOM-дерево (happy-dom) вставляется узел из другой реализации (jsdom).
comment— единственное расширение, чейrenderHTMLвозвращает живой DOM-узел; остальные inline-ноды/марки (heading,status,spoiler,mention, …) возвращают безопасные spec-массивы["span", attrs, 0].Воспроизведение
Ручное:
comment).Export failed:undefined, файл не скачан; в логах сервера — стек выше.Изолированный воспроизводитель (документ с одной comment-маркой, серверные исходники через
tsx):EXPORT FAILED: Cannot read properties of undefined (reading 'length').EXPORT OK: <p ...>Hello <span data-comment-id="c1" class="comment-mark">commented</span> world</p>.Предлагаемое исправление
1. Сервер (причина падения) —
packages/editor-ext/src/lib/comment/comment.tsЗаставить
renderHTMLна Node всегда возвращать сериализуемый spec-массив, даже если MCP инжектнул jsdom-глобалы. Признак Node устойчив к инжектуglobal.document:Браузерная ветка (клик →
ACTIVE_COMMENT_EVENT, слушается вapps/client/src/features/editor/page-editor.tsx) не меняется — интерактивность комментариев в редакторе сохраняется.2. Клиент (диагностируемость) —
apps/client/src/components/common/export-modal.tsxЗапрос идёт с
responseType: "blob", поэтому при ошибке тело приходит какBlob, иerr.response?.data.messageвсегдаundefined→ «Export failed:undefined». Нужно прочитать реальный текст ошибки из blob (с фолбэком наerr.message):Затронутые файлы
packages/editor-ext/src/lib/comment/comment.ts— фиксrenderHTML(основная причина).apps/client/src/components/common/export-modal.tsx— показ реального текста ошибки вместоundefined.apps/server/src/common/helpers/prosemirror/html/getHTMLFromFragment.ts,apps/server/src/integrations/export/export.service.ts,packages/mcp/src/lib/collaboration.ts.Критерии приёмки
commentв экспортированном HTML сериализуется как<span data-comment-id="…" class="comment-mark">…</span>(иresolved-вариант).undefined.comment-маркой при установленномglobal.document(jsdom) не бросает и даёт ожидаемый HTML.Связано
apps/server/src/common/helpers/prosemirror/html/generateHTML.tsуже фиксирует факт утечкиglobal.windowиз MCP-модуля.