Video Studio SDKv0.0.3
Plugin authoring

Publishing a plugin

From a working in-project plugin to a validated, npm-published package listed in the official VSDK plugin catalog. Structure, peer-deps, testing coverage, validation, submission.

This page is for when your plugin has outgrown your app's source tree and you want other teams to install it. That means it becomes its own npm package, has to follow the package conventions below, ships with tests, and — if you want to be listed in the official VSDK plugin catalog — passes a validation review.

If you're still building a plugin for your own app and don't plan to ship it to anyone else, this page is overkill. Stay on the Simple, Medium, or Advanced in-project tracks. You can always graduate later — the plugin code itself is identical.

Why publish?

ReasonWhat you gain
Sharing across productsMultiple apps inside your org consume one canonical implementation.
Community / ecosystemListed in the VSDK plugin catalog, discoverable, reviewable.
Versioning and changelogConsumers pin a known-good version; you ship breaking changes behind a major bump.
Independent CI / release cadencePlugin tests / releases don't gate the host app's deploy.

If none of those apply, don't bother — the in-project tiers stay simpler and easier to refactor.

Two publication paths

PathUse it whenListing
Private npmInternal-only sharing across teams in your orgNot in the catalog
Public + catalog submissionYou want any VSDK user to install and use itGoes through the validation process below

The package structure is the same for both. Only the final submission step differs.


1. Repository structure

Your plugin lives in its own git repository, not in your app's monorepo (unless your monorepo already publishes packages). The reference layout matches the official plugins:

index.ts
plugin.ts
types.ts
panel.tsx
inspector.tsx
commands.ts
config-store.ts
package.json
tsup.config.ts
tsconfig.json
vitest.config.ts
eslint.config.js
README.md
CHANGELOG.md
LICENSE

Files are roughly the same as the Advanced tier layout — plus the four configuration files (package.json, tsup.config.ts, tsconfig.json, vitest.config.ts), the tests/ folder, and the README / CHANGELOG / LICENSE package metadata.

2. package.json

The single most important file. Three things matter: the name, the peer deps, and the exports map.

package.json
{
  "name": "@yourorg/vsdk-plugin-myname",
  "version": "0.1.0",
  "description": "A one-line description of what your plugin does.",
  "type": "module",
  "license": "MIT",
  "keywords": ["vsdk", "video-studio-sdk", "plugin"],
  "homepage": "https://github.com/yourorg/vsdk-plugin-myname#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/yourorg/vsdk-plugin-myname.git"
  },
  "bugs": {
    "url": "https://github.com/yourorg/vsdk-plugin-myname/issues"
  },
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org"
  },

  "main":  "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types":  "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  },
  "files": ["dist", "README.md", "CHANGELOG.md"],

  "scripts": {
    "build":       "tsup",
    "dev":         "tsup --watch",
    "lint":        "eslint .",
    "check-types": "tsc --noEmit",
    "test":        "vitest run",
    "test:watch":  "vitest"
  },

  "peerDependencies": {
    "@studio-dev/vsdk": ">=0.0.3 <1.0.0",
    "react":            ">=18",
    "react-dom":        ">=18"
  },

  "devDependencies": {
    "@studio-dev/vsdk":     "0.0.3",
    "@testing-library/react": "^16.0.0",
    "@types/react":           "^19.0.0",
    "@types/react-dom":       "^19.0.0",
    "eslint":                 "^9.0.0",
    "jsdom":                  "^25.0.0",
    "react":                  "^19.0.0",
    "react-dom":              "^19.0.0",
    "tsup":                   "^8.0.0",
    "typescript":             "^5.9.0",
    "vitest":                 "^3.0.0"
  }
}

Hard rules

RuleReason
Name starts with vsdk-plugin-Discoverability + the catalog scrape relies on the prefix. Scoped names like @yourorg/vsdk-plugin-name are fine.
react, react-dom, @studio-dev/vsdk are peer dependenciesBundling them breaks hooks / context because the plugin's React instance differs from the host.
Peer range is open on patches / minors, closed on majorsUse ">=0.0.3 <1.0.0" style. Bump the upper bound when you've tested against a new SDK major.
exports map is set, main / module mirror itLets the SDK and the user's bundler resolve types and entry correctly.
files only lists what's needed at runtimedist, README.md, CHANGELOG.md. Don't ship src/ or tests/.

3. tsup.config.ts — externalise everything VSDK / React

tsup.config.ts
import { defineConfig } from 'tsup'

export default defineConfig({
  entry:     ['src/index.ts'],
  format:    ['esm', 'cjs'],
  dts:       true,
  sourcemap: true,
  clean:     true,
  external:  [
    'react',
    'react-dom',
    /^@studio-dev\//,            // @studio-dev/vsdk, @studio-dev/vsdk-ui, @studio-dev/vsdk/*
  ],
  treeshake: true,
  splitting: false,
})

external is non-negotiable. If you let tsup bundle react or @studio-dev/vsdk, your plugin's React (or store) will be a different instance from the host's — hooks throw, the context lookup fails silently.

4. tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "preserve",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true,
    "outDir": "dist"
  },
  "include": ["src"]
}

strict: true is required for catalog submission — type-safety is part of what makes plugins safe to install.

5. Public surface — src/index.ts

The barrel file is the contract. Anything not exported from here is private and can change in patch releases without warning.

src/index.ts
// Factory — the only thing every consumer needs
export { myPlugin, type MyPluginOptions } from './plugin'

// Adapter factories (optional — only if your plugin is adapter-backed)
export { createMyMockAdapter } from './adapters/mock'

// Public types
export type { MyAdapter, MyItem } from './types'

Re-export only:

  • The plugin factory function.
  • Adapter factories you bundle (mock + any default implementations).
  • Types consumers need to write their own adapter or inspect plugin output.

Don't export panels, inspectors, or the config store — they're internals.

6. Testing coverage

Submission to the catalog requires the following minimum test coverage. Each is enforced by the review checklist.

a. Factory smoke test — the plugin registers without throwing

tests/plugin.test.tsx
import { render } from '@testing-library/react'
import { StudioEditor, VideoStudioProvider } from '@studio-dev/vsdk'
import { myPlugin } from '../src'

test('mounts inside the provider with default options', () => {
  expect(() =>
    render(
      <VideoStudioProvider plugins={[myPlugin()]}>
        <StudioEditor />
      </VideoStudioProvider>,
    ),
  ).not.toThrow()
})

b. Panel test — UI renders with a mock adapter

tests/panel.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { StudioEditor, VideoStudioProvider } from '@studio-dev/vsdk'
import { myPlugin, createMyMockAdapter } from '../src'

test('panel opens and renders fixture data', async () => {
  render(
    <VideoStudioProvider plugins={[myPlugin({ adapter: createMyMockAdapter() })]}>
      <StudioEditor />
    </VideoStudioProvider>,
  )

  fireEvent.click(screen.getByRole('button', { name: /my plugin/i }))
  await waitFor(() => expect(screen.getByText(/example/i)).toBeInTheDocument())
})

c. Command test — commands are reversible

If you ship custom commands, prove undo reverts the changes execute made.

tests/commands.test.ts
import { createStudioStore } from '@studio-dev/vsdk/core'
import { createMyCommand } from '../src/commands'

test('myCommand is reversible', () => {
  const store = createStudioStore()
  // …seed state via store.getState() actions

  const before = store.getState().timeline.items
  const cmd = createMyCommand(/* args */)
  store.getState().executeCommand(cmd)
  expect(/* something changed */).toBe(true)

  store.getState().undo()
  expect(store.getState().timeline.items).toEqual(before)
})

d. Adapter contract — if you ship an adapter

tests/adapter.test.ts
import { createMyMockAdapter } from '../src'

test('mock adapter satisfies the contract', async () => {
  const adapter = createMyMockAdapter()
  const result = await adapter.search('anything', 1)
  expect(result.photos).toBeInstanceOf(Array)
  expect(result).toHaveProperty('hasMore')
})

Coverage targets

MetricTarget
Statements≥ 80%
Branches≥ 70%
Functions≥ 80%
Lines≥ 80%

vitest run --coverage should pass these. The catalog review re-runs your test suite — if it doesn't, your submission is held until it does.

vitest.config.ts

vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./tests/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'json-summary'],
      thresholds: {
        statements: 80,
        branches:   70,
        functions:  80,
        lines:      80,
      },
    },
  },
})
tests/setup.ts
import '@testing-library/jest-dom/vitest'
// Polyfills the SDK needs in jsdom
class ResizeObserverMock { observe() {} unobserve() {} disconnect() {} }
;(globalThis as any).ResizeObserver ??= ResizeObserverMock

7. The README.md contract

Catalog reviewers reject submissions without a clear README. The required sections:

README.md
# @yourorg/vsdk-plugin-myname

One-paragraph description of what the plugin does.

## Compatibility

| Plugin version | VSDK version |
| --- | --- |
| 0.1.x | >= 0.0.3 < 1.0.0 |

## Installation

```bash
pnpm add @yourorg/vsdk-plugin-myname

Usage

import { myPlugin } from '@yourorg/vsdk-plugin-myname'

<VideoStudioProvider plugins={[myPlugin({ /* options */ })]}>
  <StudioEditor />
</VideoStudioProvider>

Options

OptionTypeDefaultDescription
adapterMyAdapter— (required)Your I/O implementation.
ordernumber60Sidebar slot order.

Adapter contract

interface MyAdapter {
  // …each method, signature, what it does
}

Telemetry

What (if anything) the plugin sends back to your servers. Be explicit — silent telemetry is grounds for rejection from the catalog.

License

MIT (or whatever).


## 8. `CHANGELOG.md` — keep it honest

Use [Keep a Changelog](https://keepachangelog.com) format. Every release gets a section. Every
breaking change gets a clear note **and** a major version bump.

```md title="CHANGELOG.md"
# Changelog

## [0.2.0] - 2026-05-01

### Added
- HD download quality option on the adapter.

