1
0
mirror of synced 2025-12-19 09:57:42 -05:00

Consolidate octicon frontmatter property references into single file (#56342)

Co-authored-by: Robert Sese <734194+rsese@users.noreply.github.com>
This commit is contained in:
Kevin Heis
2025-06-30 13:34:55 -07:00
committed by GitHub
parent 095c647464
commit 9de26352ab
4 changed files with 223 additions and 73 deletions

View File

@@ -1,37 +1,7 @@
import { Label, LabelGroup, Link } from '@primer/react'
import {
BugIcon,
LightBulbIcon,
CodeIcon,
GearIcon,
RocketIcon,
BeakerIcon,
CopilotIcon,
HubotIcon,
LogIcon,
TerminalIcon,
BookIcon,
ShieldLockIcon,
LockIcon,
} from '@primer/octicons-react'
import { ValidOcticon, getOcticonComponent } from '../lib/octicons'
const Icons = {
bug: BugIcon,
lightbulb: LightBulbIcon,
code: CodeIcon,
gear: GearIcon,
rocket: RocketIcon,
beaker: BeakerIcon,
copilot: CopilotIcon,
hubot: HubotIcon,
log: LogIcon,
terminal: TerminalIcon,
book: BookIcon,
'shield-lock': ShieldLockIcon,
lock: LockIcon,
}
type IconType = keyof typeof Icons
type IconType = ValidOcticon
type Props = {
title: string
@@ -69,11 +39,7 @@ export const CookBookArticleCard = ({
url,
spotlight = false,
}: Props) => {
const setIcon = (icon: keyof typeof Icons) => {
return Icons[icon] || CopilotIcon
}
const IconComponent = setIcon(icon as keyof typeof Icons)
const IconComponent = getOcticonComponent(icon)
return (
<div className="m-2">
<div

View File

@@ -0,0 +1,66 @@
import {
BugIcon,
LightBulbIcon,
CodeIcon,
GearIcon,
RocketIcon,
BeakerIcon,
CopilotIcon,
HubotIcon,
LogIcon,
TerminalIcon,
BookIcon,
ShieldLockIcon,
LockIcon,
} from '@primer/octicons-react'
/**
* Mapping of octicon names to their React components
* This is the single source of truth for all supported octicons
*/
export const OCTICON_COMPONENTS = {
bug: BugIcon,
lightbulb: LightBulbIcon,
code: CodeIcon,
gear: GearIcon,
rocket: RocketIcon,
beaker: BeakerIcon,
copilot: CopilotIcon,
hubot: HubotIcon,
log: LogIcon,
terminal: TerminalIcon,
book: BookIcon,
'shield-lock': ShieldLockIcon,
lock: LockIcon,
} as const
/**
* Valid octicon types derived from the component mapping
*/
export type ValidOcticon = keyof typeof OCTICON_COMPONENTS
/**
* Array of all valid octicon names for validation, derived from component mapping
*/
export const VALID_OCTICONS = Object.keys(OCTICON_COMPONENTS) as ValidOcticon[]
/**
* Helper function to validate and cast octicon values
* @param octicon - The octicon string to validate
* @returns True if the octicon is valid, false otherwise
*/
export function isValidOcticon(octicon: string | null): octicon is ValidOcticon {
return octicon !== null && (octicon as ValidOcticon) in OCTICON_COMPONENTS
}
/**
* Get the React component for a given octicon name
* @param octicon - The octicon name
* @returns The corresponding React component, or CopilotIcon as fallback
*/
export function getOcticonComponent(octicon: ValidOcticon | undefined) {
if (!octicon || !isValidOcticon(octicon)) {
return CopilotIcon
}
return OCTICON_COMPONENTS[octicon] || CopilotIcon
}

View File

@@ -0,0 +1,148 @@
import { describe, expect, test } from 'vitest'
import {
ValidOcticon,
VALID_OCTICONS,
OCTICON_COMPONENTS,
isValidOcticon,
getOcticonComponent,
} from '../lib/octicons'
import { CopilotIcon, BugIcon, RocketIcon } from '@primer/octicons-react'
describe('octicons reference', () => {
describe('VALID_OCTICONS', () => {
test('contains expected octicon names', () => {
// Test that we have the expected number of octicons and they're all defined
expect(VALID_OCTICONS.length).toBeGreaterThan(0)
expect(VALID_OCTICONS).toEqual(expect.arrayContaining(['bug', 'rocket', 'copilot']))
})
test('all octicons are strings', () => {
VALID_OCTICONS.forEach((octicon) => {
expect(typeof octicon).toBe('string')
})
})
})
describe('OCTICON_COMPONENTS', () => {
test('has components for all valid octicons', () => {
VALID_OCTICONS.forEach((octicon) => {
expect(OCTICON_COMPONENTS[octicon]).toBeDefined()
expect(typeof OCTICON_COMPONENTS[octicon]).toBe('object')
})
})
test('maps specific octicons to correct components', () => {
expect(OCTICON_COMPONENTS.bug).toBe(BugIcon)
expect(OCTICON_COMPONENTS.rocket).toBe(RocketIcon)
expect(OCTICON_COMPONENTS.copilot).toBe(CopilotIcon)
})
})
describe('isValidOcticon', () => {
test('returns true for valid octicons', () => {
expect(isValidOcticon('bug')).toBe(true)
expect(isValidOcticon('rocket')).toBe(true)
expect(isValidOcticon('shield-lock')).toBe(true)
})
test('returns false for invalid octicons', () => {
expect(isValidOcticon('invalid-octicon')).toBe(false)
expect(isValidOcticon('pizza')).toBe(false)
expect(isValidOcticon('')).toBe(false)
})
test('returns false for null or undefined', () => {
expect(isValidOcticon(null)).toBe(false)
expect(isValidOcticon(undefined as any)).toBe(false)
})
test('provides correct type narrowing', () => {
const testOcticon: string | null = 'bug'
if (isValidOcticon(testOcticon)) {
// This should compile without type errors
const validOcticon: ValidOcticon = testOcticon
expect(validOcticon).toBe('bug')
}
})
})
describe('getOcticonComponent', () => {
test('returns correct component for valid octicons', () => {
expect(getOcticonComponent('bug')).toBe(BugIcon)
expect(getOcticonComponent('rocket')).toBe(RocketIcon)
expect(getOcticonComponent('copilot')).toBe(CopilotIcon)
})
test('returns CopilotIcon as fallback for undefined', () => {
expect(getOcticonComponent(undefined)).toBe(CopilotIcon)
})
test('returns CopilotIcon as fallback for invalid octicons', () => {
// TypeScript should prevent this, but test runtime behavior
expect(getOcticonComponent('invalid' as ValidOcticon)).toBe(CopilotIcon)
})
})
describe('type safety', () => {
test('ValidOcticon type includes all expected values', () => {
// This test ensures the type system prevents invalid octicons at compile time
// Test a few key octicons to verify the type works correctly
const testOcticons: ValidOcticon[] = ['bug', 'rocket', 'copilot']
testOcticons.forEach((octicon) => {
expect(VALID_OCTICONS.includes(octicon)).toBe(true)
})
})
})
describe('consistency checks', () => {
test('OCTICON_COMPONENTS keys match VALID_OCTICONS', () => {
const componentKeys = Object.keys(OCTICON_COMPONENTS)
const validOcticonsSet = new Set(VALID_OCTICONS)
componentKeys.forEach((key) => {
expect(validOcticonsSet.has(key as ValidOcticon)).toBe(true)
})
expect(componentKeys).toHaveLength(VALID_OCTICONS.length)
})
test('no duplicate octicons in VALID_OCTICONS', () => {
const octiconsSet = new Set(VALID_OCTICONS)
expect(octiconsSet.size).toBe(VALID_OCTICONS.length)
})
})
describe('single source of truth', () => {
test('VALID_OCTICONS is derived from OCTICON_COMPONENTS', () => {
const componentKeys = Object.keys(OCTICON_COMPONENTS).sort()
const validOcticons = [...VALID_OCTICONS].sort()
expect(validOcticons).toEqual(componentKeys)
})
test('ValidOcticon type matches OCTICON_COMPONENTS keys', () => {
// This test ensures the type system is correctly derived from the object
const testOcticon: ValidOcticon = 'bug'
expect(OCTICON_COMPONENTS[testOcticon]).toBeDefined()
// Type check - this should compile without errors
const allKeys: ValidOcticon[] = Object.keys(OCTICON_COMPONENTS) as ValidOcticon[]
expect(allKeys.length).toBeGreaterThan(0)
})
test('adding new octicon only requires updating OCTICON_COMPONENTS', () => {
// This test documents the single source of truth approach
// If you add a new octicon to OCTICON_COMPONENTS:
// 1. ValidOcticon type automatically includes it
// 2. VALID_OCTICONS array automatically includes it
// 3. All validation functions work with it
const componentCount = Object.keys(OCTICON_COMPONENTS).length
const validOcticonsCount = VALID_OCTICONS.length
expect(componentCount).toBe(validOcticonsCount)
})
})
})

View File

@@ -1,3 +1,9 @@
import { ValidOcticon, isValidOcticon } from './lib/octicons'
// Re-export ValidOcticon and isValidOcticon for compatibility with existing imports
export type { ValidOcticon }
export { isValidOcticon }
// Base type for all TOC items with core properties
export type BaseTocItem = {
fullPath: string
@@ -5,22 +11,6 @@ export type BaseTocItem = {
intro?: string | null
}
// Valid octicon types that match the CookBookArticleCard component
export type ValidOcticon =
| 'code'
| 'log'
| 'terminal'
| 'bug'
| 'lightbulb'
| 'gear'
| 'rocket'
| 'beaker'
| 'copilot'
| 'hubot'
| 'book'
| 'shield-lock'
| 'lock'
// Extended type for child TOC items with additional metadata
export type ChildTocItem = BaseTocItem & {
octicon?: ValidOcticon | null
@@ -54,26 +44,6 @@ export type RawTocItem = {
childTocItems: RawTocItem[]
}
// Helper function to validate and cast octicon values
export function isValidOcticon(octicon: string | null): octicon is ValidOcticon {
const validOcticons: ValidOcticon[] = [
'code',
'log',
'terminal',
'bug',
'lightbulb',
'gear',
'rocket',
'beaker',
'copilot',
'hubot',
'book',
'shield-lock',
'lock',
]
return octicon !== null && validOcticons.includes(octicon as ValidOcticon)
}
// Simplified TOC item type for basic landing pages that don't need extended metadata
export type SimpleTocItem = {
fullPath: string