Skip to content

Commit 76d907d

Browse files
committed
fix: Updated to prerender svgs as well for cloud flare.
1 parent 4b2f298 commit 76d907d

File tree

5 files changed

+152
-57
lines changed

5 files changed

+152
-57
lines changed

src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts

Lines changed: 44 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,30 @@ const mockApiIndex = {
99

1010
const mockSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><circle cx="256" cy="256" r="200"/></svg>'
1111

12-
jest.mock('../../../../../../utils/icons/reactIcons', () => ({
13-
getIconSvg: jest.fn((setId: string, iconName: string) => {
14-
if (setId === 'fa' && iconName === 'FaCircle') {
15-
return Promise.resolve(mockSvg)
12+
const mockIconSvgs: Record<string, Record<string, string>> = {
13+
fa: { FaCircle: mockSvg },
14+
}
15+
16+
function createFetchMock(): typeof fetch {
17+
return jest.fn((input: RequestInfo | URL) => {
18+
const url = typeof input === 'string' ? input : input.toString()
19+
const match = url.match(/\/iconsSvgs\/([^/]+)\.json/)
20+
if (match) {
21+
const setId = match[1]
22+
const svgs = mockIconSvgs[setId] ?? {}
23+
return Promise.resolve({
24+
ok: true,
25+
json: () => Promise.resolve(svgs),
26+
} as Response)
1627
}
17-
return Promise.resolve(null)
18-
}),
28+
return Promise.resolve({
29+
ok: true,
30+
json: () => Promise.resolve(mockApiIndex),
31+
} as Response)
32+
}) as typeof fetch
33+
}
34+
35+
jest.mock('../../../../../../utils/icons/reactIcons', () => ({
1936
parseIconId: jest.fn((iconId: string) => {
2037
const underscoreIndex = iconId.indexOf('_')
2138
if (underscoreIndex <= 0) {
@@ -31,12 +48,7 @@ jest.mock('../../../../../../utils/icons/reactIcons', () => ({
3148
}))
3249

3350
it('returns SVG markup for valid icon', async () => {
34-
global.fetch = jest.fn(() =>
35-
Promise.resolve({
36-
ok: true,
37-
json: () => Promise.resolve(mockApiIndex),
38-
} as Response),
39-
)
51+
global.fetch = createFetchMock()
4052

4153
const response = await GET({
4254
params: { version: 'v6', iconName: 'fa_FaCircle' },
@@ -55,12 +67,7 @@ it('returns SVG markup for valid icon', async () => {
5567
})
5668

5769
it('returns 404 when icon is not found in set', async () => {
58-
global.fetch = jest.fn(() =>
59-
Promise.resolve({
60-
ok: true,
61-
json: () => Promise.resolve(mockApiIndex),
62-
} as Response),
63-
)
70+
global.fetch = createFetchMock()
6471

6572
const response = await GET({
6673
params: { version: 'v6', iconName: 'fa_FaNonExistent' },
@@ -78,12 +85,7 @@ it('returns 404 when icon is not found in set', async () => {
7885
})
7986

8087
it('returns 400 for invalid icon name format (no underscore)', async () => {
81-
global.fetch = jest.fn(() =>
82-
Promise.resolve({
83-
ok: true,
84-
json: () => Promise.resolve(mockApiIndex),
85-
} as Response),
86-
)
88+
global.fetch = createFetchMock()
8789

8890
const response = await GET({
8991
params: { version: 'v6', iconName: 'invalid' },
@@ -101,12 +103,7 @@ it('returns 400 for invalid icon name format (no underscore)', async () => {
101103
})
102104

103105
it('returns 400 for invalid icon name format (leading underscore)', async () => {
104-
global.fetch = jest.fn(() =>
105-
Promise.resolve({
106-
ok: true,
107-
json: () => Promise.resolve(mockApiIndex),
108-
} as Response),
109-
)
106+
global.fetch = createFetchMock()
110107

111108
const response = await GET({
112109
params: { version: 'v6', iconName: '_FaCircle' },
@@ -122,12 +119,7 @@ it('returns 400 for invalid icon name format (leading underscore)', async () =>
122119
})
123120

124121
it('returns 400 when icon name parameter is missing', async () => {
125-
global.fetch = jest.fn(() =>
126-
Promise.resolve({
127-
ok: true,
128-
json: () => Promise.resolve(mockApiIndex),
129-
} as Response),
130-
)
122+
global.fetch = createFetchMock()
131123

132124
const response = await GET({
133125
params: { version: 'v6' },
@@ -143,12 +135,7 @@ it('returns 400 when icon name parameter is missing', async () => {
143135
})
144136

145137
it('returns 404 for nonexistent version', async () => {
146-
global.fetch = jest.fn(() =>
147-
Promise.resolve({
148-
ok: true,
149-
json: () => Promise.resolve(mockApiIndex),
150-
} as Response),
151-
)
138+
global.fetch = createFetchMock()
152139

153140
const response = await GET({
154141
params: { version: 'v99', iconName: 'fa_FaCircle' },
@@ -165,12 +152,7 @@ it('returns 404 for nonexistent version', async () => {
165152
})
166153

167154
it('returns 400 when version parameter is missing', async () => {
168-
global.fetch = jest.fn(() =>
169-
Promise.resolve({
170-
ok: true,
171-
json: () => Promise.resolve(mockApiIndex),
172-
} as Response),
173-
)
155+
global.fetch = createFetchMock()
174156

175157
const response = await GET({
176158
params: { iconName: 'fa_FaCircle' },
@@ -186,13 +168,20 @@ it('returns 400 when version parameter is missing', async () => {
186168
})
187169

188170
it('returns 500 when fetchApiIndex fails', async () => {
189-
global.fetch = jest.fn(() =>
190-
Promise.resolve({
191-
ok: false,
192-
status: 500,
193-
statusText: 'Internal Server Error',
194-
} as Response),
195-
)
171+
global.fetch = jest.fn((input: RequestInfo | URL) => {
172+
const url = typeof input === 'string' ? input : input.toString()
173+
if (url.includes('apiIndex.json')) {
174+
return Promise.resolve({
175+
ok: false,
176+
status: 500,
177+
statusText: 'Internal Server Error',
178+
} as Response)
179+
}
180+
return Promise.resolve({
181+
ok: true,
182+
json: () => Promise.resolve({}),
183+
} as Response)
184+
}) as typeof fetch
196185

197186
const response = await GET({
198187
params: { version: 'v6', iconName: 'fa_FaCircle' },

src/pages/api/[version]/icons/[iconName].ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
createSvgResponse,
55
} from '../../../../utils/apiHelpers'
66
import { fetchApiIndex } from '../../../../utils/apiIndex/fetch'
7-
import { getIconSvg, parseIconId } from '../../../../utils/icons/reactIcons'
7+
import { fetchIconSvgs } from '../../../../utils/icons/fetch'
8+
import { parseIconId } from '../../../../utils/icons/reactIcons'
89

910
export const prerender = false
1011

@@ -51,7 +52,8 @@ export const GET: APIRoute = async ({ params, url }) => {
5152
}
5253

5354
const { setId, iconName } = parsed
54-
const svg = await getIconSvg(setId, iconName)
55+
const svgs = await fetchIconSvgs(url, setId)
56+
const svg = svgs?.[iconName] ?? null
5557

5658
if (!svg) {
5759
return createJsonResponse(
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { APIRoute, GetStaticPaths } from 'astro'
2+
import { IconsManifest } from 'react-icons/lib'
3+
import { getIconSvgsForSet } from '../../utils/icons/reactIcons'
4+
5+
/**
6+
* Prerender at build time so this doesn't run in the Cloudflare Worker.
7+
* getIconSvgsForSet() uses dynamic imports of react-icons which fail in Workers.
8+
*/
9+
export const prerender = true
10+
11+
export const getStaticPaths: GetStaticPaths = async () =>
12+
IconsManifest.map((m) => ({
13+
params: { setId: m.id },
14+
}))
15+
16+
export const GET: APIRoute = async ({ params }) => {
17+
const { setId } = params
18+
19+
if (!setId) {
20+
return new Response(
21+
JSON.stringify({ error: 'Set ID is required' }),
22+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
23+
)
24+
}
25+
26+
try {
27+
const svgs = await getIconSvgsForSet(setId)
28+
return new Response(JSON.stringify(svgs), {
29+
status: 200,
30+
headers: {
31+
'Content-Type': 'application/json',
32+
},
33+
})
34+
} catch (error) {
35+
return new Response(
36+
JSON.stringify({
37+
error: 'Failed to load icon SVGs',
38+
details: String(error),
39+
}),
40+
{
41+
status: 500,
42+
headers: { 'Content-Type': 'application/json' },
43+
}
44+
)
45+
}
46+
}

src/utils/icons/fetch.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,26 @@ export async function fetchIconsIndex(url: URL): Promise<IconMetadata[]> {
2525
const data = (await response.json()) as IconsIndex
2626
return data.icons
2727
}
28+
29+
/**
30+
* Fetches prerendered SVG markup for all icons in a set.
31+
* Used by the icon SVG API route at runtime instead of getIconSvg() which
32+
* uses dynamic imports that fail in Cloudflare Workers.
33+
*
34+
* @param url - The URL object from the API route context
35+
* @param setId - Icon set id (e.g., "fa", "ci")
36+
* @returns Promise resolving to Record of iconName -> SVG string, or null if fetch fails
37+
*/
38+
export async function fetchIconSvgs(
39+
url: URL,
40+
setId: string,
41+
): Promise<Record<string, string> | null> {
42+
const iconsSvgsUrl = new URL(`/iconsSvgs/${setId}.json`, url.origin)
43+
const response = await fetch(iconsSvgsUrl)
44+
45+
if (!response.ok) {
46+
return null
47+
}
48+
49+
return (await response.json()) as Record<string, string>
50+
}

src/utils/icons/reactIcons.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,41 @@ export function filterIcons(
109109
)
110110
}
111111

112+
/**
113+
* Get SVG markup for all icons in a set. Used at build time for prerendering.
114+
* @param setId - Icon set id (e.g., "fa", "md")
115+
* @returns Record of iconName -> SVG string
116+
*/
117+
export async function getIconSvgsForSet(
118+
setId: string,
119+
): Promise<Record<string, string>> {
120+
if (!ICON_SET_IDS.includes(setId)) {
121+
return {}
122+
}
123+
124+
try {
125+
const module = await import(`react-icons/${setId}`)
126+
const svgs: Record<string, string> = {}
127+
128+
for (const iconName of Object.keys(module)) {
129+
const IconComponent = module[iconName]
130+
if (typeof IconComponent !== 'function' || iconName === 'default') {
131+
continue
132+
}
133+
134+
const element = React.createElement(IconComponent, {
135+
size: '1em',
136+
style: { verticalAlign: 'middle' },
137+
})
138+
svgs[iconName] = renderToStaticMarkup(element)
139+
}
140+
141+
return svgs
142+
} catch {
143+
return {}
144+
}
145+
}
146+
112147
/**
113148
* Get SVG markup for a specific icon.
114149
* @param setId - Icon set id (e.g., "fa", "md")

0 commit comments

Comments
 (0)