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.
Why publish?
| Reason | What you gain |
|---|---|
| Sharing across products | Multiple apps inside your org consume one canonical implementation. |
| Community / ecosystem | Listed in the VSDK plugin catalog, discoverable, reviewable. |
| Versioning and changelog | Consumers pin a known-good version; you ship breaking changes behind a major bump. |
| Independent CI / release cadence | Plugin 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
| Path | Use it when | Listing |
|---|---|---|
| Private npm | Internal-only sharing across teams in your org | Not in the catalog |
| Public + catalog submission | You want any VSDK user to install and use it | Goes 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:
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.
{
"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
| Rule | Reason |
|---|---|
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 dependencies | Bundling them breaks hooks / context because the plugin's React instance differs from the host. |
| Peer range is open on patches / minors, closed on majors | Use ">=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 it | Lets the SDK and the user's bundler resolve types and entry correctly. |
files only lists what's needed at runtime | dist, README.md, CHANGELOG.md. Don't ship src/ or tests/. |
3. tsup.config.ts — externalise everything VSDK / React
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
{
"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.
// 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
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
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.
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
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
| Metric | Target |
|---|---|
| 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
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,
},
},
},
})import '@testing-library/jest-dom/vitest'
// Polyfills the SDK needs in jsdom
class ResizeObserverMock { observe() {} unobserve() {} disconnect() {} }
;(globalThis as any).ResizeObserver ??= ResizeObserverMock7. The README.md contract
Catalog reviewers reject submissions without a clear README. The required sections:
# @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-mynameUsage
import { myPlugin } from '@yourorg/vsdk-plugin-myname'
<VideoStudioProvider plugins={[myPlugin({ /* options */ })]}>
<StudioEditor />
</VideoStudioProvider>Options
| Option | Type | Default | Description |
|---|---|---|---|
adapter | MyAdapter | — (required) | Your I/O implementation. |
order | number | 60 | Sidebar 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 buildproducesdist/with.js,.cjs,.d.tsfiles. - ✅
pnpm check-typesexits 0. - ✅
pnpm lintexits 0. - ✅
pnpm test --coveragepasses and meets the thresholds in section 6.
Package shape
- ✅ Name starts with
vsdk-plugin-(or@yourorg/vsdk-plugin-). - ✅
react,react-dom,@studio-dev/vsdkare inpeerDependencies, notdependencies. - ✅ The peer range is open on minors but closed on the next major.
- ✅
externalin tsup config excludes React /@studio-dev/*. - ✅
fileslists exactlydist,README.md,CHANGELOG.md. - ✅
exportsmap provides types/import/require.
SDK conventions
- ✅ Plugin factory returns from
createPlugin({ … }). - ✅ Plugin
idis namespaced (yourorg:plugin-name). - ✅ All panels / inspectors are
React.lazyimports. - ✅ Plugin tags every item it creates with
metadata.source = 'plugin-id'. - ✅ Custom commands are reversible —
undorestores the captured before-state, not the runtime state. - ✅ Every
eventBus.on/store.subscribereturns a cleanup fromonActivate. - ✅ No
tsup-bundled React or@studio-dev/*symbols (runpnpm tsup --inspectto verify). - ✅ Plugin doesn't import anything from
@studio-dev/vsdk/lambdaunless 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 (
mobileComponentif the desktop layout doesn't fit). - ✅ Hotkeys respect the
whenscope 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:
-
Publish the package to npm. Public registry, public access. The catalog scrape pulls metadata from
npm view. -
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> -
A reviewer runs your test suite and the checklist above. Expect ~2–5 business days.
-
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. -
On rejection, you'll get a list of items to fix. Resubmit once they're addressed.
11. Versioning policy
The catalog enforces strict semver:
| Change | Bump |
|---|---|
| Bug fix, doc tweak | Patch (0.0.x) |
| New option, new extension point, new adapter | Minor (0.x.0) |
| Removed / renamed export, changed default, dropped SDK major | Major (x.0.0) |
| Bumped peer-dep upper bound | Minor (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
deprecatedon 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:
peerDependenciesfor React /@studio-dev/vsdk.tsupconfig with React /@studio-dev/*externals,dts: true.- Factory + adapter + types re-exported from
src/index.ts. - One factory test, one panel test, one command test (if you ship commands).
- README with Installation + Usage + Options.
- CHANGELOG with version sections.
vitest --coveragepassing the 80/70/80/80 thresholds.git tagmatchespackage.jsonversion on every release.
Get those right and your plugin is publishable. Everything else on this page is what makes it good.