import type { Plugin } from 'vite'; import fs from 'fs'; import path from 'path'; import { spawnSync } from 'child_process'; import archiver from 'archiver'; import { getRequestPathname } from './utils/httpUtils'; import { buildAttachmentContentDisposition } from './utils/contentDisposition'; import { scanProjectEntries, writeEntriesManifestAtomic, readEntriesManifest } from './utils/entriesManifest'; interface ExportEntry { key: string; // e.g. "prototypes/my-page" group: string; // "components" | "prototypes" name: string; // "my-page" displayName: string; // "我的页面" jsPath: string; // relative path to built JS in dist/ } interface PageHtmlOptions { includeBackLink?: boolean; assetPrefix?: string; } /** * 从入口文件中提取 @name 注释作为显示名称 */ function getDisplayName(filePath: string): string | null { try { const content = fs.readFileSync(filePath, 'utf8'); const match = content.match(/@name\s+([^\n]+)/); return match ? match[1].trim() : null; } catch { return null; } } /** * 扫描 dist 目录获取构建产物 */ function scanDistEntries(projectRoot: string): ExportEntry[] { const distDir = path.join(projectRoot, 'dist'); if (!fs.existsSync(distDir)) { return []; } const entries: ExportEntry[] = []; const groups = ['components', 'prototypes']; for (const group of groups) { const groupDir = path.join(distDir, group); if (!fs.existsSync(groupDir)) continue; // 扫描子目录 const dirs = fs.readdirSync(groupDir, { withFileTypes: true }); for (const dir of dirs) { if (!dir.isDirectory()) continue; if (dir.name.startsWith('.') || dir.name.startsWith('ref-')) continue; const jsFile = path.join(groupDir, dir.name + '.js'); const jsFileInDir = path.join(groupDir, dir.name, 'index.js'); // 构建产物格式: dist/prototypes/my-page.js let jsPath: string | null = null; if (fs.existsSync(jsFile)) { jsPath = `${group}/${dir.name}.js`; } if (!jsPath) continue; // 获取显示名称 const srcIndexPath = path.join(projectRoot, 'src', group, dir.name, 'index.tsx'); const displayName = getDisplayName(srcIndexPath) || dir.name; entries.push({ key: `${group}/${dir.name}`, group, name: dir.name, displayName, jsPath, }); } } // 也处理直接位于 dist/ 下的 JS 文件(如 dist/prototypes/xxx.js) for (const group of groups) { const distEntries = fs.readdirSync(distDir, { withFileTypes: true }); // 构建产物可能直接放在 dist/ 下以 group/name.js 的形式 } return entries; } /** * 重新扫描 dist 目录,识别构建好的 JS 入口文件。 * 构建系统产出 dist/{group}/{name}.js 格式的 IIFE bundle。 */ function scanBuiltEntries(projectRoot: string, options: { includeRef?: boolean } = {}): ExportEntry[] { const distDir = path.join(projectRoot, 'dist'); if (!fs.existsSync(distDir)) return []; const entries: ExportEntry[] = []; const includeRef = options.includeRef === true; // 扫描 dist 下所有 .js 文件 const files = fs.readdirSync(distDir, { withFileTypes: true }); for (const file of files) { if (!file.isFile() || !file.name.endsWith('.js')) continue; // 文件名格式: prototypes/my-page.js → key = "prototypes/my-page" // 但构建系统实际上把入口 key 中的 / 变成了文件名的一部分 // 实际文件名: e.g. "prototypes∕my-page.js" — 需要查看实际产物 } // 根据 entries.json 的 key 查找对应的构建产物 const manifest = readEntriesManifest(projectRoot); const jsEntries = manifest.js as Record; const items = manifest.items as Record; for (const [key, item] of Object.entries(items)) { const group = item.group; if (group !== 'components' && group !== 'prototypes') continue; // 项目级导出默认跳过 ref- 前缀的参考组件/页面;单条目导出允许显式包含 if (!includeRef && item.name.startsWith('ref-')) continue; // 构建产物路径: dist/{key}.js (key 中的 / 被 rollup 保留) const builtJsPath = path.join(distDir, `${key}.js`); if (!fs.existsSync(builtJsPath)) continue; const srcIndexPath = path.join(projectRoot, 'src', key, 'index.tsx'); const displayName = getDisplayName(srcIndexPath) || item.name; entries.push({ key, group, name: item.name, displayName, jsPath: `${key}.js`, }); } return entries; } /** * 生成单个页面的查看 HTML */ function generatePageHtml(entry: ExportEntry, options: PageHtmlOptions = {}): string { const { displayName, jsPath, group } = entry; const isComponent = group === 'components'; const includeBackLink = options.includeBackLink !== false; const assetPrefix = options.assetPrefix ?? '../'; return ` ${escapeHtml(displayName)} ${includeBackLink ? '← 返回列表' : ''}