Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 23 additions & 62 deletions src/bundler/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,12 +449,8 @@ impl Linker {
if strings::starts_with(import_record.path.text, b"node:") {
// if a module is not found here, it is not found at
// all so we can just disable it
had_resolve_errors = Self::when_module_not_found::<IS_BUN>(
self.log_mut(),
target,
import_record,
source,
)?;
had_resolve_errors =
Self::when_builtin_not_found(self.log_mut(), import_record, source);

if had_resolve_errors {
return Err(crate::Error::ResolveMessage);
Expand Down Expand Up @@ -528,14 +524,11 @@ impl Linker {
Ok(())
}

// Takes the disjoint pieces explicitly rather than `&mut self` plus
// overlapping sub-borrows of `result`.
fn when_module_not_found<const IS_BUN: bool>(
fn when_builtin_not_found(
log: &mut Log,
target: BundleTarget,
import_record: &mut ImportRecord,
source: &bun_ast::Source,
) -> crate::Result<bool> {
) -> bool {
if import_record
.flags
.contains(ImportRecordFlags::HANDLES_IMPORT_ERRORS)
Expand All @@ -544,61 +537,29 @@ impl Linker {
import_record
.flags
.insert(ImportRecordFlags::WAS_UNRESOLVED);
return Ok(false);
}

if IS_BUN {
// make these happen at runtime
if import_record.kind == ImportKind::Require
|| import_record.kind == ImportKind::RequireResolve
|| import_record.kind == ImportKind::Dynamic
{
return Ok(false);
}
return false;
}

if !import_record.path.text.is_empty() && resolver::is_package_path(import_record.path.text)
// make these happen at runtime
if import_record.kind == ImportKind::Require
|| import_record.kind == ImportKind::RequireResolve
|| import_record.kind == ImportKind::Dynamic
{
if target == BundleTarget::Browser && options::is_node_builtin(import_record.path.text)
{
log.add_resolve_error(
Some(source),
import_record.range,
format_args!(
"Could not resolve: \"{}\". Try setting --target=\"node\"",
bstr::BStr::new(import_record.path.text)
),
import_record.path.text,
import_record.kind,
bun_ast::Error::ModuleNotFound,
);
} else {
log.add_resolve_error(
Some(source),
import_record.range,
format_args!(
"Could not resolve: \"{}\". Maybe you need to \"bun install\"?",
bstr::BStr::new(import_record.path.text)
),
import_record.path.text,
import_record.kind,
bun_ast::Error::ModuleNotFound,
);
}
} else {
log.add_resolve_error(
Some(source),
import_record.range,
format_args!(
"Could not resolve: \"{}\"",
bstr::BStr::new(import_record.path.text)
),
import_record.path.text,
import_record.kind,
bun_ast::Error::ModuleNotFound,
);
return false;
}
Ok(true)

log.add_resolve_error(
Some(source),
import_record.range,
format_args!(
"No such built-in module: {}",
bstr::BStr::new(import_record.path.text)
),
import_record.path.text,
import_record.kind,
bun_ast::Error::ModuleNotFound,
);
true
}

pub(crate) fn generate_import_path(
Expand Down
120 changes: 119 additions & 1 deletion test/js/bun/resolve/resolve-error.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
import path from "node:path";

describe("ResolveMessage", () => {
Expand Down Expand Up @@ -283,3 +283,121 @@ describe.concurrent("tsconfig paths wildcard with overlapping prefix/suffix", ()
await run("xy*xy", "xy");
});
});

// A file containing a static `import "node:<unknown>"` is rejected at transpile
// time when it is transpiled on the JS thread (require(), plugin onLoad
// sources, import() with the concurrent transpiler disabled), and by the
// runtime resolve hook when the concurrent transpiler handled it (import()).
// Both must throw the same Node-shaped ERR_UNKNOWN_BUILTIN_MODULE error.
describe.concurrent("static import of an unknown node: builtin", () => {
const specifier = "node:this_builtin_does_not_exist";
const pick = `(e) => ({ code: e.code, specifier: e.specifier, importKind: e.importKind, message: e.message })`;
const expected = {
code: "ERR_UNKNOWN_BUILTIN_MODULE",
specifier,
importKind: "import-statement",
message: `No such built-in module: ${specifier}`,
};

async function loadFixtures(env: Record<string, string>) {
using dir = tempDir("unknown-builtin-import", {
// One file per load so no two loads share a module registry entry.
"import-a.ts": `import "${specifier}";\n`,
"import-b.ts": `import "${specifier}";\n`,
"reexport-a.ts": `export * from "${specifier}";\n`,
"reexport-b.ts": `export * from "${specifier}";\n`,
"main.ts": `
const pick = ${pick};
const errors = {};
try { require("./import-a.ts"); } catch (e) { errors["require import"] = pick(e); }
try { await import("./import-b.ts"); } catch (e) { errors["import() import"] = pick(e); }
try { require("./reexport-a.ts"); } catch (e) { errors["require reexport"] = pick(e); }
try { await import("./reexport-b.ts"); } catch (e) { errors["import() reexport"] = pick(e); }
console.log(JSON.stringify(errors));
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "main.ts"],
env: { ...bunEnv, ...env },
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
return JSON.parse(stdout);
}

const expectedForEveryLoad = {
"require import": expected,
"import() import": expected,
"require reexport": expected,
"import() reexport": expected,
};

it("require() and import() of the importing file throw the same error", async () => {
expect(await loadFixtures({})).toEqual(expectedForEveryLoad);
});

it("import() throws the same error when the importing file is transpiled on the JS thread", async () => {
expect(await loadFixtures({ BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER: "1" })).toEqual(expectedForEveryLoad);
});

it("source provided by a plugin onLoad callback throws the same error", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
Bun.plugin({
name: "virtual",
setup(build) {
build.onResolve({ filter: /.*/, namespace: "virtual" }, ({ path }) => ({ path, namespace: "virtual" }));
build.onLoad({ filter: /.*/, namespace: "virtual" }, () => ({
contents: 'import "${specifier}";',
loader: "ts",
}));
},
});
try {
await import("virtual:entry");
} catch (e) {
console.log(JSON.stringify((${pick})(e)));
}
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual(expected);
expect(exitCode).toBe(0);
});

it("entry point reports the same message at the import's position", async () => {
using dir = tempDir("unknown-builtin-entry", {
"entry.ts": `import "${specifier}";\n`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "entry.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(normalizeBunSnapshot(stderr, String(dir))).toMatchInlineSnapshot(`
"1 | import "node:this_builtin_does_not_exist";
^
error: No such built-in module: node:this_builtin_does_not_exist
at <dir>/entry.ts:1:8

Bun v<bun-version>"
`);
expect(exitCode).toBe(1);
});
});
Loading