feat(rrlab): declarative plugin shape#229
Merged
Merged
Conversation
…d bin-probe
Plugins now declare `capabilities` (a `{ kind: service }` map) rather than
implementing an imperative `setup()`. `@rrlab/cli/plugin`'s `definePlugin`
applies `only` narrowing (typed against `keyof ReturnType<capabilities>`),
deduplicates bin probes across services that share a `pkg`, and surfaces
a single canonical "requires X to be installed" error when a peer-installed
tool is missing.
Two new helpers exported from `@rrlab/cli/plugin` capture the remaining
install-time pattern duplication across plugins: `decideScaffold` for the
"file exists? → ask user → create/patch/overwrite/skip" dialog, and
`pickPreset` for preset selection. Plugin sources are now forbidden from
touching `ctx.prompts` directly — `test/integration/plugin-discipline.test.ts`
enforces this with an AST/regex scan over each plugin's sources.
`rr check`'s help summary now lists the actual underlying tools instead of
the generic `(run-run)` label — e.g. `(biome, oxlint)` when biome composes
jsc and oxc provides tsc. Mirrors the same provider resolution as `rr jsc`
and `rr tsc` and flattens composed providers so `biome + biome` collapses
to `biome`.
`@rrlab/oxc-plugin` drops the `oxlint:tsc` UI qualifier; the command name
already disambiguates the mode and the qualifier was inconsistent with how
biome (3 capabilities) and ts (1 capability) label themselves.
Decisions: 007 (per-plugin `only`) marked Superseded by 009; 008 (package
split into `@rrlab/plugin`) kept as a Rejected record because the reasoning
for rejection is worth preserving. `decisions/README.md` adds Rejected as
a fourth status with explicit semantics. 009 is the load-bearing record.
Dogfooding: the repo-root `run-run.config.mts` swaps `ts()` for
`oxc({ only: ['tsc'] })` so `rr tsc` runs `oxlint --type-aware --type-check`
via `oxlint-tsgolint` instead of TypeScript's `tsc`. Adds `oxlint`, `oxfmt`,
and `oxlint-tsgolint` as workspace-root devDependencies. The biome +
`oxc({ only: ['tsc'] })` combination is the exact configuration the
centralized `only` narrowing was designed for.
tsgolint cleanup (19 warnings → 0 across 8 packages):
`run-run/cli`:
- `Doctor`, `Formatter`, `Linter`, `StaticChecker`, `TypeChecker`, `Packer`
switch from method syntax to function-typed property syntax in `types/tool.ts`
and `plugin/types.ts`. Reflects reality — none of the impls bind `this`
inside these methods — and fixes 7 `unbound-method` warnings in
`composed-jsc.test.ts` where `vi.mocked(linter.lint)` / `expect(formatter.format)`
were flagged.
- `PluginDefinition.install`/`uninstall` annotated with `this: void` in
`define-plugin.ts` to satisfy `unbound-method` when forwarded as property
references.
- `plugins.ts` drops the `?? {}` fallbacks in spreads (3 warnings from
`unicorn(no-useless-fallback-in-spread)`).
- `plugins.ts:215` stringifies the `unknown` branch in a template literal
with `String(err)` (`restrict-template-expressions`).
- `ctx.ts:57` captures `plugin.apiVersion` to a `got: number` local before
the guard so the template literal isn't typed `never`.
`shared/loggy`:
- `LogFnOptions | unknown` → `LogFnOptions | string` in `info`, `trace`,
`warn`, `start`, `success`. The `| unknown` collapsed to `unknown`,
losing the design intent; `| string` captures the actual call sites.
- New `LogFn` type in `types.ts` codifies the signature; `AnyLogger`'s
log methods upgrade from `AnyLogFn` (permissive) to `LogFn` so
consumers get useful parameter typing instead of `unknown[]`. `debug`
and `error` stay as `AnyLogFn` — they don't take `LogFnOptions`.
`.vscode/settings.json`: enable `oxc.typeAware: true` so the IDE matches
the CLI's tsgolint behaviour.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 9887d65 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
CI's `rr tsc dispatches to oxlint with --type-aware --type-check` in `only.test.ts` hit vitest's 5000ms default — the run took 6628ms because oxlint with `--type-aware --type-check` spawns `oxlint-tsgolint` and loads the fixture's TypeScript program. Locally it fits under 5s on faster hardware; CI runners don't. Pin the timeout for this specific test to 15000ms (vitest's third-arg form). The sibling `rr lint` / `rr format` / `rr jsc` tests stay on the default since biome is sub-second. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Preview releaseLatest commit: Some packages have been released:
Note Use the PR number as tag to install any package. For instance: |
…r` field `TOOL_VERSIONS` in each `@rrlab/*-plugin/src/tool-versions.ts` carried both `install` and `peer` ranges, kept in sync with `package.json#peerDependencies` by a per-plugin parity test. But `peer` was never read at runtime — the kernel's bin-probe error names the missing package without quoting a range, and the parity test was its only consumer. Two sources of truth for the same value, with one of them dead code. Collapse to one: `tool-versions.ts` keeps only `install` (the prescriptive pin used by `rr plugins add`'s nypm call). The peer contract lives in `package.json#peerDependencies` where npm already enforces it. The per-plugin `tool-versions.test.ts` switches from string-equality with `peer` to `semver.subset(install, peerDependencies[name])` — a stronger invariant that actually checks the install range falls inside the supported peer range. Mechanics: - Drop `peer` from `tool-versions.ts` in biome/oxc/ts/tsdown plugins. - Rewrite each `__tests__/tool-versions.test.ts` to assert subset via semver. Falls back to `satisfies(minVersion(install), peerRange)` when `subset()` can't decide (semver-subset is more conservative than satisfies for hyphen/x-ranges; the min-version check is the practical invariant). - Add `semver` + `@types/semver` as devDeps in each plugin (host-local to the plugin per "each plugin owns its tool" — no kernel coupling). - Entries without a corresponding `peerDependencies` entry (e.g. `@types/node` in ts-plugin) are skipped — they're install-time conveniences, not contracts. This commit is purely structural — install ranges are unchanged. Follow-up commit refreshes the stale 0.x install ranges that the dead `peer` field was masking. Decision: decisions/010-tool-versions-install-only.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Deps `^0.X.Y` semver on 0.x packages only allows patch bumps, so the oxc-plugin install ranges stranded users on old minors: - `oxfmt`: install `^0.30.0` → `^0.51.0`; devDep `0.35.0` → `0.51.0`. npm latest was 0.51.0; `^0.30.0` only allowed 0.30.x. - `oxlint-tsgolint`: install `^0.15.0` → `^0.23.0`; devDep `0.15.0` → `0.23.0`. npm latest was 0.23.0; `^0.15.0` only allowed 0.15.x. - `oxlint`: install stays `^1.0.0` (caret on 1.x already covers 1.66.x); devDep `1.50.0` → `1.66.0` so the plugin tests against current binary. - `@biomejs/biome`: install stays `^2.0.0` (caret covers latest); devDep `2.4.4` → `2.4.15` so the dogfooded lint matches what users get. Workspace-root devDeps mirror the prescriptive ranges: - `oxfmt`: `^0.30.0` → `^0.51.0`. - `oxlint-tsgolint`: `^0.15.0` → `^0.23.0`. Two follow-on changes biome 2.4.15 surfaced: - `biome.json` `$schema` URL bumped to 2.4.15 (otherwise biome warns about config-schema-version mismatch). - `vland/cli/src/actions/init.ts:26` — `!name || !name.trim()` rewritten as `!name?.trim()` for the new `useOptionalChain` rule that biome 2.4.15 enables by default. No code behaviour changes; this is pure version refresh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugin peers (`@rrlab/cli`) move from `workspace:*` to `workspace:^`, and Changesets gets `onlyUpdatePeerDependentsWhenOutOfRange: true`. Once on 1.x, CLI patches/minors stay in `^1.x.y` and stop propagating to plugin majors; CLI majors still escape `^1.0.0` and cascade as intended. The pending changeset escalates `@rrlab/cli` and the 4 plugins to major, landing the whole ecosystem at 1.0.0 in this PR (declarative plugin shape is itself a contract break, so 1.0 is honest about it). Rationale: decisions/011-cli-peer-bump-strategy.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three logically-grouped changes that the squash unified:
oxc({ only: ['tsc'] })at the repo root.1. Declarative plugin shape — decision 009
Plugins now declare
capabilities(a{ kind: service }map) instead of implementing an imperativesetup().Kernel (
@rrlab/cli/plugin):definePlugincentralizesonlynarrowing, typed againstkeyof ReturnType<capabilities>.pkg.decideScaffoldandpickPresetabsorb the install-time prompts that were duplicated across plugins.Discipline:
test/integration/plugin-discipline.test.tsenforces (via AST/regex scan over each plugin's sources) that plugin code does not touchctx.promptsdirectly.UX:
rr check's help summary lists the actual underlying tools instead of the generic(run-run)label — e.g.(biome, oxlint)when biome composesjscand oxc providestsc.biome + biome→biome.@rrlab/oxc-plugindrops theoxlint:tscUI qualifier — it was inconsistent with how biome (3 capabilities) and ts (1 capability) label themselves; the command name already disambiguates the mode.Decisions log:
only) → Superseded by 009.@rrlab/pluginpackage split) → Rejected (record preserved for the reasoning).decisions/README.mdadds Rejected as a fourth status.2. Dogfooded
oxc({ only: ['tsc'] })run-run.config.mtsswapsts()foroxc({ only: ['tsc'] }).rr tscnow runsoxlint --type-aware --type-checkviaoxlint-tsgolintinstead of TypeScript'stsc.oxlint,oxfmt, andoxlint-tsgolintas workspace-root devDependencies.This is the exact configuration the centralized
onlynarrowing was designed for, so the swap also validates the kernel work in (1).3. tsgolint cleanup — 19 warnings → 0 across 8 packages
run-run/cli:Doctor,Formatter,Linter,StaticChecker,TypeChecker,Packerswitch from method syntax to function-typed property syntax intypes/tool.tsandplugin/types.ts. Reflects reality — none of the impls bindthis— and fixes 7unbound-methodwarnings incomposed-jsc.test.tswherevi.mocked(linter.lint)/expect(formatter.format)were flagged.PluginDefinition.install/uninstallannotated withthis: voidindefine-plugin.tsto satisfyunbound-methodwhen forwarded as property references.plugins.tsdrops?? {}fallbacks in spreads (3unicorn(no-useless-fallback-in-spread)warnings).plugins.ts:215stringifies theunknownbranch in a template literal withString(err)(restrict-template-expressions).ctx.ts:57capturesplugin.apiVersionto agot: numberlocal before the guard so the template literal isn't typednever.shared/loggy:LogFnOptions | unknown→LogFnOptions | stringininfo,trace,warn,start,success. The| unknowncollapsed tounknown, losing design intent;| stringmatches the actual call sites.LogFntype intypes.tscodifies the signature.AnyLogger's log methods upgrade fromAnyLogFn(permissive) toLogFnso consumers get useful parameter typing instead ofunknown[].debuganderrorstay asAnyLogFn— they don't takeLogFnOptions.IDE:
.vscode/settings.jsonenablesoxc.typeAware: trueso the IDE matches the CLI's tsgolint behaviour.Test plan
pnpm rr check(lint + format + tsc) green across all 8 packages — 0 warnings, 0 errors underoxc({ only: ['tsc'] }).pnpm testgreen — new tests:define-plugin,bin-probe,decide-scaffold,pick-preset,only,plugin-discipline, plus per-pluginsetup.test.ts.jscheck+tscheck) passed on the squashed commit.rr check --helpshows real tool names (e.g.(biome, oxlint)) instead of(run-run).🤖 Generated with Claude Code