Compare commits

...

4 Commits

Author SHA1 Message Date
Claude Bot
c7c14e8e86 test: add plugin URL assertion to require syntax test
Addresses code review feedback to check for plugin documentation URL
consistently across all tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 01:09:43 +00:00
Claude Bot
58a3c2c863 fix(node): emit warning for unimplemented module.register()
module.register() is used by OpenTelemetry and other tooling to register
ESM loader hooks. Since this feature is not yet implemented in Bun, this
change emits a process warning to inform users that their loaders will
not be invoked, along with a pointer to Bun's plugin API as an alternative.

Fixes #3775

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 00:58:38 +00:00
Ciro Spaciari
22bebfc467 respect agent options and connectOpts in https module (#25937) 2026-01-14 11:52:53 -08:00
robobun
1800093a64 fix(install): use scope-specific registry for scoped packages in frozen lockfile (#26047)
## Summary
- Fixed `bun install --frozen-lockfile` to use scope-specific registry
for scoped packages when the lockfile has an empty registry URL

When parsing a `bun.lock` file with an empty registry URL for a scoped
package (like `@example/test-package`), bun was unconditionally using
the default npm registry (`https://registry.npmjs.org/`) instead of
looking up the scope-specific registry from `bunfig.toml`.

For example, with this configuration in `bunfig.toml`:
```toml
[install.scopes]
example = { url = "https://npm.pkg.github.com" }
```

And this lockfile entry with an empty registry URL:
```json
"@example/test-package": ["@example/test-package@1.0.0", "", {}, "sha512-AAAA"]
```

bun would try to fetch from
`https://registry.npmjs.org/@example/test-package/-/...` instead of
`https://npm.pkg.github.com/@example/test-package/-/...`.

The fix uses `manager.scopeForPackageName()` (the same pattern used in
`pnpm.zig`) to look up the correct scope-specific registry URL.

## Test plan
- [x] Added regression test `test/regression/issue/026039.test.ts` that
verifies:
  - Scoped packages use the scope-specific registry from `bunfig.toml`
  - Non-scoped packages continue to use the default registry
- [x] Verified test fails with system bun (without fix) and passes with
debug build (with fix)

Fixes #26039

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-01-14 10:22:30 -08:00
9 changed files with 1024 additions and 96 deletions

View File

@@ -1,6 +1,7 @@
#include "root.h"
#include "headers-handwritten.h"
#include "NodeModuleModule.h"
#include "BunProcess.h"
#include <JavaScriptCore/JSCInlines.h>
#include <JavaScriptCore/VM.h>
@@ -870,6 +871,13 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionSyncBuiltinESMExports,
JSC_DEFINE_HOST_FUNCTION(jsFunctionRegister, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = globalObject->vm();
Bun::Process::emitWarning(
globalObject,
jsString(vm, String("module.register() is not implemented in Bun. Loaders registered with module.register() will not be invoked. To intercept and transform modules, consider using Bun's plugin API: https://bun.sh/docs/bundler/plugins"_s)),
jsString(vm, String("Warning"_s)),
jsString(vm, String("BUN_UNSUPPORTED_REGISTER"_s)),
jsUndefined());
return JSC::JSValue::encode(JSC::jsUndefined());
}

View File

@@ -1708,8 +1708,14 @@ pub fn parseIntoBinaryLockfile(
};
if (registry_str.len == 0) {
// Use scope-specific registry if available, otherwise fall back to default
const registry_url = if (manager) |mgr|
mgr.scopeForPackageName(name_str).url.href
else
Npm.Registry.default_url;
const url = try ExtractTarball.buildURL(
Npm.Registry.default_url,
registry_url,
strings.StringOrTinyString.init(name.slice(string_buf.bytes.items)),
res.value.npm.version,
string_buf.bytes.items,

View File

@@ -1,8 +1,14 @@
const { isIP, isIPv6 } = require("internal/net/isIP");
const { checkIsHttpToken, validateFunction, validateInteger, validateBoolean } = require("internal/validators");
const {
checkIsHttpToken,
validateFunction,
validateInteger,
validateBoolean,
validateString,
} = require("internal/validators");
const { urlToHttpOptions } = require("internal/url");
const { isValidTLSArray } = require("internal/tls");
const { throwOnInvalidTLSArray } = require("internal/tls");
const { validateHeaderName } = require("node:_http_common");
const { getTimerDuration } = require("internal/timers");
const { ConnResetException } = require("internal/shared");
@@ -728,53 +734,48 @@ function ClientRequest(input, options, cb) {
throw new Error("pfx is not supported");
}
if (options.rejectUnauthorized !== undefined) this._ensureTls().rejectUnauthorized = options.rejectUnauthorized;
else {
let agentRejectUnauthorized = agent?.options?.rejectUnauthorized;
if (agentRejectUnauthorized !== undefined) this._ensureTls().rejectUnauthorized = agentRejectUnauthorized;
else {
// popular https-proxy-agent uses connectOpts
agentRejectUnauthorized = agent?.connectOpts?.rejectUnauthorized;
if (agentRejectUnauthorized !== undefined) this._ensureTls().rejectUnauthorized = agentRejectUnauthorized;
}
}
if (options.ca) {
if (!isValidTLSArray(options.ca))
throw new TypeError(
"ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile",
);
this._ensureTls().ca = options.ca;
}
if (options.cert) {
if (!isValidTLSArray(options.cert))
throw new TypeError(
"cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile",
);
this._ensureTls().cert = options.cert;
}
if (options.key) {
if (!isValidTLSArray(options.key))
throw new TypeError(
"key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile",
);
this._ensureTls().key = options.key;
}
if (options.passphrase) {
if (typeof options.passphrase !== "string") throw new TypeError("passphrase argument must be a string");
this._ensureTls().passphrase = options.passphrase;
}
if (options.ciphers) {
if (typeof options.ciphers !== "string") throw new TypeError("ciphers argument must be a string");
this._ensureTls().ciphers = options.ciphers;
}
if (options.servername) {
if (typeof options.servername !== "string") throw new TypeError("servername argument must be a string");
this._ensureTls().servername = options.servername;
}
// Merge TLS options using spread operator, matching Node.js behavior in createSocket:
// options = { __proto__: null, ...options, ...this.options };
// https://github.com/nodejs/node/blob/v23.6.0/lib/_http_agent.js#L242
// With spread, the last one wins, so agent.options overwrites request options.
//
// agent.options: Stored by Node.js Agent constructor
// https://github.com/nodejs/node/blob/v23.6.0/lib/_http_agent.js#L96
//
// agent.connectOpts: Used by https-proxy-agent for TLS connection options (lowest priority)
// https://github.com/TooTallNate/proxy-agents/blob/main/packages/https-proxy-agent/src/index.ts#L110-L117
const mergedTlsOptions = { __proto__: null, ...agent?.connectOpts, ...options, ...agent?.options };
if (options.secureOptions) {
if (typeof options.secureOptions !== "number") throw new TypeError("secureOptions argument must be a string");
this._ensureTls().secureOptions = options.secureOptions;
if (mergedTlsOptions.rejectUnauthorized !== undefined) {
this._ensureTls().rejectUnauthorized = mergedTlsOptions.rejectUnauthorized;
}
if (mergedTlsOptions.ca) {
throwOnInvalidTLSArray("options.ca", mergedTlsOptions.ca);
this._ensureTls().ca = mergedTlsOptions.ca;
}
if (mergedTlsOptions.cert) {
throwOnInvalidTLSArray("options.cert", mergedTlsOptions.cert);
this._ensureTls().cert = mergedTlsOptions.cert;
}
if (mergedTlsOptions.key) {
throwOnInvalidTLSArray("options.key", mergedTlsOptions.key);
this._ensureTls().key = mergedTlsOptions.key;
}
if (mergedTlsOptions.passphrase) {
validateString(mergedTlsOptions.passphrase, "options.passphrase");
this._ensureTls().passphrase = mergedTlsOptions.passphrase;
}
if (mergedTlsOptions.ciphers) {
validateString(mergedTlsOptions.ciphers, "options.ciphers");
this._ensureTls().ciphers = mergedTlsOptions.ciphers;
}
if (mergedTlsOptions.servername) {
validateString(mergedTlsOptions.servername, "options.servername");
this._ensureTls().servername = mergedTlsOptions.servername;
}
if (mergedTlsOptions.secureOptions) {
validateInteger(mergedTlsOptions.secureOptions, "options.secureOptions");
this._ensureTls().secureOptions = mergedTlsOptions.secureOptions;
}
this[kPath] = options.path || "/";
if (cb) {

View File

@@ -0,0 +1,30 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQieLggVjbubz09mX5
GdRQAwICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEJ++f2E23qU4mbP4
m3RnPasEggTQoS6zcBDvWURYyctw9Qma8L/ZnPg4SBclVzYbiZcvBPNRvCNLnYxQ
ysimU/8PTCP9m944dcsMolRqPjj0gOQCnBpqbZmnc7elwDFZIhePRfMKC2bPHZeo
ABonNOs2VstJ9gT3RA5x8Dj99dsoPdnV9rL6vkW0Gk86BPGgQq5i1ipJvYrpOtay
Bq5JgpptVX86azXZVriB8FUNfJuFOPQfxfXIY7ogHpQWZ7rIVa5ug7LlJ7sLjakj
ph/4corzRnRr88/eFfhYbV5rob/Lvoq8+I2Hgf25ypJ2XdOoWAgDOvl6+k01v/Ci
VAYAE1v9RgmiAXFIE9uYbSIyhiVibmLU6QK7Vcydv0ZaZLdP/9HwfZ6Q5u1a23rj
ltzRFOu5H7ipVXSoZU1ffw2EXi1RZJU2n5M3tU11qZsNpaDulEdcYZm74sUaqdjA
zkYSO+RBehptEUfgjXBrW8HJ42fCfd6IvQ7NtT3e3zJup105cHIEfO8IiSSt/oW3
SOupzjTpARHhAbPKSEmUVC1IXjGUvUuZs+NlN+byNkI4IhSTHp4vn5k87l22jccl
4NwW5ZIouqawvV5gyOGgBcwgSfvd4H8mcSeFfZhVmEtRDKtubREr8mqqcUWq5V/W
fEGR2LTQKRofhGGw56Jzw8FgNJNI0m6WBYIPQVtmwqqljPNPDuCQZ/icrhM6s0MR
7IyDiCUHzsz2JZxRJJO9pzItSABym/I57DTtRg1XQTEuSU+dTwhVzwkytWVldHx3
Rvbb6DUWrLtthoAs/LSDevjhrLYAdkLj4iaexqfYPcrRA22hj3KxxRpzV8zqMNvM
hI703HrjIPzlVhrqf6gMiKs7iZu2XQ4RRsQyKzWlro9bOprUvIg/abFtaJDXKqN0
sTJQ9rSpTJgUzG4sJEFiUeM0Wm2cLUO1w4N4/si89vOCcVJJUIjZgwsyFu8DpUIE
7E9rgAzuWByIBOJQ0f1hfF7zGUxAJ75qRdHm0q2aDkDPLiJk1alR1MpMs1tIcaBO
CAxnlZtORvq6QMQnERkpzuvX2PS5mtZ8w/qizPgb8GL3kU+Ex0lJHT8PBwspSXWV
Gc9AvCZ1z+YLnflUsRch/dI/suGhpIcLOX4M3pfW9qfo/i92uR52JWzIAkRKFTOi
fSiADLpar2WT2Kcz9aGfTB2swjhsL7Q6Tf8BWUCVYtfbf5FK07uPTCb9tyy+LxtU
qvtHe3XyZTO3guRBBDZotEOqNKzJw+ZUKIO7vX5JGtpMudBHL2J1KH80Qy4+uR/H
b9YyW0UFOyuOejmrMwHMP/iXkYyTsBiShETU0Uga33xvSuS10FhiCt87cXCI/WeZ
Jw1fk29QA3nx5vw9zDcVFiJRwOu9l6/JxXFpGm0ZjhYudS98yJkam3sbwJThJ+1C
fFzzCM69iUdPw/8JEPnD+Wd2okFiwjpEzHrZ+n1P5YGDF7UTyEB3gLpn3sgmBR9H
2z4yiL+ST/WI7n3ykXxzxjzcEgkDEwLfzHlguqh7jhYWuIhsDmcch7EgH8+gsyke
9lgUWJdoHXVfNZmWh4rMMkEUGi605WulXV8N9qQJJOJltN3lGdKZi+CBK6dTlPtJ
iAj5mvrk++pP/b0SplcQtq3pspGnWmjw+jw0aOVzSpn8qrco1/FZWdw=
-----END ENCRYPTED PRIVATE KEY-----

View File

@@ -1,28 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCIzOJskt6VkEJY
XKSJv/Gdil3XYkjk3NVc/+m+kzqnkTRbPtT9w+IGWgmJhuf9DJPLCwHFAEFarVwV
x16Q0PbU4ajXaLRHEYGhrH10oTMjQnJ24xVm26mxRXPQa5vaLpWJqNyIdNLIQLe+
UXUOzSGGsFTRMAjvYrkzjBe4ZUnaZV+aFY/ug0jfzeA1dJjzKZs6+yTJRbsuWUEb
8MsDmT4v+kBZDKdaDn7AFDWRVqx/38BnqsRzkM0CxpnyT2kRzw5zQajIE13gdTJo
1EHvYSUkkxrY5m30Rl9BuBBZBjhMzOHq0fYVVooHO+sf4XHPgvFTTxJum85u7J1J
oEUjrLKtAgMBAAECggEACInVNhaiqu4infZGVMy0rXMV8VwSlapM7O2SLtFsr0nK
XUmaLK6dvGzBPKK9dxdiYCFzPlMKQTkhzsAvYFWSmm3tRmikG+11TFyCRhXLpc8/
ark4vD9Io6ZkmKUmyKLwtXNjNGcqQtJ7RXc7Ga3nAkueN6JKZHqieZusXVeBGQ70
YH1LKyVNBeJggbj+g9rqaksPyNJQ8EWiNTJkTRQPazZ0o1VX/fzDFyr/a5npFtHl
4BHfafv9o1Xyr70Kie8CYYRJNViOCN+ylFs7Gd3XRaAkSkgMT/7DzrHdEM2zrrHK
yNg2gyDVX9UeEJG2X5UtU0o9BVW7WBshz/2hqIUHoQKBgQC8zsRFvC7u/rGr5vRR
mhZZG+Wvg03/xBSuIgOrzm+Qie6mAzOdVmfSL/pNV9EFitXt1yd2ROo31AbS7Evy
Bm/QVKr2mBlmLgov3B7O/e6ABteooOL7769qV/v+yo8VdEg0biHmsfGIIXDe3Lwl
OT0XwF9r/SeZLbw1zfkSsUVG/QKBgQC5fANM3Dc9LEek+6PHv5+eC1cKkyioEjUl
/y1VUD00aABI1TUcdLF3BtFN2t/S6HW0hrP3KwbcUfqC25k+GDLh1nM6ZK/gI3Yn
IGtCHxtE3S6jKhE9QcK/H+PzGVKWge9SezeYRP0GHJYDrTVTA8Kt9HgoZPPeReJl
+Ss9c8ThcQKBgECX6HQHFnNzNSufXtSQB7dCoQizvjqTRZPxVRoxDOABIGExVTYt
umUhPtu5AGyJ+/hblEeU+iBRbGg6qRzK8PPwE3E7xey8MYYAI5YjL7YjISKysBUL
AhM6uJ6Jg/wOBSnSx8xZ8kzlS+0izUda1rjKeprCSArSp8IsjlrDxPStAoGAEcPr
+P+altRX5Fhpvmb/Hb8OTif8G+TqjEIdkG9H/W38oP0ywg/3M2RGxcMx7txu8aR5
NjI7zPxZFxF7YvQkY3cLwEsGgVxEI8k6HLIoBXd90Qjlb82NnoqqZY1GWL4HMwo0
L/Rjm6M/Rwje852Hluu0WoIYzXA6F/Q+jPs6nzECgYAxx4IbDiGXuenkwSF1SUyj
NwJXhx4HDh7U6EO/FiPZE5BHE3BoTrFu3o1lzverNk7G3m+j+m1IguEAalHlukYl
rip9iUISlKYqbYZdLBoLwHAfHhszdrjqn8/v6oqbB5yR3HXjPFUWJo0WJ2pqJp56
ZshgmQQ/5Khoj6x0/dMPSg==
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDlYzosgRgXHL6v
Mh1V0ERFhsvlZrtRojSw6tafr3SQBphU793/rGiYZlL/lJ9HIlLkx9JMbuTjNm5U
2eRwHiTQIeWD4aCIESwPlkdaVYtC+IOj55bJN8xNa7h5GyJwF7PnPetAsKyE8DMB
n1gKMhaIis7HHOUtk4/K3Y4peU44d04z0yPt6JtY5Sbvi1E7pGX6T/2c9sHsdIDe
DctWnewpXXs8zkAla0KNWQfpDnpS53wxAfStTA4lSrA9daxC7hZopQlLxFIbJk+0
BLbEsXtrJ54T5iguHk+2MDVAy4MOqP9XbKV7eGHk73l6+CSwmHyHBxh4ChxRQeT5
BP0MUTn1AgMBAAECggEABtPvC5uVGr0DjQX2GxONsK8cOxoVec7U+C4pUMwBcXcM
yjxwlHdujpi/IDXtjsm+A2rSPu2vGPdKDfMFanPvPxW/Ne99noc6U0VzHsR8lnP8
wSB328nyJhzOeyZcXk9KTtgIPF7156gZsJLsZTNL+ej90i3xQWvKxCxXmrLuad5O
z/TrgZkC6wC3fgj1d3e8bMljQ7tLxbshJMYVI5o6RFTxy84DLI+rlvPkf7XbiMPf
2lsm4jcJKvfx+164HZJ9QVlx8ncqOHAnGvxb2xHHfqv4JAbz615t7yRvtaw4Paj5
6kQSf0VWnsVzgxNJWvnUZym/i/Qf5nQafjChCyKOEQKBgQD9f4SkvJrp/mFKWLHd
kDvRpSIIltfJsa5KShn1IHsQXFwc0YgyP4SKQb3Ckv+/9UFHK9EzM+WlPxZi7ZOS
hsWhIfkI4c4ORpxUQ+hPi0K2k+HIY7eYyONqDAzw5PGkKBo3mSGMHDXYywSqexhB
CCMHuHdMhwyHdz4PWYOK3C2VMQKBgQDnpsrHK7lM9aVb8wNhTokbK5IlTSzH/5oJ
lAVu6G6H3tM5YQeoDXztbZClvrvKU8DU5UzwaC+8AEWQwaram29QIDpAI3nVQQ0k
dmHHp/pCeADdRG2whaGcl418UJMMv8AUpWTRm+kVLTLqfTHBC0ji4NlCQMHCUCfd
U8TeUi5QBQKBgQDvJNd7mboDOUmLG7VgMetc0Y4T0EnuKsMjrlhimau/OYJkZX84
+BcPXwmnf4nqC3Lzs3B9/12L0MJLvZjUSHQ0mJoZOPxtF0vvasjEEbp0B3qe0wOn
DQ0NRCUJNNKJbJOfE8VEKnDZ/lx+f/XXk9eINwvElDrLqUBQtr+TxjbyYQKBgAxQ
lZ8Y9/TbajsFJDzcC/XhzxckjyjisbGoqNFIkfevJNN8EQgiD24f0Py+swUChtHK
jtiI8WCxMwGLCiYs9THxRKd8O1HW73fswy32BBvcfU9F//7OW9UTSXY+YlLfLrrq
P/3UqAN0L6y/kxGMJAfLpEEdaC+IS1Y8yc531/ZxAoGASYiasDpePtmzXklDxk3h
jEw64QAdXK2p/xTMjSeTtcqJ7fvaEbg+Mfpxq0mdTjfbTdR9U/nzAkwS7OoZZ4Du
ueMVls0IVqcNnBtikG8wgdxN27b5JPXS+GzQ0zDSpWFfRPZiIh37BAXr0D1voluJ
rEHkcals6p7hL98BoxjFIvA=
-----END PRIVATE KEY-----

View File

@@ -1,23 +1,23 @@
-----BEGIN CERTIFICATE-----
MIID5jCCAs6gAwIBAgIUN7coIsdMcLo9amZfkwogu0YkeLEwDQYJKoZIhvcNAQEL
BQAwfjELMAkGA1UEBhMCU0UxDjAMBgNVBAgMBVN0YXRlMREwDwYDVQQHDAhMb2Nh
dGlvbjEaMBgGA1UECgwRT3JnYW5pemF0aW9uIE5hbWUxHDAaBgNVBAsME09yZ2Fu
aXphdGlvbmFsIFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzA5MjExNDE2
MjNaFw0yNDA5MjAxNDE2MjNaMH4xCzAJBgNVBAYTAlNFMQ4wDAYDVQQIDAVTdGF0
ZTERMA8GA1UEBwwITG9jYXRpb24xGjAYBgNVBAoMEU9yZ2FuaXphdGlvbiBOYW1l
MRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRIwEAYDVQQDDAlsb2NhbGhv
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCIzOJskt6VkEJYXKSJ
v/Gdil3XYkjk3NVc/+m+kzqnkTRbPtT9w+IGWgmJhuf9DJPLCwHFAEFarVwVx16Q
0PbU4ajXaLRHEYGhrH10oTMjQnJ24xVm26mxRXPQa5vaLpWJqNyIdNLIQLe+UXUO
zSGGsFTRMAjvYrkzjBe4ZUnaZV+aFY/ug0jfzeA1dJjzKZs6+yTJRbsuWUEb8MsD
mT4v+kBZDKdaDn7AFDWRVqx/38BnqsRzkM0CxpnyT2kRzw5zQajIE13gdTJo1EHv
YSUkkxrY5m30Rl9BuBBZBjhMzOHq0fYVVooHO+sf4XHPgvFTTxJum85u7J1JoEUj
rLKtAgMBAAGjXDBaMA4GA1UdDwEB/wQEAwIDiDATBgNVHSUEDDAKBggrBgEFBQcD
ATAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFNzx4Rfs9m8XR5ML0WsI
sorKmB4PMA0GCSqGSIb3DQEBCwUAA4IBAQB87iQy8R0fiOky9WTcyzVeMaavS3MX
iTe1BRn1OCyDq+UiwwoNz7zdzZJFEmRtFBwPNFOe4HzLu6E+7yLFR552eYRHlqIi
/fiLb5JiZfPtokUHeqwELWBsoXtU8vKxViPiLZ09jkWOPZWo7b/xXd6QYykBfV91
usUXLzyTD2orMagpqNksLDGS3p3ggHEJBZtRZA8R7kPEw98xZHznOQpr26iv8kYz
ZWdLFoFdwgFBSfxePKax5rfo+FbwdrcTX0MhbORyiu2XsBAghf8s2vKDkHg2UQE8
haonxFYMFaASfaZ/5vWKYDTCJkJ67m/BtkpRafFEO+ad1i1S61OjfxH4
MIID4jCCAsqgAwIBAgIUcaRq6J/YF++Bo01Zc+HeQvCbnWMwDQYJKoZIhvcNAQEL
BQAwaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh
bmNpc2NvMQ0wCwYDVQQKDARPdmVuMREwDwYDVQQLDAhUZWFtIEJ1bjETMBEGA1UE
AwwKc2VydmVyLWJ1bjAeFw0yNTA5MDYwMzAwNDlaFw0zNTA5MDQwMzAwNDlaMGkx
CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNj
bzENMAsGA1UECgwET3ZlbjERMA8GA1UECwwIVGVhbSBCdW4xEzARBgNVBAMMCnNl
cnZlci1idW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDlYzosgRgX
HL6vMh1V0ERFhsvlZrtRojSw6tafr3SQBphU793/rGiYZlL/lJ9HIlLkx9JMbuTj
Nm5U2eRwHiTQIeWD4aCIESwPlkdaVYtC+IOj55bJN8xNa7h5GyJwF7PnPetAsKyE
8DMBn1gKMhaIis7HHOUtk4/K3Y4peU44d04z0yPt6JtY5Sbvi1E7pGX6T/2c9sHs
dIDeDctWnewpXXs8zkAla0KNWQfpDnpS53wxAfStTA4lSrA9daxC7hZopQlLxFIb
Jk+0BLbEsXtrJ54T5iguHk+2MDVAy4MOqP9XbKV7eGHk73l6+CSwmHyHBxh4ChxR
QeT5BP0MUTn1AgMBAAGjgYEwfzAdBgNVHQ4EFgQUw7nEnh4uOdZVZUapQzdAUaVa
An0wHwYDVR0jBBgwFoAUw7nEnh4uOdZVZUapQzdAUaVaAn0wDwYDVR0TAQH/BAUw
AwEB/zAsBgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAA
AAEwDQYJKoZIhvcNAQELBQADggEBAEA8r1fvDLMSCb8bkAURpFk8chn8pl5MChzT
YUDaLdCCBjPXJkSXNdyuwS+T/ljAGyZbW5xuDccCNKltawO4CbyEXUEZbYr3w9eq
j8uqymJPhFf0O1rKOI2han5GBCgHwG13QwKI+4uu7390nD+TlzLOhxFfvOG7OadH
QNMNLNyldgF4Nb8vWdz0FtQiGUIrO7iq4LFhhd1lCxe0q+FAYSEYcc74WtF/Yo8V
JQauXuXyoP5FqLzNt/yeNQhceyIXJGKCsjr5/bASBmVlCwgRfsD3jpG37L8YCJs1
L4WEikcY4Lzb2NF9e94IyZdQsRqd9DFBF5zP013MSUiuhiow32k=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,710 @@
/**
* All tests in this file run in both Bun and Node.js.
*
* Test that TLS options can be inherited from agent.options and agent.connectOpts.
* This is important for compatibility with libraries like https-proxy-agent.
*
* The HttpsProxyAgent tests verify that TLS options are properly passed through
* the proxy tunnel to the target HTTPS server.
*/
import { once } from "node:events";
import { readFileSync } from "node:fs";
import http from "node:http";
import https from "node:https";
import { createRequire } from "node:module";
import type { AddressInfo } from "node:net";
import net from "node:net";
import { dirname, join } from "node:path";
import { describe, test } from "node:test";
import { fileURLToPath } from "node:url";
// Use createRequire for ESM compatibility
const require = createRequire(import.meta.url);
const { HttpsProxyAgent } = require("https-proxy-agent") as {
HttpsProxyAgent: new (proxyUrl: string, options?: Record<string, unknown>) => http.Agent;
};
const __dirname = dirname(fileURLToPath(import.meta.url));
// Self-signed certificate with SANs for localhost and 127.0.0.1
// This cert is its own CA (self-signed)
const tlsCerts = {
cert: readFileSync(join(__dirname, "fixtures", "cert.pem"), "utf8"),
key: readFileSync(join(__dirname, "fixtures", "cert.key"), "utf8"),
encryptedKey: readFileSync(join(__dirname, "fixtures", "cert.encrypted.key"), "utf8"),
passphrase: "testpassword",
// Self-signed cert, so it's its own CA
get ca() {
return this.cert;
},
};
async function createHttpsServer(
options: https.ServerOptions = {},
): Promise<{ server: https.Server; port: number; hostname: string }> {
const server = https.createServer({ key: tlsCerts.key, cert: tlsCerts.cert, ...options }, (req, res) => {
res.writeHead(200);
res.end("OK");
});
await once(server.listen(0, "127.0.0.1"), "listening");
const { port } = server.address() as AddressInfo;
return { server, port, hostname: "127.0.0.1" };
}
async function createHttpServer(): Promise<{
server: http.Server;
port: number;
hostname: string;
}> {
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end("OK");
});
await once(server.listen(0, "127.0.0.1"), "listening");
const { port } = server.address() as AddressInfo;
return { server, port, hostname: "127.0.0.1" };
}
/**
* Create an HTTP CONNECT proxy server.
* This proxy handles the CONNECT method to establish tunnels for HTTPS connections.
*/
function createConnectProxy(): net.Server {
return net.createServer(clientSocket => {
let buffer: Uint8Array = new Uint8Array(0);
let tunnelEstablished = false;
let targetSocket: net.Socket | null = null;
clientSocket.on("data", (data: Uint8Array) => {
// If tunnel is already established, forward data directly
if (tunnelEstablished && targetSocket) {
targetSocket.write(data);
return;
}
// Concatenate buffers
const newBuffer = new Uint8Array(buffer.length + data.length);
newBuffer.set(buffer);
newBuffer.set(data, buffer.length);
buffer = newBuffer;
const bufferStr = new TextDecoder().decode(buffer);
// Check if we have complete headers
const headerEnd = bufferStr.indexOf("\r\n\r\n");
if (headerEnd === -1) return;
const headerPart = bufferStr.substring(0, headerEnd);
const lines = headerPart.split("\r\n");
const requestLine = lines[0];
// Check for CONNECT method
const match = requestLine.match(/^CONNECT\s+([^:]+):(\d+)\s+HTTP/);
if (!match) {
clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
clientSocket.end();
return;
}
const [, targetHost, targetPort] = match;
// Get any data after the headers (shouldn't be any for CONNECT)
// headerEnd is byte position in the string, need to account for UTF-8
const headerBytes = new TextEncoder().encode(bufferStr.substring(0, headerEnd + 4)).length;
const remainingData = buffer.subarray(headerBytes);
// Connect to target
targetSocket = net.connect(parseInt(targetPort, 10), targetHost, () => {
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
tunnelEstablished = true;
// Forward any remaining data
if (remainingData.length > 0) {
targetSocket!.write(remainingData);
}
// Set up bidirectional piping
targetSocket!.on("data", (chunk: Uint8Array) => {
clientSocket.write(chunk);
});
});
targetSocket.on("error", () => {
if (!tunnelEstablished) {
clientSocket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
}
clientSocket.end();
});
targetSocket.on("close", () => clientSocket.destroy());
clientSocket.on("close", () => targetSocket?.destroy());
});
clientSocket.on("error", () => {
targetSocket?.destroy();
});
});
}
/**
* Helper to start a proxy server and get its port.
*/
async function startProxy(server: net.Server): Promise<number> {
return new Promise<number>(resolve => {
server.listen(0, "127.0.0.1", () => {
const addr = server.address() as AddressInfo;
resolve(addr.port);
});
});
}
describe("https.request agent TLS options inheritance", () => {
describe("agent.options", () => {
test("inherits ca from agent.options", async () => {
const { server, port, hostname } = await createHttpsServer();
try {
// Create an agent with ca in options
const agent = new https.Agent({
ca: tlsCerts.ca,
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
// NO ca here - should inherit from agent.options
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
}
});
test("inherits rejectUnauthorized from agent.options", async () => {
const { server, port, hostname } = await createHttpsServer();
try {
// Create an agent with rejectUnauthorized: false in options
const agent = new https.Agent({
rejectUnauthorized: false,
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
// NO rejectUnauthorized here - should inherit from agent.options
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
}
});
test("inherits cert and key from agent.options", async () => {
// Create a server that uses TLS
const { server, port, hostname } = await createHttpsServer();
try {
// Create an agent with cert/key in options
const agent = new https.Agent({
rejectUnauthorized: false,
cert: tlsCerts.cert,
key: tlsCerts.key,
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
// NO cert/key here - should inherit from agent.options
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
}
});
});
// Test HttpsProxyAgent compatibility - these tests use real HttpsProxyAgent
// to verify HTTPS requests work through the proxy tunnel with TLS options
describe("HttpsProxyAgent TLS options", () => {
test("HttpsProxyAgent with rejectUnauthorized: false", async () => {
const { server, port, hostname } = await createHttpsServer();
const proxy = createConnectProxy();
const proxyPort = await startProxy(proxy);
try {
// Create HttpsProxyAgent for the proxy connection
const agent = new HttpsProxyAgent(`http://127.0.0.1:${proxyPort}`, {
rejectUnauthorized: false,
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
// TLS options must also be passed here for Node.js compatibility
// https-proxy-agent doesn't propagate these to target connection in Node.js
// See: https://github.com/TooTallNate/node-https-proxy-agent/issues/35
rejectUnauthorized: false,
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
proxy.close();
}
});
test("HttpsProxyAgent with ca option", async () => {
const { server, port, hostname } = await createHttpsServer();
const proxy = createConnectProxy();
const proxyPort = await startProxy(proxy);
try {
// Create HttpsProxyAgent for the proxy connection
const agent = new HttpsProxyAgent(`http://127.0.0.1:${proxyPort}`, {
ca: tlsCerts.ca,
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
// TLS options must also be passed here for Node.js compatibility
ca: tlsCerts.ca,
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
proxy.close();
}
});
test("HttpsProxyAgent with cert and key options", async () => {
const { server, port, hostname } = await createHttpsServer();
const proxy = createConnectProxy();
const proxyPort = await startProxy(proxy);
try {
// Create HttpsProxyAgent for the proxy connection
const agent = new HttpsProxyAgent(`http://127.0.0.1:${proxyPort}`, {
rejectUnauthorized: false,
cert: tlsCerts.cert,
key: tlsCerts.key,
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
// TLS options must also be passed here for Node.js compatibility
rejectUnauthorized: false,
cert: tlsCerts.cert,
key: tlsCerts.key,
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
proxy.close();
}
});
});
describe("option precedence (matches Node.js)", () => {
// In Node.js, options are merged via spread in createSocket:
// options = { __proto__: null, ...options, ...this.options };
// https://github.com/nodejs/node/blob/v23.6.0/lib/_http_agent.js#L365
// With spread, the last one wins, so agent.options overwrites request options.
test("agent.options takes precedence over direct options", async () => {
const { server, port, hostname } = await createHttpsServer();
try {
// Create an agent with correct CA
const agent = new https.Agent({
ca: tlsCerts.ca, // Correct CA in agent.options - should be used
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
ca: "wrong-ca-that-would-fail", // Wrong CA in request - should be ignored
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
}
});
test("direct options used when agent.options not set", async () => {
const { server, port, hostname } = await createHttpsServer();
try {
// Create an agent without ca
const agent = new https.Agent({});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
ca: tlsCerts.ca, // Direct option should be used since agent.options.ca is not set
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
}
});
});
describe("other TLS options", () => {
test("inherits servername from agent.options", async () => {
const { server, port, hostname } = await createHttpsServer();
try {
const agent = new https.Agent({
rejectUnauthorized: false,
servername: "localhost", // Should be passed to TLS
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
}
});
test("inherits ciphers from agent.options", async () => {
const { server, port, hostname } = await createHttpsServer();
try {
const agent = new https.Agent({
rejectUnauthorized: false,
ciphers: "HIGH:!aNULL:!MD5", // Custom cipher suite
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
}
});
test("inherits passphrase from agent.options", async () => {
// Create server that accepts connections with encrypted key
const { server, port, hostname } = await createHttpsServer({
key: tlsCerts.encryptedKey,
passphrase: tlsCerts.passphrase,
});
try {
// Create an agent with encrypted key and passphrase in options
const agent = new https.Agent({
ca: tlsCerts.ca,
cert: tlsCerts.cert,
key: tlsCerts.encryptedKey,
passphrase: tlsCerts.passphrase,
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
// NO passphrase here - should inherit from agent.options
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
}
});
test("supports multiple CAs (array)", async () => {
const { server, port, hostname } = await createHttpsServer();
try {
// Create an agent with CA as an array
const agent = new https.Agent({
ca: [tlsCerts.ca], // Array of CAs
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
}
});
});
describe("TLS error handling", () => {
test("rejects self-signed cert when rejectUnauthorized is true", async () => {
const { server, port, hostname } = await createHttpsServer();
try {
// Create an agent without CA and with rejectUnauthorized: true (default)
const agent = new https.Agent({
rejectUnauthorized: true,
// NO ca - should fail because cert is self-signed
});
const { promise, resolve, reject } = Promise.withResolvers<Error>();
const req = https.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
},
() => {
reject(new Error("Expected request to fail"));
},
);
req.on("error", resolve);
req.end();
const error = await promise;
// Should get a certificate error (self-signed cert not trusted)
if (
!(
error.message.includes("self-signed") ||
error.message.includes("SELF_SIGNED") ||
error.message.includes("certificate") ||
error.message.includes("unable to verify")
)
) {
throw new Error(`Expected certificate error, got: ${error.message}`);
}
} finally {
server.close();
}
});
});
});
describe("http.request agent options", () => {
test("does not fail when agent has TLS options (they are ignored for HTTP)", async () => {
const { server, port, hostname } = await createHttpServer();
try {
// Create an agent - TLS options passed via constructor should be ignored for HTTP
// Using type assertion since http.Agent doesn't normally accept TLS options
const agent = new (http.Agent as any)({
rejectUnauthorized: false,
ca: "some-ca",
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = http.request(
{
hostname,
port,
path: "/",
method: "GET",
agent,
},
res => {
res.on("data", () => {});
res.on("end", resolve);
},
);
req.on("error", reject);
req.end();
await promise;
} finally {
server.close();
}
});
});
// Only run in Bun to avoid infinite loop when Node.js runs this file
if (typeof Bun !== "undefined") {
const { bunEnv, nodeExe } = await import("harness");
describe("Node.js compatibility", () => {
test("all tests pass in Node.js", async () => {
const node = nodeExe();
if (!node) {
throw new Error("Node.js not found in PATH");
}
const testFile = fileURLToPath(import.meta.url);
await using proc = Bun.spawn({
cmd: [node, "--test", testFile],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
if (exitCode !== 0) {
throw new Error(`Node.js tests failed with code ${exitCode}\n${stderr}\n${stdout}`);
}
});
});
}

View File

@@ -0,0 +1,105 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
// Test for https://github.com/oven-sh/bun/issues/26039
// When parsing a bun.lock file with an empty registry URL for a scoped package,
// bun should use the scope-specific registry from bunfig.toml, not the default npm registry.
test("frozen lockfile should use scope-specific registry for scoped packages", async () => {
const dir = tempDirWithFiles("scoped-registry-test", {
"package.json": JSON.stringify({
name: "test-scoped-registry",
version: "1.0.0",
dependencies: {
"@example/test-package": "^1.0.0",
},
}),
"bunfig.toml": `
[install.scopes]
example = { url = "https://npm.pkg.github.com" }
`,
// bun.lock with empty string for registry URL - this should trigger the scope lookup
"bun.lock": JSON.stringify(
{
lockfileVersion: 1,
workspaces: {
"": {
dependencies: {
"@example/test-package": "^1.0.0",
},
},
},
packages: {
"@example/test-package": ["@example/test-package@1.0.0", "", {}, "sha512-AAAA"],
},
},
null,
2,
),
});
// Run bun install --frozen-lockfile. It will fail because the package doesn't exist,
// but the error message should show the correct registry URL (npm.pkg.github.com, not registry.npmjs.org)
const { stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "install", "--frozen-lockfile"],
cwd: dir,
env: bunEnv,
});
const stderrText = stderr.toString();
// Before the fix, this would try to fetch from https://registry.npmjs.org/@example/test-package/-/test-package-1.0.0.tgz
// After the fix, it should try to fetch from https://npm.pkg.github.com/@example/test-package/-/test-package-1.0.0.tgz
expect(stderrText).toContain("npm.pkg.github.com");
expect(stderrText).not.toContain("registry.npmjs.org");
// The install should fail because the package doesn't exist on the registry
expect(exitCode).not.toBe(0);
});
// Test that non-scoped packages still use the default registry when registry URL is empty
test("frozen lockfile should use default registry for non-scoped packages", async () => {
const dir = tempDirWithFiles("non-scoped-registry-test", {
"package.json": JSON.stringify({
name: "test-non-scoped-registry",
version: "1.0.0",
dependencies: {
"fake-nonexistent-package": "^1.0.0",
},
}),
"bunfig.toml": `
[install.scopes]
example = { url = "https://npm.pkg.github.com" }
`,
// bun.lock with empty string for registry URL for non-scoped package
"bun.lock": JSON.stringify(
{
lockfileVersion: 1,
workspaces: {
"": {
dependencies: {
"fake-nonexistent-package": "^1.0.0",
},
},
},
packages: {
"fake-nonexistent-package": ["fake-nonexistent-package@1.0.0", "", {}, "sha512-BBBB"],
},
},
null,
2,
),
});
const { stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "install", "--frozen-lockfile"],
cwd: dir,
env: bunEnv,
});
const stderrText = stderr.toString();
// Non-scoped packages should still use the default registry
expect(stderrText).toContain("registry.npmjs.org");
expect(stderrText).not.toContain("npm.pkg.github.com");
// The install should fail because the package doesn't exist on the registry
expect(exitCode).not.toBe(0);
});

View File

@@ -0,0 +1,68 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// https://github.com/oven-sh/bun/issues/3775
// module.register() should emit a warning since it's not implemented
test("module.register() emits a warning", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `import { register } from 'node:module'; register('./test.mjs', import.meta.url);`],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Check that the warning is emitted
expect(stderr).toContain("module.register() is not implemented in Bun");
expect(stderr).toContain("BUN_UNSUPPORTED_REGISTER");
expect(stderr).toContain("https://bun.sh/docs/bundler/plugins");
// Exit code should be 0 (warning, not error)
expect(exitCode).toBe(0);
});
test("module.register() with require syntax emits a warning", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `const { register } = require('node:module'); register('./test.mjs');`],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Check that the warning is emitted
expect(stderr).toContain("module.register() is not implemented in Bun");
expect(stderr).toContain("BUN_UNSUPPORTED_REGISTER");
expect(stderr).toContain("https://bun.sh/docs/bundler/plugins");
// Exit code should be 0 (warning, not error)
expect(exitCode).toBe(0);
});
test("module.register() emits warning only once per call", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
import { register } from 'node:module';
register('./test1.mjs', import.meta.url);
register('./test2.mjs', import.meta.url);
`,
],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The warning should be emitted twice (once per call)
const warningMatches = stderr.match(/module\.register\(\) is not implemented in Bun/g);
expect(warningMatches?.length).toBe(2);
// Exit code should be 0
expect(exitCode).toBe(0);
});