diff options
Diffstat (limited to 'src/bun.js/api/transpiler.zig')
-rw-r--r-- | src/bun.js/api/transpiler.zig | 1304 |
1 files changed, 1304 insertions, 0 deletions
diff --git a/src/bun.js/api/transpiler.zig b/src/bun.js/api/transpiler.zig new file mode 100644 index 000000000..6d3f3f6fd --- /dev/null +++ b/src/bun.js/api/transpiler.zig @@ -0,0 +1,1304 @@ +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 array = JSC.C.JSObjectCopyPropertyNames(globalThis.ref(), define.asObjectRef()); + defer JSC.C.JSPropertyNameArrayRelease(array); + const count = JSC.C.JSPropertyNameArrayGetCount(array); + // cannot be a temporary because it may be loaded on different threads. + var map_entries = allocator.alloc([]u8, count * 2) catch unreachable; + var names = map_entries[0..count]; + + var values = map_entries[count..]; + + var i: usize = 0; + while (i < count) : (i += 1) { + var property_name_ref = JSC.C.JSPropertyNameArrayGetNameAtIndex( + array, + i, + ); + defer JSC.C.JSStringRelease(property_name_ref); + const prop: []const u8 = JSC.C.JSStringGetCharacters8Ptr(property_name_ref)[0..JSC.C.JSStringGetLength(property_name_ref)]; + const property_value: JSC.JSValue = JSC.JSValue.fromRef( + JSC.C.JSObjectGetProperty( + globalThis.ref(), + define.asObjectRef(), + property_name_ref, + null, + ), + ); + const value_type = property_value.jsType(); + + if (!value_type.isStringLike()) { + JSC.throwInvalidArguments("define \"{s}\" must be a JSON string", .{prop}, ctx, exception); + return transpiler; + } + names[i] = allocator.dupe(u8, prop) catch unreachable; + var val = JSC.ZigString.init(""); + property_value.toZigString(&val, globalThis); + if (val.len == 0) { + val = JSC.ZigString.init("\"\""); + } + values[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; + + 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 total_name_buf_len: usize = 0; + + var array = js.JSObjectCopyPropertyNames(ctx, replace.asObjectRef()); + defer js.JSPropertyNameArrayRelease(array); + const property_names_count = @intCast(u32, js.JSPropertyNameArrayGetCount(array)); + var iter = JSC.JSPropertyNameIterator{ + .array = array, + .count = @intCast(u32, property_names_count), + }; + + { + var key_iter = iter; + while (key_iter.next()) |item| { + total_name_buf_len += JSC.C.JSStringGetLength(item); + } + } + + if (total_name_buf_len > 0) { + var total_name_buf = try std.ArrayList(u8).initCapacity(bun.default_allocator, total_name_buf_len); + errdefer total_name_buf.clearAndFree(); + + try replacements.ensureUnusedCapacity(bun.default_allocator, property_names_count); + defer { + if (exception.* != null) { + total_name_buf.clearAndFree(); + replacements.clearAndFree(bun.default_allocator); + } + } + + while (iter.next()) |item| { + const start = total_name_buf.items.len; + total_name_buf.items.len += @maximum( + // this returns a null terminated string + JSC.C.JSStringGetUTF8CString(item, total_name_buf.items.ptr + start, total_name_buf.capacity - start), + 1, + ) - 1; + JSC.C.JSStringRelease(item); + const key = total_name_buf.items[start..total_name_buf.items.len]; + // if somehow the string is empty, skip it + if (key.len == 0) + continue; + + const value = replace.get(globalThis, key).?; + if (value.isEmpty()) continue; + + if (!JSLexer.isIdentifier(key)) { + JSC.throwInvalidArguments("\"{s}\" is not a valid ECMAScript identifier", .{key}, ctx, exception); + total_name_buf.deinit(); + 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()); + var replacement_name = slice.slice(); + + if (!JSLexer.isIdentifier(replacement_name)) { + JSC.throwInvalidArguments("\"{s}\" is not a valid ECMAScript identifier", .{replacement_name}, ctx, exception); + total_name_buf.deinit(); + 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: {s}", .{@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: {s}", .{@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: JSValue) ?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.wrap_with_to_module = 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 { + 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, JSC.JSValue.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 { + 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, + 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: {s}", .{@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(); + + 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.ref(), 0, null, null)); + + var named_exports_iter = named_exports.iterator(); + var stack_fallback = std.heap.stackFallback(@sizeOf(JSC.ZigString) * 32, getAllocator(global.ref())); + 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.ref())); + 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.ref(), 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: {s}", .{@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(); +} |