diff --git a/scripts/vite-plugin-remote-miniapps.ts b/scripts/vite-plugin-remote-miniapps.ts
index b3891fdf7..79737c06e 100644
--- a/scripts/vite-plugin-remote-miniapps.ts
+++ b/scripts/vite-plugin-remote-miniapps.ts
@@ -8,168 +8,193 @@
* 使用 fetchWithEtag 实现基于 ETag 的缓存
*/
-import { type Plugin } from 'vite'
-import { resolve, join } from 'node:path'
-import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync } from 'node:fs'
-import { createServer } from 'node:http'
-import type JSZipType from 'jszip'
-import { fetchWithEtag, type FetchWithEtagOptions } from './utils/fetch-with-etag'
+import { type Plugin } from 'vite';
+import { resolve, join } from 'node:path';
+import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync } from 'node:fs';
+import { createServer } from 'node:http';
+import type JSZipType from 'jszip';
+import { fetchWithEtag, type FetchWithEtagOptions } from './utils/fetch-with-etag';
// ==================== Types ====================
+type MiniappRuntime = 'iframe' | 'wujie';
+
+interface MiniappServerConfig {
+ runtime?: MiniappRuntime;
+}
+
+interface MiniappBuildConfig {
+ runtime?: MiniappRuntime;
+ /**
+ * 重写 index.html 的 标签
+ * - true: 自动推断为 '/miniapps/{dirName}/'
+ * - string: 自定义路径
+ * - undefined/false: 不重写
+ */
+ rewriteBase?: boolean | string;
+}
+
interface RemoteMiniappConfig {
- metadataUrl: string
- dirName: string
+ metadataUrl: string;
+ dirName: string;
+ server?: MiniappServerConfig;
+ build?: MiniappBuildConfig;
}
interface RemoteMetadata {
- id: string
- name: string
- version: string
- zipUrl: string
- manifestUrl: string
- updatedAt: string
+ id: string;
+ name: string;
+ version: string;
+ zipUrl: string;
+ manifestUrl: string;
+ updatedAt: string;
}
interface RemoteMiniappsPluginOptions {
- miniapps: RemoteMiniappConfig[]
- miniappsDir?: string
- timeout?: number
- retries?: number
+ miniapps: RemoteMiniappConfig[];
+ miniappsDir?: string;
+ timeout?: number;
+ retries?: number;
}
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 RemoteMiniappServer {
- id: string
- dirName: string
- port: number
- server: ReturnType
- baseUrl: string
- manifest: MiniappManifest
+ id: string;
+ dirName: string;
+ port: number;
+ server: ReturnType;
+ baseUrl: string;
+ manifest: MiniappManifest;
}
// ==================== Plugin ====================
export function remoteMiniappsPlugin(options: RemoteMiniappsPluginOptions): Plugin {
- const { miniapps, miniappsDir = 'miniapps', timeout = 60000, retries = 3 } = options
- const fetchOptions: FetchWithEtagOptions = { timeout, retries }
+ const { miniapps, miniappsDir = 'miniapps', timeout = 60000, retries = 3 } = options;
+ const fetchOptions: FetchWithEtagOptions = { timeout, retries };
- let root: string
- let isBuild = false
- const servers: RemoteMiniappServer[] = []
- const downloadFailures: string[] = []
+ let root: string;
+ let isBuild = false;
+ const servers: RemoteMiniappServer[] = [];
+ const downloadFailures: string[] = [];
return {
name: 'vite-plugin-remote-miniapps',
configResolved(config) {
- root = config.root
- isBuild = config.command === 'build'
+ root = config.root;
+ isBuild = config.command === 'build';
},
async buildStart() {
- if (miniapps.length === 0) return
+ if (miniapps.length === 0) return;
- const miniappsPath = resolve(root, miniappsDir)
+ const miniappsPath = resolve(root, miniappsDir);
for (const config of miniapps) {
try {
- await downloadAndExtract(config, miniappsPath, fetchOptions)
+ await downloadAndExtract(config, miniappsPath, fetchOptions);
} catch (err) {
- const errorMsg = err instanceof Error ? err.message : String(err)
- console.error(`[remote-miniapps] ❌ Failed to download ${config.dirName}: ${errorMsg}`)
- downloadFailures.push(config.dirName)
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ console.error(`[remote-miniapps] ❌ Failed to download ${config.dirName}: ${errorMsg}`);
+ downloadFailures.push(config.dirName);
}
}
if (downloadFailures.length > 0 && isBuild) {
throw new Error(
`[remote-miniapps] Build aborted: failed to download remote miniapps: ${downloadFailures.join(', ')}. ` +
- `Check network connectivity to remote servers.`
- )
+ `Check network connectivity to remote servers.`,
+ );
}
},
async writeBundle(outputOptions) {
- if (!isBuild || !outputOptions.dir) return
+ if (!isBuild || !outputOptions.dir) return;
- const miniappsPath = resolve(root, miniappsDir)
- const miniappsOutputDir = resolve(outputOptions.dir, 'miniapps')
- const missing: string[] = []
+ const miniappsPath = resolve(root, miniappsDir);
+ const miniappsOutputDir = resolve(outputOptions.dir, 'miniapps');
+ const missing: string[] = [];
for (const config of miniapps) {
- const srcDir = join(miniappsPath, config.dirName)
- const destDir = join(miniappsOutputDir, config.dirName)
+ const srcDir = join(miniappsPath, config.dirName);
+ const destDir = join(miniappsOutputDir, config.dirName);
if (existsSync(srcDir)) {
- mkdirSync(destDir, { recursive: true })
- cpSync(srcDir, destDir, { recursive: true })
- console.log(`[remote-miniapps] ✅ Copied ${config.dirName} to dist`)
+ mkdirSync(destDir, { recursive: true });
+ cpSync(srcDir, destDir, { recursive: true });
+ console.log(`[remote-miniapps] ✅ Copied ${config.dirName} to dist`);
+
+ if (config.build?.rewriteBase) {
+ const basePath =
+ typeof config.build.rewriteBase === 'string' ? config.build.rewriteBase : `/miniapps/${config.dirName}/`;
+ rewriteHtmlBase(destDir, basePath);
+ }
} else {
- missing.push(config.dirName)
+ missing.push(config.dirName);
}
}
if (missing.length > 0) {
throw new Error(
`[remote-miniapps] Build failed: missing miniapps in output: ${missing.join(', ')}. ` +
- `Remote miniapps were not downloaded successfully.`
- )
+ `Remote miniapps were not downloaded successfully.`,
+ );
}
},
async configureServer(server) {
- if (miniapps.length === 0) return
+ if (miniapps.length === 0) return;
- const miniappsPath = resolve(root, miniappsDir)
+ const miniappsPath = resolve(root, miniappsDir);
for (const config of miniapps) {
try {
- await downloadAndExtract(config, miniappsPath, fetchOptions)
+ await downloadAndExtract(config, miniappsPath, fetchOptions);
} catch (err) {
- const errorMsg = err instanceof Error ? err.message : String(err)
- console.warn(`[remote-miniapps] ⚠️ Failed to download ${config.dirName} (dev mode): ${errorMsg}`)
- continue
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ console.warn(`[remote-miniapps] ⚠️ Failed to download ${config.dirName} (dev mode): ${errorMsg}`);
+ continue;
}
}
// 启动静态服务器为每个远程 miniapp
for (const config of miniapps) {
- const miniappDir = join(miniappsPath, config.dirName)
- const manifestPath = join(miniappDir, 'manifest.json')
+ const miniappDir = join(miniappsPath, config.dirName);
+ const manifestPath = join(miniappDir, 'manifest.json');
if (!existsSync(manifestPath)) {
- console.warn(`[remote-miniapps] ${config.dirName}: manifest.json not found, skipping`)
- continue
+ console.warn(`[remote-miniapps] ${config.dirName}: manifest.json not found, skipping`);
+ continue;
}
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as MiniappManifest
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as MiniappManifest;
// 启动静态服务器
- const { server: httpServer, port } = await startStaticServer(miniappDir)
- const baseUrl = `http://localhost:${port}`
+ const { server: httpServer, port } = await startStaticServer(miniappDir);
+ const baseUrl = `http://localhost:${port}`;
const serverInfo: RemoteMiniappServer = {
id: manifest.id,
@@ -178,31 +203,31 @@ export function remoteMiniappsPlugin(options: RemoteMiniappsPluginOptions): Plug
server: httpServer,
baseUrl,
manifest,
- }
+ };
- servers.push(serverInfo)
- globalRemoteServers.push(serverInfo)
+ servers.push(serverInfo);
+ globalRemoteServers.push(serverInfo);
- console.log(`[remote-miniapps] ${manifest.name} (${manifest.id}) serving at ${baseUrl}`)
+ console.log(`[remote-miniapps] ${manifest.name} (${manifest.id}) serving at ${baseUrl}`);
}
// 清理服务器
const cleanup = async () => {
for (const s of servers) {
- await new Promise((resolve) => s.server.close(() => resolve()))
+ await new Promise((resolve) => s.server.close(() => resolve()));
}
- }
+ };
- server.httpServer?.on('close', cleanup)
+ server.httpServer?.on('close', cleanup);
},
async closeBundle() {
// 关闭所有静态服务器
for (const s of servers) {
- await new Promise((resolve) => s.server.close(() => resolve()))
+ await new Promise((resolve) => s.server.close(() => resolve()));
}
},
- }
+ };
}
// ==================== Helpers ====================
@@ -210,130 +235,150 @@ export function remoteMiniappsPlugin(options: RemoteMiniappsPluginOptions): Plug
async function downloadAndExtract(
config: RemoteMiniappConfig,
miniappsPath: string,
- fetchOptions: FetchWithEtagOptions = {}
+ fetchOptions: FetchWithEtagOptions = {},
): Promise {
- const targetDir = join(miniappsPath, config.dirName)
+ const targetDir = join(miniappsPath, config.dirName);
- console.log(`[remote-miniapps] Syncing ${config.dirName}...`)
+ console.log(`[remote-miniapps] Syncing ${config.dirName}...`);
- const metadataBuffer = await fetchWithEtag(config.metadataUrl, fetchOptions)
- const metadata = JSON.parse(metadataBuffer.toString('utf-8')) as RemoteMetadata
+ const metadataBuffer = await fetchWithEtag(config.metadataUrl, fetchOptions);
+ const metadata = JSON.parse(metadataBuffer.toString('utf-8')) as RemoteMetadata;
- const localManifestPath = join(targetDir, 'manifest.json')
+ const localManifestPath = join(targetDir, 'manifest.json');
if (existsSync(localManifestPath)) {
- const localManifest = JSON.parse(readFileSync(localManifestPath, 'utf-8')) as MiniappManifest & { _zipEtag?: string }
+ const localManifest = JSON.parse(readFileSync(localManifestPath, 'utf-8')) as MiniappManifest & {
+ _zipEtag?: string;
+ };
if (localManifest.version === metadata.version && localManifest._zipEtag) {
- const baseUrl = config.metadataUrl.replace(/\/[^/]+$/, '')
- const zipUrl = metadata.zipUrl.startsWith('.')
- ? `${baseUrl}/${metadata.zipUrl.slice(2)}`
- : metadata.zipUrl
+ const baseUrl = config.metadataUrl.replace(/\/[^/]+$/, '');
+ const zipUrl = metadata.zipUrl.startsWith('.') ? `${baseUrl}/${metadata.zipUrl.slice(2)}` : metadata.zipUrl;
try {
- const headResponse = await fetch(zipUrl, { method: 'HEAD' })
- const remoteEtag = headResponse.headers.get('etag') || ''
+ const headResponse = await fetch(zipUrl, { method: 'HEAD' });
+ const remoteEtag = headResponse.headers.get('etag') || '';
if (remoteEtag === localManifest._zipEtag) {
- console.log(`[remote-miniapps] ${config.dirName} is up-to-date (v${metadata.version}, etag match)`)
- return
+ console.log(`[remote-miniapps] ${config.dirName} is up-to-date (v${metadata.version}, etag match)`);
+ return;
}
- console.log(`[remote-miniapps] ${config.dirName} zip changed (etag: ${localManifest._zipEtag} -> ${remoteEtag})`)
+ console.log(
+ `[remote-miniapps] ${config.dirName} zip changed (etag: ${localManifest._zipEtag} -> ${remoteEtag})`,
+ );
} catch {
// HEAD request failed, continue with download
}
}
}
- const baseUrl = config.metadataUrl.replace(/\/[^/]+$/, '')
+ const baseUrl = config.metadataUrl.replace(/\/[^/]+$/, '');
const manifestUrl = metadata.manifestUrl.startsWith('.')
? `${baseUrl}/${metadata.manifestUrl.slice(2)}`
- : metadata.manifestUrl
- const zipUrl = metadata.zipUrl.startsWith('.')
- ? `${baseUrl}/${metadata.zipUrl.slice(2)}`
- : metadata.zipUrl
+ : metadata.manifestUrl;
+ const zipUrl = metadata.zipUrl.startsWith('.') ? `${baseUrl}/${metadata.zipUrl.slice(2)}` : metadata.zipUrl;
- const manifestBuffer = await fetchWithEtag(manifestUrl, fetchOptions)
- const manifest = JSON.parse(manifestBuffer.toString('utf-8')) as MiniappManifest
+ const manifestBuffer = await fetchWithEtag(manifestUrl, fetchOptions);
+ const manifest = JSON.parse(manifestBuffer.toString('utf-8')) as MiniappManifest;
- const zipHeadResponse = await fetch(zipUrl, { method: 'HEAD' })
- const zipEtag = zipHeadResponse.headers.get('etag') || ''
- const zipBuffer = await fetchWithEtag(zipUrl, fetchOptions)
+ const zipHeadResponse = await fetch(zipUrl, { method: 'HEAD' });
+ const zipEtag = zipHeadResponse.headers.get('etag') || '';
+ const zipBuffer = await fetchWithEtag(zipUrl, fetchOptions);
if (existsSync(targetDir)) {
- rmSync(targetDir, { recursive: true })
+ rmSync(targetDir, { recursive: true });
}
- mkdirSync(targetDir, { recursive: true })
-
- const JSZip = (await import('jszip')).default
- const zip = await JSZip.loadAsync(zipBuffer)
- for (const [relativePath, file] of Object.entries(zip.files) as [
- string,
- JSZipType.JSZipObject,
- ][]) {
+ mkdirSync(targetDir, { recursive: true });
+
+ const JSZip = (await import('jszip')).default;
+ const zip = await JSZip.loadAsync(zipBuffer);
+ for (const [relativePath, file] of Object.entries(zip.files) as [string, JSZipType.JSZipObject][]) {
if (file.dir) {
- mkdirSync(join(targetDir, relativePath), { recursive: true })
+ mkdirSync(join(targetDir, relativePath), { recursive: true });
} else {
- const content = await file.async('nodebuffer')
- const filePath = join(targetDir, relativePath)
- mkdirSync(join(targetDir, relativePath, '..'), { recursive: true })
- writeFileSync(filePath, content)
+ const content = await file.async('nodebuffer');
+ const filePath = join(targetDir, relativePath);
+ mkdirSync(join(targetDir, relativePath, '..'), { recursive: true });
+ writeFileSync(filePath, content);
}
}
- const manifestWithDir = { ...manifest, dirName: config.dirName, _zipEtag: zipEtag }
- writeFileSync(localManifestPath, JSON.stringify(manifestWithDir, null, 2))
+ const manifestWithDir = { ...manifest, dirName: config.dirName, _zipEtag: zipEtag };
+ writeFileSync(localManifestPath, JSON.stringify(manifestWithDir, null, 2));
+
+ console.log(`[remote-miniapps] ${config.dirName} updated to v${manifest.version} (etag: ${zipEtag})`);
+}
+
+function rewriteHtmlBase(targetDir: string, basePath: string): void {
+ const indexPath = join(targetDir, 'index.html');
+ if (!existsSync(indexPath)) {
+ console.warn(`[remote-miniapps] index.html not found in ${targetDir}, skipping base rewrite`);
+ return;
+ }
+
+ let html = readFileSync(indexPath, 'utf-8');
+ html = html.replace(/]*>/gi, '');
+
+ const normalizedBase = basePath.endsWith('/') ? basePath : `${basePath}/`;
+ const baseTag = ``;
+
+ if (html.includes('')) {
+ html = html.replace(//i, `\n ${baseTag}`);
+ } else if (html.includes('')) {
+ html = html.replace(//i, `\n ${baseTag}`);
+ } else {
+ html = html.replace(/]*>/i, `$&\n \n ${baseTag}\n `);
+ }
- console.log(`[remote-miniapps] ${config.dirName} updated to v${manifest.version} (etag: ${zipEtag})`)
+ writeFileSync(indexPath, html);
+ console.log(`[remote-miniapps] Rewrote to "${normalizedBase}" in ${indexPath}`);
}
/**
* 启动简单的静态文件服务器
*/
-async function startStaticServer(
- root: string
-): Promise<{ server: ReturnType; port: number }> {
- const sirv = (await import('sirv')).default
- const handler = sirv(root, { dev: true, single: true })
+async function startStaticServer(root: string): Promise<{ server: ReturnType; port: number }> {
+ const sirv = (await import('sirv')).default;
+ const handler = sirv(root, { dev: true, single: true });
return new Promise((resolve, reject) => {
const server = createServer((req, res) => {
// CORS headers
- res.setHeader('Access-Control-Allow-Origin', '*')
- res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
- res.writeHead(204)
- res.end()
- return
+ res.writeHead(204);
+ res.end();
+ return;
}
handler(req, res, () => {
- res.writeHead(404)
- res.end('Not found')
- })
- })
+ res.writeHead(404);
+ res.end('Not found');
+ });
+ });
server.listen(0, () => {
- const address = server.address()
+ const address = server.address();
if (address && typeof address === 'object') {
- resolve({ server, port: address.port })
+ resolve({ server, port: address.port });
} else {
- reject(new Error('Failed to get server address'))
+ reject(new Error('Failed to get server address'));
}
- })
+ });
- server.on('error', reject)
- })
+ server.on('error', reject);
+ });
}
// ==================== 共享状态 ====================
/** 全局注册的远程 miniapp 服务器 */
-const globalRemoteServers: RemoteMiniappServer[] = []
+const globalRemoteServers: RemoteMiniappServer[] = [];
/**
* 获取远程 miniapps 的服务器信息 (供 ecosystem.json 生成使用)
*/
export function getRemoteMiniappServers(): RemoteMiniappServer[] {
- return [...globalRemoteServers]
+ return [...globalRemoteServers];
}
/**
@@ -346,7 +391,7 @@ export function getRemoteMiniappsForEcosystem(): Array new URL(sc, s.baseUrl).href) ?? [],
- }))
+ }));
}
-export default remoteMiniappsPlugin
+export default remoteMiniappsPlugin;
diff --git a/vite.config.ts b/vite.config.ts
index 774538b41..f317b4822 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,43 +1,43 @@
-import { defineConfig, loadEnv } from 'vite'
-import react from '@vitejs/plugin-react'
-import tailwindcss from '@tailwindcss/vite'
-import commonjs from 'vite-plugin-commonjs'
-import mkcert from 'vite-plugin-mkcert'
-import { networkInterfaces } from 'node:os'
-import { resolve } from 'node:path'
-import { mockDevToolsPlugin } from './scripts/vite-plugin-mock-devtools'
-import { miniappsPlugin } from './scripts/vite-plugin-miniapps'
-import { remoteMiniappsPlugin } from './scripts/vite-plugin-remote-miniapps'
-import { buildCheckPlugin } from './scripts/vite-plugin-build-check'
+import { defineConfig, loadEnv } from 'vite';
+import react from '@vitejs/plugin-react';
+import tailwindcss from '@tailwindcss/vite';
+import commonjs from 'vite-plugin-commonjs';
+import mkcert from 'vite-plugin-mkcert';
+import { networkInterfaces } from 'node:os';
+import { resolve } from 'node:path';
+import { mockDevToolsPlugin } from './scripts/vite-plugin-mock-devtools';
+import { miniappsPlugin } from './scripts/vite-plugin-miniapps';
+import { remoteMiniappsPlugin } from './scripts/vite-plugin-remote-miniapps';
+import { buildCheckPlugin } from './scripts/vite-plugin-build-check';
function getPreferredLanIPv4(): string | undefined {
- const ifaces = networkInterfaces()
- const ips: string[] = []
+ const ifaces = networkInterfaces();
+ const ips: string[] = [];
for (const entries of Object.values(ifaces)) {
for (const entry of entries ?? []) {
- if (entry.family !== 'IPv4' || entry.internal) continue
- const ip = entry.address
+ if (entry.family !== 'IPv4' || entry.internal) continue;
+ const ip = entry.address;
// Filter special/reserved ranges that confuse mobile debugging.
- if (ip.startsWith('127.') || ip.startsWith('169.254.') || ip.startsWith('198.18.')) continue
- if (ip === '0.0.0.0') continue
- ips.push(ip)
+ if (ip.startsWith('127.') || ip.startsWith('169.254.') || ip.startsWith('198.18.')) continue;
+ if (ip === '0.0.0.0') continue;
+ ips.push(ip);
}
}
const score = (ip: string) => {
- if (ip.startsWith('192.168.')) return 3
- if (ip.startsWith('10.')) return 2
- if (/^172\.(1[6-9]|2\\d|3[0-1])\\./.test(ip)) return 1
- return 0
- }
+ if (ip.startsWith('192.168.')) return 3;
+ if (ip.startsWith('10.')) return 2;
+ if (/^172\.(1[6-9]|2\\d|3[0-1])\\./.test(ip)) return 1;
+ return 0;
+ };
- ips.sort((a, b) => score(b) - score(a))
- return ips[0]
+ ips.sort((a, b) => score(b) - score(a));
+ return ips[0];
}
export default defineConfig(({ mode }) => {
- const env = loadEnv(mode, process.cwd(), '')
+ const env = loadEnv(mode, process.cwd(), '');
/**
* 服务实现选择(编译时)
@@ -45,156 +45,162 @@ export default defineConfig(({ mode }) => {
* - dweb: DWEB/Plaoc 平台
* - mock: 测试环境
*/
- const SERVICE_IMPL = env.SERVICE_IMPL ?? process.env.SERVICE_IMPL ?? 'web'
+ const SERVICE_IMPL = env.SERVICE_IMPL ?? process.env.SERVICE_IMPL ?? 'web';
/**
* Base URL 配置
* - 使用 './' 允许部署在任意子路径下
* - 例如: https://example.com/ 或 https://example.com/app/
*/
- const BASE_URL = env.VITE_BASE_URL ?? process.env.VITE_BASE_URL ?? './'
+ const BASE_URL = env.VITE_BASE_URL ?? process.env.VITE_BASE_URL ?? './';
- const DEV_HOST = env.VITE_DEV_HOST ?? process.env.VITE_DEV_HOST ?? getPreferredLanIPv4()
+ const DEV_HOST = env.VITE_DEV_HOST ?? process.env.VITE_DEV_HOST ?? getPreferredLanIPv4();
- const tronGridApiKey = env.TRONGRID_API_KEY ?? process.env.TRONGRID_API_KEY ?? ''
- const etherscanApiKey = env.ETHERSCAN_API_KEY ?? process.env.ETHERSCAN_API_KEY ?? ''
+ const tronGridApiKey = env.TRONGRID_API_KEY ?? process.env.TRONGRID_API_KEY ?? '';
+ const etherscanApiKey = env.ETHERSCAN_API_KEY ?? process.env.ETHERSCAN_API_KEY ?? '';
return {
- base: BASE_URL,
- server: {
- host: true,
- // 手机上的“每隔几秒自动刷新”通常是 HMR WebSocket 连不上导致的。
- // 明确指定 wss + 局域网 IP,避免客户端默认连到 localhost(在手机上等于连自己)。
- hmr: DEV_HOST
- ? {
- protocol: 'wss',
- host: DEV_HOST,
- }
- : undefined,
- },
- plugins: [
- mkcert({
- // 默认 hosts 会包含 0.0.0.0 / 某些保留网段,iOS 上偶发会导致 wss 不稳定。
- // 这里收敛到“确切可访问”的 host 列表,减少证书/SAN 干扰。
- hosts: DEV_HOST ? ['localhost', '127.0.0.1', DEV_HOST] : undefined,
- }),
- commonjs({
- filter(id) {
- // Transform .cjs files to ESM
- if (id.includes('.cjs')) {
- console.log('[commonjs] transforming:', id)
- return true
- }
- return false
- }
- }),
- react(),
- tailwindcss(),
- mockDevToolsPlugin(),
- // 远程 miniapps (必须在 miniappsPlugin 之前,以便注册到全局状态)
- remoteMiniappsPlugin({
- miniapps: [
- {
- metadataUrl: 'https://iweb.xin/rwahub.bfmeta.com.miniapp/metadata.json',
- dirName: 'rwa-hub',
+ base: BASE_URL,
+ server: {
+ host: true,
+ // 手机上的“每隔几秒自动刷新”通常是 HMR WebSocket 连不上导致的。
+ // 明确指定 wss + 局域网 IP,避免客户端默认连到 localhost(在手机上等于连自己)。
+ hmr: DEV_HOST
+ ? {
+ protocol: 'wss',
+ host: DEV_HOST,
+ }
+ : undefined,
+ },
+ plugins: [
+ mkcert({
+ // 默认 hosts 会包含 0.0.0.0 / 某些保留网段,iOS 上偶发会导致 wss 不稳定。
+ // 这里收敛到“确切可访问”的 host 列表,减少证书/SAN 干扰。
+ hosts: DEV_HOST ? ['localhost', '127.0.0.1', DEV_HOST] : undefined,
+ }),
+ commonjs({
+ filter(id) {
+ // Transform .cjs files to ESM
+ if (id.includes('.cjs')) {
+ console.log('[commonjs] transforming:', id);
+ return true;
+ }
+ return false;
},
- ],
- timeout: 60000,
- retries: 3,
- }),
- miniappsPlugin(),
- buildCheckPlugin(),
- ],
- resolve: {
- alias: {
- '@': resolve(__dirname, './src'),
-
- // ==================== Platform Services (编译时替换) ====================
- // 每个服务独立文件夹,通过 SERVICE_IMPL 环境变量选择实现
- '#biometric-impl': resolve(__dirname, `./src/services/biometric/${SERVICE_IMPL}.ts`),
- '#clipboard-impl': resolve(__dirname, `./src/services/clipboard/${SERVICE_IMPL}.ts`),
- '#toast-impl': resolve(__dirname, `./src/services/toast/${SERVICE_IMPL}.ts`),
- '#haptics-impl': resolve(__dirname, `./src/services/haptics/${SERVICE_IMPL}.ts`),
- '#storage-impl': resolve(__dirname, `./src/services/storage/${SERVICE_IMPL}.ts`),
- '#camera-impl': resolve(__dirname, `./src/services/camera/${SERVICE_IMPL}.ts`),
- '#authorize-impl': resolve(__dirname, `./src/services/authorize/${SERVICE_IMPL}.ts`),
- '#currency-exchange-impl': resolve(__dirname, `./src/services/currency-exchange/${SERVICE_IMPL === 'dweb' ? 'web' : SERVICE_IMPL}.ts`),
- '#staking-impl': resolve(__dirname, `./src/services/staking/${SERVICE_IMPL}.ts`),
- '#transaction-impl': resolve(__dirname, `./src/services/transaction/${SERVICE_IMPL}.ts`),
+ }),
+ react(),
+ tailwindcss(),
+ mockDevToolsPlugin(),
+ // 远程 miniapps (必须在 miniappsPlugin 之前,以便注册到全局状态)
+ remoteMiniappsPlugin({
+ miniapps: [
+ {
+ metadataUrl: 'https://iweb.xin/rwahub.bfmeta.com.miniapp/metadata.json',
+ dirName: 'rwa-hub',
+ build: {
+ rewriteBase: true,
+ },
+ },
+ ],
+ timeout: 60000,
+ retries: 3,
+ }),
+ miniappsPlugin(),
+ buildCheckPlugin(),
+ ],
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, './src'),
- // Node.js polyfills
- buffer: 'buffer/',
- },
- },
- define: {
- // 全局 Buffer 支持
- 'global': 'globalThis',
- // Mock 模式标识(用于条件加载 MockDevTools)
- '__MOCK_MODE__': JSON.stringify(SERVICE_IMPL === 'mock'),
- // Dev 模式标识(用于显示开发版水印)
- '__DEV_MODE__': JSON.stringify((env.VITE_DEV_MODE ?? process.env.VITE_DEV_MODE) === 'true'),
- // API Keys 对象(用于动态读取环境变量)
- '__API_KEYS__': JSON.stringify({
- TRONGRID_API_KEY: tronGridApiKey,
- ETHERSCAN_API_KEY: etherscanApiKey,
- }),
- },
- optimizeDeps: {
- include: ['buffer'],
- // Force Vite to pre-bundle the CJS bundle file
- esbuildOptions: {
- loader: {
- '.bundle.js': 'js',
- '.cjs': 'js',
+ // ==================== Platform Services (编译时替换) ====================
+ // 每个服务独立文件夹,通过 SERVICE_IMPL 环境变量选择实现
+ '#biometric-impl': resolve(__dirname, `./src/services/biometric/${SERVICE_IMPL}.ts`),
+ '#clipboard-impl': resolve(__dirname, `./src/services/clipboard/${SERVICE_IMPL}.ts`),
+ '#toast-impl': resolve(__dirname, `./src/services/toast/${SERVICE_IMPL}.ts`),
+ '#haptics-impl': resolve(__dirname, `./src/services/haptics/${SERVICE_IMPL}.ts`),
+ '#storage-impl': resolve(__dirname, `./src/services/storage/${SERVICE_IMPL}.ts`),
+ '#camera-impl': resolve(__dirname, `./src/services/camera/${SERVICE_IMPL}.ts`),
+ '#authorize-impl': resolve(__dirname, `./src/services/authorize/${SERVICE_IMPL}.ts`),
+ '#currency-exchange-impl': resolve(
+ __dirname,
+ `./src/services/currency-exchange/${SERVICE_IMPL === 'dweb' ? 'web' : SERVICE_IMPL}.ts`,
+ ),
+ '#staking-impl': resolve(__dirname, `./src/services/staking/${SERVICE_IMPL}.ts`),
+ '#transaction-impl': resolve(__dirname, `./src/services/transaction/${SERVICE_IMPL}.ts`),
+
+ // Node.js polyfills
+ buffer: 'buffer/',
},
},
- },
- build: {
- // 确保资源路径使用相对路径
- assetsDir: 'assets',
- rollupOptions: {
- input: {
- main: resolve(__dirname, 'index.html'),
- clear: resolve(__dirname, 'clear.html'),
+ define: {
+ // 全局 Buffer 支持
+ global: 'globalThis',
+ // Mock 模式标识(用于条件加载 MockDevTools)
+ __MOCK_MODE__: JSON.stringify(SERVICE_IMPL === 'mock'),
+ // Dev 模式标识(用于显示开发版水印)
+ __DEV_MODE__: JSON.stringify((env.VITE_DEV_MODE ?? process.env.VITE_DEV_MODE) === 'true'),
+ // API Keys 对象(用于动态读取环境变量)
+ __API_KEYS__: JSON.stringify({
+ TRONGRID_API_KEY: tronGridApiKey,
+ ETHERSCAN_API_KEY: etherscanApiKey,
+ }),
+ },
+ optimizeDeps: {
+ include: ['buffer'],
+ // Force Vite to pre-bundle the CJS bundle file
+ esbuildOptions: {
+ loader: {
+ '.bundle.js': 'js',
+ '.cjs': 'js',
+ },
},
- output: {
- // 使用 hash 命名避免缓存问题
- entryFileNames: 'assets/[name]-[hash].js',
- chunkFileNames: 'assets/[name]-[hash].js',
- assetFileNames: 'assets/[name]-[hash].[ext]',
- // 手动分块,减少主 chunk 体积
- manualChunks(id) {
- // React 核心
- if (id.includes('node_modules/react/') || id.includes('node_modules/react-dom/')) {
- return 'react-vendor'
- }
- // TanStack
- if (id.includes('node_modules/@tanstack/')) {
- return 'tanstack'
- }
- // Radix UI
- if (id.includes('node_modules/@radix-ui/')) {
- return 'radix'
- }
- // 动画
- if (id.includes('node_modules/motion/') || id.includes('node_modules/framer-motion/')) {
- return 'motion'
- }
- // i18n
- if (id.includes('node_modules/i18next') || id.includes('node_modules/react-i18next')) {
- return 'i18n'
- }
- // 加密库 - 最大的依赖
- if (id.includes('node_modules/@noble/') || id.includes('node_modules/@scure/')) {
- return 'crypto'
- }
- // BioForest 链库
- if (id.includes('node_modules/@bnqkl/')) {
- return 'bioforest'
- }
+ },
+ build: {
+ // 确保资源路径使用相对路径
+ assetsDir: 'assets',
+ rollupOptions: {
+ input: {
+ main: resolve(__dirname, 'index.html'),
+ clear: resolve(__dirname, 'clear.html'),
+ },
+ output: {
+ // 使用 hash 命名避免缓存问题
+ entryFileNames: 'assets/[name]-[hash].js',
+ chunkFileNames: 'assets/[name]-[hash].js',
+ assetFileNames: 'assets/[name]-[hash].[ext]',
+ // 手动分块,减少主 chunk 体积
+ manualChunks(id) {
+ // React 核心
+ if (id.includes('node_modules/react/') || id.includes('node_modules/react-dom/')) {
+ return 'react-vendor';
+ }
+ // TanStack
+ if (id.includes('node_modules/@tanstack/')) {
+ return 'tanstack';
+ }
+ // Radix UI
+ if (id.includes('node_modules/@radix-ui/')) {
+ return 'radix';
+ }
+ // 动画
+ if (id.includes('node_modules/motion/') || id.includes('node_modules/framer-motion/')) {
+ return 'motion';
+ }
+ // i18n
+ if (id.includes('node_modules/i18next') || id.includes('node_modules/react-i18next')) {
+ return 'i18n';
+ }
+ // 加密库 - 最大的依赖
+ if (id.includes('node_modules/@noble/') || id.includes('node_modules/@scure/')) {
+ return 'crypto';
+ }
+ // BioForest 链库
+ if (id.includes('node_modules/@bnqkl/')) {
+ return 'bioforest';
+ }
+ },
},
},
},
- },
-}
-})
+ };
+});