Skip to content

implement publicHoistPattern and hoistPattern - #23567

Merged
dylan-conway merged 18 commits into
mainfrom
dylan/public-hoist-pattern
Oct 21, 2025
Merged

dylan-conway merged 18 commits into
mainfrom
dylan/public-hoist-pattern

Conversation

@dylan-conway

@dylan-conway dylan-conway commented Oct 13, 2025 •

Copy link
Copy Markdown
Member

What does this PR do?

Adds support for publicHoistPattern in bunfig.toml and public-hoist-pattern from .npmrc. This setting allows you to select transitive packages to hoist to the root node_modules making them available for all workspace packages.

[install]
# can be a string
publicHoistPattern = "@types*"
# or an array
publicHoistPattern = [ "@types*", "*eslint*" ]

publicHoistPattern only affects the isolated linker.


Adds hoistPattern. hoistPattern is the same as publicHoistPattern, but applies to the node_modules/.bun/node_modules directory instead of the root node_modules. Also the default value of hoistPattern is * (everything is hoisted to node_modules/.bun/node_modules by default).


Fixes a determinism issue constructing the node_modules/.bun/node_modules directory.


closes #23481
closes #6160
closes #23548

How did you verify your code works?

Added tests for

  • only include patterns
  • only exclude patterns
  • mix of include and exclude
  • errors for unexpected expression types
  • excluding direct dependency (should still include)
  • match all with *
  • string and array expression types

@robobun

robobun commented Oct 13, 2025 •

Copy link
Copy Markdown
Collaborator
Updated 4:22 AM PT - Oct 21st, 2025

❌ @dylan-conway, your commit b7e82e5 has 4 failures in Build #29849 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 23567

That installs a local version of the PR into your bun-23567 executable, so you can run:

bun-23567 --bun

@coderabbitai

This comment was marked as duplicate.

@coderabbitai

coderabbitai Bot commented Oct 13, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The head commit changed during the review from 76c1ae8 to 2811a3b.


Comment @coderabbitai help to get the list of available commands and usage tips.

1 similar comment
@coderabbitai

coderabbitai Bot commented Oct 13, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The head commit changed during the review from 76c1ae8 to 2811a3b.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (1)
src/string/escapeRegExp.zig (1)

61-66: Same inconsistent behavior as in escapeRegExp.

This else branch has the same debug/release inconsistency as the one in escapeRegExp (lines 25-30).

Apply the same fix:

-            else => |c| {
-                if (comptime Environment.isDebug) {
-                    unreachable;
-                }
-                try writer.writeByte(c);
-            },
+            else => unreachable, // all special_characters are handled above
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between db7bcd7 and 2811a3b.

📒 Files selected for processing (18)
  • src/api/schema.zig (2 hunks)
  • src/bun.js/bindings/RegularExpression.zig (1 hunks)
  • src/bun.js/jsc.zig (1 hunks)
  • src/bun.js/test/jest.zig (1 hunks)
  • src/bun.zig (0 hunks)
  • src/bunfig.zig (1 hunks)
  • src/cli.zig (1 hunks)
  • src/cli/Arguments.zig (1 hunks)
  • src/collections/array_list.zig (1 hunks)
  • src/ini.zig (1 hunks)
  • src/install/PackageManager/PackageManagerOptions.zig (2 hunks)
  • src/install/PnpmMatcher.zig (1 hunks)
  • src/install/install.zig (1 hunks)
  • src/install/isolated_install.zig (2 hunks)
  • src/string/escapeRegExp.zig (1 hunks)
  • src/string/immutable.zig (2 hunks)
  • test/cli/install/public-hoist-pattern.test.ts (1 hunks)
  • test/harness.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • src/bun.zig
🧰 Additional context used
📓 Path-based instructions (10)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/bun.js/jsc.zig
  • src/collections/array_list.zig
  • src/string/escapeRegExp.zig
  • src/cli/Arguments.zig
  • src/install/PackageManager/PackageManagerOptions.zig
  • src/bun.js/test/jest.zig
  • src/install/PnpmMatcher.zig
  • src/install/install.zig
  • src/api/schema.zig
  • src/cli.zig
  • src/bunfig.zig
  • src/install/isolated_install.zig
  • src/string/immutable.zig
  • src/bun.js/bindings/RegularExpression.zig
  • src/ini.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/jsc.zig
  • src/bun.js/test/jest.zig
  • src/bun.js/bindings/RegularExpression.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/bun.js/jsc.zig
  • src/collections/array_list.zig
  • src/string/escapeRegExp.zig
  • src/cli/Arguments.zig
  • src/install/PackageManager/PackageManagerOptions.zig
  • src/bun.js/test/jest.zig
  • src/install/PnpmMatcher.zig
  • src/install/install.zig
  • src/api/schema.zig
  • src/cli.zig
  • src/bunfig.zig
  • src/install/isolated_install.zig
  • src/string/immutable.zig
  • src/bun.js/bindings/RegularExpression.zig
  • src/ini.zig
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/harness.ts
  • test/cli/install/public-hoist-pattern.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/harness.ts
  • test/cli/install/public-hoist-pattern.test.ts
test/**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

For large/repetitive strings, use Buffer.alloc(count, fill).toString() instead of "A".repeat(count)

Files:

  • test/harness.ts
  • test/cli/install/public-hoist-pattern.test.ts
test/cli/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/cli/**/*.{js,ts}: Place CLI command tests (e.g., bun install, bun init) under test/cli/
When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Files:

  • test/cli/install/public-hoist-pattern.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must live under test/ and end with .test.ts or .test.tsx
In tests, always use port: 0; do not hardcode ports or roll your own random port
Prefer normalizeBunSnapshot for snapshotting test output instead of asserting raw strings
Do not write tests that assert absence of crashes (e.g., 'no panic' or 'no uncaught exception')
Use Bun’s Jest-compatible runner (import { test, expect } from "bun:test") for tests
Avoid shell commands like find or grep in tests; use Bun’s Glob and built-in tools instead
Prefer running tests via bun bd test and use provided harness utilities (bunEnv, bunExe, tempDir)
Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests

Files:

  • test/cli/install/public-hoist-pattern.test.ts
test/cli/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place CLI command tests under test/cli/

Files:

  • test/cli/install/public-hoist-pattern.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun:test for files ending with *.test.{ts,js,jsx,tsx,mjs,cjs}
Prefer concurrent tests (test.concurrent/describe.concurrent) over sequential when feasible
Organize tests with describe blocks to group related tests
Use utilities like describe.each, toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach) and track resources for cleanup

Files:

  • test/cli/install/public-hoist-pattern.test.ts
🧠 Learnings (10)
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS<ClassName> and re-export toJS/fromJS/fromJSDirect

Applied to files:

  • src/bun.js/jsc.zig
  • src/bun.js/test/jest.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/bindings/generated_classes_list.zig : Update src/bun.js/bindings/generated_classes_list.zig to include new classes

Applied to files:

  • src/bun.js/jsc.zig
  • src/bun.js/test/jest.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/{**/js_*.zig,bun.js/api/**/*.zig} : Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Applied to files:

  • src/bun.js/jsc.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)

Applied to files:

  • src/bun.js/jsc.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors

Applied to files:

  • src/bun.js/test/jest.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions

Applied to files:

  • src/bun.js/test/jest.zig
