From ffddaa69ba7c716f44834d3d1e55137851448306 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Tue, 20 Jan 2026 22:47:43 +0800 Subject: [PATCH] fix(build): include remote miniapps in ecosystem.json during build generateEcosystemDataForBuild was only scanning local miniapps, ignoring remote miniapps downloaded by vite-plugin-remote-miniapps. Added scanRemoteMiniappsForBuild to include remote miniapps (directories with manifest.json but no vite.config.ts) in ecosystem.json. --- scripts/vite-plugin-miniapps.ts | 334 +++++++++++++++++--------------- 1 file changed, 180 insertions(+), 154 deletions(-) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index 77c65a900..168dec972 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -6,106 +6,106 @@ * - Build 模式:构建所有 miniapps 到 dist/miniapps/,生成 ecosystem.json */ -import { createServer, build as viteBuild, type Plugin, type ViteDevServer } from 'vite' -import { resolve, join } from 'node:path' -import { readdirSync, existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'node:fs' -import detectPort from 'detect-port' -import https from 'node:https' -import { getRemoteMiniappsForEcosystem } from './vite-plugin-remote-miniapps' +import { createServer, build as viteBuild, type Plugin, type ViteDevServer } from 'vite'; +import { resolve, join } from 'node:path'; +import { readdirSync, existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'node:fs'; +import detectPort from 'detect-port'; +import https from 'node:https'; +import { getRemoteMiniappsForEcosystem } from './vite-plugin-remote-miniapps'; // ==================== Types ==================== interface MiniappManifest { - id: string - dirName: string - name: string - description: string - longDescription?: string - icon: string - version: string - author: string - website?: string - category: 'tools' | 'exchange' | 'social' | 'games' | 'other' - tags: string[] - permissions: string[] - chains: string[] - screenshots: string[] - publishedAt: string - updatedAt: string - beta: boolean - themeColor: string - officialScore?: number - communityScore?: number + id: string; + dirName: string; + name: string; + description: string; + longDescription?: string; + icon: string; + version: string; + author: string; + website?: string; + category: 'tools' | 'exchange' | 'social' | 'games' | 'other'; + tags: string[]; + permissions: string[]; + chains: string[]; + screenshots: string[]; + publishedAt: string; + updatedAt: string; + beta: boolean; + themeColor: string; + officialScore?: number; + communityScore?: number; } interface EcosystemJson { - name: string - version: string - updated: string - icon: string - apps: Array + name: string; + version: string; + updated: string; + icon: string; + apps: Array; } interface MiniappServer { - id: string - dirName: string - port: number - server: ViteDevServer - baseUrl: string + id: string; + dirName: string; + port: number; + server: ViteDevServer; + baseUrl: string; } interface MiniappsPluginOptions { - miniappsDir?: string + miniappsDir?: string; } // ==================== Plugin ==================== export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { - const { miniappsDir = 'miniapps' } = options + const { miniappsDir = 'miniapps' } = options; - let root: string - let isBuild = false - const miniappServers: MiniappServer[] = [] + let root: string; + let isBuild = false; + const miniappServers: MiniappServer[] = []; return { name: 'vite-plugin-miniapps', configResolved(config) { - root = config.root - isBuild = config.command === 'build' + root = config.root; + isBuild = config.command === 'build'; }, async writeBundle(options) { if (isBuild && options.dir) { // 构建完成后构建 miniapps - await buildAllMiniapps(root, miniappsDir, options.dir) + await buildAllMiniapps(root, miniappsDir, options.dir); // 生成 ecosystem.json 到 miniapps/ 目录 - const ecosystem = generateEcosystemDataForBuild(root, miniappsDir) - const miniappsOutputDir = resolve(options.dir, 'miniapps') - mkdirSync(miniappsOutputDir, { recursive: true }) - const outputPath = resolve(miniappsOutputDir, 'ecosystem.json') - writeFileSync(outputPath, JSON.stringify(ecosystem, null, 2)) - console.log(`[miniapps] Generated ${outputPath}`) + const ecosystem = generateEcosystemDataForBuild(root, miniappsDir); + const miniappsOutputDir = resolve(options.dir, 'miniapps'); + mkdirSync(miniappsOutputDir, { recursive: true }); + const outputPath = resolve(miniappsOutputDir, 'ecosystem.json'); + writeFileSync(outputPath, JSON.stringify(ecosystem, null, 2)); + console.log(`[miniapps] Generated ${outputPath}`); } }, async configureServer(server: ViteDevServer) { - const miniappsPath = resolve(root, miniappsDir) - const manifests = scanMiniapps(miniappsPath) + const miniappsPath = resolve(root, miniappsDir); + const manifests = scanMiniapps(miniappsPath); // 预分配端口 - const portAssignments: Array<{ manifest: MiniappManifest; port: number }> = [] + const portAssignments: Array<{ manifest: MiniappManifest; port: number }> = []; for (const manifest of manifests) { - const port = await detectPort(0) - portAssignments.push({ manifest, port }) + const port = await detectPort(0); + portAssignments.push({ manifest, port }); } // 并行启动所有 miniapp dev servers await Promise.all( portAssignments.map(async ({ manifest, port }) => { - const miniappPath = join(miniappsPath, manifest.dirName) - const miniappServer = await createMiniappServer(manifest.id, miniappPath, port) + const miniappPath = join(miniappsPath, manifest.dirName); + const miniappServer = await createMiniappServer(manifest.id, miniappPath, port); miniappServers.push({ id: manifest.id, @@ -113,11 +113,11 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { port, server: miniappServer, baseUrl: `https://localhost:${port}`, - }) + }); - console.log(`[miniapps] ${manifest.name} (${manifest.id}) started at https://localhost:${port}`) + console.log(`[miniapps] ${manifest.name} (${manifest.id}) started at https://localhost:${port}`); }), - ) + ); // 等待所有 miniapp 启动后,fetch 各自的 /manifest.json 生成 ecosystem const generateEcosystem = async (): Promise => { @@ -125,98 +125,95 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { const localApps = await Promise.all( miniappServers.map(async (s) => { try { - const manifest = await fetchManifest(s.port) + const manifest = await fetchManifest(s.port); return { ...manifest, dirName: s.dirName, icon: new URL(manifest.icon, s.baseUrl).href, url: new URL('/', s.baseUrl).href, screenshots: manifest.screenshots.map((sc) => new URL(sc, s.baseUrl).href), - } + }; } catch (e) { - console.error(`[miniapps] Failed to fetch manifest for ${s.id}:`, e) - return null + console.error(`[miniapps] Failed to fetch manifest for ${s.id}:`, e); + return null; } }), - ) + ); // 远程 miniapps (从 vite-plugin-remote-miniapps 获取) - const remoteApps = getRemoteMiniappsForEcosystem() + const remoteApps = getRemoteMiniappsForEcosystem(); return { name: 'Bio 官方生态', version: '1.0.0', updated: new Date().toISOString().split('T')[0], icon: '/logos/logo-256.webp', - apps: [ - ...localApps.filter((a): a is NonNullable => a !== null), - ...remoteApps, - ], - } - } + apps: [...localApps.filter((a): a is NonNullable => a !== null), ...remoteApps], + }; + }; // 预生成 ecosystem - const ecosystemData = await generateEcosystem() - let ecosystemCache = JSON.stringify(ecosystemData, null, 2) + const ecosystemData = await generateEcosystem(); + let ecosystemCache = JSON.stringify(ecosystemData, null, 2); // 拦截 /miniapps/ecosystem.json 请求 server.middlewares.use((req, res, next) => { if (req.url === '/miniapps/ecosystem.json') { - res.setHeader('Content-Type', 'application/json') - res.setHeader('Access-Control-Allow-Origin', '*') - res.end(ecosystemCache) - return + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.end(ecosystemCache); + return; } - next() - }) + next(); + }); // 清理服务器 const cleanup = async () => { - await Promise.all(miniappServers.map((s) => s.server.close())) - } + await Promise.all(miniappServers.map((s) => s.server.close())); + }; - server.httpServer?.on('close', cleanup) + server.httpServer?.on('close', cleanup); }, async closeBundle() { - await Promise.all(miniappServers.map((s) => s.server.close())) + await Promise.all(miniappServers.map((s) => s.server.close())); }, - } + }; } // ==================== Helpers ==================== function scanMiniapps(miniappsPath: string): MiniappManifest[] { - if (!existsSync(miniappsPath)) return [] + if (!existsSync(miniappsPath)) return []; - const manifests: MiniappManifest[] = [] - const entries = readdirSync(miniappsPath, { withFileTypes: true }) + const manifests: MiniappManifest[] = []; + const entries = readdirSync(miniappsPath, { withFileTypes: true }); for (const entry of entries) { - if (!entry.isDirectory()) continue + if (!entry.isDirectory()) continue; - const manifestPath = join(miniappsPath, entry.name, 'manifest.json') + const manifestPath = join(miniappsPath, entry.name, 'manifest.json'); if (!existsSync(manifestPath)) { - console.warn(`[miniapps] ${entry.name}: missing manifest.json, skipping`) - continue + console.warn(`[miniapps] ${entry.name}: missing manifest.json, skipping`); + continue; } // 跳过远程 miniapps (没有 vite.config.ts 的是已构建的远程 miniapp) - const viteConfigPath = join(miniappsPath, entry.name, 'vite.config.ts') + const viteConfigPath = join(miniappsPath, entry.name, 'vite.config.ts'); if (!existsSync(viteConfigPath)) { // 远程 miniapp,由 vite-plugin-remote-miniapps 处理 - continue + continue; } try { - const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as MiniappManifest - manifests.push({ ...manifest, dirName: entry.name }) + const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as MiniappManifest; + manifests.push({ ...manifest, dirName: entry.name }); } catch (e) { - console.warn(`[miniapps] ${entry.name}: invalid manifest.json, skipping`) + console.warn(`[miniapps] ${entry.name}: invalid manifest.json, skipping`); } } - return manifests + return manifests; } async function createMiniappServer(_id: string, root: string, port: number): Promise { @@ -229,86 +226,115 @@ async function createMiniappServer(_id: string, root: string, port: number): Pro https: true as any, // Type compatibility workaround }, logLevel: 'warn', - }) + }); - await server.listen() - return server + await server.listen(); + return server; } async function fetchManifest(port: number): Promise { return new Promise((resolve, reject) => { - const req = https.get( - `https://localhost:${port}/manifest.json`, - { rejectUnauthorized: false }, - (res) => { - let data = '' - res.on('data', (chunk) => (data += chunk)) - res.on('end', () => { - try { - resolve(JSON.parse(data)) - } catch (e) { - reject(e) - } - }) - }, - ) - req.on('error', reject) - }) + const req = https.get(`https://localhost:${port}/manifest.json`, { rejectUnauthorized: false }, (res) => { + let data = ''; + res.on('data', (chunk) => (data += chunk)); + res.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch (e) { + reject(e); + } + }); + }); + req.on('error', reject); + }); } function scanScreenshots(root: string, shortId: string): string[] { - const e2eDir = resolve(root, 'e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts') - if (!existsSync(e2eDir)) return [] + const e2eDir = resolve(root, 'e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts'); + if (!existsSync(e2eDir)) return []; return readdirSync(e2eDir) .filter((f) => f.startsWith(`${shortId}-`) && f.endsWith('.png')) .slice(0, 2) - .map((f) => `screenshots/${f}`) + .map((f) => `screenshots/${f}`); } function generateEcosystemDataForBuild(root: string, miniappsDir: string): EcosystemJson { - const miniappsPath = resolve(root, miniappsDir) - const manifests = scanMiniapps(miniappsPath) + const miniappsPath = resolve(root, miniappsDir); + const manifests = scanMiniapps(miniappsPath); - // 路径使用相对于 ecosystem.json 的位置(即 miniapps/ 目录) - const apps = manifests.map((manifest) => { - const shortId = manifest.id.split('.').pop() || '' - const screenshots = scanScreenshots(root, shortId) + const localApps = manifests.map((manifest) => { + const shortId = manifest.id.split('.').pop() || ''; + const screenshots = scanScreenshots(root, shortId); - const { dirName, ...rest } = manifest + const { dirName, ...rest } = manifest; return { ...rest, dirName, url: `./${dirName}/`, icon: `./${dirName}/icon.svg`, screenshots: screenshots.map((s) => `./${dirName}/${s}`), - } - }) + }; + }); + + const remoteApps = scanRemoteMiniappsForBuild(miniappsPath); return { name: 'Bio 官方生态', version: '1.0.0', updated: new Date().toISOString().split('T')[0], icon: '../logos/logo-256.webp', - apps, + apps: [...localApps, ...remoteApps], + }; +} + +function scanRemoteMiniappsForBuild(miniappsPath: string): Array { + if (!existsSync(miniappsPath)) return []; + + const remoteApps: Array = []; + const entries = readdirSync(miniappsPath, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + + const manifestPath = join(miniappsPath, entry.name, 'manifest.json'); + const viteConfigPath = join(miniappsPath, entry.name, 'vite.config.ts'); + + if (!existsSync(manifestPath)) continue; + if (existsSync(viteConfigPath)) continue; + + try { + const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as MiniappManifest; + remoteApps.push({ + ...manifest, + dirName: entry.name, + url: `./${entry.name}/`, + icon: `./${entry.name}/${manifest.icon}`, + screenshots: manifest.screenshots?.map((s) => `./${entry.name}/${s}`) ?? [], + }); + } catch { + console.warn(`[miniapps] ${entry.name}: invalid remote manifest.json, skipping`); + } } + + return remoteApps; } async function buildAllMiniapps(root: string, miniappsDir: string, distDir: string): Promise { - const miniappsPath = resolve(root, miniappsDir) - const manifests = scanMiniapps(miniappsPath) - const miniappsDistDir = resolve(distDir, 'miniapps') + const miniappsPath = resolve(root, miniappsDir); + const manifests = scanMiniapps(miniappsPath); + const miniappsDistDir = resolve(distDir, 'miniapps'); - mkdirSync(miniappsDistDir, { recursive: true }) + mkdirSync(miniappsDistDir, { recursive: true }); - console.log(`[miniapps] Building ${manifests.length} miniapps...`) + console.log(`[miniapps] Building ${manifests.length} miniapps...`); for (const manifest of manifests) { - const miniappPath = join(miniappsPath, manifest.dirName) - const outputDir = resolve(miniappsDistDir, manifest.dirName) - const shortId = manifest.id.split('.').pop() || '' + const miniappPath = join(miniappsPath, manifest.dirName); + const outputDir = resolve(miniappsDistDir, manifest.dirName); + const shortId = manifest.id.split('.').pop() || ''; - console.log(`[miniapps] Building ${manifest.name} (${manifest.id})...`) + console.log(`[miniapps] Building ${manifest.name} (${manifest.id})...`); await viteBuild({ root: miniappPath, @@ -319,36 +345,36 @@ async function buildAllMiniapps(root: string, miniappsDir: string, distDir: stri emptyOutDir: true, }, logLevel: 'warn', - }) + }); // 复制 public 目录静态资源(icon 等) - const publicDir = join(miniappPath, 'public') + const publicDir = join(miniappPath, 'public'); if (existsSync(publicDir)) { - cpSync(publicDir, outputDir, { recursive: true }) + cpSync(publicDir, outputDir, { recursive: true }); } // 复制 e2e 截图 - const e2eScreenshotsDir = resolve(root, 'e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts') - const screenshots = scanScreenshots(root, shortId) + const e2eScreenshotsDir = resolve(root, 'e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts'); + const screenshots = scanScreenshots(root, shortId); if (screenshots.length > 0 && existsSync(e2eScreenshotsDir)) { - const screenshotsOutputDir = resolve(outputDir, 'screenshots') - mkdirSync(screenshotsOutputDir, { recursive: true }) + const screenshotsOutputDir = resolve(outputDir, 'screenshots'); + mkdirSync(screenshotsOutputDir, { recursive: true }); for (const screenshot of screenshots) { - const filename = screenshot.replace('screenshots/', '') - const src = resolve(e2eScreenshotsDir, filename) - const dest = resolve(screenshotsOutputDir, filename) + const filename = screenshot.replace('screenshots/', ''); + const src = resolve(e2eScreenshotsDir, filename); + const dest = resolve(screenshotsOutputDir, filename); if (existsSync(src)) { - cpSync(src, dest) + cpSync(src, dest); } } } - console.log(`[miniapps] ${manifest.name} built`) + console.log(`[miniapps] ${manifest.name} built`); } - console.log(`[miniapps] All miniapps built successfully`) + console.log(`[miniapps] All miniapps built successfully`); } -export default miniappsPlugin +export default miniappsPlugin;