diff options
author | 2023-09-10 22:15:35 -0800 | |
---|---|---|
committer | 2023-09-10 23:15:35 -0700 | |
commit | 51d3d4382281f789f8175079ed426a63529eb3e7 (patch) | |
tree | 14f6fe77a1e3b300488e9343d8e9d54f64bde376 | |
parent | edea4f095a3bebf54f986c0fa038482316f4cde8 (diff) | |
download | bun-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.cpp | 51 | ||||
-rw-r--r-- | src/bun.js/module_loader.zig | 75 | ||||
-rw-r--r-- | src/bun.js/modules/ObjectModule.cpp | 77 | ||||
-rw-r--r-- | src/bun.js/modules/ObjectModule.h | 8 | ||||
-rw-r--r-- | src/bundler.zig | 150 | ||||
-rw-r--r-- | src/cache.zig | 7 | ||||
-rw-r--r-- | src/fs.zig | 9 | ||||
-rw-r--r-- | src/js/_codegen/build-modules.ts | 2 | ||||
-rw-r--r-- | src/js/builtins/Module.ts | 2 | ||||
-rw-r--r-- | src/js/out/ResolvedSourceTag.zig | 1 | ||||
-rw-r--r-- | src/js/out/SyntheticModuleType.h | 1 | ||||
-rw-r--r-- | src/js/out/WebCoreJSBuiltins.cpp | 4 | ||||
-rw-r--r-- | src/js_ast.zig | 8 | ||||
-rw-r--r-- | src/string_immutable.zig | 4 | ||||
-rw-r--r-- | test/transpiler/runtime-transpiler-fixture-duplicate-keys.json | 5 | ||||
-rw-r--r-- | test/transpiler/runtime-transpiler-json-fixture.json | 8 | ||||
-rw-r--r-- | test/transpiler/runtime-transpiler.test.ts | 130 | ||||
-rw-r--r-- | test/transpiler/tsconfig.is-just-a-number.json | 1 | ||||
-rw-r--r-- | test/transpiler/tsconfig.with-commas.json | 11 |
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 +} |