diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 6d17af66ef..4196d18a16 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -1632,18 +1632,32 @@ pub const Resolver = struct { } } + var is_self_reference = false; + // Find the parent directory with the "package.json" file var dir_info_package_json: ?*DirInfo = dir_info; while (dir_info_package_json != null and dir_info_package_json.?.package_json == null) dir_info_package_json = dir_info_package_json.?.getParent(); // Check for subpath imports: https://nodejs.org/api/packages.html#subpath-imports - if (dir_info_package_json != null and - strings.hasPrefixComptime(import_path, "#") and - !forbid_imports and - dir_info_package_json.?.package_json.?.imports != null) - { - return r.loadPackageImports(import_path, dir_info_package_json.?, kind, global_cache); + if (dir_info_package_json) |_dir_info_package_json| { + const package_json = _dir_info_package_json.package_json.?; + + if (strings.hasPrefixComptime(import_path, "#") and !forbid_imports and package_json.imports != null) { + return r.loadPackageImports(import_path, _dir_info_package_json, kind, global_cache); + } + + // https://nodejs.org/api/packages.html#packages_self_referencing_a_package_using_its_name + const package_name = ESModule.Package.parseName(import_path); + if (package_name) |_package_name| { + if (strings.eql(_package_name, package_json.name) and package_json.exports != null) { + if (r.debug_logs) |*debug| { + debug.addNoteFmt("\"{s}\" is a self-reference", .{import_path}); + } + dir_info = _dir_info_package_json; + is_self_reference = true; + } + } } const esm_ = ESModule.Package.parse(import_path, bufs(.esm_subpath)); @@ -1653,21 +1667,28 @@ pub const Resolver = struct { const use_node_module_resolver = global_cache != .force; // Then check for the package in any enclosing "node_modules" directories + // or in the package root directory if it's a self-reference while (use_node_module_resolver) { // Skip directories that are themselves called "node_modules", since we // don't ever want to search for "node_modules/node_modules" - if (dir_info.hasNodeModules()) { + if (dir_info.hasNodeModules() or is_self_reference) { any_node_modules_folder = true; - var _paths = [_]string{ dir_info.abs_path, "node_modules", import_path }; - const abs_path = r.fs.absBuf(&_paths, bufs(.node_modules_check)); + const abs_path = if (is_self_reference) + dir_info.abs_path + else brk: { + var _parts = [_]string{ dir_info.abs_path, "node_modules", import_path }; + break :brk r.fs.absBuf(&_parts, bufs(.node_modules_check)); + }; if (r.debug_logs) |*debug| { debug.addNoteFmt("Checking for a package in the directory \"{s}\"", .{abs_path}); } + const prev_extension_order = r.extension_order; defer r.extension_order = prev_extension_order; if (esm_) |esm| { const abs_package_path = brk: { + if (is_self_reference) break :brk dir_info.abs_path; var parts = [_]string{ dir_info.abs_path, "node_modules", esm.name }; break :brk r.fs.absBuf(&parts, bufs(.esm_absolute_package_path)); }; diff --git a/test/cli/run/self-reference.test.ts b/test/cli/run/self-reference.test.ts new file mode 100644 index 0000000000..fe272caae1 --- /dev/null +++ b/test/cli/run/self-reference.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, test } from "bun:test"; +import { spawn } from "bun"; +import { bunExe, tmpdirSync } from "harness"; +import { join } from "path"; +import { writeFile } from "fs/promises"; + +const testWord = "bunny"; +const testString = `${testWord} ${testWord}`; + +describe("bun", () => { + test("should resolve self-imports by name", async () => { + const tempDir = tmpdirSync(); + + for (const packageName of ["pkg", "@scope/pkg"]) { + // general check without exports + await writeFile( + join(tempDir, "package.json"), + JSON.stringify({ + name: packageName, + }), + ); + await writeFile(join(tempDir, "index.js"), `module.exports.testWord = "${testWord}";`); + await writeFile( + join(tempDir, "other.js"), + `const pkg = require("${packageName}");\nimport pkg2 from "${packageName}"\nconsole.log(pkg.testWord,pkg2.testWord);`, + ); + + let subprocess = spawn({ + cmd: [bunExe(), "run", "other.js"], + cwd: tempDir, + stdout: "pipe", + }); + let out = await new Response(subprocess.stdout).text(); + expect(out).not.toContain(testString); + + // should not resolve not exported files + await writeFile( + join(tempDir, "package.json"), + JSON.stringify({ + name: packageName, + exports: { "./index.js": "./index.js" }, + }), + ); + + subprocess = spawn({ + cmd: [bunExe(), "run", "other.js"], + cwd: tempDir, + stdout: "pipe", + }); + out = await new Response(subprocess.stdout).text(); + expect(out).not.toContain(testString); + + // should resolve exported files + await writeFile( + join(tempDir, "other.js"), + `const pkg = require("${packageName}/index.js");\nimport pkg2 from "${packageName}/index.js"\nconsole.log(pkg.testWord,pkg2.testWord);`, + ); + + subprocess = spawn({ + cmd: [bunExe(), "run", "other.js"], + cwd: tempDir, + stdout: "pipe", + }); + out = await new Response(subprocess.stdout).text(); + expect(out).toContain(testString); + } + }); +});