### Changed
- **Breaking:** `adapter.download` signature now takes `(id, quality)` instead of `(id)`.

### Fixed
- Panel re-rendered on every playback frame (selector too wide).

9. Validation before submission

Run this self-check before you open a submission. The catalog reviewers run effectively the same list — if it all passes, you'll usually be approved on first review.

Code

  • pnpm build produces dist/ with .js, .cjs, .d.ts files.
  • pnpm check-types exits 0.
  • pnpm lint exits 0.
  • pnpm test --coverage passes and meets the thresholds in section 6.

Package shape

  • ✅ Name starts with vsdk-plugin- (or @yourorg/vsdk-plugin-).
  • react, react-dom, @studio-dev/vsdk are in peerDependencies, not dependencies.
  • ✅ The peer range is open on minors but closed on the next major.
  • external in tsup config excludes React / @studio-dev/*.
  • files lists exactly dist, README.md, CHANGELOG.md.
  • exports map provides types/import/require.

SDK conventions

  • ✅ Plugin factory returns from createPlugin({ … }).
  • ✅ Plugin id is namespaced (yourorg:plugin-name).
  • ✅ All panels / inspectors are React.lazy imports.
  • ✅ Plugin tags every item it creates with metadata.source = 'plugin-id'.
  • ✅ Custom commands are reversible — undo restores the captured before-state, not the runtime state.
  • ✅ Every eventBus.on / store.subscribe returns a cleanup from onActivate.
  • ✅ No tsup-bundled React or @studio-dev/* symbols (run pnpm tsup --inspect to verify).
  • ✅ Plugin doesn't import anything from @studio-dev/vsdk/lambda unless it explicitly needs Lambda.

UX

  • ✅ Adapter is required if the plugin does any I/O — no silent fallback to a hard-coded API key.
  • ✅ Panel works on both desktop and mobile (mobileComponent if the desktop layout doesn't fit).
  • ✅ Hotkeys respect the when scope and don't fire when input fields are focused.
  • ✅ License-gated features show a clear "upgrade" path, not a silent disable.

Documentation

  • ✅ README has Compatibility, Installation, Usage, Options, Adapter contract, Telemetry, License.
  • ✅ CHANGELOG follows Keep a Changelog format.
  • ✅ Repository URL, homepage, and issues URL are set in package.json.

10. Submitting to the official catalog

The official plugin catalog lives at the public docs site under /docs/plugins. To be listed:

  1. Publish the package to npm. Public registry, public access. The catalog scrape pulls metadata from npm view.

  2. Open a submission issue in the docs repo with the following template:

    ### Plugin
    Name: @yourorg/vsdk-plugin-myname
    Version: 0.1.0
    Repository: https://github.com/yourorg/vsdk-plugin-myname
    
    ### Compatibility
    Tested against VSDK: 0.0.3
    
    ### Coverage
    Statements: 84%
    Branches:   72%
    Functions:  90%
    Lines:      85%
    
    ### Description
    <one paragraph>
    
    ### Screenshots
    <one or two — panel + inspector>
  3. A reviewer runs your test suite and the checklist above. Expect ~2–5 business days.

  4. On approval, a docs PR adds your plugin to apps/docs/content/docs/plugins/<name>/ with skeleton MDX pages. You're free to PR richer content on top.

  5. On rejection, you'll get a list of items to fix. Resubmit once they're addressed.

11. Versioning policy

The catalog enforces strict semver:

ChangeBump
Bug fix, doc tweakPatch (0.0.x)
New option, new extension point, new adapterMinor (0.x.0)
Removed / renamed export, changed default, dropped SDK majorMajor (x.0.0)
Bumped peer-dep upper boundMinor (with a note in CHANGELOG)

Catalog reviewers will check git log against CHANGELOG.md — undocumented breaking changes will cause a delisting if discovered after the fact.

12. After publication

  • Monitor issues. The catalog page links to your GitHub Issues. Treat them as part of the contract.
  • Stay compatible with new SDK majors. When VSDK ships a new major, you have ~30 days to publish a compatible version (open the peer-dep upper bound, address breaking changes).
  • Deprecate, don't delete. If you stop maintaining the plugin, mark it deprecated on npm and open a docs PR — don't unpublish, which breaks lockfiles for everyone using it.

A worked example

The official @studio-dev/vsdk-plugin-stock plugin is the reference implementation that matches every rule on this page. Browse its repo for the canonical shape — tsup config, exports map, test setup, README format, CHANGELOG style.

Quick reference — minimum viable publication

If you skip everything else, the bare-minimum publication checklist is:

  1. peerDependencies for React / @studio-dev/vsdk.
  2. tsup config with React / @studio-dev/* externals, dts: true.
  3. Factory + adapter + types re-exported from src/index.ts.
  4. One factory test, one panel test, one command test (if you ship commands).
  5. README with Installation + Usage + Options.
  6. CHANGELOG with version sections.
  7. vitest --coverage passing the 80/70/80/80 thresholds.
  8. git tag matches package.json version on every release.

Get those right and your plugin is publishable. Everything else on this page is what makes it good.

On this page