Skip to content
62 changes: 61 additions & 1 deletion src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ impl<'a> Options<'a> {
hasher.update(b"no_dce");
}

// `module_type` changes the parse result for byte-identical sources
// (e.g. `export` in an explicit-CommonJS file is a hard error).
hasher.update(&[self.module_type as u8]);

self.features.hash_for_runtime_transpiler(hasher);
}

Expand Down Expand Up @@ -1626,6 +1630,57 @@ impl<'a> Parser<'a> {
exports_kind = js_ast::ExportsKind::Cjs;
} else if p.esm_export_keyword.len > 0 || p.top_level_await_keyword.len > 0 {
exports_kind = js_ast::ExportsKind::Esm;
// .cjs / "type":"commonjs" are authoritative: Node rejects `export`
// and top-level `await` in those files as a SyntaxError.
// Runtime-only; the bundler has its own format resolution.
// TypeScript excluded: `export` in a CommonJS-typed .ts/.cts is
// idiomatic (tsc compiles it to `exports.x = ...`), so rejecting it
// would break the normal way to author CJS in TS.
if p.options.module_type == options::ModuleType::Cjs
&& !p.options.bundle
&& !p.options.ts
&& p.options.features.commonjs_at_runtime
{
let (range, what): (bun_ast::Range, &'static [u8]) = if p.esm_export_keyword.len > 0
{
(
p.esm_export_keyword,
b"Cannot use 'export' in a CommonJS module",
)
} else {
(
p.top_level_await_keyword,
b"Cannot use top-level 'await' in a CommonJS module",
)
};
Comment thread
robobun marked this conversation as resolved.
Resolved
let ext = p.source.path.name().ext;
let (why, how): (&'static [u8], &'static [u8]) = if ext == b".cjs" {
(
b"This file is CommonJS because of its \".cjs\" extension",
b"To use ES module syntax, change the file extension to '.mjs'",
)
} else {
(
b"This file is CommonJS because the nearest package.json sets \"type\": \"commonjs\"",
b"To use ES module syntax, change the file extension to '.mjs' or set \"type\": \"module\" in package.json",
)
};
p.log().add_range_error_with_notes(
Some(p.source),
range,
what,
Box::new([
bun_ast::Data {
text: std::borrow::Cow::Borrowed(why),
..Default::default()
},
bun_ast::Data {
text: std::borrow::Cow::Borrowed(how),
..Default::default()
Comment thread
robobun marked this conversation as resolved.
Resolved
},
]),
);
}
} else if uses_exports_ref || uses_module_ref || p.has_top_level_return || p.has_with_scope
{
exports_kind = js_ast::ExportsKind::Cjs;
Expand Down Expand Up @@ -1724,7 +1779,12 @@ impl<'a> Parser<'a> {
&& !p.has_top_level_return
&& !p.has_with_scope
);
// Use ESM if the file has ES module syntax (import)
// Use ESM if the file has ES module syntax (import).
// Node rejects `import` in a .cjs/"type":"commonjs" file
// the same way it rejects `export`/TLA (handled above),
// but import-only in explicit-CJS is deliberately still
// allowed here: it is far more prevalent in the wild than
// export/TLA, and rejecting it would be much more breaking.
exports_kind = if p.has_es_module_syntax {
js_ast::ExportsKind::Esm
} else {
Expand Down
4 changes: 3 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ bun_core::declare_scope!(cache, visible);
/// path reinstates the bug for any previously-cached TLA module (#30887).
/// Version 23: `jsx.runtime`/`jsx.development` participate in the features hash,
/// and tsconfig `"jsx": "react-jsx"` now emits the production runtime (#4227).
const EXPECTED_VERSION: u32 = 23;
/// Version 24: `module_type` is part of the features hash; `export`/TLA in an
/// explicit-CommonJS file is now a parse error.
const EXPECTED_VERSION: u32 = 24;

/// Source files smaller than this are not written to / read from the on-disk
/// transpiler cache. Originally 50 KiB, which excluded almost every file in a
Expand Down
19 changes: 14 additions & 5 deletions src/jsc/RuntimeTranspilerStore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ impl RuntimeTranspilerStore {
path: &Fs::Path<'_>,
referrer: String,
loader: Loader,
module_type: ModuleType,
package_json: Option<&PackageJSON>,
) -> *mut c_void {
// The path text is heap-duplicated here and freed in `reset_for_pool` via
Expand Down Expand Up @@ -352,6 +353,7 @@ impl RuntimeTranspilerStore {
vm,
log: bun_ast::Log::init(),
loader,
module_type,
promise: StrongOptional::create(JSValue::from_cell(promise), global_object),
poll_ref: KeepAlive::default(),
fetcher: Fetcher::File,
Expand Down Expand Up @@ -401,6 +403,7 @@ pub struct TranspilerJob {
pub non_threadsafe_input_specifier: OwnedString,
pub non_threadsafe_referrer: OwnedString,
pub loader: Loader,
pub module_type: ModuleType,
pub promise: StrongOptional,
// Note: struct is stored in a HiveArray and crosses to a worker thread;
// raw pointers/BackRefs are used (BACKREF — VM owns the
Expand Down Expand Up @@ -816,11 +819,7 @@ impl TranspilerJob {
&& vm_main_hash == hash
&& strings::eql_long(vm_main, path.text, false);

let module_type: ModuleType = match this_tag {
ResolvedSourceTag::PackageJsonTypeCommonjs => ModuleType::Cjs,
ResolvedSourceTag::PackageJsonTypeModule => ModuleType::Esm,
_ => ModuleType::Unknown,
};
let module_type = self.module_type;

let mut parse_options = ParseOptions {
arena: &arena,
Expand Down Expand Up @@ -960,6 +959,16 @@ impl TranspilerJob {
}
}

// The parser may emit errors into the log without `parse_maybe` itself
// returning `None` (e.g. "Cannot use 'export' in a CommonJS module").
// The sync path in `transpile_source_code_inner` checks this after the
// parse; do the same here so those errors surface instead of silently
// executing the output.
if transpiler.log().errors > 0 {
self.parse_error = Some(crate::CrateError::ParseError);
return;
}

// SAFETY: leaf scalar field read; see `vm` note above. Inlined
// `VirtualMachine::use_isolation_source_provider_cache` to avoid forming
// `&VirtualMachine`.
Expand Down
39 changes: 30 additions & 9 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3909,6 +3909,9 @@ struct LoaderResult<'a> {
specifier: &'a [u8],
/// Always `None` for non-JS-like loaders (not needed there).
package_json: Option<&'a bun_resolver::package_json::PackageJSON>,
/// `"type"` from the nearest ancestor package.json, including nameless ones
/// that `enclosing_package_json` skips. `Unknown` for non-JS-like loaders.
nearest_module_type: ModuleType,
}

/// `options.getLoaderAndVirtualSource` — high-tier body.
Expand Down Expand Up @@ -4032,15 +4035,34 @@ unsafe fn get_loader_and_virtual_source<'a>(
// package.json sniff for `.js`/`.ts` module-type.
let dir = path.name().dir;
let is_js_like = loader.map(|l| l.is_java_script_like()).unwrap_or(true);
let package_json = if is_js_like && bun_paths::is_absolute(dir) {
let (package_json, nearest_module_type) = if is_js_like && bun_paths::is_absolute(dir) {
// SAFETY: per fn contract — `transpiler.resolver` is a value field of
// the VM; `read_dir_info` is re-entrant on the JS thread.
match unsafe { (*jsc_vm).transpiler.resolver.read_dir_info(dir) } {
Ok(Some(dir_info)) => dir_info.package_json().or(dir_info.enclosing_package_json),
_ => None,
Ok(Some(dir_info)) => {
// `enclosing_package_json` skips nameless package.json files, but
// Node's "type" determination uses the nearest one regardless of
// name (e.g. puppeteer `lib/esm/package.json` = {"type":"module"}
// under a root `{"type":"commonjs"}`).
let mut walk = Some(dir_info);
let nearest_module_type = loop {
match walk {
Some(di) => match di.package_json() {
Some(pj) => break pj.module_type,
None => walk = di.get_parent(),
},
None => break ModuleType::Unknown,
}
};
(
dir_info.package_json().or(dir_info.enclosing_package_json),
nearest_module_type,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
)
}
_ => (None, ModuleType::Unknown),
}
} else {
None
(None, ModuleType::Unknown)
};

Ok(LoaderResult {
Expand All @@ -4050,6 +4072,7 @@ unsafe fn get_loader_and_virtual_source<'a>(
is_main,
specifier,
package_json,
nearest_module_type,
})
}

Expand Down Expand Up @@ -4269,11 +4292,8 @@ unsafe fn transpile_file(
}
// regex /\.[jt]s$/
if ext.len() == b".ts".len() && (ext == b".js" || ext == b".ts") {
// Use the package.json module type if it exists.
break 'brk lr
.package_json
.map(|pkg| pkg.module_type)
.unwrap_or(ModuleType::Unknown);
// Use the nearest package.json's "type" (including nameless ones).
break 'brk lr.nearest_module_type;
}
// For JSX/TSX and other extensions, let the file contents decide.
ModuleType::Unknown
Expand Down Expand Up @@ -4329,6 +4349,7 @@ unsafe fn transpile_file(
&lr.path,
(*referrer).dupe_ref(),
concurrent_loader,
module_type,
lr.package_json,
)
};
Expand Down
Loading
Loading