Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
631 changes: 138 additions & 493 deletions scripts/dev.ts

Large diffs are not rendered by default.

20 changes: 16 additions & 4 deletions scripts/vite-plugin-miniapps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@ import { readdirSync, existsSync, readFileSync, writeFileSync, mkdirSync, cpSync
import detectPort from 'detect-port';
import https from 'node:https';
import { getRemoteMiniappsForEcosystem } from './vite-plugin-remote-miniapps';
import type { WujieRuntimeConfig } from '../src/services/ecosystem/types';

// ==================== Types ====================

type MiniappRuntime = 'iframe' | 'wujie';

interface MiniappRuntimeConfig {
server?: MiniappRuntime;
build?: MiniappRuntime;
server?: MiniappRuntime | { runtime: MiniappRuntime; wujieConfig?: WujieRuntimeConfig };
build?: MiniappRuntime | { runtime: MiniappRuntime; wujieConfig?: WujieRuntimeConfig };
}

function parseRuntimeConfig(config?: MiniappRuntime | { runtime: MiniappRuntime; wujieConfig?: WujieRuntimeConfig }): {
runtime: MiniappRuntime;
wujieConfig?: WujieRuntimeConfig;
} {
if (!config) return { runtime: 'iframe' };
if (typeof config === 'string') return { runtime: config };
return { runtime: config.runtime, wujieConfig: config.wujieConfig };
}

interface MiniappManifest {
Expand Down Expand Up @@ -133,14 +143,15 @@ export function miniappsPlugin(options: MiniappsPluginOptions = {}): Plugin {
try {
const manifest = await fetchManifest(s.port);
const appConfig = apps[manifest.id];
const runtime = appConfig?.server ?? 'iframe';
const { runtime, wujieConfig } = parseRuntimeConfig(appConfig?.server);
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),
runtime,
wujieConfig,
};
} catch (e) {
console.error(`[miniapps] Failed to fetch manifest for ${s.id}:`, e);
Expand Down Expand Up @@ -280,7 +291,7 @@ function generateEcosystemDataForBuild(
const shortId = manifest.id.split('.').pop() || '';
const screenshots = scanScreenshots(root, shortId);
const appConfig = apps[manifest.id];
const runtime = appConfig?.build ?? 'iframe';
const { runtime, wujieConfig } = parseRuntimeConfig(appConfig?.build);

const { dirName, ...rest } = manifest;
return {
Expand All @@ -290,6 +301,7 @@ function generateEcosystemDataForBuild(
icon: `./${dirName}/icon.svg`,
screenshots: screenshots.map((s) => `./${dirName}/${s}`),
runtime,
wujieConfig,
};
});

Expand Down
28 changes: 22 additions & 6 deletions scripts/vite-plugin-remote-miniapps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,28 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync } fr
import { createServer } from 'node:http';
import type JSZipType from 'jszip';
import { fetchWithEtag, type FetchWithEtagOptions } from './utils/fetch-with-etag';
import type { WujieRuntimeConfig } from '../src/services/ecosystem/types';

// ==================== Types ====================

type MiniappRuntime = 'iframe' | 'wujie';

interface MiniappServerConfig {
runtime?: MiniappRuntime;
wujieConfig?: WujieRuntimeConfig;
}

interface MiniappBuildConfig {
runtime?: MiniappRuntime;
wujieConfig?: WujieRuntimeConfig;
/**
* 重写 index.html 的 <base> 标签
* 插入 <base> 标签重写 HTML 路径
* 仅在 runtime: 'iframe' 时有意义
* - true: 自动推断为 '/miniapps/{dirName}/'
* - string: 自定义路径
* - undefined/false: 不重写
* - undefined/false: 不插入
*/
rewriteBase?: boolean | string;
injectBaseTag?: boolean | string;
}

interface RemoteMiniappConfig {
Expand Down Expand Up @@ -148,11 +152,22 @@ export function remoteMiniappsPlugin(options: RemoteMiniappsPluginOptions): Plug
cpSync(srcDir, destDir, { recursive: true });
console.log(`[remote-miniapps] ✅ Copied ${config.dirName} to dist`);

if (config.build?.rewriteBase) {
if (config.build?.injectBaseTag) {
const basePath =
typeof config.build.rewriteBase === 'string' ? config.build.rewriteBase : `/miniapps/${config.dirName}/`;
typeof config.build.injectBaseTag === 'string'
? config.build.injectBaseTag
: `/miniapps/${config.dirName}/`;
rewriteHtmlBase(destDir, basePath);
}

if (config.build?.wujieConfig) {
const manifestPath = join(destDir, 'manifest.json');
if (existsSync(manifestPath)) {
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
manifest.wujieConfig = config.build.wujieConfig;
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
}
}
} else {
missing.push(config.dirName);
}
Expand Down Expand Up @@ -391,7 +406,7 @@ export function getRemoteMiniappServers(): RemoteMiniappServer[] {
* 获取远程 miniapps 用于 ecosystem.json 的数据
*/
export function getRemoteMiniappsForEcosystem(): Array<
MiniappManifest & { url: string; runtime?: 'iframe' | 'wujie' }
MiniappManifest & { url: string; runtime?: 'iframe' | 'wujie'; wujieConfig?: WujieRuntimeConfig }
> {
return globalRemoteServers.map((s) => ({
...s.manifest,
Expand All @@ -400,6 +415,7 @@ export function getRemoteMiniappsForEcosystem(): Array<
url: new URL('/', s.baseUrl).href,
screenshots: s.manifest.screenshots?.map((sc) => new URL(sc, s.baseUrl).href) ?? [],
runtime: s.config.server?.runtime ?? 'iframe',
wujieConfig: s.config.server?.wujieConfig,
}));
}

Expand Down
9 changes: 9 additions & 0 deletions src/services/ecosystem/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,15 @@ export interface MiniappManifest {
sourceName?: string;
/** 运行时容器类型(由宿主注入,默认 'iframe') */
runtime?: 'iframe' | 'wujie';
/** wujie 运行时配置 (仅当 runtime: 'wujie' 时有效) */
wujieConfig?: WujieRuntimeConfig;
}

export interface WujieRuntimeConfig {
rewriteAbsolutePaths?: boolean;
alive?: boolean;
fiber?: boolean;
sync?: boolean;
}

/** Ecosystem source - JSON 文件格式 */
Expand Down
3 changes: 3 additions & 0 deletions src/services/miniapp-runtime/container/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { WujieRuntimeConfig } from '@/services/ecosystem/types';

export type ContainerType = 'iframe' | 'wujie';

export interface ContainerHandle {
Expand All @@ -16,6 +18,7 @@ export interface ContainerCreateOptions {
mountTarget: HTMLElement;
contextParams?: Record<string, string>;
onLoad?: () => void;
wujieConfig?: WujieRuntimeConfig;
}

export interface ContainerManager {
Expand Down
72 changes: 60 additions & 12 deletions src/services/miniapp-runtime/container/wujie-container.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { startApp, destroyApp, bus } from 'wujie';
import type { ContainerManager, ContainerHandle, ContainerCreateOptions } from './types';

/**
* Patch document.adoptedStyleSheets to proxy to shadowRoot.adoptedStyleSheets
* Must be called after shadowRoot is created (e.g., in afterMount)
* TODO: Submit PR to wujie upstream and remove this workaround
*/
function patchAdoptedStyleSheets(appId: string) {
const iframe = document.querySelector<HTMLIFrameElement>(`iframe[name="${appId}"]`);
if (!iframe?.contentWindow) return;
Expand Down Expand Up @@ -43,6 +38,52 @@ function patchAdoptedStyleSheets(appId: string) {
}
}

function rewriteHtmlAbsolutePaths(html: string, baseUrl: string): string {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');

const elements = doc.querySelectorAll('[href], [src]');
elements.forEach((el) => {
if (el.hasAttribute('href')) {
const original = el.getAttribute('href')!;
if (original.startsWith('/') && !original.startsWith('//')) {
el.setAttribute('href', new URL(original.slice(1), baseUrl).href);
}
}
if (el.hasAttribute('src')) {
const original = el.getAttribute('src')!;
if (original.startsWith('/') && !original.startsWith('//')) {
el.setAttribute('src', new URL(original.slice(1), baseUrl).href);
}
}
});

return doc.documentElement.outerHTML;
}

function createAbsolutePathRewriter(baseUrl: string) {
const parsedUrl = new URL(baseUrl);
const origin = parsedUrl.origin + parsedUrl.pathname.replace(/\/$/, '');

return {
fetch: (input: RequestInfo | URL, init?: RequestInit) => {
const req = new Request(input, init);
const parsedReqUrl = new URL(req.url);

if (parsedReqUrl.origin === window.location.origin) {
const rewrittenUrl = `${origin}${parsedReqUrl.pathname}${parsedReqUrl.search}${parsedReqUrl.hash}`;
return window.fetch(rewrittenUrl, init);
}
return window.fetch(req);
},
plugins: [
{
htmlLoader: (html: string) => rewriteHtmlAbsolutePaths(html, baseUrl),
},
],
};
}

class WujieContainerHandle implements ContainerHandle {
readonly type = 'wujie' as const;
readonly element: HTMLElement;
Expand Down Expand Up @@ -87,11 +128,10 @@ export class WujieContainerManager implements ContainerManager {
readonly type = 'wujie' as const;

async create(options: ContainerCreateOptions): Promise<WujieContainerHandle> {
const { appId, url, mountTarget, contextParams, onLoad } = options;
const { appId, url, mountTarget, contextParams, onLoad, wujieConfig } = options;

const container = document.createElement('div');
container.id = `miniapp-wujie-${appId}`;
// container.style.cssText = 'width: 100%; height: 100%;';
container.className = 'size-full *:size-full *:block *:overflow-auto';

mountTarget.appendChild(container);
Expand All @@ -103,17 +143,25 @@ export class WujieContainerManager implements ContainerManager {
});
}

await startApp({
const startAppOptions: Parameters<typeof startApp>[0] = {
name: appId,
url: urlWithParams.toString(),
el: container,
alive: true,
fiber: true,
sync: false,
alive: wujieConfig?.alive ?? true,
fiber: wujieConfig?.fiber ?? true,
sync: wujieConfig?.sync ?? false,
afterMount: () => {
onLoad?.();
},
});
};

if (wujieConfig?.rewriteAbsolutePaths) {
const rewriter = createAbsolutePathRewriter(urlWithParams.toString());
startAppOptions.fetch = rewriter.fetch;
startAppOptions.plugins = rewriter.plugins;
}

await startApp(startAppOptions);

patchAdoptedStyleSheets(appId);

Expand Down
1 change: 1 addition & 0 deletions src/services/miniapp-runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,7 @@ export function launchApp(
url: manifest.url,
mountTarget: document.body,
contextParams,
wujieConfig: manifest.wujieConfig,
onLoad: () => {
updateAppProcessStatus(appId, 'loaded');
if (!manifest.splashScreen) {
Expand Down
7 changes: 5 additions & 2 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,13 @@ export default defineConfig(({ mode }) => {
{
metadataUrl: 'https://iweb.xin/rwahub.bfmeta.com.miniapp/metadata.json',
dirName: 'rwa-hub',
server: { runtime: 'wujie' },
server: {
runtime: 'wujie',
wujieConfig: { rewriteAbsolutePaths: true },
},
build: {
runtime: 'wujie',
rewriteBase: true,
wujieConfig: { rewriteAbsolutePaths: true },
},
},
],
Expand Down