From cfb46994565c9b922c4dcd9741193fd54229f146 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 11:17:40 +0800 Subject: [PATCH 01/16] fix(dweb): detect environment via @plaoc/is-dweb --- package.json | 3 ++- pnpm-lock.yaml | 15 +++++++++++++++ src/lib/__tests__/dweb-update.test.ts | 5 ++++- src/lib/crypto/secure-storage.ts | 7 ++++++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8d9ac0b3c..e56b702f1 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "@noble/hashes": "^2.0.1", "@noble/secp256k1": "^3.0.0", "@number-flow/react": "^0.5.10", + "@plaoc/is-dweb": "^0.1.2", "@plaoc/plugins": "^1.1.9", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", @@ -143,8 +144,8 @@ "@axe-core/playwright": "^4.11.0", "@biochain/e2e-tools": "workspace:*", "@inquirer/prompts": "^8.0.2", - "@playwright/test": "^1.57.0", "@plaoc/cli": "^1.2.2", + "@playwright/test": "^1.57.0", "@storybook/addon-docs": "^10.1.4", "@storybook/addon-vitest": "^10.1.4", "@storybook/react": "^10.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d786e330b..34675ab56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: '@number-flow/react': specifier: ^0.5.10 version: 0.5.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@plaoc/is-dweb': + specifier: ^0.1.2 + version: 0.1.2 '@plaoc/plugins': specifier: ^1.1.9 version: 1.1.9(typescript@5.9.3) @@ -1626,6 +1629,9 @@ packages: '@dweb-browser/js-process@0.4.7': resolution: {integrity: sha512-XUBSPxcgpKjjEibIo1f7f5RWH4VqYW9M4RunUSVIsI64ENk4FjG7AjeD27FBikwbuku+rmTWzgH3wQyNDDnEyg==} + '@dweb-browser/polyfill@0.0.8': + resolution: {integrity: sha512-OhjyCJiaOKFTmUoRBugn+bcmjZqKSek6WesB/2vASnPMTODpyqKLQ6uUguGuYb9mAC64lwi0P7eoCJmRyzQ6Jw==} + '@dweb-browser/polyfill@0.0.9': resolution: {integrity: sha512-1siZWAvzXMVZ5z6TmFPCSYXHTNTu5qmZv5UR0QSlujXuQr93I+BX6bhyu2FG4pCqqkoE3OAVlQ15D9Nj+beS4g==} @@ -2877,6 +2883,9 @@ packages: resolution: {integrity: sha512-4txWq8sytf6HCNt+ryWJzjhXVtNMrckdO/sBb2Xd3MbHPBMo1rvRIvVy0FCkPHe8cODF60SF9W/tovAFyYSuVw==} hasBin: true + '@plaoc/is-dweb@0.1.2': + resolution: {integrity: sha512-H2QdJVjOVhnE75mTq5vQQYVLZzqDFHoPUVtMnJRGpFCgjkTA36mswo3St9kxnAZlAMJziDckMbu6+/1daXn87Q==} + '@plaoc/plugins@1.1.9': resolution: {integrity: sha512-TnFNddIYsX0myjhhNVRR+HZS415CPnw0Xe4ipyP0ICUm94dleGZodJTDAPkgsKZi085gI7dlSXp8hYsNI3TIhw==} @@ -9537,6 +9546,8 @@ snapshots: transitivePeerDependencies: - typescript + '@dweb-browser/polyfill@0.0.8': {} + '@dweb-browser/polyfill@0.0.9': {} '@dweb-browser/svg-wasm@0.9.0': {} @@ -10788,6 +10799,10 @@ snapshots: - typescript - utf-8-validate + '@plaoc/is-dweb@0.1.2': + dependencies: + '@dweb-browser/polyfill': 0.0.8 + '@plaoc/plugins@1.1.9(typescript@5.9.3)': dependencies: '@dweb-browser/core': 0.1.4(typescript@5.9.3) diff --git a/src/lib/__tests__/dweb-update.test.ts b/src/lib/__tests__/dweb-update.test.ts index 4606a9222..011d0b206 100644 --- a/src/lib/__tests__/dweb-update.test.ts +++ b/src/lib/__tests__/dweb-update.test.ts @@ -11,7 +11,10 @@ function stubGlobals() { vi.stubGlobal('__KEYAPP_SITE_ORIGIN__', 'https://example.com/KeyApp/') vi.stubGlobal('__DEV_MODE__', false) vi.stubGlobal('__APP_VERSION__', '1.2.3') - Object.defineProperty(window, 'dwebTarget', { value: true, configurable: true }) + Object.defineProperty(window, '__native_close_watcher_kit__', { + value: {}, + configurable: true, + }) } describe('dweb update', () => { diff --git a/src/lib/crypto/secure-storage.ts b/src/lib/crypto/secure-storage.ts index a1428eae2..fa1dc149a 100644 --- a/src/lib/crypto/secure-storage.ts +++ b/src/lib/crypto/secure-storage.ts @@ -5,6 +5,7 @@ * - Web 环境:使用 AES-GCM 密码加密 + localStorage */ +import { isDweb } from '@plaoc/is-dweb'; import { encrypt, decrypt, type EncryptedData } from './encryption'; import i18n from '@/i18n'; @@ -47,7 +48,11 @@ export interface StoredData { /** 检测是否在 DWEB 环境 */ export function isDwebEnvironment(): boolean { - return typeof window !== 'undefined' && 'dwebTarget' in window; + try { + return isDweb(); + } catch { + return false; + } } /** DWEB biometric 插件类型 */ From f7521a4a2d189a677fe06557b0a00af24badebbd Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 11:17:49 +0800 Subject: [PATCH 02/16] feat: complete implementation --- deno.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/deno.lock b/deno.lock index 01b73c89e..9b712a535 100644 --- a/deno.lock +++ b/deno.lock @@ -10153,6 +10153,7 @@ "npm:@noble/secp256k1@3", "npm:@number-flow/react@~0.5.10", "npm:@plaoc/cli@^1.2.2", + "npm:@plaoc/is-dweb@~0.1.2", "npm:@plaoc/plugins@^1.1.9", "npm:@playwright/test@^1.57.0", "npm:@radix-ui/react-avatar@^1.1.11", From f470a3db1af23f25c5e720412d09e733b37927d6 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 11:28:25 +0800 Subject: [PATCH 03/16] feat(dev): proxy miniapps via same-origin --- scripts/vite-plugin-miniapps.ts | 46 +++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index 004ac75c0..1f9006998 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -69,7 +69,6 @@ interface MiniappServer { dirName: string; port: number; server: ViteDevServer; - baseUrl: string; } interface MiniappsPluginOptions { @@ -130,13 +129,42 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { dirName: manifest.dirName, port, server: miniappServer, - baseUrl: `https://localhost:${port}`, }); console.log(`[miniapps] ${manifest.name} (${manifest.id}) started at https://localhost:${port}`); }), ); + // 同源代理:/miniapps/{dirName}/ -> miniapp dev server + for (const miniapp of miniappServers) { + const prefix = `/miniapps/${miniapp.dirName}`; + server.middlewares.use((req, res, next) => { + const url = req.url ?? ''; + if (url === prefix) { + res.statusCode = 302; + res.setHeader('Location', `${prefix}/`); + res.end(); + return; + } + if (!url.startsWith(`${prefix}/`)) { + next(); + return; + } + + const originalUrl = req.url; + req.url = url.slice(prefix.length); + miniapp.server.middlewares(req, res, (err) => { + req.url = originalUrl; + if (err) { + next(err); + return; + } + if (res.writableEnded || res.headersSent) return; + next(); + }); + }); + } + // 等待所有 miniapp 启动后,fetch 各自的 /manifest.json 生成 ecosystem const generateEcosystem = async (): Promise => { const localApps = await Promise.all( @@ -148,9 +176,9 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { 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), + icon: resolveMiniappDevAsset(manifest.icon, s.dirName), + url: `./${s.dirName}/`, + screenshots: manifest.screenshots.map((sc) => resolveMiniappDevAsset(sc, s.dirName)), runtime, wujieConfig, }; @@ -280,6 +308,14 @@ function scanScreenshots(root: string, shortId: string): string[] { .map((f) => `screenshots/${f}`); } +function resolveMiniappDevAsset(path: string, dirName: string): string { + if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('data:')) { + return path; + } + const normalized = path.replace(/^\.\//, '').replace(/^\//, ''); + return `./${dirName}/${normalized}`; +} + function generateEcosystemDataForBuild( root: string, miniappsDir: string, From 90e2050722da7082a88ed8198bc18bace16c222a Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 11:36:13 +0800 Subject: [PATCH 04/16] fix(dev): allow /{miniapp}/ access --- scripts/vite-plugin-miniapps.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index 1f9006998..c83ab20a0 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -136,8 +136,7 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { ); // 同源代理:/miniapps/{dirName}/ -> miniapp dev server - for (const miniapp of miniappServers) { - const prefix = `/miniapps/${miniapp.dirName}`; + const registerMiniappProxy = (miniapp: MiniappServer, prefix: string) => { server.middlewares.use((req, res, next) => { const url = req.url ?? ''; if (url === prefix) { @@ -163,6 +162,11 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { next(); }); }); + }; + + for (const miniapp of miniappServers) { + registerMiniappProxy(miniapp, `/miniapps/${miniapp.dirName}`); + registerMiniappProxy(miniapp, `/${miniapp.dirName}`); } // 等待所有 miniapp 启动后,fetch 各自的 /manifest.json 生成 ecosystem From c509a4af3f81c9397b710af509a029ddfd2597be Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 11:39:06 +0800 Subject: [PATCH 05/16] fix(dev): prepend miniapp proxy middleware --- scripts/vite-plugin-miniapps.ts | 50 +++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index c83ab20a0..a4c7ad13c 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -7,6 +7,7 @@ */ import { createServer, build as viteBuild, type Plugin, type ViteDevServer } from 'vite'; +import type { IncomingMessage, ServerResponse } from 'node:http'; import { resolve, join } from 'node:path'; import { readdirSync, existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'node:fs'; import detectPort from 'detect-port'; @@ -136,23 +137,27 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { ); // 同源代理:/miniapps/{dirName}/ -> miniapp dev server - const registerMiniappProxy = (miniapp: MiniappServer, prefix: string) => { - server.middlewares.use((req, res, next) => { - const url = req.url ?? ''; - if (url === prefix) { + const proxyRoutes: Array<{ prefix: string; miniapp: MiniappServer }> = []; + for (const miniapp of miniappServers) { + proxyRoutes.push({ prefix: `/miniapps/${miniapp.dirName}`, miniapp }); + proxyRoutes.push({ prefix: `/${miniapp.dirName}`, miniapp }); + } + const proxyMiddleware = (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => { + const url = req.url ?? ''; + for (const route of proxyRoutes) { + if (url === route.prefix) { res.statusCode = 302; - res.setHeader('Location', `${prefix}/`); + res.setHeader('Location', `${route.prefix}/`); res.end(); return; } - if (!url.startsWith(`${prefix}/`)) { - next(); - return; + if (!url.startsWith(`${route.prefix}/`)) { + continue; } const originalUrl = req.url; - req.url = url.slice(prefix.length); - miniapp.server.middlewares(req, res, (err) => { + req.url = url.slice(route.prefix.length); + route.miniapp.server.middlewares(req, res, (err) => { req.url = originalUrl; if (err) { next(err); @@ -161,13 +166,12 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { if (res.writableEnded || res.headersSent) return; next(); }); - }); + return; + } + next(); }; - for (const miniapp of miniappServers) { - registerMiniappProxy(miniapp, `/miniapps/${miniapp.dirName}`); - registerMiniappProxy(miniapp, `/${miniapp.dirName}`); - } + prependMiddleware(server, proxyMiddleware); // 等待所有 miniapp 启动后,fetch 各自的 /manifest.json 生成 ecosystem const generateEcosystem = async (): Promise => { @@ -320,6 +324,22 @@ function resolveMiniappDevAsset(path: string, dirName: string): string { return `./${dirName}/${normalized}`; } +function prependMiddleware( + server: ViteDevServer, + middleware: (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void, +): void { + const stack = ( + server.middlewares as { + stack?: Array<{ route: string; handle: typeof middleware }>; + } + ).stack; + if (stack) { + stack.unshift({ route: '', handle: middleware }); + return; + } + server.middlewares.use(middleware); +} + function generateEcosystemDataForBuild( root: string, miniappsDir: string, From f5e2859bc1793616773c1cf29a9513cbb1470525 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 11:43:05 +0800 Subject: [PATCH 06/16] feat(dev): add /miniapp proxy path --- scripts/vite-plugin-miniapps.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index a4c7ad13c..2986b0fc4 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -136,11 +136,11 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { }), ); - // 同源代理:/miniapps/{dirName}/ -> miniapp dev server + // 同源代理:/miniapp(s)/{dirName}/ -> miniapp dev server const proxyRoutes: Array<{ prefix: string; miniapp: MiniappServer }> = []; for (const miniapp of miniappServers) { + proxyRoutes.push({ prefix: `/miniapp/${miniapp.dirName}`, miniapp }); proxyRoutes.push({ prefix: `/miniapps/${miniapp.dirName}`, miniapp }); - proxyRoutes.push({ prefix: `/${miniapp.dirName}`, miniapp }); } const proxyMiddleware = (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => { const url = req.url ?? ''; @@ -185,7 +185,7 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { ...manifest, dirName: s.dirName, icon: resolveMiniappDevAsset(manifest.icon, s.dirName), - url: `./${s.dirName}/`, + url: `./miniapp/${s.dirName}/`, screenshots: manifest.screenshots.map((sc) => resolveMiniappDevAsset(sc, s.dirName)), runtime, wujieConfig, @@ -212,9 +212,9 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { const ecosystemData = await generateEcosystem(); let ecosystemCache = JSON.stringify(ecosystemData, null, 2); - // 拦截 /miniapps/ecosystem.json 请求 + // 拦截 /miniapp(s)/ecosystem.json 请求 server.middlewares.use((req, res, next) => { - if (req.url === '/miniapps/ecosystem.json') { + if (req.url === '/miniapps/ecosystem.json' || req.url === '/miniapp/ecosystem.json') { res.setHeader('Content-Type', 'application/json'); res.setHeader('Access-Control-Allow-Origin', '*'); res.end(ecosystemCache); @@ -321,7 +321,7 @@ function resolveMiniappDevAsset(path: string, dirName: string): string { return path; } const normalized = path.replace(/^\.\//, '').replace(/^\//, ''); - return `./${dirName}/${normalized}`; + return `./miniapp/${dirName}/${normalized}`; } function prependMiddleware( From adde3f0f0bcf5f518894598fecc4456b0027f227 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 11:44:35 +0800 Subject: [PATCH 07/16] Revert "feat(dev): add /miniapp proxy path" This reverts commit f5e2859bc1793616773c1cf29a9513cbb1470525. --- scripts/vite-plugin-miniapps.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index 2986b0fc4..a4c7ad13c 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -136,11 +136,11 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { }), ); - // 同源代理:/miniapp(s)/{dirName}/ -> miniapp dev server + // 同源代理:/miniapps/{dirName}/ -> miniapp dev server const proxyRoutes: Array<{ prefix: string; miniapp: MiniappServer }> = []; for (const miniapp of miniappServers) { - proxyRoutes.push({ prefix: `/miniapp/${miniapp.dirName}`, miniapp }); proxyRoutes.push({ prefix: `/miniapps/${miniapp.dirName}`, miniapp }); + proxyRoutes.push({ prefix: `/${miniapp.dirName}`, miniapp }); } const proxyMiddleware = (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => { const url = req.url ?? ''; @@ -185,7 +185,7 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { ...manifest, dirName: s.dirName, icon: resolveMiniappDevAsset(manifest.icon, s.dirName), - url: `./miniapp/${s.dirName}/`, + url: `./${s.dirName}/`, screenshots: manifest.screenshots.map((sc) => resolveMiniappDevAsset(sc, s.dirName)), runtime, wujieConfig, @@ -212,9 +212,9 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { const ecosystemData = await generateEcosystem(); let ecosystemCache = JSON.stringify(ecosystemData, null, 2); - // 拦截 /miniapp(s)/ecosystem.json 请求 + // 拦截 /miniapps/ecosystem.json 请求 server.middlewares.use((req, res, next) => { - if (req.url === '/miniapps/ecosystem.json' || req.url === '/miniapp/ecosystem.json') { + if (req.url === '/miniapps/ecosystem.json') { res.setHeader('Content-Type', 'application/json'); res.setHeader('Access-Control-Allow-Origin', '*'); res.end(ecosystemCache); @@ -321,7 +321,7 @@ function resolveMiniappDevAsset(path: string, dirName: string): string { return path; } const normalized = path.replace(/^\.\//, '').replace(/^\//, ''); - return `./miniapp/${dirName}/${normalized}`; + return `./${dirName}/${normalized}`; } function prependMiddleware( From 20da94f8917e71e9cc34dc4c420f2e65f9bbf544 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 11:47:50 +0800 Subject: [PATCH 08/16] fix(dev): forward originalUrl to miniapp server --- scripts/vite-plugin-miniapps.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index a4c7ad13c..5456bd518 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -156,9 +156,13 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { } const originalUrl = req.url; - req.url = url.slice(route.prefix.length); + const originalOriginalUrl = (req as IncomingMessage & { originalUrl?: string }).originalUrl; + const nextUrl = url.slice(route.prefix.length); + req.url = nextUrl; + (req as IncomingMessage & { originalUrl?: string }).originalUrl = nextUrl; route.miniapp.server.middlewares(req, res, (err) => { req.url = originalUrl; + (req as IncomingMessage & { originalUrl?: string }).originalUrl = originalOriginalUrl; if (err) { next(err); return; From ed2105ed0eb24ca0973c3994dcbb99c4c5003b88 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 12:28:01 +0800 Subject: [PATCH 09/16] fix(dev): align ecosystem urls with /miniapps --- scripts/vite-plugin-miniapps.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index 5456bd518..c7d8b080a 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -189,7 +189,7 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { ...manifest, dirName: s.dirName, icon: resolveMiniappDevAsset(manifest.icon, s.dirName), - url: `./${s.dirName}/`, + url: `/miniapps/${s.dirName}/`, screenshots: manifest.screenshots.map((sc) => resolveMiniappDevAsset(sc, s.dirName)), runtime, wujieConfig, @@ -325,7 +325,7 @@ function resolveMiniappDevAsset(path: string, dirName: string): string { return path; } const normalized = path.replace(/^\.\//, '').replace(/^\//, ''); - return `./${dirName}/${normalized}`; + return `/miniapps/${dirName}/${normalized}`; } function prependMiddleware( From fd8f81c9eda3582b68f43c16665adf0cbd783f86 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 14:25:36 +0800 Subject: [PATCH 10/16] fix(dev): rewrite miniapp html asset paths --- scripts/vite-plugin-miniapps.ts | 44 ++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index c7d8b080a..449e010c8 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -8,7 +8,7 @@ import { createServer, build as viteBuild, type Plugin, type ViteDevServer } from 'vite'; import type { IncomingMessage, ServerResponse } from 'node:http'; -import { resolve, join } from 'node:path'; +import { resolve, join, basename } from 'node:path'; import { readdirSync, existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'node:fs'; import detectPort from 'detect-port'; import https from 'node:https'; @@ -155,6 +155,27 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { continue; } + const shouldRewrite = shouldRewriteHtml(url, req); + if (shouldRewrite) { + const originalEnd = res.end.bind(res); + const chunks: Buffer[] = []; + + res.write = ((chunk: Buffer | string) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + return true; + }) as typeof res.write; + + res.end = ((chunk?: Buffer | string) => { + if (chunk) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + const body = Buffer.concat(chunks).toString('utf-8'); + const rewritten = rewriteMiniappHtml(body, route.prefix); + res.setHeader('Content-Length', Buffer.byteLength(rewritten)); + return originalEnd(rewritten); + }) as typeof res.end; + } + const originalUrl = req.url; const originalOriginalUrl = (req as IncomingMessage & { originalUrl?: string }).originalUrl; const nextUrl = url.slice(route.prefix.length); @@ -328,6 +349,27 @@ function resolveMiniappDevAsset(path: string, dirName: string): string { return `/miniapps/${dirName}/${normalized}`; } +function shouldRewriteHtml(url: string, req: IncomingMessage): boolean { + if (url.endsWith('/') || url.endsWith('.html')) return true; + const accept = req.headers.accept ?? ''; + return accept.includes('text/html'); +} + +function rewriteMiniappHtml(html: string, prefix: string): string { + const replacements: Array<[RegExp, string]> = [ + [/(['"])\/@/g, `$1${prefix}/@`], + [/(['"])\/src\//g, `$1${prefix}/src/`], + [/(['"])\/node_modules\//g, `$1${prefix}/node_modules/`], + [/(['"])\/@fs\//g, `$1${prefix}/@fs/`], + ]; + + let output = html; + for (const [pattern, replacement] of replacements) { + output = output.replace(pattern, replacement); + } + return output; +} + function prependMiddleware( server: ViteDevServer, middleware: (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void, From 03042e436d9564a554f055aaa8bb088991992ad4 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 14:25:46 +0800 Subject: [PATCH 11/16] feat: complete implementation --- vite.config.ts | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 240167eb2..41a48e5b6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -82,10 +82,10 @@ function getPreferredLanIPv4(): string | undefined { function getPackageVersion(): string { try { - const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8')) as { version?: string } - return pkg.version ?? '0.0.0' + const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8')) as { version?: string }; + return pkg.version ?? '0.0.0'; } catch { - return '0.0.0' + return '0.0.0'; } } @@ -112,16 +112,13 @@ export default defineConfig(({ mode }) => { const tronGridApiKey = env.TRONGRID_API_KEY ?? process.env.TRONGRID_API_KEY ?? ''; const etherscanApiKey = env.ETHERSCAN_API_KEY ?? process.env.ETHERSCAN_API_KEY ?? ''; const moralisApiKey = env.MORALIS_API_KEY ?? process.env.MORALIS_API_KEY ?? ''; - const isDevBuild = (env.VITE_DEV_MODE ?? process.env.VITE_DEV_MODE) === 'true' - const siteOrigin = - env.SITE_ORIGIN ?? - process.env.SITE_ORIGIN ?? - 'https://bioforestchain.github.io/KeyApp/' + const isDevBuild = (env.VITE_DEV_MODE ?? process.env.VITE_DEV_MODE) === 'true'; + const siteOrigin = env.SITE_ORIGIN ?? process.env.SITE_ORIGIN ?? 'https://bioforestchain.github.io/KeyApp/'; - const buildTime = new Date() - const pad = (value: number) => value.toString().padStart(2, '0') - const buildSuffix = `-${pad(buildTime.getUTCMonth() + 1)}${pad(buildTime.getUTCDate())}${pad(buildTime.getUTCHours())}` - const appVersion = `${getPackageVersion()}${isDevBuild ? buildSuffix : ''}` + const buildTime = new Date(); + const pad = (value: number) => value.toString().padStart(2, '0'); + const buildSuffix = `-${pad(buildTime.getUTCMonth() + 1)}${pad(buildTime.getUTCDate())}${pad(buildTime.getUTCHours())}`; + const appVersion = `${getPackageVersion()}${isDevBuild ? buildSuffix : ''}`; return { base: BASE_URL, @@ -164,12 +161,12 @@ export default defineConfig(({ mode }) => { miniappsPlugin({ apps: { 'xin.dweb.teleport': { - server: 'wujie', - build: 'wujie', + server: 'iframe', + build: 'iframe', }, 'xin.dweb.biobridge': { - server: 'wujie', - build: 'wujie', + server: 'iframe', + build: 'iframe', }, }, remoteMiniapps: remoteMiniappsConfig, From 16bd2c6d847154c3128a1531fbd4cd47812489aa Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 14:39:34 +0800 Subject: [PATCH 12/16] fix(dev): rewrite miniapp dev module paths --- scripts/vite-plugin-miniapps.ts | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index 449e010c8..b785dd522 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -155,8 +155,8 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { continue; } - const shouldRewrite = shouldRewriteHtml(url, req); - if (shouldRewrite) { + const rewriteMode = getRewriteMode(url, req); + if (rewriteMode) { const originalEnd = res.end.bind(res); const chunks: Buffer[] = []; @@ -170,7 +170,7 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } const body = Buffer.concat(chunks).toString('utf-8'); - const rewritten = rewriteMiniappHtml(body, route.prefix); + const rewritten = rewriteMiniappBody(body, route.prefix); res.setHeader('Content-Length', Buffer.byteLength(rewritten)); return originalEnd(rewritten); }) as typeof res.end; @@ -349,21 +349,38 @@ function resolveMiniappDevAsset(path: string, dirName: string): string { return `/miniapps/${dirName}/${normalized}`; } -function shouldRewriteHtml(url: string, req: IncomingMessage): boolean { - if (url.endsWith('/') || url.endsWith('.html')) return true; +function getRewriteMode(url: string, req: IncomingMessage): 'html' | 'js' | null { const accept = req.headers.accept ?? ''; - return accept.includes('text/html'); + const path = url.split('?', 1)[0] ?? ''; + + if (path.endsWith('/') || path.endsWith('.html') || accept.includes('text/html')) { + return 'html'; + } + + if ( + path.includes('/@') || + path.includes('/@id/') || + path.endsWith('.js') || + path.endsWith('.ts') || + path.endsWith('.tsx') || + path.endsWith('.jsx') + ) { + return 'js'; + } + + return null; } -function rewriteMiniappHtml(html: string, prefix: string): string { +function rewriteMiniappBody(body: string, prefix: string): string { const replacements: Array<[RegExp, string]> = [ [/(['"])\/@/g, `$1${prefix}/@`], + [/(['"])\/@id\//g, `$1${prefix}/@id/`], [/(['"])\/src\//g, `$1${prefix}/src/`], [/(['"])\/node_modules\//g, `$1${prefix}/node_modules/`], [/(['"])\/@fs\//g, `$1${prefix}/@fs/`], ]; - let output = html; + let output = body; for (const [pattern, replacement] of replacements) { output = output.replace(pattern, replacement); } From af385bc74cd5103993a1834b7bdaf56ec70a4a51 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 14:52:30 +0800 Subject: [PATCH 13/16] fix(dev): avoid setting headers after sent --- scripts/vite-plugin-miniapps.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index b785dd522..743eabb91 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -171,7 +171,9 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { } const body = Buffer.concat(chunks).toString('utf-8'); const rewritten = rewriteMiniappBody(body, route.prefix); - res.setHeader('Content-Length', Buffer.byteLength(rewritten)); + if (!res.headersSent) { + res.setHeader('Content-Length', Buffer.byteLength(rewritten)); + } return originalEnd(rewritten); }) as typeof res.end; } From 2f430b19255f7f57fea0b65628b5efe31cd944da Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 14:59:12 +0800 Subject: [PATCH 14/16] fix(dev): disable compression for rewritten miniapps --- scripts/vite-plugin-miniapps.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index 743eabb91..6a5e07d79 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -171,6 +171,9 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { } const body = Buffer.concat(chunks).toString('utf-8'); const rewritten = rewriteMiniappBody(body, route.prefix); + if (!res.headersSent) { + res.removeHeader('Content-Encoding'); + } if (!res.headersSent) { res.setHeader('Content-Length', Buffer.byteLength(rewritten)); } @@ -180,12 +183,19 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { const originalUrl = req.url; const originalOriginalUrl = (req as IncomingMessage & { originalUrl?: string }).originalUrl; + const originalAcceptEncoding = req.headers['accept-encoding']; + req.headers['accept-encoding'] = 'identity'; const nextUrl = url.slice(route.prefix.length); req.url = nextUrl; (req as IncomingMessage & { originalUrl?: string }).originalUrl = nextUrl; route.miniapp.server.middlewares(req, res, (err) => { req.url = originalUrl; (req as IncomingMessage & { originalUrl?: string }).originalUrl = originalOriginalUrl; + if (originalAcceptEncoding) { + req.headers['accept-encoding'] = originalAcceptEncoding; + } else { + delete req.headers['accept-encoding']; + } if (err) { next(err); return; From 67df416665305b269227fa0b833a853511987d41 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 15:18:25 +0800 Subject: [PATCH 15/16] fix(dev): normalize miniapps proxy prefix --- scripts/vite-plugin-miniapps.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index 6a5e07d79..d0c9140d5 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -140,7 +140,6 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { const proxyRoutes: Array<{ prefix: string; miniapp: MiniappServer }> = []; for (const miniapp of miniappServers) { proxyRoutes.push({ prefix: `/miniapps/${miniapp.dirName}`, miniapp }); - proxyRoutes.push({ prefix: `/${miniapp.dirName}`, miniapp }); } const proxyMiddleware = (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => { const url = req.url ?? ''; @@ -185,7 +184,7 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { const originalOriginalUrl = (req as IncomingMessage & { originalUrl?: string }).originalUrl; const originalAcceptEncoding = req.headers['accept-encoding']; req.headers['accept-encoding'] = 'identity'; - const nextUrl = url.slice(route.prefix.length); + const nextUrl = url.slice(route.prefix.length) || '/'; req.url = nextUrl; (req as IncomingMessage & { originalUrl?: string }).originalUrl = nextUrl; route.miniapp.server.middlewares(req, res, (err) => { From a2be7893f5efdc113f68c41496be6810c174b131 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Wed, 28 Jan 2026 17:30:56 +0800 Subject: [PATCH 16/16] feat: complete implementation --- .gitignore | 1 + miniapps/forge/src/App.tsx | 41 ++- miniapps/forge/src/api/schemas.ts | 2 +- miniapps/forge/src/i18n/locales/en.json | 2 + miniapps/forge/src/i18n/locales/zh-CN.json | 2 + miniapps/forge/src/i18n/locales/zh-TW.json | 2 + miniapps/forge/src/i18n/locales/zh.json | 2 + .../src/components/AuroraBackground.tsx | 4 +- scripts/vite-plugin-miniapps.ts | 256 ++++++++++++++---- vite.config.ts | 1 + 10 files changed, 255 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index 412a4e087..56bf26e6b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist/ dists/ dist-web/ dist-dweb/ +public/miniapps/ tmp/ .tmp/ .chat/ diff --git a/miniapps/forge/src/App.tsx b/miniapps/forge/src/App.tsx index 5001a6f10..8c30046db 100644 --- a/miniapps/forge/src/App.tsx +++ b/miniapps/forge/src/App.tsx @@ -219,8 +219,35 @@ export default function App() { setRechargeStep('confirm'); }; + const externalDecimals = + typeof selectedOption?.externalInfo.decimals === 'number' && Number.isFinite(selectedOption.externalInfo.decimals) + ? selectedOption.externalInfo.decimals + : undefined; + const externalTokenAddress = selectedOption?.externalInfo.contract?.trim(); + const externalDecimalsFallback = externalTokenAddress + ? (() => { + switch (selectedOption?.externalChain) { + case 'ETH': + case 'BSC': + return 18; + case 'TRON': + return 6; + default: + return undefined; + } + })() + : undefined; + const resolvedExternalDecimals = externalDecimals ?? externalDecimalsFallback; + const usingFallbackDecimals = + Boolean(externalTokenAddress) && externalDecimals === undefined && resolvedExternalDecimals !== undefined; + const handleConfirm = useCallback(async () => { if (!externalAccount || !internalAccount || !selectedOption) return; + const tokenAddress = selectedOption.externalInfo.contract?.trim(); + if (tokenAddress && resolvedExternalDecimals === undefined) { + setError(t('error.missingDecimals')); + return; + } setError(null); setRechargeStep('processing'); @@ -228,16 +255,16 @@ export default function App() { await forgeHook.forge({ externalChain: selectedOption.externalChain, externalAsset: selectedOption.externalAsset, - externalDecimals: selectedOption.externalInfo.decimals, + externalDecimals: resolvedExternalDecimals, depositAddress: selectedOption.externalInfo.depositAddress, - externalContract: selectedOption.externalInfo.contract, + externalContract: tokenAddress, amount, externalAccount, internalChain: selectedOption.internalChain, internalAsset: selectedOption.internalAsset, internalAccount, }); - }, [externalAccount, internalAccount, selectedOption, amount, forgeHook]); + }, [externalAccount, internalAccount, selectedOption, amount, forgeHook, resolvedExternalDecimals, t]); const handleReset = useCallback(() => { setRechargeStep('swap'); @@ -257,7 +284,6 @@ export default function App() { return groups; }, [forgeOptions]); - const externalDecimals = selectedOption?.externalInfo.decimals; const contractAddress = selectedOption?.externalInfo.contract?.trim(); const contractDisplay = contractAddress ? `${contractAddress.slice(0, 6)}...${contractAddress.slice(-4)}` @@ -455,8 +481,11 @@ export default function App() {
{t('forge.amountHint', { symbol: selectedOption.externalAsset })}
- {typeof externalDecimals === 'number' && ( -
{t('forge.decimalsHint', { decimals: externalDecimals })}
+ {typeof resolvedExternalDecimals === 'number' && ( +
+ {t('forge.decimalsHint', { decimals: resolvedExternalDecimals })} + {usingFallbackDecimals ? ` (${t('forge.decimalsFallback')})` : null} +
)} {contractDisplay && (
diff --git a/miniapps/forge/src/api/schemas.ts b/miniapps/forge/src/api/schemas.ts index d7c580aba..66f9a48a3 100644 --- a/miniapps/forge/src/api/schemas.ts +++ b/miniapps/forge/src/api/schemas.ts @@ -12,7 +12,7 @@ const externalAssetInfoItemSchema = z.object({ depositAddress: z.string(), assetType: z.string(), logo: z.string().optional(), - decimals: z.number().optional(), + decimals: z.coerce.number().optional(), }).passthrough() const redemptionConfigSchema = z.object({ diff --git a/miniapps/forge/src/i18n/locales/en.json b/miniapps/forge/src/i18n/locales/en.json index 6e55b75bc..e4ab0299d 100644 --- a/miniapps/forge/src/i18n/locales/en.json +++ b/miniapps/forge/src/i18n/locales/en.json @@ -25,6 +25,7 @@ "continue": "Continue", "amountHint": "Amount is in {{symbol}}", "decimalsHint": "Token decimals: {{decimals}}", + "decimalsFallback": "default", "contractHint": "Contract: {{address}}" }, "redemption": { @@ -75,6 +76,7 @@ "sdkNotInit": "Bio SDK not initialized", "connectionFailed": "Connection failed", "invalidAmount": "Please enter a valid amount", + "missingDecimals": "Missing token decimals configuration", "forgeFailed": "Operation failed" }, "picker": { diff --git a/miniapps/forge/src/i18n/locales/zh-CN.json b/miniapps/forge/src/i18n/locales/zh-CN.json index aaa2789e3..3661545c8 100644 --- a/miniapps/forge/src/i18n/locales/zh-CN.json +++ b/miniapps/forge/src/i18n/locales/zh-CN.json @@ -25,6 +25,7 @@ "continue": "继续", "amountHint": "数量单位:{{symbol}}", "decimalsHint": "精度:{{decimals}} 位", + "decimalsFallback": "默认", "contractHint": "合约:{{address}}" }, "redemption": { @@ -75,6 +76,7 @@ "sdkNotInit": "Bio SDK 未初始化", "connectionFailed": "连接失败", "invalidAmount": "请输入有效金额", + "missingDecimals": "缺少资产精度配置", "forgeFailed": "操作失败" }, "picker": { diff --git a/miniapps/forge/src/i18n/locales/zh-TW.json b/miniapps/forge/src/i18n/locales/zh-TW.json index 744f5d7bc..b90475f4b 100644 --- a/miniapps/forge/src/i18n/locales/zh-TW.json +++ b/miniapps/forge/src/i18n/locales/zh-TW.json @@ -25,6 +25,7 @@ "continue": "繼續", "amountHint": "數量單位:{{symbol}}", "decimalsHint": "精度:{{decimals}} 位", + "decimalsFallback": "預設", "contractHint": "合約:{{address}}" }, "redemption": { @@ -75,6 +76,7 @@ "sdkNotInit": "Bio SDK 未初始化", "connectionFailed": "連接失敗", "invalidAmount": "請輸入有效金額", + "missingDecimals": "缺少資產精度配置", "forgeFailed": "操作失敗" }, "picker": { diff --git a/miniapps/forge/src/i18n/locales/zh.json b/miniapps/forge/src/i18n/locales/zh.json index aaa2789e3..3661545c8 100644 --- a/miniapps/forge/src/i18n/locales/zh.json +++ b/miniapps/forge/src/i18n/locales/zh.json @@ -25,6 +25,7 @@ "continue": "继续", "amountHint": "数量单位:{{symbol}}", "decimalsHint": "精度:{{decimals}} 位", + "decimalsFallback": "默认", "contractHint": "合约:{{address}}" }, "redemption": { @@ -75,6 +76,7 @@ "sdkNotInit": "Bio SDK 未初始化", "connectionFailed": "连接失败", "invalidAmount": "请输入有效金额", + "missingDecimals": "缺少资产精度配置", "forgeFailed": "操作失败" }, "picker": { diff --git a/miniapps/teleport/src/components/AuroraBackground.tsx b/miniapps/teleport/src/components/AuroraBackground.tsx index f12be2902..b7caa3715 100644 --- a/miniapps/teleport/src/components/AuroraBackground.tsx +++ b/miniapps/teleport/src/components/AuroraBackground.tsx @@ -14,7 +14,7 @@ export const AuroraBackground = ({ ...props }: AuroraBackgroundProps) => { return ( -
+
{children}
-
+
); }; diff --git a/scripts/vite-plugin-miniapps.ts b/scripts/vite-plugin-miniapps.ts index d0c9140d5..28d81e83c 100644 --- a/scripts/vite-plugin-miniapps.ts +++ b/scripts/vite-plugin-miniapps.ts @@ -12,6 +12,8 @@ import { resolve, join, basename } from 'node:path'; import { readdirSync, existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'node:fs'; import detectPort from 'detect-port'; import https from 'node:https'; +import type { RollupWatcher } from 'rollup'; +import sirv from 'sirv'; import { getRemoteMiniappsForEcosystem, type RemoteMiniappConfig } from './vite-plugin-remote-miniapps'; import type { WujieRuntimeConfig } from '../src/services/ecosystem/types'; @@ -24,6 +26,8 @@ interface MiniappRuntimeConfig { build?: MiniappRuntime | { runtime: MiniappRuntime; wujieConfig?: WujieRuntimeConfig }; } +type MiniappDevMode = 'proxy' | 'build'; + function parseRuntimeConfig(config?: MiniappRuntime | { runtime: MiniappRuntime; wujieConfig?: WujieRuntimeConfig }): { runtime: MiniappRuntime; wujieConfig?: WujieRuntimeConfig; @@ -76,16 +80,21 @@ interface MiniappsPluginOptions { miniappsDir?: string; apps?: Record; remoteMiniapps?: RemoteMiniappConfig[]; + devMode?: MiniappDevMode; } // ==================== Plugin ==================== export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { - const { miniappsDir = 'miniapps', apps = {}, remoteMiniapps = [] } = options; + const { miniappsDir = 'miniapps', apps = {}, remoteMiniapps = [], devMode } = options; + const resolvedDevMode: MiniappDevMode = + devMode ?? (process.env.MINIAPPS_DEV_MODE === 'build' ? 'build' : 'proxy'); let root: string; let isBuild = false; const miniappServers: MiniappServer[] = []; + const miniappBuildWatchers: RollupWatcher[] = []; + let miniappsDevDistDir: string | null = null; return { name: 'vite-plugin-miniapps', @@ -112,6 +121,85 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { const miniappsPath = resolve(root, miniappsDir); const manifests = scanMiniapps(miniappsPath); + if (resolvedDevMode === 'build') { + miniappsDevDistDir = resolve(root, 'public', 'miniapps'); + mkdirSync(miniappsDevDistDir, { recursive: true }); + + await Promise.all( + manifests.map(async (manifest) => { + const miniappPath = join(miniappsPath, manifest.dirName); + const outputDir = resolve(miniappsDevDistDir!, manifest.dirName); + const result = await viteBuild({ + root: miniappPath, + configFile: join(miniappPath, 'vite.config.ts'), + base: './', + build: { + outDir: outputDir, + emptyOutDir: true, + watch: {}, + minify: false, + sourcemap: true, + }, + logLevel: 'warn', + }); + + if (result && typeof (result as RollupWatcher).on === 'function') { + miniappBuildWatchers.push(result as RollupWatcher); + } + + console.log(`[miniapps] ${manifest.name} built (watch mode)`); + }), + ); + + server.middlewares.use( + '/miniapps', + sirv(miniappsDevDistDir, { + dev: true, + single: false, + }), + ); + + const localApps = manifests.map((manifest) => { + const appConfig = apps[manifest.id]; + const { runtime, wujieConfig } = parseRuntimeConfig(appConfig?.server); + return { + ...manifest, + dirName: manifest.dirName, + icon: resolveMiniappDevAsset(manifest.icon, manifest.dirName), + url: `/miniapps/${manifest.dirName}/`, + screenshots: manifest.screenshots.map((sc) => resolveMiniappDevAsset(sc, manifest.dirName)), + runtime, + wujieConfig, + }; + }); + + const remoteApps = getRemoteMiniappsForEcosystem(); + const ecosystemData: EcosystemJson = { + name: 'Bio 官方生态', + version: '1.0.0', + updated: new Date().toISOString().split('T')[0], + icon: '/logos/logo-256.webp', + apps: [...localApps, ...remoteApps], + }; + const ecosystemCache = JSON.stringify(ecosystemData, null, 2); + + 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; + } + next(); + }); + + const cleanup = async () => { + await Promise.all(miniappBuildWatchers.map((watcher) => watcher.close())); + }; + server.httpServer?.on('close', cleanup); + return; + } + // 预分配端口 const portAssignments: Array<{ manifest: MiniappManifest; port: number }> = []; for (const manifest of manifests) { @@ -153,55 +241,8 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { if (!url.startsWith(`${route.prefix}/`)) { continue; } - - const rewriteMode = getRewriteMode(url, req); - if (rewriteMode) { - const originalEnd = res.end.bind(res); - const chunks: Buffer[] = []; - - res.write = ((chunk: Buffer | string) => { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - return true; - }) as typeof res.write; - - res.end = ((chunk?: Buffer | string) => { - if (chunk) { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - } - const body = Buffer.concat(chunks).toString('utf-8'); - const rewritten = rewriteMiniappBody(body, route.prefix); - if (!res.headersSent) { - res.removeHeader('Content-Encoding'); - } - if (!res.headersSent) { - res.setHeader('Content-Length', Buffer.byteLength(rewritten)); - } - return originalEnd(rewritten); - }) as typeof res.end; - } - - const originalUrl = req.url; - const originalOriginalUrl = (req as IncomingMessage & { originalUrl?: string }).originalUrl; - const originalAcceptEncoding = req.headers['accept-encoding']; - req.headers['accept-encoding'] = 'identity'; const nextUrl = url.slice(route.prefix.length) || '/'; - req.url = nextUrl; - (req as IncomingMessage & { originalUrl?: string }).originalUrl = nextUrl; - route.miniapp.server.middlewares(req, res, (err) => { - req.url = originalUrl; - (req as IncomingMessage & { originalUrl?: string }).originalUrl = originalOriginalUrl; - if (originalAcceptEncoding) { - req.headers['accept-encoding'] = originalAcceptEncoding; - } else { - delete req.headers['accept-encoding']; - } - if (err) { - next(err); - return; - } - if (res.writableEnded || res.headersSent) return; - next(); - }); + void proxyMiniappRequest(req, res, route, nextUrl).catch(next); return; } next(); @@ -269,6 +310,7 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin { async closeBundle() { await Promise.all(miniappServers.map((s) => s.server.close())); + await Promise.all(miniappBuildWatchers.map((watcher) => watcher.close())); }, }; } @@ -398,6 +440,122 @@ function rewriteMiniappBody(body: string, prefix: string): string { return output; } +async function proxyMiniappRequest( + req: IncomingMessage, + res: ServerResponse, + route: { prefix: string; miniapp: MiniappServer }, + nextUrl: string, +): Promise { + const rewriteMode = getRewriteMode(`${route.prefix}${nextUrl}`, req); + const body = await readRequestBody(req); + const headers = buildProxyHeaders(req, body, rewriteMode !== null); + + await new Promise((resolve, reject) => { + const proxyReq = https.request( + { + hostname: 'localhost', + port: route.miniapp.port, + path: nextUrl, + method: req.method, + headers, + rejectUnauthorized: false, + }, + (proxyRes) => { + res.statusCode = proxyRes.statusCode ?? 502; + + const outgoingHeaders = normalizeProxyHeaders(proxyRes.headers, rewriteMode !== null); + for (const [key, value] of Object.entries(outgoingHeaders)) { + if (typeof value !== 'undefined') { + res.setHeader(key, value); + } + } + + if (!rewriteMode) { + proxyRes.pipe(res); + proxyRes.on('end', resolve); + proxyRes.on('error', reject); + return; + } + + const chunks: Buffer[] = []; + proxyRes.on('data', (chunk) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + proxyRes.on('end', () => { + const rawBody = Buffer.concat(chunks).toString('utf-8'); + const rewritten = rewriteMiniappBody(rawBody, route.prefix); + res.setHeader('Content-Length', Buffer.byteLength(rewritten)); + res.end(rewritten); + resolve(); + }); + proxyRes.on('error', reject); + }, + ); + + proxyReq.on('error', reject); + + if (body) { + proxyReq.write(body); + } + proxyReq.end(); + }); +} + +async function readRequestBody(req: IncomingMessage): Promise { + const method = req.method?.toUpperCase(); + if (!method || method === 'GET' || method === 'HEAD' || method === 'OPTIONS') { + return null; + } + + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + if (chunks.length === 0) return null; + return Buffer.concat(chunks); +} + +function buildProxyHeaders( + req: IncomingMessage, + body: Buffer | null, + disableCompression: boolean, +): Record { + const headers: Record = {}; + for (const [key, value] of Object.entries(req.headers)) { + if (key.startsWith(':')) continue; + if (key === 'host') continue; + if (key === 'connection') continue; + if (key === 'content-length') continue; + if (key === 'transfer-encoding') continue; + if (key === 'accept-encoding' && disableCompression) continue; + if (typeof value === 'undefined') continue; + headers[key] = Array.isArray(value) ? value.join(', ') : value; + } + + if (disableCompression) { + headers['accept-encoding'] = 'identity'; + } + if (body) { + headers['content-length'] = String(body.length); + } + return headers; +} + +function normalizeProxyHeaders( + headers: IncomingMessage['headers'], + stripEncoding: boolean, +): Record { + const output: Record = {}; + for (const [key, value] of Object.entries(headers)) { + if (typeof value === 'undefined') continue; + if (stripEncoding && (key === 'content-encoding' || key === 'content-length' || key === 'transfer-encoding')) { + continue; + } + output[key] = value; + } + return output; +} + function prependMiddleware( server: ViteDevServer, middleware: (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void, diff --git a/vite.config.ts b/vite.config.ts index 41a48e5b6..0f52c2f19 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -159,6 +159,7 @@ export default defineConfig(({ mode }) => { retries: 3, }), miniappsPlugin({ + devMode: 'build', apps: { 'xin.dweb.teleport': { server: 'iframe',