Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8542b4a
fix: handle pnpm catalog for noReactForwardRef
apple-yagi Dec 8, 2025
31dbd8e
feat: add support for named catalogs in pnpm workspace
apple-yagi Dec 8, 2025
416b805
fix: update find_pnpm_workspace_catalog to use the current directory …
apple-yagi Dec 8, 2025
b800186
fix: refactor find_pnpm_workspace_catalog to take a FileSystem trait
apple-yagi Dec 8, 2025
bd7f3d8
docs: clarify catalog resolution helpers
apple-yagi Dec 9, 2025
ce1362c
refactor: simplify catalog prefix match
apple-yagi Dec 9, 2025
00d9c6e
refactor: rename pnpm catalog entry parser
apple-yagi Dec 9, 2025
857d98b
test: add tests for finding catalog from workspace file
apple-yagi Dec 9, 2025
d4c1c25
test: add tests for pnpm workspace catalog parsing and dependency res…
apple-yagi Dec 9, 2025
71bc04a
feat: add support for pnpm catalogs in dependency resolution for linting
apple-yagi Dec 9, 2025
db84985
docs: clarify PackageJson catalog field
apple-yagi Dec 9, 2025
dd60487
fix: normalize pnpm catalog parsing and fs-based tests
apple-yagi Dec 9, 2025
d484609
Update crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/…
apple-yagi Dec 9, 2025
cca1223
Update crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/…
apple-yagi Dec 9, 2025
c1d2300
refactor: use biome_yaml_parser AST to parse pnpm workspace catalogs
apple-yagi Dec 9, 2025
017c18e
test: look for pnpm-workspace.yaml next to test input
apple-yagi Dec 9, 2025
cf54a68
feat: load pnpm workspace catalogs and integrate with layout
apple-yagi Dec 10, 2025
07ea8f9
feat: refresh package catalogs when pnpm-workspace.yaml changes
apple-yagi Dec 10, 2025
828db5c
fix: update diagnostic comments in React 19 catalog snapshots
apple-yagi Dec 10, 2025
b03b7ab
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 10, 2025
60221e4
fix lint
apple-yagi Dec 12, 2025
5c3c29c
fix: update load_pnpm_workspace_catalog to accept a starting directory
apple-yagi Dec 12, 2025
6c68def
Update .changeset/nasty-cities-check.md
apple-yagi Dec 13, 2025
90efb49
fix lint
apple-yagi Dec 13, 2025
5f35cbf
Update crates/biome_test_utils/src/lib.rs
apple-yagi Dec 13, 2025
e4384d2
chore(changeset): switch pnpm catalog changeset from patch to minor
apple-yagi Feb 23, 2026
2f33f12
test: update pnpm catalog diagnostics for suspicious/noReactForwardRef
apple-yagi Feb 23, 2026
8ba6504
feat(configuration): add javascript resolver pnpm catalogs opt-in
apple-yagi Feb 24, 2026
70ae4f0
test(package): add fail-safe tests for pnpm workspace catalog parsing
apple-yagi Feb 24, 2026
bbdf0fb
docs(configuration): clarify experimentalPnpmCatalogs behavior
apple-yagi Feb 24, 2026
e81614c
fix(fs): classify pnpm-workspace.yaml as a manifest file
apple-yagi Feb 25, 2026
2d5e5a5
fix(lsp): reload workspace on pnpm-workspace.yaml changes
apple-yagi Feb 25, 2026
df172e8
fix(service): index pnpm-workspace.yaml and refresh catalogs on change
apple-yagi Feb 25, 2026
de1cccd
docs(package): document Catalogs field mapping with YAML examples
apple-yagi Feb 25, 2026
318051c
docs(changeset): note pnpm catalog support is opt-in
apple-yagi Feb 25, 2026
4a94be7
Merge branch 'next' into fix/no-react-forward-ref-pnpm-catalog
apple-yagi Feb 25, 2026
fb785de
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nasty-cities-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": minor
---

Biome now supports pnpm catalogs (default and named) when resolving dependencies for linting. This behavior is opt-in and requires setting `javascript.resolver.experimentalPnpmCatalogs` to `true`.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 80 additions & 0 deletions crates/biome_cli/tests/cases/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,86 @@ fn max_diagnostics_no_verbose() {
));
}

