@cfxlabsinc/nx-biomeWorkspace-internal Nx plugin that gives every project its own cached lint and format targets.
Adapted from Integrate Biome in 20 Minutes — see Deviations for where and why this differs.
The plugin's createNodesV2 watches for **/project.json and contributes two targets to every project except the workspace root.
lint — lint + format, with eslint| Configuration | Command |
|---|---|
| (default) | biome check <projectRoot> && eslint <projectRoot> |
fix |
biome check --fix <projectRoot> && eslint --fix … |
ci |
biome ci <projectRoot> && eslint <projectRoot> |
nx run <pkg>:lint -c fix # one project
nx affected -t lint -c ci # what CI runs
nx run-many -t lint -c fix # everything
format — formatting onlybiome check --fix already formats, so this is the narrower, faster option when you only want to reformat and don't want lint diagnostics.
| Configuration | Command |
|---|---|
| (default) | biome format --write <projectRoot> |
check / ci |
biome format <projectRoot> (no write) |
nx run-many -t format # format everything
nx affected -t format # only what changed
nx run <pkg>:format:check # dry run
Its cache is tighter than lint's: formatting is purely local, so inputs omits ^default and an unrelated upstream edit doesn't invalidate it.
This covers only what Biome formats — JS/TS/JSX/TSX, JSON/JSONC, CSS, GraphQL. .sql, .md and .yml/.yaml still belong to Prettier via nx format write --all; see rules/formatting.md.
This replaced a single root lint target running bun biome check && bun eslint over the whole repo — one task, no cache, no inputs, so every invocation re-checked all 4531 files. Per-project targets are cacheable and nx affected-aware, so a one-package change lints one package.
The workspace root is skipped explicitly (projectRoot === "."). It is its own Nx project, and giving it a lint target would restore exactly the whole-repo task these replace.
There are no per-project Biome configs today. The command scopes Biome to a directory while the root biome.jsonc still supplies the rules, so a project opts into nothing and inherits everything.
If a project ever does grow its own biome.json/biome.jsonc, Biome v2 discovers and merges it automatically when it declares "extends": "//" (which implies "root": false) — so the command never needs --config-path. The plugin's only job there is to add that file to inputs, or a project-local rule change would not invalidate the cache. See Biome: big projects.
**/project.json, not **/biome.json. The article globs Biome configs so projects can adopt Biome one at a time. This workspace already lints everything from one root config, so the opt-in file would only ever match the root — which the plugin then skips, inferring nothing at all.eslint.config.mjs globally ignores every .ts/.tsx file, so all that remains is @nx/dependency-checks over **/*/package.json — the rule that generates each package's dependencies. Dropping it would stop manifest drift being caught. --no-error-on-unmatched-pattern covers projects with nothing for eslint to match.^default in inputs. Biome never reads a dependency's sources, so for Biome alone this would be pure cache invalidation with no correctness value. It is here for the eslint half: @nx/dependency-checks walks the whole dependency closure (includeTransitiveDependencies: true) to derive a project's manifest, so an upstream import change has to invalidate this project's cache.Registered in workspace nx.json under plugins, excluding the vendored Pulumi provider SDKs (which biome.jsonc also ignores, and which the @nx/js/typescript plugin excludes the same way):
{
"plugin": "@cfxlabsinc/nx-biome",
"options": {},
"exclude": ["packages/pulumi/sdks/*/**"]
}
Resolves through the workspace dependency in the root package.json:
"@cfxlabsinc/nx-biome": "workspace:*"