> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fallow.tools/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom plugins

> Create custom fallow plugins for internal frameworks using declarative JSON or TOML files. Define entry points, config files, and used exports.

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.

<Tip>
  Start with JSONC format for `$schema` support and IDE autocomplete.
</Tip>

## Quick start

<Steps>
  <Step title="Create the plugin file">
    Create a `fallow-plugin-<name>.jsonc` file in your project root:

    ```jsonc theme={null}
    {
      "$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.
  </Step>

  <Step title="Configure the fields">
    Customize the plugin fields for your framework. Only `name` is required. Add the fields relevant to your use case.

    ### Required

    | Field  | Type   | Description                                           |
    | :----- | :----- | :---------------------------------------------------- |
    | `name` | string | Unique plugin name (shown in `fallow list --plugins`) |

    ### Optional

    | Field                 | Type                  | Description                                                                                                                                                 |
    | :-------------------- | :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `enablers`            | string\[]             | Package names that activate this plugin                                                                                                                     |
    | `entryPoints`         | string\[]             | Glob patterns for entry point files                                                                                                                         |
    | `configPatterns`      | string\[]             | Glob patterns for config files (always-used)                                                                                                                |
    | `alwaysUsed`          | string\[]             | Files always considered used                                                                                                                                |
    | `toolingDependencies` | string\[]             | Packages used via CLI, not imports                                                                                                                          |
    | `detection`           | object                | Rich activation logic (see below)                                                                                                                           |
    | `usedExports`         | object\[]             | 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 |
    | `manifestEntries`     | object\[]             | Entry points derived from framework manifest files (see [Manifest-derived entries](#manifest-derived-entries))                                              |
  </Step>

  <Step title="Verify the plugin">
    Confirm fallow picks up your plugin, and (for `manifestEntries`) see exactly what it matched and seeded:

    ```bash theme={null}
    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.
  </Step>
</Steps>

## Supported formats

| Format | Extension | Comments         |
| :----- | :-------- | :--------------- |
| JSONC  | `.jsonc`  | `//` and `/* */` |
| JSON   | `.json`   | No               |
| TOML   | `.toml`   | `#`              |

## Detection strategies

Plugins can use simple enablers or rich detection logic with boolean combinators.

<Accordion title="Simple detection (enablers)">
  The simplest way to activate a plugin. Package names are checked against `package.json`. The plugin activates if any enabler matches:

  ```jsonc theme={null}
  {
    "enablers": ["my-framework", "@myorg/"]  // prefix matching with trailing /
  }
  ```

  A trailing `/` enables prefix matching, so `@myorg/` matches any package in the `@myorg` scope.
</Accordion>

<Accordion title="File-based detection">
  Activate a plugin when specific config files exist in the project:

  ```jsonc theme={null}
  {
    "detection": { "type": "fileExists", "pattern": "nuxt.config.*" }
  }
  ```
</Accordion>

<Accordion title="Combined detection (boolean logic)">
  Combine multiple conditions with `all` (AND), `any` (OR), or `not`:

  ```jsonc theme={null}
  {
    "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.
</Accordion>

## Used exports

Mark specific exports as always-used for convention-based frameworks. These are exports consumed by name at runtime:

```jsonc theme={null}
{
  "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:

```jsonc theme={null}
{
  "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](/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`).

```jsonc theme={null}
// 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 selecting the manifest files to read                                                                                                                    |
| `format`    | `"jsonc"` \| `"json"` | Manifest format. Defaults to `jsonc`, which also parses plain JSON                                                                                                     |
| `when`      | object                | Manifest-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 |
| `entries`   | object\[]             | 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.

<Note>
  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.
</Note>

## 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

```jsonc theme={null}
// .fallowrc.json
{
  "plugins": [
    "tools/fallow-plugins/",
    "vendor/my-plugin.jsonc"
  ]
}
```

## Examples

### React Router

```jsonc theme={null}
{
  "$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

```jsonc theme={null}
{
  "$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

```jsonc theme={null}
{
  "$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"]
}
```

<Info>
  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.
</Info>

## JSON Schema

Generate the schema locally for IDE autocomplete. The schema is generated dynamically and doesn't exist at a remote URL:

```bash theme={null}
fallow plugin-schema > plugin-schema.json
```

Then reference it in your plugin file:

```jsonc theme={null}
{
  "$schema": "./plugin-schema.json"
}
```

## See also

<CardGroup cols={2}>
  <Card title="Built-in plugins" icon="puzzle-piece" href="/frameworks/built-in">
    Browse the built-in framework and tooling plugins.
  </Card>

  <Card title="fallow list" icon="list" href="/cli/list">
    Verify active plugins and inspect project metadata.
  </Card>
</CardGroup>