📚 Learning: 2025-08-30T00:11:00.890Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.890Z
Learning: Applies to **/*.zig : Wrap the Bun__<Type>__toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Applied to files:

  • src/bun.js/test/jest.zig
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to src/js/bun/** : Organize Bun-specific built-in modules under src/js/bun/ (e.g., bun:ffi, bun:sqlite)

Applied to files:

  • src/api/schema.zig
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/cli/install/public-hoist-pattern.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/cli/install/public-hoist-pattern.test.ts
🧬 Code graph analysis (1)
test/cli/install/public-hoist-pattern.test.ts (1)
test/harness.ts (5)
  • VerdaccioRegistry (1631-1767)
  • runBunInstall (1215-1271)
  • bunEnv (49-64)
  • readdirSorted (1776-1780)
  • bunExe (102-105)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (10)
src/collections/array_list.zig (1)

122-124: LGTM!

The writer method implementation is correct and follows the established delegation pattern in this file, properly converting the managed allocator to a standard allocator for the unmanaged writer.

src/string/immutable.zig (2)

2309-2310: LGTM!

The new public exports for regex escaping functions follow the established pattern in this module and provide clear, well-named API surface.


2353-2353: LGTM!

The import statement correctly follows the coding guideline to place @import directives at the bottom of Zig files.

src/string/escapeRegExp.zig (2)

1-1: LGTM!

The special_characters set correctly includes standard regex metacharacters that need escaping.


74-76: LGTM!

The import statements are correctly placed at the bottom of the file, following the coding guidelines.

src/install/isolated_install.zig (3)

415-416: LGTM!

Memory management is correct. The publicly_hoisted map is properly initialized with manager.allocator and cleanup is ensured via defer.


539-543: Ensure new_entry_dep_id is valid before indexing
The code does dependencies[new_entry_dep_id] when entry.entry_parent_id == .root; verify that new_entry_dep_id can never be invalid_dependency_id here or add a guard to skip/error if it is.


545-559: Verify root entry and duplicate insertion handling

No evidence confirms that entry_dependencies[0] always refers to the root entry or that inserting only by name prevents duplicate (entry_id, dep_id) pairs. Manually verify that:

  • The root entry is initialized exactly once at index 0.
  • The insert logic guards against duplicate (entry_id, dep_id) entries, not just duplicate names.
src/install/PnpmMatcher.zig (2)

107-130: LGTM!

The createMatcher function correctly handles:

  • Trimming whitespace
  • Exclusion prefix (!)
  • Wildcard optimization (*)
  • Regex escaping and anchoring (^...$)

The use of .cloneUTF8 ensures the regex owns its pattern string.


170-186: ```shell
#!/bin/bash

Inspect PnpmMatcher parsing logic to verify matcher order

rg -n "pub fn.*PnpmMatcher" -C3 src/install/PnpmMatcher.zig
rg -n "parse" -C3 src/install/PnpmMatcher.zig
rg -n ".append" -C3 src/install/PnpmMatcher.zig


</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread src/ini.zig
Comment on lines +1072 to +1085
if (out.get("public-hoist-pattern")) |public_hoist_pattern_expr| {
install.public_hoist_patterns = bun.install.PnpmMatcher.fromExpr(
allocator,
public_hoist_pattern_expr,
log,
source,
) catch |err| switch (err) {
error.OutOfMemory => |oom| return oom,
error.InvalidRegExp, error.UnexpectedExpr => patterns: {
log.reset();
break :patterns null;
},
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not clear the entire npmrc log on matcher errors

log.reset() nukes every earlier warning/error collected while parsing the same .npmrc, so any unrelated diagnostics disappear as soon as we hit an invalid hoist pattern. That makes troubleshooting other config mistakes impossible. Instead, let the log keep the matcher’s error (or just return null) without resetting it entirely.

Apply this diff to retain prior log entries:

-        ) catch |err| switch (err) {
-            error.OutOfMemory => |oom| return oom,
-            error.InvalidRegExp, error.UnexpectedExpr => patterns: {
-                log.reset();
-                break :patterns null;
-            },
-        };
+        ) catch |err| switch (err) {
+            error.OutOfMemory => |oom| return oom,
+            error.InvalidRegExp, error.UnexpectedExpr => null,
+        };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (out.get("public-hoist-pattern")) |public_hoist_pattern_expr| {
install.public_hoist_patterns = bun.install.PnpmMatcher.fromExpr(
allocator,
public_hoist_pattern_expr,
log,
source,
) catch |err| switch (err) {
error.OutOfMemory => |oom| return oom,
error.InvalidRegExp, error.UnexpectedExpr => patterns: {
log.reset();
break :patterns null;
},
};
}
if (out.get("public-hoist-pattern")) |public_hoist_pattern_expr| {
install.public_hoist_patterns = bun.install.PnpmMatcher.fromExpr(
allocator,
public_hoist_pattern_expr,
log,
source,
) catch |err| switch (err) {
error.OutOfMemory => |oom| return oom,
error.InvalidRegExp, error.UnexpectedExpr => null,
};
}
🤖 Prompt for AI Agents
In src/ini.zig around lines 1072 to 1085, the current error handling for
PnpmMatcher.fromExpr calls log.reset() which clears all prior npmrc diagnostics;
remove the log.reset() call and instead simply return null (or let the matcher
error remain) when InvalidRegExp or UnexpectedExpr occur so earlier
warnings/errors are preserved; update the catch arm to omit log.reset() and
break/return null as before.

Comment on lines +1 to +198
/// https://github.com/pnpm/pnpm/blob/3abd3946237aa6ba7831552310ec371ddd3616c2/config/matcher/src/index.ts
const PnpmMatcher = @This();

matchers: []const Matcher,
behavior: Behavior,

const Matcher = struct {
pattern: union(enum) {
match_all,
regex: *jsc.RegularExpression,
},
is_exclude: bool,
};

const Behavior = enum {
all_matchers_include,
all_matchers_exclude,
has_exclude_and_include_matchers,
};

const FromExprError = OOM || error{
InvalidRegExp,
UnexpectedExpr,
};

pub fn fromExpr(allocator: std.mem.Allocator, expr: ast.Expr, log: *logger.Log, source: *const logger.Source) FromExprError!PnpmMatcher {
var buf: collections.ArrayListDefault(u8) = .init();
defer buf.deinit();

bun.jsc.initialize(false);

var matchers: collections.ArrayListDefault(Matcher) = .init();

var has_include = false;
var has_exclude = false;

switch (expr.data) {
.e_string => {
const pattern = expr.data.e_string.slice(allocator);
const matcher = createMatcher(pattern, &buf) catch |err| switch (err) {
error.OutOfMemory => return err,
error.InvalidRegExp => {
try log.addErrorFmtOpts(allocator, "Invalid regex: {s}", .{pattern}, .{
.loc = expr.loc,
.redact_sensitive_information = true,
.source = source,
});
return err;
},
};
has_include = has_include or !matcher.is_exclude;
has_exclude = has_exclude or matcher.is_exclude;
try matchers.append(matcher);
},
.e_array => |patterns| {
for (patterns.slice()) |pattern_expr| {
if (try pattern_expr.asStringCloned(allocator)) |pattern| {
const matcher = createMatcher(pattern, &buf) catch |err| switch (err) {
error.OutOfMemory => return err,
error.InvalidRegExp => {
try log.addErrorFmtOpts(allocator, "Invalid regex: {s}", .{pattern}, .{
.loc = pattern_expr.loc,
.redact_sensitive_information = true,
.source = source,
});
return err;
},
};
has_include = has_include or !matcher.is_exclude;
has_exclude = has_exclude or matcher.is_exclude;
try matchers.append(matcher);
} else {
try log.addErrorOpts("Expected a string", .{
.loc = pattern_expr.loc,
.redact_sensitive_information = true,
.source = source,
});
return error.UnexpectedExpr;
}
}
},
else => {
try log.addErrorOpts("Expected a string or an array of strings", .{
.loc = expr.loc,
.redact_sensitive_information = true,
.source = source,
});
return error.UnexpectedExpr;
},
}

const behavior: Behavior = if (!has_include)
.all_matchers_exclude
else if (!has_exclude)
.all_matchers_include
else
.has_exclude_and_include_matchers;

return .{
.matchers = try matchers.toOwnedSlice(),
.behavior = behavior,
};
}

const CreateMatcherError = OOM || error{InvalidRegExp};

fn createMatcher(raw: []const u8, buf: *collections.ArrayListDefault(u8)) CreateMatcherError!Matcher {
buf.clearRetainingCapacity();
var writer = buf.writer();

var trimmed = strings.trim(raw, &strings.whitespace_chars);

var is_exclude = false;
if (strings.startsWithChar(trimmed, '!')) {
is_exclude = true;
trimmed = trimmed[1..];
}

if (strings.eqlComptime(trimmed, "*")) {
return .{ .pattern = .match_all, .is_exclude = is_exclude };
}

try writer.writeByte('^');
try strings.escapeRegExpForPackageNameMatching(trimmed, writer);
try writer.writeByte('$');

const regex = try jsc.RegularExpression.init(.cloneUTF8(buf.items()), .none);

return .{ .pattern = .{ .regex = regex }, .is_exclude = is_exclude };
}

pub fn isMatch(this: *const PnpmMatcher, name: []const u8) bool {
if (this.matchers.len == 0) {
return false;
}

const name_str: String = .fromBytes(name);

switch (this.behavior) {
.all_matchers_include => {
for (this.matchers) |matcher| {
switch (matcher.pattern) {
.match_all => {
return true;
},
.regex => |regex| {
if (regex.matches(name_str)) {
return true;
}
},
}
}
return false;
},
.all_matchers_exclude => {
for (this.matchers) |matcher| {
switch (matcher.pattern) {
.match_all => {
return false;
},
.regex => |regex| {
if (regex.matches(name_str)) {
return false;
}
},
}
}
return true;
},
.has_exclude_and_include_matchers => {
var matches = false;
for (this.matchers) |matcher| {
switch (matcher.pattern) {
.match_all => {
matches = !matcher.is_exclude;
},
.regex => |regex| {
if (regex.matches(name_str)) {
matches = !matcher.is_exclude;
}
},
}
}
return matches;
},
}
}

const std = @import("std");

const bun = @import("bun");
const OOM = bun.OOM;
const String = bun.String;
const ast = bun.ast;
const collections = bun.collections;
const jsc = bun.jsc;
const logger = bun.logger;
const strings = bun.strings;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding inline documentation.

This module implements PNPM's public-hoist-pattern matching logic. While the reference link (line 1) is helpful, consider adding brief doc comments on the public functions (fromExpr, isMatch) to explain:

  • Expected input format
  • Matching semantics for each behavior mode
  • Memory ownership (caller must deinit)

Example:

+/// Parse a bunfig/npmrc expression into a PnpmMatcher.
+/// Returns a matcher that must be deinitialized by the caller.
+/// Supports string or array of strings, with optional "!" prefix for exclusion.
 pub fn fromExpr(allocator: std.mem.Allocator, expr: ast.Expr, log: *logger.Log, source: *const logger.Source) FromExprError!PnpmMatcher {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// https://github.com/pnpm/pnpm/blob/3abd3946237aa6ba7831552310ec371ddd3616c2/config/matcher/src/index.ts
const PnpmMatcher = @This();
matchers: []const Matcher,
behavior: Behavior,
const Matcher = struct {
pattern: union(enum) {
match_all,
regex: *jsc.RegularExpression,
},
is_exclude: bool,
};
const Behavior = enum {
all_matchers_include,
all_matchers_exclude,
has_exclude_and_include_matchers,
};
const FromExprError = OOM || error{
InvalidRegExp,
UnexpectedExpr,
};
pub fn fromExpr(allocator: std.mem.Allocator, expr: ast.Expr, log: *logger.Log, source: *const logger.Source) FromExprError!PnpmMatcher {
var buf: collections.ArrayListDefault(u8) = .init();
defer buf.deinit();
bun.jsc.initialize(false);
var matchers: collections.ArrayListDefault(Matcher) = .init();
var has_include = false;
var has_exclude = false;
switch (expr.data) {
.e_string => {
const pattern = expr.data.e_string.slice(allocator);
const matcher = createMatcher(pattern, &buf) catch |err| switch (err) {
error.OutOfMemory => return err,
error.InvalidRegExp => {
try log.addErrorFmtOpts(allocator, "Invalid regex: {s}", .{pattern}, .{
.loc = expr.loc,
.redact_sensitive_information = true,
.source = source,
});
return err;
},
};
has_include = has_include or !matcher.is_exclude;
has_exclude = has_exclude or matcher.is_exclude;
try matchers.append(matcher);
},
.e_array => |patterns| {
for (patterns.slice()) |pattern_expr| {
if (try pattern_expr.asStringCloned(allocator)) |pattern| {
const matcher = createMatcher(pattern, &buf) catch |err| switch (err) {
error.OutOfMemory => return err,
error.InvalidRegExp => {
try log.addErrorFmtOpts(allocator, "Invalid regex: {s}", .{pattern}, .{
.loc = pattern_expr.loc,
.redact_sensitive_information = true,
.source = source,
});
return err;
},
};
has_include = has_include or !matcher.is_exclude;
has_exclude = has_exclude or matcher.is_exclude;
try matchers.append(matcher);
} else {
try log.addErrorOpts("Expected a string", .{
.loc = pattern_expr.loc,
.redact_sensitive_information = true,
.source = source,
});
return error.UnexpectedExpr;
}
}
},
else => {
try log.addErrorOpts("Expected a string or an array of strings", .{
.loc = expr.loc,
.redact_sensitive_information = true,
.source = source,
});
return error.UnexpectedExpr;
},
}
const behavior: Behavior = if (!has_include)
.all_matchers_exclude
else if (!has_exclude)
.all_matchers_include
else
.has_exclude_and_include_matchers;
return .{
.matchers = try matchers.toOwnedSlice(),
.behavior = behavior,
};
}
const CreateMatcherError = OOM || error{InvalidRegExp};
fn createMatcher(raw: []const u8, buf: *collections.ArrayListDefault(u8)) CreateMatcherError!Matcher {
buf.clearRetainingCapacity();
var writer = buf.writer();
var trimmed = strings.trim(raw, &strings.whitespace_chars);
var is_exclude = false;
if (strings.startsWithChar(trimmed, '!')) {
is_exclude = true;
trimmed = trimmed[1..];
}
if (strings.eqlComptime(trimmed, "*")) {
return .{ .pattern = .match_all, .is_exclude = is_exclude };
}
try writer.writeByte('^');
try strings.escapeRegExpForPackageNameMatching(trimmed, writer);
try writer.writeByte('$');
const regex = try jsc.RegularExpression.init(.cloneUTF8(buf.items()), .none);
return .{ .pattern = .{ .regex = regex }, .is_exclude = is_exclude };
}
pub fn isMatch(this: *const PnpmMatcher, name: []const u8) bool {
if (this.matchers.len == 0) {
return false;
}
const name_str: String = .fromBytes(name);
switch (this.behavior) {
.all_matchers_include => {
for (this.matchers) |matcher| {
switch (matcher.pattern) {
.match_all => {
return true;
},
.regex => |regex| {
if (regex.matches(name_str)) {
return true;
}
},
}
}
return false;
},
.all_matchers_exclude => {
for (this.matchers) |matcher| {
switch (matcher.pattern) {
.match_all => {
return false;
},
.regex => |regex| {
if (regex.matches(name_str)) {
return false;
}
},
}
}
return true;
},
.has_exclude_and_include_matchers => {
var matches = false;
for (this.matchers) |matcher| {
switch (matcher.pattern) {
.match_all => {
matches = !matcher.is_exclude;
},
.regex => |regex| {
if (regex.matches(name_str)) {
matches = !matcher.is_exclude;
}
},
}
}
return matches;
},
}
}
const std = @import("std");
const bun = @import("bun");
const OOM = bun.OOM;
const String = bun.String;
const ast = bun.ast;
const collections = bun.collections;
const jsc = bun.jsc;
const logger = bun.logger;
const strings = bun.strings;
/// Parse a bunfig/npmrc expression into a PnpmMatcher.
/// Returns a matcher that must be deinitialized by the caller.
/// Supports string or array of strings, with optional "!" prefix for exclusion.
pub fn fromExpr(allocator: std.mem.Allocator, expr: ast.Expr, log: *logger.Log, source: *const logger.Source) FromExprError!PnpmMatcher {
var buf: collections.ArrayListDefault(u8) = .init();
defer buf.deinit();
bun.jsc.initialize(false);
var matchers: collections.ArrayListDefault(Matcher) = .init();
// … rest of function unchanged …
}
🤖 Prompt for AI Agents
In src/install/PnpmMatcher.zig lines 1-198, add concise Zig doc comments (///)
at the top of the file and immediately above the public functions fromExpr and
isMatch: describe expected input formats (string or array of strings for
fromExpr), the matching semantics for each Behavior variant
(all_matchers_include, all_matchers_exclude, has_exclude_and_include_matchers),
that patterns support '!' exclude prefix and '*' match-all, and clearly state
memory ownership (fromExpr allocates matcher slice and the caller must
deinit/free it or the backing allocator must outlive usage). Keep comments
short, reference the PNPM URL already present, and ensure docs mention error
cases returned by fromExpr (InvalidRegExp, UnexpectedExpr, OOM).

Comment on lines +26 to +103
pub fn fromExpr(allocator: std.mem.Allocator, expr: ast.Expr, log: *logger.Log, source: *const logger.Source) FromExprError!PnpmMatcher {
var buf: collections.ArrayListDefault(u8) = .init();
defer buf.deinit();

bun.jsc.initialize(false);

var matchers: collections.ArrayListDefault(Matcher) = .init();

var has_include = false;
var has_exclude = false;

switch (expr.data) {
.e_string => {
const pattern = expr.data.e_string.slice(allocator);
const matcher = createMatcher(pattern, &buf) catch |err| switch (err) {
error.OutOfMemory => return err,
error.InvalidRegExp => {
try log.addErrorFmtOpts(allocator, "Invalid regex: {s}", .{pattern}, .{
.loc = expr.loc,
.redact_sensitive_information = true,
.source = source,
});
return err;
},
};
has_include = has_include or !matcher.is_exclude;
has_exclude = has_exclude or matcher.is_exclude;
try matchers.append(matcher);
},
.e_array => |patterns| {
for (patterns.slice()) |pattern_expr| {
if (try pattern_expr.asStringCloned(allocator)) |pattern| {
const matcher = createMatcher(pattern, &buf) catch |err| switch (err) {
error.OutOfMemory => return err,
error.InvalidRegExp => {
try log.addErrorFmtOpts(allocator, "Invalid regex: {s}", .{pattern}, .{
.loc = pattern_expr.loc,
.redact_sensitive_information = true,
.source = source,
});
return err;
},
};
has_include = has_include or !matcher.is_exclude;
has_exclude = has_exclude or matcher.is_exclude;
try matchers.append(matcher);
} else {
try log.addErrorOpts("Expected a string", .{
.loc = pattern_expr.loc,
.redact_sensitive_information = true,
.source = source,
});
return error.UnexpectedExpr;
}
}
},
else => {
try log.addErrorOpts("Expected a string or an array of strings", .{
.loc = expr.loc,
.redact_sensitive_information = true,
.source = source,
});
return error.UnexpectedExpr;
},
}

const behavior: Behavior = if (!has_include)
.all_matchers_exclude
else if (!has_exclude)
.all_matchers_include
else
.has_exclude_and_include_matchers;

return .{
.matchers = try matchers.toOwnedSlice(),
.behavior = behavior,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential memory leak: regex objects are not cleaned up.

The fromExpr function creates Matcher structs containing jsc.RegularExpression pointers via createMatcher, but there's no corresponding deinit or cleanup function for PnpmMatcher. If regex objects require explicit cleanup, this will leak memory.

Consider adding a deinit method:

+pub fn deinit(this: *PnpmMatcher, allocator: std.mem.Allocator) void {
+    for (this.matchers) |matcher| {
+        switch (matcher.pattern) {
+            .regex => |regex| regex.deinit(),
+            .match_all => {},
+        }
+    }
+    allocator.free(this.matchers);
+}

And ensure callers invoke it when done with the matcher.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/install/PnpmMatcher.zig around lines 26-103, the function builds Matcher
objects that hold jsc.RegularExpression pointers but there is no cleanup,
risking leaked regex objects; add a PnpmMatcher.deinit that iterates its
matchers, calls the appropriate regex deinit/free on each Matcher (and then
frees the matchers slice/array), and wire deinit usage so callers free returned
PnpmMatcher when done; additionally, ensure fromExpr does not leak on early
returns by cleaning up any partially appended matchers (for example use a defer
cleanup that runs on error paths or release matchers before returning an error).

Comment on lines +3 to +72
pub fn escapeRegExp(input: []const u8, writer: anytype) @TypeOf(writer).Error!void {
var remain = input;

while (strings.indexOfAny(remain, special_characters)) |i| {
try writer.writeAll(remain[0..i]);
switch (remain[i]) {
'|',
'\\',
'{',
'}',
'(',
')',
'[',
']',
'^',
'$',
'+',
'*',
'?',
'.',
=> |c| try writer.writeAll(&.{ '\\', c }),
'-' => try writer.writeAll("\\x2d"),
else => |c| {
if (comptime Environment.isDebug) {
unreachable;
}
try writer.writeByte(c);
},
}
remain = remain[i + 1 ..];
}

try writer.writeAll(remain);
}

/// '*' becomes '.*' instead of '\\*'
pub fn escapeRegExpForPackageNameMatching(input: []const u8, writer: anytype) @TypeOf(writer).Error!void {
var remain = input;

while (strings.indexOfAny(remain, special_characters)) |i| {
try writer.writeAll(remain[0..i]);
switch (remain[i]) {
'|',
'\\',
'{',
'}',
'(',
')',
'[',
']',
'^',
'$',
'+',
'?',
'.',
=> |c| try writer.writeAll(&.{ '\\', c }),
'*' => try writer.writeAll(".*"),
'-' => try writer.writeAll("\\x2d"),
else => |c| {
if (comptime Environment.isDebug) {
unreachable;
}
try writer.writeByte(c);
},
}
remain = remain[i + 1 ..];
}

try writer.writeAll(remain);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider reducing code duplication.

The two functions are nearly identical except for how they handle '*' (line 20 vs line 59). Consider refactoring using a comptime parameter:

pub fn escapeRegExp(input: []const u8, writer: anytype) @TypeOf(writer).Error!void {
    return escapeRegExpImpl(input, writer, false);
}

pub fn escapeRegExpForPackageNameMatching(input: []const u8, writer: anytype) @TypeOf(writer).Error!void {
    return escapeRegExpImpl(input, writer, true);
}

fn escapeRegExpImpl(input: []const u8, writer: anytype, comptime expand_star: bool) @TypeOf(writer).Error!void {
    var remain = input;
    while (strings.indexOfAny(remain, special_characters)) |i| {
        try writer.writeAll(remain[0..i]);
        switch (remain[i]) {
            '|', '\\', '{', '}', '(', ')', '[', ']', '^', '$', '+', '?', '.',
            => |c| try writer.writeAll(&.{ '\\', c }),
            '*' => try writer.writeAll(if (comptime expand_star) ".*" else &.{ '\\', '*' }),
            '-' => try writer.writeAll("\\x2d"),
            else => unreachable,
        }
        remain = remain[i + 1 ..];
    }
    try writer.writeAll(remain);
}
🤖 Prompt for AI Agents
In src/string/escapeRegExp.zig around lines 3 to 72, both functions duplicate
the same loop and switch logic and only differ in how '*' is handled; refactor
by creating a single helper fn escapeRegExpImpl with a comptime parameter (e.g.,
expand_star: bool) that the two public functions call with true/false, move the
shared loop/switch into that impl, use a comptime branch to emit ".*" when
expand_star is true or backslash-escaped '*' otherwise, preserve the '-' and
other escapes, and keep the debug/unreachable behavior as before.

@@ -0,0 +1,76 @@
const special_characters = "|\\{}()[]^$+*?.-";

pub fn escapeRegExp(input: []const u8, writer: anytype) @TypeOf(writer).Error!void {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add documentation for public API.

These functions are exported as public API but lack doc comments. Document the purpose, parameters, and special character handling.

+/// Escapes regex special characters in the input string for use in a regular expression.
+/// Special characters: | \ { } ( ) [ ] ^ $ + * ? . -
+/// The '-' character is escaped as \x2d to avoid range interpretation issues.
 pub fn escapeRegExp(input: []const u8, writer: anytype) @TypeOf(writer).Error!void {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn escapeRegExp(input: []const u8, writer: anytype) @TypeOf(writer).Error!void {
/// Escapes regex special characters in the input string for use in a regular expression.
/// Special characters: | \ { } ( ) [ ] ^ $ + * ? . -
/// The '-' character is escaped as \x2d to avoid range interpretation issues.
pub fn escapeRegExp(input: []const u8, writer: anytype) @TypeOf(writer).Error!void {
🤖 Prompt for AI Agents
In src/string/escapeRegExp.zig around line 3, the public function escapeRegExp
lacks a doc comment; add a Zig doc comment (///) immediately above the pub fn
that succinctly states the function purpose (escape characters so the input can
be used in a regular expression), documents parameters (input: []const u8 —
source string; writer: anytype — writer to receive escaped output), lists
behavior for special regex characters that are escaped and any characters left
unchanged, documents error behavior/returns (propagates writer errors via
@TypeOf(writer).Error), and optionally gives a short usage example; keep the
comment concise and formatted as standard Zig documentation.

Comment on lines +25 to +30
else => |c| {
if (comptime Environment.isDebug) {
unreachable;
}
try writer.writeByte(c);
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Inconsistent behavior between debug and release builds.

The else branch is marked unreachable only in debug mode but falls back to writeByte(c) in release mode. This creates different behavior across build types and suggests a logic error.

If special_characters is exhaustive (all characters that indexOfAny can find are handled in the switch), the else branch should always be unreachable in all build modes. If it's not exhaustive, the behavior should be consistent.

Apply this diff to make the behavior consistent:

-            else => |c| {
-                if (comptime Environment.isDebug) {
-                    unreachable;
-                }
-                try writer.writeByte(c);
-            },
+            else => unreachable, // all special_characters are handled above

If characters beyond special_characters need handling, update both the constant and switch cases to be exhaustive.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
else => |c| {
if (comptime Environment.isDebug) {
unreachable;
}
try writer.writeByte(c);
},
else => unreachable, // all special_characters are handled above
🤖 Prompt for AI Agents
In src/string/escapeRegExp.zig around lines 25-30, the else branch currently
uses comptime Environment.isDebug to call unreachable in debug builds but falls
back to writer.writeByte(c) in release builds, causing inconsistent behavior;
fix this by making the branch behavior consistent: either (A) remove the
comptime check and always call try writer.writeByte(c) so unknown characters are
handled the same in all builds, or (B) make the branch unconditionally
unreachable and ensure the special_characters constant and the switch are
exhaustive by adding any missing characters to the constant and corresponding
cases; pick one option and update the code accordingly (if choosing B, add the
missing characters and cases so no unknown char can reach the else).

Comment on lines +38 to +39
/// '*' becomes '.*' instead of '\\*'
pub fn escapeRegExpForPackageNameMatching(input: []const u8, writer: anytype) @TypeOf(writer).Error!void {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Enhance documentation with proper doc comment.

Replace the inline comment with a proper doc comment that explains the wildcard expansion behavior.

-/// '*' becomes '.*' instead of '\\*'
+/// Similar to escapeRegExp but treats '*' as a package-name wildcard.
+/// The '*' character is expanded to '.*' to match any sequence of characters,
+/// enabling patterns like "@types*" to match "@types/node", "@typescript-eslint/*", etc.
 pub fn escapeRegExpForPackageNameMatching(input: []const u8, writer: anytype) @TypeOf(writer).Error!void {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// '*' becomes '.*' instead of '\\*'
pub fn escapeRegExpForPackageNameMatching(input: []const u8, writer: anytype) @TypeOf(writer).Error!void {
/// Similar to escapeRegExp but treats '*' as a package-name wildcard.
/// The '*' character is expanded to '.*' to match any sequence of characters,
/// enabling patterns like "@types*" to match "@types/node", "@typescript-eslint/*", etc.
pub fn escapeRegExpForPackageNameMatching(input: []const u8, writer: anytype) @TypeOf(writer).Error!void {
// …rest of implementation…
}
🤖 Prompt for AI Agents
In src/string/escapeRegExp.zig around lines 38 to 39, replace the inline comment
"/// '*' becomes '.*' instead of '\\*'" with a proper Zig doc comment (///) that
clearly explains the function's wildcard expansion behavior: state that the
function escapes input for RegExp but treats '*' as a wildcard which is expanded
to '.*' rather than escaped, describe expected input and output behavior
concisely, and keep it adjacent to the function signature so external docs pick
it up.

@dylan-conway dylan-conway changed the title implement publicHoistPattern implement publicHoistPattern and hoistPattern Oct 13, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/ini.zig (1)

1072-1100: Do not wipe existing .npmrc diagnostics on matcher parse failures.

log.reset() still erases every warning/error accumulated while parsing the same .npmrc, so any unrelated diagnostics disappear the moment a hoist-pattern is invalid. Drop the reset and just return null (or let the matcher error stand) so earlier messages survive.

-        ) catch |err| switch (err) {
-            error.OutOfMemory => |oom| return oom,
-            error.InvalidRegExp, error.UnexpectedExpr => patterns: {
-                log.reset();
-                break :patterns null;
-            },
-        };
+        ) catch |err| switch (err) {
+            error.OutOfMemory => |oom| return oom,
+            error.InvalidRegExp, error.UnexpectedExpr => null,
+        };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2811a3b and 1f684eb.

📒 Files selected for processing (7)
  • src/api/schema.zig (2 hunks)
  • src/bunfig.zig (1 hunks)
  • src/ini.zig (1 hunks)
  • src/install/PackageManager/PackageManagerOptions.zig (2 hunks)
  • src/install/isolated_install.zig (3 hunks)
  • src/install/isolated_install/Installer.zig (2 hunks)
  • src/install/isolated_install/Store.zig (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/install/isolated_install/Store.zig
  • src/install/isolated_install/Installer.zig
  • src/ini.zig
  • src/install/PackageManager/PackageManagerOptions.zig
  • src/bunfig.zig
  • src/install/isolated_install.zig
  • src/api/schema.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/install/isolated_install/Store.zig
  • src/install/isolated_install/Installer.zig
  • src/ini.zig
  • src/install/PackageManager/PackageManagerOptions.zig
  • src/bunfig.zig
  • src/install/isolated_install.zig
  • src/api/schema.zig
🧠 Learnings (1)
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to src/js/bun/** : Organize Bun-specific built-in modules under src/js/bun/ (e.g., bun:ffi, bun:sqlite)

Applied to files:

  • src/api/schema.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (3)
src/install/PackageManager/PackageManagerOptions.zig (1)

74-75: LGTM! Clean addition of hoisting pattern configuration.

The new public_hoist_pattern and hoist_pattern fields follow the established pattern for optional configuration. The propagation in the load function correctly transfers values from the config to the options struct, mirroring the approach used for other configuration fields.

Also applies to: 392-398

src/install/isolated_install.zig (2)

415-419: LGTM! Proper memory management for hoisting state.

The hash maps are correctly initialized with manager.allocator and properly deferred for cleanup, following Zig memory management best practices.


514-527: LGTM! Hoisting decision logic correctly handles deduplication.

The logic appropriately:

  • Defaults hoisted to true when hoist_pattern is null (backward compatibility)
  • Uses hidden_hoisted to ensure only the first matching transitive dependency per package name is marked as hoisted
  • This prevents duplicate hoisting of packages with the same name

Comment thread src/install/isolated_install.zig Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/install/isolated_install.zig (1)

557-579: Public hoist injection logic looks correct; prior safety concern resolved.

  • Guard on new_entry_dep_id != invalid_dependency_id is present; the earlier consistency issue is addressed.
  • Root direct deps are recorded to public_hoisted, and transitive public hoists are gated by it to avoid replacement.

Please confirm:

  • Store.Entry.DependenciesOrderedArraySetCtx uniqueness compares by package name (not just dep_id) so a transitive injected dep cannot replace a different direct root dep version.
  • For root → workspace direct deps (skipped via break :skip_adding_dependency), is there any other guard to prevent a same‑name transitive dep from being public‑hoisted and overshadowing the workspace link? If not, consider marking such names in public_hoisted before the break.

You can locate the comparator/equality for the ordered set with:

#!/bin/bash
rg -n -C3 'DependenciesOrderedArraySetCtx|OrderedArraySetCtx' --type=zig
rg -n -C5 'insert\\s*\\(.*DependenciesOrderedArraySetCtx' --type=zig
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1f684eb and caeb6c3.

📒 Files selected for processing (1)
  • src/install/isolated_install.zig (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/install/isolated_install.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/install/isolated_install.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (1)
src/install/isolated_install.zig (1)

534-534: LGTM: propagate hoist decision onto Entry.

The new .hoisted field cleanly carries the decision forward.

Comment on lines +415 to +419
var public_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
defer public_hoisted.deinit();

var hidden_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
defer hidden_hoisted.deinit();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Clarify map intent (brief comments).

Add short comments explaining each map’s role (guards against replacement vs. determinism for hidden hoists). This improves readability in a complex area.

-        var public_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
+        // public_hoisted: names owned by root (direct deps) and names already public‑hoisted.
+        // Prevents public hoist from replacing direct root dependencies.
+        var public_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
         defer public_hoisted.deinit();

-        var hidden_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
+        // hidden_hoisted: names already marked hoisted to node_modules/.bun/node_modules.
+        // Ensures determinism (first writer wins) for hidden hoists.
+        var hidden_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
         defer hidden_hoisted.deinit();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var public_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
defer public_hoisted.deinit();
var hidden_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
defer hidden_hoisted.deinit();
// public_hoisted: names owned by root (direct deps) and names already public-hoisted.
// Prevents public hoist from replacing direct root dependencies.
var public_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
defer public_hoisted.deinit();
// hidden_hoisted: names already marked hoisted to node_modules/.bun/node_modules.
// Ensures determinism (first writer wins) for hidden hoists.
var hidden_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
defer hidden_hoisted.deinit();
🤖 Prompt for AI Agents
In src/install/isolated_install.zig around lines 415 to 419, the two
StringArrayHashMap declarations lack inline comments explaining their intent;
add short comments immediately above or on the same line as each declaration:
for public_hoisted note it “guards against replacement of public hoisted
entries” (or similar concise phrase), and for hidden_hoisted note it “records
hidden hoisted entries to ensure deterministic ordering” (or similar concise
phrase), keeping the comments brief and focused on role to improve readability.

Comment thread src/install/isolated_install.zig Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/install/isolated_install.zig (1)

415-419: Brief comments for map intent (repeat).

Add one-line comments clarifying each map’s role (guard against replacing root deps vs. determinism for hidden hoists). Improves readability in a complex area.

Apply this diff:

-        var public_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
+        // public_hoisted: names owned by root (direct deps) and already public‑hoisted; prevents replacement
+        var public_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
         defer public_hoisted.deinit();

-        var hidden_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
+        // hidden_hoisted: names already hoisted into node_modules/.bun/node_modules; first‑writer wins (determinism)
+        var hidden_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
         defer hidden_hoisted.deinit();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between caeb6c3 and 2c07e93.

📒 Files selected for processing (1)
  • src/install/isolated_install.zig (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/install/isolated_install.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/install/isolated_install.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (2)
src/install/isolated_install.zig (2)

539-539: Wire-up of Store.Entry.hoisted: LGTM.

Field set correctly from computed flag.


514-532: Confirm root-deps hoisting semantics: verify that direct root dependencies (parent == .root) are intentionally eligible for hoisting; if not, add a guard to skip hoisting when parent == .root.

Comment on lines +563 to +584
if (new_entry_dep_id != invalid_dependency_id) {
if (entry.entry_parent_id == .root) {
// make sure direct dependencies are not replaced
const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
try public_hoisted.put(dep_name, {});
} else {
// transitive dependencies (also direct dependencies of workspaces!)
const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
if (manager.options.public_hoist_pattern) |public_hoist_pattern| {
if (public_hoist_pattern.isMatch(dep_name)) {
const hoist_entry = try public_hoisted.getOrPut(dep_name);
if (!hoist_entry.found_existing) {
try entry_dependencies[0].insert(
lockfile.allocator,
.{ .entry_id = new_entry_id, .dep_id = new_entry_dep_id },
&ctx,
);
}
}
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Use getOrPut for root insert; avoid double key copies and duplicate dep_name eval.

Saves allocations on existing keys and trims duplication.

Apply this diff:

-                if (new_entry_dep_id != invalid_dependency_id) {
-                    if (entry.entry_parent_id == .root) {
-                        // make sure direct dependencies are not replaced
-                        const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
-                        try public_hoisted.put(dep_name, {});
-                    } else {
-                        // transitive dependencies (also direct dependencies of workspaces!)
-                        const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
-                        if (manager.options.public_hoist_pattern) |public_hoist_pattern| {
-                            if (public_hoist_pattern.isMatch(dep_name)) {
-                                const hoist_entry = try public_hoisted.getOrPut(dep_name);
-                                if (!hoist_entry.found_existing) {
-                                    try entry_dependencies[0].insert(
-                                        lockfile.allocator,
-                                        .{ .entry_id = new_entry_id, .dep_id = new_entry_dep_id },
-                                        &ctx,
-                                    );
-                                }
-                            }
-                        }
-                    }
-                }
+                if (new_entry_dep_id != invalid_dependency_id) {
+                    const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
+                    if (entry.entry_parent_id == .root) {
+                        // mark direct dependencies as reserved to prevent replacement
+                        _ = try public_hoisted.getOrPut(dep_name);
+                    } else {
+                        // transitive dependencies (also direct dependencies of workspaces!)
+                        if (manager.options.public_hoist_pattern) |public_hoist_pattern| {
+                            if (public_hoist_pattern.isMatch(dep_name)) {
+                                const hoist_entry = try public_hoisted.getOrPut(dep_name);
+                                if (!hoist_entry.found_existing) {
+                                    try entry_dependencies[0].insert(
+                                        lockfile.allocator,
+                                        .{ .entry_id = new_entry_id, .dep_id = new_entry_dep_id },
+                                        &ctx,
+                                    );
+                                }
+                            }
+                        }
+                    }
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (new_entry_dep_id != invalid_dependency_id) {
if (entry.entry_parent_id == .root) {
// make sure direct dependencies are not replaced
const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
try public_hoisted.put(dep_name, {});
} else {
// transitive dependencies (also direct dependencies of workspaces!)
const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
if (manager.options.public_hoist_pattern) |public_hoist_pattern| {
if (public_hoist_pattern.isMatch(dep_name)) {
const hoist_entry = try public_hoisted.getOrPut(dep_name);
if (!hoist_entry.found_existing) {
try entry_dependencies[0].insert(
lockfile.allocator,
.{ .entry_id = new_entry_id, .dep_id = new_entry_dep_id },
&ctx,
);
}
}
}
}
}
if (new_entry_dep_id != invalid_dependency_id) {
const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
if (entry.entry_parent_id == .root) {
// mark direct dependencies as reserved to prevent replacement
_ = try public_hoisted.getOrPut(dep_name);
} else {
// transitive dependencies (also direct dependencies of workspaces!)
if (manager.options.public_hoist_pattern) |public_hoist_pattern| {
if (public_hoist_pattern.isMatch(dep_name)) {
const hoist_entry = try public_hoisted.getOrPut(dep_name);
if (!hoist_entry.found_existing) {
try entry_dependencies[0].insert(
lockfile.allocator,
.{ .entry_id = new_entry_id, .dep_id = new_entry_dep_id },
&ctx,
);
}
}
}
}
}
🤖 Prompt for AI Agents
In src/install/isolated_install.zig around lines 563 to 584, the code currently
computes dep_name twice and does a plain put for root entries which can cause
duplicate key copies and extra allocations; update the block so dep_name is
computed once, then for root entries use getOrPut (or equivalent) instead of put
to avoid inserting duplicate keys and extra allocations, and for non-root
entries keep the public_hoist_pattern match logic but reuse the single dep_name
value and call getOrPut when interacting with public_hoisted; ensure you only
insert into entry_dependencies when getOrPut indicates the key was newly
created.

@dylan-conway
dylan-conway marked this pull request as ready for review October 21, 2025 01:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (4)
src/ini.zig (2)

1076-1089: Do not reset the npmrc log on matcher errors

log.reset() wipes unrelated diagnostics parsed earlier.

Apply:

-        ) catch |err| switch (err) {
-            error.OutOfMemory => |oom| return oom,
-            error.InvalidRegExp, error.UnexpectedExpr => patterns: {
-                log.reset();
-                break :patterns null;
-            },
-        };
+        ) catch |err| switch (err) {
+            error.OutOfMemory => |oom| return oom,
+            error.InvalidRegExp, error.UnexpectedExpr => null,
+        };

1091-1104: Same issue for hoist-pattern block — keep previous diagnostics

Mirror the fix; avoid log.reset().

Use the same diff pattern as above for this block.

src/install/isolated_install.zig (2)

415-419: Add brief intent comments for hoist tracking maps.

Document each map’s role (root-guard vs hidden-hoist determinism) to aid future readers.


563-584: Public hoist: avoid duplicate key copies and skip workspace packages.

  • Use getOrPut for the root reservation to prevent extra allocations.
  • Compute dep_name once.
  • Don’t public‑hoist workspace packages themselves.

Apply this diff:

-                if (new_entry_dep_id != invalid_dependency_id) {
-                    if (entry.entry_parent_id == .root) {
-                        // make sure direct dependencies are not replaced
-                        const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
-                        try public_hoisted.put(dep_name, {});
-                    } else {
-                        // transitive dependencies (also direct dependencies of workspaces!)
-                        const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
-                        if (manager.options.public_hoist_pattern) |public_hoist_pattern| {
-                            if (public_hoist_pattern.isMatch(dep_name)) {
-                                const hoist_entry = try public_hoisted.getOrPut(dep_name);
-                                if (!hoist_entry.found_existing) {
-                                    try entry_dependencies[0].insert(
-                                        lockfile.allocator,
-                                        .{ .entry_id = new_entry_id, .dep_id = new_entry_dep_id },
-                                        &ctx,
-                                    );
-                                }
-                            }
-                        }
-                    }
-                }
+                if (new_entry_dep_id != invalid_dependency_id) {
+                    const dep = dependencies[new_entry_dep_id];
+                    const dep_name = dep.name.slice(string_buf);
+
+                    if (entry.entry_parent_id == .root) {
+                        // mark direct dependencies as reserved to prevent replacement
+                        _ = try public_hoisted.getOrPut(dep_name);
+                    } else {
+                        // transitive dependencies (also direct dependencies of workspaces!)
+                        // never public‑hoist workspace packages themselves
+                        if (dep.version.tag != .workspace) {
+                            if (manager.options.public_hoist_pattern) |public_hoist_pattern| {
+                                if (public_hoist_pattern.isMatch(dep_name)) {
+                                    const hoist_entry = try public_hoisted.getOrPut(dep_name);
+                                    if (!hoist_entry.found_existing) {
+                                        try entry_dependencies[0].insert(
+                                            lockfile.allocator,
+                                            .{ .entry_id = new_entry_id, .dep_id = new_entry_dep_id },
+                                            &ctx,
+                                        );
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2c07e93 and cd5c3c3.

📒 Files selected for processing (10)
  • src/api/schema.zig (2 hunks)
  • src/bun.js/test/jest.zig (1 hunks)
  • src/bun.zig (0 hunks)
  • src/bunfig.zig (1 hunks)
  • src/cli.zig (1 hunks)
  • src/cli/Arguments.zig (1 hunks)
  • src/ini.zig (1 hunks)
  • src/install/PackageManager/PackageManagerOptions.zig (2 hunks)
  • src/install/isolated_install.zig (5 hunks)
  • src/install/isolated_install/Installer.zig (3 hunks)
💤 Files with no reviewable changes (1)
  • src/bun.zig
🧰 Additional context used
📓 Path-based instructions (3)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/bun.js/test/jest.zig
  • src/cli/Arguments.zig
  • src/cli.zig
  • src/install/isolated_install/Installer.zig
  • src/api/schema.zig
  • src/ini.zig
  • src/bunfig.zig
  • src/install/PackageManager/PackageManagerOptions.zig
  • src/install/isolated_install.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/test/jest.zig
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

src/**/*.zig: Use private fields in Zig with the # prefix (e.g., struct { #foo: u32 };)
Prefer decl literals in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 };)
Prefer placing @import statements at the bottom of the Zig file (formatter may reorder automatically)
Prefer @import("bun") rather than @import("root").bun or @import("../bun.zig")

