fix(site): resolve api reference slug mismatch on airplay and hlsjs pages (#1755)

This commit is contained in:
Wesley Luyten
2026-06-26 14:10:25 -07:00
committed by GitHub
parent d321522cf0
commit 29b3f0f00a
7 changed files with 62 additions and 15 deletions
@@ -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<string, string> = {
export const NAME_OVERRIDES: Record<string, string> = {
'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`.
@@ -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<string, string> = {
'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`).
@@ -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;
@@ -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;
+25
View File
@@ -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<string, string> = {
'pip-button': 'PiPButton',
'airplay-button': 'AirPlayButton',
'hlsjs-video': 'HlsJsVideo',
};
const NAME_TO_SLUG: Record<string, string> = 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);
}
+5 -5
View File
@@ -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);
@@ -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');
});
});