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
21 changes: 14 additions & 7 deletions scripts/vite-plugin-miniapps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,11 +616,18 @@ function scanRemoteMiniappsForBuild(
): Array<MiniappManifest & { url: string; runtime?: MiniappRuntime; wujieConfig?: WujieRuntimeConfig }> {
if (!existsSync(miniappsPath)) return [];

const configByDirName = new Map(
remoteConfigs
.map((c) => (c.build?.locale ? [c.build.locale.dirName, c] : null))
.filter((item): item is [string, RemoteMiniappConfig] => item !== null),
);
const configByDirName = new Map<string, RemoteMiniappConfig>();
for (const remoteConfig of remoteConfigs) {
const buildDirName = remoteConfig.build?.locale?.dirName;
if (buildDirName && !configByDirName.has(buildDirName)) {
configByDirName.set(buildDirName, remoteConfig);
}

const serverDirName = remoteConfig.server?.locale?.dirName;
if (serverDirName && !configByDirName.has(serverDirName)) {
configByDirName.set(serverDirName, remoteConfig);
}
}
const remoteApps: Array<
MiniappManifest & { url: string; runtime?: MiniappRuntime; wujieConfig?: WujieRuntimeConfig }
> = [];
Expand All @@ -645,8 +652,8 @@ function scanRemoteMiniappsForBuild(
url: baseUrl,
icon: manifest.icon.startsWith('http') ? manifest.icon : new URL(manifest.icon, baseUrl).href,
screenshots: manifest.screenshots?.map((s) => (s.startsWith('http') ? s : new URL(s, baseUrl).href)) ?? [],
runtime: config?.build?.runtime ?? 'wujie',
wujieConfig: config?.build?.wujieConfig,
runtime: config?.build?.runtime ?? config?.server?.runtime ?? 'wujie',
wujieConfig: config?.build?.wujieConfig ?? config?.server?.wujieConfig,
});
} catch {
console.warn(`[miniapps] ${entry.name}: invalid remote manifest.json, skipping`);
Expand Down
178 changes: 145 additions & 33 deletions src/services/ecosystem/__tests__/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,94 @@ describe('Miniapp Registry (Subscription v2)', () => {
expect(getAppById('xin.dweb.teleport')?.name).toBe('Teleport')
})

it('prefers the later source when duplicate app id exists', async () => {
ecosystemStore.setState(() => ({
permissions: [],
sources: [
{
url: '/ecosystem-primary.json',
name: 'Primary',
enabled: true,
lastUpdated: new Date().toISOString(),
},
{
url: '/ecosystem-override.json',
name: 'Override',
enabled: true,
lastUpdated: new Date().toISOString(),
},
],
}))

global.fetch = vi.fn((input: RequestInfo | URL) => {
const requestUrl = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url

if (requestUrl.includes('/ecosystem-primary.json')) {
return Promise.resolve({
ok: true,
status: 200,
headers: new Map(),
json: () =>
Promise.resolve({
name: 'primary',
version: '1',
updated: '2025-01-01',
apps: [
{
id: 'xin.dweb.rwahub',
name: 'RWA Hub',
url: 'https://legacy.example/',
icon: '/legacy-icon.svg',
description: 'legacy runtime',
version: '1.0.0',
runtime: 'wujie',
},
],
}),
})
}

if (requestUrl.includes('/ecosystem-override.json')) {
return Promise.resolve({
ok: true,
status: 200,
headers: new Map(),
json: () =>
Promise.resolve({
name: 'override',
version: '1',
updated: '2025-01-01',
apps: [
{
id: 'xin.dweb.rwahub',
name: 'RWA Hub',
url: 'https://latest.example/',
icon: '/latest-icon.svg',
description: 'iframe runtime',
version: '1.0.0',
runtime: 'iframe',
},
],
}),
})
}

return Promise.resolve({
ok: false,
status: 404,
headers: new Map(),
json: () => Promise.resolve({}),
})
}) as unknown as typeof fetch

await refreshSources()

const app = getAppById('xin.dweb.rwahub')
expect(app?.runtime).toBe('iframe')
expect(app?.url).toBe('https://latest.example/')
expect(app?.sourceUrl).toBe('/ecosystem-override.json')
})

it('searches cached apps by keyword', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
Expand All @@ -89,39 +177,63 @@ describe('Miniapp Registry (Subscription v2)', () => {
})

it('ranks featured apps by featuredScore', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: new Map(),
json: () =>
Promise.resolve({
name: 'x',
version: '1',
updated: '2025-01-01',
apps: [
{
id: 'xin.dweb.a',
name: 'A',
url: '/',
icon: '',
description: '',
version: '1.0.0',
officialScore: 100,
communityScore: 0,
},
{
id: 'xin.dweb.b',
name: 'B',
url: '/',
icon: '',
description: '',
version: '1.0.0',
officialScore: 0,
communityScore: 100,
},
],
}),
})
ecosystemStore.setState(() => ({
permissions: [],
sources: [
{
url: '/ecosystem-featured.json',
name: 'Featured',
enabled: true,
lastUpdated: new Date().toISOString(),
},
],
}))

global.fetch = vi.fn((input: RequestInfo | URL) => {
const requestUrl = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
if (!requestUrl.includes('/ecosystem-featured.json')) {
return Promise.resolve({
ok: false,
status: 404,
headers: new Map(),
json: () => Promise.resolve({}),
})
}

return Promise.resolve({
ok: true,
status: 200,
headers: new Map(),
json: () =>
Promise.resolve({
name: 'x',
version: '1',
updated: '2025-01-01',
apps: [
{
id: 'xin.dweb.a',
name: 'A',
url: '/',
icon: '',
description: '',
version: '1.0.0',
officialScore: 100,
communityScore: 0,
},
{
id: 'xin.dweb.b',
name: 'B',
url: '/',
icon: '',
description: '',
version: '1.0.0',
officialScore: 0,
communityScore: 100,
},
],
}),
})
}) as unknown as typeof fetch

await refreshSources()
const featured = await getFeaturedApps(1, new Date('2025-01-05T00:00:00.000Z'))
Expand Down
18 changes: 11 additions & 7 deletions src/services/ecosystem/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,22 +306,26 @@ function mergeAppsFromSources(
const normalized = normalizeAppFromSource(app, source, payload);
if (!normalized) continue;

const id = normalized.id;
if (!indexById.has(id)) {
indexById.set(id, merged.length);
const appId = normalized.id;
const existingIndex = indexById.get(appId);

if (existingIndex === undefined) {
indexById.set(appId, merged.length);
merged.push(normalized);
} else {
merged[existingIndex] = normalized;
}

if (typeof normalized.officialScore === 'number') {
const list = officialScoresById.get(id) ?? [];
const list = officialScoresById.get(appId) ?? [];
list.push(normalized.officialScore);
officialScoresById.set(id, list);
officialScoresById.set(appId, list);
}

if (typeof normalized.communityScore === 'number') {
const list = communityScoresById.get(id) ?? [];
const list = communityScoresById.get(appId) ?? [];
list.push(normalized.communityScore);
communityScoresById.set(id, list);
communityScoresById.set(appId, list);
}
}
}
Expand Down
6 changes: 2 additions & 4 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,14 @@ const remoteMiniappsConfig: RemoteMiniappConfig[] = [
metadataUrl: 'https://iweb.xin/rwahub.bfmeta.com.miniapp/metadata.json',
dirName: 'rwa-hub',
},
runtime: 'wujie',
wujieConfig: { rewriteAbsolutePaths: true },
runtime: 'iframe',
},
build: {
remote: {
name: 'RWA',
sourceUrl: 'https://iweb.xin/rwahub.bfmeta.com.miniapp/source.json',
},
runtime: 'wujie',
wujieConfig: { rewriteAbsolutePaths: true },
runtime: 'iframe',
},
},
];
Expand Down