In Zig code, be careful with allocators and use defer for cleanup

Files:

  • src/bun.js/test/jest.zig
  • src/cli/Arguments.zig
  • src/cli.zig
  • src/install/isolated_install/Installer.zig
  • src/api/schema.zig
  • src/ini.zig
  • src/bunfig.zig
  • src/install/PackageManager/PackageManagerOptions.zig
  • src/install/isolated_install.zig
🧠 Learnings (5)
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS<ClassName> and re-export toJS/fromJS/fromJSDirect

Applied to files:

  • src/bun.js/test/jest.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/bindings/generated_classes_list.zig : Update src/bun.js/bindings/generated_classes_list.zig to include new classes

Applied to files:

  • src/bun.js/test/jest.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors

Applied to files:

  • src/bun.js/test/jest.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions

Applied to files:

  • src/bun.js/test/jest.zig
📚 Learning: 2025-08-30T00:11:00.890Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.890Z
Learning: Applies to **/*.zig : Wrap the Bun__<Type>__toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Applied to files:

  • src/bun.js/test/jest.zig
🔇 Additional comments (13)
src/bun.js/test/jest.zig (1)

514-514: LGTM: Symbol rebinding aligns with refactored API surface.

The rebinding of RegularExpression from bun.RegularExpression to jsc.RegularExpression is consistent with the broader refactoring effort to expose RegularExpression via the jsc subsystem (as noted in the AI summary and related changes in src/bun.js/jsc.zig). The change aligns with the existing import pattern in this file where jsc = bun.jsc is defined at line 510.

src/cli/Arguments.zig (1)

1343-1343: LGTM: Rebinding consistent with new public API surface.

The rebinding of RegularExpression to bun.jsc.RegularExpression aligns with the refactored export path where RegularExpression is now publicly exposed via bun.jsc rather than directly from bun. The usage at line 503 correctly handles the updated error contract (error.InvalidRegExp) from the underlying RegularExpression.init implementation.

src/cli.zig (1)

1736-1736: LGTM: Symbol rebinding completes the coordinated refactoring.

The rebinding of RegularExpression to bun.jsc.RegularExpression is consistent with the parallel changes in src/bun.js/test/jest.zig and src/cli/Arguments.zig, completing the coordinated refactoring to centralize RegularExpression exports through the jsc subsystem path.

src/api/schema.zig (2)

3065-3067: Expose PNPM-style hoist matchers on BunInstall — looks good

Fields are nullable and typed to install.PnpmMatcher, matching usage elsewhere. If BunInstall ever crosses a serialization boundary, add encode/decode; otherwise fine as-is.


3230-3230: Import alias placement/style LGTM

Local alias const install = bun.install matches the repo conventions at file end.

src/install/PackageManager/PackageManagerOptions.zig (2)

74-76: Options fields added for hoist matchers — OK

Typed to bun.install.PnpmMatcher and default nulls make sense.

Please confirm where the default hoistPattern = "*" is applied when this remains null (installer side vs. parser), so behavior matches the PR spec.


393-400: Wiring from bun_install_config → Options is correct

Assignment is straightforward.

Double-check PnpmMatcher does not hold pointers to a short‑lived allocator from parsing; its allocations should outlive Options.

src/bunfig.zig (2)

735-746: Parsing publicHoistPattern via PnpmMatcher — solid

Accepts string/array; maps UnexpectedExpr/InvalidRegExp to “Invalid Bunfig”, which is reasonable.

ini.zig handles invalid patterns permissively (sets null); bunfig is strict (errors). Confirm this intentional divergence in UX.


747-757: Parsing hoistPattern — solid; mirrors publicHoistPattern

Same note on strict vs. permissive behavior relative to .npmrc handling.

src/install/isolated_install/Installer.zig (1)

445-446: Using entries.items(.hoisted) to gate behavior — LGTM

Read-only access matches surrounding patterns; non‑hoisted path remains unchanged.

src/install/isolated_install.zig (3)

539-539: Wiring hoisted flag into Store.Entry is correct.


639-639: LGTM: hoisted field plumbed for runtime use.


941-943: No action needed—linking to hidden node_modules already occurs on both paths.

The code correctly calls installer.linkToHiddenNodeModules(entry_id) for hoisted entries in both scenarios:

  1. Cache hit (skip path): Lines 941–943 in isolated_install.zig — when entry_hoisted[entry_id.get()] is true
  2. Fresh install (extract path): Line 896 in Installer.zig within the .symlink_dependency_binaries step — same condition check and logic

The .symlink_dependency_binaries step executes as part of the task callback flow after onPackageExtracted calls startTask, ensuring hoisted npm/git/tarball packages are linked during extraction, not only on cache hits.

Comment on lines +514 to +532
const hoisted = hoisted: {
if (new_entry_dep_id == invalid_dependency_id) {
break :hoisted false;
}

const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);

const hoist_pattern = manager.options.hoist_pattern orelse {
const hoist_entry = try hidden_hoisted.getOrPut(dep_name);
break :hoisted !hoist_entry.found_existing;
};

if (hoist_pattern.isMatch(dep_name)) {
const hoist_entry = try hidden_hoisted.getOrPut(dep_name);
break :hoisted !hoist_entry.found_existing;
}

break :hoisted false;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not hoist workspace packages into .bun/node_modules.

Hoisting a workspace package into the hidden tree can create cross-workspace leakage. Guard against .workspace here.

Apply this diff:

-            const hoisted = hoisted: {
-                if (new_entry_dep_id == invalid_dependency_id) {
-                    break :hoisted false;
-                }
-
-                const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
+            const hoisted = hoisted: {
+                if (new_entry_dep_id == invalid_dependency_id) break :hoisted false;
+
+                const dep = dependencies[new_entry_dep_id];
+                // Do not hoist workspace packages into .bun/node_modules
+                if (dep.version.tag == .workspace) break :hoisted false;
+
+                const dep_name = dep.name.slice(string_buf);

                 const hoist_pattern = manager.options.hoist_pattern orelse {
                     const hoist_entry = try hidden_hoisted.getOrPut(dep_name);
                     break :hoisted !hoist_entry.found_existing;
                 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const hoisted = hoisted: {
if (new_entry_dep_id == invalid_dependency_id) {
break :hoisted false;
}
const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
const hoist_pattern = manager.options.hoist_pattern orelse {
const hoist_entry = try hidden_hoisted.getOrPut(dep_name);
break :hoisted !hoist_entry.found_existing;
};
if (hoist_pattern.isMatch(dep_name)) {
const hoist_entry = try hidden_hoisted.getOrPut(dep_name);
break :hoisted !hoist_entry.found_existing;
}
break :hoisted false;
};
const hoisted = hoisted: {
if (new_entry_dep_id == invalid_dependency_id) break :hoisted false;
const dep = dependencies[new_entry_dep_id];
// Do not hoist workspace packages into .bun/node_modules
if (dep.version.tag == .workspace) break :hoisted false;
const dep_name = dep.name.slice(string_buf);
const hoist_pattern = manager.options.hoist_pattern orelse {
const hoist_entry = try hidden_hoisted.getOrPut(dep_name);
break :hoisted !hoist_entry.found_existing;
};
if (hoist_pattern.isMatch(dep_name)) {
const hoist_entry = try hidden_hoisted.getOrPut(dep_name);
break :hoisted !hoist_entry.found_existing;
}
break :hoisted false;
};
🤖 Prompt for AI Agents
In src/install/isolated_install.zig around lines 514 to 532, the hoisting logic
currently allows workspace packages to be hoisted into the hidden tree; add a
guard that prevents hoisting when the dependency is a workspace package by
checking for the workspace marker (e.g. dep_name endsWith ".workspace" or
equivalent flag on the dependency) and immediately break :hoisted false in that
case. Keep the existing hidden_hoisted lookups and existing hoist_pattern logic
unchanged; simply insert the workspace check before any getOrPut/use of
hidden_hoisted so workspace packages are never hoisted into .bun/node_modules.

Comment on lines +893 to 897
if (!entry_hoisted[this.entry_id.get()]) {
continue :next_step this.nextStep(current_step);
}

target.appendFmt("{}/node_modules/{s}", .{
Store.Entry.fmtStorePath(this.entry_id, installer.store, installer.lockfile),
pkg_name.slice(string_buf),
});

var full_target: bun.AbsPath(.{ .sep = .auto }) = .initTopLevelDir();
defer full_target.deinit();

installer.appendStorePath(&full_target, this.entry_id);

const symlinker: Symlinker = .{
.dest = hidden_hoisted_node_modules,
.target = target,
.fallback_junction_target = full_target,
};
_ = symlinker.ensureSymlink(.ignore_failure);
installer.linkToHiddenNodeModules(this.entry_id);
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Propagate hidden node_modules symlink errors

Currently ignores ensureSymlink failures. Bubble them up to surface broken installs.

Apply:

-                            installer.linkToHiddenNodeModules(this.entry_id);
+                            installer.linkToHiddenNodeModules(this.entry_id) catch |err| {
+                                return .failure(.{ .symlink_dependencies = err });
+                            };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!entry_hoisted[this.entry_id.get()]) {
continue :next_step this.nextStep(current_step);
}
target.appendFmt("{}/node_modules/{s}", .{
Store.Entry.fmtStorePath(this.entry_id, installer.store, installer.lockfile),
pkg_name.slice(string_buf),
});
var full_target: bun.AbsPath(.{ .sep = .auto }) = .initTopLevelDir();
defer full_target.deinit();
installer.appendStorePath(&full_target, this.entry_id);
const symlinker: Symlinker = .{
.dest = hidden_hoisted_node_modules,
.target = target,
.fallback_junction_target = full_target,
};
_ = symlinker.ensureSymlink(.ignore_failure);
installer.linkToHiddenNodeModules(this.entry_id);
},
if (!entry_hoisted[this.entry_id.get()]) {
continue :next_step this.nextStep(current_step);
}
installer.linkToHiddenNodeModules(this.entry_id) catch |err| {
return .failure(.{ .symlink_dependencies = err });
};
},
🤖 Prompt for AI Agents
In src/install/isolated_install/Installer.zig around lines 893 to 897, the call
to installer.linkToHiddenNodeModules currently ignores failures from
ensureSymlink; change the call to propagate errors instead of swallowing them —
use Zig's error propagation (e.g. replace the noop call with a try or
return-on-error pattern) so any symlink/ensureSymlink errors bubble up to the
caller and cause the install to fail rather than be silently ignored.

@dylan-conway
dylan-conway requested a review from nektro October 21, 2025 01:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/install/isolated_install/Installer.zig (1)

941-945: Also propagate errors in fast‑path (no install) hoist linking.

On the scheduling path, failures should surface via onTaskFail to avoid silently broken installs.

-                        if (entry_hoisted[entry_id.get()]) {
-                            installer.linkToHiddenNodeModules(entry_id);
-                        }
+                        if (entry_hoisted[entry_id.get()]) {
+                            installer.linkToHiddenNodeModules(entry_id) catch |err| {
+                                // mark failure and skip this entry
+                                entry_steps[entry_id.get()].store(.done, .monotonic);
+                                installer.onTaskFail(entry_id, .{ .symlink_dependencies = err });
+                                continue;
+                            };
+                        }
♻️ Duplicate comments (5)
src/install/isolated_install/Installer.zig (2)

894-898: Propagate hidden node_modules symlink errors (don’t ignore).

Currently ignores failures. Bubble up as symlink_dependencies to fail the task deterministically.

Apply:

-                            if (!entry_hoisted[this.entry_id.get()]) {
-                                continue :next_step this.nextStep(current_step);
-                            }
-                            installer.linkToHiddenNodeModules(this.entry_id);
+                            if (!entry_hoisted[this.entry_id.get()]) {
+                                continue :next_step this.nextStep(current_step);
+                            }
+                            installer.linkToHiddenNodeModules(this.entry_id) catch |err| {
+                                return .failure(.{ .symlink_dependencies = err });
+                            };

1198-1244: Make symlink creation robust: anchor paths, compute relative properly, return errors.

  • Build dest from top‑level (initTopLevelDir) to avoid cwd surprises.
  • Derive target via dest.relative(&full_target) instead of manual ...
  • Return !void and propagate ensureSymlink errors; update call sites.
-    pub fn linkToHiddenNodeModules(this: *const Installer, entry_id: Store.Entry.Id) void {
+    pub fn linkToHiddenNodeModules(this: *const Installer, entry_id: Store.Entry.Id) !void {
         const string_buf = this.lockfile.buffers.string_bytes.items;
-
-        const node_id = this.store.entries.items(.node_id)[entry_id.get()];
-        const pkg_id = this.store.nodes.items(.pkg_id)[node_id.get()];
-        const pkg_name = this.lockfile.packages.items(.name)[pkg_id];
-
-        var hidden_hoisted_node_modules: bun.Path(.{ .sep = .auto }) = .init();
-        defer hidden_hoisted_node_modules.deinit();
-
-        hidden_hoisted_node_modules.append(
-            "node_modules" ++ std.fs.path.sep_str ++ ".bun" ++ std.fs.path.sep_str ++ "node_modules",
-        );
-        hidden_hoisted_node_modules.append(pkg_name.slice(string_buf));
-
-        var target: bun.RelPath(.{ .sep = .auto }) = .init();
-        defer target.deinit();
-
-        target.append("..");
-        if (strings.containsChar(pkg_name.slice(string_buf), '/')) {
-            target.append("..");
-        }
-        target.appendFmt("{}/node_modules/{s}", .{
-            Store.Entry.fmtStorePath(entry_id, this.store, this.lockfile),
-            pkg_name.slice(string_buf),
-        });
-
-        var full_target: bun.AbsPath(.{ .sep = .auto }) = .initTopLevelDir();
-        defer full_target.deinit();
-
-        this.appendStorePath(&full_target, entry_id);
-
-        const symlinker: Symlinker = .{
-            .dest = hidden_hoisted_node_modules,
-            .target = target,
-            .fallback_junction_target = full_target,
-        };
-
-        // symlinks won't exist if node_modules/.bun is new
-        const link_strategy: Symlinker.Strategy = if (this.is_new_bun_modules)
-            .expect_missing
-        else
-            .expect_existing;
-
-        _ = symlinker.ensureSymlink(link_strategy);
+        const node_id = this.store.entries.items(.node_id)[entry_id.get()];
+        const pkg_id = this.store.nodes.items(.pkg_id)[node_id.get()];
+        const pkg_name = this.lockfile.packages.items(.name)[pkg_id];
+
+        // Dest: <repo>/node_modules/.bun/node_modules/<pkg_name>
+        var dest: bun.Path(.{ .sep = .auto }) = .initTopLevelDir();
+        defer dest.deinit();
+        dest.append("node_modules");
+        dest.append(".bun");
+        dest.append("node_modules");
+        dest.append(pkg_name.slice(string_buf));
+
+        // Target absolute: <repo>/node_modules/.bun/{storePath}/node_modules/<pkg_name>
+        var full_target: bun.AbsPath(.{ .sep = .auto }) = .initTopLevelDir();
+        defer full_target.deinit();
+        this.appendStorePath(&full_target, entry_id);
+
+        // Compute relative target from dest parent to full_target
+        const save = dest.save();
+        defer save.restore();
+        dest.undo(1); // parent of <pkg_name>
+        const target = dest.relative(&full_target);
+        defer target.deinit();
+
+        const symlinker: Symlinker = .{
+            .dest = save.path.*, // restored
+            .target = target,
+            .fallback_junction_target = full_target,
+        };
+
+        const link_strategy: Symlinker.Strategy = if (this.is_new_bun_modules)
+            .expect_missing
+        else
+            .expect_existing;
+
+        switch (symlinker.ensureSymlink(link_strategy)) {
+            .result => {},
+            .err => |err| return err,
+        }
     }
src/install/isolated_install.zig (3)

415-420: Add brief comments documenting map roles.

Improves readability in a complex flow.

-        var public_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
+        // Names reserved by root and already public‑hoisted (prevents replacement).
+        var public_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
         defer public_hoisted.deinit();
 
-        var hidden_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
+        // Names already hoisted to node_modules/.bun/node_modules (first‑writer wins).
+        var hidden_hoisted: bun.StringArrayHashMap(void) = .init(manager.allocator);
         defer hidden_hoisted.deinit();

514-532: Do not let workspace deps claim hidden hoist slots.

Workspace entries marked hoisted will occupy hidden_hoisted but never link (linking is gated to npm/git/tarball), causing real transitive matches to be skipped. Guard out workspaces here.

-            const hoisted = hoisted: {
-                if (new_entry_dep_id == invalid_dependency_id) {
+            const hoisted = hoisted: {
+                if (new_entry_dep_id == invalid_dependency_id) {
                     break :hoisted false;
                 }
 
-                const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
+                if (new_entry_is_workspace) {
+                    // never hoist workspaces into hidden tree
+                    break :hoisted false;
+                }
+                const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);

563-584: Avoid double key copies; compute name once; use getOrPut for root.

Saves allocs and keeps logic consistent across branches.

-                if (new_entry_dep_id != invalid_dependency_id) {
-                    if (entry.entry_parent_id == .root) {
-                        // make sure direct dependencies are not replaced
-                        const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
-                        try public_hoisted.put(dep_name, {});
-                    } else {
-                        // transitive dependencies (also direct dependencies of workspaces!)
-                        const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
-                        if (manager.options.public_hoist_pattern) |public_hoist_pattern| {
-                            if (public_hoist_pattern.isMatch(dep_name)) {
-                                const hoist_entry = try public_hoisted.getOrPut(dep_name);
-                                if (!hoist_entry.found_existing) {
-                                    try entry_dependencies[0].insert(
-                                        lockfile.allocator,
-                                        .{ .entry_id = new_entry_id, .dep_id = new_entry_dep_id },
-                                        &ctx,
-                                    );
-                                }
-                            }
-                        }
-                    }
-                }
+                if (new_entry_dep_id != invalid_dependency_id) {
+                    const dep_name = dependencies[new_entry_dep_id].name.slice(string_buf);
+                    if (entry.entry_parent_id == .root) {
+                        // reserve root names to prevent replacement
+                        _ = try public_hoisted.getOrPut(dep_name);
+                    } else if (manager.options.public_hoist_pattern) |public_hoist_pattern| {
+                        if (public_hoist_pattern.isMatch(dep_name)) {
+                            const hoist_entry = try public_hoisted.getOrPut(dep_name);
+                            if (!hoist_entry.found_existing) {
+                                try entry_dependencies[0].insert(
+                                    lockfile.allocator,
+                                    .{ .entry_id = new_entry_id, .dep_id = new_entry_dep_id },
+                                    &ctx,
+                                );
+                            }
+                        }
+                    }
+                }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4d1efee and d2a844a.

📒 Files selected for processing (2)
  • src/install/isolated_install.zig (7 hunks)
  • src/install/isolated_install/Installer.zig (4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/install/isolated_install/Installer.zig
  • src/install/isolated_install.zig
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

src/**/*.zig: Use private fields in Zig with the # prefix (e.g., struct { #foo: u32 };)
Prefer decl literals in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 };)
Prefer placing @import statements at the bottom of the Zig file (formatter may reorder automatically)
Prefer @import("bun") rather than @import("root").bun or @import("../bun.zig")