#[test]
fn reads_pnpm_workspace_catalog() {
let fs = MemoryFileSystem::default();
let mut console = BufferConsole::default();

fs.insert(
Utf8Path::new("pnpm-workspace.yaml").into(),
r#"
packages:
- "src"
catalogs:
react19:
react: 19.0.0
"#
.as_bytes(),
);

fs.insert(
Utf8Path::new("package.json").into(),
r#"
{
"name": "app",
"dependencies": {
"react": "catalog:react19"
}
}
"#
.as_bytes(),
);

fs.insert(
Utf8Path::new("biome.json").into(),
r#"
{
"javascript": {
"resolver": {
"experimentalPnpmCatalogs": true
}
},
"linter": {
"rules": {
"suspicious": {
"noReactForwardRef": "error"
}
}
}
}
"#
.as_bytes(),
);

fs.insert(
Utf8Path::new("src/input.jsx").into(),
r#"
import { forwardRef } from "react";

export const Component = forwardRef((props, ref) => {
return <div ref={ref} {...props} />;
});
"#
.as_bytes(),
);

let (fs, result) = run_cli(
fs,
&mut console,
Args::from(["check", "src/input.jsx"].as_slice()),
);

assert!(result.is_err(), "run_cli returned {result:?}");

assert_cli_snapshot(SnapshotPayload::new(
module_path!(),
"reads_pnpm_workspace_catalog",
fs,
console,
result,
));
}

