diff --git a/docs/guides/ecosystem/nextjs.mdx b/docs/guides/ecosystem/nextjs.mdx index 2a3f4188cb..0cd9ccd04b 100644 --- a/docs/guides/ecosystem/nextjs.mdx +++ b/docs/guides/ecosystem/nextjs.mdx @@ -4,54 +4,100 @@ sidebarTitle: Next.js with Bun mode: center --- -Initialize a Next.js app with `create-next-app`. This will scaffold a new Next.js project and automatically install dependencies. - -```sh terminal icon="terminal" -bun create next-app -``` - -```txt -✔ What is your project named? … my-app -✔ Would you like to use TypeScript with this project? … No / Yes -✔ Would you like to use ESLint with this project? … No / Yes -✔ Would you like to use Tailwind CSS? ... No / Yes -✔ Would you like to use `src/` directory with this project? … No / Yes -✔ Would you like to use App Router? (recommended) ... No / Yes -✔ What import alias would you like configured? … @/* -Creating a new Next.js app in /path/to/my-app. -``` +[Next.js](https://nextjs.org/) is a React framework for building full-stack web applications. It supports server-side rendering, static site generation, API routes, and more. Bun provides fast package installation and can run Next.js development and production servers. --- -You can specify a starter template using the `--example` flag. + + + Use the interactive CLI to create a new Next.js app. This will scaffold a new Next.js project and automatically install dependencies. -```sh -bun create next-app --example with-supabase -``` + ```sh terminal icon="terminal" + bun create next-app@latest my-bun-app + ``` -```txt -✔ What is your project named? … my-app -... -``` + + + Change to the project directory and run the dev server with Bun. + + ```sh terminal icon="terminal" + cd my-bun-app + bun --bun run dev + ``` + + This starts the Next.js dev server with Bun's runtime. + + Open [`http://localhost:3000`](http://localhost:3000) with your browser to see the result. Any changes you make to `app/page.tsx` will be hot-reloaded in the browser. + + + + Modify the scripts field in your `package.json` by prefixing the Next.js CLI commands with `bun --bun`. This ensures that Bun executes the Next.js CLI for common tasks like `dev`, `build`, and `start`. + + ```json package.json icon="file-json" + { + "scripts": { + "dev": "bun --bun next dev", // [!code ++] + "build": "bun --bun next build", // [!code ++] + "start": "bun --bun next start", // [!code ++] + } + } + ``` + + + --- -To start the dev server with Bun, run `bun --bun run dev` from the project root. +## Hosting -```sh terminal icon="terminal" -cd my-app -bun --bun run dev -``` +Next.js applications on Bun can be deployed to various platforms. + + + + Deploy on Vercel + + + Deploy on Railway + + + Deploy on DigitalOcean + + + Deploy on AWS Lambda + + + Deploy on Google Cloud Run + + + Deploy on Render + + --- -To run the dev server with Node.js instead, omit `--bun`. +## Templates -```sh terminal icon="terminal" -cd my-app -bun run dev -``` + + + A simple App Router starter with Bun, Next.js, and Tailwind CSS. + + + A full-stack todo application built with Bun, Next.js, and PostgreSQL. + + --- -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. Any changes you make to `(pages/app)/index.tsx` will be hot-reloaded in the browser. +[→ See Next.js's official documentation](https://nextjs.org/docs) for more information on building and deploying Next.js applications. diff --git a/docs/guides/ecosystem/tanstack-start.mdx b/docs/guides/ecosystem/tanstack-start.mdx index d7508b3b81..2071671a36 100644 --- a/docs/guides/ecosystem/tanstack-start.mdx +++ b/docs/guides/ecosystem/tanstack-start.mdx @@ -755,4 +755,38 @@ To host your TanStack Start app, you can use [Nitro](https://nitro.build/) or a --- +## Templates + + + + A Todo application built with Bun, TanStack Start, and PostgreSQL. + + + A TanStack Start template using Bun with SSR and file-based routing. + + + The basic TanStack starter using the Bun runtime and Bun's file APIs. + + + +--- + [→ See TanStack Start's official documentation](https://tanstack.com/start/latest/docs/framework/react/guide/hosting) for more information on hosting. diff --git a/docs/images/templates/bun-nextjs-basic.png b/docs/images/templates/bun-nextjs-basic.png new file mode 100644 index 0000000000..95a1c90370 Binary files /dev/null and b/docs/images/templates/bun-nextjs-basic.png differ diff --git a/docs/images/templates/bun-nextjs-todo.png b/docs/images/templates/bun-nextjs-todo.png new file mode 100644 index 0000000000..4a2d21eded Binary files /dev/null and b/docs/images/templates/bun-nextjs-todo.png differ diff --git a/docs/images/templates/bun-tanstack-basic.png b/docs/images/templates/bun-tanstack-basic.png new file mode 100644 index 0000000000..448e499e88 Binary files /dev/null and b/docs/images/templates/bun-tanstack-basic.png differ diff --git a/docs/images/templates/bun-tanstack-start.png b/docs/images/templates/bun-tanstack-start.png new file mode 100644 index 0000000000..a2c4c5f327 Binary files /dev/null and b/docs/images/templates/bun-tanstack-start.png differ diff --git a/docs/images/templates/bun-tanstack-todo.png b/docs/images/templates/bun-tanstack-todo.png new file mode 100644 index 0000000000..9701e86520 Binary files /dev/null and b/docs/images/templates/bun-tanstack-todo.png differ diff --git a/docs/snippets/guides.jsx b/docs/snippets/guides.jsx index b17af36b57..96c999dca3 100644 --- a/docs/snippets/guides.jsx +++ b/docs/snippets/guides.jsx @@ -11,6 +11,12 @@ export const GuidesList = () => { href: "/guides/ecosystem/tanstack-start", cta: "View guide", }, + { + category: "Ecosystem", + title: "Use Next.js with Bun", + href: "/guides/ecosystem/nextjs", + cta: "View guide", + }, { category: "Ecosystem", title: "Build a frontend using Vite and Bun", @@ -23,12 +29,6 @@ export const GuidesList = () => { href: "/guides/runtime/typescript", cta: "View guide", }, - { - category: "Streams", - title: "Convert a ReadableStream to a string", - href: "/guides/streams/to-string", - cta: "View guide", - }, { category: "HTTP", title: "Write a simple HTTP server", diff --git a/src/bun.js/api/YAMLObject.zig b/src/bun.js/api/YAMLObject.zig index 0c01e36feb..6862e2e6dd 100644 --- a/src/bun.js/api/YAMLObject.zig +++ b/src/bun.js/api/YAMLObject.zig @@ -843,14 +843,20 @@ const Stringifier = struct { '0' => { if (i == start) { if (i + 1 < str.length()) { - const nc = str.charAt(i + 1); - if (nc == 'x' or nc == 'X') { - base = .hex; - } else if (nc == 'o' or nc == 'O') { - base = .oct; - } else { - offset.* = i; - return false; + switch (str.charAt(i + 1)) { + 'x', 'X' => { + base = .hex; + }, + 'o', 'O' => { + base = .oct; + }, + '0'...'9' => { + // 0 prefix allowed + }, + else => { + offset.* = i; + return false; + }, } i += 1; } else { diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index e38ddf3b22..d079964114 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -962,6 +962,7 @@ static void loadSignalNumberMap() signalNameToNumberMap->add(signalNames[2], SIGQUIT); signalNameToNumberMap->add(signalNames[9], SIGKILL); signalNameToNumberMap->add(signalNames[15], SIGTERM); + signalNameToNumberMap->add(signalNames[27], SIGWINCH); #else signalNameToNumberMap->add(signalNames[0], SIGHUP); signalNameToNumberMap->add(signalNames[1], SIGINT); diff --git a/src/bun.js/test/Execution.zig b/src/bun.js/test/Execution.zig index 69f90ac347..77669082cf 100644 --- a/src/bun.js/test/Execution.zig +++ b/src/bun.js/test/Execution.zig @@ -76,7 +76,8 @@ pub const ExecutionSequence = struct { /// Index into ExecutionSequence.entries() for the entry that is not started or currently running active_entry: ?*ExecutionEntry, test_entry: ?*ExecutionEntry, - remaining_repeat_count: i64 = 1, + remaining_repeat_count: u32, + remaining_retry_count: u32, result: Result = .pending, executing: bool = false, started_at: bun.timespec = .epoch, @@ -90,11 +91,18 @@ pub const ExecutionSequence = struct { } = .not_set, maybe_skip: bool = false, - pub fn init(first_entry: ?*ExecutionEntry, test_entry: ?*ExecutionEntry) ExecutionSequence { + pub fn init(cfg: struct { + first_entry: ?*ExecutionEntry, + test_entry: ?*ExecutionEntry, + retry_count: u32 = 0, + repeat_count: u32 = 0, + }) ExecutionSequence { return .{ - .first_entry = first_entry, - .active_entry = first_entry, - .test_entry = test_entry, + .first_entry = cfg.first_entry, + .active_entry = cfg.first_entry, + .test_entry = cfg.test_entry, + .remaining_repeat_count = cfg.repeat_count, + .remaining_retry_count = cfg.retry_count, }; } @@ -349,8 +357,10 @@ fn stepSequenceOne(buntest_strong: bun_test.BunTestPtr, globalThis: *jsc.JSGloba } const next_item = sequence.active_entry orelse { - bun.debugAssert(sequence.remaining_repeat_count == 0); // repeat count is decremented when the sequence is advanced, this should only happen if the sequence were empty. which should be impossible. - groupLog.log("runOne: no repeats left; wait for group completion.", .{}); + // Sequence is complete - either because: + // 1. It ran out of entries (normal completion) + // 2. All retry/repeat attempts have been exhausted + groupLog.log("runOne: no more entries; sequence complete.", .{}); return .done; }; sequence.executing = true; @@ -455,7 +465,7 @@ fn advanceSequence(this: *Execution, sequence: *ExecutionSequence, group: *Concu sequence.executing = false; if (sequence.maybe_skip) { sequence.maybe_skip = false; - sequence.active_entry = entry.skip_to; + sequence.active_entry = if (entry.failure_skip_past) |failure_skip_past| failure_skip_past.next else null; } else { sequence.active_entry = entry.next; } @@ -465,18 +475,32 @@ fn advanceSequence(this: *Execution, sequence: *ExecutionSequence, group: *Concu if (sequence.active_entry == null) { // just completed the sequence - this.onSequenceCompleted(sequence); - sequence.remaining_repeat_count -= 1; - if (sequence.remaining_repeat_count <= 0) { - // no repeats left; indicate completion - if (group.remaining_incomplete_entries == 0) { - bun.debugAssert(false); // remaining_incomplete_entries should never go below 0 - return; - } - group.remaining_incomplete_entries -= 1; - } else { + const test_failed = sequence.result.isFail(); + const test_passed = sequence.result.isPass(.pending_is_pass); + + // Handle retry logic: if test failed and we have retries remaining, retry it + if (test_failed and sequence.remaining_retry_count > 0) { + sequence.remaining_retry_count -= 1; this.resetSequence(sequence); + return; } + + // Handle repeat logic: if test passed and we have repeats remaining, repeat it + if (test_passed and sequence.remaining_repeat_count > 0) { + sequence.remaining_repeat_count -= 1; + this.resetSequence(sequence); + return; + } + + // Only report the final result after all retries/repeats are done + this.onSequenceCompleted(sequence); + + // No more retries or repeats; mark sequence as complete + if (group.remaining_incomplete_entries == 0) { + bun.debugAssert(false); // remaining_incomplete_entries should never go below 0 + return; + } + group.remaining_incomplete_entries -= 1; } } fn onGroupStarted(_: *Execution, _: *ConcurrentGroup, globalThis: *jsc.JSGlobalObject) void { @@ -580,13 +604,13 @@ pub fn resetSequence(this: *Execution, sequence: *ExecutionSequence) void { } } - if (sequence.result.isPass(.pending_is_pass)) { - // passed or pending; run again - sequence.* = .init(sequence.first_entry, sequence.test_entry); - } else { - // already failed or skipped; don't run again - sequence.active_entry = null; - } + // Preserve the current remaining_repeat_count and remaining_retry_count + sequence.* = .init(.{ + .first_entry = sequence.first_entry, + .test_entry = sequence.test_entry, + .retry_count = sequence.remaining_retry_count, + .repeat_count = sequence.remaining_repeat_count, + }); _ = this; } diff --git a/src/bun.js/test/Order.zig b/src/bun.js/test/Order.zig index df2902c766..b1df67422e 100644 --- a/src/bun.js/test/Order.zig +++ b/src/bun.js/test/Order.zig @@ -46,9 +46,12 @@ pub fn generateAllOrder(this: *Order, entries: []const *ExecutionEntry) bun.JSEr for (entries) |entry| { if (bun.Environment.ci_assert and entry.added_in_phase != .preload) bun.assert(entry.next == null); entry.next = null; - entry.skip_to = null; + entry.failure_skip_past = null; const sequences_start = this.sequences.items.len; - try this.sequences.append(.init(entry, null)); // add sequence to concurrentgroup + try this.sequences.append(.init(.{ + .first_entry = entry, + .test_entry = null, + })); // add sequence to concurrentgroup const sequences_end = this.sequences.items.len; try this.groups.append(.init(sequences_start, sequences_end, this.groups.items.len + 1)); // add a new concurrentgroup to order this.previous_group_was_concurrent = false; @@ -139,15 +142,20 @@ pub fn generateOrderTest(this: *Order, current: *ExecutionEntry) bun.JSError!voi // set skip_to values var index = list.first; - var skip_to = current.next; + var failure_skip_past: ?*ExecutionEntry = current; while (index) |entry| : (index = entry.next) { - if (entry == skip_to) skip_to = null; - entry.skip_to = skip_to; // we should consider matching skip_to in beforeAll to skip directly to the first afterAll from its own scope rather than skipping to the first afterAll from any scope + entry.failure_skip_past = failure_skip_past; // we could consider matching skip_to in beforeAll to skip directly to the first afterAll from its own scope rather than skipping to the first afterAll from any scope + if (entry == failure_skip_past) failure_skip_past = null; } // add these as a single sequence const sequences_start = this.sequences.items.len; - try this.sequences.append(.init(list.first, current)); // add sequence to concurrentgroup + try this.sequences.append(.init(.{ + .first_entry = list.first, + .test_entry = current, + .retry_count = current.retry_count, + .repeat_count = current.repeat_count, + })); // add sequence to concurrentgroup const sequences_end = this.sequences.items.len; try appendOrExtendConcurrentGroup(this, current.base.concurrent, sequences_start, sequences_end); // add or extend the concurrent group } diff --git a/src/bun.js/test/ScopeFunctions.zig b/src/bun.js/test/ScopeFunctions.zig index ba08f8b1c9..9c44d3b388 100644 --- a/src/bun.js/test/ScopeFunctions.zig +++ b/src/bun.js/test/ScopeFunctions.zig @@ -132,10 +132,10 @@ pub fn callAsFunction(globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JS defer if (formatted_label) |label| bunTest.gpa.free(label); const bound = if (args.callback) |cb| try cb.bind(globalThis, item, &bun.String.static("cb"), 0, args_list_raw.items) else null; - try this.enqueueDescribeOrTestCallback(bunTest, globalThis, callFrame, bound, formatted_label, args.options.timeout, callback_length -| args_list.items.len, line_no); + try this.enqueueDescribeOrTestCallback(bunTest, globalThis, callFrame, bound, formatted_label, args.options, callback_length -| args_list.items.len, line_no); } } else { - try this.enqueueDescribeOrTestCallback(bunTest, globalThis, callFrame, args.callback, args.description, args.options.timeout, callback_length, line_no); + try this.enqueueDescribeOrTestCallback(bunTest, globalThis, callFrame, args.callback, args.description, args.options, callback_length, line_no); } return .js_undefined; @@ -169,7 +169,7 @@ fn filterNames(comptime Rem: type, rem: *Rem, description: ?[]const u8, parent_i } } -fn enqueueDescribeOrTestCallback(this: *ScopeFunctions, bunTest: *bun_test.BunTest, globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame, callback: ?jsc.JSValue, description: ?[]const u8, timeout: u32, callback_length: usize, line_no: u32) bun.JSError!void { +fn enqueueDescribeOrTestCallback(this: *ScopeFunctions, bunTest: *bun_test.BunTest, globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame, callback: ?jsc.JSValue, description: ?[]const u8, options: ParseArgumentsOptions, callback_length: usize, line_no: u32) bun.JSError!void { groupLog.begin(@src()); defer groupLog.end(); @@ -248,7 +248,9 @@ fn enqueueDescribeOrTestCallback(this: *ScopeFunctions, bunTest: *bun_test.BunTe _ = try bunTest.collection.active_scope.appendTest(bunTest.gpa, description, if (matches_filter) callback else null, .{ .has_done_parameter = has_done_parameter, - .timeout = timeout, + .timeout = options.timeout, + .retry_count = options.retry, + .repeat_count = options.repeats, }, base, .collection); }, } @@ -286,15 +288,16 @@ fn errorInCI(globalThis: *jsc.JSGlobalObject, signature: []const u8) bun.JSError const ParseArgumentsResult = struct { description: ?[]const u8, callback: ?jsc.JSValue, - options: struct { - timeout: u32 = 0, - retry: ?f64 = null, - repeats: ?f64 = null, - }, + options: ParseArgumentsOptions, pub fn deinit(this: *ParseArgumentsResult, gpa: std.mem.Allocator) void { if (this.description) |str| gpa.free(str); } }; +const ParseArgumentsOptions = struct { + timeout: u32 = 0, + retry: u32 = 0, + repeats: u32 = 0, +}; pub const CallbackMode = enum { require, allow }; pub const FunctionKind = enum { test_or_describe, hook }; @@ -382,13 +385,16 @@ pub fn parseArguments(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame if (!retries.isNumber()) { return globalThis.throwPretty("{f}() expects retry to be a number", .{signature}); } - result.options.retry = retries.asNumber(); + result.options.retry = std.math.lossyCast(u32, retries.asNumber()); } if (try options.get(globalThis, "repeats")) |repeats| { if (!repeats.isNumber()) { return globalThis.throwPretty("{f}() expects repeats to be a number", .{signature}); } - result.options.repeats = repeats.asNumber(); + if (result.options.retry != 0) { + return globalThis.throwPretty("{f}(): Cannot set both retry and repeats", .{signature}); + } + result.options.repeats = std.math.lossyCast(u32, repeats.asNumber()); } } else if (options.isUndefinedOrNull()) { // no options diff --git a/src/bun.js/test/bun_test.zig b/src/bun.js/test/bun_test.zig index 6931ca5780..83a0e9b858 100644 --- a/src/bun.js/test/bun_test.zig +++ b/src/bun.js/test/bun_test.zig @@ -924,6 +924,10 @@ pub const ExecutionEntryCfg = struct { /// 0 = unlimited timeout timeout: u32, has_done_parameter: bool, + /// Number of times to retry a failed test (0 = no retries) + retry_count: u32 = 0, + /// Number of times to repeat a test (0 = run once, 1 = run twice, etc.) + repeat_count: u32 = 0, }; pub const ExecutionEntry = struct { base: BaseScope, @@ -935,9 +939,14 @@ pub const ExecutionEntry = struct { /// when this entry begins executing, the timespec will be set to the current time plus the timeout(ms). timespec: bun.timespec = .epoch, added_in_phase: AddedInPhase, + /// Number of times to retry a failed test (0 = no retries) + retry_count: u32, + /// Number of times to repeat a test (0 = run once, 1 = run twice, etc.) + repeat_count: u32, next: ?*ExecutionEntry = null, - skip_to: ?*ExecutionEntry = null, + /// if this entry fails, go to the entry 'failure_skip_past.next' + failure_skip_past: ?*ExecutionEntry = null, const AddedInPhase = enum { preload, collection, execution }; @@ -948,6 +957,8 @@ pub const ExecutionEntry = struct { .timeout = cfg.timeout, .has_done_parameter = cfg.has_done_parameter, .added_in_phase = phase, + .retry_count = cfg.retry_count, + .repeat_count = cfg.repeat_count, }); if (cb) |c| { diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 015efd3bcf..5464a062ab 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -604,6 +604,10 @@ pub const CommandLineReporter = struct { writer: anytype, comptime dim: bool, ) void { + const initial_retry_count = test_entry.retry_count; + const attempts = (initial_retry_count - sequence.remaining_retry_count) + 1; + const initial_repeat_count = test_entry.repeat_count; + const repeats = (initial_repeat_count - sequence.remaining_repeat_count) + 1; var scopes_stack = bun.BoundedArray(*bun_test.DescribeScope, 64).init(0) catch unreachable; var parent_: ?*bun_test.DescribeScope = test_entry.base.parent; @@ -677,6 +681,16 @@ pub const CommandLineReporter = struct { else writer.print(comptime Output.prettyFmt(" {s}", false), .{display_label}) catch unreachable; + // Print attempt count if test was retried (attempts > 1) + if (attempts > 1) switch (Output.enable_ansi_colors_stderr) { + inline else => |enable_ansi_colors_stderr| writer.print(comptime Output.prettyFmt(" (attempt {d})", enable_ansi_colors_stderr), .{attempts}) catch unreachable, + }; + + // Print repeat count if test failed on a repeat (repeats > 1) + if (repeats > 1) switch (Output.enable_ansi_colors_stderr) { + inline else => |enable_ansi_colors_stderr| writer.print(comptime Output.prettyFmt(" (run {d})", enable_ansi_colors_stderr), .{repeats}) catch unreachable, + }; + if (elapsed_ns > (std.time.ns_per_us * 10)) { writer.print(" {f}", .{ Output.ElapsedFormatter{ diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index ba5073e7f7..180f4d7498 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -944,7 +944,7 @@ pub fn hoist( .install_root_dependencies = install_root_dependencies, .workspace_filters = workspace_filters, .packages_to_install = packages_to_install, - .pending_optional_peers = .init(bun.default_allocator), + .pending_optional_peers = .init(allocator), }; try (Tree{}).processSubtree( diff --git a/src/install/lockfile/Tree.zig b/src/install/lockfile/Tree.zig index 656353c32c..b4a25ae9cd 100644 --- a/src/install/lockfile/Tree.zig +++ b/src/install/lockfile/Tree.zig @@ -247,9 +247,11 @@ pub fn Builder(comptime method: BuilderMethod) type { queue: TreeFiller, log: *logger.Log, lockfile: *const Lockfile, - // unresolved optional peers that might resolve later. if they do we will want to assign - // builder.resolutions[peer.dep_id] to the resolved pkg_id. - pending_optional_peers: std.AutoHashMap(PackageNameHash, bun.collections.ArrayListDefault(DependencyID)), + // Unresolved optional peers that might resolve later. if they do we will want to assign + // builder.resolutions[peer.dep_id] to the resolved pkg_id. A dependency ID set is used because there + // can be multiple instances of the same package in the tree, so the same unresolved dependency ID + // could be visited multiple times before it's resolved. + pending_optional_peers: std.AutoArrayHashMap(PackageNameHash, std.AutoArrayHashMap(DependencyID, void)), manager: if (method == .filter) *const PackageManager else void, sort_buf: std.ArrayListUnmanaged(DependencyID) = .{}, workspace_filters: if (method == .filter) []const WorkspaceFilter else void = if (method == .filter) &.{}, @@ -581,29 +583,32 @@ pub fn processSubtree( .dependency_loop, .hoisted => continue, .resolve => |res_id| { - bun.assertWithLocation(pkg_id == invalid_package_id, @src()); - bun.assertWithLocation(res_id != invalid_package_id, @src()); + bun.debugAssert(pkg_id == invalid_package_id); + bun.debugAssert(res_id != invalid_package_id); builder.resolutions[dep_id] = res_id; if (comptime Environment.allow_assert) { - bun.assertWithLocation(!builder.pending_optional_peers.contains(dependency.name_hash), @src()); + bun.debugAssert(!builder.pending_optional_peers.contains(dependency.name_hash)); } - if (builder.pending_optional_peers.fetchRemove(dependency.name_hash)) |entry| { + + if (builder.pending_optional_peers.fetchSwapRemove(dependency.name_hash)) |entry| { var peers = entry.value; defer peers.deinit(); - for (peers.items()) |unresolved_dep_id| { - bun.assertWithLocation(builder.resolutions[unresolved_dep_id] == invalid_package_id, @src()); + for (peers.keys()) |unresolved_dep_id| { + // the dependency should be either unresolved or the same dependency as above + bun.debugAssert(unresolved_dep_id == dep_id or builder.resolutions[unresolved_dep_id] == invalid_package_id); builder.resolutions[unresolved_dep_id] = res_id; } } }, .resolve_replace => |replace| { - bun.assertWithLocation(pkg_id != invalid_package_id, @src()); + bun.debugAssert(pkg_id != invalid_package_id); builder.resolutions[replace.dep_id] = pkg_id; - if (builder.pending_optional_peers.fetchRemove(dependency.name_hash)) |entry| { + if (builder.pending_optional_peers.fetchSwapRemove(dependency.name_hash)) |entry| { var peers = entry.value; defer peers.deinit(); - for (peers.items()) |unresolved_dep_id| { - bun.assertWithLocation(builder.resolutions[unresolved_dep_id] == invalid_package_id, @src()); + for (peers.keys()) |unresolved_dep_id| { + // the dependency should be either unresolved or the same dependency as above + bun.debugAssert(unresolved_dep_id == replace.dep_id or builder.resolutions[unresolved_dep_id] == invalid_package_id); builder.resolutions[unresolved_dep_id] = pkg_id; } } @@ -626,9 +631,10 @@ pub fn processSubtree( // later if it's possible to resolve it. const entry = try builder.pending_optional_peers.getOrPut(dependency.name_hash); if (!entry.found_existing) { - entry.value_ptr.* = .init(); + entry.value_ptr.* = .init(builder.allocator); } - try entry.value_ptr.append(dep_id); + + try entry.value_ptr.put(dep_id, {}); }, .placement => |dest| { bun.handleOom(dependency_lists[dest.id].append(builder.allocator, dep_id)); @@ -676,21 +682,21 @@ fn hoistDependency( const res_id = builder.resolutions[dep_id]; if (res_id == invalid_package_id and package_id == invalid_package_id) { - bun.assertWithLocation(dep.behavior.isOptionalPeer(), @src()); - bun.assertWithLocation(dependency.behavior.isOptionalPeer(), @src()); + bun.debugAssert(dep.behavior.isOptionalPeer()); + bun.debugAssert(dependency.behavior.isOptionalPeer()); // both optional peers will need to be resolved if they can resolve later. // remember input package_id and dependency for later return .resolve_later; } if (res_id == invalid_package_id) { - bun.assertWithLocation(dep.behavior.isOptionalPeer(), @src()); + bun.debugAssert(dep.behavior.isOptionalPeer()); return .{ .resolve_replace = .{ .id = this.id, .dep_id = dep_id } }; } if (package_id == invalid_package_id) { - bun.assertWithLocation(dependency.behavior.isOptionalPeer(), @src()); - bun.assertWithLocation(res_id != invalid_package_id, @src()); + bun.debugAssert(dependency.behavior.isOptionalPeer()); + bun.debugAssert(res_id != invalid_package_id); // resolve optional peer to `builder.resolutions[dep_id]` return .{ .resolve = res_id }; // 1 } diff --git a/test/cli/install/hoist.test.ts b/test/cli/install/hoist.test.ts new file mode 100644 index 0000000000..12919c36b1 --- /dev/null +++ b/test/cli/install/hoist.test.ts @@ -0,0 +1,30 @@ +import { afterAll, beforeAll, test } from "bun:test"; +import { VerdaccioRegistry, bunEnv, runBunInstall } from "harness"; + +const registry = new VerdaccioRegistry(); + +beforeAll(async () => { + await registry.start(); +}); + +afterAll(() => { + registry.stop(); +}); + +test("should handle resolving optional peer from multiple instances of same package", async () => { + const { packageDir } = await registry.createTestDir({ + files: { + "package.json": JSON.stringify({ + name: "pkg", + dependencies: { + "dep-1": "npm:one-optional-peer-dep@1.0.2", + "dep-2": "npm:one-optional-peer-dep@1.0.2", + "one-dep": "1.0.0", + }, + }), + }, + }); + + // this shouldn't hit an assertion + await runBunInstall(bunEnv, packageDir); +}); diff --git a/test/js/bun/test/test-on-test-finished.test.ts b/test/js/bun/test/test-on-test-finished.test.ts index e97a24a2e4..de75e47347 100644 --- a/test/js/bun/test/test-on-test-finished.test.ts +++ b/test/js/bun/test/test-on-test-finished.test.ts @@ -114,3 +114,19 @@ describe("onTestFinished with all hooks", () => { expect(output).toEqual(["test", "inner afterAll", "afterEach", "onTestFinished"]); }); }); + +// Test that a failing test still runs the onTestFinished hook +describe("onTestFinished with failing test", () => { + const output: string[] = []; + + test.failing("failing test", () => { + onTestFinished(() => { + output.push("onTestFinished"); + }); + output.push("test"); + throw new Error("fail"); + }); + test("verify order", () => { + expect(output).toEqual(["test", "onTestFinished"]); + }); +}); diff --git a/test/js/bun/test/test-retry-repeats-basic.test.ts b/test/js/bun/test/test-retry-repeats-basic.test.ts new file mode 100644 index 0000000000..44e338dbec --- /dev/null +++ b/test/js/bun/test/test-retry-repeats-basic.test.ts @@ -0,0 +1,161 @@ +// Basic tests to verify retry and repeats functionality works +import { afterAll, afterEach, beforeEach, describe, expect, onTestFinished, test } from "bun:test"; + +describe("retry option", () => { + let attempts = 0; + test( + "retries failed test until it passes", + () => { + attempts++; + if (attempts < 3) { + throw new Error("fail"); + } + }, + { retry: 3 }, + ); + test("correct number of attempts from previous test", () => { + expect(attempts).toBe(3); + }); +}); + +describe("repeats option with hooks", () => { + let log: string[] = []; + describe("isolated test with repeats", () => { + beforeEach(() => { + log.push("beforeEach"); + }); + + afterEach(() => { + log.push("afterEach"); + }); + + test( + "repeats test multiple times", + () => { + log.push("test"); + }, + { repeats: 2 }, + ); + }); + + test("verify hooks ran for each repeat", () => { + // Should have: beforeEach, test, afterEach (first), beforeEach, test, afterEach (second), beforeEach, test, afterEach (third) + // repeats: 2 means 1 initial + 2 repeats = 3 total runs + expect(log).toEqual([ + "beforeEach", + "test", + "afterEach", + "beforeEach", + "test", + "afterEach", + "beforeEach", + "test", + "afterEach", + ]); + }); +}); + +describe("retry option with hooks", () => { + let attempts = 0; + let log: string[] = []; + describe("isolated test with retry", () => { + beforeEach(() => { + log.push("beforeEach"); + }); + + afterEach(() => { + log.push("afterEach"); + }); + + test( + "retries with hooks", + () => { + attempts++; + log.push(`test-${attempts}`); + if (attempts < 2) { + throw new Error("fail"); + } + }, + { retry: 3 }, + ); + }); + + test("verify hooks ran for each retry", () => { + // Should have: beforeEach, test-1, afterEach (fail), beforeEach, test-2, afterEach (pass) + expect(log).toEqual(["beforeEach", "test-1", "afterEach", "beforeEach", "test-2", "afterEach"]); + }); +}); +describe("repeats with onTestFinished", () => { + let log: string[] = []; + test( + "repeats with onTestFinished", + () => { + onTestFinished(() => { + log.push("onTestFinished"); + }); + log.push("test"); + }, + { repeats: 3 }, + ); + test("verify correct log", () => { + // repeats: 3 means 1 initial + 3 repeats = 4 total runs + expect(log).toEqual([ + "test", + "onTestFinished", + "test", + "onTestFinished", + "test", + "onTestFinished", + "test", + "onTestFinished", + ]); + }); +}); + +describe("retry with onTestFinished", () => { + let attempts = 0; + let log: string[] = []; + test( + "retry with onTestFinished", + () => { + attempts++; + onTestFinished(() => { + log.push("onTestFinished"); + }); + log.push(`test-${attempts}`); + if (attempts < 3) { + throw new Error("fail"); + } + }, + { retry: 3 }, + ); + test("verify correct log", () => { + expect(log).toEqual(["test-1", "onTestFinished", "test-2", "onTestFinished", "test-3", "onTestFinished"]); + }); +}); + +describe("retry with inner afterAll", () => { + let attempts = 0; + let log: string[] = []; + test( + "retry with inner afterAll", + () => { + attempts++; + afterAll(() => { + log.push("inner afterAll"); + }); + log.push(`test-${attempts}`); + if (attempts < 3) { + throw new Error("fail"); + } + }, + { retry: 3 }, + ); + test("verify correct log", () => { + expect(log).toEqual(["test-1", "inner afterAll", "test-2", "inner afterAll", "test-3", "inner afterAll"]); + }); +}); + +expect(() => { + test("can't pass both", () => {}, { retry: 5, repeats: 6 }); +}).toThrow(/Cannot set both retry and repeats/); diff --git a/test/js/bun/yaml/yaml.test.ts b/test/js/bun/yaml/yaml.test.ts index 2b451c9909..59276fc20e 100644 --- a/test/js/bun/yaml/yaml.test.ts +++ b/test/js/bun/yaml/yaml.test.ts @@ -1329,6 +1329,11 @@ my_config: // Octal numbers expect(YAML.stringify("0o777")).toBe('"0o777"'); expect(YAML.stringify("0O644")).toBe('"0O644"'); + + // Zero prefix + expect(YAML.stringify({ a: "011", b: "110" })).toBe('{a: "011",b: "110"}'); + expect(YAML.stringify(YAML.parse('"0123"'))).toBe('"0123"'); + expect(YAML.stringify("0000123")).toBe('"0000123"'); }); test("quotes strings with colons followed by spaces", () => {