aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2023-09-10 22:15:35 -0800
committerGravatar GitHub <noreply@github.com> 2023-09-10 23:15:35 -0700
commit51d3d4382281f789f8175079ed426a63529eb3e7 (patch)
tree14f6fe77a1e3b300488e9343d8e9d54f64bde376
parentedea4f095a3bebf54f986c0fa038482316f4cde8 (diff)
downloadbun-51d3d4382281f789f8175079ed426a63529eb3e7.tar.gz
bun-51d3d4382281f789f8175079ed426a63529eb3e7.tar.zst
bun-51d3d4382281f789f8175079ed426a63529eb3e7.zip
Support named imports for json & toml files at runtime (#4783)
* Support named exports in json imports * Support named imports for `*.json` files * Remove stale comments * Don't export arrays as non-default * Add test for default exports * Don't break webpack --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r--src/bun.js/bindings/ModuleLoader.cpp51
-rw-r--r--src/bun.js/module_loader.zig75
-rw-r--r--src/bun.js/modules/ObjectModule.cpp77
-rw-r--r--src/bun.js/modules/ObjectModule.h8
-rw-r--r--src/bundler.zig150
-rw-r--r--src/cache.zig7
-rw-r--r--src/fs.zig9
-rw-r--r--src/js/_codegen/build-modules.ts2
-rw-r--r--src/js/builtins/Module.ts2
-rw-r--r--src/js/out/ResolvedSourceTag.zig1
-rw-r--r--src/js/out/SyntheticModuleType.h1
-rw-r--r--src/js/out/WebCoreJSBuiltins.cpp4
-rw-r--r--src/js_ast.zig8
-rw-r--r--src/string_immutable.zig4
-rw-r--r--test/transpiler/runtime-transpiler-fixture-duplicate-keys.json5
-rw-r--r--test/transpiler/runtime-transpiler-json-fixture.json8
-rw-r--r--test/transpiler/runtime-transpiler.test.ts130
-rw-r--r--test/transpiler/tsconfig.is-just-a-number.json1
-rw-r--r--test/transpiler/tsconfig.with-commas.json11
19 files changed, 481 insertions, 73 deletions
diff --git a/src/bun.js/bindings/ModuleLoader.cpp b/src/bun.js/bindings/ModuleLoader.cpp
index 4e2de9294..252c446b1 100644
--- a/src/bun.js/bindings/ModuleLoader.cpp
+++ b/src/bun.js/bindings/ModuleLoader.cpp
@@ -516,6 +516,29 @@ JSValue fetchCommonJSModule(
RELEASE_AND_RETURN(scope, {});
}
+ // The JSONForObjectLoader tag is source code returned from Bun that needs
+ // to go through the JSON parser in JSC.
+ //
+ // We don't use JSON.parse directly in JS because we want the top-level keys of the JSON
+ // object to be accessible as named imports.
+ //
+ // We don't use Bun's JSON parser because JSON.parse is faster and
+ // handles stack overflow better.
+ //
+ // When parsing tsconfig.*.json or jsconfig.*.json, we go through Bun's JSON
+ // parser instead to support comments and trailing commas.
+ if (res->result.value.tag == SyntheticModuleType::JSONForObjectLoader) {
+ JSC::JSValue value = JSC::JSONParse(globalObject, Bun::toWTFString(res->result.value.source_code));
+ if (!value) {
+ JSC::throwException(globalObject, scope, JSC::createSyntaxError(globalObject, "Failed to parse JSON"_s));
+ RELEASE_AND_RETURN(scope, {});
+ }
+
+ target->putDirect(vm, WebCore::clientData(vm)->builtinNames().exportsPublicName(), value, value.isCell() && value.isCallable() ? JSC::PropertyAttribute::Function | 0 : 0);
+ target->hasEvaluated = true;
+ RELEASE_AND_RETURN(scope, target);
+ }
+
auto&& provider = Zig::SourceProvider::create(globalObject, res->result.value);
globalObject->moduleLoader()->provideFetch(globalObject, specifierValue, JSC::SourceCode(provider));
RETURN_IF_EXCEPTION(scope, {});
@@ -647,6 +670,34 @@ static JSValue fetchESMSourceCode(
return reject(exception);
}
+ // The JSONForObjectLoader tag is source code returned from Bun that needs
+ // to go through the JSON parser in JSC.
+ //
+ // We don't use JSON.parse directly in JS because we want the top-level keys of the JSON
+ // object to be accessible as named imports.
+ //
+ // We don't use Bun's JSON parser because JSON.parse is faster and
+ // handles stack overflow better.
+ //
+ // When parsing tsconfig.*.json or jsconfig.*.json, we go through Bun's JSON
+ // parser instead to support comments and trailing commas.
+ if (res->result.value.tag == SyntheticModuleType::JSONForObjectLoader) {
+ JSC::JSValue value = JSC::JSONParse(globalObject, Bun::toWTFString(res->result.value.source_code));
+ if (!value) {
+ return reject(JSC::JSValue(JSC::createSyntaxError(globalObject, "Failed to parse JSON"_s)));
+ }
+
+ // JSON can become strings, null, numbers, booleans so we must handle "export default 123"
+ auto function = generateJSValueModuleSourceCode(
+ globalObject,
+ value);
+ auto source = JSC::SourceCode(
+ JSC::SyntheticSourceProvider::create(WTFMove(function),
+ JSC::SourceOrigin(), Bun::toWTFString(*specifier)));
+ JSC::ensureStillAliveHere(value);
+ return rejectOrResolve(JSSourceCode::create(globalObject->vm(), WTFMove(source)));
+ }
+
auto&& provider = Zig::SourceProvider::create(globalObject, res->result.value);
return rejectOrResolve(JSC::JSSourceCode::create(vm, JSC::SourceCode(provider)));
}
diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig
index f5e5cde72..cf86cb460 100644
--- a/src/bun.js/module_loader.zig
+++ b/src/bun.js/module_loader.zig
@@ -482,10 +482,6 @@ pub const RuntimeTranspilerStore = struct {
.source_code = bun.String.createLatin1(parse_result.source.contents),
.specifier = String.create(specifier),
.source_url = ZigString.init(path.text),
- // // TODO: change hash to a bitfield
- // .hash = 1,
-
- // having JSC own the memory causes crashes
.hash = 0,
};
return;
@@ -1290,10 +1286,7 @@ pub const ModuleLoader = struct {
std.math.maxInt(u32)
else
0,
- // // TODO: change hash to a bitfield
- // .hash = 1,
- // having JSC own the memory causes crashes
.hash = 0,
};
}
@@ -1466,31 +1459,36 @@ pub const ModuleLoader = struct {
}
}
- var parse_result = jsc_vm.bundler.parseMaybeReturnFileOnly(
- parse_options,
- null,
- disable_transpilying,
- ) orelse {
- if (comptime !disable_transpilying) {
- if (jsc_vm.isWatcherEnabled()) {
- if (input_file_fd != 0) {
- if (jsc_vm.bun_watcher != null and !is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) {
- should_close_input_file_fd = false;
- jsc_vm.bun_watcher.?.addFile(
- input_file_fd,
- path.text,
- hash,
- loader,
- 0,
- package_json,
- true,
- ) catch {};
+ var parse_result = switch (disable_transpilying or
+ (loader == .json and !path.isJSONCFile())) {
+ inline else => |return_file_only| brk: {
+ break :brk jsc_vm.bundler.parseMaybeReturnFileOnly(
+ parse_options,
+ null,
+ return_file_only,
+ ) orelse {
+ if (comptime !disable_transpilying) {
+ if (jsc_vm.isWatcherEnabled()) {
+ if (input_file_fd != 0) {
+ if (jsc_vm.bun_watcher != null and !is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) {
+ should_close_input_file_fd = false;
+ jsc_vm.bun_watcher.?.addFile(
+ input_file_fd,
+ path.text,
+ hash,
+ loader,
+ 0,
+ package_json,
+ true,
+ ) catch {};
+ }
+ }
}
}
- }
- }
- return error.ParseError;
+ return error.ParseError;
+ };
+ },
};
if (parse_result.loader == .wasm) {
@@ -1535,6 +1533,18 @@ pub const ModuleLoader = struct {
return error.ParseError;
}
+ if (loader == .json and !path.isJSONCFile()) {
+ return ResolvedSource{
+ .allocator = null,
+ .source_code = bun.String.create(parse_result.source.contents),
+ .specifier = input_specifier,
+ .source_url = ZigString.init(path.text),
+
+ .hash = 0,
+ .tag = ResolvedSource.Tag.json_for_object_loader,
+ };
+ }
+
if (comptime disable_transpilying) {
return ResolvedSource{
.allocator = null,
@@ -1555,10 +1565,7 @@ pub const ModuleLoader = struct {
.source_code = bun.String.createLatin1(parse_result.source.contents),
.specifier = input_specifier,
.source_url = ZigString.init(path.text),
- // // TODO: change hash to a bitfield
- // .hash = 1,
- // having JSC own the memory causes crashes
.hash = 0,
};
}
@@ -1692,10 +1699,6 @@ pub const ModuleLoader = struct {
std.math.maxInt(u32)
else
0,
- // // TODO: change hash to a bitfield
- // .hash = 1,
-
- // having JSC own the memory causes crashes
.hash = 0,
.tag = tag,
diff --git a/src/bun.js/modules/ObjectModule.cpp b/src/bun.js/modules/ObjectModule.cpp
index 4272bec4e..ebb4af32e 100644
--- a/src/bun.js/modules/ObjectModule.cpp
+++ b/src/bun.js/modules/ObjectModule.cpp
@@ -5,7 +5,41 @@ JSC::SyntheticSourceProvider::SyntheticSourceGenerator
generateObjectModuleSourceCode(JSC::JSGlobalObject *globalObject,
JSC::JSObject *object) {
JSC::VM &vm = globalObject->vm();
+ gcProtectNullTolerant(object);
+ return [object](JSC::JSGlobalObject *lexicalGlobalObject,
+ JSC::Identifier moduleKey,
+ Vector<JSC::Identifier, 4> &exportNames,
+ JSC::MarkedArgumentBuffer &exportValues) -> void {
+ JSC::VM &vm = lexicalGlobalObject->vm();
+ GlobalObject *globalObject =
+ reinterpret_cast<GlobalObject *>(lexicalGlobalObject);
+ JSC::EnsureStillAliveScope stillAlive(object);
+
+ PropertyNameArray properties(vm, PropertyNameMode::Strings,
+ PrivateSymbolMode::Exclude);
+ object->getPropertyNames(globalObject, properties,
+ DontEnumPropertiesMode::Exclude);
+ gcUnprotectNullTolerant(object);
+ for (auto &entry : properties) {
+ exportNames.append(entry);
+
+ auto scope = DECLARE_CATCH_SCOPE(vm);
+ JSValue value = object->get(globalObject, entry);
+ if (scope.exception()) {
+ scope.clearException();
+ value = jsUndefined();
+ }
+ exportValues.append(value);
+ }
+ };
+}
+
+JSC::SyntheticSourceProvider::SyntheticSourceGenerator
+generateObjectModuleSourceCodeForJSON(JSC::JSGlobalObject *globalObject,
+ JSC::JSObject *object) {
+ JSC::VM &vm = globalObject->vm();
+ gcProtectNullTolerant(object);
return [object](JSC::JSGlobalObject *lexicalGlobalObject,
JSC::Identifier moduleKey,
Vector<JSC::Identifier, 4> &exportNames,
@@ -19,11 +53,52 @@ generateObjectModuleSourceCode(JSC::JSGlobalObject *globalObject,
PrivateSymbolMode::Exclude);
object->getPropertyNames(globalObject, properties,
DontEnumPropertiesMode::Exclude);
+ gcUnprotectNullTolerant(object);
for (auto &entry : properties) {
+ if (entry == vm.propertyNames->defaultKeyword) {
+ continue;
+ }
+
exportNames.append(entry);
- exportValues.append(object->get(globalObject, entry));
+
+ auto scope = DECLARE_CATCH_SCOPE(vm);
+ JSValue value = object->get(globalObject, entry);
+ if (scope.exception()) {
+ scope.clearException();
+ value = jsUndefined();
+ }
+ exportValues.append(value);
}
+
+ exportNames.append(vm.propertyNames->defaultKeyword);
+ exportValues.append(object);
+ };
+}
+
+JSC::SyntheticSourceProvider::SyntheticSourceGenerator
+generateJSValueModuleSourceCode(JSC::JSGlobalObject *globalObject,
+ JSC::JSValue value) {
+
+ if (value.isObject() && !JSC::isJSArray(value)) {
+ return generateObjectModuleSourceCodeForJSON(globalObject,
+ value.getObject());
+ }
+
+ if (value.isCell())
+ gcProtectNullTolerant(value.asCell());
+ return [value](JSC::JSGlobalObject *lexicalGlobalObject,
+ JSC::Identifier moduleKey,
+ Vector<JSC::Identifier, 4> &exportNames,
+ JSC::MarkedArgumentBuffer &exportValues) -> void {
+ JSC::VM &vm = lexicalGlobalObject->vm();
+ GlobalObject *globalObject =
+ reinterpret_cast<GlobalObject *>(lexicalGlobalObject);
+ exportNames.append(vm.propertyNames->defaultKeyword);
+ exportValues.append(value);
+
+ if (value.isCell())
+ gcUnprotectNullTolerant(value.asCell());
};
}
} // namespace Zig \ No newline at end of file
diff --git a/src/bun.js/modules/ObjectModule.h b/src/bun.js/modules/ObjectModule.h
index 25a3e5130..296e8000a 100644
--- a/src/bun.js/modules/ObjectModule.h
+++ b/src/bun.js/modules/ObjectModule.h
@@ -8,4 +8,12 @@ JSC::SyntheticSourceProvider::SyntheticSourceGenerator
generateObjectModuleSourceCode(JSC::JSGlobalObject *globalObject,
JSC::JSObject *object);
+JSC::SyntheticSourceProvider::SyntheticSourceGenerator
+generateObjectModuleSourceCodeForJSON(JSC::JSGlobalObject *globalObject,
+ JSC::JSObject *object);
+
+JSC::SyntheticSourceProvider::SyntheticSourceGenerator
+generateJSValueModuleSourceCode(JSC::JSGlobalObject *globalObject,
+ JSC::JSValue value);
+
} // namespace Zig \ No newline at end of file
diff --git a/src/bundler.zig b/src/bundler.zig
index 2848b4b8f..66443d165 100644
--- a/src/bundler.zig
+++ b/src/bundler.zig
@@ -1375,43 +1375,127 @@ pub const Bundler = struct {
};
},
// TODO: use lazy export AST
- .json => {
- var expr = json_parser.ParseJSON(&source, bundler.log, allocator) catch return null;
- var stmt = js_ast.Stmt.alloc(js_ast.S.ExportDefault, js_ast.S.ExportDefault{
- .value = js_ast.StmtOrExpr{ .expr = expr },
- .default_name = js_ast.LocRef{
- .loc = logger.Loc{},
- .ref = Ref.None,
- },
- }, logger.Loc{ .start = 0 });
- var stmts = allocator.alloc(js_ast.Stmt, 1) catch unreachable;
- stmts[0] = stmt;
- var parts = allocator.alloc(js_ast.Part, 1) catch unreachable;
- parts[0] = js_ast.Part{ .stmts = stmts };
+ inline .toml, .json => |kind| {
+ var expr = if (kind == .json)
+ // We allow importing tsconfig.*.json or jsconfig.*.json with comments
+ // These files implicitly become JSONC files, which aligns with the behavior of text editors.
+ if (source.path.isJSONCFile())
+ json_parser.ParseTSConfig(&source, bundler.log, allocator) catch return null
+ else
+ json_parser.ParseJSON(&source, bundler.log, allocator) catch return null
+ else if (kind == .toml)
+ TOML.parse(&source, bundler.log, allocator) catch return null
+ else
+ @compileError("unreachable");
+
+ var symbols: []js_ast.Symbol = &.{};
+
+ var parts = brk: {
+ if (expr.data == .e_object) {
+ var properties: []js_ast.G.Property = expr.data.e_object.properties.slice();
+ if (properties.len > 0) {
+ var stmts = allocator.alloc(js_ast.Stmt, 3) catch return null;
+ var decls = allocator.alloc(js_ast.G.Decl, properties.len) catch return null;
+ symbols = allocator.alloc(js_ast.Symbol, properties.len) catch return null;
+ var export_clauses = allocator.alloc(js_ast.ClauseItem, properties.len) catch return null;
+ var duplicate_key_checker = bun.StringHashMap(u32).init(allocator);
+ defer duplicate_key_checker.deinit();
+ var count: usize = 0;
+ for (properties, decls, symbols, 0..) |*prop, *decl, *symbol, i| {
+ const name = prop.key.?.data.e_string.slice(allocator);
+ // Do not make named exports for "default" exports
+ if (strings.eqlComptime(name, "default"))
+ continue;
+
+ var visited = duplicate_key_checker.getOrPut(name) catch continue;
+ if (visited.found_existing) {
+ decls[visited.value_ptr.*].value = prop.value.?;
+ continue;
+ }
+ visited.value_ptr.* = @truncate(i);
+
+ symbol.* = js_ast.Symbol{
+ .original_name = MutableString.ensureValidIdentifier(name, allocator) catch return null,
+ };
+
+ const ref = Ref.init(@truncate(i), 0, false);
+ decl.* = js_ast.G.Decl{
+ .binding = js_ast.Binding.alloc(allocator, js_ast.B.Identifier{
+ .ref = ref,
+ }, prop.key.?.loc),
+ .value = prop.value.?,
+ };
+ export_clauses[i] = js_ast.ClauseItem{
+ .name = .{
+ .ref = ref,
+ .loc = prop.key.?.loc,
+ },
+ .alias = name,
+ .alias_loc = prop.key.?.loc,
+ };
+ prop.value = js_ast.Expr.initIdentifier(ref, prop.value.?.loc);
+ count += 1;
+ }
+
+ stmts[0] = js_ast.Stmt.alloc(
+ js_ast.S.Local,
+ js_ast.S.Local{
+ .decls = js_ast.G.Decl.List.init(decls[0..count]),
+ .kind = .k_var,
+ },
+ logger.Loc{
+ .start = 0,
+ },
+ );
+ stmts[1] = js_ast.Stmt.alloc(
+ js_ast.S.ExportClause,
+ js_ast.S.ExportClause{
+ .items = export_clauses[0..count],
+ },
+ logger.Loc{
+ .start = 0,
+ },
+ );
+ stmts[2] = js_ast.Stmt.alloc(
+ js_ast.S.ExportDefault,
+ js_ast.S.ExportDefault{
+ .value = js_ast.StmtOrExpr{ .expr = expr },
+ .default_name = js_ast.LocRef{
+ .loc = logger.Loc{},
+ .ref = Ref.None,
+ },
+ },
+ logger.Loc{
+ .start = 0,
+ },
+ );
+
+ var parts_ = allocator.alloc(js_ast.Part, 1) catch unreachable;
+ parts_[0] = js_ast.Part{ .stmts = stmts };
+ break :brk parts_;
+ }
+ }
- return ParseResult{
- .ast = js_ast.Ast.initTest(parts),
- .source = source,
- .loader = loader,
- .input_fd = input_fd,
+ {
+ var stmts = allocator.alloc(js_ast.Stmt, 1) catch unreachable;
+ stmts[0] = js_ast.Stmt.alloc(js_ast.S.ExportDefault, js_ast.S.ExportDefault{
+ .value = js_ast.StmtOrExpr{ .expr = expr },
+ .default_name = js_ast.LocRef{
+ .loc = logger.Loc{},
+ .ref = Ref.None,
+ },
+ }, logger.Loc{ .start = 0 });
+
+ var parts_ = allocator.alloc(js_ast.Part, 1) catch unreachable;
+ parts_[0] = js_ast.Part{ .stmts = stmts };
+ break :brk parts_;
+ }
};
- },
- .toml => {
- var expr = TOML.parse(&source, bundler.log, allocator) catch return null;
- var stmt = js_ast.Stmt.alloc(js_ast.S.ExportDefault, js_ast.S.ExportDefault{
- .value = js_ast.StmtOrExpr{ .expr = expr },
- .default_name = js_ast.LocRef{
- .loc = logger.Loc{},
- .ref = Ref.None,
- },
- }, logger.Loc{ .start = 0 });
- var stmts = allocator.alloc(js_ast.Stmt, 1) catch unreachable;
- stmts[0] = stmt;
- var parts = allocator.alloc(js_ast.Part, 1) catch unreachable;
- parts[0] = js_ast.Part{ .stmts = stmts };
+ var ast = js_ast.Ast.fromParts(parts);
+ ast.symbols = js_ast.Symbol.List.init(symbols);
return ParseResult{
- .ast = js_ast.Ast.initTest(parts),
+ .ast = ast,
.source = source,
.loader = loader,
.input_fd = input_fd,
diff --git a/src/cache.zig b/src/cache.zig
index 4585860fa..56d91618d 100644
--- a/src/cache.zig
+++ b/src/cache.zig
@@ -294,6 +294,13 @@ pub const Json = struct {
};
}
pub fn parseJSON(cache: *@This(), log: *logger.Log, source: logger.Source, allocator: std.mem.Allocator) anyerror!?js_ast.Expr {
+ // tsconfig.* and jsconfig.* files are JSON files, but they are not valid JSON files.
+ // They are JSON files with comments and trailing commas.
+ // Sometimes tooling expects this to work.
+ if (source.path.isJSONCFile()) {
+ return try parse(cache, log, source, allocator, json_parser.ParseTSConfig);
+ }
+
return try parse(cache, log, source, allocator, json_parser.ParseJSON);
}
diff --git a/src/fs.zig b/src/fs.zig
index 2f59e09e2..d062bd0c3 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -1504,6 +1504,15 @@ pub const Path = struct {
return strings.eqlComptime(this.namespace, "macro");
}
+ pub fn isJSONCFile(this: *const Path) bool {
+ const str = this.name.filename;
+ if (!(strings.hasPrefixComptime(str, "tsconfig.") or strings.hasPrefixComptime(str, "jsconfig."))) {
+ return false;
+ }
+
+ return strings.hasSuffixComptime(str, ".json");
+ }
+
pub const PackageRelative = struct {
path: string,
name: string,
diff --git a/src/js/_codegen/build-modules.ts b/src/js/_codegen/build-modules.ts
index 5068748f7..3443db6f6 100644
--- a/src/js/_codegen/build-modules.ts
+++ b/src/js/_codegen/build-modules.ts
@@ -381,6 +381,7 @@ fs.writeFileSync(
object = 3,
file = 4,
esm = 5,
+ json_for_object_loader = 6,
// Built in modules are loaded through InternalModuleRegistry by numerical ID.
// In this enum are represented as \`(1 << 9) & id\`
@@ -403,6 +404,7 @@ fs.writeFileSync(
ObjectModule = 3,
File = 4,
ESM = 5,
+ JSONForObjectLoader = 6,
// Built in modules are loaded through InternalModuleRegistry by numerical ID.
// In this enum are represented as \`(1 << 9) & id\`
diff --git a/src/js/builtins/Module.ts b/src/js/builtins/Module.ts
index dfaf7acf2..aa08bc728 100644
--- a/src/js/builtins/Module.ts
+++ b/src/js/builtins/Module.ts
@@ -39,7 +39,7 @@ export function require(this: CommonJSModuleRecord, id: string) {
return existing.exports;
}
- if (id.endsWith(".json") || id.endsWith(".toml") || id.endsWith(".node")) {
+ if (id.endsWith(".node")) {
return $internalRequire(id);
}
diff --git a/src/js/out/ResolvedSourceTag.zig b/src/js/out/ResolvedSourceTag.zig
index 5bc228988..4d53d92cd 100644
--- a/src/js/out/ResolvedSourceTag.zig
+++ b/src/js/out/ResolvedSourceTag.zig
@@ -6,6 +6,7 @@ pub const ResolvedSourceTag = enum(u32) {
object = 3,
file = 4,
esm = 5,
+ json_for_object_loader = 6,
// Built in modules are loaded through InternalModuleRegistry by numerical ID.
// In this enum are represented as `(1 << 9) & id`
diff --git a/src/js/out/SyntheticModuleType.h b/src/js/out/SyntheticModuleType.h
index a3f850549..7338aec5f 100644
--- a/src/js/out/SyntheticModuleType.h
+++ b/src/js/out/SyntheticModuleType.h
@@ -5,6 +5,7 @@ enum SyntheticModuleType : uint32_t {
ObjectModule = 3,
File = 4,
ESM = 5,
+ JSONForObjectLoader = 6,
// Built in modules are loaded through InternalModuleRegistry by numerical ID.
// In this enum are represented as `(1 << 9) & id`
diff --git a/src/js/out/WebCoreJSBuiltins.cpp b/src/js/out/WebCoreJSBuiltins.cpp
index d767a3c39..a83ce2bd6 100644
--- a/src/js/out/WebCoreJSBuiltins.cpp
+++ b/src/js/out/WebCoreJSBuiltins.cpp
@@ -722,9 +722,9 @@ const char* const s_moduleMainCode = "(function (){\"use strict\";return @requir
const JSC::ConstructAbility s_moduleRequireCodeConstructAbility = JSC::ConstructAbility::CannotConstruct;
const JSC::ConstructorKind s_moduleRequireCodeConstructorKind = JSC::ConstructorKind::None;
const JSC::ImplementationVisibility s_moduleRequireCodeImplementationVisibility = JSC::ImplementationVisibility::Public;
-const int s_moduleRequireCodeLength = 769;
+const int s_moduleRequireCodeLength = 725;
static const JSC::Intrinsic s_moduleRequireCodeIntrinsic = JSC::NoIntrinsic;
-const char* const s_moduleRequireCode = "(function (id){\"use strict\";const existing=@requireMap.@get(id)||@requireMap.@get(id=@resolveSync(id,this.path,!1));if(existing)return @evaluateCommonJSModule(existing),existing.exports;if(id.endsWith(\".json\")||id.endsWith(\".toml\")||id.endsWith(\".node\"))return @internalRequire(id);const mod=@createCommonJSModule(id,{},!1);@requireMap.@set(id,mod);var out=this.@require(id,mod);if(out===-1){try{out=@requireESM(id)}catch(exception){throw @requireMap.@delete(id),exception}const esm=@Loader.registry.@get(id);if(esm\?.evaluated&&(esm.state\?\?0)>=@ModuleReady){const namespace=@Loader.getModuleNamespaceObject(esm.module);return mod.exports=namespace.__esModule\?namespace:Object.create(namespace,{__esModule:{value:!0}})}}return @evaluateCommonJSModule(mod),mod.exports})\n";
+const char* const s_moduleRequireCode = "(function (id){\"use strict\";const existing=@requireMap.@get(id)||@requireMap.@get(id=@resolveSync(id,this.path,!1));if(existing)return @evaluateCommonJSModule(existing),existing.exports;if(id.endsWith(\".node\"))return @internalRequire(id);const mod=@createCommonJSModule(id,{},!1);@requireMap.@set(id,mod);var out=this.@require(id,mod);if(out===-1){try{out=@requireESM(id)}catch(exception){throw @requireMap.@delete(id),exception}const esm=@Loader.registry.@get(id);if(esm\?.evaluated&&(esm.state\?\?0)>=@ModuleReady){const namespace=@Loader.getModuleNamespaceObject(esm.module);return mod.exports=namespace.__esModule\?namespace:Object.create(namespace,{__esModule:{value:!0}})}}return @evaluateCommonJSModule(mod),mod.exports})\n";
// requireResolve
const JSC::ConstructAbility s_moduleRequireResolveCodeConstructAbility = JSC::ConstructAbility::CannotConstruct;
diff --git a/src/js_ast.zig b/src/js_ast.zig
index 7c7fc3837..2e1daad1d 100644
--- a/src/js_ast.zig
+++ b/src/js_ast.zig
@@ -5998,6 +5998,14 @@ pub const Ast = struct {
pub const NamedExports = bun.StringArrayHashMap(NamedExport);
pub const ConstValuesMap = std.ArrayHashMapUnmanaged(Ref, Expr, RefHashCtx, false);
+ pub fn fromParts(parts: []Part) Ast {
+ return Ast{
+ .parts = Part.List.init(parts),
+ .allocator = bun.default_allocator,
+ .runtime_imports = .{},
+ };
+ }
+
pub fn initTest(parts: []Part) Ast {
return Ast{
.parts = Part.List.init(parts),
diff --git a/src/string_immutable.zig b/src/string_immutable.zig
index d2d71621f..03ba35e66 100644
--- a/src/string_immutable.zig
+++ b/src/string_immutable.zig
@@ -925,6 +925,10 @@ pub fn hasPrefixComptime(self: string, comptime alt: anytype) bool {
return self.len >= alt.len and eqlComptimeCheckLenWithType(u8, self[0..alt.len], alt, false);
}
+pub fn hasSuffixComptime(self: string, comptime alt: anytype) bool {
+ return self.len >= alt.len and eqlComptimeCheckLenWithType(u8, self[self.len - alt.len ..], alt, false);
+}
+
inline fn eqlComptimeCheckLenWithKnownType(comptime Type: type, a: []const Type, comptime b: []const Type, comptime check_len: bool) bool {
@setEvalBranchQuota(9999);
if (comptime check_len) {
diff --git a/test/transpiler/runtime-transpiler-fixture-duplicate-keys.json b/test/transpiler/runtime-transpiler-fixture-duplicate-keys.json
new file mode 100644
index 000000000..3093e5c80
--- /dev/null
+++ b/test/transpiler/runtime-transpiler-fixture-duplicate-keys.json
@@ -0,0 +1,5 @@
+{
+ "a": 1,
+ "b": 2,
+ "a": "4"
+}
diff --git a/test/transpiler/runtime-transpiler-json-fixture.json b/test/transpiler/runtime-transpiler-json-fixture.json
new file mode 100644
index 000000000..81517c7fe
--- /dev/null
+++ b/test/transpiler/runtime-transpiler-json-fixture.json
@@ -0,0 +1,8 @@
+{
+ "name": "Spiral 4v4 NS",
+ "description": "4v4 unshared map. 4 spawns in a spiral. Preferred to play with 4v4 NS.",
+ "version": "1.0",
+ "creator": "Grand Homie",
+ "players": [8, 8],
+ "default": { "a": 1 }
+}
diff --git a/test/transpiler/runtime-transpiler.test.ts b/test/transpiler/runtime-transpiler.test.ts
index c1881ee67..23e972be4 100644
--- a/test/transpiler/runtime-transpiler.test.ts
+++ b/test/transpiler/runtime-transpiler.test.ts
@@ -30,3 +30,133 @@ describe("// @bun", () => {
expect(exitCode).toBe(0);
});
});
+
+describe("json imports", () => {
+ test("require(*.json)", async () => {
+ const {
+ name,
+ description,
+ players,
+ version,
+ creator,
+ default: defaultExport,
+ ...other
+ } = require("./runtime-transpiler-json-fixture.json");
+ const obj = {
+ "name": "Spiral 4v4 NS",
+ "description": "4v4 unshared map. 4 spawns in a spiral. Preferred to play with 4v4 NS.",
+ "version": "1.0",
+ "creator": "Grand Homie",
+ "players": [8, 8],
+ default: { a: 1 },
+ };
+ expect({
+ name,
+ description,
+ players,
+ version,
+ creator,
+ default: { a: 1 },
+ }).toEqual(obj);
+ expect(other).toEqual({});
+
+ // This tests that importing and requiring when already in the cache keeps the state the same
+ {
+ const {
+ name,
+ description,
+ players,
+ version,
+ creator,
+ default: defaultExport,
+ // @ts-ignore
+ } = await import("./runtime-transpiler-json-fixture.json");
+ const obj = {
+ "name": "Spiral 4v4 NS",
+ "description": "4v4 unshared map. 4 spawns in a spiral. Preferred to play with 4v4 NS.",
+ "version": "1.0",
+ "creator": "Grand Homie",
+ "players": [8, 8],
+ default: { a: 1 },
+ };
+ expect({
+ name,
+ description,
+ players,
+ version,
+ creator,
+ default: { a: 1 },
+ }).toEqual(obj);
+ // They should be strictly equal
+ expect(defaultExport.players).toBe(players);
+ expect(defaultExport).toEqual(obj);
+ }
+
+ delete require.cache[require.resolve("./runtime-transpiler-json-fixture.json")];
+ });
+
+ test("import(*.json)", async () => {
+ const {
+ name,
+ description,
+ players,
+ version,
+ creator,
+ default: defaultExport,
+ // @ts-ignore
+ } = await import("./runtime-transpiler-json-fixture.json");
+ delete require.cache[require.resolve("./runtime-transpiler-json-fixture.json")];
+ const obj = {
+ "name": "Spiral 4v4 NS",
+ "description": "4v4 unshared map. 4 spawns in a spiral. Preferred to play with 4v4 NS.",
+ "version": "1.0",
+ "creator": "Grand Homie",
+ "players": [8, 8],
+ default: { a: 1 },
+ };
+ expect({
+ name,
+ description,
+ players,
+ version,
+ creator,
+ default: { a: 1 },
+ }).toEqual(obj);
+ // They should be strictly equal
+ expect(defaultExport.players).toBe(players);
+ expect(defaultExport).toEqual(obj);
+ });
+
+ test("should support comments in tsconfig.json", async () => {
+ // @ts-ignore
+ const { buildOptions, default: defaultExport } = await import("./tsconfig.with-commas.json");
+ delete require.cache[require.resolve("./tsconfig.with-commas.json")];
+ const obj = {
+ "buildOptions": {
+ "outDir": "dist",
+ "baseUrl": ".",
+ "paths": {
+ "src/*": ["src/*"],
+ },
+ },
+ };
+ expect({
+ buildOptions,
+ }).toEqual(obj);
+ // They should be strictly equal
+ expect(defaultExport.buildOptions).toBe(buildOptions);
+ expect(defaultExport).toEqual(obj);
+ });
+
+ test("should handle non-boecjts in tsconfig.json", async () => {
+ // @ts-ignore
+ const { default: num } = await import("./tsconfig.is-just-a-number.json");
+ delete require.cache[require.resolve("./tsconfig.is-just-a-number.json")];
+ expect(num).toBe(1);
+ });
+
+ test("should handle duplicate keys", async () => {
+ // @ts-ignore
+ expect((await import("./runtime-transpiler-fixture-duplicate-keys.json")).a).toBe("4");
+ });
+});
diff --git a/test/transpiler/tsconfig.is-just-a-number.json b/test/transpiler/tsconfig.is-just-a-number.json
new file mode 100644
index 000000000..d00491fd7
--- /dev/null
+++ b/test/transpiler/tsconfig.is-just-a-number.json
@@ -0,0 +1 @@
+1
diff --git a/test/transpiler/tsconfig.with-commas.json b/test/transpiler/tsconfig.with-commas.json
new file mode 100644
index 000000000..d30671e77
--- /dev/null
+++ b/test/transpiler/tsconfig.with-commas.json
@@ -0,0 +1,11 @@
+{
+ "buildOptions": {
+ "outDir": "dist",
+ "baseUrl": ".",
+ "paths": {
+ "src/*": ["src/*"]
+ }
+ }
+ // such comment
+ // very much
+}