From 5fb268c268bc9f7c089a2a58d0ab8ed27ffbf784 Mon Sep 17 00:00:00 2001 From: Rahim Date: Sun, 6 Sep 2026 20:23:57 -0700 Subject: [PATCH 1/2] fix(sandbox): keep the Sandbox's own cn helper after installing registry skins Shadcn's `utils` registry item for Tailwind v4 projects now re-exports `cn` from the new `cn` package. The install fixture's `--overwrite` let that item replace the fixture helper, and the Sandbox copied it without installing the package, so Vite failed to resolve `cn` and the sandbox build broke. Write the helper built from `clsx` and `tailwind-merge` again before copying the installed lib. Co-Authored-By: Claude Fable 5.1 --- apps/sandbox/scripts/sync-source-owned-skins.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/sandbox/scripts/sync-source-owned-skins.ts b/apps/sandbox/scripts/sync-source-owned-skins.ts index 21d5c13441..34fd6cadcf 100644 --- a/apps/sandbox/scripts/sync-source-owned-skins.ts +++ b/apps/sandbox/scripts/sync-source-owned-skins.ts @@ -20,6 +20,9 @@ const presets = ['video', 'audio', 'live-video', 'live-audio'] as const; const variants = ['', '-minimal'] as const; const inWorkspace = existsSync(resolve(workspaceDir, 'pnpm-workspace.yaml')); +/** The Shadcn `cn` helper on the packages the Sandbox depends on, kept even after the registry's `utils` item runs. */ +const CN_UTILS = `import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n`; + const localRegistry = existsSync(resolve(registryDir, 'react/registry.json')); const server = localRegistry ? createServer() : undefined; const address = server @@ -113,6 +116,9 @@ async function installCatalog(install: (typeof installs)[number], address: strin await cp(resolve(root, 'src/components/videojs'), destination, { recursive: true }); if (install.catalog.startsWith('react')) { + // The registry's `utils` item overwrites the fixture's `cn` with whatever Shadcn ships today, and that may + // import a package the Sandbox never installs. The copied helpers keep the `cn` built from what it does. + await writeFile(resolve(root, 'src/lib/utils.ts'), CN_UTILS); await cp(resolve(root, 'src/lib'), resolve(destination, '../../lib'), { recursive: true }); } @@ -179,10 +185,7 @@ async function writeFixture(root: string, address: string, alias: string): Promi await writeFile(resolve(root, 'components.json'), `${JSON.stringify(components, null, 2)}\n`); await writeFile(resolve(root, 'tsconfig.json'), `${JSON.stringify(tsconfig, null, 2)}\n`); await writeFile(resolve(root, 'src/index.css'), '@import "./components/videojs/styles/theme.css";\n'); - await writeFile( - resolve(root, 'src/lib/utils.ts'), - `import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n` - ); + await writeFile(resolve(root, 'src/lib/utils.ts'), CN_UTILS); } async function runCommand( From 1bfa7c30d00283b661b6619937fd5b620541c161 Mon Sep 17 00:00:00 2001 From: Rahim Date: Sun, 6 Sep 2026 21:16:27 -0700 Subject: [PATCH 2/2] fix(packages): adopt cn for class merging and the sandbox registry install Shadcn's `utils` registry item now re-exports `cn` from the `cn` package, so the registry skins pull that package in. Emit `ClassValue` from `cn` instead of `clsx` in the React registry items, merge utilities with `cn`'s `twMerge` in vjsc, and give the Sandbox the `cn` dependency the installed helper needs instead of rewriting it after `shadcn add`. --- apps/sandbox/package.json | 3 +- .../scripts/sync-source-owned-skins.ts | 11 ++------ packages/skins/build/packages/react.ts | 2 +- packages/skins/build/target/react.tsx | 2 +- packages/skins/package.json | 2 +- packages/skins/src/utils.ts | 2 +- packages/vjsc/package.json | 2 +- packages/vjsc/src/styles/resolved.ts | 2 +- pnpm-lock.yaml | 28 +++++++++++-------- 9 files changed, 25 insertions(+), 29 deletions(-) diff --git a/apps/sandbox/package.json b/apps/sandbox/package.json index 3cdb5e7194..dd2d2ae429 100644 --- a/apps/sandbox/package.json +++ b/apps/sandbox/package.json @@ -34,8 +34,7 @@ "@videojs/vimeo-video": "workspace:*", "@videojs/wistia-video": "workspace:*", "@videojs/youtube-video": "workspace:*", - "clsx": "^2.1.1", - "tailwind-merge": "^3.5.0" + "cn": "^0.2.5" }, "devDependencies": { "@tailwindcss/vite": "^4.3.3", diff --git a/apps/sandbox/scripts/sync-source-owned-skins.ts b/apps/sandbox/scripts/sync-source-owned-skins.ts index 34fd6cadcf..268405fa5c 100644 --- a/apps/sandbox/scripts/sync-source-owned-skins.ts +++ b/apps/sandbox/scripts/sync-source-owned-skins.ts @@ -20,9 +20,6 @@ const presets = ['video', 'audio', 'live-video', 'live-audio'] as const; const variants = ['', '-minimal'] as const; const inWorkspace = existsSync(resolve(workspaceDir, 'pnpm-workspace.yaml')); -/** The Shadcn `cn` helper on the packages the Sandbox depends on, kept even after the registry's `utils` item runs. */ -const CN_UTILS = `import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n`; - const localRegistry = existsSync(resolve(registryDir, 'react/registry.json')); const server = localRegistry ? createServer() : undefined; const address = server @@ -116,9 +113,6 @@ async function installCatalog(install: (typeof installs)[number], address: strin await cp(resolve(root, 'src/components/videojs'), destination, { recursive: true }); if (install.catalog.startsWith('react')) { - // The registry's `utils` item overwrites the fixture's `cn` with whatever Shadcn ships today, and that may - // import a package the Sandbox never installs. The copied helpers keep the `cn` built from what it does. - await writeFile(resolve(root, 'src/lib/utils.ts'), CN_UTILS); await cp(resolve(root, 'src/lib'), resolve(destination, '../../lib'), { recursive: true }); } @@ -138,9 +132,8 @@ async function writeFixture(root: string, address: string, alias: string): Promi '@videojs/core': '*', '@videojs/html': '10.0.0-beta.32', '@videojs/react': '10.0.0-beta.32', - clsx: '*', + cn: '*', react: '*', - 'tailwind-merge': '*', }, }; const components = { @@ -185,7 +178,7 @@ async function writeFixture(root: string, address: string, alias: string): Promi await writeFile(resolve(root, 'components.json'), `${JSON.stringify(components, null, 2)}\n`); await writeFile(resolve(root, 'tsconfig.json'), `${JSON.stringify(tsconfig, null, 2)}\n`); await writeFile(resolve(root, 'src/index.css'), '@import "./components/videojs/styles/theme.css";\n'); - await writeFile(resolve(root, 'src/lib/utils.ts'), CN_UTILS); + await writeFile(resolve(root, 'src/lib/utils.ts'), "export { cn } from 'cn';\n"); } async function runCommand( diff --git a/packages/skins/build/packages/react.ts b/packages/skins/build/packages/react.ts index 040bf89f9f..cdd5398611 100644 --- a/packages/skins/build/packages/react.ts +++ b/packages/skins/build/packages/react.ts @@ -173,7 +173,7 @@ function collectSharedSourcePaths(skins: readonly SkinRoot[]): ReadonlySet = defineComponent }, }, types: { - ClassNameValue: { from: 'clsx', name: 'ClassValue' }, + ClassNameValue: { from: 'cn', name: 'ClassValue' }, PropsOf: { from: 'react', name: 'ComponentProps' }, VjscNode: { from: 'react', name: 'ReactNode' }, VjscElement: { from: 'react', name: 'ReactElement' }, diff --git a/packages/skins/package.json b/packages/skins/package.json index cf05b37445..e86396b08b 100644 --- a/packages/skins/package.json +++ b/packages/skins/package.json @@ -33,7 +33,7 @@ "@videojs/icons": "workspace:*", "@videojs/utils": "workspace:*", "@vitejs/plugin-react": "^6.0.4", - "clsx": "^2.1.1", + "cn": "^0.2.5", "dashjs": "^5.2.0", "hls.js": "^1.6.7", "mux-embed": "5.17.10", diff --git a/packages/skins/src/utils.ts b/packages/skins/src/utils.ts index 949d1f1207..87395665bc 100644 --- a/packages/skins/src/utils.ts +++ b/packages/skins/src/utils.ts @@ -1,4 +1,4 @@ -import type { ClassValue } from 'clsx'; +import type { ClassValue } from 'cn'; export { cn } from '@videojs/utils/style'; diff --git a/packages/vjsc/package.json b/packages/vjsc/package.json index f23b5ee710..925c1b3697 100644 --- a/packages/vjsc/package.json +++ b/packages/vjsc/package.json @@ -87,13 +87,13 @@ "@oxc-project/types": "0.146.0", "@tailwindcss/node": "4.2.1", "@videojs/utils": "workspace:*", + "cn": "^0.2.5", "lightningcss": "^1.32.0", "magic-string": "^1.2.2", "oxc-parser": "0.146.0", "oxc-walker": "^1.1.1", "rolldown": "~1.2.5", "shadcn": "^4.16.2", - "tailwind-merge": "^3.5.0", "tailwindcss": "4.2.1" }, "devDependencies": { diff --git a/packages/vjsc/src/styles/resolved.ts b/packages/vjsc/src/styles/resolved.ts index 9e66fde358..3bb0e177d3 100644 --- a/packages/vjsc/src/styles/resolved.ts +++ b/packages/vjsc/src/styles/resolved.ts @@ -2,8 +2,8 @@ import { realpath } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { twMerge } from 'cn'; import { type OutputChunk, rolldown } from 'rolldown'; -import { twMerge } from 'tailwind-merge'; import { toArray } from '../utils/array'; import { splitClassNames } from './class-names'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1396a13e5b..2b0deee301 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,12 +210,9 @@ importers: '@videojs/youtube-video': specifier: workspace:* version: link:../../packages/adapters/youtube-video - clsx: - specifier: ^2.1.1 - version: 2.1.1 - tailwind-merge: - specifier: ^3.5.0 - version: 3.5.0 + cn: + specifier: ^0.2.5 + version: 0.2.5 devDependencies: '@tailwindcss/vite': specifier: ^4.3.3 @@ -1035,9 +1032,9 @@ importers: '@vitejs/plugin-react': specifier: ^6.0.4 version: 6.0.4(@voidzero-dev/vite-plus-core@0.2.8(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.1)(typescript@6.0.2)(unrun@0.2.39)(yaml@2.9.0))(babel-plugin-react-compiler@1.0.0) - clsx: - specifier: ^2.1.1 - version: 2.1.1 + cn: + specifier: ^0.2.5 + version: 0.2.5 dashjs: specifier: ^5.2.0 version: 5.2.0(@svta/cml-cta@1.0.6(@svta/cml-structured-field-values@1.1.3(@svta/cml-utils@1.5.0))(@svta/cml-utils@1.5.0))(@svta/cml-structured-field-values@1.1.3(@svta/cml-utils@1.5.0))(@svta/cml-utils@1.5.0) @@ -1160,6 +1157,9 @@ importers: '@videojs/utils': specifier: workspace:* version: link:../utils + cn: + specifier: ^0.2.5 + version: 0.2.5 lightningcss: specifier: ^1.32.0 version: 1.32.0 @@ -1178,9 +1178,6 @@ importers: shadcn: specifier: ^4.16.2 version: 4.16.2(supports-color@8.1.1)(typescript@6.0.2) - tailwind-merge: - specifier: ^3.5.0 - version: 3.5.0 tailwindcss: specifier: 4.2.1 version: 4.2.1 @@ -6036,6 +6033,11 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cn@0.2.5: + resolution: {integrity: sha512-OCjZtMeQfXbI4Es1+EIjkd77gvWzaE689gD8KhfexlqjClC06qR1MQBR+Z35ZMSPNEBWyHiItW1Soy0UvwNv9w==} + engines: {node: '>=20'} + hasBin: true + code-block-writer@13.0.3: resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} @@ -15397,6 +15399,8 @@ snapshots: clsx@2.1.1: {} + cn@0.2.5: {} + code-block-writer@13.0.3: {} codem-isoboxer@0.3.10: {}