Skip to main content
Create a declarative plugin file for any internal framework or tool fallow doesn’t cover. Plugins are JSON or TOML files that tell fallow about your framework’s conventions.
Start with JSONC format for $schema support and IDE autocomplete.

Quick start

1

Create the plugin file

Create a fallow-plugin-<name>.jsonc file 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 auto-discovers fallow-plugin-* files in your project root.
2

Configure the fields

Customize the plugin fields for your framework. Only name is required. Add the fields relevant to your use case.

Required

FieldTypeDescription
namestringUnique plugin name (shown in fallow list --plugins)

Optional

FieldTypeDescription
enablersstring[]Package names that activate this plugin
entryPointsstring[]Glob patterns for entry point files
configPatternsstring[]Glob patterns for config files (always-used)
alwaysUsedstring[]Files always considered used
toolingDependenciesstring[]Packages used via CLI, not imports
detectionobjectRich activation logic (see below)
usedExportsobject[]Exports always considered used
usedClassMembers(string | object)[]Class method/property names the framework invokes at runtime; scoped objects { extends?, implements?, members } match only classes with matching heritage
manifestEntriesobject[]Entry points derived from framework manifest files (see Manifest-derived entries)
3

Verify the plugin

Confirm fallow picks up your plugin, and (for manifestEntries) see exactly what it 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-check is a read-only dry-run. It reports whether each plugin activated (with the unmet requirement when it did not), and for manifestEntries rules the manifests matched, each manifest’s when-gate result, the entries seeded (with a path_exists flag), and typed warnings[]. It always exits 0.

Supported formats

FormatExtensionComments
JSONC.jsonc// and /* */
JSON.jsonNo
TOML.toml#

Detection strategies

Plugins can use simple enablers or rich detection logic with boolean combinators.
The simplest way to activate a plugin. Package names are checked against package.json. The plugin activates if any enabler matches:
{
  "enablers": ["my-framework", "@myorg/"]  // prefix matching with trailing /
}
A trailing / enables prefix matching, so @myorg/ matches any package in the @myorg scope.
Activate a plugin when specific config files exist in the project:
{
  "detection": { "type": "fileExists", "pattern": "nuxt.config.*" }
}
Combine multiple conditions with all (AND), any (OR), or not:
{
  "detection": {
    "type": "all",
    "conditions": [
      { "type": "dependency", "package": "@my-org/core" },
      { "type": "fileExists", "pattern": "my-org.config.*" }
    ]
  }
}
This plugin only activates when both @my-org/core is installed and a my-org.config.* file exists.

Used exports

Mark specific exports as always-used for convention-based frameworks. These are exports consumed by name at runtime:
{
  "usedExports": [
    { "pattern": "src/routes/**/*.{ts,tsx}", "exports": ["default", "loader", "action", "meta"] }
  ]
}

Used class members

Mark class method or property names that the framework invokes at runtime via an interface or contract pattern. Listed names extend fallow’s built-in Angular and React lifecycle allowlist, so members implementing these contracts are never flagged as unused class members. Each entry is either a plain member name (global suppression) or a scoped object that only matches classes whose heritage clause includes the configured extends or implements identifier:
{
  "name": "ag-grid",
  "enablers": ["ag-grid-angular"],
  "usedClassMembers": [
    "agInit",
    { "implements": "ICellRendererAngularComp", "members": ["refresh"] }
  ]
}
Plain names like agInit are unique enough to suppress globally. Common names like refresh or execute would produce false negatives on unrelated classes, so scope them with implements or extends. A scoped rule requires at least one of extends or implements; an unconstrained object rule is rejected at load time. This is the right tool for libraries that call methods on consumer classes reflectively: ag-Grid’s AgFrameworkComponent<T>, TypeORM’s MigrationInterface (up, down), Web Components (connectedCallback, disconnectedCallback, attributeChangedCallback), or any strategy/plugin pattern where the library invokes your methods rather than your code doing so directly. The allowlist only applies to class methods and properties. Enum members with the same names are still checked. For project-wide allowlists that apply regardless of which packages are installed, use the top-level usedClassMembers field in your fallow config instead. See Configuration overview.

Manifest-derived entries

Some frameworks declare their entry points inside per-package manifest files rather than in package.json main/exports or a single app entry. A large monorepo can have hundreds of these, and the entry file for each is conditional on the manifest’s own fields. Static entryPoints globs cannot read those fields, so manifestEntries lets a plugin derive entries from the manifests themselves. Each rule finds manifest files by a recursive glob, parses them (JSON or JSONC), and for every manifest that passes the rule’s when gate, resolves each entries[].path relative to that manifest’s directory into an entry point (using the plugin’s entryPointRole).
// 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}" }
      ]
    }
  ]
}
FieldTypeDescription
manifestsstringRecursive glob selecting the manifest files to read
format"jsonc" | "json"Manifest format. Defaults to jsonc, which also parses plain JSON
whenobjectManifest-level gate: a map of dotted field path to an expected value. ALL must match by strict equality for the manifest to be processed. Empty matches every manifest
entriesobject[]The entries to seed per matching manifest (each { path, when? })
entries[].path is a glob relative to the manifest’s directory. It must encode its own extension (public/index.{ts,tsx}), because entry-point globs are matched literally against discovered files (there is no source-extension probing). It may contain ${dotted.field} interpolation that fans out over the named manifest field: a string field yields one entry, an array field yields one entry per element, and a missing or empty field yields none. Each entry may carry its own when gate. when matching is strict equality: { "plugin.browser": true } matches a manifest whose plugin.browser is literally true, and a manifest with plugin.browser: false is skipped for that entry.
Manifest discovery respects .gitignore and skips node_modules, so manifests under ignored directories are not seen (their entries would not be analyzed 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 (a likely typo). Run fallow plugin-check --format json to see, per rule, which manifests matched, which passed the when gate, the entries seeded (with path_exists), and any warnings; those warnings (manifests-matched-none, when-excluded-all, field-path-unresolved, entries-empty, manifest-parse-failed, entry-outside-root, seeded-paths-missing) are the fastest way to debug a rule that seeds nothing.

Discovery order

Fallow searches for plugin files in this order:
  1. Explicit paths from the plugins config field
  2. .fallow/plugins/ directory
  3. 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"] }
  ]
}

ag-Grid Angular

{
  "$schema": "./plugin-schema.json",
  "name": "ag-grid",
  "enablers": ["ag-grid-angular"],
  "usedClassMembers": [
    "agInit",
    { "implements": "ICellRendererAngularComp", "members": ["refresh"] }
  ]
}
ag-Grid Angular cell renderers implement ICellRendererAngularComp, which requires agInit(params) and refresh(params) methods that ag-Grid calls at runtime. agInit is unique to ag-Grid so it stays global. refresh is a common method name, so scoping it to classes that implements ICellRendererAngularComp keeps suppression off unrelated refresh() methods elsewhere in the codebase.

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. AST-based config parsing (like the built-in ESLint or Vite plugins do) requires a built-in Rust plugin.

JSON Schema

Generate the schema locally for IDE autocomplete. The schema is generated dynamically and doesn’t exist at a remote URL:
fallow plugin-schema > plugin-schema.json
Then reference it in your plugin file:
{
  "$schema": "./plugin-schema.json"
}

See also

Built-in plugins

Browse the 123 built-in framework plugins.

fallow list

Verify active plugins and inspect project metadata.