const std = @import("std"); const Api = @import("../../api/schema.zig").Api; const FilesystemRouter = @import("../../router.zig"); const http = @import("../../http.zig"); const JavaScript = @import("../javascript.zig"); const QueryStringMap = @import("../../url.zig").QueryStringMap; const CombinedScanner = @import("../../url.zig").CombinedScanner; const bun = @import("../../global.zig"); const string = bun.string; const JSC = @import("../../jsc.zig"); const js = JSC.C; const WebCore = @import("../webcore/response.zig"); const Bundler = @import("../../bundler.zig"); const options = @import("../../options.zig"); const VirtualMachine = JavaScript.VirtualMachine; const ScriptSrcStream = std.io.FixedBufferStream([]u8); const ZigString = JSC.ZigString; const Fs = @import("../../fs.zig"); const Base = @import("../base.zig"); const getAllocator = Base.getAllocator; const JSObject = JSC.JSObject; const JSError = Base.JSError; const JSValue = JSC.JSValue; const JSGlobalObject = JSC.JSGlobalObject; const strings = @import("strings"); const NewClass = Base.NewClass; const To = Base.To; const Request = WebCore.Request; const d = Base.d; const FetchEvent = WebCore.FetchEvent; const MacroMap = @import("../../resolver/package_json.zig").MacroMap; const TSConfigJSON = @import("../../resolver/tsconfig_json.zig").TSConfigJSON; const PackageJSON = @import("../../resolver/package_json.zig").PackageJSON; const logger = @import("../../logger.zig"); const Loader = options.Loader; const Platform = options.Platform; const JSAst = @import("../../js_ast.zig"); const Transpiler = @This(); const JSParser = @import("../../js_parser.zig"); const JSPrinter = @import("../../js_printer.zig"); const ScanPassResult = JSParser.ScanPassResult; const Mimalloc = @import("../../mimalloc_arena.zig"); const Runtime = @import("../../runtime.zig").Runtime; const JSLexer = @import("../../js_lexer.zig"); const Expr = JSAst.Expr; bundler: Bundler.Bundler, arena: std.heap.ArenaAllocator, transpiler_options: TranspilerOptions, scan_pass_result: ScanPassResult, buffer_writer: ?JSPrinter.BufferWriter = null, pub const Class = NewClass( Transpiler, .{ .name = "Transpiler" }, .{ .scanImports = .{ .rfn = scanImports, }, .scan = .{ .rfn = scan, }, .transform = .{ .rfn = transform, }, .transformSync = .{ .rfn = transformSync, }, // .resolve = .{ // .rfn = resolve, // }, // .buildSync = .{ // .rfn = buildSync, // }, .finalize = finalize, }, .{}, ); pub const Constructor = JSC.NewConstructor( @This(), .{ .constructor = .{ .rfn = constructor }, }, .{}, ); const default_transform_options: Api.TransformOptions = brk: { var opts = std.mem.zeroes(Api.TransformOptions); opts.disable_hmr = true; opts.platform = Api.Platform.browser; opts.serve = false; break :brk opts; }; const TranspilerOptions = struct { transform: Api.TransformOptions = default_transform_options, default_loader: options.Loader = options.Loader.jsx, macro_map: MacroMap = MacroMap{}, tsconfig: ?*TSConfigJSON = null, tsconfig_buf: []const u8 = "", macros_buf: []const u8 = "", log: logger.Log, runtime: Runtime.Features = Runtime.Features{ .top_level_await = true }, tree_shaking: bool = false, trim_unused_imports: ?bool = null, }; // Mimalloc gets unstable if we try to move this to a different thread // threadlocal var transform_buffer: bun.MutableString = undefined; // threadlocal var transform_buffer_loaded: bool = false; // This is going to be hard to not leak pub const TransformTask = struct { input_code: ZigString = ZigString.init(""), protected_input_value: JSC.JSValue = @intToEnum(JSC.JSValue, 0), output_code: ZigString = ZigString.init(""), bundler: Bundler.Bundler = undefined, log: logger.Log, err: ?anyerror = null, macro_map: MacroMap = MacroMap{}, tsconfig: ?*TSConfigJSON = null, loader: Loader, global: *JSGlobalObject, replace_exports: Runtime.Features.ReplaceableExport.Map = .{}, pub const AsyncTransformTask = JSC.ConcurrentPromiseTask(TransformTask); pub const AsyncTransformEventLoopTask = AsyncTransformTask.EventLoopTask; pub fn create(transpiler: *Transpiler, protected_input_value: JSC.C.JSValueRef, globalThis: *JSGlobalObject, input_code: ZigString, loader: Loader) !*AsyncTransformTask { var transform_task = try bun.default_allocator.create(TransformTask); transform_task.* = .{ .input_code = input_code, .protected_input_value = if (protected_input_value != null) JSC.JSValue.fromRef(protected_input_value) else @intToEnum(JSC.JSValue, 0), .bundler = undefined, .global = globalThis, .macro_map = transpiler.transpiler_options.macro_map, .tsconfig = transpiler.transpiler_options.tsconfig, .log = logger.Log.init(bun.default_allocator), .loader = loader, .replace_exports = transpiler.transpiler_options.runtime.replace_exports, }; transform_task.bundler = transpiler.bundler; transform_task.bundler.linker.resolver = &transform_task.bundler.resolver; transform_task.bundler.setLog(&transform_task.log); transform_task.bundler.setAllocator(bun.default_allocator); return try AsyncTransformTask.createOnJSThread(bun.default_allocator, globalThis, transform_task); } pub fn run(this: *TransformTask) void { const name = this.loader.stdinName(); const source = logger.Source.initPathString(name, this.input_code.slice()); JSAst.Stmt.Data.Store.create(bun.default_allocator); JSAst.Expr.Data.Store.create(bun.default_allocator); var arena = Mimalloc.Arena.init() catch unreachable; const allocator = arena.allocator(); defer { JSAst.Stmt.Data.Store.reset(); JSAst.Expr.Data.Store.reset(); arena.deinit(); } this.bundler.setAllocator(allocator); const jsx = if (this.tsconfig != null) this.tsconfig.?.mergeJSX(this.bundler.options.jsx) else this.bundler.options.jsx; const parse_options = Bundler.Bundler.ParseOptions{ .allocator = allocator, .macro_remappings = this.macro_map, .dirname_fd = 0, .file_descriptor = null, .loader = this.loader, .jsx = jsx, .path = source.path, .virtual_source = &source, .replace_exports = this.replace_exports, // .allocator = this. }; const parse_result = this.bundler.parse(parse_options, null) orelse { this.err = error.ParseError; return; }; if (parse_result.empty) { this.output_code = ZigString.init(""); return; } var global_allocator = arena.backingAllocator(); var buffer_writer = JSPrinter.BufferWriter.init(global_allocator) catch |err| { this.err = err; return; }; buffer_writer.buffer.list.ensureTotalCapacity(global_allocator, 512) catch unreachable; buffer_writer.reset(); // defer { // transform_buffer = buffer_writer.buffer; // } var printer = JSPrinter.BufferPrinter.init(buffer_writer); const printed = this.bundler.print(parse_result, @TypeOf(&printer), &printer, .esm_ascii) catch |err| { this.err = err; return; }; if (printed > 0) { buffer_writer = printer.ctx; buffer_writer.buffer.list.items = buffer_writer.written; var output = JSC.ZigString.init(buffer_writer.written); output.mark(); this.output_code = output; } else { this.output_code = ZigString.init(""); } } pub fn then(this: *TransformTask, promise: *JSC.JSInternalPromise) void { if (this.log.hasAny() or this.err != null) { const error_value: JSValue = brk: { if (this.err) |err| { if (!this.log.hasAny()) { break :brk JSC.JSValue.fromRef(JSC.BuildError.create( this.global, bun.default_allocator, logger.Msg{ .data = logger.Data{ .text = std.mem.span(@errorName(err)) }, }, )); } } break :brk this.log.toJS(this.global, bun.default_allocator, "Transform failed"); }; promise.reject(this.global, error_value); return; } finish(this.output_code, this.global, promise); if (@enumToInt(this.protected_input_value) != 0) { this.protected_input_value = @intToEnum(JSC.JSValue, 0); } this.deinit(); } noinline fn finish(code: ZigString, global: *JSGlobalObject, promise: *JSC.JSInternalPromise) void { promise.resolve(global, code.toValueGC(global)); } pub fn deinit(this: *TransformTask) void { var should_cleanup = false; defer if (should_cleanup) bun.Global.mimalloc_cleanup(false); this.log.deinit(); if (this.input_code.isGloballyAllocated()) { this.input_code.deinitGlobal(); } if (this.output_code.isGloballyAllocated()) { should_cleanup = this.output_code.len > 512_000; this.output_code.deinitGlobal(); } bun.default_allocator.destroy(this); } }; fn exportReplacementValue(value: JSValue, globalThis: *JSGlobalObject) ?JSAst.Expr { if (value.isBoolean()) { return Expr{ .data = .{ .e_boolean = .{ .value = value.toBoolean(), }, }, .loc = logger.Loc.Empty, }; } if (value.isNumber()) { return Expr{ .data = .{ .e_number = .{ .value = value.asNumber() }, }, .loc = logger.Loc.Empty, }; } if (value.isNull()) { return Expr{ .data = .{ .e_null = .{}, }, .loc = logger.Loc.Empty, }; } if (value.isUndefined()) { return Expr{ .data = .{ .e_undefined = .{}, }, .loc = logger.Loc.Empty, }; } if (value.isString()) { var str = JSAst.E.String{ .data = std.fmt.allocPrint(bun.default_allocator, "{}", .{value.getZigString(globalThis)}) catch unreachable, }; var out = bun.default_allocator.create(JSAst.E.String) catch unreachable; out.* = str; return Expr{ .data = .{ .e_string = out, }, .loc = logger.Loc.Empty, }; } return null; } fn transformOptionsFromJSC(ctx: JSC.C.JSContextRef, temp_allocator: std.mem.Allocator, args: *JSC.Node.ArgumentsSlice, exception: JSC.C.ExceptionRef) !TranspilerOptions { var globalThis = ctx.ptr(); const object = args.next() orelse return TranspilerOptions{ .log = logger.Log.init(temp_allocator) }; if (object.isUndefinedOrNull()) return TranspilerOptions{ .log = logger.Log.init(temp_allocator) }; args.eat(); var allocator = args.arena.allocator(); var transpiler = TranspilerOptions{ .default_loader = .jsx, .transform = default_transform_options, .log = logger.Log.init(allocator), }; transpiler.log.level = .warn; if (!object.isObject()) { JSC.throwInvalidArguments("Expected an object", .{}, ctx, exception); return transpiler; } if (object.getIfPropertyExists(ctx.ptr(), "define")) |define| { define: { if (define.isUndefinedOrNull()) { break :define; } if (!define.isObject()) { JSC.throwInvalidArguments("define must be an object", .{}, ctx, exception); return transpiler; } var define_iter = JSC.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, }).init(globalThis, define.asObjectRef()); defer define_iter.deinit(); // cannot be a temporary because it may be loaded on different threads. var map_entries = allocator.alloc([]u8, define_iter.len * 2) catch unreachable; var names = map_entries[0..define_iter.len]; var values = map_entries[define_iter.len..]; while (define_iter.next()) |prop| { const property_value = define_iter.value; const value_type = property_value.jsType(); if (!value_type.isStringLike()) { JSC.throwInvalidArguments("define \"{any}\" must be a JSON string", .{prop}, ctx, exception); return transpiler; } names[define_iter.i] = prop.toOwnedSlice(allocator) catch unreachable; var val = JSC.ZigString.init(""); property_value.toZigString(&val, globalThis); if (val.len == 0) { val = JSC.ZigString.init("\"\""); } values[define_iter.i] = std.fmt.allocPrint(allocator, "{}", .{val}) catch unreachable; } transpiler.transform.define = Api.StringMap{ .keys = names, .values = values, }; } } if (object.get(globalThis, "external")) |external| { external: { if (external.isUndefinedOrNull()) break :external; const toplevel_type = external.jsType(); if (toplevel_type.isStringLike()) { var zig_str = JSC.ZigString.init(""); external.toZigString(&zig_str, globalThis); if (zig_str.len == 0) break :external; var single_external = allocator.alloc(string, 1) catch unreachable; single_external[0] = std.fmt.allocPrint(allocator, "{}", .{external}) catch unreachable; transpiler.transform.external = single_external; } else if (toplevel_type.isArray()) { const count = external.getLengthOfArray(globalThis); if (count == 0) break :external; var externals = allocator.alloc(string, count) catch unreachable; var iter = external.arrayIterator(globalThis); var i: usize = 0; while (iter.next()) |entry| { if (!entry.jsType().isStringLike()) { JSC.throwInvalidArguments("external must be a string or string[]", .{}, ctx, exception); return transpiler; } var zig_str = JSC.ZigString.init(""); entry.toZigString(&zig_str, globalThis); if (zig_str.len == 0) continue; externals[i] = std.fmt.allocPrint(allocator, "{}", .{external}) catch unreachable; i += 1; } transpiler.transform.external = externals[0..i]; } else { JSC.throwInvalidArguments("external must be a string or string[]", .{}, ctx, exception); return transpiler; } } } if (object.get(globalThis, "loader")) |loader| { if (Loader.fromJS(globalThis, loader, exception)) |resolved| { if (!resolved.isJavaScriptLike()) { JSC.throwInvalidArguments("only JavaScript-like loaders supported for now", .{}, ctx, exception); return transpiler; } transpiler.default_loader = resolved; } if (exception.* != null) { return transpiler; } } if (object.get(globalThis, "platform")) |platform| { if (Platform.fromJS(globalThis, platform, exception)) |resolved| { transpiler.transform.platform = resolved.toAPI(); } if (exception.* != null) { return transpiler; } } if (object.get(globalThis, "tsconfig")) |tsconfig| { tsconfig: { if (tsconfig.isUndefinedOrNull()) break :tsconfig; const kind = tsconfig.jsType(); var out = JSC.ZigString.init(""); if (kind.isArray()) { JSC.throwInvalidArguments("tsconfig must be a string or object", .{}, ctx, exception); return transpiler; } if (!kind.isStringLike()) { tsconfig.jsonStringify(globalThis, 0, &out); } else { tsconfig.toZigString(&out, globalThis); } if (out.len == 0) break :tsconfig; transpiler.tsconfig_buf = std.fmt.allocPrint(allocator, "{}", .{out}) catch unreachable; // TODO: JSC -> Ast conversion if (TSConfigJSON.parse( allocator, &transpiler.log, logger.Source.initPathString("tsconfig.json", transpiler.tsconfig_buf), &VirtualMachine.vm.bundler.resolver.caches.json, true, ) catch null) |parsed_tsconfig| { transpiler.tsconfig = parsed_tsconfig; } } } transpiler.runtime.allow_runtime = false; transpiler.runtime.dynamic_require = switch (transpiler.transform.platform orelse .browser) { .bun, .bun_macro => true, else => false, }; if (object.getIfPropertyExists(globalThis, "macro")) |macros| { macros: { if (macros.isUndefinedOrNull()) break :macros; const kind = macros.jsType(); const is_object = kind.isObject(); if (!(kind.isStringLike() or is_object)) { JSC.throwInvalidArguments("macro must be an object", .{}, ctx, exception); return transpiler; } var out: ZigString = ZigString.init(""); // TODO: write a converter between JSC types and Bun AST types if (is_object) { macros.jsonStringify(globalThis, 0, &out); } else { macros.toZigString(&out, globalThis); } if (out.len == 0) break :macros; transpiler.macros_buf = std.fmt.allocPrint(allocator, "{}", .{out}) catch unreachable; const source = logger.Source.initPathString("macros.json", transpiler.macros_buf); const json = (VirtualMachine.vm.bundler.resolver.caches.json.parseJSON( &transpiler.log, source, allocator, ) catch null) orelse break :macros; transpiler.macro_map = PackageJSON.parseMacrosJSON(allocator, json, &transpiler.log, &source); } } if (object.get(globalThis, "autoImportJSX")) |flag| { transpiler.runtime.auto_import_jsx = flag.toBoolean(); } if (object.get(globalThis, "allowBunRuntime")) |flag| { transpiler.runtime.allow_runtime = flag.toBoolean(); } if (object.get(globalThis, "jsxOptimizationInline")) |flag| { transpiler.runtime.jsx_optimization_inline = flag.toBoolean(); } if (object.get(globalThis, "jsxOptimizationHoist")) |flag| { transpiler.runtime.jsx_optimization_hoist = flag.toBoolean(); if (!transpiler.runtime.jsx_optimization_inline and transpiler.runtime.jsx_optimization_hoist) { JSC.throwInvalidArguments("jsxOptimizationHoist requires jsxOptimizationInline", .{}, ctx, exception); return transpiler; } } if (object.get(globalThis, "sourcemap")) |flag| { if (flag.isBoolean() or flag.isUndefinedOrNull()) { if (flag.toBoolean()) { transpiler.transform.source_map = Api.SourceMapMode.external; } else { transpiler.transform.source_map = Api.SourceMapMode.inline_into_file; } } else { var sourcemap = flag.toSlice(globalThis, allocator); if (options.SourceMapOption.map.get(sourcemap.slice())) |source| { transpiler.transform.source_map = source.toAPI(); } else { JSC.throwInvalidArguments("sourcemap must be one of \"inline\", \"external\", or \"none\"", .{}, ctx, exception); return transpiler; } } } var tree_shaking: ?bool = null; if (object.get(globalThis, "treeShaking")) |treeShaking| { tree_shaking = treeShaking.toBoolean(); } var trim_unused_imports: ?bool = null; if (object.get(globalThis, "trimUnusedImports")) |trimUnusedImports| { trim_unused_imports = trimUnusedImports.toBoolean(); } if (object.getTruthy(globalThis, "exports")) |exports| { if (!exports.isObject()) { JSC.throwInvalidArguments("exports must be an object", .{}, ctx, exception); return transpiler; } var replacements = Runtime.Features.ReplaceableExport.Map{}; errdefer replacements.clearAndFree(bun.default_allocator); if (exports.getTruthy(globalThis, "eliminate")) |eliminate| { if (!eliminate.jsType().isArray()) { JSC.throwInvalidArguments("exports.eliminate must be an array", .{}, ctx, exception); return transpiler; } var total_name_buf_len: u32 = 0; var string_count: u32 = 0; var iter = JSC.JSArrayIterator.init(eliminate, globalThis); { var length_iter = iter; while (length_iter.next()) |value| { if (value.isString()) { const length = value.getLengthOfArray(globalThis); string_count += @as(u32, @boolToInt(length > 0)); total_name_buf_len += length; } } } if (total_name_buf_len > 0) { var buf = try std.ArrayListUnmanaged(u8).initCapacity(bun.default_allocator, total_name_buf_len); try replacements.ensureUnusedCapacity(bun.default_allocator, string_count); { var length_iter = iter; while (length_iter.next()) |value| { if (!value.isString()) continue; var str = value.getZigString(globalThis); if (str.len == 0) continue; const name = std.fmt.bufPrint(buf.items.ptr[buf.items.len..buf.capacity], "{}", .{str}) catch { JSC.throwInvalidArguments("Error reading exports.eliminate. TODO: utf-16", .{}, ctx, exception); return transpiler; }; buf.items.len += name.len; if (name.len > 0) { replacements.putAssumeCapacity(name, .{ .delete = .{} }); } } } } } if (exports.getTruthy(globalThis, "replace")) |replace| { if (!replace.isObject()) { JSC.throwInvalidArguments("replace must be an object", .{}, ctx, exception); return transpiler; } var iter = JSC.JSPropertyIterator(.{ .skip_empty_name = true, .include_value = true, }).init(globalThis, replace.asObjectRef()); if (iter.len > 0) { errdefer iter.deinit(); try replacements.ensureUnusedCapacity(bun.default_allocator, iter.len); // We cannot set the exception before `try` because it could be // a double free with the `errdefer`. defer if (exception.* != null) { iter.deinit(); for (replacements.keys()) |key| { bun.default_allocator.free(bun.constStrToU8(key)); } replacements.clearAndFree(bun.default_allocator); }; while (iter.next()) |key_| { const value = iter.value; if (value.isEmpty()) continue; var key = try key_.toOwnedSlice(bun.default_allocator); if (!JSLexer.isIdentifier(key)) { JSC.throwInvalidArguments("\"{s}\" is not a valid ECMAScript identifier", .{key}, ctx, exception); bun.default_allocator.free(key); return transpiler; } var entry = replacements.getOrPutAssumeCapacity(key); if (exportReplacementValue(value, globalThis)) |expr| { entry.value_ptr.* = .{ .replace = expr }; continue; } if (value.isObject() and value.getLengthOfArray(ctx.ptr()) == 2) { const replacementValue = JSC.JSObject.getIndex(value, globalThis, 1); if (exportReplacementValue(replacementValue, globalThis)) |to_replace| { const replacementKey = JSC.JSObject.getIndex(value, globalThis, 0); var slice = (try replacementKey.toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator)); var replacement_name = slice.slice(); if (!JSLexer.isIdentifier(replacement_name)) { JSC.throwInvalidArguments("\"{any}\" is not a valid ECMAScript identifier", .{replacement_name}, ctx, exception); slice.deinit(); return transpiler; } entry.value_ptr.* = .{ .inject = .{ .name = replacement_name, .value = to_replace, }, }; continue; } } JSC.throwInvalidArguments("exports.replace values can only be string, null, undefined, number or boolean", .{}, ctx, exception); return transpiler; } } } tree_shaking = tree_shaking orelse (replacements.count() > 0); transpiler.runtime.replace_exports = replacements; } transpiler.tree_shaking = tree_shaking orelse false; transpiler.trim_unused_imports = trim_unused_imports orelse transpiler.tree_shaking; return transpiler; } pub fn constructor( ctx: js.JSContextRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSObjectRef { var temp = std.heap.ArenaAllocator.init(getAllocator(ctx)); var args = JSC.Node.ArgumentsSlice.init(ctx.bunVM(), @ptrCast([*]const JSC.JSValue, arguments.ptr)[0..arguments.len]); defer temp.deinit(); const transpiler_options: TranspilerOptions = if (arguments.len > 0) transformOptionsFromJSC(ctx, temp.allocator(), &args, exception) catch { JSC.throwInvalidArguments("Failed to create transpiler", .{}, ctx, exception); return null; } else TranspilerOptions{ .log = logger.Log.init(getAllocator(ctx)) }; if (exception.* != null) { return null; } if ((transpiler_options.log.warnings + transpiler_options.log.errors) > 0) { var out_exception = transpiler_options.log.toJS(ctx.ptr(), getAllocator(ctx), "Failed to create transpiler"); exception.* = out_exception.asObjectRef(); return null; } var log = getAllocator(ctx).create(logger.Log) catch unreachable; log.* = transpiler_options.log; var bundler = Bundler.Bundler.init( getAllocator(ctx), log, transpiler_options.transform, null, JavaScript.VirtualMachine.vm.bundler.env, ) catch |err| { if ((log.warnings + log.errors) > 0) { var out_exception = log.toJS(ctx.ptr(), getAllocator(ctx), "Failed to create transpiler"); exception.* = out_exception.asObjectRef(); return null; } JSC.throwInvalidArguments("Error creating transpiler: {any}", .{@errorName(err)}, ctx, exception); return null; }; bundler.configureLinkerWithAutoJSX(false); bundler.options.env.behavior = .disable; bundler.configureDefines() catch |err| { if ((log.warnings + log.errors) > 0) { var out_exception = log.toJS(ctx.ptr(), getAllocator(ctx), "Failed to load define"); exception.* = out_exception.asObjectRef(); return null; } JSC.throwInvalidArguments("Failed to load define: {any}", .{@errorName(err)}, ctx, exception); return null; }; if (transpiler_options.macro_map.count() > 0) { bundler.options.macro_remap = transpiler_options.macro_map; } bundler.options.tree_shaking = transpiler_options.tree_shaking; bundler.options.trim_unused_imports = transpiler_options.trim_unused_imports; bundler.options.allow_runtime = transpiler_options.runtime.allow_runtime; bundler.options.auto_import_jsx = transpiler_options.runtime.auto_import_jsx; bundler.options.hot_module_reloading = transpiler_options.runtime.hot_module_reloading; bundler.options.jsx.supports_fast_refresh = bundler.options.hot_module_reloading and bundler.options.allow_runtime and transpiler_options.runtime.react_fast_refresh; var transpiler = getAllocator(ctx).create(Transpiler) catch unreachable; transpiler.* = Transpiler{ .transpiler_options = transpiler_options, .bundler = bundler, .arena = args.arena, .scan_pass_result = ScanPassResult.init(getAllocator(ctx)), }; return Class.make(ctx, transpiler); } pub fn finalize( this: *Transpiler, ) void { this.bundler.log.deinit(); this.scan_pass_result.named_imports.deinit(); this.scan_pass_result.import_records.deinit(); this.scan_pass_result.used_symbols.deinit(); if (this.buffer_writer != null) { this.buffer_writer.?.buffer.deinit(); } // bun.default_allocator.free(this.transpiler_options.tsconfig_buf); // bun.default_allocator.free(this.transpiler_options.macros_buf); this.arena.deinit(); } fn getParseResult(this: *Transpiler, allocator: std.mem.Allocator, code: []const u8, loader: ?Loader, macro_js_ctx: Bundler.MacroJSValueType) ?Bundler.ParseResult { const name = this.transpiler_options.default_loader.stdinName(); const source = logger.Source.initPathString(name, code); const jsx = if (this.transpiler_options.tsconfig != null) this.transpiler_options.tsconfig.?.mergeJSX(this.bundler.options.jsx) else this.bundler.options.jsx; const parse_options = Bundler.Bundler.ParseOptions{ .allocator = allocator, .macro_remappings = this.transpiler_options.macro_map, .dirname_fd = 0, .file_descriptor = null, .loader = loader orelse this.transpiler_options.default_loader, .jsx = jsx, .path = source.path, .virtual_source = &source, .replace_exports = this.transpiler_options.runtime.replace_exports, .macro_js_ctx = macro_js_ctx, // .allocator = this. }; var parse_result = this.bundler.parse(parse_options, null); // necessary because we don't run the linker if (parse_result) |*res| { for (res.ast.import_records) |*import| { if (import.kind.isCommonJS()) { import.do_commonjs_transform_in_printer = true; import.module_id = @truncate(u32, std.hash.Wyhash.hash(0, import.path.pretty)); } } } return parse_result; } pub fn scan( this: *Transpiler, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) JSC.C.JSObjectRef { JSC.markBinding(@src()); var args = JSC.Node.ArgumentsSlice.init(ctx.bunVM(), @ptrCast([*]const JSC.JSValue, arguments.ptr)[0..arguments.len]); defer args.arena.deinit(); const code_arg = args.next() orelse { JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); return null; }; const code_holder = JSC.Node.StringOrBuffer.fromJS(ctx.ptr(), args.arena.allocator(), code_arg, exception) orelse { if (exception.* == null) JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); return null; }; const code = code_holder.slice(); args.eat(); const loader: ?Loader = brk: { if (args.next()) |arg| { args.eat(); break :brk Loader.fromJS(ctx.ptr(), arg, exception); } break :brk null; }; if (exception.* != null) return null; var arena = Mimalloc.Arena.init() catch unreachable; var prev_allocator = this.bundler.allocator; this.bundler.setAllocator(arena.allocator()); var log = logger.Log.init(arena.backingAllocator()); defer log.deinit(); this.bundler.setLog(&log); defer { this.bundler.setLog(&this.transpiler_options.log); this.bundler.setAllocator(prev_allocator); arena.deinit(); } defer { JSAst.Stmt.Data.Store.reset(); JSAst.Expr.Data.Store.reset(); } const parse_result = getParseResult(this, arena.allocator(), code, loader, Bundler.MacroJSValueType.zero) orelse { if ((this.bundler.log.warnings + this.bundler.log.errors) > 0) { var out_exception = this.bundler.log.toJS(ctx.ptr(), getAllocator(ctx), "Parse error"); exception.* = out_exception.asObjectRef(); return null; } JSC.throwInvalidArguments("Failed to parse", .{}, ctx, exception); return null; }; if ((this.bundler.log.warnings + this.bundler.log.errors) > 0) { var out_exception = this.bundler.log.toJS(ctx.ptr(), getAllocator(ctx), "Parse error"); exception.* = out_exception.asObjectRef(); return null; } const exports_label = JSC.ZigString.init("exports"); const imports_label = JSC.ZigString.init("imports"); const named_imports_value = namedImportsToJS( ctx.ptr(), parse_result.ast.import_records, exception, ); if (exception.* != null) return null; var named_exports_value = namedExportsToJS( ctx.ptr(), parse_result.ast.named_exports, ); return JSC.JSValue.createObject2(ctx.ptr(), &imports_label, &exports_label, named_imports_value, named_exports_value).asObjectRef(); } // pub fn build( // this: *Transpiler, // ctx: js.JSContextRef, // _: js.JSObjectRef, // _: js.JSObjectRef, // arguments: []const js.JSValueRef, // exception: js.ExceptionRef, // ) JSC.C.JSObjectRef {} pub fn transform( this: *Transpiler, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) JSC.C.JSObjectRef { JSC.markBinding(@src()); var args = JSC.Node.ArgumentsSlice.init(ctx.bunVM(), @ptrCast([*]const JSC.JSValue, arguments.ptr)[0..arguments.len]); defer args.arena.deinit(); const code_arg = args.next() orelse { JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); return null; }; const code_holder = JSC.Node.StringOrBuffer.fromJS(ctx.ptr(), this.arena.allocator(), code_arg, exception) orelse { if (exception.* == null) JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); return null; }; const code = code_holder.slice(); args.eat(); const loader: ?Loader = brk: { if (args.next()) |arg| { args.eat(); break :brk Loader.fromJS(ctx.ptr(), arg, exception); } break :brk null; }; if (exception.* != null) return null; if (code_holder == .string) { JSC.C.JSValueProtect(ctx, arguments[0]); } var task = TransformTask.create(this, if (code_holder == .string) arguments[0] else null, ctx.ptr(), ZigString.init(code), loader orelse this.transpiler_options.default_loader) catch return null; task.schedule(); return task.promise.asObjectRef(); } pub fn transformSync( this: *Transpiler, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) JSC.C.JSObjectRef { var args = JSC.Node.ArgumentsSlice.init(ctx.bunVM(), @ptrCast([*]const JSC.JSValue, arguments.ptr)[0..arguments.len]); defer args.arena.deinit(); const code_arg = args.next() orelse { JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); return null; }; var arena = Mimalloc.Arena.init() catch unreachable; defer arena.deinit(); const code_holder = JSC.Node.StringOrBuffer.fromJS(ctx.ptr(), arena.allocator(), code_arg, exception) orelse { if (exception.* == null) JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); return null; }; const code = code_holder.slice(); JSC.JSValue.c(arguments[0]).ensureStillAlive(); defer JSC.JSValue.c(arguments[0]).ensureStillAlive(); args.eat(); var js_ctx_value: JSC.JSValue = JSC.JSValue.zero; const loader: ?Loader = brk: { if (args.next()) |arg| { args.eat(); if (arg.isNumber() or arg.isString()) { break :brk Loader.fromJS(ctx.ptr(), arg, exception); } if (arg.isObject()) { js_ctx_value = arg; break :brk null; } } break :brk null; }; if (args.nextEat()) |arg| { if (arg.isObject()) { js_ctx_value = arg; } else { JSC.throwInvalidArguments("Expected a Loader or object", .{}, ctx, exception); return null; } } if (!js_ctx_value.isEmpty()) { js_ctx_value.ensureStillAlive(); } defer { if (!js_ctx_value.isEmpty()) { js_ctx_value.ensureStillAlive(); } } if (exception.* != null) return null; JSAst.Stmt.Data.Store.reset(); JSAst.Expr.Data.Store.reset(); defer { JSAst.Stmt.Data.Store.reset(); JSAst.Expr.Data.Store.reset(); } var prev_bundler = this.bundler; this.bundler.setAllocator(arena.allocator()); this.bundler.macro_context = null; var log = logger.Log.init(arena.backingAllocator()); this.bundler.setLog(&log); defer { this.bundler = prev_bundler; } var parse_result = getParseResult( this, arena.allocator(), code, loader, if (comptime JSC.is_bindgen) Bundler.MacroJSValueType.zero else js_ctx_value, ) orelse { if ((this.bundler.log.warnings + this.bundler.log.errors) > 0) { var out_exception = this.bundler.log.toJS(ctx.ptr(), getAllocator(ctx), "Parse error"); exception.* = out_exception.asObjectRef(); return null; } JSC.throwInvalidArguments("Failed to parse", .{}, ctx, exception); return null; }; if ((this.bundler.log.warnings + this.bundler.log.errors) > 0) { var out_exception = this.bundler.log.toJS(ctx.ptr(), getAllocator(ctx), "Parse error"); exception.* = out_exception.asObjectRef(); return null; } var buffer_writer = this.buffer_writer orelse brk: { var writer = JSPrinter.BufferWriter.init(arena.backingAllocator()) catch { JSC.throwInvalidArguments("Failed to create BufferWriter", .{}, ctx, exception); return null; }; writer.buffer.growIfNeeded(code.len) catch unreachable; writer.buffer.list.expandToCapacity(); break :brk writer; }; defer { this.buffer_writer = buffer_writer; } buffer_writer.reset(); var printer = JSPrinter.BufferPrinter.init(buffer_writer); _ = this.bundler.print(parse_result, @TypeOf(&printer), &printer, .esm_ascii) catch |err| { JSC.JSError(bun.default_allocator, "Failed to print code: {any}", .{@errorName(err)}, ctx, exception); return null; }; // TODO: benchmark if pooling this way is faster or moving is faster buffer_writer = printer.ctx; var out = JSC.ZigString.init(buffer_writer.written); out.mark(); out.setOutputEncoding(); return out.toValueGC(ctx.ptr()).asObjectRef(); } fn namedExportsToJS(global: *JSGlobalObject, named_exports: JSAst.Ast.NamedExports) JSC.JSValue { if (named_exports.count() == 0) return JSC.JSValue.fromRef(JSC.C.JSObjectMakeArray(global, 0, null, null)); var named_exports_iter = named_exports.iterator(); var stack_fallback = std.heap.stackFallback(@sizeOf(JSC.ZigString) * 32, getAllocator(global)); var allocator = stack_fallback.get(); var names = allocator.alloc( JSC.ZigString, named_exports.count(), ) catch unreachable; defer allocator.free(names); var i: usize = 0; while (named_exports_iter.next()) |entry| { names[i] = JSC.ZigString.init(entry.key_ptr.*); i += 1; } JSC.ZigString.sortAsc(names[0..i]); return JSC.JSValue.createStringArray(global, names.ptr, names.len, true); } const ImportRecord = @import("../../import_record.zig").ImportRecord; fn namedImportsToJS( global: *JSGlobalObject, import_records: []const ImportRecord, exception: JSC.C.ExceptionRef, ) JSC.JSValue { var stack_fallback = std.heap.stackFallback(@sizeOf(JSC.C.JSObjectRef) * 32, getAllocator(global)); var allocator = stack_fallback.get(); var i: usize = 0; const path_label = JSC.ZigString.init("path"); const kind_label = JSC.ZigString.init("kind"); var array_items = allocator.alloc( JSC.C.JSValueRef, import_records.len, ) catch unreachable; defer allocator.free(array_items); for (import_records) |record| { if (record.is_internal) continue; const path = JSC.ZigString.init(record.path.text).toValueGC(global); const kind = JSC.ZigString.init(record.kind.label()).toValue(global); array_items[i] = JSC.JSValue.createObject2(global, &path_label, &kind_label, path, kind).asObjectRef(); i += 1; } return JSC.JSValue.fromRef(JSC.C.JSObjectMakeArray(global, i, array_items.ptr, exception)); } pub fn scanImports( this: *Transpiler, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) JSC.C.JSObjectRef { var args = JSC.Node.ArgumentsSlice.init(ctx.bunVM(), @ptrCast([*]const JSC.JSValue, arguments.ptr)[0..arguments.len]); const code_arg = args.next() orelse { JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); return null; }; const code_holder = JSC.Node.StringOrBuffer.fromJS(ctx.ptr(), args.arena.allocator(), code_arg, exception) orelse { if (exception.* == null) JSC.throwInvalidArguments("Expected a string or Uint8Array", .{}, ctx, exception); return null; }; args.eat(); const code = code_holder.slice(); var loader: Loader = this.transpiler_options.default_loader; if (args.next()) |arg| { if (Loader.fromJS(ctx.ptr(), arg, exception)) |_loader| { loader = _loader; } args.eat(); } if (!loader.isJavaScriptLike()) { JSC.throwInvalidArguments("Only JavaScript-like files support this fast path", .{}, ctx, exception); return null; } if (exception.* != null) return null; var arena = Mimalloc.Arena.init() catch unreachable; var prev_allocator = this.bundler.allocator; this.bundler.setAllocator(arena.allocator()); var log = logger.Log.init(arena.backingAllocator()); defer log.deinit(); this.bundler.setLog(&log); defer { this.bundler.setLog(&this.transpiler_options.log); this.bundler.setAllocator(prev_allocator); arena.deinit(); } const source = logger.Source.initPathString(loader.stdinName(), code); var bundler = &this.bundler; const jsx = if (this.transpiler_options.tsconfig != null) this.transpiler_options.tsconfig.?.mergeJSX(this.bundler.options.jsx) else this.bundler.options.jsx; var opts = JSParser.Parser.Options.init(jsx, loader); if (this.bundler.macro_context == null) { this.bundler.macro_context = JSAst.Macro.MacroContext.init(&this.bundler); } opts.macro_context = &this.bundler.macro_context.?; JSAst.Stmt.Data.Store.reset(); JSAst.Expr.Data.Store.reset(); defer { JSAst.Stmt.Data.Store.reset(); JSAst.Expr.Data.Store.reset(); } bundler.resolver.caches.js.scan( bundler.allocator, &this.scan_pass_result, opts, bundler.options.define, &log, &source, ) catch |err| { defer this.scan_pass_result.reset(); if ((log.warnings + log.errors) > 0) { var out_exception = log.toJS(ctx.ptr(), getAllocator(ctx), "Failed to scan imports"); exception.* = out_exception.asObjectRef(); return null; } JSC.throwInvalidArguments("Failed to scan imports: {any}", .{@errorName(err)}, ctx, exception); return null; }; defer this.scan_pass_result.reset(); if ((log.warnings + log.errors) > 0) { var out_exception = log.toJS(ctx.ptr(), getAllocator(ctx), "Failed to scan imports"); exception.* = out_exception.asObjectRef(); return null; } const named_imports_value = namedImportsToJS( ctx.ptr(), this.scan_pass_result.import_records.items, exception, ); if (exception.* != null) return null; return named_imports_value.asObjectRef(); }