Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/api/schema.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3061,6 +3061,9 @@ pub const api = struct {

minimum_release_age_ms: ?f64 = null,
minimum_release_age_excludes: ?[]const []const u8 = null,

public_hoist_pattern: ?install.PnpmMatcher = null,
hoist_pattern: ?install.PnpmMatcher = null,
};

pub const ClientServerModule = struct {
Expand Down Expand Up @@ -3223,4 +3226,5 @@ const std = @import("std");

const bun = @import("bun");
const OOM = bun.OOM;
const install = bun.install;
const js_ast = bun.ast;
4 changes: 2 additions & 2 deletions src/bun.js/bindings/RegularExpression.zig
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ pub const RegularExpression = opaque {
extern fn Yarr__RegularExpression__searchRev(this: *RegularExpression) i32;
extern fn Yarr__RegularExpression__matches(this: *RegularExpression, string: bun.String) i32;

pub inline fn init(pattern: bun.String, flags: Flags) !*RegularExpression {
pub inline fn init(pattern: bun.String, flags: Flags) error{InvalidRegExp}!*RegularExpression {
var regex = Yarr__RegularExpression__init(pattern, @intFromEnum(flags));
if (!regex.isValid()) {
regex.deinit();
return error.InvalidRegex;
return error.InvalidRegExp;
}
return regex;
}
Expand Down
1 change: 1 addition & 0 deletions src/bun.js/jsc.zig
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ pub const SourceProvider = @import("./bindings/SourceProvider.zig").SourceProvid
pub const CatchScope = @import("./bindings/CatchScope.zig").CatchScope;
pub const ExceptionValidationScope = @import("./bindings/CatchScope.zig").ExceptionValidationScope;
pub const MarkedArgumentBuffer = @import("./bindings/MarkedArgumentBuffer.zig").MarkedArgumentBuffer;
pub const RegularExpression = @import("./bindings/RegularExpression.zig").RegularExpression;

// JavaScript-related
pub const Errorable = @import("./bindings/Errorable.zig").Errorable;
Expand Down
2 changes: 1 addition & 1 deletion src/bun.js/test/jest.zig
Original file line number Diff line number Diff line change
Expand Up @@ -504,13 +504,13 @@ const ExpectTypeOf = expect.ExpectTypeOf;
const bun = @import("bun");
const ArrayIdentityContext = bun.ArrayIdentityContext;
const Output = bun.Output;
const RegularExpression = bun.RegularExpression;
const default_allocator = bun.default_allocator;
const logger = bun.logger;

const jsc = bun.jsc;
const CallFrame = jsc.CallFrame;
const JSGlobalObject = jsc.JSGlobalObject;
const JSValue = jsc.JSValue;
const RegularExpression = jsc.RegularExpression;
const VirtualMachine = jsc.VirtualMachine;
const ZigString = jsc.ZigString;
2 changes: 0 additions & 2 deletions src/bun.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1991,8 +1991,6 @@ pub const WTF = struct {

pub const Wyhash11 = @import("./wyhash.zig").Wyhash11;

pub const RegularExpression = @import("./bun.js/bindings/RegularExpression.zig").RegularExpression;

const TODO_LOG = Output.scoped(.TODO, .visible);
pub inline fn todo(src: std.builtin.SourceLocation, value: anytype) @TypeOf(value) {
if (comptime Environment.allow_assert) {
Expand Down
24 changes: 24 additions & 0 deletions src/bunfig.zig
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,30 @@ pub const Bunfig = struct {
},
}
}

if (install_obj.get("publicHoistPattern")) |public_hoist_pattern_expr| {
install.public_hoist_pattern = bun.install.PnpmMatcher.fromExpr(
allocator,
public_hoist_pattern_expr,
this.log,
this.source,
) catch |err| switch (err) {
error.OutOfMemory => |oom| return oom,
error.UnexpectedExpr, error.InvalidRegExp => return error.@"Invalid Bunfig",
};
}

if (install_obj.get("hoistPattern")) |hoist_pattern_expr| {
install.hoist_pattern = bun.install.PnpmMatcher.fromExpr(
allocator,
hoist_pattern_expr,
this.log,
this.source,
) catch |err| switch (err) {
error.OutOfMemory => |oom| return oom,
error.UnexpectedExpr, error.InvalidRegExp => return error.@"Invalid Bunfig",
};
}
}

if (json.get("run")) |run_expr| {
Expand Down
2 changes: 1 addition & 1 deletion src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1727,11 +1727,11 @@ const bun = @import("bun");
const Environment = bun.Environment;
const Global = bun.Global;
const Output = bun.Output;
const RegularExpression = bun.RegularExpression;
const bun_js = bun.bun_js;
const clap = bun.clap;
const default_allocator = bun.default_allocator;
const logger = bun.logger;
const strings = bun.strings;
const File = bun.sys.File;
const RegularExpression = bun.jsc.RegularExpression;
const api = bun.schema.api;
2 changes: 1 addition & 1 deletion src/cli/Arguments.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1333,14 +1333,14 @@ const FeatureFlags = bun.FeatureFlags;
const Global = bun.Global;
const OOM = bun.OOM;
const Output = bun.Output;
const RegularExpression = bun.RegularExpression;
const clap = bun.clap;
const js_ast = bun.ast;
const logger = bun.logger;
const options = bun.options;
const resolve_path = bun.path;
const strings = bun.strings;
const Api = bun.schema.api;
const RegularExpression = bun.jsc.RegularExpression;

const CLI = bun.cli;
const Command = CLI.Command;
Expand Down
4 changes: 4 additions & 0 deletions src/collections/array_list.zig
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ pub fn ArrayListAlignedIn(
};
}

pub fn writer(self: *Self) Unmanaged.Writer {
return self.#unmanaged.writer(self.getStdAllocator());
}

/// Returns a borrowed version of the allocator.
pub fn allocator(self: *const Self) bun.allocators.Borrowed(Allocator) {
return bun.allocators.borrow(self.#allocator);
Expand Down
30 changes: 30 additions & 0 deletions src/ini.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,36 @@ pub fn loadNpmrc(
}
}

if (out.get("public-hoist-pattern")) |public_hoist_pattern_expr| {
install.public_hoist_pattern = 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;
},
};
}
Comment on lines +1076 to +1089

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.


if (out.get("hoist-pattern")) |hoist_pattern_expr| {
install.hoist_pattern = bun.install.PnpmMatcher.fromExpr(
allocator,
hoist_pattern_expr,
log,
source,
) catch |err| switch (err) {
error.OutOfMemory => |oom| return oom,
error.InvalidRegExp, error.UnexpectedExpr => patterns: {
log.reset();
break :patterns null;
},
};
}

var registry_map = install.scoped orelse bun.schema.api.NpmRegistryMap{};

// Process scopes
Expand Down
11 changes: 11 additions & 0 deletions src/install/PackageManager/PackageManagerOptions.zig
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ depth: ?usize = null,
/// isolated installs (pnpm-like) or hoisted installs (yarn-like, original)
node_linker: NodeLinker = .auto,

public_hoist_pattern: ?bun.install.PnpmMatcher = null,
hoist_pattern: ?bun.install.PnpmMatcher = null,

// Security scanner module path
security_scanner: ?[]const u8 = null,

Expand Down Expand Up @@ -387,6 +390,14 @@ pub fn load(
this.minimum_release_age_excludes = exclusions;
}

if (config.public_hoist_pattern) |public_hoist_pattern| {
this.public_hoist_pattern = public_hoist_pattern;
}

if (config.hoist_pattern) |hoist_pattern| {
this.hoist_pattern = hoist_pattern;
}

this.explicit_global_directory = config.global_dir orelse this.explicit_global_directory;
}

Expand Down
198 changes: 198 additions & 0 deletions src/install/PnpmMatcher.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,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,
};
}
Comment on lines +26 to +103

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).


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;
Comment on lines +1 to +198

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).

Loading