Skip to content

Commit d484e58

Browse files
authored
fix: resolve asset URLs relative to document.baseURI (#122)
- Add resolveAssetUrl utility to handle relative paths for GitHub Pages deployment - Update IconProvidersWrapper to resolve chain icon and token icon base URLs - Fixes icons showing as /icons/... instead of /KeyApp/webapp/icons/... on gh-pages
1 parent 72f1a6f commit d484e58

File tree

3 files changed

+130
-6
lines changed

3 files changed

+130
-6
lines changed

src/frontend-main.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { StrictMode, lazy, Suspense, useCallback } from 'react'
1+
import { StrictMode, lazy, Suspense, useCallback, useMemo } from 'react'
22
import { createRoot } from 'react-dom/client'
33
import { QueryClientProvider } from '@tanstack/react-query'
44
import { I18nextProvider } from 'react-i18next'
55
import i18n from './i18n'
66
import { queryClient } from './lib/query-client'
7+
import { resolveAssetUrl } from './lib/asset-url'
78
import { ServiceProvider } from './services'
89
import { MigrationProvider } from './contexts/MigrationContext'
910
import { StackflowApp } from './StackflowApp'
@@ -32,15 +33,24 @@ const MockDevTools = __MOCK_MODE__
3233

3334
function IconProvidersWrapper({ children }: { children: React.ReactNode }) {
3435
const configs = useChainConfigs()
35-
36+
37+
// 预处理配置:解析所有相对路径为完整 URL
38+
const resolvedConfigs = useMemo(() => {
39+
return configs.map((config) => ({
40+
...config,
41+
icon: config.icon ? resolveAssetUrl(config.icon) : undefined,
42+
tokenIconBase: config.tokenIconBase?.map(resolveAssetUrl),
43+
}))
44+
}, [configs])
45+
3646
const getIconUrl = useCallback(
37-
(chainId: string) => configs.find((c) => c.id === chainId)?.icon,
38-
[configs],
47+
(chainId: string) => resolvedConfigs.find((c) => c.id === chainId)?.icon,
48+
[resolvedConfigs],
3949
)
4050

4151
const getTokenIconBases = useCallback(
42-
(chainId: string) => configs.find((c) => c.id === chainId)?.tokenIconBase ?? [],
43-
[configs],
52+
(chainId: string) => resolvedConfigs.find((c) => c.id === chainId)?.tokenIconBase ?? [],
53+
[resolvedConfigs],
4454
)
4555

4656
return (

src/lib/asset-url.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2+
import { resolveAssetUrl } from './asset-url'
3+
4+
describe('resolveAssetUrl', () => {
5+
const originalBaseURI = document.baseURI
6+
7+
beforeEach(() => {
8+
// Mock document.baseURI
9+
Object.defineProperty(document, 'baseURI', {
10+
value: 'https://example.com/app/',
11+
configurable: true,
12+
})
13+
})
14+
15+
afterEach(() => {
16+
Object.defineProperty(document, 'baseURI', {
17+
value: originalBaseURI,
18+
configurable: true,
19+
})
20+
})
21+
22+
it('resolves absolute path starting with /', () => {
23+
expect(resolveAssetUrl('/icons/eth.svg')).toBe('https://example.com/app/icons/eth.svg')
24+
})
25+
26+
it('resolves relative path starting with ./', () => {
27+
expect(resolveAssetUrl('./icons/eth.svg')).toBe('https://example.com/app/icons/eth.svg')
28+
})
29+
30+
it('returns http URLs unchanged', () => {
31+
expect(resolveAssetUrl('http://cdn.example.com/icon.svg')).toBe('http://cdn.example.com/icon.svg')
32+
})
33+
34+
it('returns https URLs unchanged', () => {
35+
expect(resolveAssetUrl('https://cdn.example.com/icon.svg')).toBe('https://cdn.example.com/icon.svg')
36+
})
37+
38+
it('returns other strings unchanged', () => {
39+
expect(resolveAssetUrl('icon.svg')).toBe('icon.svg')
40+
})
41+
42+
describe('with GitHub Pages style baseURI', () => {
43+
beforeEach(() => {
44+
Object.defineProperty(document, 'baseURI', {
45+
value: 'https://bioforestchain.github.io/KeyApp/webapp/',
46+
configurable: true,
47+
})
48+
})
49+
50+
it('correctly resolves /icons path', () => {
51+
expect(resolveAssetUrl('/icons/ethereum/chain.svg')).toBe(
52+
'https://bioforestchain.github.io/KeyApp/webapp/icons/ethereum/chain.svg'
53+
)
54+
})
55+
56+
it('correctly resolves /icons/tokens path', () => {
57+
expect(resolveAssetUrl('/icons/bfmeta/tokens')).toBe(
58+
'https://bioforestchain.github.io/KeyApp/webapp/icons/bfmeta/tokens'
59+
)
60+
})
61+
})
62+
63+
describe('with localhost baseURI', () => {
64+
beforeEach(() => {
65+
Object.defineProperty(document, 'baseURI', {
66+
value: 'http://localhost:5173/',
67+
configurable: true,
68+
})
69+
})
70+
71+
it('correctly resolves /icons path', () => {
72+
expect(resolveAssetUrl('/icons/ethereum/chain.svg')).toBe(
73+
'http://localhost:5173/icons/ethereum/chain.svg'
74+
)
75+
})
76+
})
77+
})

src/lib/asset-url.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* 资源 URL 解析工具
3+
*
4+
* 解决部署到子路径(如 GitHub Pages)时,以 `/` 开头的相对路径无法正确解析的问题。
5+
* 使用 document.baseURI 作为基础 URL 来解析路径。
6+
*/
7+
8+
/**
9+
* 解析资源 URL
10+
*
11+
* - 以 `/` 开头的路径:基于 document.baseURI 解析
12+
* - 以 `./` 开头的路径:基于 document.baseURI 解析
13+
* - 已经是完整 URL(http/https):直接返回
14+
* - 其他情况:直接返回
15+
*
16+
* @example
17+
* // 假设 document.baseURI = 'https://example.com/app/'
18+
* resolveAssetUrl('/icons/eth.svg') // => 'https://example.com/app/icons/eth.svg'
19+
* resolveAssetUrl('./icons/eth.svg') // => 'https://example.com/app/icons/eth.svg'
20+
* resolveAssetUrl('https://cdn.com/a.svg') // => 'https://cdn.com/a.svg'
21+
*/
22+
export function resolveAssetUrl(path: string): string {
23+
// 已经是完整 URL
24+
if (path.startsWith('http://') || path.startsWith('https://')) {
25+
return path
26+
}
27+
28+
// 以 `/` 或 `./` 开头的相对路径,基于 baseURI 解析
29+
if (path.startsWith('/') || path.startsWith('./')) {
30+
// 去掉开头的 `/` 或 `./`
31+
const relativePath = path.startsWith('/') ? path.slice(1) : path.slice(2)
32+
return new URL(relativePath, document.baseURI).href
33+
}
34+
35+
// 其他情况直接返回
36+
return path
37+
}

0 commit comments

Comments
 (0)