feat(sandbox): add README and sync script (#673)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam Potts
2026-03-02 13:35:51 +11:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 12277fdac8
commit 877e4d8b6c
5 changed files with 272 additions and 7 deletions
+57
View File
@@ -0,0 +1,57 @@
# @videojs/sandbox
Vite-based playground for testing and developing Video.js 10 integrations. Each sandbox is a standalone entry point that demonstrates a different platform or scenario.
## Sandboxes
| Name | Description |
| ------------------- | -------------------------------------- |
| `core` | Framework-agnostic core API |
| `html` | Web player with HTML custom elements |
| `html-background` | Full-screen background video (HTML) |
| `react` | React player with skin switching |
| `react-tailwind` | React player styled with Tailwind CSS |
| `react-background` | Full-screen background video (React) |
## Getting started
```bash
# From the repo root
pnpm dev
# Or just the sandbox
pnpm -F sandbox dev
```
This runs `setup.ts` first, which mirrors any missing files from `templates/` into `src/`, then starts the Vite dev server. Open the root URL to see links to all sandboxes.
## How it works
The package has two parallel directories:
- **`templates/`** — Checked into git. The source of truth for each sandbox's starting point.
- **`src/`** — Gitignored (except `index.html`). Your working copy where you freely edit, experiment, and break things.
On `pnpm dev`, `setup.ts` copies any file from `templates/` that doesn't already exist in `src/`. Existing files in `src/` are never overwritten, so your local changes are preserved across restarts.
## Syncing changes back to templates
When you've made improvements in `src/` that should become the new baseline:
```bash
pnpm -F sandbox sync
```
This shows a colored diff of every changed file, then prompts for confirmation before copying `src/` changes into `templates/`. Files that only exist in `templates/` are left untouched.
Sync when:
- You've fixed a bug or improved a sandbox and want to preserve it for others.
- You're preparing a commit — templates are what gets checked in.
## Adding a new sandbox
1. Create a directory in `templates/` (e.g. `templates/my-feature/`).
2. Add an `index.html` entry point and a `main.ts` or `main.tsx`.
3. Add a link to your sandbox in `templates/index.html`.
4. Register the entry in `vite.config.ts` under `rollupOptions.input`.
5. Run `pnpm dev``setup.ts` mirrors the new template into `src/` automatically.
+11 -3
View File
@@ -6,7 +6,8 @@
"scripts": {
"dev": "tsx setup.ts && vite",
"build": "tsx setup.ts && vite build",
"preview": "vite preview"
"preview": "vite preview",
"sync": "tsx scripts/sync.ts"
},
"dependencies": {
"@videojs/core": "workspace:*",
@@ -17,13 +18,20 @@
"@videojs/utils": "workspace:*"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.0",
"@types/fs-extra": "^11.0.4",
"@types/prompts": "^2.4.9",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@tailwindcss/vite": "^4.2.0",
"@vitejs/plugin-react": "^5.1.4",
"tailwindcss": "^4.2.0",
"chalk": "^5.6.2",
"diff": "^8.0.3",
"dir-compare": "^5.0.0",
"fs-extra": "^11.3.3",
"prompts": "^2.4.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.2.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.0.0"
+100
View File
@@ -0,0 +1,100 @@
import path from 'node:path';
import chalk from 'chalk';
import { createTwoFilesPatch } from 'diff';
import { compareSync } from 'dir-compare';
import fs from 'fs-extra';
import prompts from 'prompts';
const SOURCE = './src';
const TEMPLATES = './templates';
function colorizePatch(patch: string): string {
return patch
.split('\n')
.map((line) => {
if (line.startsWith('---') || line.startsWith('+++')) return chalk.dim(line);
if (line.startsWith('-')) return chalk.red(line);
if (line.startsWith('+')) return chalk.green(line);
if (line.startsWith('@@')) return chalk.cyan(line);
return chalk.gray(line);
})
.join('\n');
}
function printDiff(oldPath: string, newPath: string, label: string): void {
const oldContent = fs.existsSync(oldPath) ? fs.readFileSync(oldPath, 'utf8') : '';
const newContent = fs.existsSync(newPath) ? fs.readFileSync(newPath, 'utf8') : '';
const patch = createTwoFilesPatch(label, label, oldContent, newContent, undefined, undefined, { context: 3 });
// Strip the first two header lines (Index + ===) that createTwoFilesPatch adds
const lines = patch.split('\n');
const body = lines.slice(2).join('\n');
console.log(colorizePatch(body));
}
console.log(chalk.bold(`\nComparing ${SOURCE}${TEMPLATES}\n`));
const res = compareSync(SOURCE, TEMPLATES, { compareContent: true });
const changes = (res.diffSet ?? []).filter(
(d) => d.state !== 'equal' && d.type1 !== 'directory' && d.type2 !== 'directory'
);
if (changes.length === 0) {
console.log(chalk.gray('No differences found. Directories are in sync.'));
process.exit(0);
}
for (const diff of changes) {
const name = diff.name1 ?? diff.name2;
if (!name) continue;
const filePath = path.join(diff.relativePath, name);
if (diff.state === 'left') {
console.log(chalk.green(` + ${filePath} (new in src)`));
printDiff('', path.join(SOURCE, diff.relativePath, name), filePath);
}
if (diff.state === 'right') {
console.log(chalk.red(` - ${filePath} (only in templates)`));
}
if (diff.state === 'distinct') {
console.log(chalk.yellow(` ~ ${filePath} (modified)`));
printDiff(path.join(TEMPLATES, diff.relativePath, name), path.join(SOURCE, diff.relativePath, name), filePath);
}
}
console.log();
console.log(chalk.bold(`${changes.length} change(s) found.`));
console.log();
const { ok } = await prompts({
type: 'confirm',
name: 'ok',
message: `Copy all changes from ${SOURCE} to ${TEMPLATES}?`,
initial: false,
});
if (!ok) {
console.log(chalk.gray('\nAborted. No files were copied.'));
process.exit(0);
}
// Only copy changed/new files from src — skip 'right' entries (only in templates)
for (const diff of changes) {
if (diff.state === 'right') continue; // don't delete files that only exist in templates
if (!diff.name1) continue;
const relPath = path.join(diff.relativePath, diff.name1);
const src = path.join(SOURCE, relPath);
const dest = path.join(TEMPLATES, relPath);
await fs.copy(src, dest, { overwrite: true });
console.log(chalk.green(` ✔ Copied ${relPath}`));
}
console.log(chalk.bold.green('\nDone!\n'));
+1 -1
View File
@@ -6,5 +6,5 @@
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx"
},
"include": ["src"]
"include": ["src", "scripts"]
}
+103 -3
View File
@@ -338,6 +338,12 @@ importers:
'@tailwindcss/vite':
specifier: ^4.2.0
version: 4.2.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))
'@types/fs-extra':
specifier: ^11.0.4
version: 11.0.4
'@types/prompts':
specifier: ^2.4.9
version: 2.4.9
'@types/react':
specifier: ^19.0.0
version: 19.2.7
@@ -347,6 +353,21 @@ importers:
'@vitejs/plugin-react':
specifier: ^5.1.4
version: 5.1.4(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))
chalk:
specifier: ^5.6.2
version: 5.6.2
diff:
specifier: ^8.0.3
version: 8.0.3
dir-compare:
specifier: ^5.0.0
version: 5.0.0
fs-extra:
specifier: ^11.3.3
version: 11.3.3
prompts:
specifier: ^2.4.2
version: 2.4.2
react:
specifier: ^19.0.0
version: 19.2.3
@@ -3175,12 +3196,18 @@ packages:
'@types/fontkit@2.0.8':
resolution: {integrity: sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew==}
'@types/fs-extra@11.0.4':
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/jsesc@2.5.1':
resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==}
'@types/jsonfile@6.1.4':
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -3223,6 +3250,9 @@ packages:
'@types/postcss-prefix-selector@1.16.3':
resolution: {integrity: sha512-YZLPWRkJIrYjwaqojVDXzaRCAEYslRAm8Shznwwn+ZFA4iKQR4LZlS3d+ZMVteFz4iyQnngZZG7k/GIzV1f3mQ==}
'@types/prompts@2.4.9':
resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==}
'@types/prop-types@15.7.15':
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
@@ -3657,6 +3687,9 @@ packages:
resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==}
engines: {node: '>=18'}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
@@ -3871,6 +3904,9 @@ packages:
resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==}
engines: {node: '>= 14'}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
@@ -4147,6 +4183,9 @@ packages:
resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
engines: {node: '>=0.3.1'}
dir-compare@5.0.0:
resolution: {integrity: sha512-/GCjdixGQyJ9sj/HiMTYaNGztXqHnj0kWuKDfrGU6fCvZzQBihMBQLToG/dwg5cSs7lm5707iwbKOdsJh5VV4g==}
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
@@ -4518,6 +4557,10 @@ packages:
forwarded-parse@2.1.2:
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
fs-extra@11.3.3:
resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
engines: {node: '>=14.14'}
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@@ -4577,6 +4620,7 @@ packages:
git-raw-commits@4.0.0:
resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==}
engines: {node: '>=16'}
deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead.
hasBin: true
github-slugger@2.0.0:
@@ -4588,12 +4632,13 @@ packages:
glob@10.5.0:
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
deprecated: Glob versions prior to v9 are no longer supported
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
global-directory@4.0.1:
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
@@ -5002,6 +5047,9 @@ packages:
jsonc-parser@3.3.1:
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
jsonparse@1.3.1:
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
engines: {'0': node >= 0.2.0}
@@ -5577,6 +5625,9 @@ packages:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
minimatch@5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
@@ -6549,11 +6600,12 @@ packages:
tar@7.5.2:
resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==}
engines: {node: '>=18'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
tar@7.5.7:
resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==}
engines: {node: '>=18'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
test-exclude@7.0.1:
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
@@ -6883,6 +6935,10 @@ packages:
universal-user-agent@7.0.3:
resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==}
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
unixify@1.0.0:
resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==}
engines: {node: '>=0.10.0'}
@@ -10203,12 +10259,21 @@ snapshots:
dependencies:
'@types/node': 22.19.3
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
'@types/node': 22.19.3
'@types/hast@3.0.4':
dependencies:
'@types/unist': 3.0.3
'@types/jsesc@2.5.1': {}
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 22.19.3
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -10260,6 +10325,11 @@ snapshots:
dependencies:
postcss: 8.5.6
'@types/prompts@2.4.9':
dependencies:
'@types/node': 22.19.3
kleur: 3.0.3
'@types/prop-types@15.7.15': {}
'@types/react-dom@19.2.3(@types/react@19.2.7)':
@@ -10486,7 +10556,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.3)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.3)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)
'@vitest/utils@3.2.4':
dependencies:
@@ -10906,6 +10976,11 @@ snapshots:
widest-line: 5.0.0
wrap-ansi: 9.0.2
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@2.0.2:
dependencies:
balanced-match: 1.0.2
@@ -11098,6 +11173,8 @@ snapshots:
normalize-path: 3.0.0
readable-stream: 4.7.0
concat-map@0.0.1: {}
confbox@0.1.8: {}
consola@3.4.2: {}
@@ -11350,6 +11427,11 @@ snapshots:
diff@8.0.3: {}
dir-compare@5.0.0:
dependencies:
minimatch: 3.1.5
p-limit: 3.1.0
dlv@1.1.3: {}
dom-accessibility-api@0.5.16: {}
@@ -11788,6 +11870,12 @@ snapshots:
forwarded-parse@2.1.2: {}
fs-extra@11.3.3:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.2.0
universalify: 2.0.1
fs.realpath@1.0.0: {}
fsevents@2.3.3:
@@ -12393,6 +12481,12 @@ snapshots:
jsonc-parser@3.3.1: {}
jsonfile@6.2.0:
dependencies:
universalify: 2.0.1
optionalDependencies:
graceful-fs: 4.2.11
jsonparse@1.3.1: {}
jsonpointer@5.0.1: {}
@@ -13184,6 +13278,10 @@ snapshots:
min-indent@1.0.1: {}
minimatch@3.1.5:
dependencies:
brace-expansion: 1.1.12
minimatch@5.1.6:
dependencies:
brace-expansion: 2.0.2
@@ -14597,6 +14695,8 @@ snapshots:
universal-user-agent@7.0.3: {}
universalify@2.0.1: {}
unixify@1.0.0:
dependencies:
normalize-path: 2.1.1