Custom plugins
Stop false positives from an internal framework or a tool that fallow does not know. A custom plugin is a JSON or TOML file that declares entry points, config files, and used exports.
When fallow does not know an internal framework or tool, it can report the files and exports that the framework uses as unused. A custom plugin fixes this. It is a declarative JSON or TOML file that tells fallow the conventions of your framework.
Start with the JSONC format. It supports $schema and IDE autocomplete.
Quick start
Create the plugin file
Create a
fallow-plugin-<name>.jsoncfile in your project root:{ "$schema": "./plugin-schema.json", "name": "my-framework", "enablers": ["my-framework"], "entryPoints": ["src/routes/**/*.{ts,tsx}"], "alwaysUsed": ["src/setup.ts"], "toolingDependencies": ["my-framework-cli"], "usedExports": [ { "pattern": "src/routes/**/*.{ts,tsx}", "exports": ["default", "loader", "action"] } ] }Fallow discovers
fallow-plugin-*files in your project root automatically.Configure the fields
Set the plugin fields for your framework. Only
nameis required. Add the fields that you need.Required
Field Type Description namestring Unique plugin name (shown in fallow list --plugins)Optional
Field Type Description enablersstring[] Package names that activate this plugin entryPointsstring[] Glob patterns for entry point files entryPointRolestring Coverage role of the entry points: runtime,test, orsupport. Defaults tosupportconfigPatternsstring[] Glob patterns for config files (always used) alwaysUsedstring[] Files that fallow always treats as used toolingDependenciesstring[] Packages that you use through a CLI, not through imports detectionobject Activation logic with conditions (see below) usedExportsobject[] Exports that fallow always treats as used usedClassMembers(string | object)[] Class method or property names that the framework calls at runtime. A scoped object { extends?, implements?, members }matches only classes with matching heritagemanifestEntriesobject[] Entry points that come from framework manifest files (see Manifest-derived entries) Verify the plugin
Make sure that fallow loads your plugin. For
manifestEntries, also see what the plugin matched and seeded:fallow list --plugins # is the plugin active? fallow plugin-check --format json # what did each rule match, seed, and warn about?fallow plugin-checkis a read-only dry run. It always exits 0. It reports whether each plugin activated, and the unmet requirement when a plugin did not. FormanifestEntriesrules, it also reports:- The manifests that matched.
- The
when-gate result for each manifest. - The entries seeded, with a
path_existsflag. - Typed
warnings[].
Supported formats
| Format | Extension | Comments |
|---|---|---|
| JSONC | .jsonc | // and /* */ |
| JSON | .json | No |
| TOML | .toml | # |
Detection strategies
A plugin activates through simple enablers, or through detection logic with boolean combinators.
Simple detection (enablers)
This is the simplest way to activate a plugin. Fallow checks each package name against package.json. The plugin activates if any enabler matches:
{
"enablers": ["my-framework", "@myorg/"] // prefix matching with trailing /
}A trailing / turns on prefix matching, so @myorg/ matches any package in the @myorg scope.
File-based detection
Activate a plugin when specific config files exist in the project:
{
"detection": { "type": "fileExists", "pattern": "nuxt.config.*" }
}Combined detection (boolean logic)
Combine conditions with all (AND) or any (OR). There is no not condition:
{
"detection": {
"type": "all",
"conditions": [
{ "type": "dependency", "package": "@my-org/core" },
{ "type": "fileExists", "pattern": "my-org.config.*" }
]
}
}This plugin activates only when @my-org/core is installed and a my-org.config.* file exists.
Used exports
In a framework that works by convention, the framework uses some exports by name at runtime. Mark these exports as always used:
{
"usedExports": [
{ "pattern": "src/routes/**/*.{ts,tsx}", "exports": ["default", "loader", "action", "meta"] }
]
}
Used class members
List the class method or property names that the framework calls at runtime through an interface or contract. Fallow adds these names to its built-in Angular and React lifecycle allowlist. It then never flags members that implement these contracts as unused class members.
Each entry is one of these:
- A plain member name, which suppresses the name everywhere.
- A scoped object, which matches only classes whose heritage clause includes the configured
extendsorimplementsidentifier.
{
"name": "ag-grid",
"enablers": ["ag-grid-angular"],
"usedClassMembers": [
"agInit",
{ "implements": "ICellRendererAngularComp", "members": ["refresh"] }
]
}
A plain name like agInit is unique enough to suppress everywhere. A common name like refresh or execute would cause false negatives on unrelated classes, so scope it with implements or extends. A scoped rule needs at least one of extends or implements. Fallow rejects an object rule without either at load time.
Use usedClassMembers for libraries that call methods on your classes through reflection. Examples:
- The ag-Grid
AgFrameworkComponent<T>. - The TypeORM
MigrationInterface(up,down). - Web Components (
connectedCallback,disconnectedCallback,attributeChangedCallback). - Any strategy or plugin pattern in which the library calls your methods, and your own code does not.
The allowlist applies only to class methods and properties. Fallow still checks enum members with the same names.
For a project-wide allowlist that applies whatever packages are installed, use the top-level usedClassMembers field in your fallow config. See Configuration overview.
Manifest-derived entries
Some frameworks declare their entry points in a manifest file in each package, not in package.json main/exports or a single app entry. A large monorepo can have hundreds of these manifests, and the fields of each manifest decide its entry file. Static entryPoints globs cannot read those fields. With manifestEntries, a plugin gets its entries from the manifests.
Each rule does these steps:
- Finds manifest files with a recursive glob.
- Parses them (JSON or JSONC).
- For every manifest that passes the
whengate of the rule, resolves eachentries[].pathrelative to the directory of that manifest into an entry point. The entry point gets theentryPointRoleof the plugin.
// fallow-plugin-kibana.jsonc
{
"name": "kibana",
"detection": { "type": "fileExists", "pattern": "**/kibana.jsonc" },
"entryPointRole": "runtime",
"manifestEntries": [
{
"manifests": "**/kibana.jsonc", // recursive glob selecting manifests
"when": { "type": "plugin" }, // only manifests whose `type` is "plugin"
"entries": [
{ "path": "public/index.{ts,tsx}", "when": { "plugin.browser": true } },
{ "path": "server/index.{ts,tsx}", "when": { "plugin.server": true } },
{ "path": "${plugin.extraPublicDirs}/index.{ts,tsx}" }
]
}
]
}
| Field | Type | Description |
|---|---|---|
manifests | string | Recursive glob that selects the manifest files to read |
format | "jsonc" | "json" | Manifest format. Defaults to jsonc, which also parses plain JSON |
when | object | Gate for the manifest: a map from dotted field path to an expected value or an { "exists": true } / { "exists": false } presence test. Fallow processes the manifest only when ALL conditions match. An empty map matches every manifest |
entries | object[] | The entries to seed for each matching manifest (each { path, when? }) |
entries[].path is a glob relative to the directory of the manifest. It must include its own extension (public/index.{ts,tsx}). Fallow matches entry-point globs literally against discovered files and does not try other source extensions.
The path can contain ${dotted.field} interpolation, which expands over the named manifest field:
- A string field gives one entry.
- An array field gives one entry for each element.
- A missing or empty field gives no entry.
Each entry can have its own when gate.
when matching uses strict equality. { "plugin.browser": true } matches a manifest whose plugin.browser is literally true. Fallow skips that entry for a manifest with plugin.browser: false. To test only that a field is present, use { "plugin.browser": { "exists": true } }. A path can go through an array of objects with [*], and it then matches when any value matches.
Manifest discovery respects .gitignore and skips node_modules. Fallow does not see manifests in ignored directories, and it would not analyze their entries anyway.
Fallow emits a warning when a manifests glob matches nothing, when a when gate excludes every manifest, or when a field path resolves in none of the matched manifests (probably a typo).
To debug a rule that seeds nothing, run fallow plugin-check --format json. For each rule, it shows which manifests matched, which passed the when gate, the entries seeded (with path_exists), and any warnings. The warnings are manifests-matched-none, when-excluded-all, field-path-unresolved, entries-empty, manifest-parse-failed, field-values-limit-exceeded, entry-expansion-limit-exceeded, entry-outside-root, and seeded-paths-missing.
Discovery order
Fallow searches for plugin files in this order:
- Explicit paths from the
pluginsconfig field .fallow/plugins/directory- Project root:
fallow-plugin-*.{jsonc,json,toml}files
Using the plugins config field
// .fallowrc.json
{
"plugins": [
"tools/fallow-plugins/",
"vendor/my-plugin.jsonc"
]
}
Examples
React Router
{
"$schema": "./plugin-schema.json",
"name": "react-router",
"enablers": ["react-router", "@tanstack/react-router"],
"entryPoints": ["src/routes/**/*.{ts,tsx}", "app/routes/**/*.{ts,tsx}"],
"configPatterns": ["react-router.config.{ts,js}"],
"toolingDependencies": ["@react-router/dev"],
"usedExports": [
{ "pattern": "{src,app}/routes/**/*.{ts,tsx}", "exports": ["default", "loader", "action", "meta", "handle", "shouldRevalidate"] }
]
}
Internal tooling
{
"$schema": "./plugin-schema.json",
"name": "our-build-system",
"enablers": ["@internal/build"],
"configPatterns": ["build.config.{ts,js}", ".buildrc"],
"alwaysUsed": ["scripts/build/**/*.ts", "config/**/*.ts"],
"toolingDependencies": ["@internal/build", "@internal/lint-rules"]
}
External plugins cover most use cases. To parse config files with an AST, as the built-in ESLint and Vite plugins do, you need a built-in Rust plugin.
JSON Schema
For IDE autocomplete, generate the schema locally. Fallow generates the schema on demand, and it is not available at a remote URL:
fallow plugin-schema > plugin-schema.json
Then reference it in your plugin file:
{
"$schema": "./plugin-schema.json"
}