Skip to content

Latest commit

 

History

History
563 lines (417 loc) · 19.3 KB

File metadata and controls

563 lines (417 loc) · 19.3 KB

CLAUDE.md

Guidance for Claude Code (claude.ai/code) and other AI agents working with this repository.

Overview

Video.js 10 is a Turborepo‑managed monorepo, organized by runtime and platform. Refer to CONTRIBUTING.md for setup, development, and lint/test instructions.

Package Layout

Package Path Purpose
packages/utils Shared utilities (/dom subpath for DOM‑specific helpers).
packages/element Custom element base class for web components.
packages/store State management (/html, /react subpaths for platforms).
packages/spf Stream Processing Framework (/dom and /playback-engine subpaths for DOM bindings and the HLS engine).
packages/core Core runtime‑agnostic logic (/dom subpath for DOM bindings).
packages/icons SVG icon library (private, consumed by html and react).
packages/skins Shared skin CSS and Tailwind tokens (private).
packages/html Web player—DOM/Browser‑specific implementation.
packages/react React player—adapts core state to React components.
packages/react-native React Native player (planned, not yet implemented).
packages/cli @videojs/cli — CLI for reading docs, and more in the future.
apps/sandbox Vite‑based dev playground (private, not published).
apps/e2e Playwright end‑to‑end and visual snapshot tests (private).
site/ Astro‑based docs and website.

Dependency Hierarchy

