diff options
author | 2022-01-19 23:07:03 -0800 | |
---|---|---|
committer | 2022-01-19 23:07:03 -0800 | |
commit | a09b99565138bb4bc73a5328397428fb5025817b (patch) | |
tree | 3412ae330b4a6078173d0048665b9d547b3d718b /src | |
parent | 4098484ff5d1c66a5f146e773a9be25cbcd2a1f4 (diff) | |
download | bun-a09b99565138bb4bc73a5328397428fb5025817b.tar.gz bun-a09b99565138bb4bc73a5328397428fb5025817b.tar.zst bun-a09b99565138bb4bc73a5328397428fb5025817b.zip |
Bun.Transpiler – API for scanning imports/exports of JSX/TSX/TS/JS files
Diffstat (limited to 'src')
-rw-r--r-- | src/api/schema.d.ts | 3 | ||||
-rw-r--r-- | src/api/schema.js | 4 | ||||
-rw-r--r-- | src/api/schema.peechy | 1 | ||||
-rw-r--r-- | src/api/schema.zig | 3 | ||||
-rw-r--r-- | src/bundler.zig | 20 | ||||
-rw-r--r-- | src/import_record.zig | 17 | ||||
-rw-r--r-- | src/javascript/jsc/api/transpiler.zig | 616 | ||||
-rw-r--r-- | src/javascript/jsc/base.zig | 10 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/bindings.zig | 28 | ||||
-rw-r--r-- | src/javascript/jsc/javascript.zig | 17 | ||||
-rw-r--r-- | src/javascript/jsc/test/jest.zig | 42 | ||||
-rw-r--r-- | src/logger.zig | 29 | ||||
-rw-r--r-- | src/options.zig | 89 | ||||
-rw-r--r-- | src/resolver/package_json.zig | 127 | ||||
-rw-r--r-- | src/runtime.version | 2 |
15 files changed, 934 insertions, 74 deletions
diff --git a/src/api/schema.d.ts b/src/api/schema.d.ts index 273f8d9d3..490a74421 100644 --- a/src/api/schema.d.ts +++ b/src/api/schema.d.ts @@ -120,6 +120,7 @@ export enum Platform { browser = 1, node = 2, bun = 3, + bun_macro = 4, } export const PlatformKeys = { 1: "browser", @@ -128,6 +129,8 @@ export const PlatformKeys = { node: "node", 3: "bun", bun: "bun", + 4: "bun_macro", + bun_macro: "bun_macro", }; export enum CSSInJSBehavior { facade = 1, diff --git a/src/api/schema.js b/src/api/schema.js index 8b2043e9c..8339484da 100644 --- a/src/api/schema.js +++ b/src/api/schema.js @@ -534,17 +534,21 @@ const Platform = { 1: 1, 2: 2, 3: 3, + 4: 4, browser: 1, node: 2, bun: 3, + bun_macro: 4, }; const PlatformKeys = { 1: "browser", 2: "node", 3: "bun", + 4: "bun_macro", browser: "browser", node: "node", bun: "bun", + bun_macro: "bun_macro", }; const CSSInJSBehavior = { 1: 1, diff --git a/src/api/schema.peechy b/src/api/schema.peechy index 7c5b482a2..cd5134299 100644 --- a/src/api/schema.peechy +++ b/src/api/schema.peechy @@ -109,6 +109,7 @@ smol Platform { browser = 1; node = 2; bun = 3; + bun_macro = 4; } smol CSSInJSBehavior { diff --git a/src/api/schema.zig b/src/api/schema.zig index 8a850c8f1..4fe8d8acb 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -797,6 +797,9 @@ pub const Api = struct { /// bun bun, + /// bun_macro + bun_macro, + _, pub fn jsonStringify(self: *const @This(), opts: anytype, o: anytype) !void { diff --git a/src/bundler.zig b/src/bundler.zig index 8127a4dc5..a4633421e 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -237,7 +237,7 @@ pub const Bundler = struct { }; } - pub fn configureLinker(bundler: *ThisBundler) void { + pub fn configureLinkerWithAutoJSX(bundler: *ThisBundler, auto_jsx: bool) void { bundler.linker = Linker.init( bundler.allocator, bundler.log, @@ -248,17 +248,23 @@ pub const Bundler = struct { bundler.fs, ); - // If we don't explicitly pass JSX, try to get it from the root tsconfig - if (bundler.options.transform_options.jsx == null) { - // Most of the time, this will already be cached - if (bundler.resolver.readDirInfo(bundler.fs.top_level_dir) catch null) |root_dir| { - if (root_dir.tsconfig_json) |tsconfig| { - bundler.options.jsx = tsconfig.jsx; + if (auto_jsx) { + // If we don't explicitly pass JSX, try to get it from the root tsconfig + if (bundler.options.transform_options.jsx == null) { + // Most of the time, this will already be cached + if (bundler.resolver.readDirInfo(bundler.fs.top_level_dir) catch null) |root_dir| { + if (root_dir.tsconfig_json) |tsconfig| { + bundler.options.jsx = tsconfig.jsx; + } } } } } + pub fn configureLinker(bundler: *ThisBundler) void { + bundler.configureLinkerWithAutoJSX(true); + } + pub fn runEnvLoader(this: *ThisBundler) !void { switch (this.options.env.behavior) { .prefix, .load_all => { diff --git a/src/import_record.zig b/src/import_record.zig index 66bfc128d..c73896a0b 100644 --- a/src/import_record.zig +++ b/src/import_record.zig @@ -31,6 +31,23 @@ pub const ImportKind = enum(u8) { internal, + pub const Label = std.EnumArray(ImportKind, []const u8); + pub const all_labels: Label = brk: { + var labels = Label.initFill("internal"); + labels.set(ImportKind.entry_point, "entry-point"); + labels.set(ImportKind.stmt, "import-statement"); + labels.set(ImportKind.require, "require-call"); + labels.set(ImportKind.dynamic, "dynamic-import"); + labels.set(ImportKind.require_resolve, "require-resolve"); + labels.set(ImportKind.at, "import-rule"); + labels.set(ImportKind.url, "url-token"); + break :brk labels; + }; + + pub inline fn label(this: ImportKind) []const u8 { + return all_labels.get(this); + } + pub inline fn isCommonJS(this: ImportKind) bool { return switch (this) { .require, .require_resolve => true, diff --git a/src/javascript/jsc/api/transpiler.zig b/src/javascript/jsc/api/transpiler.zig new file mode 100644 index 000000000..07d07763e --- /dev/null +++ b/src/javascript/jsc/api/transpiler.zig @@ -0,0 +1,616 @@ +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("../../../query_string_map.zig").QueryStringMap; +const CombinedScanner = @import("../../../query_string_map.zig").CombinedScanner; +const _global = @import("../../../global.zig"); +const string = _global.string; +const JSC = @import("javascript_core"); +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 ScanPassResult = JSParser.ScanPassResult; + +bundler: Bundler.Bundler, +arena: std.heap.ArenaAllocator, +transpiler_options: TranspilerOptions, +scan_pass_result: ScanPassResult, + +pub const Class = NewClass( + Transpiler, + .{ .name = "Transpiler" }, + .{ + .scanImports = .{ + .rfn = scanImports, + }, + .scan = .{ + .rfn = scan, + }, + .finalize = finalize, + }, + .{}, +); + +pub const TranspilerConstructor = NewClass( + void, + .{ .name = "Transpiler" }, + .{ + .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, +}; + +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); + var map_entries = temp_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; + } + } + } + + if (object.getIfPropertyExists(globalThis, "macro")) |macros| { + macros: { + if (macros.isUndefinedOrNull()) break :macros; + const kind = macros.jsType(); + const is_object = kind == JSC.JSValue.JSType.Object; + 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); + } + } + + 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(@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) + 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.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; + }; + + 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)), + }; + + transpiler.bundler.macro_context = JSAst.Macro.MacroContext.init(&transpiler.bundler); + if (transpiler_options.macro_map.count() > 0) { + transpiler.bundler.macro_context.?.remap = transpiler_options.macro_map; + } + + 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(); + + // _global.default_allocator.free(this.transpiler_options.tsconfig_buf); + // _global.default_allocator.free(this.transpiler_options.macros_buf); + this.arena.deinit(); +} + +fn getParseResult(this: *Transpiler, allocator: std.mem.Allocator, code: []const u8, loader: ?Loader) ?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, + // .allocator = this. + }; + + return this.bundler.parse(parse_options, null); +} + +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(@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; + } + + 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(); +} + +fn namedExportsToJS(global: *JSGlobalObject, named_exports: JSAst.Ast.NamedExports) JSC.JSValue { + 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(@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(), 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; + + 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); + opts.macro_context = &this.bundler.macro_context.?; + var log = logger.Log.init(getAllocator(ctx)); + defer log.deinit(); + + 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(); +} diff --git a/src/javascript/jsc/base.zig b/src/javascript/jsc/base.zig index cf2ed1bad..4d61fb33a 100644 --- a/src/javascript/jsc/base.zig +++ b/src/javascript/jsc/base.zig @@ -1263,7 +1263,7 @@ pub fn NewClass( if (comptime property_name_refs.len > 0) { comptime var i: usize = 0; if (!property_name_refs_set) { - property_name_refs_set =true; + property_name_refs_set = true; inline while (i < property_name_refs.len) : (i += 1) { property_name_refs[i] = js.JSStringCreateStatic(property_names[i].ptr, property_names[i].len); } @@ -1687,7 +1687,8 @@ pub const ArrayBuffer = struct { return ArrayBuffer{ .byte_len = @truncate(u32, JSC.C.JSObjectGetTypedArrayByteLength(ctx, value.asObjectRef(), exception)), .offset = @truncate(u32, JSC.C.JSObjectGetTypedArrayByteOffset(ctx, value.asObjectRef(), exception)), - .ptr = @ptrCast([*]u8, JSC.C.JSObjectGetTypedArrayBytesPtr(ctx, value.asObjectRef(), exception).?), + .ptr = @ptrCast([*]u8, JSC.C.JSObjectGetArrayBufferBytesPtr(ctx, value.asObjectRef(), exception) orelse + JSC.C.JSObjectGetTypedArrayBytesPtr(ctx, value.asObjectRef(), exception).?), // TODO .typed_array_type = js.JSTypedArrayType.kJSTypedArrayTypeUint8Array, .len = @truncate(u32, JSC.C.JSObjectGetTypedArrayLength(ctx, value.asObjectRef(), exception)), @@ -1697,7 +1698,8 @@ pub const ArrayBuffer = struct { pub fn fromArrayBuffer(ctx: JSC.C.JSContextRef, value: JSC.JSValue, exception: JSC.C.ExceptionRef) ArrayBuffer { var buffer = ArrayBuffer{ .byte_len = @truncate(u32, JSC.C.JSObjectGetArrayBufferByteLength(ctx, value.asObjectRef(), exception)), - .ptr = @ptrCast([*]u8, JSC.C.JSObjectGetArrayBufferBytesPtr(ctx, value.asObjectRef(), exception).?), + .ptr = @ptrCast([*]u8, JSC.C.JSObjectGetArrayBufferBytesPtr(ctx, value.asObjectRef(), exception) orelse + JSC.C.JSObjectGetTypedArrayBytesPtr(ctx, value.asObjectRef(), exception).?), // TODO .typed_array_type = js.JSTypedArrayType.kJSTypedArrayTypeUint8Array, .len = 0, @@ -1804,6 +1806,7 @@ const NodeFS = JSC.Node.NodeFS; const DirEnt = JSC.Node.DirEnt; const Stats = JSC.Node.Stats; const BigIntStats = JSC.Node.BigIntStats; +const Transpiler = @import("./api/transpiler.zig"); pub const JSPrivateDataPtr = TaggedPointerUnion(.{ ResolveError, BuildError, @@ -1824,6 +1827,7 @@ pub const JSPrivateDataPtr = TaggedPointerUnion(.{ Stats, BigIntStats, DirEnt, + Transpiler, }); pub inline fn GetJSPrivateData(comptime Type: type, ref: js.JSObjectRef) ?*Type { diff --git a/src/javascript/jsc/bindings/bindings.zig b/src/javascript/jsc/bindings/bindings.zig index a28717400..4dad9a926 100644 --- a/src/javascript/jsc/bindings/bindings.zig +++ b/src/javascript/jsc/bindings/bindings.zig @@ -107,6 +107,22 @@ pub const ZigString = extern struct { }; } + pub fn sortDesc(slice_: []ZigString) void { + std.sort.sort(ZigString, slice_, {}, cmpDesc); + } + + pub fn cmpDesc(_: void, a: ZigString, b: ZigString) bool { + return strings.cmpStringsDesc(void{}, a.slice(), b.slice()); + } + + pub fn sortAsc(slice_: []ZigString) void { + std.sort.sort(ZigString, slice_, {}, cmpAsc); + } + + pub fn cmpAsc(_: void, a: ZigString, b: ZigString) bool { + return strings.cmpStringsAsc(void{}, a.slice(), b.slice()); + } + pub fn init(slice_: []const u8) ZigString { return ZigString{ .ptr = slice_.ptr, .len = slice_.len }; } @@ -1446,6 +1462,13 @@ pub const JSValue = enum(i64) { else => false, }; } + + pub inline fn isArray(this: JSType) bool { + return switch (this) { + .Array, .DerivedArray => true, + else => false, + }; + } }; pub inline fn cast(ptr: anytype) JSValue { @@ -1735,11 +1758,14 @@ pub const JSValue = enum(i64) { return cppFn("getIfPropertyExistsImpl", .{ this, global, ptr, len }); } - pub fn getIfPropertyExists(this: JSValue, global: *JSGlobalObject, property: []const u8) ?JSValue { + pub fn get(this: JSValue, global: *JSGlobalObject, property: []const u8) ?JSValue { const value = getIfPropertyExistsImpl(this, global, property.ptr, @intCast(u32, property.len)); return if (@enumToInt(value) != 0) value else return null; } + /// Alias for getIfPropertyExists + pub const getIfPropertyExists = get; + pub fn createTypeError(message: *const ZigString, code: *const ZigString, global: *JSGlobalObject) JSValue { return cppFn("createTypeError", .{ message, code, global }); } diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig index 64147bc5c..2299e9339 100644 --- a/src/javascript/jsc/javascript.zig +++ b/src/javascript/jsc/javascript.zig @@ -77,6 +77,7 @@ const ZigGlobalObject = @import("../../jsc.zig").ZigGlobalObject; const VM = @import("../../jsc.zig").VM; const Config = @import("./config.zig"); const URL = @import("../../query_string_map.zig").URL; +const Transpiler = @import("./api/transpiler.zig"); pub const GlobalClasses = [_]type{ Request.Class, Response.Class, @@ -746,9 +747,23 @@ pub const Bun = struct { .enableANSIColors = .{ .get = enableANSIColors, }, + .Transpiler = .{ + .get = getTranspilerConstructor, + .ts = d.ts{ .name = "Transpiler", .@"return" = "Transpiler.prototype" }, + }, }, ); + pub fn getTranspilerConstructor( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, + ) js.JSValueRef { + return js.JSObjectMake(ctx, Transpiler.TranspilerConstructor.get().?[0], null); + } + // For testing the segfault handler pub fn __debug__doSegfault( _: void, @@ -1702,7 +1717,7 @@ pub const VirtualMachine = struct { } const main_file_name: string = "bun:main"; - threadlocal var errors_stack: [256]*anyopaque = undefined; + pub threadlocal var errors_stack: [256]*anyopaque = undefined; pub fn fetch(ret: *ErrorableResolvedSource, global: *JSGlobalObject, specifier: ZigString, source: ZigString) callconv(.C) void { var log = logger.Log.init(vm.bundler.allocator); const spec = specifier.slice(); diff --git a/src/javascript/jsc/test/jest.zig b/src/javascript/jsc/test/jest.zig index 1398e9058..2ffc8bd01 100644 --- a/src/javascript/jsc/test/jest.zig +++ b/src/javascript/jsc/test/jest.zig @@ -365,6 +365,47 @@ pub const Expect = struct { } return thisObject; } + + pub fn toHaveLength( + this: *Expect, + ctx: js.JSContextRef, + _: js.JSObjectRef, + thisObject: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, + ) js.JSValueRef { + if (arguments.len != 1) { + JSC.JSError( + getAllocator(ctx), + ".toHaveLength() takes 1 argument", + .{}, + ctx, + exception, + ); + return js.JSValueMakeUndefined(ctx); + } + this.scope.tests.items[this.test_id].counter.actual += 1; + + const expected = JSC.JSValue.fromRef(arguments[0]).toU32(); + const actual = JSC.JSValue.fromRef(this.value).getLengthOfArray(ctx.ptr()); + if (expected != actual) { + JSC.JSError( + getAllocator(ctx), + "Expected length to equal {d} but received {d}\n Expected: {d}\n Actual: {d}\n", + .{ + expected, + actual, + expected, + actual, + }, + ctx, + exception, + ); + return null; + } + return thisObject; + } + pub const toHaveBeenCalledTimes = notImplementedFn; pub const toHaveBeenCalledWith = notImplementedFn; pub const toHaveBeenLastCalledWith = notImplementedFn; @@ -373,7 +414,6 @@ pub const Expect = struct { pub const toHaveReturnedWith = notImplementedFn; pub const toHaveLastReturnedWith = notImplementedFn; pub const toHaveNthReturnedWith = notImplementedFn; - pub const toHaveLength = notImplementedFn; pub const toHaveProperty = notImplementedFn; pub const toBeCloseTo = notImplementedFn; pub const toBeGreaterThan = notImplementedFn; diff --git a/src/logger.zig b/src/logger.zig index 5ac17f13c..1de5dcf9e 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -12,7 +12,7 @@ const MutableString = _global.MutableString; const stringZ = _global.stringZ; const default_allocator = _global.default_allocator; const C = _global.C; - +const JSC = @import("./jsc.zig"); const fs = @import("fs.zig"); const unicode = std.unicode; @@ -522,6 +522,33 @@ pub const Log = struct { }); } + pub fn toJS(this: Log, global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, comptime fmt: string) JSC.JSValue { + const msgs: []const Msg = this.msgs.items; + const count = @intCast(u16, @minimum(msgs.len, JSC.VirtualMachine.errors_stack.len)); + switch (count) { + 0 => return JSC.JSValue.jsUndefined(), + 1 => { + const msg = msgs[0]; + return JSC.JSValue.fromRef(JSC.BuildError.create(global, allocator, msg)); + }, + else => { + for (msgs[0..count]) |msg, i| { + switch (msg.metadata) { + .build => { + JSC.VirtualMachine.errors_stack[i] = JSC.BuildError.create(global, allocator, msg).?; + }, + .resolve => { + JSC.VirtualMachine.errors_stack[i] = JSC.ResolveError.create(global, allocator, msg, "").?; + }, + } + } + const out = JSC.ZigString.init(fmt); + const agg = global.createAggregateError(JSC.VirtualMachine.errors_stack[0..count].ptr, count, &out); + return agg; + }, + } + } + pub fn appendTo(self: *Log, other: *Log) !void { var notes_count: usize = 0; diff --git a/src/options.zig b/src/options.zig index 48fa6c0c5..2e2f684b6 100644 --- a/src/options.zig +++ b/src/options.zig @@ -24,6 +24,7 @@ const stringZ = _global.stringZ; const default_allocator = _global.default_allocator; const C = _global.C; const StoredFileDescriptorType = _global.StoredFileDescriptorType; +const JSC = @import("./jsc.zig"); const Analytics = @import("./analytics/analytics_thread.zig"); @@ -259,7 +260,6 @@ pub const ExternalModules = struct { "dns", "domain", "events", - "fs", "http", "http2", "https", @@ -368,6 +368,43 @@ pub const Platform = enum { bun_macro, node, + pub fn fromJS(global: *JSC.JSGlobalObject, value: JSC.JSValue, exception: JSC.C.ExceptionRef) ?Platform { + if (!value.jsType().isStringLike()) { + JSC.throwInvalidArguments("platform must be a string", .{}, global.ref(), exception); + + return null; + } + var zig_str = JSC.ZigString.init(""); + value.toZigString(&zig_str, global); + + var slice = zig_str.slice(); + + const Eight = strings.ExactSizeMatcher(8); + + return switch (Eight.match(slice)) { + Eight.case("deno"), Eight.case("browser") => Platform.browser, + Eight.case("bun") => Platform.bun, + Eight.case("macro") => Platform.bun_macro, + Eight.case("node") => Platform.node, + Eight.case("neutral") => Platform.neutral, + else => { + JSC.throwInvalidArguments("platform must be one of: deno, browser, bun, macro, node, neutral", .{}, global.ref(), exception); + + return null; + }, + }; + } + + pub fn toAPI(this: Platform) Api.Platform { + return switch (this) { + .node => .node, + .browser => .browser, + .bun => .bun, + .bun_macro => .bun_macro, + else => ._none, + }; + } + pub inline fn isServerSide(this: Platform) bool { return switch (this) { .bun_macro, .node, .bun => true, @@ -461,6 +498,7 @@ pub const Platform = enum { .node => .node, .browser => .browser, .bun => .bun, + .bun_macro => .bun_macro, else => .browser, }; } @@ -584,6 +622,55 @@ pub const Loader = enum(u3) { css, file, json, + pub const Map = std.EnumArray(Loader, string); + pub const stdin_name: Map = brk: { + var map = Map.initFill(""); + map.set(Loader.jsx, "input.jsx"); + map.set(Loader.js, "input.js"); + map.set(Loader.ts, "input.ts"); + map.set(Loader.tsx, "input.tsx"); + map.set(Loader.css, "input.css"); + map.set(Loader.file, "input"); + map.set(Loader.json, "input.json"); + break :brk map; + }; + + pub inline fn stdinName(this: Loader) string { + return stdin_name.get(this); + } + + pub fn fromJS(global: *JSC.JSGlobalObject, loader: JSC.JSValue, exception: JSC.C.ExceptionRef) ?Loader { + if (loader.isUndefinedOrNull()) return null; + + if (!loader.jsType().isStringLike()) { + JSC.throwInvalidArguments("loader must be a string", .{}, global.ref(), exception); + return null; + } + + var zig_str = JSC.ZigString.init(""); + loader.toZigString(&zig_str, global); + if (zig_str.len == 0) return null; + + const LoaderMatcher = strings.ExactSizeMatcher(4); + var slice = zig_str.slice(); + if (slice.len > 0 and slice[0] == '.') { + slice = slice[1..]; + } + + return switch (LoaderMatcher.matchLower(slice)) { + LoaderMatcher.case("js") => Loader.js, + LoaderMatcher.case("jsx") => Loader.jsx, + LoaderMatcher.case("ts") => Loader.ts, + LoaderMatcher.case("tsx") => Loader.tsx, + LoaderMatcher.case("css") => Loader.css, + LoaderMatcher.case("file") => Loader.file, + LoaderMatcher.case("json") => Loader.json, + else => { + JSC.throwInvalidArguments("invalid loader – must be js, jsx, tsx, ts, css, file, or json", .{}, global.ref(), exception); + return null; + }, + }; + } pub fn supportsClientEntryPoint(this: Loader) bool { return switch (this) { diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index f2918cffa..eefb5474b 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -448,6 +448,74 @@ pub const PackageJSON = struct { } } + pub fn parseMacrosJSON( + allocator: std.mem.Allocator, + macros: js_ast.Expr, + log: *logger.Log, + json_source: *const logger.Source, + ) MacroMap { + var macro_map = MacroMap{}; + if (macros.data != .e_object) return macro_map; + + const properties = macros.data.e_object.properties; + + for (properties) |property| { + const key = property.key.?.asString(allocator) orelse continue; + if (!resolver.isPackagePath(key)) { + log.addRangeWarningFmt( + json_source, + json_source.rangeOfString(property.key.?.loc), + allocator, + "\"{s}\" is not a package path. \"macros\" remaps package paths to macros. Skipping.", + .{key}, + ) catch unreachable; + continue; + } + + const value = property.value.?; + if (value.data != .e_object) { + log.addWarningFmt( + json_source, + value.loc, + allocator, + "Invalid macro remapping in \"{s}\": expected object where the keys are import names and the value is a string path to replace", + .{key}, + ) catch unreachable; + continue; + } + + const remap_properties = value.data.e_object.properties; + if (remap_properties.len == 0) continue; + + var map = MacroImportReplacementMap.init(allocator); + map.ensureUnusedCapacity(remap_properties.len) catch unreachable; + for (remap_properties) |remap| { + const import_name = remap.key.?.asString(allocator) orelse continue; + const remap_value = remap.value.?; + if (remap_value.data != .e_string or remap_value.data.e_string.utf8.len == 0) { + log.addWarningFmt( + json_source, + remap_value.loc, + allocator, + "Invalid macro remapping for import \"{s}\": expected string to remap to. e.g. \"graphql\": \"bun-macro-relay\" ", + .{import_name}, + ) catch unreachable; + continue; + } + + const remap_value_str = remap_value.data.e_string.utf8; + + map.putAssumeCapacityNoClobber(import_name, remap_value_str); + } + + if (map.count() > 0) { + macro_map.put(allocator, key, map) catch unreachable; + } + } + + return macro_map; + } + pub fn parse( comptime ResolverType: type, r: *ResolverType, @@ -569,64 +637,7 @@ pub const PackageJSON = struct { } if (bun_json.expr.asProperty("macros")) |macros| { - if (macros.expr.data == .e_object) { - const properties = macros.expr.data.e_object.properties; - - for (properties) |property| { - const key = property.key.?.asString(r.allocator) orelse continue; - if (!resolver.isPackagePath(key)) { - r.log.addRangeWarningFmt( - &json_source, - json_source.rangeOfString(property.key.?.loc), - r.allocator, - "\"{s}\" is not a package path. \"macros\" remaps package paths to macros. Skipping.", - .{key}, - ) catch unreachable; - continue; - } - - const value = property.value.?; - if (value.data != .e_object) { - r.log.addWarningFmt( - &json_source, - value.loc, - r.allocator, - "Invalid macro remapping in \"{s}\": expected object where the keys are import names and the value is a string path to replace", - .{key}, - ) catch unreachable; - continue; - } - - const remap_properties = value.data.e_object.properties; - if (remap_properties.len == 0) continue; - - var map = MacroImportReplacementMap.init(r.allocator); - map.ensureUnusedCapacity(remap_properties.len) catch unreachable; - for (remap_properties) |remap| { - const import_name = remap.key.?.asString(r.allocator) orelse continue; - const remap_value = remap.value.?; - if (remap_value.data != .e_string or remap_value.data.e_string.utf8.len == 0) { - r.log.addWarningFmt( - &json_source, - remap_value.loc, - r.allocator, - "Invalid macro remapping for import \"{s}\": expected string to remap to. e.g. \"graphql\": \"bun-macro-relay\" ", - .{import_name}, - ) catch unreachable; - continue; - } - - const remap_value_str = remap_value.data.e_string.utf8; - - map.putAssumeCapacityNoClobber(import_name, remap_value_str); - } - - if (map.count() > 0) { - package_json.macros.put(r.allocator, key, map) catch unreachable; - } - } - // for (var i = 0; i < bundle_.expr.data.e_array.len; i++) { - } + package_json.macros = parseMacrosJSON(r.allocator, macros.expr, r.log, &json_source); } } diff --git a/src/runtime.version b/src/runtime.version index 5a9880de4..3fabcd4a7 100644 --- a/src/runtime.version +++ b/src/runtime.version @@ -1 +1 @@ -21bcc5d5dcff77b6
\ No newline at end of file +f7965245ee95858c
\ No newline at end of file |