npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

partisan

v1.0.0

Published

Build-time dependency indirection in the style of the Sanity v2 parts system

Readme

partisan

A Vite + TypeScript reimagining of the Sanity v2 "parts" system, unbundled from Sanity.

This is… not really meant for you to use. It exists to find out how cleanly the old parts mechanic rebuilds on modern web tooling. You probably shouldn't use it in anything you care about. Reasons below.

What "parts" means

A parts system is build-time dependency indirection. Instead of importing a concrete module path, you import an abstract name like part:app/root, and the system rewires that name to whichever plugin provides an implementation. Plugins can declare new names, implement existing ones, override implementations from other plugins, or contribute multiple implementations to a single name.

Three import flavors:

// Single implementation. Last plugin to implement wins.
import Root from 'part:app/root'

// Optional. Returns null if nothing implements it.
import logo from 'part:app/brand-logo?'

// All implementations as an array, in plugin order.
import tools from 'all:part:app/tool'

In Sanity v2, parts powered tool plugins, custom inputs, login screens, brand customization, and a few other extension points. The mechanic itself was useful for extensible apps. The implementation, less so.

Why this exists

Sanity v2's parts ran through webpack. That worked, but it also forced webpack-specific behavior onto every other tool that wanted to understand the project: PostCSS, ESLint, TypeScript etc. Each tool had to be patched or taught about parts, and each time the ecosystem moved, something broke.

While looking through some Vite documentation, I spooted Virtual Modules, which made me chuckle a bit. The virtual:some-identifier syntax is very similar to part:some-name, after all. And that made me wonder how hard it would be to support now, given how good Vite has become:

  • Vite resolves modules via Rollup conventions that everything else (especially TypeScript) already understands.
  • CSS handling is built into Vite. PostCSS just works because resolved part imports are real file paths.
  • TypeScript module wildcards plus codegen express the parts contract directly, without a TS plugin or compiler patch.
  • ESLint resolvers follow .d.ts declarations the same way they follow tsconfig paths.

Result: the parts mechanic, rebuilt today is a ton easier than what it was back then.

Why you probably shouldn't use this

The original parts system was retired in Sanity v3 for good reasons. Indirection costs more than it looks like it should:

  • "Where is this implemented?" requires reading partisan.json files instead of following an import.
  • Override behavior is implicit and depends on the order of plugins arrays.
  • New contributors have to learn a project-specific mechanic before they can read the code.
  • The TypeScript story here is functional, but it leans on codegen and ambient declarations. It's not free.
  • Other tooling likely won't work or will crash

For most apps, ordinary module imports plus an explicit plugin/registry API are clearer and just as flexible. Reach for partisan if you specifically want the v2 ergonomics back, or you want to play with the design.

How partisan works

partisan.json

Every plugin and the consuming app has a partisan.json at its package root:

{
  "root": true,
  "pluginPrefix": "acme-plugin",
  "plugins": ["@acme/base", "toolbar"],
  "parts": [
    {"name": "part:acme/root", "description": "Root component"},
    {"implements": "part:acme/root", "path": "./src/Root.ts"}
  ]
}
  • plugins lists dependencies in load order. Last wins for overrides.
  • pluginPrefix is the project's convention for resolving bare plugin names. With acme-plugin, the entry "toolbar" resolves to the npm package acme-plugin-toolbar. Scoped names like @acme/base and relative paths pass through unchanged.
  • parts is a flat list mixing declarations (name, description) and implementations (implements, path).

The import scheme itself (part:, all:part:) is fixed and not configurable. Only the plugin naming convention is.

Resolution

When Vite starts, partisan:

  1. Reads the project's partisan.json.
  2. For each entry in plugins, resolves it via Node's package resolution, reads that package's partisan.json, and recurses.
  3. Visits depth-first, post-order. The consuming project is added last. This is what makes the project's own implementations win over plugin implementations.

Import handling

