mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 05:37:21 +00:00
fix(docs): split ejected React skin sample into player + component files (#1588)
This commit is contained in:
@@ -14,7 +14,7 @@ const OUT_DIR = resolve(import.meta.dirname, '../apps/vite/src/_generated');
|
||||
interface EjectedSkinEntry {
|
||||
id: string;
|
||||
platform: string;
|
||||
tsx?: string;
|
||||
tsx?: Record<string, string>;
|
||||
css?: string;
|
||||
}
|
||||
|
||||
@@ -22,13 +22,19 @@ const skins: EjectedSkinEntry[] = JSON.parse(readFileSync(EJECTED_SKINS_JSON, 'u
|
||||
|
||||
const reactVideo = skins.find((s) => s.id === 'default-video-react');
|
||||
|
||||
if (!reactVideo?.tsx) {
|
||||
const componentSource = reactVideo?.tsx?.['VideoPlayer.tsx'];
|
||||
const playerSource = reactVideo?.tsx?.['player.ts'];
|
||||
|
||||
if (!componentSource || !playerSource) {
|
||||
throw new Error('Ejected skin "default-video-react" not found. Run `pnpm -F site ejected-skins` first.');
|
||||
}
|
||||
|
||||
mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
writeFileSync(resolve(OUT_DIR, 'ejected-react-video-skin.tsx'), reactVideo.tsx);
|
||||
// Component file is renamed to match the import path used in generate-pages.ts;
|
||||
// `player.ts` keeps its name so the relative `./player` import resolves.
|
||||
writeFileSync(resolve(OUT_DIR, 'ejected-react-video-skin.tsx'), componentSource);
|
||||
writeFileSync(resolve(OUT_DIR, 'player.ts'), playerSource);
|
||||
|
||||
if (reactVideo.css) {
|
||||
writeFileSync(resolve(OUT_DIR, 'player.css'), reactVideo.css);
|
||||
|
||||
@@ -79,8 +79,8 @@ interface EjectedSkinEntry {
|
||||
platform: 'html' | 'react';
|
||||
style: 'css' | 'tailwind';
|
||||
html?: string;
|
||||
tsx?: string;
|
||||
jsx?: string;
|
||||
tsx?: Record<string, string>;
|
||||
jsx?: Record<string, string>;
|
||||
css?: string;
|
||||
}
|
||||
|
||||
@@ -1397,11 +1397,17 @@ function destructureSkinProps(source: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten the skin into a Player component: merge SkinProps into PlayerProps
|
||||
* (adding `src`), inline the skin body into VideoPlayer/AudioPlayer wrapped
|
||||
* in `Player.Provider`, and remove the separate Skin export.
|
||||
* Flatten the skin into a Player component. Produces two files:
|
||||
* - `player.ts`: owns the `createPlayer({ features })` call and exports `Player`.
|
||||
* - `VideoPlayer.tsx` / `AudioPlayer.tsx`: imports `Player` from `./player` and
|
||||
* owns the React component. Splitting these avoids React Fast Refresh bailing
|
||||
* out (a file must export only components for Fast Refresh to apply edits).
|
||||
*
|
||||
* Also: merges SkinProps into PlayerProps (adding `src`), inlines the skin body
|
||||
* into VideoPlayer/AudioPlayer wrapped in `Player.Provider`, and removes the
|
||||
* separate Skin export.
|
||||
*/
|
||||
function flattenSkinIntoPlayer(source: string, mediaType: MediaType): string {
|
||||
function flattenSkinIntoPlayer(source: string, mediaType: MediaType): { player: string; component: string } {
|
||||
const isVideo = mediaType === 'video';
|
||||
const mediaTag = isVideo ? 'Video' : 'Audio';
|
||||
const features = isVideo ? 'videoFeatures' : 'audioFeatures';
|
||||
@@ -1409,25 +1415,28 @@ function flattenSkinIntoPlayer(source: string, mediaType: MediaType): string {
|
||||
const subpath = isVideo ? 'video' : 'audio';
|
||||
const playsInline = isVideo ? ' playsInline' : '';
|
||||
|
||||
// 1. Add createPlayer to the @videojs/react import
|
||||
source = source.replace(
|
||||
/import \{([^}]+)\} from '@videojs\/react';/,
|
||||
(_, names) => `import { createPlayer,${names}} from '@videojs/react';`
|
||||
);
|
||||
const player = [
|
||||
`import { createPlayer } from '@videojs/react';`,
|
||||
`import { ${features} } from '@videojs/react/${subpath}';`,
|
||||
'',
|
||||
`export const Player = createPlayer({ features: ${features} });`,
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
// 2. Add Video/Audio + features import and CSS import
|
||||
const mediaImport = `import { ${mediaTag}, ${features} } from '@videojs/react/${subpath}';`;
|
||||
// 1. Add Video/Audio import, CSS import, and Player import from ./player
|
||||
const mediaImport = `import { ${mediaTag} } from '@videojs/react/${subpath}';`;
|
||||
const cssImport = "import './player.css';";
|
||||
source = source.replace(/(import \{[^}]*\} from '@videojs\/react';)/, `$1\n${mediaImport}\n${cssImport}`);
|
||||
|
||||
// 3. Add Player const above the interface, rename SkinProps → PlayerProps, replace `children` with `src`
|
||||
const playerImport = "import { Player } from './player';";
|
||||
source = source.replace(
|
||||
/export interface \w+SkinProps/,
|
||||
`export const Player = createPlayer({ features: ${features} });\n\nexport interface ${playerName}Props`
|
||||
/(import \{[^}]*\} from '@videojs\/react';)/,
|
||||
`$1\n${mediaImport}\n${cssImport}\n${playerImport}`
|
||||
);
|
||||
|
||||
// 2. Rename SkinProps → PlayerProps, replace `children` with `src`
|
||||
source = source.replace(/export interface \w+SkinProps/, `export interface ${playerName}Props`);
|
||||
source = source.replace(/(\s*)children\?: ReactNode;/, `$1src: string;`);
|
||||
|
||||
// 4. Replace the skin function: rename, swap children→src, wrap in Player.Provider
|
||||
// 3. Replace the skin function: rename, swap children→src, wrap in Player.Provider
|
||||
// Match the destructured form: function XSkin({ children, className, poster, ...rest }: XSkinProps): ReactNode {
|
||||
source = source.replace(
|
||||
/export function \w+Skin\(\{ children, ([^}]+)\}: \w+SkinProps\): ReactNode \{\n([\s\S]*?)\n\}/,
|
||||
@@ -1452,7 +1461,7 @@ function flattenSkinIntoPlayer(source: string, mediaType: MediaType): string {
|
||||
const hasPoster = destructuredRest.includes('poster');
|
||||
const posterExample = hasPoster ? `\n * poster="${DEMO_POSTER_SRC}"` : '';
|
||||
|
||||
// Player const, @example JSDoc, and the function signature
|
||||
// @example JSDoc and the function signature
|
||||
const header = [
|
||||
'/**',
|
||||
' * @example',
|
||||
@@ -1469,17 +1478,19 @@ function flattenSkinIntoPlayer(source: string, mediaType: MediaType): string {
|
||||
}
|
||||
);
|
||||
|
||||
// 5. Remove the "Skin" section header (it's now part of "Player")
|
||||
// 4. Remove the "Skin" section header (it's now part of "Player")
|
||||
source = source.replace(/\/\/ =+\n\/\/ Skin\n\/\/ =+\n\n/, `${sectionHeader('Player')}\n\n`);
|
||||
|
||||
return source;
|
||||
return { player, component: source };
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a React skin: rewrite icon imports, resolve imports,
|
||||
* and produce both TSX and JSX versions.
|
||||
*/
|
||||
async function processReactSkin(skin: ReactSkinDef): Promise<{ tsx: string; jsx: string }> {
|
||||
async function processReactSkin(
|
||||
skin: ReactSkinDef
|
||||
): Promise<{ tsx: Record<string, string>; jsx: Record<string, string> }> {
|
||||
const absPath = resolve(ROOT, skin.source);
|
||||
let source = readFileSync(absPath, 'utf-8');
|
||||
source = rewriteReactIconImports(source);
|
||||
@@ -1521,12 +1532,21 @@ async function processReactSkin(skin: ReactSkinDef): Promise<{ tsx: string; jsx:
|
||||
// 10. Destructure skin props in function argument instead of body
|
||||
tsx = destructureSkinProps(tsx);
|
||||
|
||||
// 11. Flatten skin into player (merge props, inline body, wrap in Player.Provider)
|
||||
tsx = flattenSkinIntoPlayer(tsx, getSkinMediaType(skin));
|
||||
// 11. Flatten skin into player (split into player.ts + Player.tsx, wrap in Player.Provider)
|
||||
const mediaType = getSkinMediaType(skin);
|
||||
const { player, component } = flattenSkinIntoPlayer(tsx, mediaType);
|
||||
const componentFile = mediaType === 'video' ? 'VideoPlayer' : 'AudioPlayer';
|
||||
|
||||
const jsx = tsxToJsx(tsx);
|
||||
|
||||
return { tsx, jsx };
|
||||
return {
|
||||
tsx: {
|
||||
'player.ts': player,
|
||||
[`${componentFile}.tsx`]: component,
|
||||
},
|
||||
jsx: {
|
||||
'player.js': tsxToJsx(player),
|
||||
[`${componentFile}.jsx`]: tsxToJsx(component),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import { getEntry } from 'astro:content';
|
||||
import type { BundledLanguage } from 'shiki';
|
||||
import ServerCode from '@/components/Code/ServerCode.astro';
|
||||
import { Tab, TabsList, TabsPanel, TabsRoot } from '@/components/Tabs.tsx';
|
||||
|
||||
@@ -11,24 +12,50 @@ const { id } = Astro.props;
|
||||
const entry = await getEntry('ejectedSkins', id);
|
||||
if (!entry) return;
|
||||
const skin = entry.data;
|
||||
|
||||
const EXT_TO_LANG: Record<string, BundledLanguage> = {
|
||||
ts: 'ts',
|
||||
tsx: 'tsx',
|
||||
js: 'js',
|
||||
jsx: 'jsx',
|
||||
};
|
||||
|
||||
function langFromFilename(filename: string): BundledLanguage {
|
||||
const ext = filename.split('.').pop() ?? '';
|
||||
return EXT_TO_LANG[ext] ?? 'tsx';
|
||||
}
|
||||
|
||||
// Order React files so the component file is first (initial tab), then config files.
|
||||
const reactFiles = skin.tsx
|
||||
? Object.entries(skin.tsx).sort(([a], [b]) => {
|
||||
const aIsComponent = a.endsWith('.tsx') || a.endsWith('.jsx');
|
||||
const bIsComponent = b.endsWith('.tsx') || b.endsWith('.jsx');
|
||||
if (aIsComponent === bIsComponent) return a.localeCompare(b);
|
||||
return aIsComponent ? -1 : 1;
|
||||
})
|
||||
: [];
|
||||
---
|
||||
|
||||
{
|
||||
skin.platform === "react" ? (
|
||||
<TabsRoot client:idle>
|
||||
<TabsList client:idle label={`${skin.name} implementation`}>
|
||||
<Tab client:idle value="tsx" initial>
|
||||
Skin.tsx
|
||||
</Tab>
|
||||
{reactFiles.map(([filename], index) => (
|
||||
<Tab client:idle value={filename} initial={index === 0}>
|
||||
{filename}
|
||||
</Tab>
|
||||
))}
|
||||
{skin.css && (
|
||||
<Tab client:idle value="css">
|
||||
skin.css
|
||||
</Tab>
|
||||
)}
|
||||
</TabsList>
|
||||
<TabsPanel client:idle value="tsx" initial>
|
||||
<ServerCode code={skin.tsx!} lang="tsx" />
|
||||
</TabsPanel>
|
||||
{reactFiles.map(([filename, code], index) => (
|
||||
<TabsPanel client:idle value={filename} initial={index === 0}>
|
||||
<ServerCode code={code} lang={langFromFilename(filename)} />
|
||||
</TabsPanel>
|
||||
))}
|
||||
{skin.css && (
|
||||
<TabsPanel client:idle value="css">
|
||||
<ServerCode code={skin.css} lang="css" />
|
||||
|
||||
@@ -78,9 +78,14 @@ const minimalVideoReact = (await getEntry('ejectedSkins', 'minimal-video-react')
|
||||
<ServerCode slot="defaultHtmlCss" code={defaultVideo.css!} lang="css" />
|
||||
<ServerCode
|
||||
slot="defaultReactCode"
|
||||
code={defaultVideoReact.tsx!}
|
||||
code={defaultVideoReact.tsx!["VideoPlayer.tsx"]!}
|
||||
lang="tsx"
|
||||
/>
|
||||
<ServerCode
|
||||
slot="defaultReactPlayer"
|
||||
code={defaultVideoReact.tsx!["player.ts"]!}
|
||||
lang="ts"
|
||||
/>
|
||||
<ServerCode
|
||||
slot="defaultReactCss"
|
||||
code={defaultVideoReact.css!}
|
||||
@@ -94,9 +99,14 @@ const minimalVideoReact = (await getEntry('ejectedSkins', 'minimal-video-react')
|
||||
<ServerCode slot="minimalHtmlCss" code={minimalVideo.css!} lang="css" />
|
||||
<ServerCode
|
||||
slot="minimalReactCode"
|
||||
code={minimalVideoReact.tsx!}
|
||||
code={minimalVideoReact.tsx!["VideoPlayer.tsx"]!}
|
||||
lang="tsx"
|
||||
/>
|
||||
<ServerCode
|
||||
slot="minimalReactPlayer"
|
||||
code={minimalVideoReact.tsx!["player.ts"]!}
|
||||
lang="ts"
|
||||
/>
|
||||
<ServerCode
|
||||
slot="minimalReactCss"
|
||||
code={minimalVideoReact.css!}
|
||||
|
||||
@@ -7,10 +7,12 @@ interface EjectDemoProps {
|
||||
defaultHtmlCode: React.ReactNode;
|
||||
defaultHtmlCss: React.ReactNode;
|
||||
defaultReactCode: React.ReactNode;
|
||||
defaultReactPlayer: React.ReactNode;
|
||||
defaultReactCss: React.ReactNode;
|
||||
minimalHtmlCode: React.ReactNode;
|
||||
minimalHtmlCss: React.ReactNode;
|
||||
minimalReactCode: React.ReactNode;
|
||||
minimalReactPlayer: React.ReactNode;
|
||||
minimalReactCss: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -28,6 +30,7 @@ export default function EjectDemo(props: EjectDemoProps) {
|
||||
: isDefault
|
||||
? props.defaultReactCode
|
||||
: props.minimalReactCode;
|
||||
const playerSlot = isHtml ? null : isDefault ? props.defaultReactPlayer : props.minimalReactPlayer;
|
||||
const cssSlot = isHtml
|
||||
? isDefault
|
||||
? props.defaultHtmlCss
|
||||
@@ -44,6 +47,11 @@ export default function EjectDemo(props: EjectDemoProps) {
|
||||
<Tab variant="expanded" value="code" initial>
|
||||
{codeLabel}
|
||||
</Tab>
|
||||
{playerSlot && (
|
||||
<Tab variant="expanded" value="player">
|
||||
JS
|
||||
</Tab>
|
||||
)}
|
||||
<Tab variant="expanded" value="css">
|
||||
CSS
|
||||
</Tab>
|
||||
@@ -51,6 +59,11 @@ export default function EjectDemo(props: EjectDemoProps) {
|
||||
<TabsPanel variant="expanded" value="code" initial className="bg-faded-black dark:bg-soot m-2.5 mt-0">
|
||||
{codeSlot}
|
||||
</TabsPanel>
|
||||
{playerSlot && (
|
||||
<TabsPanel variant="expanded" value="player" className="bg-faded-black dark:bg-soot m-2.5 mt-0">
|
||||
{playerSlot}
|
||||
</TabsPanel>
|
||||
)}
|
||||
<TabsPanel variant="expanded" value="css" className="bg-faded-black dark:bg-soot m-2.5 mt-0">
|
||||
{cssSlot}
|
||||
</TabsPanel>
|
||||
|
||||
@@ -183,8 +183,8 @@ const ejectedSkins = defineCollection({
|
||||
platform: z.enum(['html', 'react']),
|
||||
style: z.enum(['css', 'tailwind']),
|
||||
html: z.string().optional(),
|
||||
tsx: z.string().optional(),
|
||||
jsx: z.string().optional(),
|
||||
tsx: z.record(z.string(), z.string()).optional(),
|
||||
jsx: z.record(z.string(), z.string()).optional(),
|
||||
css: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user