In Zig code, be careful with allocators and use defer for cleanup

Files:

  • src/install/isolated_install/Installer.zig
  • src/install/isolated_install.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (6)
src/install/isolated_install/Installer.zig (2)

10-10: LGTM: field plumbed for state.

is_new_bun_modules is a useful flag to drive symlink strategy.


446-446: LGTM: hoist state read.

entry_hoisted read enables conditional hidden linking.

src/install/isolated_install.zig (4)

539-540: LGTM: hoist flag stored.

Persists decision on Store.Entry.


612-767: Windows path: confirm intent re dot‑entries; parity with POSIX .cache handling.

Windows branch skips entries starting with '.' (leaves .bin, .cache, etc. in root), while POSIX moves all then specifically restores .cache. Confirm this divergence is intentional; if not, align behavior (e.g., move all then restore .cache).


795-796: LGTM: hoist state exposed to scheduler.

Enables skip‑install fast path to link hidden hoists.


827-828: LGTM: wire is_new_bun_modules into installer.

Drives correct symlink strategy.

Comment on lines 941 to 945
if (!needs_install) {
if (entry_hoisted[entry_id.get()]) {
installer.linkToHiddenNodeModules(entry_id);
}
// .monotonic is okay because the task isn't running on another thread.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Propagate errors when linking existing hoists (no‑install path).

Mirror task‑thread behavior to avoid silent failures.

-                        if (entry_hoisted[entry_id.get()]) {
-                            installer.linkToHiddenNodeModules(entry_id);
-                        }
+                        if (entry_hoisted[entry_id.get()]) {
+                            installer.linkToHiddenNodeModules(entry_id) catch |err| {
+                                entry_steps[entry_id.get()].store(.done, .monotonic);
+                                installer.onTaskFail(entry_id, .{ .symlink_dependencies = err });
+                                continue;
+                            };
+                        }
🤖 Prompt for AI Agents
In src/install/isolated_install.zig around lines 941-945, the no-install path
calls installer.linkToHiddenNodeModules(entry_id) but ignores any returned
error; mirror the task-thread behavior by propagating or handling that error
instead of silently ignoring it. Replace the bare call with the same error
propagation used in the task thread (e.g., use try
installer.linkToHiddenNodeModules(entry_id) or capture and return/log the error
consistently) so failures linking existing hoists are surfaced to the caller.

// Windows:
// 1. create 'node_modules/.old_modules-{hex}'
// 2. for each entry in 'node_modules' rename into 'node_modules/.old_modules-{hex}'
// 3. for each workspace 'node_modules' rename into 'node_modules/.old_modules-{hex}/old_{basename}_modules'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it impossible for there to be conflicting basenames?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes they could collide. will fix

Comment thread src/string/escapeRegExp.zig Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/string/escapeRegExp.zig (1)

3-72: Address past review comments.

Previous review comments on this code segment remain unaddressed:

  1. Code duplication: The two functions are nearly identical except for '*' handling. Consider refactoring with a comptime parameter as suggested in the past review.

  2. Inconsistent debug/release behavior (lines 25-30, 61-66): The else branch is unreachable in debug but falls back to writeByte(c) in release, creating different behavior across build types. This should be made consistent.

  3. Missing documentation: Public functions lack doc comments explaining parameters, special character handling, and purpose.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d2a844a and 9281437.

📒 Files selected for processing (4)
  • src/js/internal-for-testing.ts (1 hunks)
  • src/string.zig (1 hunks)
  • src/string/escapeRegExp.zig (1 hunks)
  • test/js/bun/util/escapeRegExp.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (10)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/string/escapeRegExp.zig
  • src/string.zig
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

src/**/*.zig: Use private fields in Zig with the # prefix (e.g., struct { #foo: u32 };)
Prefer decl literals in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 };)
Prefer placing @import statements at the bottom of the Zig file (formatter may reorder automatically)
Prefer @import("bun") rather than @import("root").bun or @import("../bun.zig")