#[test]
fn should_fail_when_max_diagnostics_is_zero() {
let fs = MemoryFileSystem::default();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
source: crates/biome_cli/tests/snap_test.rs
expression: redactor(content)
---
## `biome.json`

```json
{
"javascript": {
"resolver": {
"experimentalPnpmCatalogs": true
}
},
"linter": {
"rules": {
"suspicious": {
"noReactForwardRef": "error"
}
}
}
}
```

## `package.json`

```json

{
"name": "app",
"dependencies": {
"react": "catalog:react19"
}
}

```

## `pnpm-workspace.yaml`

```yaml

packages:
- "src"
catalogs:
react19:
react: 19.0.0

```

## `src/input.jsx`

```jsx

import { forwardRef } from "react";

export const Component = forwardRef((props, ref) => {
return <div ref={ref} {...props} />;
});

```

# Termination Message

```block
check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Some errors were emitted while running checks.



```

# Emitted Messages

```block
src/input.jsx:4:26 lint/suspicious/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Use of forwardRef is detected, which is deprecated.

2 │ import { forwardRef } from "react";
3 │
> 4 │ export const Component = forwardRef((props, ref) => {
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 5 │ return <div ref={ref} {...props} />;
> 6 │ });
│ ^^
7 │

i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead.

i Replace the use of forwardRef with passing ref as a prop.

i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop.

2 2 │ import { forwardRef } from "react";
3 3 │
4 │ - export·const·Component·=·forwardRef((props,·ref)·=>·{
4 │ + export·const·Component·=·({·ref,·...props·})·=>·{
5 5 │ return <div ref={ref} {...props} />;
6 │ - });
6 │ + };
7 7 │


```

```block
src/input.jsx format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Formatter would have printed the following content:

1 │ -
2 1 │ import { forwardRef } from "react";
3 2 │
4 3 │ export const Component = forwardRef((props, ref) => {
5 │ - ··return·<div·ref={ref}·{...props}·/>;
4 │ + → return·<div·ref={ref}·{...props}·/>;
6 5 │ });
7 6 │


```

```block
Checked 1 file in <TIME>. No fixes applied.
Found 2 errors.
```
41 changes: 41 additions & 0 deletions crates/biome_configuration/src/javascript/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
use std::str::FromStr;

pub type ExperimentalEmbeddedSnippetsEnabled = Bool<false>;
pub type ExperimentalPnpmCatalogsEnabled = Bool<false>;

/// A set of options applied to the JavaScript files
#[derive(
Expand Down Expand Up @@ -35,6 +36,11 @@ pub struct JsConfiguration {
#[serde(skip_serializing_if = "Option::is_none")]
pub assist: Option<JsAssistConfiguration>,

/// Module/dependency resolver options
#[bpaf(external(js_resolver_configuration), optional)]
#[serde(skip_serializing_if = "Option::is_none")]
pub resolver: Option<JsResolverConfiguration>,

/// A list of global bindings that should be ignored by the analyzers
///
/// If defined here, they should not emit diagnostics.
Expand All @@ -53,6 +59,41 @@ pub struct JsConfiguration {
pub experimental_embedded_snippets_enabled: Option<ExperimentalEmbeddedSnippetsEnabled>,
}

/// Resolver options specific to JavaScript files
#[derive(
Bpaf, Clone, Debug, Default, Deserializable, Deserialize, Eq, Merge, PartialEq, Serialize,
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
pub struct JsResolverConfiguration {
/// Enables pnpm workspace catalog resolution for JavaScript package manifests.
///
/// Opt-in:
/// - Set `javascript.resolver.experimentalPnpmCatalogs` to `true`.
///
/// Scope:
/// - Resolves `catalog:` and `catalog:<name>` dependency versions from
/// `package.json`.
/// - Applies to `dependencies`, `devDependencies`, and `peerDependencies`.
///
/// Fail-safe behavior:
/// - If `pnpm-workspace.yaml` is missing, unreadable, or cannot be parsed,
/// Biome silently falls back to the default behavior (as if this option
/// were disabled).
/// - Unknown keys and unsupported value shapes in `pnpm-workspace.yaml` are
/// ignored.
///
/// Limitations:
/// - Only `pnpm-workspace.yaml` is read.
/// - Biome only reads top-level `catalog` / `catalogs` mappings and scalar
/// string entries.
///
/// Default: `false`.
#[bpaf(hide)]
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental_pnpm_catalogs: Option<ExperimentalPnpmCatalogsEnabled>,
}

pub type UnsafeParameterDecoratorsEnabled = Bool<false>;
pub type JsxEverywhere = Bool<true>;
pub type JsGritMetavariable = Bool<false>;
Expand Down
13 changes: 11 additions & 2 deletions crates/biome_fs/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,19 @@ impl BiomePath {

/// The priority of the file.
/// - `biome.json` and `biome.jsonc` have the highest priority
/// - `package.json`, `tsconfig.json`/`jsconfig.json`, and `turbo.json` have the second-highest priority, and they are considered as manifest files
/// - `package.json`, `tsconfig.json`/`jsconfig.json`, `turbo.json`, and `pnpm-workspace.yaml` have the second-highest priority, and they are considered as manifest files
/// - Other files are considered as files to handle
fn priority(file_name: &str) -> FileKinds {
if file_name == ConfigName::biome_json() || file_name == ConfigName::biome_jsonc() {
FileKinds::Config
} else if matches!(
file_name,
"package.json" | "tsconfig.json" | "jsconfig.json" | "turbo.json" | "turbo.jsonc"
"package.json"
| "tsconfig.json"
| "jsconfig.json"
| "turbo.json"
| "turbo.jsonc"
| "pnpm-workspace.yaml"
) {
FileKinds::Manifest
} else if matches!(file_name, ".gitignore" | ".ignore") {
Expand Down Expand Up @@ -300,6 +305,10 @@ mod test {
assert_eq!(BiomePath::priority("package.json"), FileKinds::Manifest);
assert_eq!(BiomePath::priority("turbo.json"), FileKinds::Manifest);
assert_eq!(BiomePath::priority("turbo.jsonc"), FileKinds::Manifest);
assert_eq!(
BiomePath::priority("pnpm-workspace.yaml"),
FileKinds::Manifest
);
assert_eq!(BiomePath::priority("biome.json"), FileKinds::Config);
assert_eq!(BiomePath::priority("biome.jsonc"), FileKinds::Config);
assert_eq!(BiomePath::priority(".gitignore"), FileKinds::Ignore);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* should generate diagnostics */

import { forwardRef } from "react";

const Component = forwardRef((props, ref) => {
return <div ref={ref} {...props} />;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
assertion_line: 154
expression: catalog.js
---
# Input
```js
/* should generate diagnostics */

import { forwardRef } from "react";

const Component = forwardRef((props, ref) => {
return <div ref={ref} {...props} />;
});

```

_Note: The parser emitted 4 diagnostics which are not shown here._

# Diagnostics
```
catalog.js:5:19 lint/suspicious/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! Use of forwardRef is detected, which is deprecated.

3 │ import { forwardRef } from "react";
4 │
> 5 │ const Component = forwardRef((props, ref) => {
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 6 │ return <div ref={ref} {...props} />;
> 7 │ });
│ ^^
8 │

i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead.

i Replace the use of forwardRef with passing ref as a prop.

i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop.

3 3 │ import { forwardRef } from "react";
4 4 │
5 │ - const·Component·=·forwardRef((props,·ref)·=>·{
5 │ + const·Component·=·({·ref,·...props·})·=>·{
6 6 │ return <div ref={ref} {...props} />;
7 │ - });
7 │ + };
8 8 │


```
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"react": "catalog:"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
packages:
- "./*"

catalog:
react: 19.0.0
Loading
Loading