From 29b3f0f00abb97d48208e46bad41e8a7c7c78ebd Mon Sep 17 00:00:00 2001 From: Wesley Luyten Date: Fri, 26 Jun 2026 14:10:25 -0700 Subject: [PATCH] fix(site): resolve api reference slug mismatch on airplay and hlsjs pages (#1755) --- .../references/builder-conventions.md | 7 ++++-- site/scripts/api-docs-builder/src/pipeline.ts | 8 +++--- .../api-reference/ComponentReference.astro | 4 +-- .../docs/api-reference/MediaReference.astro | 4 +-- site/src/utils/api-reference-overrides.ts | 25 +++++++++++++++++++ site/src/utils/remarkConditionalHeadings.js | 10 ++++---- .../tests/api-reference-overrides.test.ts | 19 ++++++++++++++ 7 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 site/src/utils/api-reference-overrides.ts create mode 100644 site/src/utils/tests/api-reference-overrides.test.ts diff --git a/.claude/skills/api-reference/references/builder-conventions.md b/.claude/skills/api-reference/references/builder-conventions.md index de90c6ac..78744732 100644 --- a/.claude/skills/api-reference/references/builder-conventions.md +++ b/.claude/skills/api-reference/references/builder-conventions.md @@ -28,16 +28,19 @@ The builder derives PascalCase from kebab-case using `kebabCase` from es-toolkit ## NAME_OVERRIDES -When kebab-to-pascal conversion doesn't produce the correct name, add an override in `site/scripts/api-docs-builder/src/index.ts`: +When kebab-to-pascal conversion doesn't produce the correct name, add an override in `site/src/utils/api-reference-overrides.ts` (the shared map the builder imports and the reference pages invert for slug lookup): ```ts -const NAME_OVERRIDES: Record = { +export const NAME_OVERRIDES: Record = { 'pip-button': 'PiPButton', + 'airplay-button': 'AirPlayButton', }; ``` Use overrides only when the standard conversion fails (e.g., acronyms like PiP). Prefer aligning component naming with the standard conversion when possible. +The same map covers media elements whose PascalCase name doesn't kebab-case to their element tag name (e.g. `'hlsjs-video': 'HlsJsVideo'`). It is keyed by the generated-reference file slug regardless of component vs. media. + ## Multi-Part Components **Detection**: Presence of `packages/react/src/ui/{name}/index.parts.ts`. diff --git a/site/scripts/api-docs-builder/src/pipeline.ts b/site/scripts/api-docs-builder/src/pipeline.ts index 01c62e11..4554059e 100644 --- a/site/scripts/api-docs-builder/src/pipeline.ts +++ b/site/scripts/api-docs-builder/src/pipeline.ts @@ -8,6 +8,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as ts from 'typescript'; import * as tae from 'typescript-api-extractor'; +import { NAME_OVERRIDES } from '../../../src/utils/api-reference-overrides.js'; import { extractCore } from './core-handler.js'; import { extractCSSVars } from './css-vars-handler.js'; import { extractDataAttrs } from './data-attrs-handler.js'; @@ -31,10 +32,9 @@ import { kebabToPascal, partKebabFromSource, sortProps } from './utils.js'; // ─── Overrides ───────────────────────────────────────────────────── -// Components whose PascalCase name doesn't match simple kebab-to-pascal conversion. -export const NAME_OVERRIDES: Record = { - 'pip-button': 'PiPButton', -}; +// `NAME_OVERRIDES` is the source of truth shared with the site's reference +// pages — re-exported here for the builder's existing import surface. +export { NAME_OVERRIDES }; // Parts whose HTML element file doesn't follow the `{component}-{part}-element.ts` convention. // Key: `{component}/{part-kebab}`, Value: element file basename (without `.ts`). diff --git a/site/src/components/docs/api-reference/ComponentReference.astro b/site/src/components/docs/api-reference/ComponentReference.astro index e5fb03d8..a76060cf 100644 --- a/site/src/components/docs/api-reference/ComponentReference.astro +++ b/site/src/components/docs/api-reference/ComponentReference.astro @@ -1,6 +1,5 @@ --- import { getEntry } from 'astro:content'; -import { kebabCase } from 'es-toolkit/string'; import ContentWidth from '@/components/frames/ContentWidth.astro'; import H2 from '@/components/typography/H2Markdown.astro'; import H3 from '@/components/typography/H3Markdown.astro'; @@ -9,6 +8,7 @@ import MarkdownCode from '@/components/typography/MarkdownCode.astro'; import P from '@/components/typography/P.astro'; import type { ComponentReference } from '@/types/component-reference'; import { isValidFramework } from '@/types/docs'; +import { resolveReferenceSlug } from '@/utils/api-reference-overrides'; import { createComponentReferenceModel } from '@/utils/componentReferenceModel'; import FrameworkCase from '../FrameworkCase.astro'; import ApiCSSVarsTable from './ApiCSSVarsTable.astro'; @@ -28,7 +28,7 @@ if (!framework || !isValidFramework(framework)) { throw new Error(`Invalid or missing framework param.`); } -const entry = await getEntry('componentReference', kebabCase(component)); +const entry = await getEntry('componentReference', resolveReferenceSlug(component)); const apiRef: ComponentReference | null = entry?.data ?? null; if (!apiRef) return; diff --git a/site/src/components/docs/api-reference/MediaReference.astro b/site/src/components/docs/api-reference/MediaReference.astro index 14fc818e..d972db90 100644 --- a/site/src/components/docs/api-reference/MediaReference.astro +++ b/site/src/components/docs/api-reference/MediaReference.astro @@ -1,6 +1,5 @@ --- import { getEntry } from 'astro:content'; -import { kebabCase } from 'es-toolkit/string'; import ContentWidth from '@/components/frames/ContentWidth.astro'; import A from '@/components/typography/A.astro'; import H2 from '@/components/typography/H2Markdown.astro'; @@ -14,6 +13,7 @@ import Th from '@/components/typography/Th.astro'; import Thead from '@/components/typography/Thead.astro'; import Tr from '@/components/typography/Tr.astro'; import type { MediaReference } from '@/types/media-reference'; +import { resolveReferenceSlug } from '@/utils/api-reference-overrides'; import { createMediaReferenceModel } from '@/utils/mediaReferenceModel'; import ApiCSSVarsTable from './ApiCSSVarsTable.astro'; import CodeChip from './CodeChip.astro'; @@ -26,7 +26,7 @@ interface Props { const { media } = Astro.props; const framework = Astro.params.framework === 'react' ? 'react' : 'html'; -const entry = await getEntry('mediaReference', kebabCase(media)); +const entry = await getEntry('mediaReference', resolveReferenceSlug(media)); const ref: MediaReference | null = entry?.data ?? null; if (!ref) return; diff --git a/site/src/utils/api-reference-overrides.ts b/site/src/utils/api-reference-overrides.ts new file mode 100644 index 00000000..fd11cbe5 --- /dev/null +++ b/site/src/utils/api-reference-overrides.ts @@ -0,0 +1,25 @@ +import { kebabCase } from 'es-toolkit/string'; + +/** + * Reference entries whose PascalCase name doesn't match a simple + * kebab-to-pascal conversion. Keyed by generated-reference file slug (a + * component's kebab directory name or a media element's tag name) → + * PascalCase name. + * + * Consumed by the api-docs-builder for component generation, and inverted + * below so reference pages can resolve a file slug from the public name. + */ +export const NAME_OVERRIDES: Record = { + 'pip-button': 'PiPButton', + 'airplay-button': 'AirPlayButton', + 'hlsjs-video': 'HlsJsVideo', +}; + +const NAME_TO_SLUG: Record = Object.fromEntries( + Object.entries(NAME_OVERRIDES).map(([slug, name]) => [name, slug]) +); + +/** Resolve a reference's generated file slug from its PascalCase name. */ +export function resolveReferenceSlug(name: string): string { + return NAME_TO_SLUG[name] ?? kebabCase(name); +} diff --git a/site/src/utils/remarkConditionalHeadings.js b/site/src/utils/remarkConditionalHeadings.js index 283d6cbe..c927557d 100644 --- a/site/src/utils/remarkConditionalHeadings.js +++ b/site/src/utils/remarkConditionalHeadings.js @@ -3,6 +3,7 @@ import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { kebabCase } from 'es-toolkit/string'; import GithubSlugger from 'github-slugger'; +import { resolveReferenceSlug } from './api-reference-overrides'; import { buildComponentReferenceTocHeadings, createComponentReferenceModel } from './componentReferenceModel'; import { buildFeatureReferenceTocHeadings, createFeatureReferenceModel } from './featureReferenceModel'; import { buildMediaReferenceTocHeadings, createMediaReferenceModel } from './mediaReferenceModel'; @@ -14,9 +15,8 @@ const FEATURE_REF_DIR = path.resolve(__dirname, '../content/generated-feature-re const UTIL_REF_DIR = path.resolve(__dirname, '../content/generated-util-reference'); const MEDIA_REF_DIR = path.resolve(__dirname, '../content/generated-media-reference'); -function readComponentRefJson(componentName) { - const kebab = kebabCase(componentName); - const filePath = path.join(COMPONENT_REF_DIR, `${kebab}.json`); +function readComponentRefJson(slug) { + const filePath = path.join(COMPONENT_REF_DIR, `${slug}.json`); try { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { @@ -142,7 +142,7 @@ function injectComponentReferenceHeadings(node, headingsWithMetadata, reservedSl const componentName = typeof componentAttr?.value === 'string' ? componentAttr.value : null; if (!componentName) return; - const json = readComponentRefJson(componentName); + const json = readComponentRefJson(resolveReferenceSlug(componentName)); if (!json) return; const partOrderAttr = node.attributes?.find((a) => a.name === 'partOrder'); @@ -226,7 +226,7 @@ function injectMediaReferenceHeadings(node, headingsWithMetadata, reservedSlugs) const mediaName = typeof mediaAttr?.value === 'string' ? mediaAttr.value : null; if (!mediaName) return; - const json = readMediaRefJson(kebabCase(mediaName)); + const json = readMediaRefJson(resolveReferenceSlug(mediaName)); if (!json) return; const mediaModel = createMediaReferenceModel(mediaName, json); diff --git a/site/src/utils/tests/api-reference-overrides.test.ts b/site/src/utils/tests/api-reference-overrides.test.ts new file mode 100644 index 00000000..82b4f507 --- /dev/null +++ b/site/src/utils/tests/api-reference-overrides.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import { resolveReferenceSlug } from '../api-reference-overrides'; + +describe('resolveReferenceSlug', () => { + it('kebab-cases names without an override', () => { + expect(resolveReferenceSlug('PlayButton')).toBe('play-button'); + expect(resolveReferenceSlug('VolumeSlider')).toBe('volume-slider'); + expect(resolveReferenceSlug('SimpleHlsVideo')).toBe('simple-hls-video'); + }); + + it('uses the inverted NAME_OVERRIDES map for special casing', () => { + // kebabCase('PiPButton') would be 'pi-p-button'; the override maps it back. + expect(resolveReferenceSlug('PiPButton')).toBe('pip-button'); + // kebabCase('AirPlayButton') would be 'air-play-button'. + expect(resolveReferenceSlug('AirPlayButton')).toBe('airplay-button'); + // kebabCase('HlsJsVideo') would be 'hls-js-video'; the tag name is 'hlsjs-video'. + expect(resolveReferenceSlug('HlsJsVideo')).toBe('hlsjs-video'); + }); +});