Each import scheme is handled differently:

  • part:foo/bar resolves directly to the absolute file path of the last implementation. No virtual module. This is the important part: CSS, SVGs, and other non-JS assets go through Vite's normal pipeline (PostCSS, asset hashing, etc.) with zero special handling, because we never invent a synthetic module for them.
  • part:foo/bar? is a virtual module that re-exports from the last implementation, or export default null if there's no implementation.
  • all:part:foo/bar is a virtual module that imports each implementation and exports both default (an array of the impls' defaults) and modules (an array of full namespace objects, in case you need named exports too).

Dynamic imports work as long as the specifier is statically resolvable. await import('part:foo/bar') is fine. await import(\part:${name}`)` is not, because the resolver runs at build time.

Filesystem allow

Plugin implementations often live outside Vite's auto-detected workspace root (especially with npm link, file: deps, or globally installed packages). The plugin contributes every resolved plugin's package root to server.fs.allow automatically, so consumers don't have to think about it.

TypeScript

This is where v2 hurt most. Partisan tries to make it boring with three layers, most-specific first:

  1. Codegen. On dev-server start, partisan writes partisan-env.d.ts to the project root. It walks the resolved part graph and emits declare module blocks that re-export from the actual implementation files. This gives concrete, inferred types, and "go to definition" works in any editor with a TS server because the declarations point at real files.

  2. Plugin contract types. Plugins that declare parts can ship a parts.d.ts describing each part's contract (the Tool interface, a string, etc.). Consumers of those parts get useful types even before the app's codegen has run.

  3. Ambient wildcards. partisan/client exports declare module 'part:*' and friends, typed as unknown. Last-resort fallback for parts that haven't been narrowed by either layer above.

TypeScript picks the most specific matching declaration, so the layers compose. An app sees concrete impl types. A plugin sees contract types. Anything not covered falls through to unknown.

Using it

pnpm add partisan vite

Vite config

// vite.config.ts
import {defineConfig} from 'vite'
import {partisan} from 'partisan/vite'

export default defineConfig({
  plugins: [partisan()],
})

That's the whole integration. Codegen runs on dev-server start and on partisan.json changes.

Plugin options:

  • projectRoot (string): override the directory containing the root partisan.json. Defaults to Vite's config.root.
  • generateTypes (string | false): output path for codegen. Defaults to <projectRoot>/partisan-env.d.ts. Pass false to skip codegen.

App partisan.json

{
  "root": true,
  "pluginPrefix": "myapp-plugin",
  "plugins": ["@myorg/base", "extra-tool"]
}

"extra-tool" resolves to myapp-plugin-extra-tool. "@myorg/base" is used verbatim.

App tsconfig.json

{
  "compilerOptions": {
    "types": ["partisan/client"]
  },
  "include": ["src/**/*", "partisan-env.d.ts"]
}

Decide whether to commit partisan-env.d.ts. Either way works: it's reproducible from partisan.json plus the resolved plugin tree, so .gitignore-ing it is fine.

Authoring a plugin

A plugin is an npm package with a partisan.json at its root:

{
  "parts": [
    {"name": "part:myapp/root", "description": "Mounts the app"},
    {"implements": "part:myapp/root", "path": "./src/Root.ts"}
  ]
}

If your plugin declares parts (as opposed to only implementing parts declared elsewhere), ship a parts.d.ts so downstream code gets useful types without waiting on the consuming app's codegen:

// parts.d.ts
declare module 'all:part:myapp/tool' {
  import type {Tool} from './src/types.ts'
  const value: readonly Tool[]
  export default value
}

Give the plugin its own tsconfig.json so editors pick it up:

{
  "compilerOptions": {
    "types": ["partisan/client"]
  },
  "include": ["src/**/*", "parts.d.ts"]
}

Codegen

Two ways to trigger it:

  1. Automatically via Vite. The Vite plugin runs codegen in its config() hook (on server start) and on any partisan.json change. If Vite is running, codegen is running.

  2. Via the partisan CLI. The package ships a bin you can run when Vite isn't:

    partisan                    # writes <cwd>/partisan-env.d.ts
    partisan --project ./app    # use ./app as the project root
    partisan --out types/parts.d.ts

    This is what you want in CI before tsc --noEmit, since tsc won't trigger Vite. The example app's typecheck script just runs partisan && tsc --noEmit.

    The CLI is plain compiled JavaScript shipped in the dist/ directory of the package. Node 22.12 or newer is required (enforced via engines.node).

If you want to commit the generated file, set generateTypes to a stable path and check it in. Otherwise add partisan-env.d.ts to .gitignore.

To skip codegen entirely (relying on partisan/client and plugin parts.d.ts files for types):

partisan({generateTypes: false})

Example

examples/acme-app/ contains a small demo that exercises the four part flavors:

  • part:acme/root is declared by @acme/base, implemented by base, then overridden by the project. Last wins.
  • all:part:acme/tool collects implementations from both base and acme-plugin-toolbar.
  • part:acme/brand-logo? is declared but never implemented, so it returns null.
  • part:acme/styles is a CSS file imported through the parts system. Vite's CSS pipeline handles it as if you'd imported the file directly.

Run it:

pnpm install
pnpm dev

Limitations

  • No fully dynamic specifiers. The resolver runs at build time, so import(\part:${runtimeName}`)won't work. AloadPart(name)` runtime helper backed by a generated registry would close the gap if you need it.
  • part:foo? and all:part:foo assume every implementation has a default export. The virtual modules generated for these schemes default-import each impl, so an impl without a default will fail to build. Plain part:foo is just a path redirect and has no such restriction: it accepts any import shape (default, named, namespace, side-effect) that works against the underlying file.
  • HMR for partisan.json changes triggers a full reload rather than a hot patch. Good enough for now.
  • First-time editor opens may briefly show "cannot find module" errors before Vite runs and produces partisan-env.d.ts. The ambient wildcards in partisan/client cover the gap for plugins.

License

MIT. See LICENSE.