Read the writing-a-plugin docs first:
- Writing A Plugin — plugin responsibilities, lifecycle,
resolveConfig - Inputs — the
Inputtype and helpers liketoEntryandtoDependency - Argument Parsing —
Plugin.argsfor binaries and arguments in scripts
Consider implementing resolve* functions only for custom plugin-specific needs
(core takes care of module resolution, imports, exports, external dependencies).
- Come up with a kebab-cased
name. - Run
pnpm create-plugin --name [name]frompackages/knip. Use--forceif files already exist from a partial run. - Clean up the generated template: remove unused arrays (
config,entry,production),resolveConfig, andtypes.tsif not needed. - Consult similar plugins and the tool's website before implementation.
- Run tests individually first:
bun test test/plugins/[name].test.ts
entry— files to mark as entries but not parse (e.g.src/routes/**/*.tsx). Core handles module graph;resolveConfig/resolveFromASTdoes not run.config— files the plugin parses to extract references (inputs, entries, plugins, etc.).resolveConfig/resolveFromASTrun on these; they're auto-added as entries.
Prefer resolveFromAST (fast, AST-only, no execution) over resolveConfig
(loads and executes the config). Use resolveConfig only when the config is
JSON or when execution is unavoidable (e.g. function configs).
resolveConfig should include entry and production items, either from the
parsed config or the plugin defaults (so they can be overriden). Include the
default entry and production in the plugin export for the docs generator.
See tsdown, rollup, rolldown for simple resolveFromAST examples using
collectPropertyValues. See ast-helpers.ts for available utilities.
Don't walk and parse manifest.scripts manually. Use Plugin.args and find
what core script parser handles. See Argument Parsing and other plugins
like prisma, vitest, webpack.
Non-JS/TS files (.css, .mdx, .vue) are invisible until a compiler
registers their extension: it makes **/*.<ext> project files (unreferenced →
unused) and extracts imports so refs are followed. Built-ins (src/compilers/:
mdx/scss/less/ stylus) auto-enable on a known dependency; plugins gate on
hasDependency. Regex extractors, not parsers.
Library-specific AST patterns that generic traversal misses are caught by
visitors in core's single walk. A create*Visitor(ctx) feeds the graph via
ctx: addImport (knex, pino), addImportGlob (vite import.meta.glob,
webpack/rspack require.context), addScript (execa, nano-spawn),
markExportRegistered (custom elements: lit/fast/stencil/catalyst).
Enabled-only, zero cost when off. Factories in plugins/[name]/visitors/,
shared in _custom-elements/.
- Use non-default file names in config fields (e.g.
input: 'src/app.ts', not'index.ts'or'main.ts') so the test actually verifies the plugin resolves them — default entry patterns would pick those up regardless.