Skip to content

Commit 9de2635

Browse files
heiskrrsese
andauthored
Consolidate octicon frontmatter property references into single file (#56342)
Co-authored-by: Robert Sese <734194+rsese@users.noreply.github.com>
1 parent 095c647 commit 9de2635

File tree

4 files changed

+223
-73
lines changed

4 files changed

+223
-73
lines changed

src/landings/components/CookBookArticleCard.tsx

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,7 @@
11
import { Label, LabelGroup, Link } from '@primer/react'
2-
import {
3-
BugIcon,
4-
LightBulbIcon,
5-
CodeIcon,
6-
GearIcon,
7-
RocketIcon,
8-
BeakerIcon,
9-
CopilotIcon,
10-
HubotIcon,
11-
LogIcon,
12-
TerminalIcon,
13-
BookIcon,
14-
ShieldLockIcon,
15-
LockIcon,
16-
} from '@primer/octicons-react'
2+
import { ValidOcticon, getOcticonComponent } from '../lib/octicons'
173

18-
const Icons = {
19-
bug: BugIcon,
20-
lightbulb: LightBulbIcon,
21-
code: CodeIcon,
22-
gear: GearIcon,
23-
rocket: RocketIcon,
24-
beaker: BeakerIcon,
25-
copilot: CopilotIcon,
26-
hubot: HubotIcon,
27-
log: LogIcon,
28-
terminal: TerminalIcon,
29-
book: BookIcon,
30-
'shield-lock': ShieldLockIcon,
31-
lock: LockIcon,
32-
}
33-
34-
type IconType = keyof typeof Icons
4+
type IconType = ValidOcticon
355

366
type Props = {
377
title: string
@@ -69,11 +39,7 @@ export const CookBookArticleCard = ({
6939
url,
7040
spotlight = false,
7141
}: Props) => {
72-
const setIcon = (icon: keyof typeof Icons) => {
73-
return Icons[icon] || CopilotIcon
74-
}
75-
76-
const IconComponent = setIcon(icon as keyof typeof Icons)
42+
const IconComponent = getOcticonComponent(icon)
7743
return (
7844
<div className="m-2">
7945
<div

src/landings/lib/octicons.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {
2+
BugIcon,
3+
LightBulbIcon,
4+
CodeIcon,
5+
GearIcon,
6+
RocketIcon,
7+
BeakerIcon,
8+
CopilotIcon,
9+
HubotIcon,
10+
LogIcon,
11+
TerminalIcon,
12+
BookIcon,
13+
ShieldLockIcon,
14+
LockIcon,
15+
} from '@primer/octicons-react'
16+
17+
/**
18+
* Mapping of octicon names to their React components
19+
* This is the single source of truth for all supported octicons
20+
*/
21+
export const OCTICON_COMPONENTS = {
22+
bug: BugIcon,
23+
lightbulb: LightBulbIcon,
24+
code: CodeIcon,
25+
gear: GearIcon,
26+
rocket: RocketIcon,
27+
beaker: BeakerIcon,
28+
copilot: CopilotIcon,
29+
hubot: HubotIcon,
30+
log: LogIcon,
31+
terminal: TerminalIcon,
32+
book: BookIcon,
33+
'shield-lock': ShieldLockIcon,
34+
lock: LockIcon,
35+
} as const
36+
37+
/**
38+
* Valid octicon types derived from the component mapping
39+
*/
40+
export type ValidOcticon = keyof typeof OCTICON_COMPONENTS
41+
42+
/**
43+
* Array of all valid octicon names for validation, derived from component mapping
44+
*/
45+
export const VALID_OCTICONS = Object.keys(OCTICON_COMPONENTS) as ValidOcticon[]
46+
47+
/**
48+
* Helper function to validate and cast octicon values
49+
* @param octicon - The octicon string to validate
50+
* @returns True if the octicon is valid, false otherwise
51+
*/
52+
export function isValidOcticon(octicon: string | null): octicon is ValidOcticon {
53+
return octicon !== null && (octicon as ValidOcticon) in OCTICON_COMPONENTS
54+
}
55+
56+
/**
57+
* Get the React component for a given octicon name
58+
* @param octicon - The octicon name
59+
* @returns The corresponding React component, or CopilotIcon as fallback
60+
*/
61+
export function getOcticonComponent(octicon: ValidOcticon | undefined) {
62+
if (!octicon || !isValidOcticon(octicon)) {
63+
return CopilotIcon
64+
}
65+
return OCTICON_COMPONENTS[octicon] || CopilotIcon
66+
}

src/landings/tests/octicons.test.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { describe, expect, test } from 'vitest'
2+
import {
3+
ValidOcticon,
4+
VALID_OCTICONS,
5+
OCTICON_COMPONENTS,
6+
isValidOcticon,
7+
getOcticonComponent,
8+
} from '../lib/octicons'
9+
import { CopilotIcon, BugIcon, RocketIcon } from '@primer/octicons-react'
10+
11+
describe('octicons reference', () => {
12+
describe('VALID_OCTICONS', () => {
13+
test('contains expected octicon names', () => {
14+
// Test that we have the expected number of octicons and they're all defined
15+
expect(VALID_OCTICONS.length).toBeGreaterThan(0)
16+
expect(VALID_OCTICONS).toEqual(expect.arrayContaining(['bug', 'rocket', 'copilot']))
17+
})
18+
19+
test('all octicons are strings', () => {
20+
VALID_OCTICONS.forEach((octicon) => {
21+
expect(typeof octicon).toBe('string')
22+
})
23+
})
24+
})
25+
26+
describe('OCTICON_COMPONENTS', () => {
27+
test('has components for all valid octicons', () => {
28+
VALID_OCTICONS.forEach((octicon) => {
29+
expect(OCTICON_COMPONENTS[octicon]).toBeDefined()
30+
expect(typeof OCTICON_COMPONENTS[octicon]).toBe('object')
31+
})
32+
})
33+
34+
test('maps specific octicons to correct components', () => {
35+
expect(OCTICON_COMPONENTS.bug).toBe(BugIcon)
36+
expect(OCTICON_COMPONENTS.rocket).toBe(RocketIcon)
37+
expect(OCTICON_COMPONENTS.copilot).toBe(CopilotIcon)
38+
})
39+
})
40+
41+
describe('isValidOcticon', () => {
42+
test('returns true for valid octicons', () => {
43+
expect(isValidOcticon('bug')).toBe(true)
44+
expect(isValidOcticon('rocket')).toBe(true)
45+
expect(isValidOcticon('shield-lock')).toBe(true)
46+
})
47+
48+
test('returns false for invalid octicons', () => {
49+
expect(isValidOcticon('invalid-octicon')).toBe(false)
50+
expect(isValidOcticon('pizza')).toBe(false)
51+
expect(isValidOcticon('')).toBe(false)
52+
})
53+
54+
test('returns false for null or undefined', () => {
55+
expect(isValidOcticon(null)).toBe(false)
56+
expect(isValidOcticon(undefined as any)).toBe(false)
57+
})
58+
59+
test('provides correct type narrowing', () => {
60+
const testOcticon: string | null = 'bug'
61+
62+
if (isValidOcticon(testOcticon)) {
63+
// This should compile without type errors
64+
const validOcticon: ValidOcticon = testOcticon
65+
expect(validOcticon).toBe('bug')
66+
}
67+
})
68+
})
69+
70+
describe('getOcticonComponent', () => {
71+
test('returns correct component for valid octicons', () => {
72+
expect(getOcticonComponent('bug')).toBe(BugIcon)
73+
expect(getOcticonComponent('rocket')).toBe(RocketIcon)
74+
expect(getOcticonComponent('copilot')).toBe(CopilotIcon)
75+
})
76+
77+
test('returns CopilotIcon as fallback for undefined', () => {
78+
expect(getOcticonComponent(undefined)).toBe(CopilotIcon)
79+
})
80+
81+
test('returns CopilotIcon as fallback for invalid octicons', () => {
82+
// TypeScript should prevent this, but test runtime behavior
83+
expect(getOcticonComponent('invalid' as ValidOcticon)).toBe(CopilotIcon)
84+
})
85+
})
86+
87+
describe('type safety', () => {
88+
test('ValidOcticon type includes all expected values', () => {
89+
// This test ensures the type system prevents invalid octicons at compile time
90+
// Test a few key octicons to verify the type works correctly
91+
const testOcticons: ValidOcticon[] = ['bug', 'rocket', 'copilot']
92+
93+
testOcticons.forEach((octicon) => {
94+
expect(VALID_OCTICONS.includes(octicon)).toBe(true)
95+
})
96+
})
97+
})
98+
99+
describe('consistency checks', () => {
100+
test('OCTICON_COMPONENTS keys match VALID_OCTICONS', () => {
101+
const componentKeys = Object.keys(OCTICON_COMPONENTS)
102+
const validOcticonsSet = new Set(VALID_OCTICONS)
103+
104+
componentKeys.forEach((key) => {
105+
expect(validOcticonsSet.has(key as ValidOcticon)).toBe(true)
106+
})
107+
108+
expect(componentKeys).toHaveLength(VALID_OCTICONS.length)
109+
})
110+
111+
test('no duplicate octicons in VALID_OCTICONS', () => {
112+
const octiconsSet = new Set(VALID_OCTICONS)
113+
expect(octiconsSet.size).toBe(VALID_OCTICONS.length)
114+
})
115+
})
116+
117+
describe('single source of truth', () => {
118+
test('VALID_OCTICONS is derived from OCTICON_COMPONENTS', () => {
119+
const componentKeys = Object.keys(OCTICON_COMPONENTS).sort()
120+
const validOcticons = [...VALID_OCTICONS].sort()
121+
122+
expect(validOcticons).toEqual(componentKeys)
123+
})
124+
125+
test('ValidOcticon type matches OCTICON_COMPONENTS keys', () => {
126+
// This test ensures the type system is correctly derived from the object
127+
const testOcticon: ValidOcticon = 'bug'
128+
expect(OCTICON_COMPONENTS[testOcticon]).toBeDefined()
129+
130+
// Type check - this should compile without errors
131+
const allKeys: ValidOcticon[] = Object.keys(OCTICON_COMPONENTS) as ValidOcticon[]
132+
expect(allKeys.length).toBeGreaterThan(0)
133+
})
134+
135+
test('adding new octicon only requires updating OCTICON_COMPONENTS', () => {
136+
// This test documents the single source of truth approach
137+
// If you add a new octicon to OCTICON_COMPONENTS:
138+
// 1. ValidOcticon type automatically includes it
139+
// 2. VALID_OCTICONS array automatically includes it
140+
// 3. All validation functions work with it
141+
142+
const componentCount = Object.keys(OCTICON_COMPONENTS).length
143+
const validOcticonsCount = VALID_OCTICONS.length
144+
145+
expect(componentCount).toBe(validOcticonsCount)
146+
})
147+
})
148+
})

src/landings/types.ts

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
1+
import { ValidOcticon, isValidOcticon } from './lib/octicons'
2+
3+
// Re-export ValidOcticon and isValidOcticon for compatibility with existing imports
4+
export type { ValidOcticon }
5+
export { isValidOcticon }
6+
17
// Base type for all TOC items with core properties
28
export type BaseTocItem = {
39
fullPath: string
410
title: string
511
intro?: string | null
612
}
713

8-
// Valid octicon types that match the CookBookArticleCard component
9-
export type ValidOcticon =
10-
| 'code'
11-
| 'log'
12-
| 'terminal'
13-
| 'bug'
14-
| 'lightbulb'
15-
| 'gear'
16-
| 'rocket'
17-
| 'beaker'
18-
| 'copilot'
19-
| 'hubot'
20-
| 'book'
21-
| 'shield-lock'
22-
| 'lock'
23-
2414
// Extended type for child TOC items with additional metadata
2515
export type ChildTocItem = BaseTocItem & {
2616
octicon?: ValidOcticon | null
@@ -54,26 +44,6 @@ export type RawTocItem = {
5444
childTocItems: RawTocItem[]
5545
}
5646

57-
// Helper function to validate and cast octicon values
58-
export function isValidOcticon(octicon: string | null): octicon is ValidOcticon {
59-
const validOcticons: ValidOcticon[] = [
60-
'code',
61-
'log',
62-
'terminal',
63-
'bug',
64-
'lightbulb',
65-
'gear',
66-
'rocket',
67-
'beaker',
68-
'copilot',
69-
'hubot',
70-
'book',
71-
'shield-lock',
72-
'lock',
73-
]
74-
return octicon !== null && validOcticons.includes(octicon as ValidOcticon)
75-
}
76-
7747
// Simplified TOC item type for basic landing pages that don't need extended metadata
7848
export type SimpleTocItem = {
7949
fullPath: string

0 commit comments

Comments
 (0)