diff --git a/Cargo.lock b/Cargo.lock
index 3dc7556e0e67..85b03a20c24d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -132,6 +132,7 @@ dependencies = [
  "biome_parser",
  "biome_rowan",
  "biome_suppression",
+ "camino",
  "enumflags2",
  "indexmap",
  "rustc-hash 2.1.0",
@@ -799,6 +800,7 @@ dependencies = [
  "biome_js_syntax",
  "biome_package",
  "biome_plugin_loader",
+ "biome_project_layout",
  "biome_rowan",
  "biome_string_case",
  "biome_suppression",
@@ -1177,6 +1179,17 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "biome_project_layout"
+version = "0.0.1"
+dependencies = [
+ "biome_package",
+ "biome_parser",
+ "camino",
+ "papaya",
+ "rustc-hash 2.1.0",
+]
+
 [[package]]
 name = "biome_rowan"
 version = "0.5.7"
@@ -1234,6 +1247,7 @@ dependencies = [
  "biome_json_syntax",
  "biome_package",
  "biome_parser",
+ "biome_project_layout",
  "biome_rowan",
  "biome_string_case",
  "biome_text_edit",
@@ -1291,6 +1305,7 @@ dependencies = [
  "biome_formatter",
  "biome_json_parser",
  "biome_package",
+ "biome_project_layout",
  "biome_rowan",
  "biome_service",
  "camino",
diff --git a/Cargo.toml b/Cargo.toml
index 668a6c094bb1..3f5207897d45 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -148,6 +148,7 @@ biome_markdown_factory       = { version = "0.0.1", path = "./crates/biome_markd
 biome_markdown_parser        = { version = "0.0.1", path = "./crates/biome_markdown_parser" }
 biome_markdown_syntax        = { version = "0.0.1", path = "./crates/biome_markdown_syntax" }
 biome_plugin_loader          = { version = "0.0.1", path = "./crates/biome_plugin_loader" }
+biome_project_layout         = { version = "0.0.1", path = "./crates/biome_project_layout" }
 biome_ungrammar              = { version = "0.3.1", path = "./crates/biome_ungrammar" }
 biome_yaml_factory           = { version = "0.0.1", path = "./crates/biome_yaml_factory" }
 biome_yaml_parser            = { version = "0.0.1", path = "./crates/biome_yaml_parser" }
diff --git a/benchmark/package.json b/benchmark/package.json
index 022cbbdc95fb..e4f33c8f3543 100644
--- a/benchmark/package.json
+++ b/benchmark/package.json
@@ -12,6 +12,7 @@
   },
   "devDependencies": {
     "@typescript-eslint/eslint-plugin": "8.18.1",
+    "@typescript-eslint/parser": "8.3.0",
     "dprint": "0.48.0",
     "eslint": "9.17.0",
     "prettier": "3.3.3"
diff --git a/biome.json b/biome.json
index aac6d02b41d9..ded692c24334 100644
--- a/biome.json
+++ b/biome.json
@@ -52,6 +52,9 @@
     "enabled": true,
     "rules": {
       "recommended": true,
+      "correctness": {
+        "noUndeclaredDependencies": "error"
+      },
       "style": {
         "noNonNullAssertion": "off",
         "useNodejsImportProtocol": "error"
diff --git a/crates/biome_analyze/Cargo.toml b/crates/biome_analyze/Cargo.toml
index 98cf8e0c0905..df83a2e465ec 100644
--- a/crates/biome_analyze/Cargo.toml
+++ b/crates/biome_analyze/Cargo.toml
@@ -20,6 +20,7 @@ biome_diagnostics        = { workspace = true }
 biome_parser             = { workspace = true }
 biome_rowan              = { workspace = true }
 biome_suppression        = { workspace = true }
+camino                   = { workspace = true }
 enumflags2               = { workspace = true }
 indexmap                 = { workspace = true }
 rustc-hash               = { workspace = true }
@@ -27,7 +28,6 @@ schemars                 = { workspace = true, optional = true }
 serde                    = { workspace = true, features = ["derive"], optional = true }
 tracing                  = { workspace = true }
 
-
 [features]
 schema = ["dep:schemars", "biome_console/schema", "serde"]
 serde  = ["dep:serde", "dep:biome_deserialize", "dep:biome_deserialize_macros"]
diff --git a/crates/biome_analyze/src/analyzer_plugin.rs b/crates/biome_analyze/src/analyzer_plugin.rs
index fb7536c84e1f..e7b59220ea27 100644
--- a/crates/biome_analyze/src/analyzer_plugin.rs
+++ b/crates/biome_analyze/src/analyzer_plugin.rs
@@ -1,10 +1,11 @@
 use crate::RuleDiagnostic;
 use biome_parser::AnyParse;
-use std::{fmt::Debug, path::PathBuf};
+use camino::Utf8PathBuf;
+use std::fmt::Debug;
 
 /// Definition of an analyzer plugin.
 pub trait AnalyzerPlugin: Debug {
-    fn evaluate(&self, root: AnyParse, path: PathBuf) -> Vec<RuleDiagnostic>;
+    fn evaluate(&self, root: AnyParse, path: Utf8PathBuf) -> Vec<RuleDiagnostic>;
 
     fn supports_css(&self) -> bool;
 
diff --git a/crates/biome_analyze/src/context.rs b/crates/biome_analyze/src/context.rs
index c2fbf3701d12..e3650eea31f3 100644
--- a/crates/biome_analyze/src/context.rs
+++ b/crates/biome_analyze/src/context.rs
@@ -2,8 +2,8 @@ use crate::options::{JsxRuntime, PreferredQuote};
 use crate::{registry::RuleRoot, FromServices, Queryable, Rule, RuleKey, ServiceBag};
 use crate::{GroupCategory, RuleCategory, RuleGroup, RuleMetadata};
 use biome_diagnostics::{Error, Result};
+use camino::Utf8Path;
 use std::ops::Deref;
-use std::path::Path;
 
 type RuleQueryResult<R> = <<R as Rule>::Query as Queryable>::Output;
 type RuleServiceBag<R> = <<R as Rule>::Query as Queryable>::Services;
@@ -14,7 +14,7 @@ pub struct RuleContext<'a, R: Rule> {
     bag: &'a ServiceBag,
     services: RuleServiceBag<R>,
     globals: &'a [&'a str],
-    file_path: &'a Path,
+    file_path: &'a Utf8Path,
     options: &'a R::Options,
     preferred_quote: &'a PreferredQuote,
     jsx_runtime: Option<JsxRuntime>,
@@ -30,7 +30,7 @@ where
         root: &'a RuleRoot<R>,
         services: &'a ServiceBag,
         globals: &'a [&'a str],
-        file_path: &'a Path,
+        file_path: &'a Utf8Path,
         options: &'a R::Options,
         preferred_quote: &'a PreferredQuote,
         jsx_runtime: Option<JsxRuntime>,
@@ -159,7 +159,7 @@ where
     }
 
     /// The file path of the current file
-    pub fn file_path(&self) -> &Path {
+    pub fn file_path(&self) -> &Utf8Path {
         self.file_path
     }
 
diff --git a/crates/biome_analyze/src/options.rs b/crates/biome_analyze/src/options.rs
index 5fe34c9cf188..b1639cb387dc 100644
--- a/crates/biome_analyze/src/options.rs
+++ b/crates/biome_analyze/src/options.rs
@@ -1,9 +1,9 @@
+use camino::Utf8PathBuf;
 use rustc_hash::FxHashMap;
 
 use crate::{FixKind, Rule, RuleKey};
 use std::any::{Any, TypeId};
 use std::fmt::Debug;
-use std::path::PathBuf;
 
 /// A convenient new type data structure to store the options that belong to a rule
 #[derive(Debug)]
@@ -98,14 +98,14 @@ pub struct AnalyzerOptions {
     pub(crate) configuration: AnalyzerConfiguration,
 
     /// The file that is being analyzed
-    pub(crate) file_path: PathBuf,
+    pub file_path: Utf8PathBuf,
 
     /// Suppression reason used when applying a suppression code action
     pub(crate) suppression_reason: Option<String>,
 }
 
 impl AnalyzerOptions {
-    pub fn with_file_path(mut self, file_path: impl Into<PathBuf>) -> Self {
+    pub fn with_file_path(mut self, file_path: impl Into<Utf8PathBuf>) -> Self {
         self.file_path = file_path.into();
         self
     }
diff --git a/crates/biome_cli/tests/commands/lint.rs b/crates/biome_cli/tests/commands/lint.rs
index 10d2d2fe1183..aa5d349a0e1b 100644
--- a/crates/biome_cli/tests/commands/lint.rs
+++ b/crates/biome_cli/tests/commands/lint.rs
@@ -5,7 +5,9 @@ use crate::configs::{
     CONFIG_LINTER_SUPPRESSED_GROUP, CONFIG_LINTER_SUPPRESSED_RULE,
     CONFIG_LINTER_UPGRADE_DIAGNOSTIC, CONFIG_RECOMMENDED_GROUP,
 };
-use crate::snap_test::{assert_file_contents, markup_to_string, SnapshotPayload};
+use crate::snap_test::{
+    assert_cli_snapshot_with_redactor, assert_file_contents, markup_to_string, SnapshotPayload,
+};
 use crate::{
     assert_cli_snapshot, run_cli, run_cli_with_dyn_fs, run_cli_with_server_workspace, FORMATTED,
     LINT_ERROR, PARSE_ERROR,
@@ -3879,13 +3881,16 @@ fn linter_finds_package_json_for_no_undeclared_dependencies() {
         &mut console,
         Args::from(["lint", file.as_str()].as_slice()),
     );
-    assert_cli_snapshot(SnapshotPayload::new(
-        module_path!(),
-        "linter_finds_package_json_for_no_undeclared_dependencies",
-        fs,
-        console,
-        result,
-    ));
+    assert_cli_snapshot_with_redactor(
+        SnapshotPayload::new(
+            module_path!(),
+            "linter_finds_package_json_for_no_undeclared_dependencies",
+            fs,
+            console,
+            result,
+        ),
+        |content| content.replace("frontend\\", "frontend/"),
+    );
 }
 
 #[test]
@@ -3934,13 +3939,16 @@ fn linter_finds_nested_package_json_for_no_undeclared_dependencies() {
         &mut console,
         Args::from(["lint", file.as_str()].as_slice()),
     );
-    assert_cli_snapshot(SnapshotPayload::new(
-        module_path!(),
-        "linter_finds_nested_package_json_for_no_undeclared_dependencies",
-        fs,
-        console,
-        result,
-    ));
+    assert_cli_snapshot_with_redactor(
+        SnapshotPayload::new(
+            module_path!(),
+            "linter_finds_nested_package_json_for_no_undeclared_dependencies",
+            fs,
+            console,
+            result,
+        ),
+        |content| content.replace("frontend\\", "frontend/"),
+    );
 }
 
 #[test]
diff --git a/crates/biome_cli/tests/snap_test.rs b/crates/biome_cli/tests/snap_test.rs
index 014428ed699f..b7d572e81dc7 100644
--- a/crates/biome_cli/tests/snap_test.rs
+++ b/crates/biome_cli/tests/snap_test.rs
@@ -12,6 +12,7 @@ use camino::{Utf8Path, Utf8PathBuf};
 use regex::Regex;
 use std::borrow::Cow;
 use std::collections::BTreeMap;
+use std::convert::identity;
 use std::env::{current_exe, temp_dir};
 use std::fmt::Write as _;
 use std::path::MAIN_SEPARATOR;
@@ -405,6 +406,17 @@ impl<'a> SnapshotPayload<'a> {
 
 /// Function used to snapshot a session test of the a CLI run.
 pub fn assert_cli_snapshot(payload: SnapshotPayload<'_>) {
+    assert_cli_snapshot_with_redactor(payload, identity)
+}
+
+/// Used to snapshot a session test of the a CLI run.
+///
+/// Takes a custom `redactor` that allows the snapshotted content to be
+/// normalized so it remains stable across test runs.
+pub fn assert_cli_snapshot_with_redactor(
+    payload: SnapshotPayload<'_>,
+    redactor: impl FnOnce(String) -> String,
+) {
     let module_path = payload.module_path.to_owned();
     let test_name = payload.test_name;
     let cli_snapshot = CliSnapshot::from(payload);
@@ -416,9 +428,9 @@ pub fn assert_cli_snapshot(payload: SnapshotPayload<'_>) {
 
     insta::with_settings!({
         prepend_module_to_snapshot => false,
-        snapshot_path => snapshot_path
+        snapshot_path => snapshot_path,
     }, {
-        insta::assert_snapshot!(test_name, content);
+        insta::assert_snapshot!(test_name, redactor(content));
 
     });
 }
diff --git a/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_nested_package_json_for_no_undeclared_dependencies.snap b/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_nested_package_json_for_no_undeclared_dependencies.snap
index 7ef880891a32..67dc83f282ff 100644
--- a/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_nested_package_json_for_no_undeclared_dependencies.snap
+++ b/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_nested_package_json_for_no_undeclared_dependencies.snap
@@ -47,7 +47,7 @@ import 'react-dom'
 ```block
 frontend/file1.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  i The current dependency isn't specified in your package.json.
+  i Dependency react-dom isn't specified in frontend/package.json.
   
   > 1 │ import 'react-dom'
       │        ^^^^^^^^^^^
diff --git a/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_package_json_for_no_undeclared_dependencies.snap b/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_package_json_for_no_undeclared_dependencies.snap
index 06520b6470ff..adae399919f1 100644
--- a/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_package_json_for_no_undeclared_dependencies.snap
+++ b/crates/biome_cli/tests/snapshots/main_commands_lint/linter_finds_package_json_for_no_undeclared_dependencies.snap
@@ -37,7 +37,7 @@ import 'react-dom'
 ```block
 frontend/file1.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  i The current dependency isn't specified in your package.json.
+  i Dependency react-dom isn't specified in frontend/package.json.
   
   > 1 │ import 'react-dom'
       │        ^^^^^^^^^^^
diff --git a/crates/biome_css_analyze/Cargo.toml b/crates/biome_css_analyze/Cargo.toml
index 6e3cdd8b6d32..1f3046c84b7d 100644
--- a/crates/biome_css_analyze/Cargo.toml
+++ b/crates/biome_css_analyze/Cargo.toml
@@ -23,6 +23,7 @@ biome_diagnostics        = { workspace = true }
 biome_rowan              = { workspace = true }
 biome_string_case        = { workspace = true }
 biome_suppression        = { workspace = true }
+camino                   = { workspace = true }
 regex                    = { workspace = true }
 rustc-hash               = { workspace = true }
 schemars                 = { workspace = true, optional = true }
diff --git a/crates/biome_grit_patterns/src/grit_context.rs b/crates/biome_grit_patterns/src/grit_context.rs
index e05c4c08c0a3..3ac76f3c3d35 100644
--- a/crates/biome_grit_patterns/src/grit_context.rs
+++ b/crates/biome_grit_patterns/src/grit_context.rs
@@ -314,7 +314,7 @@ fn new_file_owner(
 /// that can use the Biome workspace.
 #[derive(Clone, Debug)]
 pub struct GritTargetFile {
-    pub path: PathBuf,
+    pub path: Utf8PathBuf,
     pub parse: AnyParse,
 }
 
@@ -323,9 +323,6 @@ impl GritTargetFile {
         let parser = target_language.get_parser();
         let parse = parser.parse_with_path(source, &path);
 
-        Self {
-            parse,
-            path: path.into(),
-        }
+        Self { parse, path }
     }
 }
diff --git a/crates/biome_grit_patterns/src/grit_query.rs b/crates/biome_grit_patterns/src/grit_query.rs
index 29569187878c..73bd5f85e707 100644
--- a/crates/biome_grit_patterns/src/grit_query.rs
+++ b/crates/biome_grit_patterns/src/grit_query.rs
@@ -82,8 +82,12 @@ impl GritQuery {
 
         let var_registry = VarRegistry::from_locations(&self.variable_locations);
 
-        let file_registry =
-            FileRegistry::new_from_paths(files.iter().map(|file| &file.path).collect());
+        // FIXME: Can be simplified when https://github.com/getgrit/gritql/pull/594/files is released.
+        let paths: Vec<PathBuf> = files
+            .iter()
+            .map(|file| file.path.clone().into_std_path_buf())
+            .collect();
+        let file_registry = FileRegistry::new_from_paths(paths.iter().collect());
         let binding = FilePattern::Single(file_ptr);
 
         let mut state = State::new(var_registry.into(), file_registry);
diff --git a/crates/biome_js_analyze/Cargo.toml b/crates/biome_js_analyze/Cargo.toml
index 29b3db59022b..037f094b6f44 100644
--- a/crates/biome_js_analyze/Cargo.toml
+++ b/crates/biome_js_analyze/Cargo.toml
@@ -24,6 +24,7 @@ biome_js_factory         = { workspace = true }
 biome_js_semantic        = { workspace = true }
 biome_js_syntax          = { workspace = true }
 biome_package            = { workspace = true }
+biome_project_layout     = { workspace = true }
 biome_rowan              = { workspace = true }
 biome_string_case        = { workspace = true }
 biome_suppression        = { workspace = true }
diff --git a/crates/biome_js_analyze/src/lib.rs b/crates/biome_js_analyze/src/lib.rs
index c2f6446cdb44..b342e9c0fd75 100644
--- a/crates/biome_js_analyze/src/lib.rs
+++ b/crates/biome_js_analyze/src/lib.rs
@@ -9,7 +9,7 @@ use biome_analyze::{
 use biome_aria::AriaRoles;
 use biome_diagnostics::Error as DiagnosticError;
 use biome_js_syntax::{JsFileSource, JsLanguage};
-use biome_package::PackageJson;
+use biome_project_layout::ProjectLayout;
 use biome_rowan::TextRange;
 use biome_suppression::{parse_suppression_comment, SuppressionDiagnostic};
 use std::ops::Deref;
@@ -53,7 +53,7 @@ pub fn analyze_with_inspect_matcher<'a, V, F, B>(
     options: &'a AnalyzerOptions,
     plugins: Vec<Box<dyn AnalyzerPlugin>>,
     source_type: JsFileSource,
-    manifest: Option<PackageJson>,
+    project_layout: Arc<ProjectLayout>,
     mut emit_signal: F,
 ) -> (Option<B>, Vec<DiagnosticError>)
 where
@@ -116,8 +116,11 @@ where
     }
 
     services.insert_service(Arc::new(AriaRoles));
-    services.insert_service(Arc::new(manifest));
     services.insert_service(source_type);
+
+    services.insert_service(project_layout.get_node_manifest_for_path(&options.file_path));
+    services.insert_service(project_layout);
+
     (
         analyzer.run(AnalyzerContext {
             root: root.clone(),
@@ -138,7 +141,7 @@ pub fn analyze<'a, F, B>(
     options: &'a AnalyzerOptions,
     plugins: Vec<Box<dyn AnalyzerPlugin>>,
     source_type: JsFileSource,
-    manifest: Option<&'a PackageJson>,
+    project_layout: Arc<ProjectLayout>,
     emit_signal: F,
 ) -> (Option<B>, Vec<DiagnosticError>)
 where
@@ -152,7 +155,7 @@ where
         options,
         plugins,
         source_type,
-        manifest.cloned(),
+        project_layout,
         emit_signal,
     )
 }
@@ -167,7 +170,7 @@ mod tests {
     use biome_package::{Dependencies, PackageJson};
     use std::slice;
 
-    use crate::{analyze, AnalysisFilter, ControlFlow};
+    use super::*;
 
     // #[ignore]
     #[test]
@@ -191,6 +194,7 @@ let bar = 33;
 
         let mut dependencies = Dependencies::default();
         dependencies.add("buffer", "latest");
+
         analyze(
             &parsed.tree(),
             AnalysisFilter {
@@ -200,10 +204,7 @@ let bar = 33;
             &options,
             Vec::new(),
             JsFileSource::tsx(),
-            Some(&PackageJson {
-                dependencies,
-                ..Default::default()
-            }),
+            project_layout_with_top_level_dependencies(dependencies),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     error_ranges.push(diag.location().span.unwrap());
@@ -252,7 +253,7 @@ let bar = 33;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     let error = diag
@@ -338,7 +339,7 @@ let bar = 33;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     let span = diag.get_span();
@@ -410,7 +411,7 @@ let bar = 33;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     let code = diag.category().unwrap();
@@ -455,7 +456,7 @@ let bar = 33;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     let error = diag
@@ -507,7 +508,7 @@ debugger;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     let error = diag
@@ -554,7 +555,7 @@ debugger;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     let error = diag
@@ -603,7 +604,7 @@ debugger;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     let error = diag
@@ -653,7 +654,7 @@ let bar = 33;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     let code = diag.category().unwrap();
@@ -701,7 +702,7 @@ let bar = 33;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     let error = diag
@@ -752,7 +753,7 @@ let c;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     let code = diag.category().unwrap();
@@ -804,7 +805,7 @@ debugger;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     has_diagnostics = true;
@@ -857,7 +858,7 @@ let d;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     let error = diag
@@ -905,7 +906,7 @@ a == b;
             &options,
             Vec::new(),
             JsFileSource::js_module(),
-            None,
+            Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     has_diagnostics = true;
@@ -920,4 +921,18 @@ a == b;
         );
         assert!(has_diagnostics, "must have diagnostics");
     }
+
+    fn project_layout_with_top_level_dependencies(
+        dependencies: Dependencies,
+    ) -> Arc<ProjectLayout> {
+        let manifest = PackageJson {
+            dependencies,
+            ..Default::default()
+        };
+
+        let project_layout = ProjectLayout::default();
+        project_layout.insert_node_manifest("/".into(), manifest);
+
+        Arc::new(project_layout)
+    }
 }
diff --git a/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs b/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs
index 04aa58ebb8c7..cc159de1d870 100644
--- a/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs
+++ b/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs
@@ -1,5 +1,3 @@
-use std::path::Path;
-
 use biome_analyze::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic, RuleSource};
 use biome_console::markup;
 use biome_deserialize::{
@@ -8,6 +6,7 @@ use biome_deserialize::{
 use biome_deserialize_macros::Deserializable;
 use biome_js_syntax::{AnyJsImportClause, AnyJsImportLike};
 use biome_rowan::AstNode;
+use camino::Utf8Path;
 
 use crate::{globals::is_node_builtin_module, services::manifest::Manifest};
 
@@ -164,7 +163,7 @@ impl schemars::JsonSchema for DependencyAvailability {
 }
 
 impl DependencyAvailability {
-    fn is_available(&self, path: &Path) -> bool {
+    fn is_available(&self, path: &Utf8Path) -> bool {
         match self {
             Self::Bool(b) => *b,
             Self::Patterns(globs) => {
@@ -270,11 +269,24 @@ impl Rule for NoUndeclaredDependencies {
             is_optional_dependency_available,
         } = state;
 
+        let Some(package_path) = ctx.package_path.as_ref() else {
+            return Some(RuleDiagnostic::new(
+                rule_category!(),
+                ctx.query().range(),
+                markup! {
+                    "Dependency "<Emphasis>{package_name}</Emphasis>" cannot be verified because no package.json file was found."
+                },
+            ));
+        };
+
+        let mut manifest_path = package_path.clone();
+        manifest_path.push("package.json");
+
         let diag = RuleDiagnostic::new(
             rule_category!(),
             ctx.query().range(),
             markup! {
-                "The current dependency isn't specified in your package.json."
+                "Dependency "<Emphasis>{package_name}</Emphasis>" isn't specified in "<Emphasis>{manifest_path.as_str()}</Emphasis>"."
             },
         );
 
diff --git a/crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs b/crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs
index a95205e70474..a2bb73639694 100644
--- a/crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs
+++ b/crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs
@@ -149,7 +149,7 @@ impl Rule for UseImportExtensions {
     fn run(ctx: &RuleContext<Self>) -> Self::Signals {
         let node = ctx.query();
 
-        let file_ext = ctx.file_path().extension().and_then(|ext| ext.to_str())?;
+        let file_ext = ctx.file_path().extension()?;
 
         let custom_suggested_imports = &ctx.options().suggested_extensions;
 
diff --git a/crates/biome_js_analyze/src/lint/nursery/no_common_js.rs b/crates/biome_js_analyze/src/lint/nursery/no_common_js.rs
index a6a2ee742598..bf1c3feb65fe 100644
--- a/crates/biome_js_analyze/src/lint/nursery/no_common_js.rs
+++ b/crates/biome_js_analyze/src/lint/nursery/no_common_js.rs
@@ -70,7 +70,7 @@ impl Rule for NoCommonJs {
     fn run(ctx: &RuleContext<Self>) -> Self::Signals {
         let file_ext = ctx.file_path().extension();
         // cjs and cts files can only use CommonJs modules
-        if file_ext.is_some_and(|file_ext| matches!(file_ext.as_encoded_bytes(), b"cjs" | b"cts")) {
+        if file_ext.is_some_and(|file_ext| matches!(file_ext.as_bytes(), b"cjs" | b"cts")) {
             return None;
         }
 
diff --git a/crates/biome_js_analyze/src/lint/nursery/no_document_import_in_page.rs b/crates/biome_js_analyze/src/lint/nursery/no_document_import_in_page.rs
index 681af0811362..8ed42489833a 100644
--- a/crates/biome_js_analyze/src/lint/nursery/no_document_import_in_page.rs
+++ b/crates/biome_js_analyze/src/lint/nursery/no_document_import_in_page.rs
@@ -70,8 +70,8 @@ impl Rule for NoDocumentImportInPage {
             return None;
         }
 
-        let file_name = path.file_stem()?.to_str()?;
-        let parent_name = path.parent()?.file_stem()?.to_str()?;
+        let file_name = path.file_stem()?;
+        let parent_name = path.parent()?.file_stem()?;
 
         if parent_name == "_document" || file_name == "_document" {
             return None;
diff --git a/crates/biome_js_analyze/src/lint/nursery/no_head_import_in_document.rs b/crates/biome_js_analyze/src/lint/nursery/no_head_import_in_document.rs
index 2747dbe23c27..dd6a5392dbd2 100644
--- a/crates/biome_js_analyze/src/lint/nursery/no_head_import_in_document.rs
+++ b/crates/biome_js_analyze/src/lint/nursery/no_head_import_in_document.rs
@@ -83,14 +83,14 @@ impl Rule for NoHeadImportInDocument {
             return None;
         }
 
-        let file_name = path.file_stem()?.to_str()?;
+        let file_name = path.file_stem()?;
 
         // pages/_document.(jsx|tsx)
         if file_name == "_document" {
             return Some(());
         }
 
-        let parent_name = path.parent()?.file_stem()?.to_str()?;
+        let parent_name = path.parent()?.file_stem()?;
 
         // pages/_document/index.(jsx|tsx)
         if parent_name == "_document" && file_name == "index" {
@@ -101,7 +101,7 @@ impl Rule for NoHeadImportInDocument {
     }
 
     fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
-        let path = ctx.file_path().to_str()?.split("pages").nth(1)?;
+        let path = ctx.file_path().as_str().split("pages").nth(1)?;
         let path = if cfg!(debug_assertions) {
             path.replace(MAIN_SEPARATOR, "/")
         } else {
diff --git a/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs b/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs
index a4a7d5de5517..fdc00c4b9385 100644
--- a/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs
+++ b/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs
@@ -141,7 +141,7 @@ impl Rule for UseComponentExportOnlyModules {
     type Options = UseComponentExportOnlyModulesOptions;
 
     fn run(ctx: &RuleContext<Self>) -> Self::Signals {
-        if let Some(file_name) = ctx.file_path().file_name().and_then(|x| x.to_str()) {
+        if let Some(file_name) = ctx.file_path().file_name() {
             if !JSX_FILE_EXT.iter().any(|ext| file_name.ends_with(ext)) {
                 return Vec::new().into_boxed_slice();
             }
diff --git a/crates/biome_js_analyze/src/lint/style/use_filenaming_convention.rs b/crates/biome_js_analyze/src/lint/style/use_filenaming_convention.rs
index 4d2246c7c3bb..6a7818503cad 100644
--- a/crates/biome_js_analyze/src/lint/style/use_filenaming_convention.rs
+++ b/crates/biome_js_analyze/src/lint/style/use_filenaming_convention.rs
@@ -156,7 +156,7 @@ impl Rule for UseFilenamingConvention {
     type Options = Box<FilenamingConventionOptions>;
 
     fn run(ctx: &RuleContext<Self>) -> Self::Signals {
-        let file_name = ctx.file_path().file_name()?.to_str()?;
+        let file_name = ctx.file_path().file_name()?;
         let options = ctx.options();
         if options.require_ascii && !file_name.is_ascii() {
             return Some(FileNamingConventionState::Ascii);
@@ -273,7 +273,7 @@ impl Rule for UseFilenamingConvention {
     }
 
     fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
-        let file_name = ctx.file_path().file_name()?.to_str()?;
+        let file_name = ctx.file_path().file_name()?;
         let options = ctx.options();
         match state {
             FileNamingConventionState::Ascii => {
diff --git a/crates/biome_js_analyze/src/services/manifest.rs b/crates/biome_js_analyze/src/services/manifest.rs
index ba39c5a78ca5..1c383ae5d688 100644
--- a/crates/biome_js_analyze/src/services/manifest.rs
+++ b/crates/biome_js_analyze/src/services/manifest.rs
@@ -5,45 +5,39 @@ use biome_analyze::{
 use biome_js_syntax::{AnyJsRoot, JsLanguage, JsSyntaxNode};
 use biome_package::PackageJson;
 use biome_rowan::AstNode;
-use std::sync::Arc;
+use camino::Utf8PathBuf;
 
 #[derive(Debug, Clone)]
 pub struct ManifestServices {
-    pub(crate) manifest: Arc<Option<PackageJson>>,
+    pub(crate) package_path: Option<Utf8PathBuf>,
+    pub(crate) manifest: Option<PackageJson>,
 }
 
 impl ManifestServices {
     pub(crate) fn name(&self) -> Option<&str> {
-        self.manifest
-            .as_ref()
-            .as_ref()
-            .and_then(|pkg| pkg.name.as_deref())
+        self.manifest.as_ref().and_then(|pkg| pkg.name.as_deref())
     }
 
     pub(crate) fn is_dependency(&self, specifier: &str) -> bool {
         self.manifest
-            .as_ref()
             .as_ref()
             .is_some_and(|pkg| pkg.dependencies.contains(specifier))
     }
 
     pub(crate) fn is_dev_dependency(&self, specifier: &str) -> bool {
         self.manifest
-            .as_ref()
             .as_ref()
             .is_some_and(|pkg| pkg.dev_dependencies.contains(specifier))
     }
 
     pub(crate) fn is_peer_dependency(&self, specifier: &str) -> bool {
         self.manifest
-            .as_ref()
             .as_ref()
             .is_some_and(|pkg| pkg.peer_dependencies.contains(specifier))
     }
 
     pub(crate) fn is_optional_dependency(&self, specifier: &str) -> bool {
         self.manifest
-            .as_ref()
             .as_ref()
             .is_some_and(|pkg| pkg.optional_dependencies.contains(specifier))
     }
@@ -54,12 +48,19 @@ impl FromServices for ManifestServices {
         rule_key: &RuleKey,
         services: &ServiceBag,
     ) -> biome_diagnostics::Result<Self, MissingServicesDiagnostic> {
-        let manifest: &Arc<Option<PackageJson>> = services.get_service().ok_or_else(|| {
-            MissingServicesDiagnostic::new(rule_key.rule_name(), &["PackageJson"])
-        })?;
+        let manifest_info: &Option<(Utf8PathBuf, PackageJson)> =
+            services.get_service().ok_or_else(|| {
+                MissingServicesDiagnostic::new(rule_key.rule_name(), &["PackageJson"])
+            })?;
+
+        let (package_path, manifest) = match manifest_info {
+            Some((package_path, manifest)) => (Some(package_path.clone()), Some(manifest.clone())),
+            None => (None, None),
+        };
 
         Ok(Self {
-            manifest: manifest.clone(),
+            package_path,
+            manifest,
         })
     }
 }
@@ -70,7 +71,7 @@ impl Phase for ManifestServices {
     }
 }
 
-/// Query type usable by lint rules **that uses the semantic model** to match on specific [AstNode] types
+/// Query type usable by lint rules **that uses the package manifest** and matches on specific [AstNode] types.
 #[derive(Clone)]
 pub struct Manifest<N>(pub N);
 
diff --git a/crates/biome_js_analyze/src/syntax/correctness/no_type_only_import_attributes.rs b/crates/biome_js_analyze/src/syntax/correctness/no_type_only_import_attributes.rs
index 84a58e732c90..17ce8db23613 100644
--- a/crates/biome_js_analyze/src/syntax/correctness/no_type_only_import_attributes.rs
+++ b/crates/biome_js_analyze/src/syntax/correctness/no_type_only_import_attributes.rs
@@ -40,7 +40,7 @@ impl Rule for NoTypeOnlyImportAttributes {
 
     fn run(ctx: &RuleContext<Self>) -> Self::Signals {
         let extension = ctx.file_path().extension()?;
-        if extension.as_encoded_bytes() == b"cts" {
+        if extension.as_bytes() == b"cts" {
             // Ignore `*.cts`
             return None;
         }
diff --git a/crates/biome_js_analyze/tests/quick_test.rs b/crates/biome_js_analyze/tests/quick_test.rs
index f1f1aac23f4a..e01eb91e23b1 100644
--- a/crates/biome_js_analyze/tests/quick_test.rs
+++ b/crates/biome_js_analyze/tests/quick_test.rs
@@ -4,8 +4,8 @@ use biome_diagnostics::{DiagnosticExt, Severity};
 use biome_js_parser::{parse, JsParserOptions};
 use biome_js_syntax::JsFileSource;
 use biome_test_utils::{
-    code_fix_to_string, create_analyzer_options, diagnostic_to_string, load_manifest,
-    parse_test_path, scripts_from_json,
+    code_fix_to_string, create_analyzer_options, diagnostic_to_string, parse_test_path,
+    project_layout_with_node_manifest, scripts_from_json,
 };
 use camino::Utf8Path;
 use std::ops::Deref;
@@ -71,7 +71,7 @@ fn analyze(
     let mut diagnostics = Vec::new();
     let mut code_fixes = Vec::new();
     let options = create_analyzer_options(input_file, &mut diagnostics);
-    let manifest = load_manifest(input_file, &mut diagnostics);
+    let project_layout = project_layout_with_node_manifest(input_file, &mut diagnostics);
 
     let (_, errors) = biome_js_analyze::analyze(
         &root,
@@ -79,7 +79,7 @@ fn analyze(
         &options,
         Vec::new(),
         source_type,
-        manifest.as_ref(),
+        project_layout,
         |event| {
             if let Some(mut diag) = event.diagnostic() {
                 for action in event.actions() {
diff --git a/crates/biome_js_analyze/tests/spec_tests.rs b/crates/biome_js_analyze/tests/spec_tests.rs
index f62324e256d5..5854b5585d9c 100644
--- a/crates/biome_js_analyze/tests/spec_tests.rs
+++ b/crates/biome_js_analyze/tests/spec_tests.rs
@@ -11,8 +11,8 @@ use biome_plugin_loader::AnalyzerGritPlugin;
 use biome_rowan::AstNode;
 use biome_test_utils::{
     assert_errors_are_absent, code_fix_to_string, create_analyzer_options, diagnostic_to_string,
-    has_bogus_nodes_or_empty_slots, load_manifest, parse_test_path, register_leak_checker,
-    scripts_from_json, write_analyzer_snapshot, CheckActionType,
+    has_bogus_nodes_or_empty_slots, parse_test_path, project_layout_with_node_manifest,
+    register_leak_checker, scripts_from_json, write_analyzer_snapshot, CheckActionType,
 };
 use camino::Utf8Path;
 use std::ops::Deref;
@@ -113,9 +113,9 @@ pub(crate) fn analyze_and_snap(
 ) -> usize {
     let mut diagnostics = Vec::new();
     let mut code_fixes = Vec::new();
-    let manifest = load_manifest(input_file, &mut diagnostics);
+    let project_layout = project_layout_with_node_manifest(input_file, &mut diagnostics);
 
-    if let Some(manifest) = &manifest {
+    if let Some((_, manifest)) = project_layout.get_node_manifest_for_path(input_file) {
         if manifest.r#type == Some(PackageType::Commonjs) &&
             // At the moment we treat JS and JSX at the same way
             (source_type.file_extension() == "js" || source_type.file_extension() == "jsx" )
@@ -123,6 +123,7 @@ pub(crate) fn analyze_and_snap(
             source_type.set_module_kind(ModuleKind::Script)
         }
     }
+
     let parsed = parse(input_code, source_type, parser_options.clone());
     let root = parsed.tree();
 
@@ -134,7 +135,7 @@ pub(crate) fn analyze_and_snap(
         &options,
         plugins,
         source_type,
-        manifest.as_ref(),
+        project_layout,
         |event| {
             if let Some(mut diag) = event.diagnostic() {
                 for action in event.actions() {
diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js.snap b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js.snap
index f1ebda526d86..07baece7026f 100644
--- a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js.snap
+++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.js.snap
@@ -1,7 +1,6 @@
 ---
 source: crates/biome_js_analyze/tests/spec_tests.rs
 expression: invalid.js
-snapshot_kind: text
 ---
 # Input
 ```js
@@ -19,7 +18,7 @@ require("@testing-library/react");
 ```
 invalid.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  ! The current dependency isn't specified in your package.json.
+  ! Dependency notInstalled isn't specified in package.json.
   
   > 1 │ import "notInstalled";
       │        ^^^^^^^^^^^^^^
@@ -36,7 +35,7 @@ invalid.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━━
 ```
 invalid.js:2:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  ! The current dependency isn't specified in your package.json.
+  ! Dependency notInstalled isn't specified in package.json.
   
     1 │ import "notInstalled";
   > 2 │ import("notInstalled");
@@ -54,7 +53,7 @@ invalid.js:2:1 lint/correctness/noUndeclaredDependencies ━━━━━━━
 ```
 invalid.js:3:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  ! The current dependency isn't specified in your package.json.
+  ! Dependency notInstalled isn't specified in package.json.
   
     1 │ import "notInstalled";
     2 │ import("notInstalled");
@@ -73,7 +72,7 @@ invalid.js:3:1 lint/correctness/noUndeclaredDependencies ━━━━━━━
 ```
 invalid.js:5:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  ! The current dependency isn't specified in your package.json.
+  ! Dependency @testing-library/react isn't specified in package.json.
   
     3 │ require("notInstalled")
     4 │ 
@@ -92,7 +91,7 @@ invalid.js:5:8 lint/correctness/noUndeclaredDependencies ━━━━━━━
 ```
 invalid.js:6:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  ! The current dependency isn't specified in your package.json.
+  ! Dependency @testing-library/react isn't specified in package.json.
   
     5 │ import "@testing-library/react";
   > 6 │ import("@testing-library/react");
@@ -110,7 +109,7 @@ invalid.js:6:1 lint/correctness/noUndeclaredDependencies ━━━━━━━
 ```
 invalid.js:7:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  ! The current dependency isn't specified in your package.json.
+  ! Dependency @testing-library/react isn't specified in package.json.
   
     5 │ import "@testing-library/react";
     6 │ import("@testing-library/react");
diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js.snap b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js.snap
index 5c70dc6205a2..f04ab49cdb0f 100644
--- a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js.snap
+++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredDependencies/invalid.test.js.snap
@@ -1,7 +1,6 @@
 ---
 source: crates/biome_js_analyze/tests/spec_tests.rs
 expression: invalid.test.js
-snapshot_kind: text
 ---
 # Input
 ```js
@@ -19,7 +18,7 @@ require("@testing-library/react");
 ```
 invalid.test.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  ! The current dependency isn't specified in your package.json.
+  ! Dependency notInstalled isn't specified in package.json.
   
   > 1 │ import "notInstalled";
       │        ^^^^^^^^^^^^^^
@@ -36,7 +35,7 @@ invalid.test.js:1:8 lint/correctness/noUndeclaredDependencies ━━━━━━
 ```
 invalid.test.js:2:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  ! The current dependency isn't specified in your package.json.
+  ! Dependency notInstalled isn't specified in package.json.
   
     1 │ import "notInstalled";
   > 2 │ import("notInstalled");
@@ -54,7 +53,7 @@ invalid.test.js:2:1 lint/correctness/noUndeclaredDependencies ━━━━━━
 ```
 invalid.test.js:3:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  ! The current dependency isn't specified in your package.json.
+  ! Dependency notInstalled isn't specified in package.json.
   
     1 │ import "notInstalled";
     2 │ import("notInstalled");
@@ -73,7 +72,7 @@ invalid.test.js:3:1 lint/correctness/noUndeclaredDependencies ━━━━━━
 ```
 invalid.test.js:5:8 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  ! The current dependency isn't specified in your package.json.
+  ! Dependency @testing-library/react isn't specified in package.json.
   
     3 │ require("notInstalled")
     4 │ 
@@ -92,7 +91,7 @@ invalid.test.js:5:8 lint/correctness/noUndeclaredDependencies ━━━━━━
 ```
 invalid.test.js:6:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  ! The current dependency isn't specified in your package.json.
+  ! Dependency @testing-library/react isn't specified in package.json.
   
     5 │ import "@testing-library/react";
   > 6 │ import("@testing-library/react");
@@ -110,7 +109,7 @@ invalid.test.js:6:1 lint/correctness/noUndeclaredDependencies ━━━━━━
 ```
 invalid.test.js:7:1 lint/correctness/noUndeclaredDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
-  ! The current dependency isn't specified in your package.json.
+  ! Dependency @testing-library/react isn't specified in package.json.
   
     5 │ import "@testing-library/react";
     6 │ import("@testing-library/react");
diff --git a/crates/biome_package/src/lib.rs b/crates/biome_package/src/lib.rs
index 14432ceae4eb..931f8e074289 100644
--- a/crates/biome_package/src/lib.rs
+++ b/crates/biome_package/src/lib.rs
@@ -28,8 +28,8 @@ pub trait Manifest: Default + Debug {
 pub trait Package {
     type Manifest: Manifest;
 
-    /// Use this function to prepare the package, like loading the manifest.
-    fn deserialize_manifest(&mut self, root: &PackageRoot<Self>);
+    /// Inserts a manifest into the package, taking care of deserialization.
+    fn insert_serialized_manifest(&mut self, root: &PackageRoot<Self>);
 
     fn manifest(&self) -> Option<&Self::Manifest> {
         None
diff --git a/crates/biome_package/src/node_js_package/mod.rs b/crates/biome_package/src/node_js_package/mod.rs
index 64f2ccfbfe40..bc1cf23d8909 100644
--- a/crates/biome_package/src/node_js_package/mod.rs
+++ b/crates/biome_package/src/node_js_package/mod.rs
@@ -37,11 +37,11 @@ pub(crate) type ProjectLanguageRoot<M> = <<M as Manifest>::Language as Language>
 impl Package for NodeJsPackage {
     type Manifest = PackageJson;
 
-    fn deserialize_manifest(&mut self, content: &ProjectLanguageRoot<Self::Manifest>) {
-        let manifest = Self::Manifest::deserialize_manifest(content);
-        let (package, deserialize_diagnostics) = manifest.consume();
-        self.manifest = package.unwrap_or_default();
-        self.diagnostics = deserialize_diagnostics
+    fn insert_serialized_manifest(&mut self, content: &ProjectLanguageRoot<Self::Manifest>) {
+        let deserialized = Self::Manifest::deserialize_manifest(content);
+        let (manifest, diagnostics) = deserialized.consume();
+        self.manifest = manifest.unwrap_or_default();
+        self.diagnostics = diagnostics
             .into_iter()
             .map(biome_diagnostics::serde::Diagnostic::new)
             .collect();
diff --git a/crates/biome_package/tests/manifest_spec_tests.rs b/crates/biome_package/tests/manifest_spec_tests.rs
index 2b375a921633..4c9b399920a3 100644
--- a/crates/biome_package/tests/manifest_spec_tests.rs
+++ b/crates/biome_package/tests/manifest_spec_tests.rs
@@ -24,7 +24,7 @@ fn run_invalid_manifests(input: &'static str, _: &str, _: &str, _: &str) {
     match input_file.extension().map(OsStr::as_encoded_bytes) {
         Some(b"json") => {
             let parsed = parse_json(input_code.as_str(), JsonParserOptions::default());
-            package.deserialize_manifest(&parsed.tree());
+            package.insert_serialized_manifest(&parsed.tree());
         }
         _ => {
             panic!("Extension not supported");
diff --git a/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs b/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs
index b2d08c723b13..d71af3a18263 100644
--- a/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs
+++ b/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs
@@ -9,10 +9,10 @@ use biome_grit_patterns::{
 };
 use biome_parser::AnyParse;
 use biome_rowan::TextRange;
-use camino::Utf8Path;
+use camino::{Utf8Path, Utf8PathBuf};
 use grit_pattern_matcher::{binding::Binding, pattern::ResolvedPattern};
 use grit_util::{error::GritPatternError, AnalysisLogs};
-use std::{borrow::Cow, fmt::Debug, path::PathBuf, rc::Rc};
+use std::{borrow::Cow, fmt::Debug, rc::Rc};
 
 use crate::{AnalyzerPlugin, PluginDiagnostic};
 
@@ -48,7 +48,7 @@ impl AnalyzerGritPlugin {
 }
 
 impl AnalyzerPlugin for AnalyzerGritPlugin {
-    fn evaluate(&self, root: AnyParse, path: PathBuf) -> Vec<RuleDiagnostic> {
+    fn evaluate(&self, root: AnyParse, path: Utf8PathBuf) -> Vec<RuleDiagnostic> {
         let name: &str = self.grit_query.name.as_deref().unwrap_or("anonymous");
 
         let file = GritTargetFile { parse: root, path };
diff --git a/crates/biome_project_layout/Cargo.toml b/crates/biome_project_layout/Cargo.toml
new file mode 100644
index 000000000000..a22b40f33515
--- /dev/null
+++ b/crates/biome_project_layout/Cargo.toml
@@ -0,0 +1,22 @@
+
+[package]
+authors.workspace    = true
+categories.workspace = true
+description          = "Data structure for tracking projects and their packages"
+edition.workspace    = true
+homepage.workspace   = true
+keywords.workspace   = true
+license.workspace    = true
+name                 = "biome_project_layout"
+repository.workspace = true
+version              = "0.0.1"
+
+[lints]
+workspace = true
+
+[dependencies]
+biome_package = { workspace = true }
+biome_parser  = { workspace = true }
+camino        = { workspace = true }
+papaya        = { workspace = true }
+rustc-hash    = { workspace = true }
diff --git a/crates/biome_project_layout/src/lib.rs b/crates/biome_project_layout/src/lib.rs
new file mode 100644
index 000000000000..b9d256276fb5
--- /dev/null
+++ b/crates/biome_project_layout/src/lib.rs
@@ -0,0 +1,3 @@
+mod project_layout;
+
+pub use project_layout::ProjectLayout;
diff --git a/crates/biome_project_layout/src/project_layout.rs b/crates/biome_project_layout/src/project_layout.rs
new file mode 100644
index 000000000000..23bc9e248578
--- /dev/null
+++ b/crates/biome_project_layout/src/project_layout.rs
@@ -0,0 +1,160 @@
+use biome_package::{NodeJsPackage, Package, PackageJson};
+use biome_parser::AnyParse;
+use camino::{Utf8Path, Utf8PathBuf};
+use papaya::HashMap;
+use rustc_hash::FxBuildHasher;
+
+/// The layout used across all open projects.
+///
+/// Projects are comprised of zero or more packages. This arrangement is
+/// intended to reflect the common usage of monorepos, where a single repository
+/// may host many packages, and each package is allowed to have its own
+/// settings.
+///
+/// For Biome, a project is where the **top-level** configuration file is, while
+/// packages may have their own nested configuration files.
+///
+/// As a data structure, the project layout is simply a flat mapping from paths
+/// to package data. This means that in order to lookup the package that is
+/// most relevant for a given file, we may need to do multiple lookups from the
+/// most-specific possible package path to the least. This means performance
+/// degrades linearly with the depth of the path of a file. For now though, this
+/// approach makes it very easy for us to invalidate part of the layout when
+/// there are file system changes.
+#[derive(Debug, Default)]
+pub struct ProjectLayout(HashMap<Utf8PathBuf, PackageData, FxBuildHasher>);
+
+/// The information tracked for each package.
+///
+/// Because Biome is intended to support multiple kinds of JavaScript projects,
+/// the term "package" is somewhat loosely defined. It may be an NPM package,
+/// a JSR package, or simply a directory with its own nested `biome.json`.
+#[derive(Debug, Default)]
+pub struct PackageData {
+    /// The settings of the package.
+    ///
+    /// Usually inferred from a configuration file, e.g. `biome.json`.
+    // TODO: Uncomment this.
+    // Probably best done when Ema has finished with https://github.com/biomejs/biome/pull/4845
+    //settings: Settings,
+
+    /// Optional Node.js-specific package information, if relevant for the
+    /// package.
+    node_package: Option<NodeJsPackage>,
+}
+
+impl ProjectLayout {
+    /// Returns the `package.json` that should be used for the given `path`,
+    /// together with the absolute path of the manifest file.
+    pub fn get_node_manifest_for_path(
+        &self,
+        path: &Utf8Path,
+    ) -> Option<(Utf8PathBuf, PackageJson)> {
+        // Note I also tried an alternative approach where instead of iterating
+        // over all entries and finding the closest match, I would do repeated
+        // lookups like this:
+        //
+        // ```rs
+        // let packages = self.0.pin();
+        // path.ancestors().skip(1).find_map(|package_path| {
+        //     packages
+        //         .get(package_path)
+        //         .and_then(|data| data.node_package.as_ref())
+        //         .map(|node_package| (package_path.to_path_buf(), node_package.manifest.clone()))
+        // })
+        // ```
+        //
+        // Contrary to what I expected however, the below implementation
+        // appeared significantly faster (tested on the `unleash` repository).
+
+        let mut result: Option<(&Utf8PathBuf, &PackageJson)> = None;
+
+        let packages = self.0.pin();
+        for (package_path, data) in packages.iter() {
+            let Some(node_manifest) = data
+                .node_package
+                .as_ref()
+                .map(|node_package| &node_package.manifest)
+            else {
+                continue;
+            };
+
+            let is_closest_match = path.strip_prefix(package_path).is_ok()
+                && result.is_none_or(|(matched_package_path, _)| {
+                    package_path.as_str().len() > matched_package_path.as_str().len()
+                });
+
+            if is_closest_match {
+                result = Some((package_path, node_manifest));
+            }
+        }
+
+        result.map(|(package_path, package_json)| (package_path.clone(), package_json.clone()))
+    }
+
+    pub fn insert_node_manifest(&self, path: Utf8PathBuf, manifest: PackageJson) {
+        self.0.pin().update_or_insert_with(
+            path,
+            |data| {
+                let mut node_js_package = NodeJsPackage {
+                    manifest: Default::default(),
+                    diagnostics: Default::default(),
+                    tsconfig: data
+                        .node_package
+                        .as_ref()
+                        .map(|package| package.tsconfig.clone())
+                        .unwrap_or_default(),
+                };
+                node_js_package.manifest = manifest.clone();
+
+                PackageData {
+                    node_package: Some(node_js_package),
+                }
+            },
+            || {
+                let node_js_package = NodeJsPackage {
+                    manifest: manifest.clone(),
+                    ..Default::default()
+                };
+
+                PackageData {
+                    node_package: Some(node_js_package),
+                }
+            },
+        );
+    }
+
+    pub fn insert_serialized_node_manifest(&self, path: Utf8PathBuf, manifest: AnyParse) {
+        self.0.pin().update_or_insert_with(
+            path,
+            |data| {
+                let mut node_js_package = NodeJsPackage {
+                    manifest: Default::default(),
+                    diagnostics: Default::default(),
+                    tsconfig: data
+                        .node_package
+                        .as_ref()
+                        .map(|package| package.tsconfig.clone())
+                        .unwrap_or_default(),
+                };
+                node_js_package.insert_serialized_manifest(&manifest.tree());
+
+                PackageData {
+                    node_package: Some(node_js_package),
+                }
+            },
+            || {
+                let mut node_js_package = NodeJsPackage::default();
+                node_js_package.insert_serialized_manifest(&manifest.tree());
+
+                PackageData {
+                    node_package: Some(node_js_package),
+                }
+            },
+        );
+    }
+
+    pub fn remove_package(&self, path: &Utf8Path) {
+        self.0.pin().remove(path);
+    }
+}
diff --git a/crates/biome_service/Cargo.toml b/crates/biome_service/Cargo.toml
index a25238e95b0d..e235dc6b8de9 100644
--- a/crates/biome_service/Cargo.toml
+++ b/crates/biome_service/Cargo.toml
@@ -49,6 +49,7 @@ biome_json_parser       = { workspace = true }
 biome_json_syntax       = { workspace = true }
 biome_package           = { workspace = true }
 biome_parser            = { workspace = true }
+biome_project_layout    = { workspace = true }
 biome_rowan             = { workspace = true, features = ["serde"] }
 biome_string_case       = { workspace = true }
 biome_text_edit         = { workspace = true }
diff --git a/crates/biome_service/src/file_handlers/css.rs b/crates/biome_service/src/file_handlers/css.rs
index 91c788c976f1..c05a52058307 100644
--- a/crates/biome_service/src/file_handlers/css.rs
+++ b/crates/biome_service/src/file_handlers/css.rs
@@ -326,7 +326,7 @@ fn lint(params: LintParams) -> LintResults {
             .with_skip(&params.skip)
             .with_path(params.path.as_path())
             .with_enabled_rules(&params.enabled_rules)
-            .with_manifest(params.manifest.as_ref())
+            .with_project_layout(params.project_layout.clone())
             .finish();
 
     let filter = AnalysisFilter {
@@ -353,7 +353,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult {
         range,
         workspace,
         path,
-        manifest,
+        project_layout,
         language,
         only,
         skip,
@@ -379,7 +379,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult {
             .with_skip(&skip)
             .with_path(path.as_path())
             .with_enabled_rules(&rules)
-            .with_manifest(manifest.as_ref())
+            .with_project_layout(project_layout)
             .finish();
 
     let filter = AnalysisFilter {
@@ -437,7 +437,7 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr
             .with_skip(&params.skip)
             .with_path(params.biome_path.as_path())
             .with_enabled_rules(&params.enabled_rules)
-            .with_manifest(params.manifest.as_ref())
+            .with_project_layout(params.project_layout)
             .finish();
 
     let filter = AnalysisFilter {
diff --git a/crates/biome_service/src/file_handlers/graphql.rs b/crates/biome_service/src/file_handlers/graphql.rs
index 39376ef00262..08ce24885cdd 100644
--- a/crates/biome_service/src/file_handlers/graphql.rs
+++ b/crates/biome_service/src/file_handlers/graphql.rs
@@ -304,7 +304,7 @@ fn lint(params: LintParams) -> LintResults {
             .with_skip(&params.skip)
             .with_path(params.path.as_path())
             .with_enabled_rules(&params.enabled_rules)
-            .with_manifest(params.manifest.as_ref())
+            .with_project_layout(params.project_layout.clone())
             .finish();
 
     let filter = AnalysisFilter {
@@ -331,7 +331,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult {
         range,
         workspace,
         path,
-        manifest,
+        project_layout,
         language,
         only,
         skip,
@@ -360,7 +360,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult {
             .with_skip(&skip)
             .with_path(path.as_path())
             .with_enabled_rules(&rules)
-            .with_manifest(manifest.as_ref())
+            .with_project_layout(project_layout)
             .finish();
 
     let filter = AnalysisFilter {
@@ -418,7 +418,7 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr
             .with_skip(&params.skip)
             .with_path(params.biome_path.as_path())
             .with_enabled_rules(&params.enabled_rules)
-            .with_manifest(params.manifest.as_ref())
+            .with_project_layout(params.project_layout)
             .finish();
 
     let filter = AnalysisFilter {
diff --git a/crates/biome_service/src/file_handlers/javascript.rs b/crates/biome_service/src/file_handlers/javascript.rs
index bb9a66224dd5..49bff1cf059d 100644
--- a/crates/biome_service/src/file_handlers/javascript.rs
+++ b/crates/biome_service/src/file_handlers/javascript.rs
@@ -395,7 +395,7 @@ fn debug_control_flow(parse: AnyParse, cursor: TextSize) -> String {
         &options,
         Vec::new(),
         JsFileSource::default(),
-        None,
+        Default::default(),
         |_| ControlFlow::<Never>::Continue(()),
     );
 
@@ -445,7 +445,7 @@ pub(crate) fn lint(params: LintParams) -> LintResults {
             .with_skip(&params.skip)
             .with_path(params.path.as_path())
             .with_enabled_rules(&params.enabled_rules)
-            .with_manifest(params.manifest.as_ref())
+            .with_project_layout(params.project_layout.clone())
             .finish();
 
     let filter = AnalysisFilter {
@@ -463,7 +463,7 @@ pub(crate) fn lint(params: LintParams) -> LintResults {
         &analyzer_options,
         Vec::new(),
         file_source,
-        params.manifest.as_ref(),
+        params.project_layout,
         |signal| process_lint.process_signal(signal),
     );
 
@@ -477,7 +477,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult {
         range,
         workspace,
         path,
-        manifest,
+        project_layout,
         language,
         only,
         skip,
@@ -496,7 +496,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult {
             .with_skip(&skip)
             .with_path(path.as_path())
             .with_enabled_rules(&rules)
-            .with_manifest(manifest.as_ref())
+            .with_project_layout(project_layout.clone())
             .finish();
 
     let filter = AnalysisFilter {
@@ -524,7 +524,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult {
         &analyzer_options,
         Vec::new(),
         source_type,
-        manifest.as_ref(),
+        project_layout,
         |signal| {
             actions.extend(signal.actions().into_code_action_iter().map(|item| {
                 trace!("Pulled action category {:?}", item.category);
@@ -569,7 +569,7 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr
             .with_skip(&params.skip)
             .with_path(params.biome_path.as_path())
             .with_enabled_rules(&params.enabled_rules)
-            .with_manifest(params.manifest.as_ref())
+            .with_project_layout(params.project_layout.clone())
             .finish();
 
     let filter = AnalysisFilter {
@@ -598,7 +598,7 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr
             &analyzer_options,
             Vec::new(),
             file_source,
-            params.manifest.as_ref(),
+            params.project_layout.clone(),
             |signal| {
                 let current_diagnostic = signal.diagnostic();
 
diff --git a/crates/biome_service/src/file_handlers/json.rs b/crates/biome_service/src/file_handlers/json.rs
index 1d7ffa303047..d9c905ce543a 100644
--- a/crates/biome_service/src/file_handlers/json.rs
+++ b/crates/biome_service/src/file_handlers/json.rs
@@ -348,7 +348,7 @@ fn lint(params: LintParams) -> LintResults {
             .with_skip(&params.skip)
             .with_path(params.path.as_path())
             .with_enabled_rules(&params.enabled_rules)
-            .with_manifest(params.manifest.as_ref())
+            .with_project_layout(params.project_layout.clone())
             .finish();
 
     let filter = AnalysisFilter {
@@ -390,7 +390,7 @@ fn code_actions(params: CodeActionsParams) -> PullActionsResult {
         range,
         workspace,
         path,
-        manifest,
+        project_layout,
         language,
         skip,
         only,
@@ -412,7 +412,7 @@ fn code_actions(params: CodeActionsParams) -> PullActionsResult {
             .with_skip(&skip)
             .with_path(path.as_path())
             .with_enabled_rules(&rules)
-            .with_manifest(manifest.as_ref())
+            .with_project_layout(project_layout)
             .finish();
 
     let filter = AnalysisFilter {
@@ -474,7 +474,7 @@ fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceError> {
             .with_skip(&params.skip)
             .with_path(params.biome_path.as_path())
             .with_enabled_rules(&params.enabled_rules)
-            .with_manifest(params.manifest.as_ref())
+            .with_project_layout(params.project_layout)
             .finish();
 
     let filter = AnalysisFilter {
diff --git a/crates/biome_service/src/file_handlers/mod.rs b/crates/biome_service/src/file_handlers/mod.rs
index b3fe1fc21595..365bb3ed7785 100644
--- a/crates/biome_service/src/file_handlers/mod.rs
+++ b/crates/biome_service/src/file_handlers/mod.rs
@@ -37,8 +37,8 @@ use biome_js_syntax::{
 };
 use biome_json_analyze::METADATA as json_metadata;
 use biome_json_syntax::{JsonFileSource, JsonLanguage};
-use biome_package::PackageJson;
 use biome_parser::AnyParse;
+use biome_project_layout::ProjectLayout;
 use biome_rowan::{FileSourceError, NodeCache};
 use biome_string_case::StrLikeExtension;
 
@@ -48,6 +48,7 @@ use html::HtmlFileHandler;
 pub use javascript::JsFormatterSettings;
 use rustc_hash::FxHashSet;
 use std::borrow::Cow;
+use std::sync::Arc;
 use tracing::instrument;
 
 mod astro;
@@ -387,7 +388,7 @@ pub struct FixAllParams<'a> {
     /// Whether it should format the code action
     pub(crate) should_format: bool,
     pub(crate) biome_path: &'a BiomePath,
-    pub(crate) manifest: Option<PackageJson>,
+    pub(crate) project_layout: Arc<ProjectLayout>,
     pub(crate) document_file_source: DocumentFileSource,
     pub(crate) only: Vec<RuleSelector>,
     pub(crate) skip: Vec<RuleSelector>,
@@ -450,7 +451,7 @@ pub(crate) struct LintParams<'a> {
     pub(crate) only: Vec<RuleSelector>,
     pub(crate) skip: Vec<RuleSelector>,
     pub(crate) categories: RuleCategories,
-    pub(crate) manifest: Option<PackageJson>,
+    pub(crate) project_layout: Arc<ProjectLayout>,
     pub(crate) suppression_reason: Option<String>,
     pub(crate) enabled_rules: Vec<RuleSelector>,
 }
@@ -573,7 +574,7 @@ pub(crate) struct CodeActionsParams<'a> {
     pub(crate) range: Option<TextRange>,
     pub(crate) workspace: &'a WorkspaceSettingsHandle,
     pub(crate) path: &'a BiomePath,
-    pub(crate) manifest: Option<PackageJson>,
+    pub(crate) project_layout: Arc<ProjectLayout>,
     pub(crate) language: DocumentFileSource,
     pub(crate) only: Vec<RuleSelector>,
     pub(crate) skip: Vec<RuleSelector>,
@@ -791,7 +792,7 @@ pub(crate) fn search(
 ) -> Result<Vec<TextRange>, WorkspaceError> {
     let result = query
         .execute(GritTargetFile {
-            path: path.as_std_path().to_path_buf(),
+            path: path.to_path_buf(),
             parse,
         })
         .map_err(|err| {
@@ -936,7 +937,7 @@ struct LintVisitor<'a, 'b> {
     skip: Option<&'b [RuleSelector]>,
     settings: Option<&'b Settings>,
     path: Option<&'b Utf8Path>,
-    manifest: Option<&'b PackageJson>,
+    project_layout: Arc<ProjectLayout>,
     analyzer_options: &'b mut AnalyzerOptions,
 }
 
@@ -946,7 +947,7 @@ impl<'a, 'b> LintVisitor<'a, 'b> {
         skip: Option<&'b [RuleSelector]>,
         settings: Option<&'b Settings>,
         path: Option<&'b Utf8Path>,
-        manifest: Option<&'b PackageJson>,
+        project_layout: Arc<ProjectLayout>,
         analyzer_options: &'b mut AnalyzerOptions,
     ) -> Self {
         Self {
@@ -956,7 +957,7 @@ impl<'a, 'b> LintVisitor<'a, 'b> {
             skip,
             settings,
             path,
-            manifest,
+            project_layout,
             analyzer_options,
         }
     }
@@ -977,7 +978,10 @@ impl<'a, 'b> LintVisitor<'a, 'b> {
             .is_none_or(|d| d.is_empty());
 
         if no_only && no_domains {
-            if let Some(manifest) = self.manifest {
+            if let Some((_, manifest)) = self
+                .path
+                .and_then(|path| self.project_layout.get_node_manifest_for_path(path))
+            {
                 for domain in R::METADATA.domains {
                     self.analyzer_options
                         .push_globals(domain.globals().iter().map(|s| Box::from(*s)).collect());
@@ -1361,7 +1365,7 @@ pub(crate) struct AnalyzerVisitorBuilder<'a> {
     skip: Option<&'a [RuleSelector]>,
     path: Option<&'a Utf8Path>,
     enabled_rules: Option<&'a [RuleSelector]>,
-    manifest: Option<&'a PackageJson>,
+    project_layout: Arc<ProjectLayout>,
     analyzer_options: AnalyzerOptions,
 }
 
@@ -1373,7 +1377,7 @@ impl<'b> AnalyzerVisitorBuilder<'b> {
             skip: None,
             path: None,
             enabled_rules: None,
-            manifest: None,
+            project_layout: Default::default(),
             analyzer_options,
         }
     }
@@ -1403,8 +1407,8 @@ impl<'b> AnalyzerVisitorBuilder<'b> {
     }
 
     #[must_use]
-    pub(crate) fn with_manifest(mut self, manifest: Option<&'b PackageJson>) -> Self {
-        self.manifest = manifest;
+    pub(crate) fn with_project_layout(mut self, project_layout: Arc<ProjectLayout>) -> Self {
+        self.project_layout = project_layout;
         self
     }
 
@@ -1434,7 +1438,7 @@ impl<'b> AnalyzerVisitorBuilder<'b> {
             self.skip,
             self.settings,
             self.path,
-            self.manifest,
+            self.project_layout,
             &mut analyzer_options,
         );
 
diff --git a/crates/biome_service/src/lib.rs b/crates/biome_service/src/lib.rs
index 754fe6c435a4..1ae2c352a94d 100644
--- a/crates/biome_service/src/lib.rs
+++ b/crates/biome_service/src/lib.rs
@@ -2,7 +2,6 @@ pub mod documentation;
 pub mod file_handlers;
 
 pub mod matcher;
-pub mod project_layout;
 pub mod projects;
 pub mod settings;
 pub mod workspace;
diff --git a/crates/biome_service/src/project_layout.rs b/crates/biome_service/src/project_layout.rs
deleted file mode 100644
index a9cfa3a8d06a..000000000000
--- a/crates/biome_service/src/project_layout.rs
+++ /dev/null
@@ -1,106 +0,0 @@
-use biome_package::{NodeJsPackage, Package, PackageJson};
-use biome_parser::AnyParse;
-use camino::{Utf8Path, Utf8PathBuf};
-use papaya::HashMap;
-use rustc_hash::FxBuildHasher;
-
-/// The layout used across all open projects.
-///
-/// Projects are comprised of zero or more packages. This arrangement is
-/// intended to reflect the common usage of monorepos, where a single repository
-/// may host many packages, and each package is allowed to have its own
-/// settings.
-///
-/// For Biome, a project is where the **top-level** configuration file is, while
-/// packages may have their own nested configuration files.
-///
-/// As a data structure, the project layout is simply a flat mapping from paths
-/// to package data. This means that in order to lookup the package that is
-/// most relevant for a given file, we may need to do a dumb iteration over all
-/// entries to find which is the closest match. This means performance becomes
-/// O(N) with the number of open packages, so if this becomes a bottleneck, we
-/// may want to reconsider this approach. For now though, it makes sense because
-/// it makes it very easy for us to invalidate part of the layout when there are
-/// file system changes.
-#[derive(Debug, Default)]
-pub struct ProjectLayout(HashMap<Utf8PathBuf, PackageData, FxBuildHasher>);
-
-/// The information tracked for each package.
-///
-/// Because Biome is intended to support multiple kinds of JavaScript projects,
-/// the term "package" is somewhat loosely defined. It may be an NPM package,
-/// a JSR package, or simply a directory with its own nested `biome.json`.
-#[derive(Debug, Default)]
-pub struct PackageData {
-    /// The settings of the package.
-    ///
-    /// Usually inferred from a configuration file, e.g. `biome.json`.
-    // TODO: Uncomment this.
-    // Probably best done when Ema has finished with https://github.com/biomejs/biome/pull/4845
-    //settings: Settings,
-
-    /// Optional Node.js-specific package information, if relevant for the
-    /// package.
-    node_package: Option<NodeJsPackage>,
-}
-
-impl ProjectLayout {
-    pub fn get_node_manifest_for_path(&self, path: &Utf8Path) -> Option<PackageJson> {
-        self.0
-            .pin()
-            .iter()
-            .fold(
-                None::<(&Utf8PathBuf, PackageJson)>,
-                |result, (package_path, data)| {
-                    let node_manifest = data
-                        .node_package
-                        .as_ref()
-                        .map(|node_package| &node_package.manifest)?;
-                    if path.strip_prefix(package_path).is_err() {
-                        return None;
-                    }
-
-                    result
-                        .is_none_or(|(matched_package_path, _)| {
-                            package_path.as_str().len() > matched_package_path.as_str().len()
-                        })
-                        .then(|| (package_path, node_manifest.clone()))
-                },
-            )
-            .map(|(_, package_json)| package_json)
-    }
-
-    pub fn insert_node_manifest(&self, path: Utf8PathBuf, manifest: AnyParse) {
-        self.0.pin().update_or_insert_with(
-            path,
-            |data| {
-                let mut node_js_package = NodeJsPackage {
-                    manifest: Default::default(),
-                    diagnostics: Default::default(),
-                    tsconfig: data
-                        .node_package
-                        .as_ref()
-                        .map(|package| package.tsconfig.clone())
-                        .unwrap_or_default(),
-                };
-                node_js_package.deserialize_manifest(&manifest.tree());
-
-                PackageData {
-                    node_package: Some(node_js_package),
-                }
-            },
-            || {
-                let mut node_js_package = NodeJsPackage::default();
-                node_js_package.deserialize_manifest(&manifest.tree());
-
-                PackageData {
-                    node_package: Some(node_js_package),
-                }
-            },
-        );
-    }
-
-    pub fn remove_package(&self, path: &Utf8Path) {
-        self.0.pin().remove(path);
-    }
-}
diff --git a/crates/biome_service/src/workspace/scanner.rs b/crates/biome_service/src/workspace/scanner.rs
index 164e7eed9232..ef727f8d3a49 100644
--- a/crates/biome_service/src/workspace/scanner.rs
+++ b/crates/biome_service/src/workspace/scanner.rs
@@ -185,7 +185,7 @@ impl<'app> TraversalContext for ScanContext<'app> {
     }
 
     fn can_handle(&self, path: &BiomePath) -> bool {
-        DocumentFileSource::try_from_path(path).is_ok()
+        path.is_dir() || DocumentFileSource::try_from_path(path).is_ok()
     }
 
     fn handle_path(&self, path: BiomePath) {
diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs
index d85c07cc75af..d2a4722952c3 100644
--- a/crates/biome_service/src/workspace/server.rs
+++ b/crates/biome_service/src/workspace/server.rs
@@ -14,7 +14,6 @@ use crate::file_handlers::{
     Capabilities, CodeActionsParams, DocumentFileSource, FixAllParams, LintParams, ParseResult,
 };
 use crate::is_dir;
-use crate::project_layout::ProjectLayout;
 use crate::projects::Projects;
 use crate::workspace::{
     FileFeaturesResult, GetFileContentParams, IsPathIgnoredParams, RageEntry, RageParams,
@@ -35,15 +34,16 @@ use biome_grit_patterns::{compile_pattern_with_options, CompilePatternOptions, G
 use biome_js_syntax::ModuleKind;
 use biome_json_parser::JsonParserOptions;
 use biome_json_syntax::JsonFileSource;
-use biome_package::{PackageJson, PackageType};
+use biome_package::PackageType;
 use biome_parser::AnyParse;
+use biome_project_layout::ProjectLayout;
 use biome_rowan::NodeCache;
 use camino::{Utf8Path, Utf8PathBuf};
 use papaya::HashMap;
 use rustc_hash::{FxBuildHasher, FxHashMap};
 use std::panic::RefUnwindSafe;
 use std::sync::atomic::{AtomicUsize, Ordering};
-use std::sync::Mutex;
+use std::sync::{Arc, Mutex};
 use tracing::{debug, info, info_span, warn};
 
 pub(super) struct WorkspaceServer {
@@ -55,7 +55,7 @@ pub(super) struct WorkspaceServer {
     projects: Projects,
 
     /// The layout of projects and their internal packages.
-    project_layout: ProjectLayout,
+    project_layout: Arc<ProjectLayout>,
 
     /// Stores the document (text content + version number) associated with a URL
     documents: HashMap<Utf8PathBuf, Document, FxBuildHasher>,
@@ -238,20 +238,6 @@ impl WorkspaceServer {
         }
     }
 
-    /// Returns the parsed `package.json` for a given path.
-    ///
-    /// ## Errors
-    ///
-    /// - If no document is found in the workspace. Usually, you'll have to call
-    ///   [WorkspaceServer::set_manifest_for_project] to store said document.
-    #[tracing::instrument(level = "trace", skip(self))]
-    fn get_node_manifest_for_path(
-        &self,
-        path: &Utf8Path,
-    ) -> Result<Option<PackageJson>, WorkspaceError> {
-        Ok(self.project_layout.get_node_manifest_for_path(path))
-    }
-
     /// Returns a previously inserted file source by index.
     ///
     /// File sources can be inserted using `insert_source()`.
@@ -299,10 +285,19 @@ impl WorkspaceServer {
     ) -> Result<(), WorkspaceError> {
         let path: Utf8PathBuf = path.into();
         let mut source = document_file_source.unwrap_or(DocumentFileSource::from_path(&path));
-        let manifest = self.get_node_manifest_for_path(&path)?;
+        let manifest = if opened_by_scanner {
+            // FIXME: It doesn't make sense to retrieve the manifest when the
+            //        file is opened by the scanner, because it means the
+            //        project layout isn't yet initialized anyway. But that
+            //        highlights an issue with the CommonJS check below, since
+            //        we can't seem to set this correctly now.
+            None
+        } else {
+            self.project_layout.get_node_manifest_for_path(&path)
+        };
 
         if let DocumentFileSource::Js(js) = &mut source {
-            if let Some(manifest) = manifest {
+            if let Some((_, manifest)) = manifest {
                 if manifest.r#type == Some(PackageType::Commonjs) && js.file_extension() == "js" {
                     js.set_module_kind(ModuleKind::Script);
                 }
@@ -526,7 +521,7 @@ impl WorkspaceServer {
                 .ok_or_else(WorkspaceError::not_found)?;
             let parsed = self.get_parse(path)?;
             self.project_layout
-                .insert_node_manifest(package_path, parsed);
+                .insert_serialized_node_manifest(package_path, parsed);
         }
 
         Ok(())
@@ -853,7 +848,6 @@ impl Workspace for WorkspaceServer {
         }: PullDiagnosticsParams,
     ) -> Result<PullDiagnosticsResult, WorkspaceError> {
         let parse = self.get_parse(&path)?;
-        let manifest = self.get_node_manifest_for_path(&path)?;
         let (diagnostics, errors, skipped_diagnostics) =
             if let Some(lint) = self.get_file_capabilities(&path).analyzer.lint {
                 info_span!("Pulling diagnostics", categories =? categories).in_scope(|| {
@@ -866,7 +860,7 @@ impl Workspace for WorkspaceServer {
                         skip,
                         language: self.get_file_source(&path),
                         categories,
-                        manifest,
+                        project_layout: self.project_layout.clone(),
                         suppression_reason: None,
                         enabled_rules,
                     });
@@ -923,14 +917,13 @@ impl Workspace for WorkspaceServer {
             .ok_or_else(self.build_capability_error(&path))?;
 
         let parse = self.get_parse(&path)?;
-        let manifest = self.get_node_manifest_for_path(&path)?;
         let language = self.get_file_source(&path);
         Ok(code_actions(CodeActionsParams {
             parse,
             range,
             workspace: &self.projects.get_settings(project_key).into(),
             path: &path,
-            manifest,
+            project_layout: self.project_layout.clone(),
             language,
             only,
             skip,
@@ -1030,7 +1023,6 @@ impl Workspace for WorkspaceServer {
             .ok_or_else(self.build_capability_error(&path))?;
         let parse = self.get_parse(&path)?;
 
-        let manifest = self.get_node_manifest_for_path(&path)?;
         let language = self.get_file_source(&path);
         fix_all(FixAllParams {
             parse,
@@ -1038,7 +1030,7 @@ impl Workspace for WorkspaceServer {
             workspace: self.projects.get_settings(project_key).into(),
             should_format,
             biome_path: &path,
-            manifest,
+            project_layout: self.project_layout.clone(),
             document_file_source: language,
             only,
             skip,
diff --git a/crates/biome_test_utils/Cargo.toml b/crates/biome_test_utils/Cargo.toml
index 6ac61a65b172..37064da24c26 100644
--- a/crates/biome_test_utils/Cargo.toml
+++ b/crates/biome_test_utils/Cargo.toml
@@ -14,21 +14,22 @@ version              = "0.0.0"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-biome_analyze       = { workspace = true }
-biome_configuration = { workspace = true }
-biome_console       = { workspace = true }
-biome_deserialize   = { workspace = true }
-biome_diagnostics   = { workspace = true }
-biome_formatter     = { workspace = true }
-biome_json_parser   = { workspace = true }
-biome_package       = { workspace = true }
-biome_rowan         = { workspace = true }
-biome_service       = { workspace = true }
-camino              = { workspace = true }
-countme             = { workspace = true, features = ["enable"] }
-json_comments       = "0.2.2"
-serde_json          = { workspace = true }
-similar             = { workspace = true }
+biome_analyze        = { workspace = true }
+biome_configuration  = { workspace = true }
+biome_console        = { workspace = true }
+biome_deserialize    = { workspace = true }
+biome_diagnostics    = { workspace = true }
+biome_formatter      = { workspace = true }
+biome_json_parser    = { workspace = true }
+biome_package        = { workspace = true }
+biome_project_layout = { workspace = true }
+biome_rowan          = { workspace = true }
+biome_service        = { workspace = true }
+camino               = { workspace = true }
+countme              = { workspace = true, features = ["enable"] }
+json_comments        = "0.2.2"
+serde_json           = { workspace = true }
+similar              = { workspace = true }
 
 [lints]
 workspace = true
diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs
index 2c50e80b7d0a..69ff36973ccc 100644
--- a/crates/biome_test_utils/src/lib.rs
+++ b/crates/biome_test_utils/src/lib.rs
@@ -7,6 +7,7 @@ use biome_diagnostics::termcolor::Buffer;
 use biome_diagnostics::{DiagnosticExt, Error, PrintDiagnostic};
 use biome_json_parser::{JsonParserOptions, ParseDiagnostic};
 use biome_package::PackageJson;
+use biome_project_layout::ProjectLayout;
 use biome_rowan::{SyntaxKind, SyntaxNode, SyntaxSlot};
 use biome_service::configuration::to_analyzer_rules;
 use biome_service::file_handlers::DocumentFileSource;
@@ -17,7 +18,7 @@ use json_comments::StripComments;
 use similar::{DiffableStr, TextDiff};
 use std::ffi::c_int;
 use std::fmt::Write;
-use std::sync::Once;
+use std::sync::{Arc, Once};
 
 pub fn scripts_from_json(extension: &str, input_code: &str) -> Option<Vec<String>> {
     if extension == "json" || extension == "jsonc" {
@@ -157,7 +158,10 @@ where
     }
 }
 
-pub fn load_manifest(input_file: &Utf8Path, diagnostics: &mut Vec<String>) -> Option<PackageJson> {
+pub fn project_layout_with_node_manifest(
+    input_file: &Utf8Path,
+    diagnostics: &mut Vec<String>,
+) -> Arc<ProjectLayout> {
     let options_file = input_file.with_extension("package.json");
     if let Ok(json) = std::fs::read_to_string(options_file.clone()) {
         let deserialized = biome_deserialize::json::deserialize_from_json_str::<PackageJson>(
@@ -176,10 +180,15 @@ pub fn load_manifest(input_file: &Utf8Path, diagnostics: &mut Vec<String>) -> Op
                     .collect::<Vec<_>>(),
             );
         } else {
-            return deserialized.into_deserialized();
+            let project_layout = ProjectLayout::default();
+            project_layout.insert_node_manifest(
+                Utf8PathBuf::new(),
+                deserialized.into_deserialized().unwrap_or_default(),
+            );
+            return Arc::new(project_layout);
         }
     }
-    None
+    Default::default()
 }
 
 pub fn diagnostic_to_string(name: &str, source: &str, diag: Error) -> String {
diff --git a/knope.toml b/knope.toml
index 90da859221cc..c59fe4c448df 100644
--- a/knope.toml
+++ b/knope.toml
@@ -230,6 +230,10 @@ versioned_files = ["crates/biome_glob/Cargo.toml"]
 changelog       = "crates/biome_syntax_codegen/CHANGELOG.md"
 versioned_files = ["crates/biome_syntax_codegen/Cargo.toml"]
 
+[packages.biome_project_layout]
+changelog       = "crates/biome_project_layout/CHANGELOG.md"
+versioned_files = ["crates/biome_project_layout/Cargo.toml"]
+
 ## End of crates. DO NOT CHANGE!
 
 # Workflow to create a changeset
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 72da6d163350..f2d95c0f3d3f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13,6 +13,9 @@ importers:
       '@typescript-eslint/eslint-plugin':
         specifier: 8.18.1
         version: 8.18.1(@typescript-eslint/parser@8.3.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
+      '@typescript-eslint/parser':
+        specifier: 8.3.0
+        version: 8.3.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
       dprint:
         specifier: 0.48.0
         version: 0.48.0
diff --git a/xtask/bench/src/language.rs b/xtask/bench/src/language.rs
index 963bed655d27..cc919b8cfb5c 100644
--- a/xtask/bench/src/language.rs
+++ b/xtask/bench/src/language.rs
@@ -200,7 +200,7 @@ impl Analyze {
                     &options,
                     Vec::new(),
                     JsFileSource::default(),
-                    None,
+                    Default::default(),
                     |event| {
                         black_box(event.diagnostic());
                         black_box(event.actions());
diff --git a/xtask/rules_check/src/lib.rs b/xtask/rules_check/src/lib.rs
index 5bb533c8b2ef..f20ab885c2bc 100644
--- a/xtask/rules_check/src/lib.rs
+++ b/xtask/rules_check/src/lib.rs
@@ -432,7 +432,7 @@ fn assert_lint(
                     &options,
                     vec![],
                     file_source,
-                    None,
+                    Default::default(),
                     |signal| {
                         if let Some(mut diag) = signal.diagnostic() {
                             for action in signal.actions() {