Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/dull-shirts-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@biomejs/biome": minor
---

Added `bundleDependencies` option to [NoUndeclaredDependencies](https://biomejs.dev/linter/rules/no-undeclared-dependencies) rule.

This rule now supports imports of packages that are defined only in `bundleDependencies` array.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ declare_lint_rule! {
/// - `devDependencies`: If set to `false`, then the rule will show an error when `devDependencies` are imported. Defaults to `true`.
/// - `peerDependencies`: If set to `false`, then the rule will show an error when `peerDependencies` are imported. Defaults to `true`.
/// - `optionalDependencies`: If set to `false`, then the rule will show an error when `optionalDependencies` are imported. Defaults to `true`.
/// - `bundleDependencies`: If set to `false`, then the rule will show an error when `bundleDependencies` are imported. Defaults to `true`.
///
/// You can set the options like this:
///
Expand All @@ -74,7 +75,8 @@ declare_lint_rule! {
/// "options": {
/// "devDependencies": false,
/// "peerDependencies": false,
/// "optionalDependencies": false
/// "optionalDependencies": false,
/// "bundleDependencies": false
/// }
/// }
/// ```
Expand All @@ -86,9 +88,8 @@ declare_lint_rule! {
///
/// ### Example using the `devDependencies` option
///
/// In this example, only test files can use dependencies in the
/// `devDependencies` section. `dependencies`, `peerDependencies`, and
/// `optionalDependencies` are always available.
/// In this example, only test files can use dependencies in the `devDependencies` section.
/// `dependencies`, `peerDependencies`, `optionalDependencies` and `bundleDependencies` are always available.
///
/// ```json,options
/// {
Expand Down Expand Up @@ -133,6 +134,7 @@ pub struct RuleState {
is_dev_dependency_available: bool,
is_peer_dependency_available: bool,
is_optional_dependency_available: bool,
is_bundle_dependency_available: bool,
}

impl Rule for NoUndeclaredDependencies {
Expand Down Expand Up @@ -165,12 +167,18 @@ impl Rule for NoUndeclaredDependencies {
.optional_dependencies
.as_ref()
.is_none_or(|dep| dep.is_available(path));
let is_bundle_dependency_available = ctx
.options()
.bundle_dependencies
.as_ref()
.is_none_or(|dep| dep.is_available(path));

let is_available = |package_name| {
ctx.is_dependency(package_name)
|| (is_dev_dependency_available && ctx.is_dev_dependency(package_name))
|| (is_peer_dependency_available && ctx.is_peer_dependency(package_name))
|| (is_optional_dependency_available && ctx.is_optional_dependency(package_name))
|| (is_bundle_dependency_available && ctx.is_bundle_dependency(package_name))
};

let token_text = node.inner_string_text()?;
Expand Down Expand Up @@ -206,6 +214,7 @@ impl Rule for NoUndeclaredDependencies {
is_dev_dependency_available,
is_peer_dependency_available,
is_optional_dependency_available,
is_bundle_dependency_available,
})
}

Expand All @@ -215,6 +224,7 @@ impl Rule for NoUndeclaredDependencies {
is_dev_dependency_available,
is_peer_dependency_available,
is_optional_dependency_available,
is_bundle_dependency_available,
} = state;

let Some(package_path) = ctx.package_path.as_ref() else {
Expand Down Expand Up @@ -252,6 +262,8 @@ impl Rule for NoUndeclaredDependencies {
Some("peerDependencies")
} else if ctx.is_optional_dependency(package_name) && !is_optional_dependency_available {
Some("optionalDependencies")
} else if ctx.is_bundle_dependency(package_name) && !is_bundle_dependency_available {
Some("bundleDependencies")
} else {
None
};
Expand Down
7 changes: 7 additions & 0 deletions crates/biome_js_analyze/src/services/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ impl ManifestServices {
.as_deref()
.is_some_and(|pkg| pkg.optional_dependencies.contains(specifier))
}

pub(crate) fn is_bundle_dependency(&self, specifier: &str) -> bool {
self.manifest.as_deref().is_some_and(|pkg| {
pkg.bundle_dependencies.contains(specifier)
|| pkg.bundled_dependencies.contains(specifier)
})
}
}

impl FromServices for ManifestServices {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
},
"devDependencies": {
"@testing-library/react": "1.0.0"
}
},
"bundleDependencies": true,
"bundledDependencies": false
Comment on lines +8 to +9
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

bundleDependencies: true has real npm semantics — worth a coverage note.

Per the npm spec, "bundleDependencies": true means "bundle all dependencies", not merely a no-op. The current fixture cleverly uses booleans to verify the parser doesn't crash on non-array values, which is good. However, there's a small coverage gap: when the field is true, a strict implementation could infer that every package listed under "dependencies" is also bundled. If the parser simply skips non-array values (the most pragmatic approach), that's fine — but it may be worth adding a brief comment in the corresponding .ts invalid test file explaining why booleans are used here, since future contributors may otherwise wonder why not an array.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.package.json`
around lines 8 - 9, The fixture uses boolean values for "bundleDependencies" and
"bundledDependencies" which could be misinterpreted by a strict implementation
as meaning "bundle all dependencies"; update the corresponding invalid test .ts
file to add a short comment explaining that the booleans are intentionally used
to ensure the parser does not crash on non-array values and that the test
deliberately does not assert "all dependencies are bundled" semantics; reference
the JSON fields "bundleDependencies", "bundledDependencies", and "dependencies"
in the comment so future contributors understand why a boolean rather than an
array is provided.

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,11 @@
},
"optionalDependencies": {
"optional-dep": "1.0.0"
}
},
"bundleDependencies": [
"bundle-dep"
],
"bundledDependencies": [
"bundled-dep"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { fontFamily } from "tailwindcss/defaultTheme";

import "peer-dep";
import "optional-dep";
import "bundle-dep";
import "bundled-dep";

import "my-package"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { fontFamily } from "tailwindcss/defaultTheme";

import "peer-dep";
import "optional-dep";
import "bundle-dep";
import "bundled-dep";

import "my-package"

Expand Down
91 changes: 91 additions & 0 deletions crates/biome_package/src/node_js_package/package_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub struct PackageJson {
pub dev_dependencies: Dependencies,
pub peer_dependencies: Dependencies,
pub optional_dependencies: Dependencies,
pub bundle_dependencies: BundleDependencies,
pub bundled_dependencies: BundleDependencies,
pub license: Option<(Box<str>, TextRange)>,

pub author: Option<Box<str>>,
Expand Down Expand Up @@ -191,6 +193,66 @@ impl Dependencies {
}
}

#[derive(Debug, Default, Clone)]
pub struct BundleDependencies(pub Box<[Box<str>]>);

/// The "bundleDependencies" field is usually an array of package names (not a map like the other dependencies fields).
/// It can also be a boolean (`true` to mean “bundle everything”).
impl Deserializable for BundleDependencies {
fn deserialize(
ctx: &mut impl DeserializationContext,
value: &impl DeserializableValue,
name: &str,
) -> Option<Self> {
struct Visitor;

impl DeserializationVisitor for Visitor {
type Output = BundleDependencies;

const EXPECTED_TYPE: DeserializableTypes =
DeserializableTypes::ARRAY.union(DeserializableTypes::BOOL);

fn visit_array(
self,
ctx: &mut impl DeserializationContext,
items: impl ExactSizeIterator<Item = Option<impl DeserializableValue>>,
_range: TextRange,
name: &str,
) -> Option<Self::Output> {
let values = items
.filter_map(|item| {
let item = item?;
Deserializable::deserialize(ctx, &item, name)
})
.collect::<Vec<Box<str>>>();

Some(BundleDependencies(values.into_boxed_slice()))
}

fn visit_bool(
self,
_ctx: &mut impl DeserializationContext,
_value: bool,
_range: TextRange,
_name: &str,
) -> Option<Self::Output> {
// Allow boolean value and treat it as “no explicit list”.
Some(BundleDependencies::default())
}
}

value.deserialize(ctx, Visitor, name)
}
}

impl BundleDependencies {
pub fn contains(&self, specifier: &str) -> bool {
self.0
.iter()
.any(|dependency| dependency.as_ref() == specifier)
}
}
Comment on lines +196 to +254
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

"bundleDependencies": true is silently swallowed.

npm supports a boolean value: true bundles all dependencies, false bundles none. When the field holds true, Vec<Box<str>>::deserialize returns None, the if let Some(deps) guard is never taken, and the field stays as the default empty BundleDependencies. The rule will then report every import from that package as undeclared, producing false positives.

This is a known edge case; a full fix would require knowing all dependencies keys at that point, so it's fine to leave for a follow-up. A short // TODO comment here would prevent future confusion.

💡 Suggested TODO
 impl Deserializable for BundleDependencies {
     fn deserialize(
         ctx: &mut impl DeserializationContext,
         value: &impl DeserializableValue,
         name: &str,
     ) -> Option<Self> {
+        // TODO: handle `"bundleDependencies": true` (bundle all deps) –
+        // that requires access to the `dependencies` map, which isn't available here.
         let values: Vec<Box<str>> = Deserializable::deserialize(ctx, value, name)?;
         Some(Self(values.into_boxed_slice()))
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_package/src/node_js_package/package_json.rs` around lines 197 -
217, The BundleDependencies.deserialize currently treats non-array values
(notably the npm shorthand "bundleDependencies": true) as absent because
Vec<Box<str>>::deserialize returns None; add a concise TODO comment inside the
BundleDependencies::deserialize impl (near the let values: Vec<Box<str>> = ...
line) noting the known edge case where a boolean true means "bundle all
dependencies" and is silently ignored, and mention that a fuller fix would
require resolving the package's dependencies keys later; keep the existing
behavior but document it to avoid future confusion.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I am aware of this but handling this case is rather not needed because all used packages must be defined in dependencies field already so any undeclared package will be detected

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While handling the Boolean value isn't needed to the feature, Biome must understand the Boolean value too as per docs https://docs.npmjs.com/cli/v8/configuring-npm/package-json#bundledependencies

If we don't, Biome will start failing the moment it will try to deserialise Boolean values

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Biome must understand the Boolean value

sure, you are right, I will handle that in the PackageJson struct

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, to support 2 different value types I used similar approach as in Dependencies with DeserializationVisitor, I hope it makes sense


#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Version {
SemVer(Range),
Expand Down Expand Up @@ -286,6 +348,16 @@ impl DeserializationVisitor for PackageJsonVisitor {
result.optional_dependencies = deps;
}
}
"bundleDependencies" => {
if let Some(deps) = Deserializable::deserialize(ctx, &value, &key_text) {
result.bundle_dependencies = deps;
}
}
"bundledDependencies" => {
if let Some(deps) = Deserializable::deserialize(ctx, &value, &key_text) {
result.bundled_dependencies = deps;
}
}
"type" => {
result.r#type = Deserializable::deserialize(ctx, &value, &key_text);
}
Expand Down Expand Up @@ -392,4 +464,23 @@ mod tests {

assert_eq!(result, Ok(Version::Literal("~0.x.0".to_string())));
}

#[test]
fn parse_package_json_bundle_dependencies_field_with_bool() {
let deserialized = deserialize_from_json_str::<PackageJson>(
r#"{
"name": "@shared/format",
"bundleDependencies": true,
"bundledDependencies": false
}"#,
JsonParserOptions::default(),
"",
);
let (package_json, errors) = deserialized.consume();
assert!(errors.is_empty());

let package_json = package_json.expect("parsing must have succeeded");
assert!(package_json.bundle_dependencies.0.is_empty());
assert!(package_json.bundled_dependencies.0.is_empty());
}
}
4 changes: 4 additions & 0 deletions crates/biome_rule_options/src/no_undeclared_dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ pub struct NoUndeclaredDependenciesOptions {
/// If set to `false`, then the rule will show an error when `optionalDependencies` are imported. Defaults to `true`.
#[serde(skip_serializing_if = "Option::<_>::is_none")]
pub optional_dependencies: Option<DependencyAvailability>,

/// If set to `false`, then the rule will show an error when `bundleDependencies` are imported. Defaults to `true`.
#[serde(skip_serializing_if = "Option::<_>::is_none")]
pub bundle_dependencies: Option<DependencyAvailability>,
}

#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
Expand Down
4 changes: 4 additions & 0 deletions packages/@biomejs/backend-jsonrpc/src/workspace.ts

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

7 changes: 7 additions & 0 deletions packages/@biomejs/biome/configuration_schema.json

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