utils/*         ← shared utilities
utils/dom       ← DOM-specific helpers

element         ← custom element base

store           ← state management
store/html      ← HTML bindings (controllers, mixins)
store/react     ← React bindings

spf                  ← framework primitives (composition, signals, tasks, actors, reactors)
spf/dom              ← DOM bindings for SPF
spf/playback-engine  ← HLS playback engine + SpfMedia adapter

core            ← runtime-agnostic logic
core/dom        ← DOM bindings

icons           ← SVG icon library (private)
skins           ← shared skin CSS (private)

html            ← Web player (DOM/Browser)
react           ← React player
react-native    ← React Native player (planned, not yet implemented)
utils ← element
utils ← store ← core ← html / react
utils ← spf ← core
icons, skins → html / react

Workspace

  • Uses PNPM workspaces + Turbo for task orchestration.
  • Internal deps are linked with workspace:*.
  • Always use PNPM, do not use other package managers.

Common Root Commands

# Install workspace deps
pnpm install

# Run all demos/sites in parallel
pnpm dev

# Typecheck across repo (fast - uses TypeScript project references)
# Always run from root, not per-package
pnpm typecheck

# Build all packages/apps
pnpm build
# Build all packages (no apps)
pnpm build:packages
# Build sandbox (and its package deps)
pnpm build:sandbox
# Build specific package
pnpm -F <pkg> build

# Run unit tests across all packages
pnpm test
# Run tests for specific package
pnpm -F <pkg> test
# Run tests matching a name or pattern
pnpm -F <pkg> test -t "test name pattern"
# Run tests for a specific file
pnpm -F <pkg> test src/path/to/file.test.ts
# Run tests matching a glob or filter
pnpm -F <pkg> test src/core

# Run e2e tests (Chromium + WebKit)
pnpm test:e2e
# Run e2e tests (Chromium only, faster)
pnpm test:e2e:vite
# Update visual snapshot baselines
pnpm test:e2e:update

# Lint all workspace packages
pnpm lint
# Lint and fix a single file
pnpm lint:fix:file <file>

# Remove all dist and types outputs
pnpm clean

# Validate workspace consistency (CI coverage, scopes, define imports, etc.)
pnpm check:workspace

# Measure bundle size (SPF only)
pnpm -F @videojs/spf size        # Public API (minified + gzipped)
pnpm -F @videojs/spf size:all    # All exports (minified + gzipped)

Dev Workflow

  1. Make changes.
  2. If you added/changed exported types in a package, run pnpm -F <pkg> build first.
    • pnpm typecheck uses TypeScript project references against built .d.ts files.
    • New/changed types won't be visible until tsdown builds them.
  3. Typecheck, fix all issues.
  4. Run test/s, fix all issues. If there are no tests add them.
  5. Lint file/s, fix all issues.
  6. Run build/s, fix all errors.
  7. Run pnpm check:workspace — fix any consistency warnings.
  8. Before creating a PR pnpm test.
  9. If your changes introduced new patterns or conventions, ask the user to run /claude-update.

Be efficient when running operations, see "Common Root Commands".

Testing

File Organization

Tests live in a tests/ directory next to the implementation they cover:

packages/utils/src/dom/
├── listen.ts
├── event.ts
└── tests/
    ├── listen.test.ts
    └── event.test.ts

Conventions

  • Use Vitest as the test runner.
  • Import test utilities from vitest: describe, it, expect, vi.
  • Name test files <module>.test.ts matching the source file.
  • Write or update matching tests for each new or modified behavior.
  • Follow the act → assert pattern.
  • Use vi.fn() for mocks and spies.

Test describe() Names

Use the exact exported name being tested (preserving case):

// snapshot-controller.test.ts — class export
describe('SnapshotController', () => { ... });

// provider-mixin.test.ts — factory function export
describe('createStoreProviderMixin', () => { ... });

// event-like.test.ts — lowercase module/export
describe('event-like', () => { ... });

Guidelines

When generating or editing code in this repository, follow these rules to ensure safe, high‑quality contributions:

  1. Edit Precisely

    • Modify only the relevant lines or files.
    • Never overwrite large sections or regenerate entire files.
    • Preserve comments, type signatures, and existing code style.
  2. Match Existing Conventions

    • Follow the repo's Biome and TypeScript settings automatically.
    • Use consistent naming (camelCase for variables, PascalCase for components).
    • Biome handles import organization (side-effects → external → internal).
  3. Type Safety First

    • Never remove or bypass TypeScript types.
    • Avoid any; use unknown and proper narrowing if needed.
    • Always ensure edits pass pnpm typecheck.
  4. Framework‑Agnostic Mindset

    • Core modules must remain DOM‑ and framework‑independent.
    • Place platform‑specific logic in the appropriate directory or adapter (HTML, React, RN).
  5. A11y, Styling & Performance

    • Maintain accessibility: ARIA roles, keyboard interactions, focus management.
    • Use data‑attributes and CSS variables for style hooks—no inline animation JS.
    • Ensure logic runs at 60 FPS; prefer CSS transitions over manual DOM mutations.
  6. Dev‑Only Diagnostics

    • Use __DEV__ for warnings, debug helpers, and displayName assignments.
    • Keep production builds free of dev‑only logging and checks.
  7. Commit Scope

    • Use semantic commit messages (enforced by commitlint).
    • One focused change per commit—no mixed updates.
    • Breaking changes use !.
  8. Keep AI Documentation Current — When introducing new patterns, ask the user to run /claude-update for guidance.

Code Rules

File Organization

  • Types live next to implementations — Don't create separate types.ts files. Export types from the same file as their implementation.
  • Tests in tests/ directories — See Testing section above.

Utilities

Prefer existing utilities over inline implementations:

Instead of Use
x === undefined isUndefined(x) from @videojs/utils/predicate
x === null isNull(x) from @videojs/utils/predicate
typeof x === 'function' isFunction(x) from @videojs/utils/predicate
typeof x === 'string' isString(x) from @videojs/utils/predicate

Before writing new helpers, check @videojs/utils for existing utilities.

Naming Conventions

Pattern Prefix Example
Type inference Infer* InferFeatureState<F>
Type resolution Resolve* ResolveRequestHandler<R>
Type constraint Ensure* EnsureTaskRecord<T>
Union type helpers Union* UnionFeatureState<Features>
Default loose types Default* DefaultTaskRecord
Type guards is* isStoreError(error)
Factory functions create* createQueue(), createFeature()
Falsy wrapper Falsy* Falsy<T> (value that might be falsy)
Constructor types *Constructor Constructor<T>, AnyConstructor<T>
Mixin types Mixin Mixin<Base, Result>

Note on create* prefix: Use create* for factory functions that construct stateful objects or classes (e.g., createQueue(), createStore()). Simple utility functions that return cleanup callbacks don't use this prefix (e.g., listen(), animationFrame(), idleCallback()).

Component/Hook Namespace Pattern

Use namespaces to co-locate Props and Result types with components/hooks:

// Component with Props namespace
export function Video({ src, ...props }: VideoProps): JSX.Element {
  // ...
}

export namespace Video {
  export type Props = VideoProps;
}

// Hook with Result namespace
export function useMutation(name: string): MutationResult {
  // ...
}

export namespace useMutation {
  export type Result = MutationResult;
}

Usage:

// Props type via namespace
const props: Video.Props = { src: 'video.mp4' };

// Result type via namespace
const mutation: useMutation.Result = useMutation('play');

Type Guards

Always return value is Type for proper type narrowing:

function isStoreError(value: unknown): value is StoreError {
  return value instanceof StoreError;
}

Symbol Identification Pattern

Use symbols to identify objects when instanceof isn't reliable (e.g., cross-realm, serialization boundaries):

const STORE_SYMBOL = Symbol('@videojs/store');

interface Store {
  [STORE_SYMBOL]: true;
  // ...
}

function createStore(): Store {
  return {
    [STORE_SYMBOL]: true,
    // ...
  };
}

function isStore(value: unknown): value is Store {
  return isObject(value) && STORE_SYMBOL in value;
}
  • Symbol constant named *_SYMBOL in SCREAMING_CASE
  • Symbol description is @videojs/*
  • Add [SYMBOL]: true property to the object/interface
  • Type guard checks isObject(value) && SYMBOL in value

Symbol() vs Symbol.for():

  • Use Symbol.for('@videojs/*') for symbols that need cross-realm identity (e.g., metadata that must be recognized across module boundaries)
  • Use Symbol('@videojs/*') for instance-unique identifiers (e.g., task IDs, feature IDs)

Subscribe Pattern

Subscriptions return an unsubscribe function:

subscribe(callback: Callback): () => void {
  this.#subscribers.add(callback);
  return () => this.#subscribers.delete(callback);
}

Optional Key Parameter

Methods that operate on one or all items use optional key:

// If key provided: operate on that item
// If no key: operate on all items
reset(key?: keyof Tasks): void {
  if (!isUndefined(key)) {
    // Single item
    return;
  }
  // All items
}

Destroy Pattern

Guard re-entry, set flag first, cleanup in order:

destroy(): void {
  if (this.#destroyed) return;
  this.#destroyed = true;
  this.abort();
  this.#subscribers.clear();
}

Cleanup Pattern

Use AbortController when managing multiple cleanups. It works with listen and any API that accepts a signal:

#disconnect: AbortController | null = null;

connect(): void {
  this.#disconnect?.abort();
  this.#disconnect = new AbortController();

  store.subscribe(() => {}, { signal: this.#disconnect.signal });
  listen(element, 'click', handler, { signal: this.#disconnect.signal });
}

disconnect(): void {
  this.#disconnect?.abort();
  this.#disconnect = null;
}

For single cleanup, use a simple unsubscribe function.

Promise Cleanup

Use .finally() for cleanup that runs regardless of success or failure:

// Good - when awaiting or returning the promise
await promise.finally(() => cache.delete(key));

// Good - fire-and-forget cleanup that shouldn't propagate rejection
promise.then(
  () => cache.delete(key),
  () => cache.delete(key)
);

Note: .finally() propagates rejections to its returned promise. If you're not awaiting or returning it, use .then() with both handlers to avoid unhandled rejections.

No Hungarian Type Notation

Never prefix type parameters with T. Use descriptive names instead:

// Bad
type Mixin<TBase extends Constructor> = ...
function createStore<TFeatures extends AnyFeature[]>(...) { ... }

// Good
type Mixin<Base extends Constructor> = ...
function createStore<Features extends AnyFeature[]>(...) { ... }

React: Lazy Initialization

Use useState with initializer function for objects that should only be created once. Don't use useRef with inline object creation — the object is created on every render even though only the first value is kept:

// Bad - creates new Set on every render
const trackedRef = useRef(new Set<string>());

// Good - initializer only runs once
const [tracked] = useState(() => new Set<string>());

No Obvious Comments

Don't write comments that restate what the code does. Comments should explain why, not what:

// Bad
// Create the store
const store = createStore(config);

// Loop through items
for (const item of items) { ... }

// Good
// Create store before rendering to allow pre-hydration
const store = createStore(config);

No Pointless Type Casts

Avoid casts that don't add value. If TypeScript can infer the type, don't cast:

// Bad - already typed
const value = someFunction() as SomeType;

// Bad - use generic type argument
const media = node.querySelector('video, audio') as HTMLMediaElement | null;

Minimal JSDoc

JSDoc should add value, not restate what TypeScript already shows:

No redundant @param/@returns (exception: API reference exports — see below) — TypeScript signatures are the documentation:

// Bad
/**
 * @param callback - The callback to invoke
 * @returns A cleanup function
 */