In Zig code, be careful with allocators and use defer for cleanup

Files:

  • src/string/escapeRegExp.zig
  • src/string.zig
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/js/bun/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun:test for files ending with *.test.{ts,js,jsx,tsx,mjs,cjs}
Prefer concurrent tests (test.concurrent/describe.concurrent) over sequential when feasible
Organize tests with describe blocks to group related tests
Use utilities like describe.each, toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach) and track resources for cleanup

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

For large/repetitive strings, use Buffer.alloc(count, fill).toString() instead of "A".repeat(count)

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/js/{bun,node}/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests by module under /test/js/bun/ and /test/js/node/

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must be created under test/ and end with .test.ts or .test.tsx
In tests that open network ports, always use port: 0; do not hardcode port numbers or roll your own random port
Use normalizeBunSnapshot to normalize snapshot output instead of comparing raw stdout
Do not write tests that merely assert absence of strings like "panic" or "uncaught exception"
Use tempDir from "harness" to create temporary directories; do not use fs.mkdtempSync or tmpdirSync
When spawning processes in tests, assert output before asserting the exit code
Avoid shell commands in tests (e.g., find, grep); prefer Bun's Glob and built-in tools
Prefer snapshot tests over direct string equality assertions on stdout

Files:

  • test/js/bun/util/escapeRegExp.test.ts
