Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
27 changes: 27 additions & 0 deletions .changeset/based-bears-brawl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@biomejs/biome": minor
---

Biome's resolver now supports `baseUrl` if specified in `tsconfig.json`. This
means that the following now resolves:

**`tsconfig.json`**
```json
{
"compilerOptions": {
"baseUrl": "./src",
}
}
```

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.

Can you add a paragraph that explains the example below? That's the format that we try with the docs. E.g. "Given the following file structure ..., when importing a "foo" from index.ts, Biome will automatically pick up src/foo.ts"

**`index.ts`**
```ts
import { foo } from "foo"; // This will now work.
```

**`src/foo.ts`**
```ts
export function foo() {}
```

Fixes [#6432](https://github.com/biomejs/biome/issues/6432).
2 changes: 1 addition & 1 deletion crates/biome_package/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use biome_fs::FileSystem;
use camino::Utf8Path;
pub use license::generated::*;
pub use node_js_package::{
Dependencies, NodeJsPackage, PackageJson, PackageType, TsConfigJson, Version,
CompilerOptions, Dependencies, NodeJsPackage, PackageJson, PackageType, TsConfigJson, Version,
};

use std::any::TypeId;
Expand Down
2 changes: 1 addition & 1 deletion crates/biome_package/src/node_js_package/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod package_json;
mod tsconfig_json;

pub use package_json::{Dependencies, PackageJson, PackageType, Version};
pub use tsconfig_json::TsConfigJson;
pub use tsconfig_json::{CompilerOptions, TsConfigJson};

use biome_rowan::Language;

Expand Down
7 changes: 6 additions & 1 deletion crates/biome_package/src/node_js_package/tsconfig_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,19 @@ impl TsConfigJson {

#[derive(Clone, Debug, Default, Deserializable)]
pub struct CompilerOptions {
/// https://www.typescriptlang.org/tsconfig/#baseUrl
///
/// The base URL is normalised to an absolute path after parsing.
pub base_url: Option<Utf8PathBuf>,

Comment on lines +111 to 115

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.

💡 Verification agent

🧩 Analysis chain

Ensure JSON key maps to base_url (baseUrl) during deserialisation

Unless the derive macro auto-maps camelCase to snake_case, base_url won’t deserialize from baseUrl. Adding an explicit rename is cheap insurance.

Apply this diff:

     /// https://www.typescriptlang.org/tsconfig/#baseUrl
     ///
     /// The base URL is normalised to an absolute path after parsing.
-    pub base_url: Option<Utf8PathBuf>,
+    #[deserializable(rename = "baseUrl")]
+    pub base_url: Option<Utf8PathBuf>,

To confirm whether auto-mapping is already handled, run:


🏁 Script executed:

#!/bin/bash
# Inspect how camelCase keys are handled elsewhere and if baseUrl mapping exists
rg -n -C2 --type=rust 'Deserializable.*rename' 
rg -n -C3 --type=rust '\bcompiler_options\b' 
rg -n -C2 --type=rust '"baseUrl"|base_url'

Length of output: 9285


I want to double-check how TsConfigJson is derived and whether there’s a rename_all = "camelCase" in place.


🏁 Script executed:

#!/bin/bash
# Inspect TsConfigJson derive and serde attributes
rg -n '#\[derive' crates/biome_package/src/node_js_package/tsconfig_json.rs
rg -n 'rename_all' -C2 --type=rust crates/biome_package/src/node_js_package/tsconfig_json.rs
rg -n 'serde' -C2 --type=rust crates/biome_package/src/node_js_package/tsconfig_json.rs

Length of output: 458


Let’s peek at the top of tsconfig_json.rs to see which derive and import paths Deserializable comes from:


🏁 Script executed:

#!/bin/bash
sed -n '1,30p' crates/biome_package/src/node_js_package/tsconfig_json.rs

Length of output: 1081


Explicitly rename base_url to match JSON baseUrl

The TsConfigJson derive has no rename_all, so base_url will map to "base_url", not "baseUrl". Add an explicit rename:

     /// The base URL is normalised to an absolute path after parsing.
-    pub base_url: Option<Utf8PathBuf>,
+    #[deserializable(rename = "baseUrl")]
+    pub base_url: Option<Utf8PathBuf>,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// https://www.typescriptlang.org/tsconfig/#baseUrl
///
/// The base URL is normalised to an absolute path after parsing.
pub base_url: Option<Utf8PathBuf>,
/// https://www.typescriptlang.org/tsconfig/#baseUrl
///
/// The base URL is normalised to an absolute path after parsing.
#[deserializable(rename = "baseUrl")]
pub base_url: Option<Utf8PathBuf>,
🤖 Prompt for AI Agents
In crates/biome_package/src/node_js_package/tsconfig_json.rs around lines 89 to
93, the struct field `base_url` will serialize/deserialize as "base_url" because
the derive has no rename_all; add an explicit serde rename to match the
TypeScript tsconfig key by annotating the field with #[serde(rename =
"baseUrl")] so it maps to "baseUrl" for both serialization and deserialization.

/// Path aliases.
pub paths: Option<CompilerOptionsPathsMap>,

/// The actual base from where path aliases are resolved.
///
/// The base URL is normalised to an absolute path.
#[deserializable(skip)]
paths_base: Utf8PathBuf,
pub paths_base: Utf8PathBuf,

/// See: https://www.typescriptlang.org/tsconfig/#typeRoots
#[deserializable(rename = "typeRoots")]
Expand Down
38 changes: 28 additions & 10 deletions crates/biome_resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,15 @@ fn resolve_module_with_package_json(
) -> Result<Utf8PathBuf, ResolveError> {
// `tsconfig.json` may only be found in directories containing a
// `package.json`, so this is the only place we need to attempt to use it.
let tsconfig = fs.read_tsconfig_json(&package_path.join("tsconfig.json"));
if let Some(path) = tsconfig.as_ref().ok().and_then(|ts_config| {
resolve_paths_mapping(specifier, ts_config, package_path, fs, options).ok()
let tsconfig = match &options.tsconfig {
DiscoverableManifest::Auto => fs
.read_tsconfig_json(&package_path.join("tsconfig.json"))
.map(Cow::Owned),
DiscoverableManifest::Explicit { manifest, .. } => Ok(Cow::Borrowed(*manifest)),
DiscoverableManifest::Off => Err(ResolveError::NotFound),
};
Comment on lines -186 to +192

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This fixes a slightly unrelated oversight: If an explicit manifest was configured for tsconfig.json, it would be ignored in favour of the one on disk.

This came to light as I added tests.

if let Some(path) = tsconfig.as_ref().ok().and_then(|tsconfig| {
resolve_paths_mapping(specifier, tsconfig, package_path, fs, options).ok()
}) {
return Ok(path);
}
Expand Down Expand Up @@ -222,6 +228,17 @@ fn resolve_module_with_package_json(
);
}

if let Some(base_url) = tsconfig
.as_ref()
.ok()
.and_then(|tsconfig| tsconfig.compiler_options.base_url.as_ref())
{
match resolve_relative_path(specifier, base_url, fs, options) {
Err(ResolveError::NotFound) => { /* continue below */ }
result => return result,
}
}

resolve_dependency(specifier, package_path, fs, options)
}

Expand Down Expand Up @@ -337,12 +354,12 @@ fn resolve_paths_mapping(

let resolve_specifier = |specifier: &str| {
if is_relative_specifier(specifier) {
let base_dir = match &tsconfig_json.compiler_options.base_url {
Some(base_url) => base_url.as_path(),
None => package_path,
};

resolve_relative_path(specifier, base_dir, fs, options)
resolve_relative_path(
specifier,
&tsconfig_json.compiler_options.paths_base,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

paths_base was explicitly intended for this purpose, although it was unused until now. I noticed this as I extended the documentation comments.

fs,
options,
)
} else {
resolve_dependency(specifier, package_path, fs, options)
}
Expand Down Expand Up @@ -686,6 +703,7 @@ fn strip_query_and_fragment(specifier: &str) -> &str {
}

/// Options to pass to the resolver.
#[derive(Clone)]
pub struct ResolveOptions<'a> {
/// If `true`, specifiers are assumed to be relative paths. Resolving them
/// as a package will still be attempted if resolving as a relative path
Expand Down Expand Up @@ -898,7 +916,7 @@ impl<'a> ResolveOptions<'a> {
/// `tsconfig.json` will be automatically discovered, but this enum allows to
/// turn them off completely, or to provide an explicit manifest to be used
/// instead.
#[derive(Debug, Default)]
#[derive(Clone, Debug, Default)]
pub enum DiscoverableManifest<T> {
#[default]
Auto,
Expand Down

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "resolver_cases_7"
}
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"baseUrl": "./src"
}
}
81 changes: 81 additions & 0 deletions crates/biome_resolver/tests/spec_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use biome_fs::OsFileSystem;
use biome_package::{CompilerOptions, TsConfigJson};
use biome_resolver::*;
use camino::{Utf8Path, Utf8PathBuf};

Expand Down Expand Up @@ -533,3 +534,83 @@ fn test_resolve_type_definitions_without_type_specification() {
)))
);
}

