diff --git a/README.md b/README.md index 996378b49d..7ecf3e6f47 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Bun is a new: All in one fast & easy-to-use tool. Instead of 1,000 node_modules for development, you only need Bun. -Bun is experimental software. Join [Bun's Discord](https://bun.sh/discord) for help and have a look at [things that don't work yet](#things-that-dont-work-yet). Most notably, this early version of Bun is not for building +Bun is experimental software. Join [Bun's Discord](https://bun.sh/discord) for help and have a look at [things that don't work yet](#things-that-dont-work-yet). ## Install: @@ -100,7 +100,18 @@ Bun is a project with incredibly large scope, and it's early days. | [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) (in SSR) | Bun.js | | `bun run` command | Bun.js | -All of these are planned to work eventually, maybe with the exception of `setTimeout` inside of SSR. +## Limitations & intended usage + +Bun is great for building websites & webapps. For libraries, consider using Rollup or esbuild instead. Bun currently doesn't minify code and Bun's dead code elimination doesn't look beyond the current file. + +Bun is focused on: + +- Development, not production +- Compatibility with existing frameworks & tooling + +Ideally, most projects can use Bun with their existing tooling while making few changes to their codebase. That means using Bun in development, and continuing to use Webpack, esbuild, or another bundler in production. Using two bundlers might sound strange at first, but after all the production-only AST transforms, minification, and special development/production-only imported files...it's not far from the status quo. + +# Configuration # Building from source diff --git a/examples/hello-next/babel.js.REMOVED.git-id b/examples/hello-next/babel.js.REMOVED.git-id deleted file mode 100644 index c6e41fc711..0000000000 --- a/examples/hello-next/babel.js.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -e2e5ce1979cde7a27f105fc408e375bbc6a9d78d \ No newline at end of file diff --git a/examples/hello-next/bun-framework-next/bun-error/powered-by.png b/examples/hello-next/bun-framework-next/bun-error/powered-by.png new file mode 100644 index 0000000000..7e71f1357d Binary files /dev/null and b/examples/hello-next/bun-framework-next/bun-error/powered-by.png differ diff --git a/examples/hello-next/bun-framework-next/bun-error/powered-by.webp b/examples/hello-next/bun-framework-next/bun-error/powered-by.webp new file mode 100644 index 0000000000..0f48488ead Binary files /dev/null and b/examples/hello-next/bun-framework-next/bun-error/powered-by.webp differ diff --git a/examples/hello-next/bun-framework-next/bun-runtime-error.ts b/examples/hello-next/bun-framework-next/bun-runtime-error.ts new file mode 100644 index 0000000000..331040b36c --- /dev/null +++ b/examples/hello-next/bun-framework-next/bun-runtime-error.ts @@ -0,0 +1,163 @@ +// Based on https://github.com/stacktracejs/error-stack-parser/blob/master/error-stack-parser.js + +import type { + StackFrame as StackFrameType, + StackFramePosition, + StackFrameScope, +} from "../../../src/api/schema"; + +export class StackFrame implements StackFrameType { + function_name: string; + file: string; + position: StackFramePosition; + scope: StackFrameScope; + lineText: string = ""; + constructor({ + functionName: function_name = "", + fileName: file = "", + lineNumber: line = -1, + columnNumber: column = -1, + source = "", + }) { + this.function_name = function_name; + this.file = file; + if (source) this.lineText = source; + this.scope = 3; + this.position = { + line: line, + source_offset: -1, + line_start: -1, + line_stop: -1, + column_start: column, + column_stop: -1, + expression_start: -1, + expression_stop: -1, + }; + } +} + +const FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/; +const CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; +const SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/; + +export default class RuntimeError { + original: Error; + stack: StackFrame[]; + + static from(error: Error) { + const runtime = new RuntimeError(); + runtime.original = error; + runtime.stack = this.parseStack(error); + return RuntimeError; + } + + /** + * Given an Error object, extract the most information from it. + * + * @param {Error} error object + * @return {Array} of StackFrames + */ + static parseStack(error) { + if (error.stack && error.stack.match(CHROME_IE_STACK_REGEXP)) { + return this.parseV8OrIE(error); + } else if (error.stack) { + return this.parseFFOrSafari(error); + } else { + return []; + } + } + + // Separate line and column numbers from a string of the form: (URI:Line:Column) + static extractLocation(urlLike) { + // Fail-fast but return locations like "(native)" + if (urlLike.indexOf(":") === -1) { + return [urlLike]; + } + + var regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/; + var parts = regExp.exec(urlLike.replace(/[()]/g, "")); + return [parts[1], parts[2] || undefined, parts[3] || undefined]; + } + + static parseV8OrIE(error) { + var filtered = error.stack.split("\n").filter(function (line) { + return !!line.match(CHROME_IE_STACK_REGEXP); + }, this); + + return filtered.map(function (line) { + if (line.indexOf("(eval ") > -1) { + // Throw away eval information until we implement stacktrace.js/stackframe#8 + line = line + .replace(/eval code/g, "eval") + .replace(/(\(eval at [^()]*)|(\),.*$)/g, ""); + } + var sanitizedLine = line.replace(/^\s+/, "").replace(/\(eval code/g, "("); + + // capture and preseve the parenthesized location "(/foo/my bar.js:12:87)" in + // case it has spaces in it, as the string is split on \s+ later on + var location = sanitizedLine.match(/ (\((.+):(\d+):(\d+)\)$)/); + + // remove the parenthesized location from the line, if it was matched + sanitizedLine = location + ? sanitizedLine.replace(location[0], "") + : sanitizedLine; + + var tokens = sanitizedLine.split(/\s+/).slice(1); + // if a location was matched, pass it to extractLocation() otherwise pop the last token + var locationParts = this.extractLocation( + location ? location[1] : tokens.pop() + ); + var functionName = tokens.join(" ") || undefined; + var fileName = + ["eval", ""].indexOf(locationParts[0]) > -1 + ? undefined + : locationParts[0]; + + return new StackFrame({ + functionName: functionName, + fileName: fileName, + lineNumber: locationParts[1], + columnNumber: locationParts[2], + source: line, + }); + }, this); + } + + static parseFFOrSafari(error) { + var filtered = error.stack.split("\n").filter(function (line) { + return !line.match(SAFARI_NATIVE_CODE_REGEXP); + }, this); + + return filtered.map(function (line) { + // Throw away eval information until we implement stacktrace.js/stackframe#8 + if (line.indexOf(" > eval") > -1) { + line = line.replace( + / line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, + ":$1" + ); + } + + if (line.indexOf("@") === -1 && line.indexOf(":") === -1) { + // Safari eval frames only have function names and nothing else + return new StackFrame({ + functionName: line, + }); + } else { + var functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/; + var matches = line.match(functionNameRegex); + var functionName = matches && matches[1] ? matches[1] : undefined; + var locationParts = this.extractLocation( + line.replace(functionNameRegex, "") + ); + + return new StackFrame({ + functionName: functionName, + fileName: locationParts[0], + lineNumber: locationParts[1], + columnNumber: locationParts[2], + source: line, + }); + } + }, this); + } +} diff --git a/src/api/schema.d.ts b/src/api/schema.d.ts index 3fd082f51a..159cf40617 100644 --- a/src/api/schema.d.ts +++ b/src/api/schema.d.ts @@ -534,6 +534,7 @@ type uint32 = number; export interface WebsocketMessageWelcome { epoch: uint32; javascriptReloader: Reloader; + cwd: string; } export interface WebsocketMessageFileChangeNotification { diff --git a/src/api/schema.js b/src/api/schema.js index b4e98f44f7..1b928aa205 100644 --- a/src/api/schema.js +++ b/src/api/schema.js @@ -2405,6 +2405,7 @@ function decodeWebsocketMessageWelcome(bb) { result["epoch"] = bb.readUint32(); result["javascriptReloader"] = Reloader[bb.readByte()]; + result["cwd"] = bb.readString(); return result; } @@ -2426,6 +2427,13 @@ bb.writeByte(encoded); throw new Error("Missing required field \"javascriptReloader\""); } + var value = message["cwd"]; + if (value != null) { + bb.writeString(value); + } else { + throw new Error("Missing required field \"cwd\""); + } + } function decodeWebsocketMessageFileChangeNotification(bb) { diff --git a/src/api/schema.peechy b/src/api/schema.peechy index 17bda96543..a74059dcaa 100644 --- a/src/api/schema.peechy +++ b/src/api/schema.peechy @@ -451,6 +451,7 @@ struct WebsocketMessage { struct WebsocketMessageWelcome { uint32 epoch; Reloader javascriptReloader; + string cwd; } struct WebsocketMessageFileChangeNotification { diff --git a/src/api/schema.zig b/src/api/schema.zig index 593b02bddb..9803466987 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -2410,18 +2410,23 @@ epoch: u32 = 0, /// javascriptReloader javascript_reloader: Reloader, +/// cwd +cwd: []const u8, + pub fn decode(reader: anytype) anyerror!WebsocketMessageWelcome { var this = std.mem.zeroes(WebsocketMessageWelcome); this.epoch = try reader.readValue(u32); this.javascript_reloader = try reader.readValue(Reloader); + this.cwd = try reader.readValue([]const u8); return this; } pub fn encode(this: *const @This(), writer: anytype) anyerror!void { try writer.writeInt(this.epoch); try writer.writeEnum(this.javascript_reloader); + try writer.writeValue(this.cwd); } }; diff --git a/src/bundler.zig b/src/bundler.zig index 484a252019..8b13df4a2f 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -901,14 +901,17 @@ pub fn NewBundler(cache_files: bool) type { const basename = std.fs.path.basename(std.mem.span(destination)); const extname = std.fs.path.extension(basename); - javascript_bundle.import_from_name = try std.fmt.allocPrint( - this.allocator, - "/{s}.{x}.bun", - .{ - basename[0 .. basename.len - extname.len], - etag_u64, - }, - ); + javascript_bundle.import_from_name = if (bundler.options.platform == .bun) + "/node_modules.server.bun" + else + try std.fmt.allocPrint( + this.allocator, + "/{s}.{x}.bun", + .{ + basename[0 .. basename.len - extname.len], + etag_u64, + }, + ); javascript_bundle_container.bundle_format_version = current_version; javascript_bundle_container.bundle = javascript_bundle; @@ -2820,36 +2823,9 @@ pub const ClientEntryPoint = struct { code = try std.fmt.bufPrint( &entry.code_buffer, \\globalThis.Bun_disableCSSImports = true; - \\var lastErrorHandler = globalThis.onerror; - \\var loaded = {{boot: false, entry: false, onError: null}}; - \\if (!lastErrorHandler || !lastErrorHandler.__onceTag) {{ - \\ globalThis.onerror = function (evt) {{ - \\ if (this.onError && typeof this.onError == 'function') {{ - \\ this.onError(evt, loaded); - \\ }} - \\ console.error(evt.error); - \\ debugger; - \\ }}; - \\ globalThis.onerror.__onceTag = true; - \\ globalThis.onerror.loaded = loaded; - \\}} - \\ \\import boot from '{s}'; - \\loaded.boot = true; - \\if ('setLoaded' in boot) boot.setLoaded(loaded); \\import * as EntryPoint from '{s}{s}'; - \\loaded.entry = true; - \\ - \\if (!boot) {{ - \\ const now = Date.now(); - \\ debugger; - \\ const elapsed = Date.now() - now; - \\ if (elapsed < 1000) {{ - \\ throw new Error('Expected framework to export default a function. Instead, framework exported:', Object.keys(boot)); - \\ }} - \\}} - \\ - \\boot(EntryPoint, loaded); + \\boot(EntryPoint); , .{ client, @@ -2860,36 +2836,10 @@ pub const ClientEntryPoint = struct { } else { code = try std.fmt.bufPrint( &entry.code_buffer, - \\var lastErrorHandler = globalThis.onerror; - \\var loaded = {{boot: false, entry: false, onError: null}}; - \\if (!lastErrorHandler || !lastErrorHandler.__onceTag) {{ - \\ globalThis.onerror = function (evt) {{ - \\ if (this.onError && typeof this.onError == 'function') {{ - \\ this.onError(evt, loaded); - \\ }} - \\ console.error(evt.error); - \\ debugger; - \\ }}; - \\ globalThis.onerror.__onceTag = true; - \\ globalThis.onerror.loaded = loaded; - \\}} - \\ \\import boot from '{s}'; - \\loaded.boot = true; \\if ('setLoaded' in boot) boot.setLoaded(loaded); \\import * as EntryPoint from '{s}{s}'; - \\loaded.entry = true; - \\ - \\if (!boot) {{ - \\ const now = Date.now(); - \\ debugger; - \\ const elapsed = Date.now() - now; - \\ if (elapsed < 1000) {{ - \\ throw new Error('Expected framework to export default a function. Instead, framework exported:', Object.keys(boot)); - \\ }} - \\}} - \\ - \\boot(EntryPoint, loaded); + \\boot(EntryPoint); , .{ client, diff --git a/src/fallback.version b/src/fallback.version index fb76dcb20e..75f3323de1 100644 --- a/src/fallback.version +++ b/src/fallback.version @@ -1 +1 @@ -c098be5f3e938123 \ No newline at end of file +a5559a0075104616 \ No newline at end of file diff --git a/src/http.zig b/src/http.zig index 8565022949..c8ca3157b9 100644 --- a/src/http.zig +++ b/src/http.zig @@ -12,6 +12,7 @@ const logger = @import("logger.zig"); const Fs = @import("./fs.zig"); const Options = @import("./options.zig"); const Fallback = @import("./runtime.zig").Fallback; +const ErrorCSS = @import("./runtime.zig").ErrorCSS; const Css = @import("css_scanner.zig"); const NodeModuleBundle = @import("./node_module_bundle.zig").NodeModuleBundle; const resolve_path = @import("./resolver/resolve_path.zig"); @@ -639,6 +640,7 @@ pub const RequestContext = struct { value: Value, id: u32, timestamp: u32, + log: logger.Log, bytes: []const u8 = "", approximate_newline_count: usize = 0, pub const Value = union(Tag) { @@ -650,7 +652,7 @@ pub const RequestContext = struct { fail, }; }; - pub fn build(this: *WatchBuilder, id: u32, from_timestamp: u32) !WatchBuildResult { + pub fn build(this: *WatchBuilder, id: u32, from_timestamp: u32, allocator: *std.mem.Allocator) !WatchBuildResult { if (this.count == 0) { var writer = try js_printer.BufferWriter.init(this.allocator); this.printer = js_printer.BufferPrinter.init(writer); @@ -658,8 +660,8 @@ pub const RequestContext = struct { } defer this.count += 1; - var log = logger.Log.init(this.allocator); - errdefer log.deinit(); + + var log = logger.Log.init(allocator); const index = std.mem.indexOfScalar(u32, this.watcher.watchlist.items(.hash), id) orelse { @@ -667,6 +669,7 @@ pub const RequestContext = struct { return WatchBuildResult{ .value = .{ .fail = std.mem.zeroes(Api.WebsocketMessageBuildFailure) }, .id = id, + .log = log, .timestamp = WebsocketHandler.toTimestamp(Server.global_start_time.read()), }; }; @@ -674,7 +677,6 @@ pub const RequestContext = struct { const file_path_str = this.watcher.watchlist.items(.file_path)[index]; const fd = this.watcher.watchlist.items(.fd)[index]; const loader = this.watcher.watchlist.items(.loader)[index]; - const path = Fs.Path.init(file_path_str); var old_log = this.bundler.log; this.bundler.setLog(&log); @@ -695,7 +697,7 @@ pub const RequestContext = struct { this.bundler.resetStore(); var parse_result = this.bundler.parse( - this.bundler.allocator, + allocator, path, loader, 0, @@ -704,26 +706,64 @@ pub const RequestContext = struct { null, ) orelse { return WatchBuildResult{ - .value = .{ .fail = std.mem.zeroes(Api.WebsocketMessageBuildFailure) }, + .value = .{ + .fail = .{ + .id = id, + .from_timestamp = from_timestamp, + .loader = loader.toAPI(), + .module_path = this.bundler.fs.relativeTo(file_path_str), + .log = try log.toAPI(allocator), + }, + }, .id = id, + .log = log, .timestamp = WebsocketHandler.toTimestamp(Server.global_start_time.read()), }; }; this.printer.ctx.reset(); + { + var old_allocator = this.bundler.linker.allocator; + this.bundler.linker.allocator = allocator; + defer this.bundler.linker.allocator = old_allocator; + this.bundler.linker.link( + Fs.Path.init(file_path_str), + &parse_result, + .absolute_url, + false, + ) catch |err| { + return WatchBuildResult{ + .value = .{ + .fail = .{ + .id = id, + .from_timestamp = from_timestamp, + .loader = loader.toAPI(), + .module_path = this.bundler.fs.relativeTo(file_path_str), + .log = try log.toAPI(allocator), + }, + }, - try this.bundler.linker.link( - Fs.Path.init(file_path_str), - &parse_result, - .absolute_url, - false, - ); + .id = id, + .timestamp = WebsocketHandler.toTimestamp(Server.global_start_time.read()), + .log = log, + }; + }; + } var written = this.bundler.print(parse_result, @TypeOf(&this.printer), &this.printer, .esm) catch |err| { return WatchBuildResult{ - .value = .{ .fail = std.mem.zeroes(Api.WebsocketMessageBuildFailure) }, + .value = .{ + .fail = .{ + .id = id, + .from_timestamp = from_timestamp, + .loader = loader.toAPI(), + .module_path = this.bundler.fs.relativeTo(file_path_str), + .log = try log.toAPI(allocator), + }, + }, .id = id, .timestamp = WebsocketHandler.toTimestamp(Server.global_start_time.read()), + .log = log, }; }; @@ -735,13 +775,13 @@ pub const RequestContext = struct { .loader = parse_result.loader.toAPI(), .module_path = this.bundler.fs.relativeTo(file_path_str), .blob_length = @truncate(u32, written), - // .log = std.mem.zeroes(Api.Log), }, }, .id = id, .bytes = this.printer.ctx.written, .approximate_newline_count = parse_result.ast.approximate_newline_count, .timestamp = WebsocketHandler.toTimestamp(Server.global_start_time.read()), + .log = log, }; }, .css => { @@ -767,7 +807,7 @@ pub const RequestContext = struct { const count = brk: { if (this.bundler.options.hot_module_reloading) { - break :brk try CSSBundlerHMR.bundle( + break :brk CSSBundlerHMR.bundle( file_path_str, this.bundler.fs, &this.printer, @@ -780,7 +820,7 @@ pub const RequestContext = struct { &this.bundler.linker, ); } else { - break :brk try CSSBundler.bundle( + break :brk CSSBundler.bundle( file_path_str, this.bundler.fs, &this.printer, @@ -793,6 +833,21 @@ pub const RequestContext = struct { &this.bundler.linker, ); } + } catch { + return WatchBuildResult{ + .value = .{ + .fail = .{ + .id = id, + .from_timestamp = from_timestamp, + .loader = loader.toAPI(), + .module_path = this.bundler.fs.relativeTo(file_path_str), + .log = try log.toAPI(allocator), + }, + }, + .id = id, + .timestamp = WebsocketHandler.toTimestamp(Server.global_start_time.read()), + .log = log, + }; }; return WatchBuildResult{ @@ -811,6 +866,7 @@ pub const RequestContext = struct { .approximate_newline_count = count.approximate_newline_count, // .approximate_newline_count = parse_result.ast.approximate_newline_count, .timestamp = WebsocketHandler.toTimestamp(Server.global_start_time.read()), + .log = log, }; }, else => { @@ -818,6 +874,7 @@ pub const RequestContext = struct { .value = .{ .fail = std.mem.zeroes(Api.WebsocketMessageBuildFailure) }, .id = id, .timestamp = WebsocketHandler.toTimestamp(Server.global_start_time.read()), + .log = log, }; }, } @@ -1420,31 +1477,34 @@ pub const RequestContext = struct { .kind = .welcome, }; var cmd_reader: ApiReader = undefined; - var byte_buf: [32]u8 = undefined; - var fbs = std.io.fixedBufferStream(&byte_buf); - var writer = ByteApiWriter.init(&fbs); + { + var byte_buf: [32 + std.fs.MAX_PATH_BYTES]u8 = undefined; + var fbs = std.io.fixedBufferStream(&byte_buf); + var writer = ByteApiWriter.init(&fbs); - try msg.encode(&writer); - var reloader = Api.Reloader.disable; - if (ctx.bundler.options.hot_module_reloading) { - reloader = Api.Reloader.live; - if (ctx.bundler.options.jsx.supports_fast_refresh) { - if (ctx.bundler.options.node_modules_bundle) |bundle| { - if (bundle.hasFastRefresh()) { - reloader = Api.Reloader.fast_refresh; + try msg.encode(&writer); + var reloader = Api.Reloader.disable; + if (ctx.bundler.options.hot_module_reloading) { + reloader = Api.Reloader.live; + if (ctx.bundler.options.jsx.supports_fast_refresh) { + if (ctx.bundler.options.node_modules_bundle) |bundle| { + if (bundle.hasFastRefresh()) { + reloader = Api.Reloader.fast_refresh; + } } } } - } - const welcome_message = Api.WebsocketMessageWelcome{ - .epoch = WebsocketHandler.toTimestamp(handler.ctx.timer.start_time), - .javascript_reloader = reloader, - }; - try welcome_message.encode(&writer); - if ((try handler.websocket.writeBinary(fbs.getWritten())) == 0) { - handler.tombstone = true; - is_socket_closed = true; - Output.prettyErrorln("ERR: Websocket failed to write.", .{}); + const welcome_message = Api.WebsocketMessageWelcome{ + .epoch = WebsocketHandler.toTimestamp(handler.ctx.timer.start_time), + .javascript_reloader = reloader, + .cwd = handler.ctx.bundler.fs.top_level_dir, + }; + try welcome_message.encode(&writer); + if ((try handler.websocket.writeBinary(fbs.getWritten())) == 0) { + handler.tombstone = true; + is_socket_closed = true; + Output.prettyErrorln("ERR: Websocket failed to write.", .{}); + } } while (!handler.tombstone) { @@ -1485,7 +1545,11 @@ pub const RequestContext = struct { switch (cmd.kind) { .build => { var request = try Api.WebsocketCommandBuild.decode(&cmd_reader); - var build_result = try handler.builder.build(request.id, cmd.timestamp); + + var arena = std.heap.ArenaAllocator.init(default_allocator); + defer arena.deinit(); + + var build_result = try handler.builder.build(request.id, cmd.timestamp, &arena.allocator); const file_path = switch (build_result.value) { .fail => |fail| fail.module_path, .success => |fail| fail.module_path, @@ -1993,6 +2057,26 @@ pub const RequestContext = struct { return; } + if (strings.eqlComptime(path, "erro.css")) { + const buffer = ErrorCSS.sourceContent(); + ctx.appendHeader("Content-Type", MimeType.css.value); + if (FeatureFlags.strong_etags_for_built_files) { + const did_send = ctx.writeETag(buffer) catch false; + if (did_send) return; + } + + if (buffer.len == 0) { + return try ctx.sendNoContent(); + } + const send_body = ctx.method == .GET; + defer ctx.done(); + try ctx.writeStatus(200); + try ctx.prepareToSendBody(buffer.len, false); + if (!send_body) return; + _ = try ctx.writeSocket(buffer, SOCKET_FLAGS); + return; + } + if (strings.eqlComptime(path, "fallback")) { const resolved = try ctx.bundler.resolver.resolve(ctx.bundler.fs.top_level_dir, ctx.bundler.options.framework.?.fallback.path, .stmt); const resolved_path = resolved.pathConst() orelse return try ctx.sendNotFound(); @@ -2012,6 +2096,77 @@ pub const RequestContext = struct { return; } + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Dest + pub fn isScriptOrStyleRequest(ctx: *RequestContext) bool { + const header_ = ctx.header("Sec-Fetch-Dest") orelse return false; + return strings.eqlComptime(header_.value, "script") or + strings.eqlComptime(header_.value, "style"); + } + + fn handleSrcURL(ctx: *RequestContext, server: *Server) !void { + var input_path = ctx.url.path["src:".len..]; + while (std.mem.indexOfScalar(u8, input_path, ':')) |i| { + input_path = input_path[0..i]; + } + if (input_path.len == 0) return ctx.sendNotFound(); + + const pathname = Fs.PathName.init(input_path); + const result = try ctx.buildFile(input_path, pathname.ext); + + switch (result.file.value) { + .pending => |resolve_result| { + const path = resolve_result.pathConst() orelse return try ctx.sendNotFound(); + + var needs_close = false; + const fd = if (resolve_result.file_fd != 0) + resolve_result.file_fd + else brk: { + var file = std.fs.openFileAbsoluteZ(path.textZ(), .{ .read = true }) catch |err| { + Output.prettyErrorln("Failed to open {s} due to error {s}", .{ path.text, @errorName(err) }); + return try ctx.sendInternalError(err); + }; + needs_close = true; + break :brk file.handle; + }; + defer { + if (needs_close) { + std.os.close(fd); + } + } + + const content_length = brk: { + var file = std.fs.File{ .handle = fd }; + var stat = file.stat() catch |err| { + Output.prettyErrorln("Failed to read {s} due to error {s}", .{ path.text, @errorName(err) }); + return try ctx.sendInternalError(err); + }; + break :brk stat.size; + }; + + if (content_length == 0) { + return try ctx.sendNoContent(); + } + + ctx.appendHeader("Content-Type", "text/plain"); + defer ctx.done(); + + try ctx.writeStatus(200); + try ctx.prepareToSendBody(content_length, false); + + _ = try std.os.sendfile( + ctx.conn.client.socket.fd, + fd, + 0, + content_length, + &[_]std.os.iovec_const{}, + &[_]std.os.iovec_const{}, + 0, + ); + }, + else => return try ctx.sendNotFound(), + } + } + pub fn handleReservedRoutes(ctx: *RequestContext, server: *Server) !bool { if (strings.eqlComptime(ctx.url.extname, "bun") and ctx.bundler.options.node_modules_bundle != null) { try ctx.sendJSB(); @@ -2023,59 +2178,66 @@ pub const RequestContext = struct { return true; } - if (ctx.url.path.len > "bun:".len and strings.eqlComptimeIgnoreLen(ctx.url.path[0.."bun:".len], "bun:")) { + const isMaybePrefix = ctx.url.path.len > "bun:".len; + if (isMaybePrefix and strings.eqlComptimeIgnoreLen(ctx.url.path[0.."bun:".len], "bun:")) { try ctx.handleBunURL(server); return true; + } else if (isMaybePrefix and strings.eqlComptimeIgnoreLen(ctx.url.path[0.."src:".len], "src:")) { + try ctx.handleSrcURL(server); + return true; } return false; } - pub fn handleGet(ctx: *RequestContext) !void { - const result = brk: { - if (ctx.bundler.options.isFrontendFrameworkEnabled()) { - if (serve_as_package_path) { - break :brk try ctx.bundler.buildFile( - &ctx.log, - ctx.allocator, - ctx.url.pathWithoutAssetPrefix(ctx.bundler.options.routes.asset_prefix_path), - ctx.url.extname, - true, - true, - ); - } else { - break :brk try ctx.bundler.buildFile( - &ctx.log, - ctx.allocator, - ctx.url.pathWithoutAssetPrefix(ctx.bundler.options.routes.asset_prefix_path), - ctx.url.extname, - true, - false, - ); - } + pub inline fn buildFile(ctx: *RequestContext, path_name: string, extname: string) !bundler.ServeResult { + if (ctx.bundler.options.isFrontendFrameworkEnabled()) { + if (serve_as_package_path) { + return try ctx.bundler.buildFile( + &ctx.log, + ctx.allocator, + path_name, + extname, + true, + true, + ); } else { - if (serve_as_package_path) { - break :brk try ctx.bundler.buildFile( - &ctx.log, - ctx.allocator, - ctx.url.pathWithoutAssetPrefix(ctx.bundler.options.routes.asset_prefix_path), - ctx.url.extname, - false, - true, - ); - } else { - break :brk try ctx.bundler.buildFile( - &ctx.log, - ctx.allocator, - ctx.url.pathWithoutAssetPrefix(ctx.bundler.options.routes.asset_prefix_path), - ctx.url.extname, - false, - false, - ); - } + return try ctx.bundler.buildFile( + &ctx.log, + ctx.allocator, + path_name, + extname, + true, + false, + ); } - }; - + } else { + if (serve_as_package_path) { + return try ctx.bundler.buildFile( + &ctx.log, + ctx.allocator, + path_name, + extname, + false, + true, + ); + } else { + return try ctx.bundler.buildFile( + &ctx.log, + ctx.allocator, + path_name, + extname, + false, + false, + ); + } + } + } + pub fn handleGet(ctx: *RequestContext) !void { + const result = try ctx.buildFile( + ctx.url.pathWithoutAssetPrefix(ctx.bundler.options.routes.asset_prefix_path), + ctx.url.extname, + ); try @call(.{ .modifier = .always_inline }, RequestContext.renderServeResult, .{ ctx, result }); } @@ -2583,7 +2745,9 @@ pub const Server = struct { pub fn detectFastRefresh(this: *Server) void { defer this.bundler.resetStore(); - _ = this.bundler.resolver.resolve(this.bundler.fs.top_level_dir, "react-refresh/runtime", .internal) catch |err| { + // 1. Try react refresh + _ = this.bundler.resolver.resolve(this.bundler.fs.top_level_dir, this.bundler.options.jsx.refresh_runtime, .internal) catch |err| { + // 2. Try react refresh from import source perspective this.bundler.options.jsx.supports_fast_refresh = false; return; }; diff --git a/src/javascript/jsc/bindings/ZigGlobalObject.cpp b/src/javascript/jsc/bindings/ZigGlobalObject.cpp index edebae6dea..226abced6f 100644 --- a/src/javascript/jsc/bindings/ZigGlobalObject.cpp +++ b/src/javascript/jsc/bindings/ZigGlobalObject.cpp @@ -84,7 +84,7 @@ extern "C" JSC__JSGlobalObject *Zig__GlobalObject__create(JSClassRef *globalObje void *console_client) { JSC::Options::useSourceProviderCache() = true; JSC::Options::useUnlinkedCodeBlockJettisoning() = false; - JSC::Options::useTopLevelAwait() = true; + // JSC::Options::useTopLevelAwait() = true; JSC::Options::exposeInternalModuleLoader() = true; std::set_terminate([]() { Zig__GlobalObject__onCrash(); }); diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig index 14b010b172..24b59f91f3 100644 --- a/src/javascript/jsc/javascript.zig +++ b/src/javascript/jsc/javascript.zig @@ -324,6 +324,7 @@ pub const Bun = struct { ); }; +const bun_file_import_path = "/node_modules.server.bun"; pub const LazyClasses = [_]type{}; pub const Module = struct { @@ -459,7 +460,7 @@ pub const VirtualMachine = struct { std.debug.assert(VirtualMachine.vm_loaded); std.debug.assert(VirtualMachine.vm.global == global); - if (vm.node_modules != null and strings.eql(vm.bundler.linker.nodeModuleBundleImportPath(), _specifier)) { + if (vm.node_modules != null and strings.eqlComptime(_specifier, bun_file_import_path)) { // We kind of need an abstraction around this. // Basically we should subclass JSC::SourceCode with: // - hash @@ -471,11 +472,11 @@ pub const VirtualMachine = struct { return ResolvedSource{ .allocator = null, .source_code = ZigString.init(code), - .specifier = ZigString.init(vm.bundler.linker.nodeModuleBundleImportPath()), - .source_url = ZigString.init(vm.bundler.options.node_modules_bundle_pretty_path), + .specifier = ZigString.init(bun_file_import_path), + .source_url = ZigString.init(bun_file_import_path[1..]), .hash = 0, // TODO .bytecodecache_fd = std.math.lossyCast(u64, vm.node_modules.?.fetchByteCodeCache( - vm.bundler.options.node_modules_bundle_pretty_path, + bun_file_import_path[1..], &vm.bundler.fs.fs, ) orelse 0), }; @@ -658,8 +659,8 @@ pub const VirtualMachine = struct { if (vm.node_modules == null and strings.eqlComptime(specifier, Runtime.Runtime.Imports.Name)) { ret.path = Runtime.Runtime.Imports.Name; return; - } else if (vm.node_modules != null and strings.eql(specifier, vm.bundler.linker.nodeModuleBundleImportPath())) { - ret.path = vm.bundler.linker.nodeModuleBundleImportPath(); + } else if (vm.node_modules != null and strings.eql(specifier, bun_file_import_path)) { + ret.path = bun_file_import_path; return; } else if (strings.eqlComptime(specifier, main_file_name)) { ret.result = null; @@ -713,7 +714,7 @@ pub const VirtualMachine = struct { if (node_modules_bundle.findModuleIDInPackage(package, package_relative_path) == null) break :node_module_checker; - ret.path = vm.bundler.linker.nodeModuleBundleImportPath(); + ret.path = bun_file_import_path; return; } } @@ -910,7 +911,7 @@ pub const VirtualMachine = struct { // 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(vm.bundler.linker.nodeModuleBundleImportPath()))); + promise = JSModuleLoader.loadAndEvaluateModule(this.global, ZigString.init(std.mem.span(bun_file_import_path))); this.global.vm().drainMicrotasks(); @@ -1208,33 +1209,43 @@ pub const VirtualMachine = struct { writer.writeByteNTimes(' ', pad) catch unreachable; const top = exception.stack.frames()[0]; var remainder = std.mem.trim(u8, source.text, "\n"); - const prefix = remainder[0..@intCast(usize, top.position.column_start)]; - const underline = remainder[@intCast(usize, top.position.column_start)..@intCast(usize, top.position.column_stop)]; - const suffix = remainder[@intCast(usize, top.position.column_stop)..]; + if (@intCast(usize, top.position.column_stop) > remainder.len) { + writer.print( + comptime Output.prettyFmt( + "{d} | {s}\n", + allow_ansi_color, + ), + .{ source.line, remainder }, + ) catch unreachable; + } else { + const prefix = remainder[0..@intCast(usize, top.position.column_start)]; + const underline = remainder[@intCast(usize, top.position.column_start)..@intCast(usize, top.position.column_stop)]; + const suffix = remainder[@intCast(usize, top.position.column_stop)..]; - writer.print( - comptime Output.prettyFmt( - "{d} | {s}{s}{s}\n", + writer.print( + comptime Output.prettyFmt( + "{d} | {s}{s}{s}\n", + allow_ansi_color, + ), + .{ + source.line, + prefix, + underline, + suffix, + }, + ) catch unreachable; + var first_non_whitespace = @intCast(u32, top.position.column_start); + while (first_non_whitespace < source.text.len and source.text[first_non_whitespace] == ' ') { + first_non_whitespace += 1; + } + const indent = @intCast(usize, pad) + " | ".len + first_non_whitespace + 1; + + writer.writeByteNTimes(' ', indent) catch unreachable; + writer.print(comptime Output.prettyFmt( + "^\n", allow_ansi_color, - ), - .{ - source.line, - prefix, - underline, - suffix, - }, - ) catch unreachable; - var first_non_whitespace = @intCast(u32, top.position.column_start); - while (first_non_whitespace < source.text.len and source.text[first_non_whitespace] == ' ') { - first_non_whitespace += 1; + ), .{}) catch unreachable; } - const indent = @intCast(usize, pad) + " | ".len + first_non_whitespace + 1; - - writer.writeByteNTimes(' ', indent) catch unreachable; - writer.print(comptime Output.prettyFmt( - "^\n", - allow_ansi_color, - ), .{}) catch unreachable; if (name.len > 0 and message.len > 0) { writer.print(comptime Output.prettyFmt(" {s}: {s}\n", allow_ansi_color), .{ diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig index ca6e38edbb..274b79c209 100644 --- a/src/js_parser/js_parser.zig +++ b/src/js_parser/js_parser.zig @@ -1442,14 +1442,6 @@ fn notimpl() noreturn { Global.panic("Not implemented yet!!", .{}); } -fn lexerpanic() noreturn { - Global.panic("LexerPanic", .{}); -} - -fn fail() noreturn { - Global.panic("Something went wrong :cry;", .{}); -} - const ExprBindingTuple = struct { expr: ?ExprNodeIndex = null, binding: ?Binding = null, override_expr: ?ExprNodeIndex = null }; const TempRef = struct { @@ -5486,7 +5478,7 @@ pub fn NewParser( if (p.lexer.token == .t_default) { if (foundDefault) { try p.log.addRangeError(p.source, p.lexer.range(), "Multiple default clauses are not allowed"); - fail(); + return error.SyntaxError; } foundDefault = true; @@ -5673,7 +5665,7 @@ pub fn NewParser( if (p.lexer.isContextualKeyword("of") or isForAwait) { if (bad_let_range) |r| { try p.log.addRangeError(p.source, r, "\"let\" must be wrapped in parentheses to be used as an expression here"); - fail(); + return error.SyntaxError; } if (isForAwait and !p.lexer.isContextualKeyword("of")) { @@ -5989,7 +5981,7 @@ pub fn NewParser( try p.log.addError(p.source, logger.Loc{ .start = loc.start + 5, }, "Unexpected newline after \"throw\""); - fail(); + return error.SyntaxError; } const expr = try p.parseExpr(.lowest); try p.lexer.expectOrInsertSemicolon(); @@ -6656,7 +6648,7 @@ pub fn NewParser( // Commas after spread elements are not allowed if (has_spread and p.lexer.token == .t_comma) { p.log.addRangeError(p.source, p.lexer.range(), "Unexpected \",\" after rest pattern") catch unreachable; - fail(); + return error.SyntaxError; } } @@ -6703,7 +6695,7 @@ pub fn NewParser( // Commas after spread elements are not allowed if (property.flags.is_spread and p.lexer.token == .t_comma) { p.log.addRangeError(p.source, p.lexer.range(), "Unexpected \",\" after rest pattern") catch unreachable; - fail(); + return error.SyntaxError; } if (p.lexer.token != .t_comma) { @@ -7450,7 +7442,7 @@ pub fn NewParser( // Newlines are not allowed before "=>" if (p.lexer.has_newline_before) { try p.log.addRangeError(p.source, p.lexer.range(), "Unexpected newline before \"=>\""); - fail(); + return error.SyntaxError; } try p.lexer.expect(T.t_equals_greater_than); diff --git a/src/linker.zig b/src/linker.zig index dea14f6758..a1d72663fa 100644 --- a/src/linker.zig +++ b/src/linker.zig @@ -166,8 +166,9 @@ pub fn NewLinker(comptime BundlerType: type) type { } pub inline fn nodeModuleBundleImportPath(this: *const ThisLinker) string { - return if (this.options.platform != .bun and - this.options.node_modules_bundle_url.len > 0) + if (this.options.platform == .bun) return "/node_modules.server.bun"; + + return if (this.options.node_modules_bundle_url.len > 0) this.options.node_modules_bundle_url else this.options.node_modules_bundle.?.bundle.import_from_name; @@ -198,6 +199,7 @@ pub fn NewLinker(comptime BundlerType: type) type { var externals = std.ArrayList(u32).init(linker.allocator); var needs_bundle = false; var first_bundled_index: ?u32 = null; + var had_resolve_errors = false; // Step 1. Resolve imports & requires switch (result.loader) { @@ -349,18 +351,21 @@ pub fn NewLinker(comptime BundlerType: type) type { result.ast.needs_runtime = true; } } else |err| { + had_resolve_errors = true; + switch (err) { error.ModuleNotFound => { - if (Resolver.isPackagePath(import_record.path.text)) { + if (import_record.path.text.len > 0 and Resolver.isPackagePath(import_record.path.text)) { if (linker.options.platform.isWebLike() and Options.ExternalModules.isNodeBuiltin(import_record.path.text)) { try linker.log.addResolveError( &result.source, import_record.range, linker.allocator, - "Could not resolve: \"{s}\". Try setting --platform=\"node\"", + "Could not resolve: \"{s}\". Try setting --platform=\"node\" (after bun build exists)", .{import_record.path.text}, import_record.kind, ); + continue; } else { try linker.log.addResolveError( &result.source, @@ -370,6 +375,7 @@ pub fn NewLinker(comptime BundlerType: type) type { .{import_record.path.text}, import_record.kind, ); + continue; } } else { try linker.log.addResolveError( @@ -386,6 +392,17 @@ pub fn NewLinker(comptime BundlerType: type) type { } }, else => { + try linker.log.addResolveError( + &result.source, + import_record.range, + linker.allocator, + "{s} resolving \"{s}\"", + .{ + @errorName(err), + import_record.path.text, + }, + import_record.kind, + ); continue; }, } @@ -394,6 +411,7 @@ pub fn NewLinker(comptime BundlerType: type) type { }, else => {}, } + if (had_resolve_errors) return error.LinkError; result.ast.externals = externals.toOwnedSlice(); if (result.ast.needs_runtime and result.ast.runtime_import_record_id == null) { diff --git a/src/runtime.zig b/src/runtime.zig index c426ccb078..32c5f752eb 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -8,6 +8,33 @@ const Schema = @import("./api/schema.zig"); const Api = Schema.Api; +const ErrorCSSPath = "../examples/hello-next/bun-framework-next/bun-error.css"; + +pub const ErrorCSS = struct { + pub const ProdSourceContent = @embedFile(ErrorCSSPath); + + pub fn sourceContent() string { + if (comptime isDebug) { + var dirpath = std.fs.path.dirname(@src().file).?; + var env = std.process.getEnvMap(default_allocator) catch unreachable; + + const dir = std.mem.replaceOwned( + u8, + default_allocator, + dirpath, + "jarred", + env.get("USER").?, + ) catch unreachable; + var runtime_path = std.fs.path.join(default_allocator, &[_]string{ dir, ErrorCSSPath }) catch unreachable; + const file = std.fs.openFileAbsolute(runtime_path, .{}) catch unreachable; + defer file.close(); + return file.readToEndAlloc(default_allocator, (file.stat() catch unreachable).size) catch unreachable; + } else { + return ProdSourceContent; + } + } +}; + pub const Fallback = struct { pub const ProdSourceContent = @embedFile("./fallback.out.js"); pub const HTMLTemplate = @embedFile("./fallback.html"); diff --git a/src/runtime/hmr.ts b/src/runtime/hmr.ts index 2c4f8eb35c..dc72f469d6 100644 --- a/src/runtime/hmr.ts +++ b/src/runtime/hmr.ts @@ -373,6 +373,8 @@ if (typeof window !== "undefined") { css: new CSSLoader(), }; + sessionId: number; + start() { if (runOnce) { __hmrlog.warn( @@ -448,6 +450,7 @@ if (typeof window !== "undefined") { // key: module id // value: server-timestamp builds = new Map(); + cwd: string; indexOfModuleId(id: number): number { return HMRModule.dependencies.graph.indexOf(id); @@ -586,6 +589,25 @@ if (typeof window !== "undefined") { } this.client = new HMRClient(); + // if ( + // "sessionStorage" in globalThis && + // globalThis.sessionStorage.getItem("bun-hmr-session-id") + // ) { + // this.client.sessionId = parseInt( + // globalThis.sessionStorage.getItem("bun-hmr-session-id"), + // 16 + // ); + // } else { + // this.client.sessionId = Math.floor(Math.random() * 65534); + // if ("sessionStorage" in globalThis) { + // try { + // globalThis.sessionStorage.setItem( + // "bun-hmr-session-id", + // this.client.sessionId.toString(16) + // ); + // } catch (exception) {} + // } + // } this.client.verbose = verbose; this.client.start(); globalThis["__BUN_HMR"] = this.client; @@ -596,20 +618,29 @@ if (typeof window !== "undefined") { const build = API.decodeWebsocketMessageBuildFailure(buffer); const id = build.id; - const index = this.indexOfModuleId(id); - // Ignore build failures of modules that are not loaded - if (index === -1) { - return; - } + // const index = this.indexOfModuleId(id); + // // Ignore build failures of modules that are not loaded + // if (index === -1) { + // this.maybeReportBuildFailure(build); + // return; + // } - // Build failed for a module we didn't request? - const minTimestamp = this.builds.get(index); - if (!minTimestamp) { - return; - } - const fail = API.decodeWebsocketMessageBuildFailure(buffer); - // TODO: finish this. - __hmrlog.error("Build failed", fail.module_path); + // // Build failed for a module we didn't request? + // const minTimestamp = this.builds.get(index); + // if (!minTimestamp) { + // return; + // } + // const fail = API.decodeWebsocketMessageBuildFailure(buffer); + + this.reportBuildFailure(build); + } + + maybeReportBuildFailure(failure: API.WebsocketMessageBuildFailure) { + globalThis.renderBuildFailure(failure, this.cwd); + } + reportBuildFailure(failure: API.WebsocketMessageBuildFailure) { + __hmrlog.error("Build failed", failure.module_path); + globalThis.renderBuildFailure(failure, this.cwd); } verbose = false; @@ -849,6 +880,7 @@ if (typeof window !== "undefined") { const welcome = API.decodeWebsocketMessageWelcome(buffer); this.epoch = welcome.epoch; this.javascriptReloader = welcome.javascriptReloader; + this.cwd = welcome.cwd; if (!this.epoch) { __hmrlog.warn("Internal HMR error"); } @@ -1306,6 +1338,8 @@ if (typeof window !== "undefined") { document.addEventListener("onimportcss", HMRClient.onCSSImport, { passive: true, }); + + window.addEventListener("error", HMRClient.onError, { passive: true }); } } diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 055239c1ac..238706f93d 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -363,6 +363,23 @@ inline fn eqlComptimeCheckLen(self: string, comptime alt: anytype, comptime chec const second = comptime std.mem.readIntNative(u64, alt[8..15]); return ((comptime !check_len) or self.len == alt.len) and first == std.mem.readIntNative(u64, self[0..8]) and second == std.mem.readIntNative(u64, self[8..16]); }, + 23 => { + const first = comptime std.mem.readIntNative(u64, alt[0..8]); + const second = comptime std.mem.readIntNative(u64, alt[8..15]); + return ((comptime !check_len) or self.len == alt.len) and + first == std.mem.readIntNative(u64, self[0..8]) and + second == std.mem.readIntNative(u64, self[8..16]) and + eqlComptimeIgnoreLen(self[16..23], comptime alt[16..23]); + }, + 24 => { + const first = comptime std.mem.readIntNative(u64, alt[0..8]); + const second = comptime std.mem.readIntNative(u64, alt[8..16]); + const third = comptime std.mem.readIntNative(u64, alt[16..24]); + return ((comptime !check_len) or self.len == alt.len) and + first == std.mem.readIntNative(u64, self[0..8]) and + second == std.mem.readIntNative(u64, self[8..16]) and + third == std.mem.readIntNative(u64, self[16..24]); + }, else => { @compileError(alt ++ " is too long."); },