🧠 Learnings (5)
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Add tests for new functionality

Applied to files:

  • src/string/escapeRegExp.zig
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/js/bun/**/*.{js,ts} : Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Applied to files:

  • test/js/bun/util/escapeRegExp.test.ts
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending with `*.test.{ts,js,jsx,tsx,mjs,cjs}`

Applied to files:

  • test/js/bun/util/escapeRegExp.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/bun/util/escapeRegExp.test.ts
📚 Learning: 2025-10-19T02:44:46.330Z
Learnt from: theshadow27
PR: oven-sh/bun#23798
File: packages/bun-otel/context-propagation.test.ts:1-1
Timestamp: 2025-10-19T02:44:46.330Z
Learning: In the Bun repository, standalone packages under packages/ (e.g., bun-vscode, bun-inspector-protocol, bun-plugin-yaml, bun-plugin-svelte, bun-debug-adapter-protocol, bun-otel) co-locate their tests with package source code using *.test.ts files. This follows standard npm/monorepo patterns. The test/ directory hierarchy (test/js/bun/, test/cli/, test/js/node/) is reserved for testing Bun's core runtime APIs and built-in functionality, not standalone packages.

Applied to files:

  • test/js/bun/util/escapeRegExp.test.ts
🧬 Code graph analysis (1)
test/js/bun/util/escapeRegExp.test.ts (1)
src/js/internal-for-testing.ts (2)
  • escapeRegExp (28-28)
  • escapeRegExpForPackageNameMatching (29-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (4)
src/string.zig (1)

1163-1165: LGTM: Safe guard for WTF-allocated strings.

The guard prevents deinitializing UTF-8 slices that are allocated by WTF, which have a different lifecycle and are managed externally. This prevents potential double-free issues.

src/string/escapeRegExp.zig (1)

74-118: JS interop wrappers look correct.

Both jsEscapeRegExp and jsEscapeRegExpForPackageNameMatching follow the proper pattern: validate input, convert to Bun string with cleanup, delegate to the escape function, and return a JS value.

src/js/internal-for-testing.ts (1)

28-33: LGTM: Test bindings correctly expose escapeRegExp utilities.

The bindings properly wire up the Zig functions with correct arity and naming conventions.

test/js/bun/util/escapeRegExp.test.ts (1)

5-8: LGTM: Basic escapeRegExp test coverage.

The test correctly validates escaping of special regex characters and the hyphen-to-\x2d transformation.

Comment thread test/js/bun/util/escapeRegExp.test.ts Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9281437 and e8289e7.

📒 Files selected for processing (1)
  • test/js/bun/util/escapeRegExp.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/js/bun/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun:test for files ending with *.test.{ts,js,jsx,tsx,mjs,cjs}
Prefer concurrent tests (test.concurrent/describe.concurrent) over sequential when feasible
Organize tests with describe blocks to group related tests
Use utilities like describe.each, toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach) and track resources for cleanup

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

