implement publicHoistPattern and hoistPattern (#23567)

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

```toml
[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
- [x] only include patterns
- [x] only exclude patterns
- [x] mix of include and exclude
- [x] errors for unexpected expression types
- [x] excluding direct dependency (should still include)
- [x] match all with `*`
- [x] string and array expression types

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Dylan Conway
2025-10-21 14:18:39 -07:00
committed by GitHub
parent 7662de9632
commit 150338faab
22 changed files with 1119 additions and 195 deletions

122
src/string/escapeRegExp.zig Normal file
View File

@@ -0,0 +1,122 @@
const special_characters = "|\\{}()[]^$+*?.-";
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);
}
pub fn jsEscapeRegExp(global: *JSGlobalObject, call_frame: *jsc.CallFrame) JSError!JSValue {
const input_value = call_frame.argument(0);
if (!input_value.isString()) {
return global.throw("expected string argument", .{});
}
var input = try input_value.toSlice(global, bun.default_allocator);
defer input.deinit();
var buf: bun.collections.ArrayListDefault(u8) = .init();
defer buf.deinit();
try escapeRegExp(input.slice(), buf.writer());
var output = String.cloneUTF8(buf.items());
return output.toJS(global);
}
pub fn jsEscapeRegExpForPackageNameMatching(global: *JSGlobalObject, call_frame: *jsc.CallFrame) JSError!JSValue {
const input_value = call_frame.argument(0);
if (!input_value.isString()) {
return global.throw("expected string argument", .{});
}
var input = try input_value.toSlice(global, bun.default_allocator);
defer input.deinit();
var buf: bun.collections.ArrayListDefault(u8) = .init();
defer buf.deinit();
try escapeRegExpForPackageNameMatching(input.slice(), buf.writer());
var output = String.cloneUTF8(buf.items());
return output.toJS(global);
}
const bun = @import("bun");
const Environment = bun.Environment;
const JSError = bun.JSError;
const String = bun.String;
const strings = bun.strings;
const jsc = bun.jsc;
const JSGlobalObject = jsc.JSGlobalObject;
const JSValue = jsc.JSValue;

View File

@@ -2306,6 +2306,9 @@ pub const visibleCodepointWidthType = visible_.visibleCodepointWidthType;
pub const escapeHTMLForLatin1Input = escapeHTML_.escapeHTMLForLatin1Input;
pub const escapeHTMLForUTF16Input = escapeHTML_.escapeHTMLForUTF16Input;
pub const escapeRegExp = escapeRegExp_.escapeRegExp;
pub const escapeRegExpForPackageNameMatching = escapeRegExp_.escapeRegExpForPackageNameMatching;
pub const addNTPathPrefix = paths_.addNTPathPrefix;
pub const addNTPathPrefixIfNeeded = paths_.addNTPathPrefixIfNeeded;
pub const addLongPathPrefix = paths_.addLongPathPrefix;
@@ -2347,6 +2350,7 @@ pub const CodePoint = i32;
const string = []const u8;
const escapeHTML_ = @import("./immutable/escapeHTML.zig");
const escapeRegExp_ = @import("./escapeRegExp.zig");
const paths_ = @import("./immutable/paths.zig");
const std = @import("std");
const unicode = @import("./immutable/unicode.zig");