vercel/next.js · #100726

Turbopack: fix trailing-slash package root resolution

mischnic · merged Oct 29, 20266 files · 150 + / 10 −
turbopack/crates/turbopack-core/src/resolve/mod.rs118 + / 10 −
@@ -2971,9 +2971,6 @@ async fn resolve_into_package(     let options_value = options.await?;     let mut results = Vec::new(); -    let is_root_match = path.is_match("") || path.is_match("/");-    let could_match_others = path.could_match_others("");-     let mut export_path_request = path.clone();     export_path_request.push_front(rcstr!(".").into());     for resolve_into_package in options_value.into_package.iter() {@@ -3013,15 +3010,24 @@ async fn resolve_into_package(         }     } -    // apply main field(s) or fallback to index.js if there's no subpath-    if is_root_match {-        results.push(resolve_into_folder(-            package_path.clone(),-            options.with_fully_specified(false),-        ));+    // Apply main field(s) or fall back to index.js for each matching package root.+    // Keep the trailing slash in the request key: `package` and `package/` may+    // resolve to the same source, but they are different requests.+    let matches_root = path.is_match("");+    let matches_root_with_slash = path.is_match("/");+    if matches_root || matches_root_with_slash {+        let root_result =+            resolve_into_folder(package_path.clone(), options.with_fully_specified(false));+        if matches_root {+            results.push(root_result);+        }+        if matches_root_with_slash {+            results.push(root_result.with_request(rcstr!("./")));+        }     } -    if could_match_others {+    // The slash-only root is already handled above; only resolve deeper subpaths here.+    if path.could_match_others("/") {         let mut new_pat = path.clone();         new_pat.push_front(rcstr!(".").into()); @@ -3908,6 +3914,108 @@ mod tests {         .await;     } +    #[rstest::rstest]+    #[case::main(rcstr!("package").into(), r#"{"main":"./main.js"}"#, vec![("package", "main.js")])]+    #[case::main_trailing_slash(rcstr!("package/").into(), r#"{"main":"./main.js"}"#, vec![("package/", "main.js")])]+    #[case::index(rcstr!("package").into(), "{}", vec![("package", "index.js")])]+    #[case::index_trailing_slash(rcstr!("package/").into(), "{}", vec![("package/", "index.js")])]+    #[case::exports(rcstr!("package").into(), r#"{"exports":"./main.js"}"#, vec![("package", "main.js")])]+    #[case::exports_trailing_slash(rcstr!("package/").into(), r#"{"exports":"./main.js"}"#, vec![])]+    #[case::exports_subpath(rcstr!("package/subpath.js").into(), r#"{"exports":{"./subpath.js":"./main.js"}}"#, vec![("package/subpath.js", "main.js")])]+    #[case::dynamic_subpath(+        Pattern::concat([rcstr!("package/").into(), Pattern::Dynamic, rcstr!(".js").into()]),+        r#"{"main":"./main.js"}"#,+        vec![("package/index.js", "index.js"), ("package/main.js", "main.js"), ("package/subpath.js", "subpath.js")]+    )]+    #[case::dynamic_trailing_slash(+        Pattern::concat([rcstr!("package/").into(), Pattern::Dynamic]),+        r#"{"main":"./main.js"}"#,+        vec![+            ("package/", "main.js"),+            ("package/index", "index.js"), ("package/index.js", "index.js"),+            ("package/main", "main.js"), ("package/main.js", "main.js"),+            ("package/package", "package.json"), ("package/package.json", "package.json"),+            ("package/subpath", "subpath.js"), ("package/subpath.js", "subpath.js")+        ]+    )]+    #[case::dynamic_roots(+        Pattern::Alternatives(vec![+            rcstr!("package").into(),+            Pattern::concat([rcstr!("package/").into(), Pattern::Dynamic]),+        ]),+        r#"{"main":"./main.js"}"#,+        vec![+            ("package", "main.js"), ("package/", "main.js"),+            ("package/index", "index.js"), ("package/index.js", "index.js"),+            ("package/main", "main.js"), ("package/main.js", "main.js"),+            ("package/package", "package.json"), ("package/package.json", "package.json"),+            ("package/subpath", "subpath.js"), ("package/subpath.js", "subpath.js")+        ]+    )]+    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]+    async fn test_package_root_request_keys(+        #[case] pattern: Pattern,+        #[case] package_json: &str,+        #[case] expected: Vec<(&str, &str)>,+    ) {+        let scratch = tempfile::tempdir().unwrap();+        let package_path = scratch.path().join("node_modules/package");+        create_dir_all(&package_path).unwrap();+        std::fs::write(package_path.join("package.json"), package_json).unwrap();+        for name in ["main.js", "index.js", "subpath.js"] {+            std::fs::write(package_path.join(name), "").unwrap();+        }++        let path = RcStr::from(scratch.path().to_str().unwrap());+        let mut expected: Vec<(String, String)> = expected+            .into_iter()+            .map(|(key, file)| (key.to_string(), format!("node_modules/package/{file}")))+            .collect();+        expected.sort();+        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(+            BackendOptions::default(),+            noop_backing_storage(),+        ));++        #[turbo_tasks::value(transparent)]+        struct PackageRequestOutput(Vec<(String, String)>);++        #[turbo_tasks::function(operation, root)]+        async fn resolve_package_request_operation(+            path: RcStr,+            pattern: Pattern,+        ) -> Result<Vc<PackageRequestOutput>> {+            let fs = DiskFileSystem::new(rcstr!("temp"), Vc::cell(path));+            let lookup_path = fs.root().owned().await?;+            let result = resolve(+                lookup_path,+                ReferenceType::CommonJs(Default::default()),+                Request::parse(pattern),+                node_cjs_resolve_options(),+            )+            .await?;+            let mut entries = Vec::new();+            for (key, item) in &result.primary {+                let ResolveResultItem::Source(source) = item else {+                    panic!("expected a source, got {item:?}");+                };+                entries.push((key.to_string(), source.ident().await?.path.path.to_string()));+            }+            entries.sort();+            Ok(Vc::cell(entries))+        }++        tt.run_once(async move {+            let result = resolve_package_request_operation(path, pattern)+                .read_strongly_consistent()+                .await?;+            assert_eq!(&*result, &expected);+            anyhow::Ok(())+        })+        .await+        .unwrap();+    }+     /// Parameters for resolve_relative_request_test     struct TestParams<'a> {         files: Vec<&'a str>,
turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/package-root-trailing-slash/input/index.jsadded24 + / 0 −
@@ -0,0 +1,24 @@+function load(suffix) {+  return require('package-with-main/' + suffix)+}++it('resolves static package roots through the main field', () => {+  expect(require('package-with-main')).toBe('main')+  expect(require('package-with-main/')).toBe('main')+})++it('resolves a dynamic trailing-slash package root', () => {+  expect(load('')).toBe('main')+})++it('preserves dynamic subpath resolution', () => {+  expect(load('main.js')).toBe('main')+  expect(load('index.js')).toBe('index')+  expect(load('subpath.js')).toBe('subpath')+})++it('rejects missing dynamic subpaths', () => {+  expect(() => load('missing.js')).toThrow(+    "Cannot find module 'package-with-main/missing.js'"+  )+})
turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/package-root-trailing-slash/input/node_modules/package-with-main/index.jsadded1 + / 0 −
@@ -0,0 +1 @@+module.exports = 'index'
turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/package-root-trailing-slash/input/node_modules/package-with-main/main.jsadded1 + / 0 −
@@ -0,0 +1 @@+module.exports = 'main'
turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/package-root-trailing-slash/input/node_modules/package-with-main/package.jsonadded5 + / 0 −
@@ -0,0 +1,5 @@+{+  "name": "package-with-main",+  "version": "1.0.0",+  "main": "./main.js"+}
turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/package-root-trailing-slash/input/node_modules/package-with-main/subpath.jsadded1 + / 0 −
@@ -0,0 +1 @@+module.exports = 'subpath'