For large/repetitive strings, use Buffer.alloc(count, fill).toString() instead of "A".repeat(count)

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/js/{bun,node}/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests by module under /test/js/bun/ and /test/js/node/

Files:

  • test/js/bun/util/escapeRegExp.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must be created under test/ and end with .test.ts or .test.tsx
In tests that open network ports, always use port: 0; do not hardcode port numbers or roll your own random port
Use normalizeBunSnapshot to normalize snapshot output instead of comparing raw stdout
Do not write tests that merely assert absence of strings like "panic" or "uncaught exception"
Use tempDir from "harness" to create temporary directories; do not use fs.mkdtempSync or tmpdirSync
When spawning processes in tests, assert output before asserting the exit code
Avoid shell commands in tests (e.g., find, grep); prefer Bun's Glob and built-in tools
Prefer snapshot tests over direct string equality assertions on stdout

Files:

  • test/js/bun/util/escapeRegExp.test.ts
🧠 Learnings (4)
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/js/bun/**/*.{js,ts} : Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Applied to files:

  • test/js/bun/util/escapeRegExp.test.ts
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending with `*.test.{ts,js,jsx,tsx,mjs,cjs}`

Applied to files:

  • test/js/bun/util/escapeRegExp.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/bun/util/escapeRegExp.test.ts
📚 Learning: 2025-10-19T02:44:46.330Z
Learnt from: theshadow27
PR: oven-sh/bun#23798
File: packages/bun-otel/context-propagation.test.ts:1-1
Timestamp: 2025-10-19T02:44:46.330Z
Learning: In the Bun repository, standalone packages under packages/ (e.g., bun-vscode, bun-inspector-protocol, bun-plugin-yaml, bun-plugin-svelte, bun-debug-adapter-protocol, bun-otel) co-locate their tests with package source code using *.test.ts files. This follows standard npm/monorepo patterns. The test/ directory hierarchy (test/js/bun/, test/cli/, test/js/node/) is reserved for testing Bun's core runtime APIs and built-in functionality, not standalone packages.

Applied to files:

  • test/js/bun/util/escapeRegExp.test.ts
