feat(cli): add @videojs/cli docs command for LLM-friendly installation (#1214)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-04-14 11:07:49 -05:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 9681515446
commit 24b8b77c8a
49 changed files with 2061 additions and 460 deletions
+6
View File
@@ -0,0 +1,6 @@
---
---
<div class="contents" data-llms-ignore>
<slot />
</div>
+6
View File
@@ -0,0 +1,6 @@
---
---
<div hidden data-llms-only>
<slot />
</div>
@@ -1,8 +1,8 @@
import { useEffect, useRef } from 'react';
import ClientCode from '@/components/Code/ClientCode';
import { Tab, TabsList, TabsPanel, TabsRoot } from '@/components/Tabs';
import type { InstallMethod } from '@/stores/installation';
import { installMethod } from '@/stores/installation';
import type { InstallMethod } from '@/utils/installation/types';
import HTMLCdnCodeBlock from './HTMLCdnCodeBlock';
export default function HTMLInstallTabs() {
@@ -23,7 +23,6 @@ export default function HTMLInstallTabs() {
}
});
// Observe all tab elements for attribute changes
const tabs = root.querySelectorAll('[role="tab"]');
tabs.forEach((tab) => {
observer.observe(tab, { attributes: true, attributeFilter: ['data-tab-active'] });
@@ -1,134 +1,8 @@
import { useStore } from '@nanostores/react';
import ClientCode from '@/components/Code/ClientCode';
import { Tab, TabsList, TabsPanel, TabsRoot } from '@/components/Tabs';
import { VJS10_DEMO_VIDEO } from '@/consts';
import type { Renderer, Skin, UseCase } from '@/stores/installation';
import { installMethod, renderer, skin, sourceUrl, useCase } from '@/stores/installation';
function getRendererTag(renderer: Renderer): string {
const map: Record<Renderer, string> = {
'background-video': 'background-video',
// cloudflare: 'cloudflare-video',
// dash: 'dash-video',
hls: 'hls-video',
'html5-audio': 'audio',
'html5-video': 'video',
// jwplayer: 'jwplayer-video',
// 'mux-audio': 'mux-audio',
// 'mux-background-video': 'mux-background-video',
// 'mux-video': 'mux-video',
// shaka: 'shaka-video',
// spotify: 'spotify-audio',
// vimeo: 'vimeo-video',
// wistia: 'wistia-video',
// youtube: 'youtube-video',
};
return map[renderer];
}
function getProviderTag(useCase: UseCase): string {
const map: Record<UseCase, string> = {
'default-video': 'video-player',
'default-audio': 'audio-player',
'background-video': 'background-video-player',
};
return map[useCase];
}
function getSkinTag(useCase: UseCase, skin: Skin): string {
// Background video has fixed skin
if (useCase === 'background-video') {
return 'background-video-skin';
}
const map: Record<Skin, string> = {
video: 'video-skin',
audio: 'audio-skin',
'minimal-video': 'video-minimal-skin',
'minimal-audio': 'audio-minimal-skin',
};
return map[skin];
}
function isVideoLikeRenderer(renderer: Renderer): boolean {
return renderer === 'html5-video' || renderer === 'hls' || renderer === 'background-video';
}
function getRendererElement(renderer: Renderer, url: string): string {
const tag = getRendererTag(renderer);
const src = url.trim() || getDefaultSourceUrl(renderer);
const playsInline = isVideoLikeRenderer(renderer) ? ' playsinline' : '';
return `<${tag} src="${src}"${playsInline}></${tag}>`;
}
function getDefaultSourceUrl(renderer: Renderer): string {
switch (renderer) {
case 'hls':
return VJS10_DEMO_VIDEO.hls;
case 'background-video':
case 'html5-audio':
case 'html5-video':
default:
return VJS10_DEMO_VIDEO.mp4;
}
}
function generateHTMLCode(useCase: UseCase, skin: Skin, renderer: Renderer, url: string): string {
const providerTag = getProviderTag(useCase);
const skinTag = getSkinTag(useCase, skin);
const rendererElement = getRendererElement(renderer, url);
return `<!--
The PlayerProvider passes state between the UI components
and Media, and makes fully custom UIs possible.
It does not have layout by default (display:contents)
-->
<${providerTag}>
<!--
Skins contain the entire player UI and are easily swappable.
They can each be "ejected" for full control and customization
of UI components.
-->
<${skinTag}>
<!--
Media are players without UIs, handling networking
and display of the media. They are easily swappable
to handle different sources.
-->
${rendererElement}
</${skinTag}>
</${providerTag}>`;
}
function getSkinImportParts(skin: Skin): { group: string; skinFile: string } {
if (skin === 'minimal-video') return { group: 'video', skinFile: 'minimal-skin' };
if (skin === 'minimal-audio') return { group: 'audio', skinFile: 'minimal-skin' };
return { group: skin, skinFile: 'skin' };
}
function getMediaImportSubpath(renderer: Renderer): string | null {
const map: Partial<Record<Renderer, string>> = {
hls: 'hls-video',
// 'mux-audio': 'mux-audio',
// 'mux-background-video': 'mux-background-video',
// 'mux-video': 'mux-video',
};
return map[renderer] ?? null;
}
function generateJS(useCase: UseCase, skin: Skin, renderer: Renderer): string {
if (useCase === 'background-video') {
const mediaSubpath = getMediaImportSubpath(renderer);
const mediaImport = mediaSubpath ? `\nimport '@videojs/html/media/${mediaSubpath}';` : '';
return `import '@videojs/html/background/player';
import '@videojs/html/background/skin';
import '@videojs/html/background/video';${mediaImport}`;
}
const { group, skinFile } = getSkinImportParts(skin);
const mediaSubpath = getMediaImportSubpath(renderer);
const mediaImport = mediaSubpath ? `\nimport '@videojs/html/media/${mediaSubpath}';` : '';
return `import '@videojs/html/${group}/player';
import '@videojs/html/${group}/${skinFile}';${mediaImport}`;
}
import { generateHTMLUsageCode } from '@/utils/installation/codegen';
export default function HTMLUsageCodeBlock() {
const $useCase = useStore(useCase);
@@ -137,9 +11,17 @@ export default function HTMLUsageCodeBlock() {
const $installMethod = useStore(installMethod);
const $sourceUrl = useStore(sourceUrl);
const result = generateHTMLUsageCode({
useCase: $useCase,
skin: $skin,
renderer: $renderer,
sourceUrl: $sourceUrl,
installMethod: $installMethod,
});
return (
<>
{$installMethod !== 'cdn' && (
{result.js && (
<TabsRoot maxWidth={false}>
<TabsList label="HTML implementation">
<Tab value="javascript" initial>
@@ -147,7 +29,7 @@ export default function HTMLUsageCodeBlock() {
</Tab>
</TabsList>
<TabsPanel value="javascript" initial>
<ClientCode code={generateJS($useCase, $skin, $renderer)} lang="javascript" />
<ClientCode code={result.js} lang="javascript" />
</TabsPanel>
</TabsRoot>
)}
@@ -158,7 +40,7 @@ export default function HTMLUsageCodeBlock() {
</Tab>
</TabsList>
<TabsPanel value="html" initial>
<ClientCode code={generateHTMLCode($useCase, $skin, $renderer, $sourceUrl)} lang="html" />
<ClientCode code={result.html} lang="html" />
</TabsPanel>
</TabsRoot>
</>
@@ -1,143 +1,20 @@
import { useStore } from '@nanostores/react';
import ClientCode from '@/components/Code/ClientCode';
import { Tab, TabsList, TabsPanel, TabsRoot } from '@/components/Tabs';
import type { Renderer, Skin, UseCase } from '@/stores/installation';
import { renderer, skin, useCase } from '@/stores/installation';
function getRendererComponent(renderer: Renderer): string {
const map: Record<Renderer, string> = {
'background-video': 'BackgroundVideo',
// cloudflare: 'CloudflareVideo',
// dash: 'DashVideo',
hls: 'HlsVideo',
'html5-audio': 'Audio',
'html5-video': 'Video',
// jwplayer: 'JwplayerVideo',
// mux-audio: 'MuxAudio',
// mux-background-video: 'MuxBackgroundVideo',
// mux-video: 'MuxVideo',
// shaka: 'ShakaVideo',
// spotify: 'SpotifyAudio',
// vimeo: 'VimeoVideo',
// wistia: 'WistiaVideo',
// youtube: 'YoutubeVideo',
};
return map[renderer];
}
function getSkinComponent(skin: Skin): string {
const map: Record<Skin, string> = {
video: 'VideoSkin',
audio: 'AudioSkin',
'minimal-video': 'MinimalVideoSkin',
'minimal-audio': 'MinimalAudioSkin',
};
return map[skin];
}
function getSkinImportParts(skin: Skin): { group: string; skinFile: string } {
if (skin === 'minimal-video') return { group: 'video', skinFile: 'minimal-skin' };
if (skin === 'minimal-audio') return { group: 'audio', skinFile: 'minimal-skin' };
return { group: skin, skinFile: 'skin' };
}
function getUseCaseFeatures(useCase: UseCase): string {
const map: Record<UseCase, string> = {
'default-video': 'videoFeatures',
'default-audio': 'audioFeatures',
'background-video': 'backgroundFeatures',
};
return map[useCase];
}
function isPresetRenderer(renderer: Renderer): boolean {
return renderer === 'html5-video' || renderer === 'html5-audio' || renderer === 'background-video';
}
function isVideoLikeRenderer(renderer: Renderer): boolean {
return renderer === 'html5-video' || renderer === 'hls' || renderer === 'background-video';
}
function getRendererMediaSubpath(renderer: Renderer): string {
const map: Partial<Record<Renderer, string>> = {
// cloudflare: 'cloudflare-video',
// dash: 'dash-video',
hls: 'hls-video',
// jwplayer: 'jwplayer-video',
// 'mux-audio': 'mux-audio',
// 'mux-background-video': 'mux-background-video',
// 'mux-video': 'mux-video',
// spotify: 'spotify-audio',
// vimeo: 'vimeo-video',
// wistia: 'wistia-video',
// youtube: 'youtube-video',
};
return map[renderer] ?? renderer;
}
function generateReactCode(useCase: UseCase, skin: Skin, renderer: Renderer): string {
const rendererComponent = getRendererComponent(renderer);
const featureType = getUseCaseFeatures(useCase);
// Background video has fixed skin and subpath imports, others use skin picker value
const isBackgroundVideo = useCase === 'background-video';
const skinComponent = isBackgroundVideo ? 'BackgroundVideoSkin' : getSkinComponent(skin);
const { group, skinFile } = getSkinImportParts(skin);
const skinCssImport = isBackgroundVideo
? '@videojs/react/background/skin.css'
: `@videojs/react/${group}/${skinFile}.css`;
// Preset subpath where skin + default media components live
const presetSubpath = isBackgroundVideo ? 'background' : group;
// Skin and media imports — preset renderers share a subpath with the skin
let presetImport: string;
let mediaImport: string | null = null;
if (isPresetRenderer(renderer)) {
presetImport = `import { ${skinComponent}, ${rendererComponent} } from '@videojs/react/${presetSubpath}';`;
} else {
presetImport = `import { ${skinComponent} } from '@videojs/react/${presetSubpath}';`;
mediaImport = `import { ${rendererComponent} } from '@videojs/react/media/${getRendererMediaSubpath(renderer)}';`;
}
// Determine props — mux variants use src with stream.mux.com URL
const propsInterface = 'interface MyPlayerProps {\n src: string;\n}';
const destructuredProp = 'src';
const rendererProps = isVideoLikeRenderer(renderer) ? 'src={src} playsInline' : 'src={src}';
const rendererJsx = `<${rendererComponent} ${rendererProps} />`;
const imports = [
`import '${skinCssImport}';`,
`import { createPlayer, ${featureType} } from '@videojs/react';`,
presetImport,
...(mediaImport ? [mediaImport] : []),
].join('\n');
return `'use client';
${imports}
const Player = createPlayer({ features: ${featureType} });
${propsInterface}
export const MyPlayer = ({ ${destructuredProp} }: MyPlayerProps) => {
return (
<Player.Provider>
<${skinComponent}>
${rendererJsx}
</${skinComponent}>
</Player.Provider>
);
};`;
}
import { generateReactCreateCode } from '@/utils/installation/codegen';
export default function ReactCreateCodeBlock() {
const $useCase = useStore(useCase);
const $skin = useStore(skin);
const $renderer = useStore(renderer);
const result = generateReactCreateCode({
useCase: $useCase,
skin: $skin,
renderer: $renderer,
});
return (
<TabsRoot maxWidth={false}>
<TabsList label="React implementation">
@@ -146,7 +23,7 @@ export default function ReactCreateCodeBlock() {
</Tab>
</TabsList>
<TabsPanel value="react" initial>
<ClientCode code={generateReactCode($useCase, $skin, $renderer)} lang="tsx" />
<ClientCode code={result['MyPlayer.tsx']} lang="tsx" />
</TabsPanel>
</TabsRoot>
);
@@ -1,34 +1,18 @@
import { useStore } from '@nanostores/react';
import ClientCode from '@/components/Code/ClientCode';
import { Tab, TabsList, TabsPanel, TabsRoot } from '@/components/Tabs';
import { VJS10_DEMO_VIDEO } from '@/consts';
import type { Renderer } from '@/stores/installation';
import { renderer, sourceUrl } from '@/stores/installation';
function getDefaultSourceUrl(renderer: Renderer): string {
return renderer === 'hls' ? VJS10_DEMO_VIDEO.hls : VJS10_DEMO_VIDEO.mp4;
}
function generateUsageCode(url: string, renderer: Renderer): string {
const source = url.trim() || getDefaultSourceUrl(renderer);
const playerProp = `src="${source}"`;
return `import { MyPlayer } from '../components/player';
export const HomePage = () => {
return (
<div>
<h1>Welcome to My App</h1>
<MyPlayer ${playerProp} />
</div>
);
};`;
}
import { generateReactUsageCode } from '@/utils/installation/codegen';
export default function ReactUsageCodeBlock() {
const $renderer = useStore(renderer);
const $sourceUrl = useStore(sourceUrl);
const result = generateReactUsageCode({
renderer: $renderer,
sourceUrl: $sourceUrl,
});
return (
<TabsRoot maxWidth={false}>
<TabsList label="React usage">
@@ -37,7 +21,7 @@ export default function ReactUsageCodeBlock() {
</Tab>
</TabsList>
<TabsPanel value="react" initial>
<ClientCode code={generateUsageCode($sourceUrl, $renderer)} lang="tsx" />
<ClientCode code={result['App.tsx']} lang="tsx" />
</TabsPanel>
</TabsRoot>
);
@@ -1,9 +1,10 @@
import { useStore } from '@nanostores/react';
import { useEffect } from 'react';
import { Select, type SelectOption } from '@/components/Select';
import type { Renderer, UseCase } from '@/stores/installation';
import { renderer, sourceUrl, useCase, VALID_RENDERERS } from '@/stores/installation';
import { renderer, sourceUrl, useCase } from '@/stores/installation';
import { articleFor, detectRenderer } from '@/utils/installation/detect-renderer';
import type { Renderer, UseCase } from '@/utils/installation/types';
import { VALID_RENDERERS } from '@/utils/installation/types';
const RENDERER_LABELS: Record<Renderer, string> = {
'background-video': 'Background Video',
@@ -3,8 +3,8 @@ import { Minus, Sparkles } from 'lucide-react';
import { useEffect } from 'react';
import type { ImageRadioOption } from '@/components/ImageRadioGroup';
import ImageRadioGroup from '@/components/ImageRadioGroup';
import type { Skin } from '@/stores/installation';
import { skin, useCase } from '@/stores/installation';
import type { Skin } from '@/utils/installation/types';
const VIDEO_SKINS: ImageRadioOption<Skin>[] = [
{ value: 'video', label: 'Default', image: <Sparkles size={32} /> },
@@ -1,8 +1,8 @@
import { useStore } from '@nanostores/react';
import { Globe, Image } from 'lucide-react';
import ImageRadioGroup from '@/components/ImageRadioGroup';
import type { UseCase } from '@/stores/installation';
import { useCase } from '@/stores/installation';
import type { UseCase } from '@/utils/installation/types';
export default function UseCasePicker() {
const $useCase = useStore(useCase);