From 940570af5904b35784e24f7ea35ccc1fa1f260e7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 24 Sep 2021 16:41:45 -0700 Subject: [PATCH] skeleton code --- src/bundler.zig | 13 +- src/defines.zig | 4 +- src/feature_flags.zig | 2 + src/javascript/jsc/base.zig | 2 + src/javascript/jsc/javascript.zig | 116 ++++++- src/js_parser/js_parser.zig | 554 +++++++++++++++++++----------- src/linker.zig | 6 +- src/options.zig | 21 +- src/runtime.zig | 1 + 9 files changed, 500 insertions(+), 219 deletions(-) diff --git a/src/bundler.zig b/src/bundler.zig index ef38f7d96c6139..d36795d52b1b5d 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -123,7 +123,7 @@ pub const Bundler = struct { // must be pointer array because we can't we don't want the source to point to invalid memory if the array size is reallocated virtual_modules: std.ArrayList(*ClientEntryPoint), - macro_context: ?*js_ast.Macro.MacroContext = null, + macro_context: ?js_ast.Macro.MacroContext = null, pub const isCacheEnabled = cache_files; @@ -826,7 +826,7 @@ pub const Bundler = struct { } else {} for (bundler.options.entry_points) |entry_point| { - if (bundler.options.platform == .bun) continue; + if (bundler.options.platform.isBun()) continue; defer this.bundler.resetStore(); const entry_point_path = bundler.normalizeEntryPointPath(entry_point); @@ -846,7 +846,7 @@ pub const Bundler = struct { bundler.options.framework.?.override_modules_hashes[i] = std.hash.Wyhash.hash(0, key); } } - if (bundler.options.platform == .bun) { + if (bundler.options.platform.isBun()) { if (framework.server.isEnabled()) { const resolved = try bundler.linker.resolver.resolve( bundler.fs.top_level_dir, @@ -1047,7 +1047,7 @@ pub const Bundler = struct { const basename = std.fs.path.basename(std.mem.span(destination)); const extname = std.fs.path.extension(basename); - javascript_bundle.import_from_name = if (bundler.options.platform == .bun) + javascript_bundle.import_from_name = if (bundler.options.platform.isBun()) "/node_modules.server.bun" else try std.fmt.allocPrint( @@ -2406,20 +2406,21 @@ pub const Bundler = struct { // or you're running in SSR // or the file is a node_module opts.features.hot_module_reloading = bundler.options.hot_module_reloading and - bundler.options.platform != .bun and + bundler.options.platform.isNotBun() and (!opts.can_import_from_bundle or (opts.can_import_from_bundle and !path.isNodeModule())); opts.features.react_fast_refresh = opts.features.hot_module_reloading and jsx.parse and bundler.options.jsx.supports_fast_refresh; opts.filepath_hash_for_hmr = file_hash orelse 0; - opts.warn_about_unbundled_modules = bundler.options.platform != .bun; + opts.warn_about_unbundled_modules = bundler.options.platform.isNotBun(); if (bundler.macro_context == null) { bundler.macro_context = js_ast.Macro.MacroContext.init(bundler); } opts.macro_context = &bundler.macro_context.?; + opts.features.is_macro_runtime = bundler.options.platform == .bun_macro; const value = (bundler.resolver.caches.js.parse( allocator, diff --git a/src/defines.zig b/src/defines.zig index 3d08c936efa19a..bb5ccae3bea99d 100644 --- a/src/defines.zig +++ b/src/defines.zig @@ -268,10 +268,10 @@ pub const Define = struct { // Step 2. Swap in certain literal values because those can be constant folded define.identifiers.putAssumeCapacity("undefined", value_define); define.identifiers.putAssumeCapacity("NaN", DefineData{ - .value = js_ast.Expr.Data{ .e_number = &nan_val }, + .value = js_ast.Expr.Data{ .e_number = nan_val }, }); define.identifiers.putAssumeCapacity("Infinity", DefineData{ - .value = js_ast.Expr.Data{ .e_number = &inf_val }, + .value = js_ast.Expr.Data{ .e_number = inf_val }, }); // Step 3. Load user data into hash tables diff --git a/src/feature_flags.zig b/src/feature_flags.zig index a79e1b9b6b0570..9aa8098795d8d2 100644 --- a/src/feature_flags.zig +++ b/src/feature_flags.zig @@ -67,3 +67,5 @@ pub const CSSInJSImportBehavior = enum { // having issues compiling WebKit with this enabled pub const remote_inspector = false; pub const auto_import_buffer = false; + +pub const is_macro_enabled = env.isDebug; diff --git a/src/javascript/jsc/base.zig b/src/javascript/jsc/base.zig index 65300f07c047f4..36f00f59e08460 100644 --- a/src/javascript/jsc/base.zig +++ b/src/javascript/jsc/base.zig @@ -1539,6 +1539,7 @@ export fn MarkedArrayBuffer_deallocator(bytes_: *c_void, ctx_: *c_void) void { pub fn castObj(obj: js.JSObjectRef, comptime Type: type) *Type { return JSPrivateDataPtr.from(js.JSObjectGetPrivate(obj)).as(Type); } +const JSExpr = @import("../../js_ast.zig").Macro.JSExpr; pub const JSPrivateDataPtr = TaggedPointerUnion(.{ ResolveError, @@ -1549,6 +1550,7 @@ pub const JSPrivateDataPtr = TaggedPointerUnion(.{ Headers, Body, Router, + JSExpr, }); pub inline fn GetJSPrivateData(comptime Type: type, ref: js.JSObjectRef) ?*Type { diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig index 352b9caacc025a..8d7626afb6b796 100644 --- a/src/javascript/jsc/javascript.zig +++ b/src/javascript/jsc/javascript.zig @@ -4,6 +4,7 @@ const Fs = @import("../../fs.zig"); const Resolver = @import("../../resolver/resolver.zig"); const ast = @import("../../import_record.zig"); const NodeModuleBundle = @import("../../node_module_bundle.zig").NodeModuleBundle; +const MacroEntryPoint = @import("../../bundler.zig").MacroEntryPoint; const logger = @import("../../logger.zig"); const Api = @import("../../api/schema.zig").Api; const options = @import("../../options.zig"); @@ -11,6 +12,7 @@ const Bundler = @import("../../bundler.zig").Bundler; const ServerEntryPoint = @import("../../bundler.zig").ServerEntryPoint; const js_printer = @import("../../js_printer.zig"); const js_parser = @import("../../js_parser.zig"); +const js_ast = @import("../../js_ast.zig"); const hash_map = @import("../../hash_map.zig"); const http = @import("../../http.zig"); const ImportKind = ast.ImportKind; @@ -87,6 +89,41 @@ pub const Bun = struct { return css_imports_list_strings[0..tail]; } + pub fn registerMacro( + this: void, + ctx: js.JSContextRef, + function: js.JSObjectRef, + thisObject: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, + ) js.JSValueRef { + if (arguments.len != 2 or !js.JSValueIsNumber(ctx, arguments[0])) { + JSError(getAllocator(ctx), "Internal error registering macros: invalid args", .{}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + // TODO: make this faster + const id = @truncate(i32, @floatToInt(i64, js.JSValueToNumber(ctx, arguments[0], exception))); + if (id == -1 or id == 0) { + JSError(getAllocator(ctx), "Internal error registering macros: invalid id", .{}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + + if (!js.JSValueIsObject(ctx, arguments[1]) or !js.JSObjectIsFunction(ctx, arguments[1])) { + JSError(getAllocator(ctx), "Macro must be a function. Received: {s}", .{@tagName(js.JSValueGetType(ctx, arguments[1]))}, ctx, exception); + return js.JSValueMakeUndefined(ctx); + } + + var get_or_put_result = VirtualMachine.vm.macros.getOrPut(id) catch unreachable; + if (get_or_put_result.found_existing) { + js.JSValueUnprotect(ctx, get_or_put_result.value_ptr.*); + } + + js.JSValueProtect(ctx, arguments[1]); + get_or_put_result.value_ptr.* = arguments[1]; + + return js.JSValueMakeUndefined(ctx); + } + pub fn getCWD( this: void, ctx: js.JSContextRef, @@ -403,6 +440,13 @@ pub const Bun = struct { .@"return" = "string", }, }, + .registerMacro = .{ + .rfn = Bun.registerMacro, + .ts = d.ts{ + .name = "registerMacro", + .@"return" = "undefined", + }, + }, }, .{ .main = .{ @@ -462,8 +506,25 @@ pub const VirtualMachine = struct { transpiled_count: usize = 0, resolved_count: usize = 0, had_errors: bool = false, - pub var vm_loaded = false; - pub var vm: *VirtualMachine = undefined; + + macros: MacroMap, + macro_entry_points: std.AutoArrayHashMap(i32, *MacroEntryPoint), + macro_mode: bool = false, + + pub const MacroMap = std.AutoArrayHashMap(i32, js.JSObjectRef); + + pub threadlocal var vm_loaded = false; + pub threadlocal var vm: *VirtualMachine = undefined; + + pub fn enableMacroMode(this: *VirtualMachine) void { + this.bundler.options.platform = .bun_macro; + this.macro_mode = true; + } + + pub fn disableMacroMode(this: *VirtualMachine) void { + this.bundler.options.platform = .bun; + this.macro_mode = false; + } pub fn init( allocator: *std.mem.Allocator, @@ -502,6 +563,9 @@ pub const VirtualMachine = struct { .log = log, .flush_list = std.ArrayList(string).init(allocator), .blobs = try Blob.Group.init(allocator), + + .macros = MacroMap.init(allocator), + .macro_entry_points = @TypeOf(VirtualMachine.vm.macro_entry_points).init(allocator), }; VirtualMachine.vm.bundler.configureLinker(); @@ -771,10 +835,16 @@ pub const VirtualMachine = struct { ret.result = null; ret.path = vm.entry_point.source.path.text; return; + } else if (specifier.len > js_ast.Macro.namespaceWithColon.len and strings.eqlComptimeIgnoreLen(specifier[0..js_ast.Macro.namespaceWithColon.len], js_ast.Macro.namespaceWithColon)) { + ret.result = null; + ret.path = specifier; + return; } + const is_special_source = strings.eqlComptime(source, main_file_name) or js_ast.Macro.isMacroPath(source); + const result = try vm.bundler.resolver.resolve( - if (!strings.eqlComptime(source, main_file_name)) Fs.PathName.init(source).dirWithTrailingSlash() else VirtualMachine.vm.bundler.fs.top_level_dir, + if (!is_special_source) Fs.PathName.init(source).dirWithTrailingSlash() else VirtualMachine.vm.bundler.fs.top_level_dir, specifier, .stmt, ); @@ -1050,6 +1120,46 @@ pub const VirtualMachine = struct { return promise; } + pub fn loadMacroEntryPoint(this: *VirtualMachine, entry_path: string, function_name: string, specifier: string, hash: i32) !*JSInternalPromise { + var entry_point_entry = try this.macro_entry_points.getOrPut(hash); + + if (!entry_point_entry.found_existing) { + var macro_entry_pointer: *MacroEntryPoint = this.allocator.create(MacroEntryPoint) catch unreachable; + entry_point_entry.value_ptr.* = macro_entry_pointer; + try macro_entry_pointer.generate(&this.bundler, Fs.PathName.init(entry_path), function_name, hash, specifier); + } + var entry_point = entry_point_entry.value_ptr.*; + + var promise: *JSInternalPromise = undefined; + // We first import the node_modules bundle. This prevents any potential TDZ issues. + // The contents of the node_modules bundle are lazy, so hopefully this should be pretty quick. + if (this.node_modules != null) { + promise = JSModuleLoader.loadAndEvaluateModule(this.global, ZigString.init(std.mem.span(bun_file_import_path))); + + this.global.vm().drainMicrotasks(); + + while (promise.status(this.global.vm()) == JSPromise.Status.Pending) { + this.global.vm().drainMicrotasks(); + } + + if (promise.status(this.global.vm()) == JSPromise.Status.Rejected) { + return promise; + } + + _ = promise.result(this.global.vm()); + } + + promise = JSModuleLoader.loadAndEvaluateModule(this.global, ZigString.init(entry_point.source.path.text)); + + this.global.vm().drainMicrotasks(); + + while (promise.status(this.global.vm()) == JSPromise.Status.Pending) { + this.global.vm().drainMicrotasks(); + } + + return promise; + } + // When the Error-like object is one of our own, it's best to rely on the object directly instead of serializing it to a ZigException. // This is for: // - BuildError diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig index 1de8c465624ff0..dcccea8a6653ca 100644 --- a/src/js_parser/js_parser.zig +++ b/src/js_parser/js_parser.zig @@ -20,6 +20,7 @@ const ScopeOrderList = std.ArrayListUnmanaged(?ScopeOrder); const JSXFactoryName = "JSX"; const JSXAutomaticName = "jsx_module"; +const MacroRefs = std.AutoArrayHashMap(Ref, u32); pub fn ExpressionTransposer( comptime Kontext: type, @@ -1773,6 +1774,8 @@ pub const Parser = struct { filepath_hash_for_hmr: u32 = 0, features: RuntimeFeatures = RuntimeFeatures{}, + macro_context: *js_ast.Macro.MacroContext = undefined, + warn_about_unbundled_modules: bool = true, // Used when bundling node_modules @@ -1881,6 +1884,9 @@ pub const Parser = struct { } pub fn parse(self: *Parser) !js_ast.Result { + if (self.options.ts and self.options.features.is_macro_runtime) return try self._parse(TSParserMacro); + if (!self.options.ts and self.options.features.is_macro_runtime) return try self._parse(JSParserMacro); + if (self.options.ts and self.options.jsx.parse) { if (self.options.features.react_fast_refresh) { return try self._parse(TSXParserFastRefresh); @@ -2556,8 +2562,8 @@ pub const Prefill = struct { pub var ColumnNumber = [_]u8{ 'c', 'o', 'l', 'u', 'm', 'n', 'N', 'u', 'm', 'b', 'e', 'r' }; }; pub const Value = struct { - pub var EThis = E.This{}; - pub var Zero = E.Number{ .value = 0.0 }; + pub const EThis = E.This{}; + pub const Zero = E.Number{ .value = 0.0 }; }; pub const String = struct { pub var Key = E.String{ .utf8 = &Prefill.StringLiteral.Key }; @@ -2579,8 +2585,8 @@ pub const Prefill = struct { pub var Filename = Expr.Data{ .e_string = &Prefill.String.Filename }; pub var LineNumber = Expr.Data{ .e_string = &Prefill.String.LineNumber }; pub var ColumnNumber = Expr.Data{ .e_string = &Prefill.String.ColumnNumber }; - pub var This = Expr.Data{ .e_this = E.This{} }; - pub var Zero = Expr.Data{ .e_number = &Value.Zero }; + pub const This = Expr.Data{ .e_this = E.This{} }; + pub const Zero = Expr.Data{ .e_number = Value.Zero }; }; pub const Runtime = struct { pub var JSXFilename = "__jsxFilename"; @@ -2603,9 +2609,15 @@ pub const ImportOrRequireScanResults = struct { import_records: List(ImportRecord), }; +const JSXTransformType = enum { + none, + react, + macro, +}; + const ParserFeatures = struct { typescript: bool = false, - jsx: bool = false, + jsx: JSXTransformType = JSXTransformType.none, scan_only: bool = false, // *** How React Fast Refresh works *** @@ -2667,7 +2679,7 @@ pub fn NewParser( comptime js_parser_features: ParserFeatures, ) type { const is_typescript_enabled = js_parser_features.typescript; - const is_jsx_enabled = js_parser_features.jsx; + const is_jsx_enabled = js_parser_features.jsx != .none; const only_scan_imports_and_do_not_visit = js_parser_features.scan_only; const is_react_fast_refresh_enabled = js_parser_features.react_fast_refresh; @@ -2679,6 +2691,8 @@ pub fn NewParser( // public only because of Binding.ToExpr return struct { const P = @This(); + pub const jsx_transform_type: JSXTransformType = js_parser_features.jsx; + macro_refs: MacroRefs = undefined, allocator: *std.mem.Allocator, options: Parser.Options, log: *logger.Log, @@ -3579,22 +3593,28 @@ pub fn NewParser( p.recordUsage(p.runtime_imports.__HMRClient.?.ref); } - if (is_jsx_enabled) { - if (p.options.jsx.development) { - p.jsx_filename = p.declareGeneratedSymbol(.other, "jsxFilename") catch unreachable; - } - p.jsx_fragment = p.declareGeneratedSymbol(.other, "Fragment") catch unreachable; - p.jsx_runtime = p.declareGeneratedSymbol(.other, "jsx") catch unreachable; - p.jsxs_runtime = p.declareGeneratedSymbol(.other, "jsxs") catch unreachable; - p.jsx_factory = p.declareGeneratedSymbol(.other, "Factory") catch unreachable; + switch (comptime jsx_transform_type) { + .react => { + if (p.options.jsx.development) { + p.jsx_filename = p.declareGeneratedSymbol(.other, "jsxFilename") catch unreachable; + } + p.jsx_fragment = p.declareGeneratedSymbol(.other, "Fragment") catch unreachable; + p.jsx_runtime = p.declareGeneratedSymbol(.other, "jsx") catch unreachable; + p.jsxs_runtime = p.declareGeneratedSymbol(.other, "jsxs") catch unreachable; + p.jsx_factory = p.declareGeneratedSymbol(.other, "Factory") catch unreachable; - if (p.options.jsx.factory.len > 1 or FeatureFlags.jsx_runtime_is_cjs) { - p.jsx_classic = p.declareGeneratedSymbol(.other, "ClassicImportSource") catch unreachable; - } + if (p.options.jsx.factory.len > 1 or FeatureFlags.jsx_runtime_is_cjs) { + p.jsx_classic = p.declareGeneratedSymbol(.other, "ClassicImportSource") catch unreachable; + } - if (p.options.jsx.import_source.len > 0) { - p.jsx_automatic = p.declareGeneratedSymbol(.other, "ImportSource") catch unreachable; - } + if (p.options.jsx.import_source.len > 0) { + p.jsx_automatic = p.declareGeneratedSymbol(.other, "ImportSource") catch unreachable; + } + }, + .macro => { + p.jsx_fragment = p.declareGeneratedSymbol(.other, "Fragment") catch unreachable; + }, + else => {}, } } @@ -6109,6 +6129,15 @@ pub fn NewParser( p.import_records.items[stmt.import_record_index].was_originally_bare_import = was_originally_bare_import; try p.lexer.expectOrInsertSemicolon(); + const is_macro = FeatureFlags.is_macro_enabled and js_ast.Macro.isMacroPath(path.text); + + if (is_macro) { + p.import_records.items[stmt.import_record_index].path.namespace = js_ast.Macro.namespace; + if (comptime only_scan_imports_and_do_not_visit) { + p.import_records.items[stmt.import_record_index].path.is_disabled = true; + } + } + if (stmt.star_name_loc) |star| { const name = p.loadNameFromRef(stmt.namespace_ref); stmt.namespace_ref = try p.declareSymbol(.import, star, name); @@ -6118,6 +6147,17 @@ pub fn NewParser( .import_record_index = stmt.import_record_index, }) catch unreachable; } + + if (is_macro) { + p.log.addErrorFmt( + p.source, + star, + p.allocator, + "Macro cannot be a * import, must be default or an {{item}}", + .{}, + ) catch unreachable; + return error.SyntaxError; + } } else { var path_name = fs.PathName.init(strings.append(p.allocator, "import_", path.text) catch unreachable); const name = try path_name.nonUniqueNameString(p.allocator); @@ -6140,6 +6180,10 @@ pub fn NewParser( .import_record_index = stmt.import_record_index, }) catch unreachable; } + + if (is_macro) { + try p.macro_refs.put(ref, stmt.import_record_index); + } } if (stmt.items.len > 0) { @@ -6158,6 +6202,10 @@ pub fn NewParser( .import_record_index = stmt.import_record_index, }) catch unreachable; } + + if (is_macro) { + try p.macro_refs.put(ref, stmt.import_record_index); + } } } @@ -10860,216 +10908,289 @@ pub fn NewParser( p.panic("Unexpected private identifier. This is an internal error - not your fault.", .{}); }, .e_jsx_element => |e_| { - const tag: Expr = tagger: { - if (e_.tag) |_tag| { - break :tagger p.visitExpr(_tag); - } else { - break :tagger p.jsxStringsToMemberExpression(expr.loc, p.jsx_fragment.ref); - } - }; - - for (e_.properties) |property, i| { - if (property.kind != .spread) { - e_.properties[i].key = p.visitExpr(e_.properties[i].key.?); - } - - if (property.value != null) { - e_.properties[i].value = p.visitExpr(e_.properties[i].value.?); - } + switch (comptime jsx_transform_type) { + .macro => { + const IdentifierOrNodeType = union(Tag) { + identifier: Expr, + expression: Expr.Tag, + pub const Tag = enum { identifier, expression }; + }; + const tag: IdentifierOrNodeType = tagger: { + if (e_.tag) |_tag| { + switch (_tag.data) { + .e_string => |str| { + if (Expr.Tag.find(str.utf8)) |tagname| { + break :tagger IdentifierOrNodeType{ .expression = tagname }; + } - if (property.initializer != null) { - e_.properties[i].initializer = p.visitExpr(e_.properties[i].initializer.?); - } - } + p.log.addErrorFmt( + p.source, + expr.loc, + p.allocator, + "Invalid expression tag: \"<{s}>\". Valid tags are:\n" ++ Expr.Tag.valid_names_list ++ "\n", + .{str.utf8}, + ) catch unreachable; + break :tagger IdentifierOrNodeType{ .identifier = p.visitExpr(_tag) }; + }, + else => { + break :tagger IdentifierOrNodeType{ .identifier = p.visitExpr(_tag) }; + }, + } + } else { + break :tagger IdentifierOrNodeType{ .expression = Expr.Tag.e_array }; + } + }; - const runtime = if (p.options.jsx.runtime == .automatic and !e_.flags.is_key_before_rest) options.JSX.Runtime.automatic else options.JSX.Runtime.classic; - var children_count = e_.children.len; + for (e_.properties) |property, i| { + if (property.kind != .spread) { + e_.properties[i].key = p.visitExpr(e_.properties[i].key.?); + } - const is_childless_tag = FeatureFlags.react_specific_warnings and children_count > 0 and tag.data == .e_string and tag.data.e_string.isUTF8() and js_lexer.ChildlessJSXTags.has(tag.data.e_string.utf8); + if (property.value != null) { + e_.properties[i].value = p.visitExpr(e_.properties[i].value.?); + } - children_count = if (is_childless_tag) 0 else children_count; + if (property.initializer != null) { + e_.properties[i].initializer = p.visitExpr(e_.properties[i].initializer.?); + } + } - if (children_count != e_.children.len) { - // Error: meta is a void element tag and must neither have `children` nor use `dangerouslySetInnerHTML`. - // ^ from react-dom - p.log.addWarningFmt(p.source, tag.loc, p.allocator, "<{s} /> is a void element and must not have \"children\"", .{tag.data.e_string.utf8}) catch {}; - } + return p.e(E.Missing{}, expr.loc); + }, + .react => { + const tag: Expr = tagger: { + if (e_.tag) |_tag| { + break :tagger p.visitExpr(_tag); + } else { + break :tagger p.jsxStringsToMemberExpression(expr.loc, p.jsx_fragment.ref); + } + }; - // TODO: maybe we should split these into two different AST Nodes - // That would reduce the amount of allocations a little - switch (runtime) { - .classic => { - // Arguments to createElement() - const args = p.allocator.alloc(Expr, 2 + children_count) catch unreachable; - // There are at least two args: - // - name of the tag - // - props - var i: usize = 1; - args[0] = tag; - if (e_.properties.len > 0) { - for (e_.properties) |prop, prop_i| { - if (prop.key) |key| { - e_.properties[prop_i].key = p.visitExpr(key); - } + for (e_.properties) |property, i| { + if (property.kind != .spread) { + e_.properties[i].key = p.visitExpr(e_.properties[i].key.?); + } - if (prop.value) |val| { - e_.properties[prop_i].value = p.visitExpr(val); - } + if (property.value != null) { + e_.properties[i].value = p.visitExpr(e_.properties[i].value.?); } - if (e_.key) |key| { - var props = p.allocator.alloc(G.Property, e_.properties.len + 1) catch unreachable; - std.mem.copy(G.Property, props, e_.properties); - props[props.len - 1] = G.Property{ .key = Expr{ .loc = key.loc, .data = keyExprData }, .value = key }; - args[1] = p.e(E.Object{ .properties = props }, expr.loc); - } else { - args[1] = p.e(E.Object{ .properties = e_.properties }, expr.loc); + if (property.initializer != null) { + e_.properties[i].initializer = p.visitExpr(e_.properties[i].initializer.?); } - i = 2; - } else { - args[1] = p.e(E.Null{}, expr.loc); - i = 2; } - for (e_.children[0..children_count]) |child| { - args[i] = p.visitExpr(child); - i += @intCast(usize, @boolToInt(args[i].data != .e_missing)); + const runtime = if (p.options.jsx.runtime == .automatic and !e_.flags.is_key_before_rest) options.JSX.Runtime.automatic else options.JSX.Runtime.classic; + var children_count = e_.children.len; + + const is_childless_tag = FeatureFlags.react_specific_warnings and children_count > 0 and tag.data == .e_string and tag.data.e_string.isUTF8() and js_lexer.ChildlessJSXTags.has(tag.data.e_string.utf8); + + children_count = if (is_childless_tag) 0 else children_count; + + if (children_count != e_.children.len) { + // Error: meta is a void element tag and must neither have `children` nor use `dangerouslySetInnerHTML`. + // ^ from react-dom + p.log.addWarningFmt(p.source, tag.loc, p.allocator, "<{s} /> is a void element and must not have \"children\"", .{tag.data.e_string.utf8}) catch {}; } - // Call createElement() - return p.e(E.Call{ - .target = p.jsxStringsToMemberExpression(expr.loc, p.jsx_factory.ref), - .args = args[0..i], - // Enable tree shaking - .can_be_unwrapped_if_unused = !p.options.ignore_dce_annotations, - }, expr.loc); - }, - // function jsxDEV(type, config, maybeKey, source, self) { - .automatic => { - // Either: - // jsxDEV(type, arguments, key, isStaticChildren, source, self) - // jsx(type, arguments, key) - const args = p.allocator.alloc(Expr, if (p.options.jsx.development) @as(usize, 6) else @as(usize, 4)) catch unreachable; - args[0] = tag; - var props = List(G.Property).fromOwnedSlice(p.allocator, e_.properties); - // arguments needs to be like - // { - // ...props, - // children: [el1, el2] - // } + // TODO: maybe we should split these into two different AST Nodes + // That would reduce the amount of allocations a little + switch (runtime) { + .classic => { + // Arguments to createElement() + const args = p.allocator.alloc(Expr, 2 + children_count) catch unreachable; + // There are at least two args: + // - name of the tag + // - props + var i: usize = 1; + args[0] = tag; + if (e_.properties.len > 0) { + for (e_.properties) |prop, prop_i| { + if (prop.key) |key| { + e_.properties[prop_i].key = p.visitExpr(key); + } + + if (prop.value) |val| { + e_.properties[prop_i].value = p.visitExpr(val); + } + } - const is_static_jsx = e_.children.len == 0 or e_.children.len > 1 or e_.children[0].data != .e_array; - - // if (p.options.jsx.development) { - switch (children_count) { - 0 => {}, - 1 => { - // static jsx must always be an array - if (is_static_jsx) { - const children_key = Expr{ .data = jsxChildrenKeyData, .loc = expr.loc }; - e_.children[0] = p.visitExpr(e_.children[0]); - props.append(G.Property{ - .key = children_key, - .value = p.e(E.Array{ - .items = e_.children[0..children_count], - .is_single_line = e_.children.len < 2, - }, expr.loc), - }) catch unreachable; + if (e_.key) |key| { + var props = p.allocator.alloc(G.Property, e_.properties.len + 1) catch unreachable; + std.mem.copy(G.Property, props, e_.properties); + props[props.len - 1] = G.Property{ .key = Expr{ .loc = key.loc, .data = keyExprData }, .value = key }; + args[1] = p.e(E.Object{ .properties = props }, expr.loc); + } else { + args[1] = p.e(E.Object{ .properties = e_.properties }, expr.loc); + } + i = 2; } else { - const children_key = Expr{ .data = jsxChildrenKeyData, .loc = expr.loc }; - props.append(G.Property{ - .key = children_key, - .value = p.visitExpr(e_.children[0]), - }) catch unreachable; + args[1] = p.e(E.Null{}, expr.loc); + i = 2; } - }, - else => { - for (e_.children[0..children_count]) |child, i| { - e_.children[i] = p.visitExpr(child); + + for (e_.children[0..children_count]) |child| { + args[i] = p.visitExpr(child); + i += @intCast(usize, @boolToInt(args[i].data != .e_missing)); } - const children_key = Expr{ .data = jsxChildrenKeyData, .loc = expr.loc }; - props.append(G.Property{ - .key = children_key, - .value = p.e(E.Array{ - .items = e_.children[0..children_count], - .is_single_line = e_.children.len < 2, - }, expr.loc), - }) catch unreachable; + + // Call createElement() + return p.e(E.Call{ + .target = p.jsxStringsToMemberExpression(expr.loc, p.jsx_factory.ref), + .args = args[0..i], + // Enable tree shaking + .can_be_unwrapped_if_unused = !p.options.ignore_dce_annotations, + }, expr.loc); }, - } + // function jsxDEV(type, config, maybeKey, source, self) { + .automatic => { + // Either: + // jsxDEV(type, arguments, key, isStaticChildren, source, self) + // jsx(type, arguments, key) + const args = p.allocator.alloc(Expr, if (p.options.jsx.development) @as(usize, 6) else @as(usize, 4)) catch unreachable; + args[0] = tag; + var props = List(G.Property).fromOwnedSlice(p.allocator, e_.properties); + // arguments needs to be like + // { + // ...props, + // children: [el1, el2] + // } - args[1] = p.e(E.Object{ - .properties = props.toOwnedSlice(), - }, expr.loc); + const is_static_jsx = e_.children.len == 0 or e_.children.len > 1 or e_.children[0].data != .e_array; + + // if (p.options.jsx.development) { + switch (children_count) { + 0 => {}, + 1 => { + // static jsx must always be an array + if (is_static_jsx) { + const children_key = Expr{ .data = jsxChildrenKeyData, .loc = expr.loc }; + e_.children[0] = p.visitExpr(e_.children[0]); + props.append(G.Property{ + .key = children_key, + .value = p.e(E.Array{ + .items = e_.children[0..children_count], + .is_single_line = e_.children.len < 2, + }, expr.loc), + }) catch unreachable; + } else { + const children_key = Expr{ .data = jsxChildrenKeyData, .loc = expr.loc }; + props.append(G.Property{ + .key = children_key, + .value = p.visitExpr(e_.children[0]), + }) catch unreachable; + } + }, + else => { + for (e_.children[0..children_count]) |child, i| { + e_.children[i] = p.visitExpr(child); + } + const children_key = Expr{ .data = jsxChildrenKeyData, .loc = expr.loc }; + props.append(G.Property{ + .key = children_key, + .value = p.e(E.Array{ + .items = e_.children[0..children_count], + .is_single_line = e_.children.len < 2, + }, expr.loc), + }) catch unreachable; + }, + } - if (e_.key) |key| { - args[2] = key; - } else { - // if (maybeKey !== undefined) - args[2] = Expr{ - .loc = expr.loc, - .data = .{ - .e_undefined = E.Undefined{}, - }, - }; - } + args[1] = p.e(E.Object{ + .properties = props.toOwnedSlice(), + }, expr.loc); - if (p.options.jsx.development) { - // is the return type of the first child an array? - // It's dynamic - // Else, it's static - args[3] = Expr{ - .loc = expr.loc, - .data = .{ - .e_boolean = .{ - .value = is_static_jsx, - }, - }, - }; + if (e_.key) |key| { + args[2] = key; + } else { + // if (maybeKey !== undefined) + args[2] = Expr{ + .loc = expr.loc, + .data = .{ + .e_undefined = E.Undefined{}, + }, + }; + } - var source = p.allocator.alloc(G.Property, 2) catch unreachable; - p.recordUsage(p.jsx_filename.ref); - source[0] = G.Property{ - .key = Expr{ .loc = expr.loc, .data = Prefill.Data.Filename }, - .value = p.e(E.Identifier{ .ref = p.jsx_filename.ref }, expr.loc), - }; + if (p.options.jsx.development) { + // is the return type of the first child an array? + // It's dynamic + // Else, it's static + args[3] = Expr{ + .loc = expr.loc, + .data = .{ + .e_boolean = .{ + .value = is_static_jsx, + }, + }, + }; - source[1] = G.Property{ - .key = Expr{ .loc = expr.loc, .data = Prefill.Data.LineNumber }, - .value = p.e(E.Number{ .value = @intToFloat(f64, expr.loc.start) }, expr.loc), - }; + var source = p.allocator.alloc(G.Property, 2) catch unreachable; + p.recordUsage(p.jsx_filename.ref); + source[0] = G.Property{ + .key = Expr{ .loc = expr.loc, .data = Prefill.Data.Filename }, + .value = p.e(E.Identifier{ .ref = p.jsx_filename.ref }, expr.loc), + }; + + source[1] = G.Property{ + .key = Expr{ .loc = expr.loc, .data = Prefill.Data.LineNumber }, + .value = p.e(E.Number{ .value = @intToFloat(f64, expr.loc.start) }, expr.loc), + }; - // Officially, they ask for columnNumber. But I don't see any usages of it in the code! - // source[2] = G.Property{ - // .key = Expr{ .loc = expr.loc, .data = Prefill.Data.ColumnNumber }, - // .value = p.e(E.Number{ .value = @intToFloat(f64, expr.loc.start) }, expr.loc), - // }; - - args[4] = p.e(E.Object{ - .properties = source, - }, expr.loc); - args[5] = Expr{ .data = Prefill.Data.This, .loc = expr.loc }; - } - - return p.e(E.Call{ - .target = p.jsxStringsToMemberExpressionAutomatic(expr.loc, is_static_jsx), - .args = args, - // Enable tree shaking - .can_be_unwrapped_if_unused = !p.options.ignore_dce_annotations, - .was_jsx_element = true, - }, expr.loc); + // Officially, they ask for columnNumber. But I don't see any usages of it in the code! + // source[2] = G.Property{ + // .key = Expr{ .loc = expr.loc, .data = Prefill.Data.ColumnNumber }, + // .value = p.e(E.Number{ .value = @intToFloat(f64, expr.loc.start) }, expr.loc), + // }; + + args[4] = p.e(E.Object{ + .properties = source, + }, expr.loc); + args[5] = Expr{ .data = Prefill.Data.This, .loc = expr.loc }; + } + + return p.e(E.Call{ + .target = p.jsxStringsToMemberExpressionAutomatic(expr.loc, is_static_jsx), + .args = args, + // Enable tree shaking + .can_be_unwrapped_if_unused = !p.options.ignore_dce_annotations, + .was_jsx_element = true, + }, expr.loc); + }, + else => unreachable, + } }, else => unreachable, } }, .e_template => |e_| { + for (e_.parts) |*part| { + part.value = p.visitExpr(part.value); + } + if (e_.tag) |tag| { e_.tag = p.visitExpr(tag); - } - for (e_.parts) |*part| { - part.value = p.visitExpr(part.value); + if (comptime FeatureFlags.is_macro_enabled and jsx_transform_type != .macro) { + if (e_.tag.?.data == .e_import_identifier) { + const ref = e_.tag.?.data.e_import_identifier.ref; + if (p.macro_refs.get(ref)) |import_record_id| { + const name = p.symbols.items[ref.inner_index].original_name; + const record = &p.import_records.items[import_record_id]; + return p.options.macro_context.call( + record.path.text, + p.source.path.sourceDir(), + p.log, + p.source, + record.range, + expr, + &.{}, + name, + ) catch return expr; + } + } + } } }, @@ -11818,6 +11939,7 @@ pub fn NewParser( e_.target = p.visitExprInOut(e_.target, ExprIn{ .has_chain_parent = (e_.optional_chain orelse js_ast.OptionalChain.start) == .ccontinue, }); + // TODO: wan about import namespace call var has_spread = false; for (e_.args) |*arg| { @@ -11837,6 +11959,26 @@ pub fn NewParser( } } + if (comptime FeatureFlags.is_macro_enabled and jsx_transform_type != .macro) { + if (e_.target.data == .e_import_identifier) { + const ref = e_.target.data.e_import_identifier.ref; + if (p.macro_refs.get(ref)) |import_record_id| { + const name = p.symbols.items[ref.inner_index].original_name; + const record = &p.import_records.items[import_record_id]; + return p.options.macro_context.call( + record.path.text, + p.source.path.sourceDir(), + p.log, + p.source, + record.range, + expr, + &.{}, + name, + ) catch return expr; + } + } + } + return expr; }, .e_new => |e_| { @@ -15090,18 +15232,26 @@ pub fn NewParser( // '../../build/macos-x86_64/bun node_modules/react-dom/cjs/react-dom.development.js --resolve=disable' ran // 1.02 ± 0.07 times faster than '../../bun.before-comptime-js-parser node_modules/react-dom/cjs/react-dom.development.js --resolve=disable' const JavaScriptParser = NewParser(.{}); -const JSXParser = NewParser(.{ .jsx = true }); -const TSXParser = NewParser(.{ .jsx = true, .typescript = true }); +const JSXParser = NewParser(.{ .jsx = .react }); +const TSXParser = NewParser(.{ .jsx = .react, .typescript = true }); const TypeScriptParser = NewParser(.{ .typescript = true }); +const JSParserMacro = NewParser(.{ + .jsx = .macro, +}); +const TSParserMacro = NewParser(.{ + .jsx = .react, + .typescript = true, +}); + const JavaScriptParserFastRefresh = NewParser(.{ .react_fast_refresh = true }); -const JSXParserFastRefresh = NewParser(.{ .jsx = true, .react_fast_refresh = true }); -const TSXParserFastRefresh = NewParser(.{ .jsx = true, .typescript = true, .react_fast_refresh = true }); +const JSXParserFastRefresh = NewParser(.{ .jsx = .react, .react_fast_refresh = true }); +const TSXParserFastRefresh = NewParser(.{ .jsx = .react, .typescript = true, .react_fast_refresh = true }); const TypeScriptParserFastRefresh = NewParser(.{ .typescript = true, .react_fast_refresh = true }); const JavaScriptImportScanner = NewParser(.{ .scan_only = true }); -const JSXImportScanner = NewParser(.{ .jsx = true, .scan_only = true }); -const TSXImportScanner = NewParser(.{ .jsx = true, .typescript = true, .scan_only = true }); +const JSXImportScanner = NewParser(.{ .jsx = .react, .scan_only = true }); +const TSXImportScanner = NewParser(.{ .jsx = .react, .typescript = true, .scan_only = true }); const TypeScriptImportScanner = NewParser(.{ .typescript = true, .scan_only = true }); // The "await" and "yield" expressions are never allowed in argument lists but diff --git a/src/linker.zig b/src/linker.zig index 4e42423b8013b0..16d6d834218086 100644 --- a/src/linker.zig +++ b/src/linker.zig @@ -166,7 +166,7 @@ pub const Linker = struct { } pub inline fn nodeModuleBundleImportPath(this: *const ThisLinker) string { - if (this.options.platform == .bun) return "/node_modules.server.bun"; + if (this.options.platform.isBun()) return "/node_modules.server.bun"; return if (this.options.node_modules_bundle_url.len > 0) this.options.node_modules_bundle_url @@ -197,7 +197,7 @@ pub const Linker = struct { comptime ignore_runtime: bool, ) !void { var needs_runtime = result.ast.uses_exports_ref or result.ast.uses_module_ref or result.ast.runtime_imports.hasAny(); - const source_dir = if (file_path.is_symlink and file_path.pretty.len > 0 and import_path_format == .absolute_url and linker.options.platform != .bun) + const source_dir = if (file_path.is_symlink and file_path.pretty.len > 0 and import_path_format == .absolute_url and linker.options.platform.isNotBun()) Fs.PathName.init(file_path.pretty).dirWithTrailingSlash() else file_path.sourceDir(); @@ -644,7 +644,7 @@ pub const Linker = struct { import_record.path = try linker.generateImportPath( source_dir, - if (path.is_symlink and import_path_format == .absolute_url and linker.options.platform != .bun) path.pretty else path.text, + if (path.is_symlink and import_path_format == .absolute_url and linker.options.platform.isNotBun()) path.pretty else path.text, if (resolve_result.package_json) |package_json| package_json.version else "", Bundler.isCacheEnabled and loader == .file, path.namespace, diff --git a/src/options.zig b/src/options.zig index 78c302807d33ec..af574ca1dd847a 100644 --- a/src/options.zig +++ b/src/options.zig @@ -338,18 +338,33 @@ pub const Platform = enum { neutral, browser, bun, + bun_macro, node, + pub inline fn isBun(this: Platform) bool { + return switch (this) { + .bun_macro, .bun => true, + else => false, + }; + } + + pub inline fn isNotBun(this: Platform) bool { + return switch (this) { + .bun_macro, .bun => false, + else => true, + }; + } + pub inline fn isClient(this: Platform) bool { return switch (this) { - .bun => false, + .bun_macro, .bun => false, else => true, }; } pub inline fn supportsBrowserField(this: Platform) bool { return switch (this) { - .neutral, .browser, .bun => true, + .bun_macro, .neutral, .browser, .bun => true, else => false, }; } @@ -360,7 +375,7 @@ pub const Platform = enum { pub inline fn processBrowserDefineValue(this: Platform) ?string { return switch (this) { .browser => browser_define_value_true, - .bun, .node => browser_define_value_false, + .bun_macro, .bun, .node => browser_define_value_false, else => null, }; } diff --git a/src/runtime.zig b/src/runtime.zig index 77a0430d192253..c8aed0dc7ebd6d 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -205,6 +205,7 @@ pub const Runtime = struct { hot_module_reloading: bool = false, hot_module_reloading_entry: bool = false, keep_names_for_arrow_functions: bool = true, + is_macro_runtime: bool = false, }; pub const Names = struct {