🧬 Code graph analysis (1)
test/js/bun/util/escapeRegExp.test.ts (1)
src/js/internal-for-testing.ts (2)
  • escapeRegExp (28-28)
  • escapeRegExpForPackageNameMatching (29-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (2)
test/js/bun/util/escapeRegExp.test.ts (2)

1-3: LGTM!

The imports are correct and follow the coding guidelines for Bun tests.


12-15: LGTM!

The test correctly verifies that * is treated as a wildcard pattern (.*) in package name matching, distinguishing it from the standard escapeRegExp behavior. The function calls use the correct single-argument signature.

Comment on lines +5 to +8
test("escapeRegExp", () => {
expect(escapeRegExp("\\ ^ $ * + ? . ( ) | { } [ ]")).toBe("\\\\ \\^ \\$ \\* \\+ \\? \\. \\( \\) \\| \\{ \\} \\[ \\]");
expect(escapeRegExp("foo - bar")).toBe("foo \\x2d bar");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding edge case tests.

The test covers basic functionality well. Consider adding test cases for edge scenarios to improve robustness:

  • Empty string: escapeRegExp("")
  • No special characters: escapeRegExp("foobar")
  • Multiple consecutive special characters: escapeRegExp("***")
🤖 Prompt for AI Agents
In test/js/bun/util/escapeRegExp.test.ts around lines 5 to 8, add edge-case
assertions to cover empty input, no-special-characters, and
repeated-special-characters: assert that escapeRegExp("") returns an empty
string, escapeRegExp("foobar") returns "foobar", and escapeRegExp("***") returns
each asterisk escaped (i.e., "\*\*\*"). Add these three expect checks alongside
the existing ones to improve coverage.

expect(escapeRegExp("foo - bar")).toBe("foo \\x2d bar");
});

test("escapeRegExpForPackageName", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix test name to match function name.

The test name is "escapeRegExpForPackageName" but the function being tested is escapeRegExpForPackageNameMatching. The test name should match the function name for clarity and searchability.

Apply this diff:

-test("escapeRegExpForPackageName", () => {
+test("escapeRegExpForPackageNameMatching", () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("escapeRegExpForPackageName", () => {
test("escapeRegExpForPackageNameMatching", () => {
🤖 Prompt for AI Agents
In test/js/bun/util/escapeRegExp.test.ts around line 10, the test title string
is "escapeRegExpForPackageName" but the tested function is
escapeRegExpForPackageNameMatching; update the test name to exactly match the
function name (escapeRegExpForPackageNameMatching) so the title reflects the
actual function under test and improves clarity/searchability.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e8289e7 and b7e82e5.

📒 Files selected for processing (1)
  • src/string/escapeRegExp.zig (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/string/escapeRegExp.zig
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

src/**/*.zig: Use private fields in Zig with the # prefix (e.g., struct { #foo: u32 };)
Prefer decl literals in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 };)
Prefer placing @import statements at the bottom of the Zig file (formatter may reorder automatically)
Prefer @import("bun") rather than @import("root").bun or @import("../bun.zig")

In Zig code, be careful with allocators and use defer for cleanup

Files:

  • src/string/escapeRegExp.zig
🧠 Learnings (1)
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Add tests for new functionality

Applied to files:

  • src/string/escapeRegExp.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (3)
src/string/escapeRegExp.zig (3)

74-92: JS interop implementation follows best practices.

The function properly validates string input, handles errors, and uses defer for resource cleanup (lines 82, 85). Memory management follows the coding guidelines for careful allocator usage in Zig.


94-112: JS interop implementation is consistent and correct.

This function mirrors the quality of jsEscapeRegExp with proper input validation, error handling, and resource management via defer.


114-122: Import placement follows coding guidelines.

The imports are correctly placed at the bottom of the file and use @import("bun") rather than alternative forms, as specified in the coding guidelines.

Based on coding guidelines.

@@ -0,0 +1,122 @@
const special_characters = "|\\{}()[]^$+*?.-";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add a comment documenting the special characters constant.

The special_characters constant would benefit from a brief comment explaining that these are regex metacharacters that require escaping. This complements the documentation requests for the public functions.

Apply this diff:

+// Regex metacharacters that must be escaped to match literally
 const special_characters = "|\\{}()[]^$+*?.-";
🤖 Prompt for AI Agents
In src/string/escapeRegExp.zig around line 1, the constant special_characters
currently has no documentation; add a brief single-line comment above it stating
that this string lists regex metacharacters that must be escaped when building a
safe regex (e.g., characters with special meaning like | \ { } ( ) [ ] ^ $ + * ?
. -), and note that the list should be updated if the set of characters to
escape changes; use a simple // comment so it complements the public function
docs.

@dylan-conway
dylan-conway merged commit 150338f into main Oct 21, 2025
59 of 62 checks passed
@dylan-conway
dylan-conway deleted the dylan/public-hoist-pattern branch October 21, 2025 21:18
@coderabbitai coderabbitai Bot mentioned this pull request Nov 12, 2025
4 tasks done
Jarred-Sumner pushed a commit that referenced this pull request Aug 21, 2026
…t initialize JSC (#39896)

### Problem
- With `[install] hoistPattern` or `publicHoistPattern` in the project's
`bunfig.toml`, `bun -p '1 + 1'` prints `undefined`. Every `--print`
value is lost. `bun test --isolate` there keeps the default FTL
threshold (64000, not 1000000).
- Cause: `create_matcher` (`src/install_types/NodeLinker.rs`) compiled
each pattern while the config loaded. `__bun_regex_compile`
(`src/jsc/RegularExpression.rs`) calls `jsc::initialize` first, without
`eval_mode` or `short_lived_globals`. `JSCInitialize` applies its
options once per process, so the command's own call later
(`run_command.rs:941`, `TestCommand::exec`) did nothing.

### Fix
- `RegularExpression` in `NodeLinker.rs` stores the escaped pattern and
compiles it in `matches`, through a `OnceCell`. `create_matcher` no
longer touches JSC. The other hunks remove the error type only the eager
compile could produce.
- Correct because only the install linker calls `is_match`
(`build_store`, `prune.rs`). A command that runs JS has initialized JSC
with its own options before then. `bun install` never does otherwise, so
`__bun_regex_compile` still initializes JSC, now with the defaults
(Notes on #39880).
- Two other visible changes. Commands that never run JS (`bun run
<script>`, a `bun install` that does not reach the isolated linker) no
longer initialize JSC when a hoist pattern is configured. And since the
escaping makes every metacharacter literal, Yarr rejects a pattern only
above its 1 MiB size limit: such a pattern now never matches instead of
failing the config load.
- Verified: `test/cli/run/run-eval.test.ts` (8 new cases) and
`test/cli/test/isolation.test.ts` (1 new case) fail on bun 1.4.0 and
pass here. The install pattern suites pass too (Notes).

### Background
- `PnpmMatcher` is the include/exclude list behind the bunfig and
`.npmrc` hoist patterns. Each entry is a Yarr regex. Run and test
commands parse `[install]` too, so every command builds it.
- `jsc::initialize` hands per-command options to `JSCInitialize`, which
applies them inside `std::call_once`. Later calls are no-ops.
- `JSC::Options::evalMode` is a Bun addition to JSC. With it set, the
bytecode generator keeps the completion value of module and function
bodies. `bun -p` prints that value.

<details><summary>Notes</summary>

Layering: `bun_install_types` sits below `bun_jsc`, so it reaches Yarr
through the link-time `__bun_regex_*` functions defined in
`src/jsc/RegularExpression.rs`. That is why the JSC initialization lives
in `__bun_regex_compile` and not in the matcher.

Probes with bun 1.4.0 in a directory whose `bunfig.toml` has
`hoistPattern`:

```
$ bun -p 'Math.max(1, 9)'
undefined
$ bun -p 5
undefined
$ BUN_JSC_dumpOptions=2 bun -p 5 2>&1 | grep evalMode
   evalMode=false                      # evalMode=true without the bunfig
$ BUN_JSC_dumpOptions=2 bun test --isolate 2>&1 | grep thresholdForFTLOptimizeAfterWarmUp
   thresholdForFTLOptimizeAfterWarmUp=64000   # 1000000 without the bunfig
```

With this change both print `9` / `5`, `evalMode=true`, and the
threshold is `1000000`. `.npmrc` `hoist-pattern` did not show the
symptom because only install commands read `.npmrc`, but it builds the
same matcher and gets the same lazy compile. The same goes for the
global `~/.bunfig.toml`: `Tag::read_global_config`
(`src/options_types/command_tag.rs`) is true only for the install
family, so a hoist pattern there never affected `bun -p` or `bun test`
(checked: with the pattern only in `$XDG_CONFIG_HOME/.bunfig.toml`,
1.4.0 prints `2` for `bun -p '1 + 1'`).

The side effect named in Fix, checked with `BUN_JSC_dumpOptions=1`,
which prints a header when JSC initializes: `bun install` in a package
with no dependencies and a hoist pattern in `.bunfig.toml` prints the
header on 1.4.0 and nothing with this change. A self-review pass over
the diff found nothing else to change in the code. It pointed out the
two description errors fixed above, and suggested two follow-ups that
this PR does not need: a JSC-free matcher for these `*` patterns, and a
debug check in `jsc::initialize` for a later call that asks for options
the first call did not set.

`bun -p 5` is affected too, not only calls: with `evalMode` off, JSC's
`BytecodeGenerator` sets `m_defaultAllowCallIgnoreResultOptimization`
for module and function code, and `SourceElements::emitBytecode` then
never materializes a completion value.

The bug dates from the feature (#23567): the Zig `PnpmMatcher.fromExpr`
called `bun.jsc.initialize(false)` eagerly. It is not a regression from
the Rust port.

The removed error path: a pattern made of every regex metacharacter,
`\`, `/`, newlines, and non-ASCII loads fine on 1.4.0, because
`escape_reg_exp_for_package_name_matching` escapes all of them (`*`
becomes `.*`). A 1.1 million character pattern is the one input that
produced `error: Invalid regex` on 1.4.0 (and that error fired for `bun
-p` too, which never uses the pattern). It now loads, and in `bun
install` it never matches.

Related: #34869 moves the `bun test -t` regex compile after
`jsc::initialize` in `test_command.rs`, a different pre-init compile,
and does not overlap with this PR. #39880 (merged, synced into this
branch in 2bcef5c) made `jsc::initialize` take `InitializeOptions` and
had `__bun_regex_compile` pass `one_shot` from argv because the compile
ran during config load. With the compile moved to the first match, that
call is the first one only inside the install commands, where the scan
returns false, so it passes the default like the other subcommand
callers in #39880. Checked on the merged debug build in a hoistPattern
directory: `bun -e` keeps `useConcurrentJIT=false`, `bun -p` gets
`evalMode=true`, `bun test --isolate` gets the 1000000 threshold, and
`test/bundler/compile-argv.test.ts` passes.

`is_match` runs on the main thread only (`build_store` is sequential),
and the matcher types were already `!Send + !Sync` through the raw Yarr
pointer, so the `OnceCell` changes no thread contract.

Suites run with the debug build: `test/cli/run/run-eval.test.ts` (45
pass), `test/cli/test/isolation.test.ts` (28 pass),
`test/cli/install/public-hoist-pattern.test.ts` (14 pass, bunfig and
`.npmrc` patterns), `test/cli/install/isolated-install.test.ts -t hoist`
(6 pass), `test/cli/install/bun-prune.test.ts -t publicHoistPattern` (2
pass), `test/cli/install/config-precedence.test.ts` (51 pass),
`test/cli/install/npmrc.test.ts` (40 pass).
</details>

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 0 · Platform-specific test(s) that do not
run on this machine. Deferring to CI, which covers all platforms:
test/cli/run/run-eval.test.ts

<!-- robobun:evidence:end -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants