From 009e0d0877f92fb222b4d07731ebcdf9849540c9 Mon Sep 17 00:00:00 2001 From: suyao Date: Fri, 12 Sep 2025 07:18:03 +0800 Subject: [PATCH] feat: add PDF export functionality and related enhancements --- electron.vite.config.ts | 18 ++++ package.json | 1 + packages/shared/IpcChannel.ts | 1 + packages/shared/utils.ts | 1 - scripts/after-pack.js | 13 +++ src/main/constant.ts | 33 +++++++ src/main/ipc.ts | 6 ++ src/main/services/ExportService.ts | 99 ++++++++++++++++++- src/preload/index.ts | 3 +- src/renderer/src/i18n/locales/en-us.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 1 + src/renderer/src/i18n/locales/zh-tw.json | 1 + src/renderer/src/pages/notes/HeaderNavbar.tsx | 15 ++- src/renderer/src/pages/notes/MenuConfig.tsx | 47 ++++++++- src/renderer/src/pages/notes/NotesPage.tsx | 2 + src/renderer/src/pages/notes/NotesSidebar.tsx | 22 ++++- yarn.lock | 92 +++++++++++++++-- 17 files changed, 338 insertions(+), 18 deletions(-) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 83f82b5f4c..0b2087df5f 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -3,6 +3,8 @@ import { CodeInspectorPlugin } from 'code-inspector-plugin' import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import { resolve } from 'path' import { visualizer } from 'rollup-plugin-visualizer' +import { normalizePath } from 'vite' +import { viteStaticCopy } from 'vite-plugin-static-copy' import pkg from './package.json' assert { type: 'json' } @@ -75,6 +77,22 @@ export default defineConfig({ ] ] }), + viteStaticCopy({ + targets: [ + { + src: normalizePath(resolve(__dirname, 'src/renderer/src/assets/styles/color.css')), + dest: normalizePath(resolve(__dirname, 'resources/styles')) + }, + { + src: normalizePath(resolve(__dirname, 'src/renderer/src/assets/styles/font.css')), + dest: normalizePath(resolve(__dirname, 'resources/styles')) + }, + { + src: normalizePath(resolve(__dirname, 'src/renderer/src/assets/styles/richtext.css')), + dest: normalizePath(resolve(__dirname, 'resources/styles')) + } + ] + }), ...(isDev ? [CodeInspectorPlugin({ bundler: 'vite' })] : []), // 只在开发环境下启用 CodeInspectorPlugin ...visualizerPlugin('renderer') ], diff --git a/package.json b/package.json index 153be63374..674048e2cb 100644 --- a/package.json +++ b/package.json @@ -335,6 +335,7 @@ "unified": "^11.0.5", "uuid": "^10.0.0", "vite": "npm:rolldown-vite@latest", + "vite-plugin-static-copy": "^3.1.2", "vitest": "^3.2.4", "webdav": "^5.8.0", "winston": "^3.17.0", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index fa02991682..0bf971b8fc 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -189,6 +189,7 @@ export enum IpcChannel { FileService_Retrieve = 'file-service:retrieve', Export_Word = 'export:word', + Export_PDF = 'export:pdf', Shortcuts_Update = 'shortcuts:update', diff --git a/packages/shared/utils.ts b/packages/shared/utils.ts index 4101a65c06..e87e2f2bef 100644 --- a/packages/shared/utils.ts +++ b/packages/shared/utils.ts @@ -1,7 +1,6 @@ export const defaultAppHeaders = () => { return { 'HTTP-Referer': 'https://cherry-ai.com', - Referer: 'https://cherry-ai.com', 'X-Title': 'Cherry Studio' } } diff --git a/scripts/after-pack.js b/scripts/after-pack.js index b8e4c8af1d..2a8a0c0ba9 100644 --- a/scripts/after-pack.js +++ b/scripts/after-pack.js @@ -7,4 +7,17 @@ exports.default = async function (context) { fs.rmSync(path.join(context.appOutDir, 'LICENSE.electron.txt'), { force: true }) fs.rmSync(path.join(context.appOutDir, 'LICENSES.chromium.html'), { force: true }) } + + // Clean up resources/styles directory from project root after packing + const projectRoot = path.resolve(__dirname, '..') + const resourcesStylesPath = path.join(projectRoot, 'resources', 'styles') + + try { + if (fs.existsSync(resourcesStylesPath)) { + fs.rmSync(resourcesStylesPath, { recursive: true, force: true }) + console.log('✓ Cleaned up resources/styles directory from project root') + } + } catch (error) { + console.warn('Warning: Could not clean up resources/styles directory:', error.message) + } } diff --git a/src/main/constant.ts b/src/main/constant.ts index 4ec56765b7..d1a9e9b67c 100644 --- a/src/main/constant.ts +++ b/src/main/constant.ts @@ -3,3 +3,36 @@ export const isWin = process.platform === 'win32' export const isLinux = process.platform === 'linux' export const isDev = process.env.NODE_ENV === 'development' export const isPortable = isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env + +export const PRINT_HTML_TEMPLATE = ` + + + + + {{filename}} + + + +
+
{{content}}
+
+ + + ` diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 6b36a96a35..9a16dbea45 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -89,6 +89,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { // Initialize Python service with main window pythonService.setMainWindow(mainWindow) + // Initialize export service with main window + exportService.setMainWindow(mainWindow) + const checkMainWindow = () => { if (!mainWindow || mainWindow.isDestroyed()) { throw new Error('Main window does not exist or has been destroyed') @@ -512,6 +515,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { // export ipcMain.handle(IpcChannel.Export_Word, exportService.exportToWord.bind(exportService)) + // PDF export + ipcMain.handle(IpcChannel.Export_PDF, exportService.exportToPDF.bind(exportService)) + // open path ipcMain.handle(IpcChannel.Open_Path, async (_, path: string) => { await shell.openPath(path) diff --git a/src/main/services/ExportService.ts b/src/main/services/ExportService.ts index 8aa10cb466..5b3f6d8554 100644 --- a/src/main/services/ExportService.ts +++ b/src/main/services/ExportService.ts @@ -1,7 +1,12 @@ /* eslint-disable no-case-declarations */ // ExportService +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + import { loggerService } from '@logger' +import { PRINT_HTML_TEMPLATE } from '@main/constant' import { AlignmentType, BorderStyle, @@ -18,7 +23,7 @@ import { VerticalAlign, WidthType } from 'docx' -import { dialog } from 'electron' +import { app, BrowserWindow, dialog } from 'electron' import MarkdownIt from 'markdown-it' import { fileStorage } from './FileStorage' @@ -26,11 +31,16 @@ import { fileStorage } from './FileStorage' const logger = loggerService.withContext('ExportService') export class ExportService { private md: MarkdownIt + private mainWindow: BrowserWindow | null = null constructor() { this.md = new MarkdownIt() } + public setMainWindow(window: BrowserWindow) { + this.mainWindow = window + } + private convertMarkdownToDocxElements(markdown: string) { const tokens = this.md.parse(markdown, {}) const elements: any[] = [] @@ -405,4 +415,91 @@ export class ExportService { throw error } } + + public exportToPDF = async (_: Electron.IpcMainInvokeEvent, content: string, filename: string): Promise => { + if (!this.mainWindow) { + throw new Error('Main window not set') + } + + try { + const loadCssFile = async (filename: string): Promise => { + try { + let cssPath: string + if (app.isPackaged) { + cssPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'resources', 'styles', filename) + } else { + cssPath = path.join(app.getAppPath(), 'src', 'renderer', 'src', 'assets', 'styles', filename) + } + return await fs.promises.readFile(cssPath, 'utf-8') + } catch (error) { + logger.warn(`Could not load ${filename}, using fallback:`, error as Error) + return '' + } + } + + const colorCss = await loadCssFile('color.css') + const fontCss = await loadCssFile('font.css') + const richtextCss = await loadCssFile('richtext.css') + + const tempHtmlPath = path.join(os.tmpdir(), `temp_${Date.now()}.html`) + await fs.promises.writeFile( + tempHtmlPath, + PRINT_HTML_TEMPLATE.replace('{{filename}}', filename.replace('.pdf', '')) + .replace('{{colorCss}}', colorCss) + .replace('{{richtextCss}}', richtextCss) + .replace('{{fontCss}}', fontCss) + .replace('{{content}}', content) + ) + + const printWindow = new BrowserWindow({ + width: 800, + height: 600, + show: false, + webPreferences: { + nodeIntegration: false, + contextIsolation: true + } + }) + + await printWindow.loadFile(tempHtmlPath) + + // Show save dialog for PDF + const result = await dialog.showSaveDialog(this.mainWindow, { + defaultPath: filename, + filters: [{ name: 'PDF Files', extensions: ['pdf'] }] + }) + + if (result.canceled || !result.filePath) { + printWindow.close() + await fs.promises.unlink(tempHtmlPath) + return { success: false, message: 'Export cancelled' } + } + + // Generate PDF using printToPDF for vector output + const pdfData = await printWindow.webContents.printToPDF({ + margins: { + top: 0.5, + bottom: 0.5, + left: 0.5, + right: 0.5 + }, + pageSize: 'A4', + printBackground: true, + scale: 1.0, + preferCSSPageSize: false, + landscape: false + }) + + await fs.promises.writeFile(result.filePath, pdfData) + + // Clean up + printWindow.close() + await fs.promises.unlink(tempHtmlPath) + + return { success: true, filePath: result.filePath } + } catch (error: any) { + logger.error('Failed to export PDF:', error) + return { success: false, error: error.message } + } + } } diff --git a/src/preload/index.ts b/src/preload/index.ts index d8c55dd256..257456308b 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -203,7 +203,8 @@ const api = { readText: (pathOrUrl: string): Promise => ipcRenderer.invoke(IpcChannel.Fs_ReadText, pathOrUrl) }, export: { - toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName) + toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName), + toPDF: (content: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_PDF, content, fileName) }, obsidian: { getVaults: () => ipcRenderer.invoke(IpcChannel.Obsidian_GetVaults), diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index b4dc172bd2..9fbe04c4a2 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1711,6 +1711,7 @@ "drop_markdown_hint": "Drop markdown files or folders here to import", "empty": "No notes available yet", "expand": "unfold", + "exportPDF": "Export to PDF", "export_failed": "Failed to export to knowledge base", "export_knowledge": "Export notes to knowledge base", "export_success": "Successfully exported to the knowledge base", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 5b8cae2695..8ec7bcf361 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1712,6 +1712,7 @@ "drop_markdown_hint": "拖拽 Markdown 文件或目录到此处导入", "empty": "暂无笔记", "expand": "展开", + "exportPDF": "导出为PDF", "export_failed": "导出到知识库失败", "export_knowledge": "导出笔记到知识库", "export_success": "成功导出到知识库", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 40639664d2..2a7db994b8 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1711,6 +1711,7 @@ "drop_markdown_hint": "拖拽 Markdown 文件或資料夾到此處導入", "empty": "暫無筆記", "expand": "展開", + "exportPDF": "匯出為PDF", "export_failed": "匯出至知識庫失敗", "export_knowledge": "匯出筆記至知識庫", "export_success": "成功匯出至知識庫", diff --git a/src/renderer/src/pages/notes/HeaderNavbar.tsx b/src/renderer/src/pages/notes/HeaderNavbar.tsx index 5f386f4b38..45b015d15b 100644 --- a/src/renderer/src/pages/notes/HeaderNavbar.tsx +++ b/src/renderer/src/pages/notes/HeaderNavbar.tsx @@ -11,11 +11,11 @@ import { MoreHorizontal, PanelLeftClose, PanelRightClose, Star } from 'lucide-re import { useCallback, useEffect, useState } from 'react' import styled from 'styled-components' -import { menuItems } from './MenuConfig' +import { handleExportPDF, MenuContext, menuItems } from './MenuConfig' const logger = loggerService.withContext('HeaderNavbar') -const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar }) => { +const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, editorRef, currentContent }) => { const { showWorkspace, toggleShowWorkspace } = useShowWorkspace() const { activeNode } = useActiveNode(notesTree) const [breadcrumbItems, setBreadcrumbItems] = useState['items']>([]) @@ -47,6 +47,15 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar }) => { } }, [getCurrentNoteContent]) + const handleExportPDFAction = useCallback(async () => { + const menuContext: MenuContext = { + editorRef, + currentContent, + fileName: activeNode?.name || t('notes.title') + } + await handleExportPDF(menuContext) + }, [editorRef, currentContent, activeNode]) + const buildMenuItem = (item: any) => { if (item.type === 'divider') { return { type: 'divider' as const, key: item.key } @@ -88,6 +97,8 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar }) => { onClick: () => { if (item.copyAction) { handleCopyContent() + } else if (item.exportPdfAction) { + handleExportPDFAction() } else if (item.action) { item.action(settings, updateSettings) } diff --git a/src/renderer/src/pages/notes/MenuConfig.tsx b/src/renderer/src/pages/notes/MenuConfig.tsx index 2e637ab781..bb564c09aa 100644 --- a/src/renderer/src/pages/notes/MenuConfig.tsx +++ b/src/renderer/src/pages/notes/MenuConfig.tsx @@ -1,17 +1,52 @@ +import { loggerService } from '@logger' +import { RichEditorRef } from '@renderer/components/RichEditor/types' import { NotesSettings } from '@renderer/store/note' -import { Copy, MonitorSpeaker, Type } from 'lucide-react' -import { ReactNode } from 'react' +import { Copy, Download, MonitorSpeaker, Type } from 'lucide-react' +import { ReactNode, RefObject } from 'react' + +const logger = loggerService.withContext('MenuConfig') +export interface MenuContext { + editorRef: RefObject + currentContent: string + fileName: string +} export interface MenuItem { key: string type?: 'divider' | 'component' labelKey: string icon?: React.ComponentType - action?: (settings: NotesSettings, updateSettings: (newSettings: Partial) => void) => void + action?: ( + settings: NotesSettings, + updateSettings: (newSettings: Partial) => void, + context?: MenuContext + ) => void children?: MenuItem[] isActive?: (settings: NotesSettings) => boolean component?: (settings: NotesSettings, updateSettings: (newSettings: Partial) => void) => ReactNode copyAction?: boolean + exportPdfAction?: boolean +} + +export const handleExportPDF = async (context: MenuContext) => { + if (!context.editorRef?.current || !context.currentContent?.trim()) { + return + } + + try { + // Use Tiptap's getHTML API to get HTML content + const htmlContent = context.editorRef.current.getHtml() + const filename = context.fileName ? `${context.fileName}.pdf` : 'note.pdf' + const result = await window.api.export.toPDF(htmlContent, filename) + + if (result.success) { + logger.info('PDF exported successfully to:', result.filePath) + } else { + logger.error('PDF export failed:', result.message) + } + } catch (error: any) { + logger.error('PDF export error:', error) + } } export const menuItems: MenuItem[] = [ @@ -21,6 +56,12 @@ export const menuItems: MenuItem[] = [ icon: Copy, copyAction: true }, + { + key: 'export-pdf', + labelKey: 'notes.exportPDF', + icon: Download, + exportPdfAction: true + }, { key: 'divider0', type: 'divider', diff --git a/src/renderer/src/pages/notes/NotesPage.tsx b/src/renderer/src/pages/notes/NotesPage.tsx index c85793e781..b3a3d564db 100644 --- a/src/renderer/src/pages/notes/NotesPage.tsx +++ b/src/renderer/src/pages/notes/NotesPage.tsx @@ -631,6 +631,8 @@ const NotesPage: FC = () => { notesTree={notesTree} getCurrentNoteContent={getCurrentNoteContent} onToggleStar={handleToggleStar} + editorRef={editorRef} + currentContent={currentContent} /> void onSortNodes: (sortType: NotesSortType) => void onUploadFiles: (files: File[]) => void + onExportToPDF?: (nodeId: string) => void notesTree: NotesTreeNode[] selectedFolderId?: string | null } @@ -54,6 +56,7 @@ const NotesSidebar: FC = ({ onMoveNode, onSortNodes, onUploadFiles, + onExportToPDF, notesTree, selectedFolderId }) => { @@ -184,6 +187,15 @@ const NotesSidebar: FC = ({ [bases.length, t] ) + const handleExportToPDF = useCallback( + (note: NotesTreeNode) => { + if (onExportToPDF && note.type === 'file') { + onExportToPDF(note.id) + } + }, + [onExportToPDF] + ) + const handleDragStart = useCallback((e: React.DragEvent, node: NotesTreeNode) => { setDraggedNodeId(node.id) e.dataTransfer.effectAllowed = 'move' @@ -330,6 +342,14 @@ const NotesSidebar: FC = ({ onClick: () => { handleExportKnowledge(node) } + }, + { + label: t('notes.exportPDF'), + key: 'export_pdf', + icon: , + onClick: () => { + handleExportToPDF(node) + } } ) } @@ -348,7 +368,7 @@ const NotesSidebar: FC = ({ return baseMenuItems }, - [t, handleStartEdit, onToggleStar, handleExportKnowledge, handleDeleteNode] + [t, handleStartEdit, onToggleStar, handleExportKnowledge, handleExportToPDF, handleDeleteNode] ) const renderTreeNode = useCallback( diff --git a/yarn.lock b/yarn.lock index eb0cde8a17..444800c7b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13278,6 +13278,7 @@ __metadata: unified: "npm:^11.0.5" uuid: "npm:^10.0.0" vite: "npm:rolldown-vite@latest" + vite-plugin-static-copy: "npm:^3.1.2" vitest: "npm:^3.2.4" webdav: "npm:^5.8.0" winston: "npm:^3.17.0" @@ -13642,6 +13643,16 @@ __metadata: languageName: node linkType: hard +"anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac + languageName: node + linkType: hard + "app-builder-bin@npm:5.0.0-alpha.12": version: 5.0.0-alpha.12 resolution: "app-builder-bin@npm:5.0.0-alpha.12" @@ -14041,7 +14052,7 @@ __metadata: languageName: node linkType: hard -"binary-extensions@npm:^2.2.0": +"binary-extensions@npm:^2.0.0, binary-extensions@npm:^2.2.0": version: 2.3.0 resolution: "binary-extensions@npm:2.3.0" checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 @@ -14163,7 +14174,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.3": +"braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -14685,6 +14696,25 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^3.6.0": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462 + languageName: node + linkType: hard + "chokidar@npm:^4.0.3": version: 4.0.3 resolution: "chokidar@npm:4.0.3" @@ -18182,6 +18212,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.3.0": + version: 11.3.1 + resolution: "fs-extra@npm:11.3.1" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10c0/61e5b7285b1ca72c68dfe1058b2514294a922683afac2a80aa90540f9bd85370763d675e3b408ef500077d355956fece3bd24b546790e261c3d3015967e2b2d9 + languageName: node + linkType: hard + "fs-extra@npm:^8.1.0": version: 8.1.0 resolution: "fs-extra@npm:8.1.0" @@ -18240,7 +18281,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:~2.3.3": +"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -18259,7 +18300,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": +"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -18424,7 +18465,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.2": +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -19382,6 +19423,15 @@ __metadata: languageName: node linkType: hard +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 + languageName: node + linkType: hard + "is-buffer@npm:^2.0.0": version: 2.0.5 resolution: "is-buffer@npm:2.0.5" @@ -19469,7 +19519,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -22656,7 +22706,7 @@ __metadata: languageName: node linkType: hard -"normalize-path@npm:^3.0.0": +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 @@ -23072,7 +23122,7 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^7.0.2": +"p-map@npm:^7.0.2, p-map@npm:^7.0.3": version: 7.0.3 resolution: "p-map@npm:7.0.3" checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c @@ -23439,7 +23489,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.3.1": +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be @@ -25152,6 +25202,15 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b + languageName: node + linkType: hard + "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -28287,6 +28346,21 @@ __metadata: languageName: node linkType: hard +"vite-plugin-static-copy@npm:^3.1.2": + version: 3.1.2 + resolution: "vite-plugin-static-copy@npm:3.1.2" + dependencies: + chokidar: "npm:^3.6.0" + fs-extra: "npm:^11.3.0" + p-map: "npm:^7.0.3" + picocolors: "npm:^1.1.1" + tinyglobby: "npm:^0.2.14" + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10c0/1a65f4c9d291cc27483a5b225b1ac5610edc3aa2f13fa3a76a77327874c83bbee52e1011ee0bf5b0168b9b7b974213d49fe800e44af398cfbcb6607814b45c5b + languageName: node + linkType: hard + "vite@npm:rolldown-vite@latest": version: 7.1.5 resolution: "rolldown-vite@npm:7.1.5"