fix(site): skip docs links for features without reference pages (#1394)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-07-02 11:04:31 -07:00
committed by GitHub
co-authored by Claude
parent fb50e331f2
commit 2a8cbd0df4
8 changed files with 115 additions and 34 deletions
@@ -193,6 +193,22 @@ function main() {
log.info(`Done! Generated ${presetSuccessCount} preset files.`);
// Report features referenced by presets that lack a reference page.
// These render as plain text in the preset table instead of links.
const unlinked = new Map<string, string>();
for (const { reference } of presetResults) {
for (const feature of reference.features) {
if (!feature.hasReference) unlinked.set(feature.name, feature.slug);
}
}
if (unlinked.size > 0) {
log.warn(`${unlinked.size} preset feature(s) have no reference page:`);
for (const [name, slug] of unlinked) {
log.warn(` - ${name} → site/src/content/docs/${slug}.mdx (missing)`);
}
}
console.warn = originalWarn;
if (errorCount > 0) {
@@ -552,11 +552,17 @@ export interface PresetSkinDef {
cssImport?: string;
}
export interface PresetFeatureRef {
name: string;
slug: string;
hasReference: boolean;
}
export interface PresetReference {
name: string;
description?: string;
featureBundle: string;
features: string[];
features: PresetFeatureRef[];
html: {
skins: PresetSkinDef[];
mediaElement?: string;
@@ -24,7 +24,7 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as ts from 'typescript';
import type { PresetReference, PresetResult, PresetSkinDef } from './pipeline.js';
import type { PresetFeatureRef, PresetReference, PresetResult, PresetSkinDef } from './pipeline.js';
// ─── Types ──────────────────────────────────────────────────────────
@@ -307,6 +307,31 @@ function findReactMediaElement(filePath: string): string | undefined {
// ─── Feature Bundle Resolution ──────────────────────────────────────
/**
* Feature names whose kebab-cased form doesn't match the docs page slug.
* Example: `textTrack` → `feature-text-tracks.mdx`.
*/
const FEATURE_SLUG_OVERRIDES: Record<string, string> = {
textTrack: 'text-tracks',
};
function featureDocsSlug(featureName: string): string {
const override = FEATURE_SLUG_OVERRIDES[featureName];
if (override) return `reference/feature-${override}`;
const kebab = featureName.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
return `reference/feature-${kebab}`;
}
function featureReferenceExists(monorepoRoot: string, slug: string): boolean {
const mdxPath = path.join(monorepoRoot, 'site/src/content/docs', `${slug}.mdx`);
return fs.existsSync(mdxPath);
}
function resolveFeatureRef(name: string, monorepoRoot: string): PresetFeatureRef {
const slug = featureDocsSlug(name);
return { name, slug, hasReference: featureReferenceExists(monorepoRoot, slug) };
}
function parseFeatureBundles(presetsFilePath: string): Map<string, string[]> {
const map = new Map<string, string[]>();
if (!fs.existsSync(presetsFilePath)) return map;
@@ -416,7 +441,11 @@ function scanReactDirectory(scanDir: string, barrelPath: string, presetName: str
// ─── Preset Reference Building ──────────────────────────────────────
function buildPresetReference(preset: PresetInfo, featureBundleMap: Map<string, string[]>): PresetResult | null {
function buildPresetReference(
preset: PresetInfo,
featureBundleMap: Map<string, string[]>,
monorepoRoot: string
): PresetResult | null {
// Find feature bundle name from barrel files (try both frameworks)
const bundleName =
(preset.html && findFeatureBundleExport(preset.html.barrelPath)) ??
@@ -424,7 +453,8 @@ function buildPresetReference(preset: PresetInfo, featureBundleMap: Map<string,
if (!bundleName) return null;
const features = featureBundleMap.get(bundleName) ?? [];
const featureNames = featureBundleMap.get(bundleName) ?? [];
const features = featureNames.map((name) => resolveFeatureRef(name, monorepoRoot));
// Scan HTML directory
const htmlResult = preset.html ? scanHtmlDirectory(preset.html.scanDir) : { skins: [] as PresetSkinDef[] };
@@ -465,7 +495,7 @@ export function generatePresetReferences(monorepoRoot: string): PresetResult[] {
const results: PresetResult[] = [];
for (const preset of presets) {
const result = buildPresetReference(preset, featureBundleMap);
const result = buildPresetReference(preset, featureBundleMap, monorepoRoot);
if (result) results.push(result);
}
@@ -917,10 +917,30 @@ describe('Preset pipeline (end-to-end)', () => {
it('resolves feature names from the bundle', () => {
const ref = findPreset('video')!.reference;
expect(ref.features).toEqual(expect.arrayContaining(['playback', 'volume']));
expect(ref.features.map((f) => f.name)).toEqual(expect.arrayContaining(['playback', 'volume']));
expect(ref.features.length).toBe(2);
});
it('emits docs slugs for features', () => {
const ref = findPreset('video')!.reference;
const playback = ref.features.find((f) => f.name === 'playback');
const volume = ref.features.find((f) => f.name === 'volume');
expect(playback?.slug).toBe('reference/feature-playback');
expect(volume?.slug).toBe('reference/feature-volume');
});
it('flags hasReference true when the feature MDX page exists', () => {
const ref = findPreset('video')!.reference;
const playback = ref.features.find((f) => f.name === 'playback');
expect(playback?.hasReference).toBe(true);
});
it('flags hasReference false when the feature MDX page is missing', () => {
const ref = findPreset('video')!.reference;
const volume = ref.features.find((f) => f.name === 'volume');
expect(volume?.hasReference).toBe(false);
});
it('detects HTML skins with tagNames', () => {
const skins = findPreset('video')!.reference.html.skins;
expect(skins).toEqual(
@@ -974,7 +994,7 @@ describe('Preset pipeline (end-to-end)', () => {
it('resolves feature names (subset of video)', () => {
const ref = findPreset('audio')!.reference;
expect(ref.features).toEqual(['playback']);
expect(ref.features.map((f) => f.name)).toEqual(['playback']);
});
it('detects single HTML skin', () => {
@@ -1049,8 +1069,8 @@ describe('Preset pipeline (end-to-end)', () => {
const featureSlugs = featureResults.map((r) => r.slug);
const videoPreset = findPreset('video')!.reference;
for (const featureName of videoPreset.features) {
expect(featureSlugs).toContain(featureName);
for (const feature of videoPreset.features) {
expect(featureSlugs).toContain(feature.name);
}
});
});
@@ -0,0 +1,4 @@
---
title: Playback feature
description: Fixture stub used by the preset pipeline e2e test to exercise hasReference=true.
---
@@ -30,14 +30,6 @@ const presets: PresetReferenceType[] = entries
const { framework } = Astro.params;
const pkg = framework && isValidFramework(framework) ? `@videojs/${framework}` : '@videojs/html';
/**
* Feature slug overrides for cases where kebabCase(featureName) doesn't
* match the docs page slug.
*/
const FEATURE_SLUG_OVERRIDES: Record<string, string> = {
textTrack: 'text-tracks',
};
/**
* Native media elements used by presets that don't have a custom element.
* The pipeline can't detect these since they're not classes with `static tagName`.
@@ -47,13 +39,6 @@ const NATIVE_MEDIA_ELEMENTS: Record<string, string> = {
audio: 'audio',
};
function featureDocsSlug(featureName: string): string {
const override = FEATURE_SLUG_OVERRIDES[featureName];
if (override) return `reference/feature-${override}`;
const kebab = featureName.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
return `reference/feature-${kebab}`;
}
function htmlMediaElement(preset: PresetReferenceType): string | undefined {
return preset.html.mediaElement ?? NATIVE_MEDIA_ELEMENTS[preset.name];
}
@@ -122,19 +107,27 @@ const listFormat = new Intl.ListFormat('en', { style: 'long', type: 'unit' });
<dd>
{preset.features.length > 0
? listFormat
.formatToParts(preset.features)
.map((part) =>
part.type === "element" ? (
.formatToParts(preset.features.map((f) => f.name))
.map((part) => {
if (part.type !== "element") {
return <span>{part.value}</span>;
}
const feature = preset.features.find(
(f) => f.name === part.value,
)!;
return feature.hasReference ? (
<DocsLink
class="inline-block whitespace-nowrap"
slug={featureDocsSlug(part.value)}
slug={feature.slug}
>
{part.value}
{feature.name}
</DocsLink>
) : (
<span>{part.value}</span>
),
)
<span class="inline-block whitespace-nowrap">
{feature.name}
</span>
);
})
: ""}
</dd>
+8 -1
View File
@@ -9,11 +9,17 @@ export const PresetSkinDefSchema = z.object({
cssImport: z.string().optional(),
});
export const PresetFeatureRefSchema = z.object({
name: z.string(),
slug: z.string(),
hasReference: z.boolean(),
});
export const PresetReferenceSchema = z.object({
name: z.string(),
description: z.string().optional(),
featureBundle: z.string(),
features: z.array(z.string()),
features: z.array(PresetFeatureRefSchema),
html: z.object({
skins: z.array(PresetSkinDefSchema),
mediaElement: z.string().optional(),
@@ -25,4 +31,5 @@ export const PresetReferenceSchema = z.object({
});
export type PresetSkinDef = z.infer<typeof PresetSkinDefSchema>;
export type PresetFeatureRef = z.infer<typeof PresetFeatureRefSchema>;
export type PresetReference = z.infer<typeof PresetReferenceSchema>;
+6 -1
View File
@@ -4,7 +4,12 @@
"tasks": {
"api-docs": {
"dependsOn": ["^build"],
"outputs": ["src/content/generated-component-reference/**", "src/content/generated-util-reference/**"]
"outputs": [
"src/content/generated-component-reference/**",
"src/content/generated-util-reference/**",
"src/content/generated-feature-reference/**",
"src/content/generated-preset-reference/**"
]
},
"ejected-skins": {
"dependsOn": ["^build"],