diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 3bc9e5acffe..91c5d85af88 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -1904,7 +1904,7 @@ pub const Resolver = struct { dir_info = source_dir_info; // this is the magic! - if (global_cache.canUse(any_node_modules_folder) and r.usePackageManager() and esm_ != null) { + if (global_cache.canUse(any_node_modules_folder) and r.usePackageManager() and esm_ != null and strings.isNPMPackageName(esm_.?.name)) { const esm = esm_.?.withAutoVersion(); load_module_from_cache: { // If the source directory doesn't have a node_modules directory, we can diff --git a/test/js/bun/resolve/resolve-autoinstall-invalid-name.test.ts b/test/js/bun/resolve/resolve-autoinstall-invalid-name.test.ts new file mode 100644 index 00000000000..ca1f8a323cb --- /dev/null +++ b/test/js/bun/resolve/resolve-autoinstall-invalid-name.test.ts @@ -0,0 +1,94 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +// When auto-install is enabled, the resolver must not attempt to install a +// package whose name is not a valid npm package name. Attempting to do so +// reentrantly ticks the JS event loop from inside the resolver, which has led +// to crashes when reached via mock.module() / vi.mock() / Bun.resolveSync() +// with arbitrary user-provided strings. +test("resolver does not attempt auto-install for invalid npm package names", async () => { + let requests = 0; + await using server = Bun.serve({ + port: 0, + fetch() { + requests++; + return new Response("{}", { status: 404, headers: { "content-type": "application/json" } }); + }, + }); + + using dir = tempDir("resolve-autoinstall-invalid-name", { + "index.js": ` + const specifiers = [ + "function f2() {\\n const v6 = new ArrayBuffer();\\n}", + "has spaces", + "(parens)", + "{braces}", + "line1\\nline2", + "foo\\tbar", + "a\\u0000b", + ]; + for (const s of specifiers) { + try { + Bun.resolveSync(s, import.meta.dir); + } catch {} + } + console.log("ok"); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "--install=force", "index.js"], + env: { + ...bunEnv, + BUN_CONFIG_REGISTRY: server.url.href, + NPM_CONFIG_REGISTRY: server.url.href, + }, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toContain("ok"); + expect(requests).toBe(0); + expect(exitCode).toBe(0); +}); + +test("resolver still attempts auto-install for valid npm package names", async () => { + let requests = 0; + await using server = Bun.serve({ + port: 0, + fetch() { + requests++; + return new Response("{}", { status: 404, headers: { "content-type": "application/json" } }); + }, + }); + + using dir = tempDir("resolve-autoinstall-valid-name", { + "index.js": ` + try { + Bun.resolveSync("some-package-that-does-not-exist-abc123", import.meta.dir); + } catch {} + console.log("ok"); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "--install=force", "index.js"], + env: { + ...bunEnv, + BUN_CONFIG_REGISTRY: server.url.href, + NPM_CONFIG_REGISTRY: server.url.href, + }, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toContain("ok"); + expect(requests).toBeGreaterThan(0); + expect(exitCode).toBe(0); +}); diff --git a/test/js/bun/test/mock/mock-module-non-string.test.ts b/test/js/bun/test/mock/mock-module-non-string.test.ts index f7137f1ea64..13551aa8e5d 100644 --- a/test/js/bun/test/mock/mock-module-non-string.test.ts +++ b/test/js/bun/test/mock/mock-module-non-string.test.ts @@ -16,3 +16,23 @@ test("mock.module still works with valid string argument", async () => { const m = await import("mock-module-non-string-test-fixture"); expect(m.default).toBe(42); }); + +test("mock.module does not crash on specifiers that are not valid npm package names", () => { + const specifiers = [ + "function f2() {\n const v6 = new ArrayBuffer();\n v6.transferToFixedLength();\n}", + "foo\nbar", + "foo\rbar", + "has spaces", + "(parens)", + "{braces}", + "[brackets]", + ]; + for (const specifier of specifiers) { + expect(() => mock.module(specifier, () => ({ default: 1 }))).not.toThrow(); + } + for (const specifier of specifiers) { + // @ts-expect-error missing callback on purpose + expect(() => mock.module(specifier)).toThrow("mock(module, fn) requires a function"); + } + Bun.gc(true); +});