aboutsummaryrefslogtreecommitdiff
path: root/src/javascript/jsc/api/transpiler.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/javascript/jsc/api/transpiler.zig')
-rw-r--r--src/javascript/jsc/api/transpiler.zig317
1 files changed, 317 insertions, 0 deletions
diff --git a/src/javascript/jsc/api/transpiler.zig b/src/javascript/jsc/api/transpiler.zig
index 21209bc69..cdc5dc39a 100644
--- a/src/javascript/jsc/api/transpiler.zig
+++ b/src/javascript/jsc/api/transpiler.zig
@@ -37,12 +37,14 @@ 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;
bundler: Bundler.Bundler,
arena: std.heap.ArenaAllocator,
transpiler_options: TranspilerOptions,
scan_pass_result: ScanPassResult,
+buffer_writer: ?JSPrinter.BufferWriter = null,
pub const Class = NewClass(
Transpiler,
@@ -54,6 +56,12 @@ pub const Class = NewClass(
.scan = .{
.rfn = scan,
},
+ .transform = .{
+ .rfn = transform,
+ },
+ .transformSync = .{
+ .rfn = transformSync,
+ },
.finalize = finalize,
},
.{},
@@ -85,6 +93,178 @@ const TranspilerOptions = struct {
tsconfig_buf: []const u8 = "",
macros_buf: []const u8 = "",
log: logger.Log,
+ pending_tasks: u32 = 0,
+};
+
+// Mimalloc gets unstable if we try to move this to a different thread
+// threadlocal var transform_buffer: _global.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,
+
+ 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 _global.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(_global.default_allocator),
+ .loader = loader,
+ };
+ 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(_global.default_allocator);
+ return try AsyncTransformTask.createOnJSThread(_global.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());
+ const Mimalloc = @import("../../../mimalloc_arena.zig");
+
+ JSAst.Stmt.Data.Store.create(_global.default_allocator);
+ JSAst.Expr.Data.Store.create(_global.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,
+ // .allocator = this.
+ };
+
+ const parse_result = this.bundler.parse(parse_options, null) orelse {
+ this.err = error.ParseError;
+ return;
+ };
+ defer {
+ if (parse_result.ast.symbol_pool) |pool| {
+ pool.release();
+ }
+ }
+ 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;
+ };
+ // 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.expandToCapacity();
+
+ // This works around a mimalloc and/or Zig allocator bug
+ buffer_writer.buffer.list.items = buffer_writer.buffer.list.items[0..printed];
+ var output_code = JSC.ZigString.init(buffer_writer.buffer.toOwnedSlice());
+ output_code.mark();
+ this.output_code = output_code;
+ } 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,
+ _global.default_allocator,
+ logger.Msg{
+ .data = logger.Data{ .text = std.mem.span(@errorName(err)) },
+ },
+ ));
+ }
+ }
+
+ break :brk this.log.toJS(this.global, _global.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) _global.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();
+ }
+
+ _global.default_allocator.destroy(this);
+ }
};
fn transformOptionsFromJSC(ctx: JSC.C.JSContextRef, temp_allocator: std.mem.Allocator, args: *JSC.Node.ArgumentsSlice, exception: JSC.C.ExceptionRef) TranspilerOptions {
@@ -372,6 +552,9 @@ pub fn finalize(
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();
+ }
// _global.default_allocator.free(this.transpiler_options.tsconfig_buf);
// _global.default_allocator.free(this.transpiler_options.macros_buf);
@@ -477,6 +660,140 @@ pub fn scan(
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(@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(), 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(@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(), 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;
+
+ defer {
+ JSAst.Stmt.Data.Store.reset();
+ JSAst.Expr.Data.Store.reset();
+ }
+
+ const parse_result = getParseResult(this, args.arena.allocator(), code, loader) 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;
+ };
+ defer {
+ if (parse_result.ast.symbol_pool) |symbols| {
+ symbols.release();
+ }
+ }
+
+ 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;
+ }
+ if (this.buffer_writer == null) {
+ this.buffer_writer = JSPrinter.BufferWriter.init(_global.default_allocator) catch {
+ JSC.throwInvalidArguments("Failed to create BufferWriter", .{}, ctx, exception);
+ return null;
+ };
+ this.buffer_writer.?.buffer.growIfNeeded(code.len) catch unreachable;
+ this.buffer_writer.?.buffer.list.expandToCapacity();
+ }
+
+ this.buffer_writer.?.reset();
+ var printer = JSPrinter.BufferPrinter.init(this.buffer_writer.?);
+ const printed = this.bundler.print(parse_result, @TypeOf(printer), printer, .esm_ascii) catch |err| {
+ JSC.JSError(_global.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
+ this.buffer_writer = printer.ctx;
+ this.buffer_writer.?.buffer.list.expandToCapacity();
+ var out = JSC.ZigString.init(this.buffer_writer.?.buffer.list.items[0..printed]);
+ 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));