From 17f7fed1dbf6f3cf4b3799ebdf8bdab30fab4c89 Mon Sep 17 00:00:00 2001 From: weichen Date: Sat, 19 Sep 2026 09:51:04 +0000 Subject: [PATCH] feat(web): load export, preview, and charts only when opened The login shell was downloading the document engines with the workbench. Those modules now load on click or when a matching block appears, and a failed chunk can be retried without submitting an edit. Co-authored-by: Cursor --- .doc/ops/CURRENT.md | 2 +- .../long-term/2026-09-19-audit-remainder.md | 4 +- .doc/status/FEATURE-MATRIX.md | 2 +- web/src/App.tsx | 86 ++++++++++++------- web/src/components/chat/ReplyActionBar.tsx | 3 +- web/src/components/doc/BlockRenderer.tsx | 24 +++++- .../doc/preview/FilePreviewPanel.tsx | 24 ++++-- web/src/components/lazy/OnDemand.tsx | 52 +++++++++++ web/src/features/replyExport/download.ts | 53 ------------ web/src/features/replyExport/downloadDocx.ts | 53 ++++++++++++ web/src/loginShellImports.test.ts | 39 +++++++++ 11 files changed, 242 insertions(+), 100 deletions(-) create mode 100644 web/src/components/lazy/OnDemand.tsx create mode 100644 web/src/features/replyExport/downloadDocx.ts create mode 100644 web/src/loginShellImports.test.ts diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 5d3ec609..411e2044 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -18,7 +18,7 @@ | Cursor | audit-b2-authz-hardening | fix/audit-b2-authz-hardening | - | authz | done | | Cursor | audit-b3-sse-events | fix/audit-b3-sse-events | - | chat+events | done | | Cursor | audit-b4-frontend | fix/audit-b4-frontend | - | web | done | -| Cursor | audit-remainder-backlog | docs/audit-remediation-backlog | - | audit | done | +| Cursor-Auto | 2026-09-19-audit-remainder-closeout | fix/audit-remainder-closeout | - | audit-remainder | in_progress | | Cursor-Auto | 2026-09-19-remove-control-center | fix/remove-control-center | - | controlcenter | done | | Cursor-Auto | 2026-09-17-control-center-server | feature/control-center-server | - | controlcenter | cancelled | | Kimi | 2026-09-18-user-manual | feature/user-manual | .worktrees/feature-user-manual | docs | done | diff --git a/.doc/ops/long-term/2026-09-19-audit-remainder.md b/.doc/ops/long-term/2026-09-19-audit-remainder.md index f368e0af..528698a2 100644 --- a/.doc/ops/long-term/2026-09-19-audit-remainder.md +++ b/.doc/ops/long-term/2026-09-19-audit-remainder.md @@ -46,10 +46,10 @@ updated: 2026-09-19 | FE 缓存 | done(版本字段)。窗口缓存写入 `{schema,data}`,无 schema 的旧载荷丢弃。`window cache: 无 schema` | | FE lucide | 关闭理由:`package-lock.json` 锁定 `lucide-react@1.42.0`,来源 `registry.npmjs.org`,带 integrity。`^1.16.0` 是合法的 1.x 范围,不是来源不明的包 | | M7 | done(出站意图 + 同键重放)。`document_content_projections` 与账本同一事务写入;投影成功才销账。`TestDocRevision_PatchOrder_RetryProjectsWithoutSecondLedgerCommit`。账本库与正文库仍不是一个 SQL 事务,进程退出后靠未投影行重放,不靠客户端记忆 | -| FE App.tsx | 仍开放。4600 行拆 hook 是独立重构,本轮未拆,避免在没有行为变更需求时改写工作台 | +| FE App.tsx | 部分。登录壳不再静态引用导出引擎、文件预览、管理台、块编辑器和图表。入口包 2.83 MB → 1.00 MB(br 626 KB → 235 KB)。`loginShellImports.test.ts`。工作台主体仍在 `App.tsx`,未按 hook 拆文件 | | H1-del | 关闭理由:`allowLegacyTestPrincipal` 仍被 `AllowInMemory && ContextDbHost==""` 的 dev/test 内存测试使用。删除会把这批测试变成未登录 401。保留告警,删除另开、先改测试夹具 | -仍开放:FE `App.tsx` 拆分、rdev S2/S3 与 P2–P4、首次加载 U1/U3–U7、dev 容器重建与治理库 PG。本分支已跑通 `cd web && npm test`(tsx 407 + vitest 32,全过)和 `npm run build`(主包 `index-*.js` 仍约 2.83 MB,U3 拆包未做)。H2 批注/审计的项目成员检查在 Postgres 上走 `RequireProjectAccess`;内存授权器在无库时把任意正用户 id 视为成员,这是 `TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite` 的隔离前提,不能改成默认拒绝。H2 的 HTTP 负例因此要在 `TEST_DATABASE_URL` 上补,本机未设置该变量。M7 的投影意图已随账本提交(goose `00053`);容器 schema 仍是 52,要等占槽重建后才升到 53。 +仍开放:rdev S2/S3 与 P2–P4、首次加载 U1/U4–U7、dev 容器重建与治理库 PG。U3 只完成登录壳拆包(入口包 1.00 MB / br 235 KB),消息公式引擎仍在入口图里。本分支已跑通 `cd web && npm test` 与 `npm run build`。H2 批注/审计的项目成员检查在 Postgres 上走 `RequireProjectAccess`;内存授权器在无库时把任意正用户 id 视为成员,这是 `TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite` 的隔离前提,不能改成默认拒绝。H2 的 HTTP 负例因此要在 `TEST_DATABASE_URL` 上补,本机未设置该变量。M7 的投影意图已随账本提交(goose `00053`);容器 schema 仍是 52,要等占槽重建后才升到 53。 红线关闭(不是实现): diff --git a/.doc/status/FEATURE-MATRIX.md b/.doc/status/FEATURE-MATRIX.md index 894815c6..7d72eb53 100644 --- a/.doc/status/FEATURE-MATRIX.md +++ b/.doc/status/FEATURE-MATRIX.md @@ -96,7 +96,7 @@ ## 2026-09-17 首次加载性能专项 -首次加载与重启首访性能优化:**partial**(U2 静态交付已验收;不改变现有功能 done/发布门禁结论)。[审计](../audit/2026-09-17-initial-load-performance-audit.md) · [方案](../remediation/2026-09-17-initial-load-performance-plan.md) · [U2 报告](../test-reports/2026-09-17-initial-load-perf-u2-test-report.md)。U2:自托管字体 + br/gzip + assets 404;主 JS 传输 2.82 MB → ~624 KB。U1/U3–U7 未完成。mutex `web + static-delivery`(U2 done)/ `loading-performance-audit`。 +首次加载与重启首访性能优化:**partial**(U2 静态交付已验收;U3 只做了登录壳拆包,未关闭)。[审计](../audit/2026-09-17-initial-load-performance-audit.md) · [方案](../remediation/2026-09-17-initial-load-performance-plan.md) · [U2 报告](../test-reports/2026-09-17-initial-load-perf-u2-test-report.md)。U2:自托管字体 + br/gzip。U3 进展(`fix/audit-remainder-closeout`):导出、预览、管理台、块编辑器、Mermaid/公式改为点击或出现时再加载。入口包 `index-*.js` 由 2.83 MB / br 626 KB 降到 1.00 MB / br 235 KB。消息气泡里的公式引擎仍在入口图里。U1、U4–U7 未做。mutex `web + static-delivery`(U2 done)/ `loading-performance-audit`。 ## 2026-09-17 文档编制人兼任方案 diff --git a/web/src/App.tsx b/web/src/App.tsx index 0f182bd5..08bd6fee 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -5,7 +5,6 @@ import { InputArea } from './components/chat/InputArea'; import { SlimTabBar } from './components/doc/SlimTabBar'; import { DeckDrawer } from './components/doc/DeckDrawer'; import { BlockRenderer } from './components/doc/BlockRenderer'; -import { BlockEditorModal } from './components/doc/BlockEditorModal'; import { DocumentRevisionToolbar } from './components/doc/DocumentRevisionToolbar'; import { CommentSidePanel } from './components/doc/CommentSidePanel'; import { DocGovActionBar } from './components/doc/DocGovActionBar'; @@ -106,17 +105,16 @@ import { StaleDocumentNotice } from './components/workflow/StaleDocumentNotice'; import { StageAdvanceBanner } from './components/workflow/StageAdvanceBanner'; import { deriveReplyTitle } from './features/replyExport/title'; import { copyRichReplyToClipboard, copyMarkdownReply } from './features/replyExport/clipboard'; -import { downloadMarkdownReply, downloadDocxReply } from './features/replyExport/download'; +import { downloadMarkdownReply } from './features/replyExport/download'; import { ProposalConfirmCard } from './components/doc/ProposalConfirmCard'; import { UserProjectRightsModal } from './components/user/UserProjectRightsModal'; import { UserSettingsModal } from './components/user/UserSettingsModal'; -import { AdminConsoleView } from './components/admin/console/AdminConsoleView'; +import { OnDemand } from './components/lazy/OnDemand'; import { DocVisibilityModal } from './components/doc/DocVisibilityModal'; import { CreateProjectModal, type CreateProjectConfirmData } from './components/project/CreateProjectModal'; import { ProjectMembersModal } from './components/project/ProjectMembersModal'; import { ProjectSettingsModal } from './components/project/ProjectSettingsModal'; import { fetchProjectSettings } from './api/projectSettings'; -import { FilePreviewPanel } from './components/doc/preview/FilePreviewPanel'; import { ShieldAlert } from 'lucide-react'; import { LoginView } from './components/auth/LoginView'; import { FirstLoginPasswordModal } from './components/auth/FirstLoginPasswordModal'; @@ -153,8 +151,6 @@ import { hasPermission, filterVisibleDeliverablesWithRBAC, } from './engine/rbac'; -import { exportDocumentToDocx } from './engine/docxRenderer'; -import { exportDocumentToXlsx } from './engine/xlsxRenderer'; import { findMatchingBlockForReview, createRejectConfirmationCard, @@ -225,7 +221,6 @@ import { HistoryCalendar } from './components/history/HistoryCalendar'; import { ChatSearchModal } from './components/history/ChatSearchModal'; import { DigestTimelineModal } from './components/history/DigestTimelineModal'; import { navigateToAnchor, parseAnchorHash, resolveTurnAnchorId } from './engine/anchorNavigator'; -import { ChatExportModal } from './components/history/ChatExportModal'; import { useUnreadMarks } from './hooks/useUnreadMarks'; import { CategoryDock } from './components/chat/CategoryDock'; import { fetchChatCategories, fetchChatDates, mapTaskStatusToMessageStatus } from './api/chatWindow'; @@ -284,6 +279,15 @@ export { MOCK_USERS, } from '@demo-seeds'; +const loadFilePreviewPanel = () => + import('./components/doc/preview/FilePreviewPanel').then((m) => ({ default: m.FilePreviewPanel })); +const loadAdminConsoleView = () => + import('./components/admin/console/AdminConsoleView').then((m) => ({ default: m.AdminConsoleView })); +const loadChatExportModal = () => + import('./components/history/ChatExportModal').then((m) => ({ default: m.ChatExportModal })); +const loadBlockEditorModal = () => + import('./components/doc/BlockEditorModal').then((m) => ({ default: m.BlockEditorModal })); + export default function App() { // Wave 2:启动时幂等剔除 localStorage 中的 demo seed 残留(须先于 load*) purgeDemoSeedsFromLocalStorage(); @@ -3653,15 +3657,16 @@ export default function App() { // 导出 DOCX (Word 渲染器) const handleExportDocx = async () => { try { + const { exportDocumentToDocx } = await import('./engine/docxRenderer'); await exportDocumentToDocx(docAST); } catch (err) { console.error('导出 DOCX 失败:', err); } }; - // 导出单条智能体回复 DOCX (轻量专属 Word 导出) const handleExportReplyDocx = async (message: ChatMessage) => { try { + const { downloadDocxReply } = await import('./features/replyExport/downloadDocx'); await downloadDocxReply(message.content || '', undefined, { createdAt: message.createdAt || message.timestamp, }); @@ -3670,10 +3675,9 @@ export default function App() { } }; - - // 导出 XLSX (Excel 渲染器) const handleExportXlsx = async () => { try { + const { exportDocumentToXlsx } = await import('./engine/xlsxRenderer'); await exportDocumentToXlsx(docAST); } catch (err) { console.error('导出 XLSX 失败:', err); @@ -3996,15 +4000,19 @@ export default function App() { // 全屏系统管理台:与工作台并列的顶层视图,不经过 AppShell if (view === 'admin') { return ( - setView('workbench')} - onUsersChanged={async () => { - const users = await authClient.getAdminUsers(); - setAllUsers(users); + setView('workbench'), + onUsersChanged: async () => { + const users = await authClient.getAdminUsers(); + setAllUsers(users); + }, + onRolesChanged: (newRoles: RoleDefinition[]) => setRoles(newRoles), }} - onRolesChanged={(newRoles) => setRoles(newRoles)} /> ); } @@ -4117,11 +4125,15 @@ export default function App() { /> )} {isExportOpen && ( - setIsExportOpen(false)} - sessionId={activeSessionId} - dayStack={chatWindow.displayDayStack} + setIsExportOpen(false), + sessionId: activeSessionId, + dayStack: chatWindow.displayDayStack, + }} /> )}
@@ -4356,11 +4368,15 @@ export default function App() { ); if (isUploadedFile && activeDeliverable) { return ( - { - setTargetDocForVisibility(activeDeliverable); - setDocVisibilityModalOpen(true); + { + setTargetDocForVisibility(activeDeliverable); + setDocVisibilityModalOpen(true); + }, }} /> ); @@ -4466,11 +4482,15 @@ export default function App() { {/* 块就地编辑弹窗 (支持富文本工具栏、多维表格与未保存拦截) */} {editorSession && ( - )} diff --git a/web/src/components/chat/ReplyActionBar.tsx b/web/src/components/chat/ReplyActionBar.tsx index 497c9024..4e03c23f 100644 --- a/web/src/components/chat/ReplyActionBar.tsx +++ b/web/src/components/chat/ReplyActionBar.tsx @@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { Copy, Check, Bookmark, Download, ChevronDown, FileText, FileCode } from 'lucide-react'; import { ChatMessage } from '../../types/workbench'; import { copyRichReplyToClipboard, copyMarkdownReply } from '../../features/replyExport/clipboard'; -import { downloadMarkdownReply, downloadDocxReply } from '../../features/replyExport/download'; +import { downloadMarkdownReply } from '../../features/replyExport/download'; export interface ReplyActionBarProps { message: ChatMessage; @@ -334,6 +334,7 @@ export const ReplyActionBar: React.FC = ({ if (onExportDocx) { await onExportDocx(message); } else { + const { downloadDocxReply } = await import('../../features/replyExport/downloadDocx'); await downloadDocxReply(message.content || '', undefined, { createdAt: message.createdAt || message.timestamp, }); diff --git a/web/src/components/doc/BlockRenderer.tsx b/web/src/components/doc/BlockRenderer.tsx index 4a55d77f..be35613b 100644 --- a/web/src/components/doc/BlockRenderer.tsx +++ b/web/src/components/doc/BlockRenderer.tsx @@ -7,14 +7,18 @@ import { HeadingBlockView } from './blocks/HeadingBlockView'; import { ParagraphBlockView } from './blocks/ParagraphBlockView'; import { TableBlockView } from './blocks/TableBlockView'; import { CalloutBlockView } from './blocks/CalloutBlockView'; -import { MermaidBlockView } from './blocks/MermaidBlockView'; -import { MathBlockView } from './blocks/MathBlockView'; import { ListBlockView } from './blocks/ListBlockView'; +import { OnDemand } from '../lazy/OnDemand'; import { BlockContextMenu } from './BlockContextMenu'; import { canAddBlockComment } from '../../lib/commentPolicy'; import { documentBlockDomAttrs } from '../../lib/documentBlockDom'; import { Plus, ArrowUp, ArrowDown, Trash2, MessageSquare } from 'lucide-react'; +const loadMermaidBlock = () => + import('./blocks/MermaidBlockView').then((m) => ({ default: m.MermaidBlockView })); +const loadMathBlock = () => + import('./blocks/MathBlockView').then((m) => ({ default: m.MathBlockView })); + export interface BlockRendererProps { ast: DocumentAST; currentRole: UserRole; @@ -75,9 +79,21 @@ export const BlockRenderer: React.FC = ({ case 'callout': return ; case 'mermaid': - return ; + return ( + + ); case 'math': - return ; + return ( + + ); case 'list': return ; case 'divider': diff --git a/web/src/components/doc/preview/FilePreviewPanel.tsx b/web/src/components/doc/preview/FilePreviewPanel.tsx index 71d4b914..17a50dd5 100644 --- a/web/src/components/doc/preview/FilePreviewPanel.tsx +++ b/web/src/components/doc/preview/FilePreviewPanel.tsx @@ -15,12 +15,14 @@ import { ShieldCheck, } from 'lucide-react'; import { DeliverableItem } from '../../../types/workbench'; -import { DocxViewer } from './renderers/DocxViewer'; -import { PdfViewer } from './renderers/PdfViewer'; -import { ExcelViewer } from './renderers/ExcelViewer'; import { ImageViewer } from './renderers/ImageViewer'; +import { PdfViewer } from './renderers/PdfViewer'; import { PptViewer } from './renderers/PptViewer'; import { TextViewer } from './renderers/TextViewer'; +import { OnDemand } from '../../lazy/OnDemand'; + +const loadDocxViewer = () => import('./renderers/DocxViewer').then((m) => ({ default: m.DocxViewer })); +const loadExcelViewer = () => import('./renderers/ExcelViewer').then((m) => ({ default: m.ExcelViewer })); interface FilePreviewPanelProps { deliverable: DeliverableItem; @@ -273,11 +275,23 @@ export const FilePreviewPanel: React.FC = ({ } switch (fileType) { case 'docx': - return ; + return ( + + ); case 'pdf': return ; case 'excel': - return ; + return ( + + ); case 'image': return ; case 'ppt': diff --git a/web/src/components/lazy/OnDemand.tsx b/web/src/components/lazy/OnDemand.tsx new file mode 100644 index 00000000..81b36bf8 --- /dev/null +++ b/web/src/components/lazy/OnDemand.tsx @@ -0,0 +1,52 @@ +import { Component, lazy, Suspense, useMemo, useState, type ComponentType, type ReactNode } from 'react'; + +type BoundaryProps = { + label: string; + onRetry: () => void; + children: ReactNode; +}; + +type BoundaryState = { error: Error | null }; + +/** Catches a failed dynamic import. Retry builds a new lazy() so a rejected chunk is not sticky. */ +export class LazyBoundary extends Component { + state: BoundaryState = { error: null }; + + static getDerivedStateFromError(error: Error): BoundaryState { + return { error }; + } + + render() { + if (this.state.error) { + return ( +
+

+ {this.props.label}加载失败,可重试。这次失败不会提交文档或聊天动作。 +

+ +
+ ); + } + return ( + {this.props.label}加载中…
}> + {this.props.children} + + ); + } +} + +export function OnDemand

(props: { + label: string; + load: () => Promise<{ default: ComponentType

}>; + componentProps: P; +}) { + const [attempt, setAttempt] = useState(0); + const Comp = useMemo(() => lazy(props.load), [attempt, props.load]); + return ( + setAttempt((n) => n + 1)}> + + + ); +} diff --git a/web/src/features/replyExport/download.ts b/web/src/features/replyExport/download.ts index 7f78523f..afaa1442 100644 --- a/web/src/features/replyExport/download.ts +++ b/web/src/features/replyExport/download.ts @@ -1,7 +1,4 @@ -import { Packer } from 'docx'; import { deriveReplyTitle, sanitizeFilename } from './title'; -import { markdownToBlockAST } from '../../engine/markdownToBlock'; -import { buildDocxModel } from '../../engine/docxRenderer'; export interface DownloadOptions { includeSource?: boolean; @@ -42,53 +39,3 @@ export function downloadMarkdownReply( return { filename }; } -/** - * 触发 Word (.docx) 专用轻量排版导出并直接下载 - */ -export async function downloadDocxReply( - markdown: string, - title?: string, - options: DownloadOptions = {} -): Promise<{ filename: string; blob: Blob }> { - const effectiveTitle = title || deriveReplyTitle(markdown, options.createdAt); - const dateStr = new Date().toISOString().substring(0, 10); - const baseName = sanitizeFilename(effectiveTitle); - const filename = options.filenameOverride || `${baseName}_${dateStr}.docx`; - - let exportContent = (markdown || '').trim(); - if (options.includeSource) { - const metaTime = options.createdAt || new Date().toLocaleString(); - exportContent += `\n\n---\n*来源:水木东方项目助手回复 · 生成时间:${metaTime}*`; - } - - const ast = markdownToBlockAST(exportContent, { - docId: `reply-docx-${Date.now()}`, - title: effectiveTitle, - taskKey: 'reply_export', - version: 'V1.0', - category: 'private', - lifecycleState: 'drafting', - isClaimed: true, - author: '智能体助手回复', - reviewer: '', - approver: '', - updatedAt: new Date().toISOString().replace('T', ' ').substring(0, 16), - }); - - const doc = await buildDocxModel(ast); - const blob = await Packer.toBlob(doc); - - if (typeof window !== 'undefined' && typeof document !== 'undefined') { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - setTimeout(() => URL.revokeObjectURL(url), 1000); - } - - return { filename, blob }; -} - diff --git a/web/src/features/replyExport/downloadDocx.ts b/web/src/features/replyExport/downloadDocx.ts new file mode 100644 index 00000000..a83f1efa --- /dev/null +++ b/web/src/features/replyExport/downloadDocx.ts @@ -0,0 +1,53 @@ +import { Packer } from 'docx'; +import { deriveReplyTitle, sanitizeFilename } from './title'; +import { markdownToBlockAST } from '../../engine/markdownToBlock'; +import { buildDocxModel } from '../../engine/docxRenderer'; +import type { DownloadOptions } from './download'; + +/** Word 导出。只在点击时动态引入,避免 docx 进入登录壳。 */ +export async function downloadDocxReply( + markdown: string, + title?: string, + options: DownloadOptions = {} +): Promise<{ filename: string; blob: Blob }> { + const effectiveTitle = title || deriveReplyTitle(markdown, options.createdAt); + const dateStr = new Date().toISOString().substring(0, 10); + const baseName = sanitizeFilename(effectiveTitle); + const filename = options.filenameOverride || `${baseName}_${dateStr}.docx`; + + let exportContent = (markdown || '').trim(); + if (options.includeSource) { + const metaTime = options.createdAt || new Date().toLocaleString(); + exportContent += `\n\n---\n*来源:水木东方项目助手回复 · 生成时间:${metaTime}*`; + } + + const ast = markdownToBlockAST(exportContent, { + docId: `reply-docx-${Date.now()}`, + title: effectiveTitle, + taskKey: 'reply_export', + version: 'V1.0', + category: 'private', + lifecycleState: 'drafting', + isClaimed: true, + author: '智能体助手回复', + reviewer: '', + approver: '', + updatedAt: new Date().toISOString().replace('T', ' ').substring(0, 16), + }); + + const doc = await buildDocxModel(ast); + const blob = await Packer.toBlob(doc); + + if (typeof window !== 'undefined' && typeof document !== 'undefined') { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 1000); + } + + return { filename, blob }; +} diff --git a/web/src/loginShellImports.test.ts b/web/src/loginShellImports.test.ts new file mode 100644 index 00000000..c1d60ca9 --- /dev/null +++ b/web/src/loginShellImports.test.ts @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { test } from 'node:test'; + +function staticImports(rel: string): string[] { + const text = readFileSync(new URL(rel, import.meta.url), 'utf8'); + return text + .split('\n') + .filter((line) => /^\s*import\s/.test(line) && !line.includes('import(')); +} + +test('login shell does not statically import export, preview, admin, or chart engines', () => { + const app = staticImports('./App.tsx').join('\n'); + for (const banned of [ + 'docxRenderer', + 'xlsxRenderer', + 'downloadDocx', + 'FilePreviewPanel', + 'AdminConsoleView', + 'ChatExportModal', + 'BlockEditorModal', + "from 'docx'", + "from 'mermaid'", + "from 'xlsx'", + ]) { + assert.equal(app.includes(banned), false, `App.tsx static import still mentions ${banned}`); + } + const appText = readFileSync(new URL('./App.tsx', import.meta.url), 'utf8'); + assert.match(appText, /import\('\.\/engine\/docxRenderer'\)/); + assert.match(appText, /import\('\.\/engine\/xlsxRenderer'\)/); + assert.match(appText, /import\('\.\/features\/replyExport\/downloadDocx'\)/); + + const blocks = staticImports('./components/doc/BlockRenderer.tsx').join('\n'); + assert.equal(blocks.includes('MermaidBlockView'), false); + assert.equal(blocks.includes('MathBlockView'), false); + + const markdownDownload = readFileSync(new URL('./features/replyExport/download.ts', import.meta.url), 'utf8'); + assert.equal(markdownDownload.includes("from 'docx'"), false); +});