diff options
Diffstat (limited to 'src/javascript/jsc/api')
-rw-r--r-- | src/javascript/jsc/api/server.zig | 139 | ||||
-rw-r--r-- | src/javascript/jsc/api/transpiler.zig | 243 |
2 files changed, 311 insertions, 71 deletions
diff --git a/src/javascript/jsc/api/server.zig b/src/javascript/jsc/api/server.zig index 9f38045f0..a76ddf9ab 100644 --- a/src/javascript/jsc/api/server.zig +++ b/src/javascript/jsc/api/server.zig @@ -254,7 +254,7 @@ pub const ServerConfig = struct { args.development = false; } - const PORT_ENV = .{ "PORT", "BUN_PORT" }; + const PORT_ENV = .{ "PORT", "BUN_PORT", "NODE_PORT" }; inline for (PORT_ENV) |PORT| { if (env.get(PORT)) |port| { @@ -428,6 +428,74 @@ pub const ServerConfig = struct { } }; +pub fn NewRequestContextStackAllocator(comptime RequestContext: type, comptime count: usize) type { + // Pre-allocate up to 2048 requests + // use a bitset to track which ones are used + return struct { + buf: [count]RequestContext = undefined, + unused: Set = undefined, + fallback_allocator: std.mem.Allocator = undefined, + + pub const Set = std.bit_set.ArrayBitSet(usize, count); + + pub fn get(this: *@This()) std.mem.Allocator { + this.unused = Set.initFull(); + return std.mem.Allocator.init(this, alloc, resize, free); + } + + fn alloc(self: *@This(), a: usize, b: u29, c: u29, d: usize) ![]u8 { + if (self.unused.findFirstSet()) |i| { + self.unused.unset(i); + return std.mem.asBytes(&self.buf[i]); + } + + return try self.fallback_allocator.rawAlloc(a, b, c, d); + } + + fn resize( + _: *@This(), + _: []u8, + _: u29, + _: usize, + _: u29, + _: usize, + ) ?usize { + unreachable; + } + + fn sliceContainsSlice(container: []u8, slice: []u8) bool { + return @ptrToInt(slice.ptr) >= @ptrToInt(container.ptr) and + (@ptrToInt(slice.ptr) + slice.len) <= (@ptrToInt(container.ptr) + container.len); + } + + fn free( + self: *@This(), + buf: []u8, + buf_align: u29, + return_address: usize, + ) void { + _ = buf_align; + _ = return_address; + const bytes = std.mem.asBytes(&self.buf); + if (sliceContainsSlice(bytes, buf)) { + const index = if (bytes[0..buf.len].ptr != buf.ptr) + (@ptrToInt(buf.ptr) - @ptrToInt(bytes)) / @sizeOf(RequestContext) + else + @as(usize, 0); + + if (comptime Environment.allow_assert) { + std.debug.assert(@intToPtr(*RequestContext, @ptrToInt(buf.ptr)) == &self.buf[index]); + std.debug.assert(!self.unused.isSet(index)); + } + + self.unused.set(index); + } else { + self.fallback_allocator.rawFree(buf, buf_align, return_address); + } + } + }; +} + // This is defined separately partially to work-around an LLVM debugger bug. fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comptime ThisServer: type) type { return struct { @@ -436,6 +504,8 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp pub threadlocal var pool: ?*RequestContext.RequestContextStackAllocator = null; pub threadlocal var pool_allocator: std.mem.Allocator = undefined; + pub const RequestContextStackAllocator = NewRequestContextStackAllocator(RequestContext, 2048); + server: *ThisServer, resp: *App.Response, /// thread-local default heap allocator @@ -459,76 +529,11 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp sendfile: SendfileContext = undefined, request_js_object: JSC.C.JSObjectRef = null, request_body_buf: std.ArrayListUnmanaged(u8) = .{}, + /// Used either for temporary blob data or fallback /// When the response body is a temporary value response_buf_owned: std.ArrayListUnmanaged(u8) = .{}, - // Pre-allocate up to 2048 requests - // use a bitset to track which ones are used - pub const RequestContextStackAllocator = struct { - buf: [2048]RequestContext = undefined, - unused: Set = undefined, - fallback_allocator: std.mem.Allocator = undefined, - - pub const Set = std.bit_set.ArrayBitSet(usize, 2048); - - pub fn get(this: *@This()) std.mem.Allocator { - this.unused = Set.initFull(); - return std.mem.Allocator.init(this, alloc, resize, free); - } - - fn alloc(self: *@This(), a: usize, b: u29, c: u29, d: usize) ![]u8 { - if (self.unused.findFirstSet()) |i| { - self.unused.unset(i); - return std.mem.asBytes(&self.buf[i]); - } - - return try self.fallback_allocator.rawAlloc(a, b, c, d); - } - - fn resize( - _: *@This(), - _: []u8, - _: u29, - _: usize, - _: u29, - _: usize, - ) ?usize { - unreachable; - } - - fn sliceContainsSlice(container: []u8, slice: []u8) bool { - return @ptrToInt(slice.ptr) >= @ptrToInt(container.ptr) and - (@ptrToInt(slice.ptr) + slice.len) <= (@ptrToInt(container.ptr) + container.len); - } - - fn free( - self: *@This(), - buf: []u8, - buf_align: u29, - return_address: usize, - ) void { - _ = buf_align; - _ = return_address; - const bytes = std.mem.asBytes(&self.buf); - if (sliceContainsSlice(bytes, buf)) { - const index = if (bytes[0..buf.len].ptr != buf.ptr) - (@ptrToInt(buf.ptr) - @ptrToInt(bytes)) / @sizeOf(RequestContext) - else - @as(usize, 0); - - if (comptime Environment.allow_assert) { - std.debug.assert(@intToPtr(*RequestContext, @ptrToInt(buf.ptr)) == &self.buf[index]); - std.debug.assert(!self.unused.isSet(index)); - } - - self.unused.set(index); - } else { - self.fallback_allocator.rawFree(buf, buf_align, return_address); - } - } - }; - // TODO: support builtin compression const can_sendfile = !ssl_enabled; diff --git a/src/javascript/jsc/api/transpiler.zig b/src/javascript/jsc/api/transpiler.zig index e7c78ac78..9b4fce4de 100644 --- a/src/javascript/jsc/api/transpiler.zig +++ b/src/javascript/jsc/api/transpiler.zig @@ -41,6 +41,8 @@ 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, @@ -64,6 +66,12 @@ pub const Class = NewClass( .transformSync = .{ .rfn = transformSync, }, + // .resolve = .{ + // .rfn = resolve, + // }, + // .buildSync = .{ + // .rfn = buildSync, + // }, .finalize = finalize, }, .{}, @@ -82,7 +90,6 @@ const default_transform_options: Api.TransformOptions = brk: { opts.disable_hmr = true; opts.platform = Api.Platform.browser; opts.serve = false; - break :brk opts; }; @@ -94,8 +101,9 @@ const TranspilerOptions = struct { tsconfig_buf: []const u8 = "", macros_buf: []const u8 = "", log: logger.Log, - pending_tasks: u32 = 0, 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 @@ -114,6 +122,7 @@ pub const TransformTask = struct { tsconfig: ?*TSConfigJSON = null, loader: Loader, global: *JSGlobalObject, + replace_exports: Runtime.Features.ReplaceableExport.Map = .{}, pub const AsyncTransformTask = JSC.ConcurrentPromiseTask(TransformTask); pub const AsyncTransformEventLoopTask = AsyncTransformTask.EventLoopTask; @@ -129,6 +138,7 @@ pub const TransformTask = struct { .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; @@ -170,6 +180,7 @@ pub const TransformTask = struct { .jsx = jsx, .path = source.path, .virtual_source = &source, + .replace_exports = this.replace_exports, // .allocator = this. }; @@ -265,7 +276,63 @@ pub const TransformTask = struct { } }; -fn transformOptionsFromJSC(ctx: JSC.C.JSContextRef, temp_allocator: std.mem.Allocator, args: *JSC.Node.ArgumentsSlice, exception: JSC.C.ExceptionRef) TranspilerOptions { +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{ + .utf8 = 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) }; @@ -498,6 +565,168 @@ fn transformOptionsFromJSC(ctx: JSC.C.JSContextRef, temp_allocator: std.mem.Allo } } + 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; } @@ -511,7 +740,10 @@ pub fn constructor( 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) + 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)) }; @@ -561,6 +793,8 @@ pub fn constructor( 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; @@ -612,6 +846,7 @@ fn getParseResult(this: *Transpiler, allocator: std.mem.Allocator, code: []const .jsx = jsx, .path = source.path, .virtual_source = &source, + .replace_exports = this.transpiler_options.runtime.replace_exports, // .allocator = this. }; |