#[test]
fn test_resolve_from_base_url() {
let base_dir = get_fixtures_path("resolver_cases_7");
let fs = OsFileSystem::new(base_dir.clone());

let options = ResolveOptions {
condition_names: &["default"],
extensions: &["js", "ts"],
..Default::default()
};

// Make sure resolution works with explicitly specified `tsconfig.json`.
assert_eq!(
resolve(
"bar",
&base_dir.join("src"),
&fs,
&options
.clone()
.with_tsconfig(DiscoverableManifest::Explicit {
package_path: Utf8PathBuf::from(format!("{base_dir}/tsconfig.json")),
manifest: &TsConfigJson {
compiler_options: CompilerOptions {
base_url: Some(base_dir.join("src")),
..Default::default()
},
..Default::default()
}
})
),
Ok(Utf8PathBuf::from(format!("{base_dir}/src/bar.ts")))
);

// Make sure resolution works with auto-discovered `tsconfig.json`.
assert_eq!(
resolve("bar", &base_dir.join("src"), &fs, &options),
Ok(Utf8PathBuf::from(format!("{base_dir}/src/bar.ts")))
);

// It shouldn't matter if we're resolving from a different base directory.
assert_eq!(
resolve("bar", &base_dir, &fs, &options),
Ok(Utf8PathBuf::from(format!("{base_dir}/src/bar.ts")))
);
assert_eq!(
resolve("bar", &base_dir.join("src/foo"), &fs, &options),
Ok(Utf8PathBuf::from(format!("{base_dir}/src/bar.ts")))
);

// Resolution should work in subfolders too.
assert_eq!(
resolve("foo/foo", &base_dir.join("src"), &fs, &options),
Ok(Utf8PathBuf::from(format!("{base_dir}/src/foo/foo.ts")))
);

// Make sure resolution falls back to `node_modules/` without `baseUrl`.
assert_eq!(
resolve(
"bar",
&base_dir.join("src"),
&fs,
&options
.clone()
.with_tsconfig(DiscoverableManifest::Explicit {
package_path: Utf8PathBuf::from(format!("{base_dir}/tsconfig.json")),
manifest: &TsConfigJson {
compiler_options: CompilerOptions {
base_url: None,
..Default::default()
},
..Default::default()
}
})
),
Ok(Utf8PathBuf::from(format!(
"{base_dir}/node_modules/bar/index.js"
)))
);
}
Loading