Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
50 changes: 44 additions & 6 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5244,18 +5244,22 @@ pub mod __gated_printer {
self.print_whitespacer(ws!(b"from "));
}

let irp = &self.import_record(s.import_record_index as usize).path.text;
self.print_import_record_path(
self.import_record(s.import_record_index as usize),
);
let record = self.import_record(s.import_record_index as usize);
let irp = &record.path.text;
let implies_json = Self::record_implies_json_type(record);
self.print_import_record_path(record);
if IS_BUN_PLATFORM && implies_json {
self.print_whitespacer(ws!(b" with { type: \"json\" }"));
}
self.print_semicolon_after_statement();

if Self::MAY_HAVE_MODULE_INFO {
if let Some(mi) = self.module_info() {
let irp_id = mi.str(irp);
use analyze_transpiled_module::FetchParameters as FP;
mi.request_module(
irp_id,
analyze_transpiled_module::FetchParameters::None,
if implies_json { FP::Json } else { FP::None },
);
if let Some(alias) = &s.alias {
let alias_id = mi.str(alias.original_name.slice());
Expand Down Expand Up @@ -5481,7 +5485,11 @@ pub mod __gated_printer {

self.print_whitespacer(ws!(b"} from "));
let irp = &import_record.path.text;
let implies_json = Self::record_implies_json_type(import_record);
self.print_import_record_path(import_record);
if IS_BUN_PLATFORM && implies_json {
self.print_whitespacer(ws!(b" with { type: \"json\" }"));
}
self.print_semicolon_after_statement();

if Self::MAY_HAVE_MODULE_INFO && self.module_info.is_some() {
Expand All @@ -5490,7 +5498,8 @@ pub mod __gated_printer {
let irp_id = {
let mi = self.module_info().expect("infallible: module_info enabled");
let id = mi.str(irp);
mi.request_module(id, analyze_transpiled_module::FetchParameters::None);
use analyze_transpiled_module::FetchParameters as FP;
mi.request_module(id, if implies_json { FP::Json } else { FP::None });
id
};
for item in slice_of(s.items).iter() {
Expand Down Expand Up @@ -5999,6 +6008,8 @@ pub mod __gated_printer {
self.print_whitespacer(ws!(b" with { type: \"md\" }"))
}
}
} else if Self::record_implies_json_type(record) {
self.print_whitespacer(ws!(b" with { type: \"json\" }"));
Comment thread
robobun marked this conversation as resolved.
}
}
self.print_semicolon_after_statement();
Expand Down Expand Up @@ -6039,6 +6050,8 @@ pub mod __gated_printer {
Loader::Json5 => FP::host_defined(mi.str(b"json5")),
Loader::Md => FP::host_defined(mi.str(b"md")),
}
} else if Self::record_implies_json_type(record) {
FP::Json
} else {
FP::None
}
Expand Down Expand Up @@ -6179,6 +6192,31 @@ pub mod __gated_printer {
self.print(b"module.exports");
}

/// No `with { type }` on a `.json` specifier: emit one so JSC's
/// `(specifier, ScriptFetchParameters::Type)` module-map key matches
/// the attributed form. Must agree with `specifierImpliesJsonType` in
/// `ZigGlobalObject.cpp` (both inspect the as-written specifier).
Comment thread
robobun marked this conversation as resolved.
fn record_implies_json_type(record: &ImportRecord) -> bool {
if record.loader.is_some() {
return false;
}
let text = record.path.text;
let end = text.iter().position(|&c| c == b'?').unwrap_or(text.len());
// `?raw` selects the text loader; a synthesized attribute would override it.
if &text[end..] == b"?raw" {
return false;
}
let path = &text[..end];
if !strings::has_suffix_comptime(path, b".json") {
return false;
}
// `loader_for_path` routes these to jsonc; a synthesized attribute would force strict JSON.
let filename = bun_paths::basename(path);
!(filename == b"package.json"
|| strings::has_prefix_comptime(filename, b"tsconfig.")
|| strings::has_prefix_comptime(filename, b"jsconfig."))
}
Comment thread
claude[bot] marked this conversation as resolved.

pub fn print_import_record_path(&mut self, import_record: &ImportRecord) {
if IS_JSON {
unreachable!();
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: attribute-less `.json` imports emit `with { type: "json" }` and
/// record `FetchParameters::Json` for the Bun target (#35914).
Comment thread
robobun marked this conversation as resolved.
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
28 changes: 28 additions & 0 deletions src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3650,6 +3650,31 @@ JSC::Identifier GlobalObject::moduleLoaderResolve(JSGlobalObject* jsGlobalObject
}
}

// Attribute-less dynamic import() whose specifier ends in `.json`: supply
// Type::JSON so JSC's (specifier, Type) module-map key matches the
// `with { type: "json" }` form and the printer's static-side normalization.
// Must inspect the as-written specifier, not the resolved path, so a specifier
// that resolves to a `.json` file but doesn't literally end in one stays
// consistent with the printer (which never resolves).
Comment thread
robobun marked this conversation as resolved.
static ALWAYS_INLINE bool specifierImpliesJsonType(StringView specifier)
{
size_t q = specifier.find('?');
if (q != notFound) {
// `?raw` selects the text loader; a synthesized `type: "json"` would override it.
if (specifier.substring(q) == "?raw"_s)
return false;
specifier = specifier.left(q);
}
if (!specifier.endsWith(".json"_s))
return false;
size_t slash = specifier.reverseFind('/');
size_t backslash = specifier.reverseFind('\\');
size_t sep = slash == notFound ? backslash : (backslash == notFound ? slash : std::max(slash, backslash));
StringView filename = sep == notFound ? specifier : specifier.substring(sep + 1);
// `loader_for_path` routes these to jsonc; a synthesized `type: "json"` would force strict JSON.
return filename != "package.json"_s && !filename.startsWith("tsconfig."_s) && !filename.startsWith("jsconfig."_s);
}

JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalObject,
JSModuleLoader*,
JSString* moduleNameValue,
Expand All @@ -3676,6 +3701,9 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO
auto moduleName = moduleNameValue->value(globalObject);
RETURN_IF_EXCEPTION(scope, nullptr);

if (!parameters && specifierImpliesJsonType(moduleName))
parameters = JSC::ScriptFetchParameters::create(JSC::ScriptFetchParameters::Type::JSON);

auto sourceURL = sourceOrigin.url();
String sourceOriginStringHolder;
int64_t referrerAsyncOrder = -1;
Expand Down
Loading
Loading