Skip to content

Declarative external-plugin format: extensions to cover more plugins + migration candidates #1774

Description

@BartWaardenburg

Background

manifestEntries lets a user-authored external plugin (fallow-plugin-*.jsonc) seed entry points DERIVED from framework manifest files: find manifests by a recursive glob, parse JSON/JSONC, gate on a dotted-field when (strict equality), and resolve each entries[].path (with ${dotted.field} string/array fan-out) relative to the manifest's directory. This makes manifest-driven frameworks self-serviceable in a config file instead of requiring a built-in plugin.

That raises a natural question: can existing built-in plugins move to the declarative format, shrinking the compiled-Rust plugin surface? A survey of the built-in plugins splits them into three tiers, and the answer is "a few, once the format grows a little."

Survey of the built-in plugins

  • AST config parsers (~62): parse arbitrary JS/TS config through the Oxc parser (resolve.alias, entry, plugins, path aliases, referenced deps). Declarative data cannot execute or parse JavaScript, so these are not expressible. Out of scope; they stay in Rust.
  • JSON/JSONC manifest entry seeders (a handful): browser-extension, obsidian, commit-and-tag-version. This is manifestEntries' family, but they use capabilities v1 deliberately deferred (below).
  • Static-only (~40): enablers plus a couple of entryPoints / alwaysUsed globs plus toolingDependencies (ava, mocha, karma, husky, cspell, svgo, and similar). These are already expressible with the existing external-plugin static fields; they never needed manifestEntries.

Format extensions needed to cover the Tier-2 seeders

  1. Object-array iteration (content_scripts[*].js). ${field} currently fans out over SCALAR arrays only. browser-extension seeds content_scripts[].js (an array of objects, each carrying a js array), which needs iteration over an object array plus a sub-field select. Size: M.
  2. Presence / exists operator on when. obsidian activates on a manifest SHAPE (id / name / version / minAppVersion present) rather than a specific value. v1 when is strict-equality only; a { "field": { "exists": true } } form was reserved for exactly this. Size: S.
  3. Declarative pathAliases / referencedDependencies outputs. Several plugins credit dependencies or path aliases derived from a manifest, not just entries. The external format seeds entries / always-used / tooling only today. Size: M.
  4. TOML / YAML manifest parsing. v1 parses JSON / JSONC. Size: S.
  5. Per-seed role (entries currently inherit the plugin's single entryPointRole). Additive-safe. Size: S.

Migration candidates (each gated on an extension above)

Recommendation

Do NOT mass-migrate the built-ins. They are compiled, individually tested (activation checks plus negative-check fixtures), and each is tiny. Moving them to data trades away compile-time safety and their per-plugin fixtures for a modest maintenance gain, with real regression risk. manifestEntries' value is new and unsupported manifest-driven frameworks plus adopter self-serve, not replacing plugins that already work.

If shrinking the Rust plugin surface becomes a goal, the honest sequence is:

  1. Land extensions chore(deps): bump actions/setup-node from 4 to 6 #1 (object-array iteration) and chore(deps): bump actions/download-artifact from 4 to 8 #2 (presence when) - the two that unlock real Tier-2 candidates.
  2. Migrate obsidian and browser-extension as a proof of value, keeping their existing fixtures as parity tests (the migrated declarative plugin must produce byte-identical results on the fixture before the Rust version is removed).
  3. Reassess before going wider.

A complementary author-experience item (already noted separately): a fallow plugin check dry-run that shows which manifests matched, which passed each when, and which entries were seeded, would make authoring and migrating declarative plugins far less blind.

Non-goals

  • The ~62 AST-config plugins stay in Rust.
  • No wholesale migration of working static plugins without a byte-identical parity-test guarantee per plugin.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions