Merge pull request #11 from cloud-neutral-toolkit/codex/fix-missing-modules-in-next.js-16-build
Protect runtime loader and lazy-load mermaid on the client
This commit is contained in:
commit
efeedc6753
@ -7,8 +7,8 @@ import { templates } from '../../../config/wechat-templates'
|
||||
import { useState, useRef, useEffect, useMemo } from 'react'
|
||||
import { type CodeThemeId } from '../../../config/code-themes'
|
||||
import { useTheme } from 'next-themes'
|
||||
import mermaid from 'mermaid'
|
||||
import { useScrollSync } from '../hooks/useScrollSync'
|
||||
import { loadMermaid } from '../../../lib/markdown/mermaid-utils'
|
||||
|
||||
interface EditorPreviewProps {
|
||||
previewRef: React.RefObject<HTMLDivElement>
|
||||
@ -37,41 +37,57 @@ export function EditorPreview({
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const { handlePreviewScroll } = useScrollSync()
|
||||
const { theme } = useTheme()
|
||||
const mermaidRef = useRef<Awaited<ReturnType<typeof loadMermaid>>>()
|
||||
|
||||
// 初始化 Mermaid
|
||||
useEffect(() => {
|
||||
mermaid.initialize({
|
||||
theme: theme === 'dark' ? 'dark' : 'default',
|
||||
startOnLoad: false,
|
||||
securityLevel: 'loose',
|
||||
fontFamily: 'var(--font-sans)',
|
||||
fontSize: 14,
|
||||
flowchart: {
|
||||
htmlLabels: true,
|
||||
curve: 'basis',
|
||||
padding: 15,
|
||||
useMaxWidth: false,
|
||||
defaultRenderer: 'dagre-d3'
|
||||
},
|
||||
sequence: {
|
||||
useMaxWidth: false,
|
||||
boxMargin: 10,
|
||||
mirrorActors: false,
|
||||
bottomMarginAdj: 2,
|
||||
rightAngles: true,
|
||||
showSequenceNumbers: false
|
||||
},
|
||||
pie: {
|
||||
useMaxWidth: true,
|
||||
textPosition: 0.5,
|
||||
useWidth: 800
|
||||
},
|
||||
gantt: {
|
||||
useMaxWidth: false,
|
||||
leftPadding: 75,
|
||||
rightPadding: 20
|
||||
}
|
||||
})
|
||||
let cancelled = false
|
||||
|
||||
const initializeMermaidTheme = async () => {
|
||||
const mermaid = await loadMermaid()
|
||||
if (!mermaid || cancelled) return
|
||||
|
||||
mermaid.initialize({
|
||||
theme: theme === 'dark' ? 'dark' : 'default',
|
||||
startOnLoad: false,
|
||||
securityLevel: 'loose',
|
||||
fontFamily: 'var(--font-sans)',
|
||||
fontSize: 14,
|
||||
flowchart: {
|
||||
htmlLabels: true,
|
||||
curve: 'basis',
|
||||
padding: 15,
|
||||
useMaxWidth: false,
|
||||
defaultRenderer: 'dagre-d3'
|
||||
},
|
||||
sequence: {
|
||||
useMaxWidth: false,
|
||||
boxMargin: 10,
|
||||
mirrorActors: false,
|
||||
bottomMarginAdj: 2,
|
||||
rightAngles: true,
|
||||
showSequenceNumbers: false
|
||||
},
|
||||
pie: {
|
||||
useMaxWidth: true,
|
||||
textPosition: 0.5,
|
||||
useWidth: 800
|
||||
},
|
||||
gantt: {
|
||||
useMaxWidth: false,
|
||||
leftPadding: 75,
|
||||
rightPadding: 20
|
||||
}
|
||||
})
|
||||
|
||||
mermaidRef.current = mermaid
|
||||
}
|
||||
|
||||
initializeMermaidTheme()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
// 使用 memo 包装预览内容
|
||||
@ -93,6 +109,9 @@ export function EditorPreview({
|
||||
// 渲染 Mermaid 图表
|
||||
useEffect(() => {
|
||||
const renderMermaid = async () => {
|
||||
const mermaid = mermaidRef.current ?? (await loadMermaid())
|
||||
if (!mermaid) return
|
||||
|
||||
try {
|
||||
const elements = document.querySelectorAll('.mermaid')
|
||||
if (!elements.length) return
|
||||
|
||||
@ -22,7 +22,7 @@ export function MermaidRenderer({
|
||||
try {
|
||||
// 初始化 mermaid
|
||||
const currentTheme = getCurrentTheme()
|
||||
initializeMermaid(currentTheme, config)
|
||||
await initializeMermaid(currentTheme, config)
|
||||
|
||||
// 清空容器内容
|
||||
containerRef.current.innerHTML = ''
|
||||
|
||||
@ -1,32 +1,35 @@
|
||||
import mermaid from 'mermaid'
|
||||
import type { MermaidConfig } from 'mermaid'
|
||||
import { loadMermaid } from './mermaid-utils'
|
||||
|
||||
let initialized = false
|
||||
|
||||
// Initialize mermaid with default configuration
|
||||
const config: MermaidConfig = {
|
||||
startOnLoad: false,
|
||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'default',
|
||||
securityLevel: 'loose' as const,
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Microsoft YaHei", sans-serif',
|
||||
themeVariables: {
|
||||
'fontSize': '16px',
|
||||
'fontFamily': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Microsoft YaHei", sans-serif',
|
||||
'primaryColor': document.documentElement.classList.contains('dark') ? '#7c3aed' : '#4f46e5',
|
||||
'primaryTextColor': document.documentElement.classList.contains('dark') ? '#fff' : '#000',
|
||||
'primaryBorderColor': document.documentElement.classList.contains('dark') ? '#7c3aed' : '#4f46e5',
|
||||
'lineColor': document.documentElement.classList.contains('dark') ? '#666' : '#999',
|
||||
'textColor': document.documentElement.classList.contains('dark') ? '#fff' : '#333'
|
||||
},
|
||||
pie: {
|
||||
textPosition: 0.75,
|
||||
useMaxWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存已渲染的图表
|
||||
const renderedDiagrams = new Map<string, string>()
|
||||
|
||||
const createMermaidConfig = (): MermaidConfig => {
|
||||
return {
|
||||
startOnLoad: false,
|
||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'default',
|
||||
securityLevel: 'loose' as const,
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Microsoft YaHei", sans-serif',
|
||||
themeVariables: {
|
||||
fontSize: '16px',
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Microsoft YaHei", sans-serif',
|
||||
primaryColor: document.documentElement.classList.contains('dark') ? '#7c3aed' : '#4f46e5',
|
||||
primaryTextColor: document.documentElement.classList.contains('dark') ? '#fff' : '#000',
|
||||
primaryBorderColor: document.documentElement.classList.contains('dark') ? '#7c3aed' : '#4f46e5',
|
||||
lineColor: document.documentElement.classList.contains('dark') ? '#666' : '#999',
|
||||
textColor: document.documentElement.classList.contains('dark') ? '#fff' : '#333',
|
||||
},
|
||||
pie: {
|
||||
textPosition: 0.75,
|
||||
useMaxWidth: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a valid ID for mermaid diagrams
|
||||
function generateMermaidId() {
|
||||
return `mermaid-${Math.floor(Math.random() * 100000)}`
|
||||
@ -70,9 +73,16 @@ function processPieChart(definition: string): string {
|
||||
// Function to initialize Mermaid diagrams
|
||||
export async function initMermaid() {
|
||||
try {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const mermaid = await loadMermaid()
|
||||
if (!mermaid) return
|
||||
|
||||
// 确保只初始化一次
|
||||
if (!initialized) {
|
||||
mermaid.initialize(config)
|
||||
mermaid.initialize(createMermaidConfig())
|
||||
initialized = true
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import mermaid from 'mermaid'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
mermaid: {
|
||||
@ -43,10 +41,35 @@ export const MERMAID_CONFIG = {
|
||||
securityLevel: 'loose' as const
|
||||
}
|
||||
|
||||
type MermaidModule = typeof import('mermaid')
|
||||
|
||||
let mermaidLoader: Promise<MermaidModule> | null = null
|
||||
|
||||
export const loadMermaid = async (): Promise<MermaidModule | null> => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!mermaidLoader) {
|
||||
mermaidLoader = import('mermaid').then((module) => {
|
||||
const mermaidModule = module.default ?? (module as unknown as MermaidModule)
|
||||
window.mermaid = mermaidModule as unknown as Window['mermaid']
|
||||
return mermaidModule
|
||||
})
|
||||
}
|
||||
|
||||
return mermaidLoader
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 Mermaid
|
||||
*/
|
||||
export const initializeMermaid = async () => {
|
||||
const mermaid = await loadMermaid()
|
||||
if (!mermaid || typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 初始化配置
|
||||
mermaid.initialize(MERMAID_CONFIG)
|
||||
|
||||
@ -1,17 +1,29 @@
|
||||
import mermaid from 'mermaid'
|
||||
import type { MermaidConfig } from 'mermaid'
|
||||
import type { MermaidError, MermaidTheme } from '../types/mermaid'
|
||||
import { createMermaidConfig } from '../config/mermaid'
|
||||
import { loadMermaid } from '../mermaid-utils'
|
||||
|
||||
const getMermaid = async () => {
|
||||
const mermaid = await loadMermaid()
|
||||
return mermaid ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 Mermaid
|
||||
*/
|
||||
export const initializeMermaid = (theme: MermaidTheme = 'default', config?: Partial<MermaidConfig>) => {
|
||||
const defaultConfig = createMermaidConfig(theme)
|
||||
mermaid.initialize({
|
||||
...defaultConfig,
|
||||
...config
|
||||
})
|
||||
export const initializeMermaid = async (theme: MermaidTheme = 'default', config?: Partial<MermaidConfig>) => {
|
||||
try {
|
||||
const mermaid = await getMermaid()
|
||||
if (!mermaid) return
|
||||
|
||||
const defaultConfig = createMermaidConfig(theme)
|
||||
mermaid.initialize({
|
||||
...defaultConfig,
|
||||
...config
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize mermaid runtime:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -22,6 +34,11 @@ export const renderMermaidDiagram = async (
|
||||
config?: Partial<MermaidConfig>
|
||||
): Promise<{ svg: string }> => {
|
||||
try {
|
||||
const mermaid = await getMermaid()
|
||||
if (!mermaid) {
|
||||
throw new Error('Mermaid is only available in the browser environment.')
|
||||
}
|
||||
|
||||
// 先尝试解析,检查语法错误
|
||||
await mermaid.parse(content)
|
||||
|
||||
@ -45,6 +62,9 @@ export const renderMermaidDiagram = async (
|
||||
*/
|
||||
export const renderMermaidDiagrams = async (selector = '.mermaid') => {
|
||||
try {
|
||||
const mermaid = await getMermaid()
|
||||
if (!mermaid) return
|
||||
|
||||
const elements = document.querySelectorAll<HTMLElement>(selector)
|
||||
if (!elements.length) return
|
||||
|
||||
|
||||
@ -8,6 +8,33 @@ import yaml from 'js-yaml'
|
||||
// 使用 process.cwd() 获取项目根目录,避免 __dirname 在生产环境的问题
|
||||
const configDir = path.join(process.cwd(), 'src', 'config')
|
||||
|
||||
const FORBIDDEN_IMPORT_CONTEXTS = [
|
||||
`${path.sep}src${path.sep}modules${path.sep}markdown-editor${path.sep}`,
|
||||
`${path.sep}src${path.sep}components${path.sep}ui${path.sep}`,
|
||||
'tiptap',
|
||||
'mermaid',
|
||||
'next-themes',
|
||||
]
|
||||
|
||||
function assertServerOnlyContext() {
|
||||
if (typeof window !== 'undefined') {
|
||||
throw new Error('runtime-loader.ts is server-only and cannot run in the browser.')
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const stack = new Error().stack ?? ''
|
||||
const forbiddenCaller = FORBIDDEN_IMPORT_CONTEXTS.find((pattern) => stack.includes(pattern))
|
||||
|
||||
if (forbiddenCaller) {
|
||||
throw new Error(
|
||||
`[runtime-config] runtime-loader.ts must not be imported alongside UI/editor runtimes (${forbiddenCaller}).`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertServerOnlyContext()
|
||||
|
||||
function loadYamlSource(sourceKey: RuntimeSourceKey): string | undefined {
|
||||
try {
|
||||
const filePath = path.join(configDir, `runtime-service-config.${sourceKey}.yaml`)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user