export function animationFrame(callback: FrameRequestCallback): () => void;

// Good
/** Request an animation frame with cleanup. */
export function animationFrame(callback: FrameRequestCallback): () => void;

Single JSDoc for overloads (exception: API reference exports — see below) — Document the first overload only:

/** Wait for an event to occur on a target. */
export function onEvent<K extends keyof HTMLMediaElementEventMap>(...): Promise<...>;
export function onEvent<K extends keyof HTMLElementEventMap>(...): Promise<...>;

One example per function — Consolidate into a single representative example.

No JSDoc for self-documenting code — Skip JSDoc when names are clear:

// No JSDoc needed
export function supportsIdleCallback(): boolean { ... }
get size(): number { ... }
add(cleanup: CleanupFn): void { ... }

// Bad - comment restates the obvious
/** Media element contract. */
export interface Media extends HTMLMediaElement {}

/** Feature capability availability. */
export type FeatureAvailability = 'available' | 'unavailable' | 'unsupported';

// Good - no comment needed
export interface Media extends HTMLMediaElement {}
export type FeatureAvailability = 'available' | 'unavailable' | 'unsupported';

API reference exports are different — Exports that feed the api-docs-builder (use* hooks, *Controller classes, create* factories, selectors, and @public-annotated exports) need richer JSDoc for the generated reference pages. See the api-reference skill → references/util-conventions.md for the full rules. Key differences from above:

  • @param name - description is required (the builder extracts these into parameter tables).
  • Multi-overload functions get per-overload JSDoc with @label tags (not a single JSDoc block).
  • @public opts in exports that don't match naming conventions.

Design Documents

Location Purpose
internal/design/ Architecture specs and feature designs you own
internal/decisions/ ADR-style records of single tactical decisions
rfc/ Proposals needing buy-in — get alignment before committing
.claude/plans/ Implementation notes, AI-agent context, working drafts

Design Doc vs RFC

Design Doc RFC
Scope Your area of work Shared concerns or public API
Approval None needed Needs buy-in from others
Purpose Document for posterity Get alignment first

Design Docs — Architecture specs and feature designs in your area. Longer-form, status ranges from draftdecidedimplementedsuperseded. See internal/design/README.md.

Decisions — ADR-style records of a single tactical decision: what was chosen, why, what was ruled out. Short, always decided, often cross-reference each other. See internal/decisions/README.md.

RFCs — Cross-team alignment. Write one when the decision affects multiple areas, changes shared API surface, or is hard to reverse. See rfc/README.md.

Implementation plans — Step-by-step details for how to implement. Use .claude/plans/ for implementation notes, debugging discoveries, and AI-agent context. Compact before merging.

Rule Placement

CLAUDE.md contains repo-wide conventions. Domain-specific patterns live in skills:

Domain Location
Naming, testing, utilities CLAUDE.md Code Rules
Component patterns and APIs component skill
Accessibility aria skill
Documentation docs skill
Component reference pages api-reference skill
API design and DX api skill
CSS → Tailwind migration css-to-tailwind skill
Updating AI docs claude-update skill

When adding a new rule, ask: "Who needs this?" If it's domain-specific, put it in the relevant skill.