diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000000000..a21be158eba425 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,26 @@ +name: Lint + +on: + pull_request: + workflow_dispatch: + +env: + BUN_VERSION: "1.1.38" + OXLINT_VERSION: "0.15.0" + +jobs: + lint-js: + name: "Lint JavaScript" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + - name: Lint + run: bunx oxlint --config oxlint.json --quiet --format github + + + + diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml new file mode 100644 index 00000000000000..74c2aaec89a2c6 --- /dev/null +++ b/.github/workflows/typos.yml @@ -0,0 +1,19 @@ +name: Typos + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Spellcheck + uses: crate-ci/typos@v1.29.4 + with: + files: docs/**/* diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000000000..5b36b34dde8fa5 --- /dev/null +++ b/.mailmap @@ -0,0 +1,2 @@ +# To learn more about git's mailmap: https://ntietz.com/blog/git-mailmap-for-name-changes +chloe caruso diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 00000000000000..7819bc056fe4a8 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,2 @@ +[type.md] +extend-ignore-words-re = ["^ba"] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d483bf06844619..2aa1a1b113cece 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ Configuring a development environment for Bun can take 10-30 minutes depending on your internet connection and computer speed. You will need ~10GB of free disk space for the repository and build artifacts. -If you are using Windows, please refer to [this guide](/docs/project/building-windows.md) +If you are using Windows, please refer to [this guide](https://bun.sh/docs/project/building-windows) ## Install Dependencies @@ -63,7 +63,7 @@ $ brew install llvm@18 ```bash#Ubuntu/Debian $ # LLVM has an automatic installation script that is compatible with all versions of Ubuntu -$ wget https://apt.llvm.org/llvm.sh -O - | sudo bash -s -- 16 all +$ wget https://apt.llvm.org/llvm.sh -O - | sudo bash -s -- 18 all ``` ```bash#Arch @@ -71,23 +71,21 @@ $ sudo pacman -S llvm clang lld ``` ```bash#Fedora -$ sudo dnf install 'dnf-command(copr)' -$ sudo dnf copr enable -y @fedora-llvm-team/llvm17 -$ sudo dnf install llvm16 clang16 lld16-devel +$ sudo dnf install llvm18 clang18 lld18-devel ``` ```bash#openSUSE Tumbleweed -$ sudo zypper install clang16 lld16 llvm16 +$ sudo zypper install clang18 lld18 llvm18 ``` {% /codetabs %} -If none of the above solutions apply, you will have to install it [manually](https://github.com/llvm/llvm-project/releases/tag/llvmorg-16.0.6). +If none of the above solutions apply, you will have to install it [manually](https://github.com/llvm/llvm-project/releases/tag/llvmorg-18.1.8). Make sure Clang/LLVM 18 is in your path: ```bash -$ which clang-16 +$ which clang-18 ``` If not, run this to manually add it: @@ -96,13 +94,13 @@ If not, run this to manually add it: ```bash#macOS (Homebrew) # use fish_add_path if you're using fish -# use path+="$(brew --prefix llvm@16)/bin" if you are using zsh -$ export PATH="$(brew --prefix llvm@16)/bin:$PATH" +# use path+="$(brew --prefix llvm@18)/bin" if you are using zsh +$ export PATH="$(brew --prefix llvm@18)/bin:$PATH" ``` ```bash#Arch # use fish_add_path if you're using fish -$ export PATH="$PATH:/usr/lib/llvm16/bin" +$ export PATH="$PATH:/usr/lib/llvm18/bin" ``` {% /codetabs %} @@ -163,7 +161,7 @@ The binary will be located at `./build/release/bun` and `./build/release/bun-pro ### Download release build from pull requests -To save you time spent building a release build locally, we provide a way to run release builds from pull requests. This is useful for manully testing changes in a release build before they are merged. +To save you time spent building a release build locally, we provide a way to run release builds from pull requests. This is useful for manually testing changes in a release build before they are merged. To run a release build from a pull request, you can use the `bun-pr` npm package: @@ -209,7 +207,7 @@ $ git clone https://github.com/oven-sh/WebKit vendor/WebKit # Make a debug build of JSC. This will output build artifacts in ./vendor/WebKit/WebKitBuild/Debug # Optionally, you can use `make jsc` for a release build -$ make jsc-debug +$ make jsc-debug && rm vendor/WebKit/WebKitBuild/Debug/JavaScriptCore/DerivedSources/inspector/InspectorProtocolObjects.h # Build bun with the local JSC build $ bun run build:local @@ -240,7 +238,7 @@ The issue may manifest when initially running `bun setup` as Clang being unable ``` The C++ compiler - "/usr/bin/clang++-16" + "/usr/bin/clang++-18" is not able to compile a simple test program. ``` diff --git a/LATEST b/LATEST index 2c6bb72b8ce859..0663412d155ed0 100644 --- a/LATEST +++ b/LATEST @@ -1 +1 @@ -1.1.41 \ No newline at end of file +1.1.43 \ No newline at end of file diff --git a/bench/bun.lockb b/bench/bun.lockb index e77a3b406cbb3b..7ccc5f77c570b4 100755 Binary files a/bench/bun.lockb and b/bench/bun.lockb differ diff --git a/bench/hot-module-reloading/css-stress-test/src/index.tsx b/bench/hot-module-reloading/css-stress-test/src/index.tsx index 5eefb430406aaa..c8b470ec7aa7ea 100644 --- a/bench/hot-module-reloading/css-stress-test/src/index.tsx +++ b/bench/hot-module-reloading/css-stress-test/src/index.tsx @@ -1,7 +1,7 @@ import ReactDOM from "react-dom"; import { Main } from "./main"; -const Base = ({}) => { +const Base = () => { const name = typeof location !== "undefined" ? decodeURIComponent(location.search.substring(1)) : null; return
; }; diff --git a/bench/package.json b/bench/package.json index a80d7566dc6650..d71efc00aa6445 100644 --- a/bench/package.json +++ b/bench/package.json @@ -13,7 +13,7 @@ "execa": "^8.0.1", "fast-glob": "3.3.1", "fdir": "^6.1.0", - "mitata": "^1.0.10", + "mitata": "^1.0.25", "react": "^18.3.1", "react-dom": "^18.3.1", "string-width": "7.1.0", diff --git a/bench/snippets/byteLength.mjs b/bench/snippets/byteLength.mjs new file mode 100644 index 00000000000000..810bf487fd572b --- /dev/null +++ b/bench/snippets/byteLength.mjs @@ -0,0 +1,27 @@ +import { Buffer } from "node:buffer"; +import { bench, run } from "../runner.mjs"; + +const variations = [ + ["latin1", "hello world"], + ["utf16", "hello emoji 🤔"], +]; + +for (const [label, string] of variations) { + const big = Buffer.alloc(1000000, string).toString(); + const small = Buffer.from(string).toString(); + const substring = big.slice(0, big.length - 2); + + bench(`${substring.length}`, () => { + return Buffer.byteLength(substring, "utf8"); + }); + + bench(`${small.length}`, () => { + return Buffer.byteLength(small); + }); + + bench(`${big.length}`, () => { + return Buffer.byteLength(big); + }); +} + +await run(); diff --git a/bench/snippets/native-overhead.mjs b/bench/snippets/native-overhead.mjs index 32d459247e26e5..43576b21d4a594 100644 --- a/bench/snippets/native-overhead.mjs +++ b/bench/snippets/native-overhead.mjs @@ -1,20 +1,14 @@ +import { noOpForTesting as noop } from "bun:internal-for-testing"; import { bench, run } from "../runner.mjs"; // These are no-op C++ functions that are exported to JS. -const lazy = globalThis[Symbol.for("Bun.lazy")]; -const noop = lazy("noop"); const fn = noop.function; -const regular = noop.functionRegular; const callback = noop.callback; bench("C++ callback into JS", () => { callback(() => {}); }); -bench("C++ fn regular", () => { - regular(); -}); - bench("C++ fn", () => { fn(); }); diff --git a/bench/snippets/node-zlib-brotli.mjs b/bench/snippets/node-zlib-brotli.mjs new file mode 100644 index 00000000000000..01208d3ec99faf --- /dev/null +++ b/bench/snippets/node-zlib-brotli.mjs @@ -0,0 +1,37 @@ +import { bench, run } from "../runner.mjs"; +import { brotliCompress, brotliDecompress, createBrotliCompress, createBrotliDecompress } from "node:zlib"; +import { promisify } from "node:util"; +import { pipeline } from "node:stream/promises"; +import { Readable } from "node:stream"; +import { readFileSync } from "node:fs"; + +const brotliCompressAsync = promisify(brotliCompress); +const brotliDecompressAsync = promisify(brotliDecompress); + +const testData = + process.argv.length > 2 + ? readFileSync(process.argv[2]) + : Buffer.alloc(1024 * 1024 * 16, "abcdefghijklmnopqrstuvwxyz"); +let compressed; + +bench("brotli compress", async () => { + compressed = await brotliCompressAsync(testData); +}); + +bench("brotli decompress", async () => { + await brotliDecompressAsync(compressed); +}); + +bench("brotli compress stream", async () => { + const source = Readable.from([testData]); + const compress = createBrotliCompress(); + await pipeline(source, compress); +}); + +bench("brotli decompress stream", async () => { + const source = Readable.from([compressed]); + const decompress = createBrotliDecompress(); + await pipeline(source, decompress); +}); + +await run(); diff --git a/bench/snippets/urlsearchparams.mjs b/bench/snippets/urlsearchparams.mjs index 83a874dc5f191a..4663dbfedf2c33 100644 --- a/bench/snippets/urlsearchparams.mjs +++ b/bench/snippets/urlsearchparams.mjs @@ -10,7 +10,6 @@ bench("new URLSearchParams(obj)", () => { "Content-Length": "123", "User-Agent": "node-fetch/1.0", "Accept-Encoding": "gzip,deflate", - "Content-Length": "0", "Content-Range": "bytes 0-9/10", }); }); diff --git a/build.zig b/build.zig index 0fc377326fbf50..e7dfcb2e1f64b2 100644 --- a/build.zig +++ b/build.zig @@ -470,6 +470,11 @@ pub fn addInstallObjectFile( name: []const u8, out_mode: ObjectFormat, ) *Step { + if (@import("builtin").os.tag != .windows and std.posix.getenvZ("COMPILE_ERRORS_ONLY") != null) { + const failstep = b.addSystemCommand(&.{"COMPILE_ERRORS_ONLY set but there were no compile errors"}); + failstep.step.dependOn(&compile.step); + return &failstep.step; + } // bin always needed to be computed or else the compilation will do nothing. zig build system bug? const bin = compile.getEmittedBin(); return &b.addInstallFile(switch (out_mode) { diff --git a/bunfig.node-test.toml b/bunfig.node-test.toml new file mode 100644 index 00000000000000..284945e3528887 --- /dev/null +++ b/bunfig.node-test.toml @@ -0,0 +1,4 @@ +# FIXME: move this back to test/js/node +# https://github.com/oven-sh/bun/issues/16289 +[test] +preload = ["./test/js/node/harness.ts", "./test/preload.ts"] diff --git a/cmake/tools/SetupWebKit.cmake b/cmake/tools/SetupWebKit.cmake index 712f41aac84e3e..4422e25376825b 100644 --- a/cmake/tools/SetupWebKit.cmake +++ b/cmake/tools/SetupWebKit.cmake @@ -2,13 +2,15 @@ option(WEBKIT_VERSION "The version of WebKit to use") option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading") if(NOT WEBKIT_VERSION) - set(WEBKIT_VERSION ba17b88ba9a5c1140bf74cf3a1637fbc89a3b9e7) + set(WEBKIT_VERSION e1a802a2287edfe7f4046a9dd8307c8b59f5d816) endif() +string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX) + if(WEBKIT_LOCAL) set(DEFAULT_WEBKIT_PATH ${VENDOR_PATH}/WebKit/WebKitBuild/${CMAKE_BUILD_TYPE}) else() - set(DEFAULT_WEBKIT_PATH ${CACHE_PATH}/webkit-${WEBKIT_VERSION}) + set(DEFAULT_WEBKIT_PATH ${CACHE_PATH}/webkit-${WEBKIT_VERSION_PREFIX}) endif() option(WEBKIT_PATH "The path to the WebKit directory") @@ -28,6 +30,8 @@ if(WEBKIT_LOCAL) ${WEBKIT_PATH} ${WEBKIT_PATH}/JavaScriptCore/Headers/JavaScriptCore ${WEBKIT_PATH}/JavaScriptCore/PrivateHeaders + ${WEBKIT_PATH}/JavaScriptCore/DerivedSources/inspector + ${WEBKIT_PATH}/JavaScriptCore/PrivateHeaders/JavaScriptCore ${WEBKIT_PATH}/bmalloc/Headers ${WEBKIT_PATH}/WTF/Headers ) diff --git a/completions/bun.bash b/completions/bun.bash index ccabb1d73b61c5..eabdc343fb4aec 100644 --- a/completions/bun.bash +++ b/completions/bun.bash @@ -87,7 +87,7 @@ _bun_completions() { GLOBAL_OPTIONS[LONG_OPTIONS]="--use --cwd --bunfile --server-bunfile --config --disable-react-fast-refresh --disable-hmr --env-file --extension-order --jsx-factory --jsx-fragment --extension-order --jsx-factory --jsx-fragment --jsx-import-source --jsx-production --jsx-runtime --main-fields --no-summary --version --platform --public-dir --tsconfig-override --define --external --help --inject --loader --origin --port --dump-environment-variables --dump-limits --disable-bun-js"; GLOBAL_OPTIONS[SHORT_OPTIONS]="-c -v -d -e -h -i -l -u -p"; - PACKAGE_OPTIONS[ADD_OPTIONS_LONG]="--development --optional"; + PACKAGE_OPTIONS[ADD_OPTIONS_LONG]="--development --optional --peer"; PACKAGE_OPTIONS[ADD_OPTIONS_SHORT]="-d"; PACKAGE_OPTIONS[REMOVE_OPTIONS_LONG]=""; PACKAGE_OPTIONS[REMOVE_OPTIONS_SHORT]=""; diff --git a/completions/bun.zsh b/completions/bun.zsh index 49264ec3f9e7a6..f885ac03adc74c 100644 --- a/completions/bun.zsh +++ b/completions/bun.zsh @@ -35,6 +35,7 @@ _bun_add_completion() { '-D[]' \ '--development[]' \ '--optional[Add dependency to "optionalDependencies]' \ + '--peer[Add dependency to "peerDependencies]' \ '--exact[Add the exact version instead of the ^range]' && ret=0 @@ -339,6 +340,7 @@ _bun_install_completion() { '--development[]' \ '-D[]' \ '--optional[Add dependency to "optionalDependencies]' \ + '--peer[Add dependency to "peerDependencies]' \ '--exact[Add the exact version instead of the ^range]' && ret=0 diff --git a/docs/api/cc.md b/docs/api/cc.md index 212b928df54043..0cdf0b0a75bafa 100644 --- a/docs/api/cc.md +++ b/docs/api/cc.md @@ -179,16 +179,16 @@ type Flags = string | string[]; These are flags like `-I` for include directories and `-D` for preprocessor definitions. -#### `defines: Record` +#### `define: Record` -The `defines` is an optional object that should be passed to the TinyCC compiler. +The `define` is an optional object that should be passed to the TinyCC compiler. ```ts type Defines = Record; cc({ source: "hello.c", - defines: { + define: { "NDEBUG": "1", }, }); diff --git a/docs/api/ffi.md b/docs/api/ffi.md index 6284689cb271b5..18fe2bb4391404 100644 --- a/docs/api/ffi.md +++ b/docs/api/ffi.md @@ -297,6 +297,20 @@ setTimeout(() => { When you're done with a JSCallback, you should call `close()` to free the memory. +### Experimental thread-safe callbacks +`JSCallback` has experimental support for thread-safe callbacks. This will be needed if you pass a callback function into a different thread from it's instantiation context. You can enable it with the optional `threadsafe` option flag. +```ts +const searchIterator = new JSCallback( + (ptr, length) => /hello/.test(new CString(ptr, length)), + { + returns: "bool", + args: ["ptr", "usize"], + threadsafe: true, // Optional. Defaults to `false` + }, +); +``` +Be aware that there are still cases where this does not 100% work. + {% callout %} **⚡️ Performance tip** — For a slight performance boost, directly pass `JSCallback.prototype.ptr` instead of the `JSCallback` object: diff --git a/docs/api/s3.md b/docs/api/s3.md new file mode 100644 index 00000000000000..f467cd21723a8d --- /dev/null +++ b/docs/api/s3.md @@ -0,0 +1,678 @@ +Production servers often read, upload, and write files to S3-compatible object storage services instead of the local filesystem. Historically, that means local filesystem APIs you use in development can't be used in production. When you use Bun, things are different. + +Bun provides fast, native bindings for interacting with S3-compatible object storage services. Bun's S3 API is designed to be simple and feel similar to fetch's `Response` and `Blob` APIs (like Bun's local filesystem APIs). + +```ts +import { s3, write, S3Client } from "bun"; + +// Bun.s3 reads environment variables for credentials +// file() returns a lazy reference to a file on S3 +const metadata = s3.file("123.json"); + +// Download from S3 as JSON +const data = await metadata.json(); + +// Upload to S3 +await write(metadata, JSON.stringify({ name: "John", age: 30 })); + +// Presign a URL (synchronous - no network request needed) +const url = metadata.presign({ + acl: "public-read", + expiresIn: 60 * 60 * 24, // 1 day +}); + +// Delete the file +await metadata.delete(); +``` + +S3 is the [de facto standard](https://en.wikipedia.org/wiki/De_facto_standard) internet filesystem. Bun's S3 API works with S3-compatible storage services like: + +- AWS S3 +- Cloudflare R2 +- DigitalOcean Spaces +- MinIO +- Backblaze B2 +- ...and any other S3-compatible storage service + +## Basic Usage + +There are several ways to interact with Bun's S3 API. + +### `Bun.S3Client` & `Bun.s3` + +`Bun.s3` is equivalent to `new Bun.S3Client()`, relying on environment variables for credentials. + +To explicitly set credentials, pass them to the `Bun.S3Client` constructor. + +```ts +import { S3Client } from "bun"; + +const client = new S3Client({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // sessionToken: "..." + // acl: "public-read", + // endpoint: "https://s3.us-east-1.amazonaws.com", + // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 + // endpoint: "https://.digitaloceanspaces.com", // DigitalOcean Spaces + // endpoint: "http://localhost:9000", // MinIO +}); + +// Bun.s3 is a global singleton that is equivalent to `new Bun.S3Client()` +Bun.s3 = client; +``` + +### Working with S3 Files + +The **`file`** method in `S3Client` returns a **lazy reference to a file on S3**. + +```ts +// A lazy reference to a file on S3 +const s3file: S3File = client.file("123.json"); +``` + +Like `Bun.file(path)`, the `S3Client`'s `file` method is synchronous. It does zero network requests until you call a method that depends on a network request. + +### Reading files from S3 + +If you've used the `fetch` API, you're familiar with the `Response` and `Blob` APIs. `S3File` extends `Blob`. The same methods that work on `Blob` also work on `S3File`. + +```ts +// Read an S3File as text +const text = await s3file.text(); + +// Read an S3File as JSON +const json = await s3file.json(); + +// Read an S3File as an ArrayBuffer +const buffer = await s3file.arrayBuffer(); + +// Get only the first 1024 bytes +const partial = await s3file.slice(0, 1024).text(); + +// Stream the file +const stream = s3file.stream(); +for await (const chunk of stream) { + console.log(chunk); +} +``` + +#### Memory optimization + +Methods like `text()`, `json()`, `bytes()`, or `arrayBuffer()` avoid duplicating the string or bytes in memory when possible. + +If the text happens to be ASCII, Bun directly transfers the string to JavaScriptCore (the engine) without transcoding and without duplicating the string in memory. When you use `.bytes()` or `.arrayBuffer()`, it will also avoid duplicating the bytes in memory. + +These helper methods not only simplify the API, they also make it faster. + +### Writing & uploading files to S3 + +Writing to S3 is just as simple. + +```ts +// Write a string (replacing the file) +await s3file.write("Hello World!"); + +// Write a Buffer (replacing the file) +await s3file.write(Buffer.from("Hello World!")); + +// Write a Response (replacing the file) +await s3file.write(new Response("Hello World!")); + +// Write with content type +await s3file.write(JSON.stringify({ name: "John", age: 30 }), { + type: "application/json", +}); + +// Write using a writer (streaming) +const writer = s3file.writer({ type: "application/json" }); +writer.write("Hello"); +writer.write(" World!"); +await writer.end(); + +// Write using Bun.write +await Bun.write(s3file, "Hello World!"); +``` + +### Working with large files (streams) + +Bun automatically handles multipart uploads for large files and provides streaming capabilities. The same API that works for local files also works for S3 files. + +```ts +// Write a large file +const bigFile = Buffer.alloc(10 * 1024 * 1024); // 10MB +const writer = s3file.writer({ + // Automatically retry on network errors up to 3 times + retry: 3, + + // Queue up to 10 requests at a time + queueSize: 10, + + // Upload in 5 MB chunks + partSize: 5 * 1024 * 1024, +}); +for (let i = 0; i < 10; i++) { + await writer.write(bigFile); +} +await writer.end(); +``` + +## Presigning URLs + +When your production service needs to let users upload files to your server, it's often more reliable for the user to upload directly to S3 instead of your server acting as an intermediary. + +To facilitate this, you can presign URLs for S3 files. This generates a URL with a signature that allows a user to securely upload that specific file to S3, without exposing your credentials or granting them unnecessary access to your bucket. + +```ts +import { s3 } from "bun"; + +// Generate a presigned URL that expires in 24 hours (default) +const url = s3.presign("my-file.txt", { + expiresIn: 3600, // 1 hour +}); +``` + +### Setting ACLs + +To set an ACL (access control list) on a presigned URL, pass the `acl` option: + +```ts +const url = s3file.presign({ + acl: "public-read", + expiresIn: 3600, +}); +``` + +You can pass any of the following ACLs: + +| ACL | Explanation | +| ----------------------------- | ------------------------------------------------------------------- | +| `"public-read"` | The object is readable by the public. | +| `"private"` | The object is readable only by the bucket owner. | +| `"public-read-write"` | The object is readable and writable by the public. | +| `"authenticated-read"` | The object is readable by the bucket owner and authenticated users. | +| `"aws-exec-read"` | The object is readable by the AWS account that made the request. | +| `"bucket-owner-read"` | The object is readable by the bucket owner. | +| `"bucket-owner-full-control"` | The object is readable and writable by the bucket owner. | +| `"log-delivery-write"` | The object is writable by AWS services used for log delivery. | + +### Expiring URLs + +To set an expiration time for a presigned URL, pass the `expiresIn` option. + +```ts +const url = s3file.presign({ + // Seconds + expiresIn: 3600, // 1 hour + + // access control list + acl: "public-read", + + // HTTP method + method: "PUT", +}); +``` + +### `method` + +To set the HTTP method for a presigned URL, pass the `method` option. + +```ts +const url = s3file.presign({ + method: "PUT", + // method: "DELETE", + // method: "GET", + // method: "HEAD", + // method: "POST", + // method: "PUT", +}); +``` + +### `new Response(S3File)` + +To quickly redirect users to a presigned URL for an S3 file, pass an `S3File` instance to a `Response` object as the body. + +```ts +const response = new Response(s3file); +console.log(response); +``` + +This will automatically redirect the user to the presigned URL for the S3 file, saving you the memory, time, and bandwidth cost of downloading the file to your server and sending it back to the user. + +```ts +Response (0 KB) { + ok: false, + url: "", + status: 302, + statusText: "", + headers: Headers { + "location": "https://.r2.cloudflarestorage.com/...", + }, + redirected: true, + bodyUsed: false +} +``` + +## Support for S3-Compatible Services + +Bun's S3 implementation works with any S3-compatible storage service. Just specify the appropriate endpoint: + +### Using Bun's S3Client with AWS S3 + +AWS S3 is the default. You can also pass a `region` option instead of an `endpoint` option for AWS S3. + +```ts +import { S3Client } from "bun"; + +// AWS S3 +const s3 = new S3Client({ + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", + // region: "us-east-1", +}); +``` + +### Using Bun's S3Client with Google Cloud Storage + +To use Bun's S3 client with [Google Cloud Storage](https://cloud.google.com/storage), set `endpoint` to `"https://storage.googleapis.com"` in the `S3Client` constructor. + +```ts +import { S3Client } from "bun"; + +// Google Cloud Storage +const gcs = new S3Client({ + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + endpoint: "https://storage.googleapis.com", +}); +``` + +### Using Bun's S3Client with Cloudflare R2 + +To use Bun's S3 client with [Cloudflare R2](https://developers.cloudflare.com/r2/), set `endpoint` to the R2 endpoint in the `S3Client` constructor. The R2 endpoint includes your account ID. + +```ts +import { S3Client } from "bun"; + +// CloudFlare R2 +const r2 = new S3Client({ + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + endpoint: "https://.r2.cloudflarestorage.com", +}); +``` + +### Using Bun's S3Client with DigitalOcean Spaces + +To use Bun's S3 client with [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces/), set `endpoint` to the DigitalOcean Spaces endpoint in the `S3Client` constructor. + +```ts +import { S3Client } from "bun"; + +const spaces = new S3Client({ + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + // region: "nyc3", + endpoint: "https://.digitaloceanspaces.com", +}); +``` + +### Using Bun's S3Client with MinIO + +To use Bun's S3 client with [MinIO](https://min.io/), set `endpoint` to the URL that MinIO is running on in the `S3Client` constructor. + +```ts +import { S3Client } from "bun"; + +const minio = new S3Client({ + accessKeyId: "access-key", + secretAccessKey: "secret-key", + bucket: "my-bucket", + + // Make sure to use the correct endpoint URL + // It might not be localhost in production! + endpoint: "http://localhost:9000", +}); +``` + +## Credentials + +Credentials are one of the hardest parts of using S3, and we've tried to make it as easy as possible. By default, Bun reads the following environment variables for credentials. + +| Option name | Environment variable | +| ----------------- | ---------------------- | +| `accessKeyId` | `S3_ACCESS_KEY_ID` | +| `secretAccessKey` | `S3_SECRET_ACCESS_KEY` | +| `region` | `S3_REGION` | +| `endpoint` | `S3_ENDPOINT` | +| `bucket` | `S3_BUCKET` | +| `sessionToken` | `S3_SESSION_TOKEN` | + +If the `S3_*` environment variable is not set, Bun will also check for the `AWS_*` environment variable, for each of the above options. + +| Option name | Fallback environment variable | +| ----------------- | ----------------------------- | +| `accessKeyId` | `AWS_ACCESS_KEY_ID` | +| `secretAccessKey` | `AWS_SECRET_ACCESS_KEY` | +| `region` | `AWS_REGION` | +| `endpoint` | `AWS_ENDPOINT` | +| `bucket` | `AWS_BUCKET` | +| `sessionToken` | `AWS_SESSION_TOKEN` | + +These environment variables are read from [`.env` files](/docs/runtime/env) or from the process environment at initialization time (`process.env` is not used for this). + +These defaults are overridden by the options you pass to `s3(credentials)`, `new Bun.S3Client(credentials)`, or any of the methods that accept credentials. So if, for example, you use the same credentials for different buckets, you can set the credentials once in your `.env` file and then pass `bucket: "my-bucket"` to the `s3()` helper function without having to specify all the credentials again. + +### `S3Client` objects + +When you're not using environment variables or using multiple buckets, you can create a `S3Client` object to explicitly set credentials. + +```ts +import { S3Client } from "bun"; + +const client = new S3Client({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // sessionToken: "..." + endpoint: "https://s3.us-east-1.amazonaws.com", + // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 + // endpoint: "http://localhost:9000", // MinIO +}); + +// Write using a Response +await file.write(new Response("Hello World!")); + +// Presign a URL +const url = file.presign({ + expiresIn: 60 * 60 * 24, // 1 day + acl: "public-read", +}); + +// Delete the file +await file.delete(); +``` + +### `S3Client.prototype.write` + +To upload or write a file to S3, call `write` on the `S3Client` instance. + +```ts +const client = new Bun.S3Client({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + endpoint: "https://s3.us-east-1.amazonaws.com", + bucket: "my-bucket", +}); +await client.write("my-file.txt", "Hello World!"); +await client.write("my-file.txt", new Response("Hello World!")); + +// equivalent to +// await client.file("my-file.txt").write("Hello World!"); +``` + +### `S3Client.prototype.delete` + +To delete a file from S3, call `delete` on the `S3Client` instance. + +```ts +const client = new Bun.S3Client({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", +}); + +await client.delete("my-file.txt"); +// equivalent to +// await client.file("my-file.txt").delete(); +``` + +### `S3Client.prototype.exists` + +To check if a file exists in S3, call `exists` on the `S3Client` instance. + +```ts +const client = new Bun.S3Client({ + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", +}); + +const exists = await client.exists("my-file.txt"); +// equivalent to +// const exists = await client.file("my-file.txt").exists(); +``` + +## `S3File` + +`S3File` instances are created by calling the `S3` instance method or the `s3()` helper function. Like `Bun.file()`, `S3File` instances are lazy. They don't refer to something that necessarily exists at the time of creation. That's why all the methods that don't involve network requests are fully synchronous. + +```ts +interface S3File extends Blob { + slice(start: number, end?: number): S3File; + exists(): Promise; + unlink(): Promise; + presign(options: S3Options): string; + text(): Promise; + json(): Promise; + bytes(): Promise; + arrayBuffer(): Promise; + stream(options: S3Options): ReadableStream; + write( + data: + | string + | Uint8Array + | ArrayBuffer + | Blob + | ReadableStream + | Response + | Request, + options?: BlobPropertyBag, + ): Promise; + + exists(options?: S3Options): Promise; + unlink(options?: S3Options): Promise; + delete(options?: S3Options): Promise; + presign(options?: S3Options): string; + + stat(options?: S3Options): Promise; + /** + * Size is not synchronously available because it requires a network request. + * + * @deprecated Use `stat()` instead. + */ + size: NaN; + + // ... more omitted for brevity +} +``` + +Like `Bun.file()`, `S3File` extends [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob), so all the methods that are available on `Blob` are also available on `S3File`. The same API for reading data from a local file is also available for reading data from S3. + +| Method | Output | +| ---------------------------- | ---------------- | +| `await s3File.text()` | `string` | +| `await s3File.bytes()` | `Uint8Array` | +| `await s3File.json()` | `JSON` | +| `await s3File.stream()` | `ReadableStream` | +| `await s3File.arrayBuffer()` | `ArrayBuffer` | + +That means using `S3File` instances with `fetch()`, `Response`, and other web APIs that accept `Blob` instances just works. + +### Partial reads with `slice` + +To read a partial range of a file, you can use the `slice` method. + +```ts +const partial = s3file.slice(0, 1024); + +// Read the partial range as a Uint8Array +const bytes = await partial.bytes(); + +// Read the partial range as a string +const text = await partial.text(); +``` + +Internally, this works by using the HTTP `Range` header to request only the bytes you want. This `slice` method is the same as [`Blob.prototype.slice`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice). + +### Deleting files from S3 + +To delete a file from S3, you can use the `delete` method. + +```ts +await s3file.delete(); +// await s3File.unlink(); +``` + +`delete` is the same as `unlink`. + +## Error codes + +When Bun's S3 API throws an error, it will have a `code` property that matches one of the following values: + +- `ERR_S3_MISSING_CREDENTIALS` +- `ERR_S3_INVALID_METHOD` +- `ERR_S3_INVALID_PATH` +- `ERR_S3_INVALID_ENDPOINT` +- `ERR_S3_INVALID_SIGNATURE` +- `ERR_S3_INVALID_SESSION_TOKEN` + +When the S3 Object Storage service returns an error (that is, not Bun), it will be an `S3Error` instance (an `Error` instance with the name `"S3Error"`). + +## `S3Client` static methods + +The `S3Client` class provides several static methods for interacting with S3. + +### `S3Client.presign` (static) + +To generate a presigned URL for an S3 file, you can use the `S3Client.presign` static method. + +```ts +import { S3Client } from "bun"; + +const credentials = { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", + // endpoint: "https://.r2.cloudflarestorage.com", // Cloudflare R2 +}; + +const url = S3Client.presign("my-file.txt", { + ...credentials, + expiresIn: 3600, +}); +``` + +This is equivalent to calling `new S3Client(credentials).presign("my-file.txt", { expiresIn: 3600 })`. + +### `S3Client.exists` (static) + +To check if an S3 file exists, you can use the `S3Client.exists` static method. + +```ts +import { S3Client } from "bun"; + +const credentials = { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", +}; + +const exists = await S3Client.exists("my-file.txt", credentials); +``` + +The same method also works on `S3File` instances. + +```ts +const s3file = Bun.s3("my-file.txt", { + ...credentials, +}); +const exists = await s3file.exists(); +``` + +### `S3Client.stat` (static) + +To get the size, etag, and other metadata of an S3 file, you can use the `S3Client.stat` static method. + +```ts +import { S3Client } from "bun"; + +const credentials = { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", +}; + +const stat = await S3Client.stat("my-file.txt", credentials); +// { +// etag: "\"7a30b741503c0b461cc14157e2df4ad8\"", +// lastModified: 2025-01-07T00:19:10.000Z, +// size: 1024, +// type: "text/plain;charset=utf-8", +// } +``` + +### `S3Client.delete` (static) + +To delete an S3 file, you can use the `S3Client.delete` static method. + +```ts +import { S3Client } from "bun"; + +const credentials = { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + bucket: "my-bucket", + // endpoint: "https://s3.us-east-1.amazonaws.com", +}; + +await S3Client.delete("my-file.txt", credentials); +// equivalent to +// await new S3Client(credentials).delete("my-file.txt"); + +// S3Client.unlink is alias of S3Client.delete +await S3Client.unlink("my-file.txt", credentials); +``` + +## s3:// protocol + +To make it easier to use the same code for local files and S3 files, the `s3://` protocol is supported in `fetch` and `Bun.file()`. + +```ts +const response = await fetch("s3://my-bucket/my-file.txt"); +const file = Bun.file("s3://my-bucket/my-file.txt"); +``` + +You can additionally pass `s3` options to the `fetch` and `Bun.file` functions. + +```ts +const response = await fetch("s3://my-bucket/my-file.txt", { + s3: { + accessKeyId: "your-access-key", + secretAccessKey: "your-secret-key", + endpoint: "https://s3.us-east-1.amazonaws.com", + }, + headers: { + "range": "bytes=0-1023", + }, +}); +``` + +### UTF-8, UTF-16, and BOM (byte order mark) + +Like `Response` and `Blob`, `S3File` assumes UTF-8 encoding by default. + +When calling one of the `text()` or `json()` methods on an `S3File`: + +- When a UTF-16 byte order mark (BOM) is detected, it will be treated as UTF-16. JavaScriptCore natively supports UTF-16, so it skips the UTF-8 transcoding process (and strips the BOM). This is mostly good, but it does mean if you have invalid surrogate pairs characters in your UTF-16 string, they will be passed through to JavaScriptCore (same as source code). +- When a UTF-8 BOM is detected, it gets stripped before the string is passed to JavaScriptCore and invalid UTF-8 codepoints are replaced with the Unicode replacement character (`\uFFFD`). +- UTF-32 is not supported. diff --git a/docs/api/sqlite.md b/docs/api/sqlite.md index 49c84136bb6de9..f9a707d27c30da 100644 --- a/docs/api/sqlite.md +++ b/docs/api/sqlite.md @@ -82,7 +82,7 @@ const strict = new Database( // throws error because of the typo: const query = strict .query("SELECT $message;") - .all({ messag: "Hello world" }); + .all({ message: "Hello world" }); const notStrict = new Database( ":memory:" @@ -90,7 +90,7 @@ const notStrict = new Database( // does not throw error: notStrict .query("SELECT $message;") - .all({ messag: "Hello world" }); + .all({ message: "Hello world" }); ``` ### Load via ES module import diff --git a/docs/api/utils.md b/docs/api/utils.md index 8c96472f01be70..979e4068511d4b 100644 --- a/docs/api/utils.md +++ b/docs/api/utils.md @@ -121,7 +121,7 @@ const id = randomUUIDv7(); A UUID v7 is a 128-bit value that encodes the current timestamp, a random value, and a counter. The timestamp is encoded using the lowest 48 bits, and the random value and counter are encoded using the remaining bits. -The `timestamp` parameter defaults to the current time in milliseconds. When the timestamp changes, the counter is reset to a psuedo-random integer wrapped to 4096. This counter is atomic and threadsafe, meaning that using `Bun.randomUUIDv7()` in many Workers within the same process running at the same timestamp will not have colliding counter values. +The `timestamp` parameter defaults to the current time in milliseconds. When the timestamp changes, the counter is reset to a pseudo-random integer wrapped to 4096. This counter is atomic and threadsafe, meaning that using `Bun.randomUUIDv7()` in many Workers within the same process running at the same timestamp will not have colliding counter values. The final 8 bytes of the UUID are a cryptographically secure random value. It uses the same random number generator used by `crypto.randomUUID()` (which comes from BoringSSL, which in turn comes from the platform-specific system random number generator usually provided by the underlying hardware). diff --git a/docs/bundler/plugins.md b/docs/bundler/plugins.md index 8e6b79c0e7df9f..1831e8d6cf0324 100644 --- a/docs/bundler/plugins.md +++ b/docs/bundler/plugins.md @@ -69,7 +69,7 @@ await Bun.build({ ### Namespaces -`onLoad` and `onResolve` accept an optional `namespace` string. What is a namespaace? +`onLoad` and `onResolve` accept an optional `namespace` string. What is a namespace? Every module has a namespace. Namespaces are used to prefix the import in transpiled code; for instance, a loader with a `filter: /\.yaml$/` and `namespace: "yaml:"` will transform an import from `./myfile.yaml` into `yaml:./myfile.yaml`. @@ -239,7 +239,7 @@ One of the arguments passed to the `onLoad` callback is a `defer` function. This This allows you to delay execution of the `onLoad` callback until all other modules have been loaded. -This is useful for returning contens of a module that depends on other modules. +This is useful for returning contents of a module that depends on other modules. ##### Example: tracking and reporting unused exports diff --git a/docs/cli/add.md b/docs/cli/add.md index ff90730d737e83..ca9a8af46e0e9c 100644 --- a/docs/cli/add.md +++ b/docs/cli/add.md @@ -33,6 +33,14 @@ To add a package as an optional dependency (`"optionalDependencies"`): $ bun add --optional lodash ``` +## `--peer` + +To add a package as a peer dependency (`"peerDependencies"`): + +```bash +$ bun add --peer @types/bun +``` + ## `--exact` {% callout %} diff --git a/docs/cli/filter.md b/docs/cli/filter.md index b4ee0dcf3f3ff4..62c53658a3da1f 100644 --- a/docs/cli/filter.md +++ b/docs/cli/filter.md @@ -1,4 +1,51 @@ -Use the `--filter` flag to execute lifecycle scripts in multiple packages at once: +The `--filter` (or `-F`) flag is used for selecting packages by pattern in a monorepo. Patterns can be used to match package names or package paths, with full glob syntax support. + +Currently `--filter` is supported by `bun install` and `bun outdated`, and can also be used to run scripts for multiple packages at once. + +## Matching + +### Package Name `--filter ` + +Name patterns select packages based on the package name, as specified in `package.json`. For example, if you have packages `pkg-a`, `pkg-b` and `other`, you can match all packages with `*`, only `pkg-a` and `pkg-b` with `pkg*`, and a specific package by providing the full name of the package. + +### Package Path `--filter ./` + +Path patterns are specified by starting the pattern with `./`, and will select all packages in directories that match the pattern. For example, to match all packages in subdirectories of `packages`, you can use `--filter './packages/**'`. To match a package located in `packages/foo`, use `--filter ./packages/foo`. + +## `bun install` and `bun outdated` + +Both `bun install` and `bun outdated` support the `--filter` flag. + +`bun install` by default will install dependencies for all packages in the monorepo. To install dependencies for specific packages, use `--filter`. + +Given a monorepo with workspaces `pkg-a`, `pkg-b`, and `pkg-c` under `./packages`: + +```bash +# Install dependencies for all workspaces except `pkg-c` +$ bun install --filter '!pkg-c' + +# Install dependencies for packages in `./packages` (`pkg-a`, `pkg-b`, `pkg-c`) +$ bun install --filter './packages/*' + +# Save as above, but exclude the root package.json +$ bun install --filter --filter '!./' --filter './packages/*' +``` + +Similarly, `bun outdated` will display outdated dependencies for all packages in the monorepo, and `--filter` can be used to restrict the command to a subset of the packages: + +```bash +# Display outdated dependencies for workspaces starting with `pkg-` +$ bun outdated --filter 'pkg-*' + +# Display outdated dependencies for only the root package.json +$ bun outdated --filter './' +``` + +For more information on both these commands, see [`bun install`](https://bun.sh/docs/cli/install) and [`bun outdated`](https://bun.sh/docs/cli/outdated). + +## Running scripts with `--filter` + +Use the `--filter` flag to execute scripts in multiple packages at once: ```bash bun --filter + + +

Hello World

+ +`, + "/src/styles.css": "body { background-color: red; }", + "/src/script.js": "console.log('Hello World')", + }, + experimentalHtml: true, + experimentalCss: true, + root: "/src", + entryPoints: ["/src/index.html"], + + onAfterBundle(api) { + // Check that output HTML references hashed filenames + api.expectFile("out/index.html").not.toContain("styles.css"); + api.expectFile("out/index.html").not.toContain("script.js"); + api.expectFile("out/index.html").toMatch(/href=".*\.css"/); + api.expectFile("out/index.html").toMatch(/src=".*\.js"/); + }, + }); + // Test multiple script and style bundling itBundled("html/multiple-assets", { outdir: "out/", @@ -721,4 +753,162 @@ body { expect(cssBundle).toContain("box-sizing: border-box"); }, }); + + // Test absolute paths in HTML + itBundled("html/absolute-paths", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + +

Absolute Paths

+ + +`, + "/styles/main.css": "body { margin: 0; }", + "/scripts/app.js": "console.log('App loaded')", + "/images/logo.png": "fake image content", + }, + experimentalHtml: true, + experimentalCss: true, + entryPoints: ["/index.html"], + onAfterBundle(api) { + // Check that absolute paths are handled correctly + const htmlBundle = api.readFile("out/index.html"); + + // CSS should be bundled and hashed + api.expectFile("out/index.html").not.toContain("/styles/main.css"); + api.expectFile("out/index.html").toMatch(/href=".*\.css"/); + + // JS should be bundled and hashed + api.expectFile("out/index.html").not.toContain("/scripts/app.js"); + api.expectFile("out/index.html").toMatch(/src=".*\.js"/); + + // Image should be hashed + api.expectFile("out/index.html").not.toContain("/images/logo.png"); + api.expectFile("out/index.html").toMatch(/src=".*\.png"/); + + // Get the bundled files and verify their contents + const cssMatch = htmlBundle.match(/href="(.*\.css)"/); + const jsMatch = htmlBundle.match(/src="(.*\.js)"/); + const imgMatch = htmlBundle.match(/src="(.*\.png)"/); + + expect(cssMatch).not.toBeNull(); + expect(jsMatch).not.toBeNull(); + expect(imgMatch).not.toBeNull(); + + const cssBundle = api.readFile("out/" + cssMatch![1]); + const jsBundle = api.readFile("out/" + jsMatch![1]); + + expect(cssBundle).toContain("margin: 0"); + expect(jsBundle).toContain("App loaded"); + }, + }); + + // Test that sourcemap comments are not included in HTML and CSS files + itBundled("html/no-sourcemap-comments", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + +

No Sourcemap Comments

+ +`, + "/styles.css": ` +body { + background-color: red; +} +/* This is a comment */`, + "/script.js": "console.log('Hello World')", + }, + experimentalHtml: true, + experimentalCss: true, + sourceMap: "linked", + entryPoints: ["/index.html"], + onAfterBundle(api) { + // Check HTML file doesn't contain sourcemap comments + const htmlContent = api.readFile("out/index.html"); + api.expectFile("out/index.html").not.toContain("sourceMappingURL"); + api.expectFile("out/index.html").not.toContain("debugId"); + + // Get the CSS filename from the HTML + const cssMatch = htmlContent.match(/href="(.*\.css)"/); + expect(cssMatch).not.toBeNull(); + const cssFile = cssMatch![1]; + + // Check CSS file doesn't contain sourcemap comments + api.expectFile("out/" + cssFile).not.toContain("sourceMappingURL"); + api.expectFile("out/" + cssFile).not.toContain("debugId"); + + // Get the JS filename from the HTML + const jsMatch = htmlContent.match(/src="(.*\.js)"/); + expect(jsMatch).not.toBeNull(); + const jsFile = jsMatch![1]; + + // JS file SHOULD contain sourcemap comment since it's supported + api.expectFile("out/" + jsFile).toContain("sourceMappingURL"); + }, + }); + + // Also test with inline sourcemaps + itBundled("html/no-sourcemap-comments-inline", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + +

No Sourcemap Comments

+ +`, + "/styles.css": ` +body { + background-color: red; +} +/* This is a comment */`, + "/script.js": "console.log('Hello World')", + }, + experimentalHtml: true, + experimentalCss: true, + sourceMap: "inline", + entryPoints: ["/index.html"], + onAfterBundle(api) { + // Check HTML file doesn't contain sourcemap comments + const htmlContent = api.readFile("out/index.html"); + api.expectFile("out/index.html").not.toContain("sourceMappingURL"); + api.expectFile("out/index.html").not.toContain("debugId"); + + // Get the CSS filename from the HTML + const cssMatch = htmlContent.match(/href="(.*\.css)"/); + expect(cssMatch).not.toBeNull(); + const cssFile = cssMatch![1]; + + // Check CSS file doesn't contain sourcemap comments + api.expectFile("out/" + cssFile).not.toContain("sourceMappingURL"); + api.expectFile("out/" + cssFile).not.toContain("debugId"); + + // Get the JS filename from the HTML + const jsMatch = htmlContent.match(/src="(.*\.js)"/); + expect(jsMatch).not.toBeNull(); + const jsFile = jsMatch![1]; + + // JS file SHOULD contain sourcemap comment since it's supported + api.expectFile("out/" + jsFile).toContain("sourceMappingURL"); + }, + }); }); diff --git a/test/bundler/esbuild/default.test.ts b/test/bundler/esbuild/default.test.ts index 39847ec22af728..785a5eca752696 100644 --- a/test/bundler/esbuild/default.test.ts +++ b/test/bundler/esbuild/default.test.ts @@ -1,4 +1,5 @@ import assert from "assert"; +import path from "path"; import { describe, expect } from "bun:test"; import { osSlashes } from "harness"; import { dedent, ESBUILD_PATH, itBundled } from "../expectBundled"; @@ -2797,20 +2798,25 @@ describe("bundler", () => { }, bundling: false, }); - itBundled("default/ImportMetaCommonJS", { + itBundled("default/ImportMetaCommonJS", ({ root }) => ({ + // Currently Bun emits `import.meta` instead of correctly + // polyfilling its properties. + todo: true, files: { "/entry.js": ` - import fs from "fs"; - import { fileURLToPath } from "url"; - console.log(fs.existsSync(fileURLToPath(import.meta.url)), fs.existsSync(import.meta.path)); + import fs from "fs"; + import { fileURLToPath } from "url"; + console.log(fileURLToPath(import.meta.url) === ${JSON.stringify(path.join(root, "out.cjs"))}); `, }, + outfile: "out.cjs", format: "cjs", target: "node", run: { + runtime: "node", stdout: "true true", }, - }); + })); itBundled("default/ImportMetaES6", { files: { "/entry.js": `console.log(import.meta.url, import.meta.path)`, diff --git a/test/bundler/esbuild/hello.ts b/test/bundler/esbuild/hello.ts new file mode 100644 index 00000000000000..483b7660f686de --- /dev/null +++ b/test/bundler/esbuild/hello.ts @@ -0,0 +1,3 @@ +import fs from "fs"; +import { fileURLToPath } from "url"; +console.log(fs.existsSync(fileURLToPath(import.meta.url)), fs.existsSync(import.meta.path)); diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index a96a3aed1cad5c..f229a603670b3f 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -1558,7 +1558,7 @@ for (const [key, blob] of build.outputs) { if (run.errorLineMatch) { const stackTraceLine = stack.pop()!; - const match = /at (.*):(\d+):(\d+)$/.exec(stackTraceLine); + const match = /at (?:<[^>]+> \()?([^)]+):(\d+):(\d+)\)?$/.exec(stackTraceLine); if (match) { const line = readFileSync(match[1], "utf-8").split("\n")[+match[2] - 1]; if (!run.errorLineMatch.test(line)) { @@ -1589,6 +1589,11 @@ for (const [key, blob] of build.outputs) { // no idea why this logs. ¯\_(ツ)_/¯ result = result.replace(/\[Event_?Loop\] enqueueTaskConcurrent\(RuntimeTranspilerStore\)\n/gi, ""); + // when the inspector runs (can be due to VSCode extension), there is + // a bug that in debug modes the console logs extra stuff + if (name === "stderr" && process.env.BUN_INSPECT_CONNECT_TO) { + result = result.replace(/(?:^|\n)\/[^\n]*: CONSOLE LOG[^\n]*(\n|$)/g, "$1").trim(); + } if (typeof expected === "string") { expected = dedent(expected).trim(); diff --git a/test/bundler/native-plugin.test.ts b/test/bundler/native-plugin.test.ts index 8a09905eaf7601..942461228f6896 100644 --- a/test/bundler/native-plugin.test.ts +++ b/test/bundler/native-plugin.test.ts @@ -74,6 +74,11 @@ values;`, await Bun.$`${bunExe()} i && ${bunExe()} build:napi`.env(bunEnv).cwd(tempdir); }); + beforeEach(() => { + const tempdir2 = tempDirWithFiles("native-plugins", {}); + process.chdir(tempdir2); + }); + afterEach(async () => { await Bun.$`rm -rf ${outdir}`; process.chdir(cwd); @@ -672,6 +677,7 @@ console.log(JSON.stringify(json)) }, }, ], + throw: true, }); expect(result.success).toBeTrue(); diff --git a/test/bundler/transpiler/function-tostring-require.test.ts b/test/bundler/transpiler/function-tostring-require.test.ts new file mode 100644 index 00000000000000..2ea355f1a3ca51 --- /dev/null +++ b/test/bundler/transpiler/function-tostring-require.test.ts @@ -0,0 +1,13 @@ +import { test, expect } from "bun:test"; + +test("toString doesnt observe import.meta.require", () => { + function hello() { + return typeof require("fs") === "string" ? "from eval" : "main function"; + } + const newFunctionBody = `return ${hello.toString()}`; + const loadFakeModule = new Function("require", newFunctionBody)(id => `fake require ${id}`); + expect(hello()).toBe("main function"); + expect(loadFakeModule()).toBe("from eval"); +}); + +export {}; diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index 9b53bb733c3420..8453a87dde99c8 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun"; +import { spawn, stderr } from "bun"; import { beforeEach, expect, it } from "bun:test"; import { copyFileSync, cpSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs"; import { bunEnv, bunExe, isDebug, tmpdirSync, waitForFileToExist } from "harness"; @@ -450,7 +450,7 @@ ${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`, let it = str.split("\n"); let line; while ((line = it.shift())) { - if (!line.includes("error")) continue; + if (!line.includes("error:")) continue; str = ""; if (reloadCounter === 50) { @@ -530,7 +530,7 @@ ${" ".repeat(reloadCounter * 2)}throw new Error(${reloadCounter});`, let it = str.split("\n"); let line; while ((line = it.shift())) { - if (!line.includes("error")) continue; + if (!line.includes("error:")) continue; str = ""; if (reloadCounter === 50) { diff --git a/test/cli/install/registry/__snapshots__/bun-install-registry.test.ts.snap b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap similarity index 99% rename from test/cli/install/registry/__snapshots__/bun-install-registry.test.ts.snap rename to test/cli/install/__snapshots__/bun-install-registry.test.ts.snap index 9d2bebfe0ceb2a..c7af63e37749b5 100644 --- a/test/cli/install/registry/__snapshots__/bun-install-registry.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap @@ -140,6 +140,7 @@ exports[`text lockfile workspace sorting 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "no-deps": "1.0.0", }, @@ -173,6 +174,7 @@ exports[`text lockfile workspace sorting 2`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "no-deps": "1.0.0", }, @@ -214,6 +216,7 @@ exports[`text lockfile --frozen-lockfile 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "a-dep": "^1.0.2", "no-deps": "^1.0.0", @@ -244,6 +247,7 @@ exports[`binaries each type of binary serializes correctly to text lockfile 1`] "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "dir-bin": "./dir-bin", "file-bin": "./file-bin", @@ -270,6 +274,7 @@ exports[`binaries root resolution bins 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "fooooo", "dependencies": { "fooooo": ".", "no-deps": "1.0.0", @@ -290,6 +295,7 @@ exports[`hoisting text lockfile is hoisted 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "hoist-lockfile-1": "1.0.0", "hoist-lockfile-2": "1.0.0", @@ -317,6 +323,7 @@ exports[`it should ignore peerDependencies within workspaces 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "peerDependencies": { "no-deps": ">=1.0.0", }, diff --git a/test/cli/install/__snapshots__/bun-install.test.ts.snap b/test/cli/install/__snapshots__/bun-install.test.ts.snap index 96d0b005509748..593bd7b0ddd523 100644 --- a/test/cli/install/__snapshots__/bun-install.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install.test.ts.snap @@ -61,7 +61,9 @@ exports[`should read install.saveTextLockfile from bunfig.toml 1`] = ` "{ "lockfileVersion": 0, "workspaces": { - "": {}, + "": { + "name": "foo", + }, "packages/pkg1": { "name": "pkg-one", "version": "1.0.0", diff --git a/test/cli/install/__snapshots__/bun-lock.test.ts.snap b/test/cli/install/__snapshots__/bun-lock.test.ts.snap new file mode 100644 index 00000000000000..4f15c6c30c47f0 --- /dev/null +++ b/test/cli/install/__snapshots__/bun-lock.test.ts.snap @@ -0,0 +1,45 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`should escape names 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "quote-in-dependency-name", + }, + "packages/\\"": { + "name": "\\"", + }, + "packages/pkg1": { + "name": "pkg1", + "dependencies": { + "\\"": "*", + }, + }, + }, + "packages": { + "\\"": ["\\"@workspace:packages/\\"", {}], + + "pkg1": ["pkg1@workspace:packages/pkg1", { "dependencies": { "\\"": "*" } }], + } +} +" +`; + +exports[`should write plaintext lockfiles 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "test-package", + "dependencies": { + "dummy-package": "file:./bar-0.0.2.tgz", + }, + }, + }, + "packages": { + "dummy-package": ["bar@./bar-0.0.2.tgz", {}], + } +} +" +`; diff --git a/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap b/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap index 423a2b49aed2a6..b419206fba6fa6 100644 --- a/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap @@ -1,2177 +1,2177 @@ // Bun Snapshot v1, https://goo.gl/fbAQLP exports[`dependency on workspace without version in package.json: version: * 1`] = ` -{ - "dependencies": [ +"{ + "format": "v2", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", + "package_index": { + "no-deps": 1, + "foo": 0, + "bar": 2 + }, + "trees": [ { - "behavior": { - "workspace": true, - }, "id": 0, - "literal": "packages/bar", + "path": "node_modules", + "depth": 0, + "dependencies": { + "bar": { + "id": 0, + "package_id": 2 + }, + "no-deps": { + "id": 1, + "package_id": 1 + } + } + } + ], + "dependencies": [ + { "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, - "id": 2, + "id": 1 + }, + { + "name": "no-deps", "literal": "*", - "name": "lodash", "npm": { - "name": "lodash", - "version": ">=0.0.0", + "name": "no-deps", + "version": ">=0.0.0" }, "package_id": 1, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": 1, - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, - }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: *.*.* 1`] = ` +"{ + "format": "v2", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", + "package_index": { + "no-deps": 1, + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", - }, + "package_id": 1 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: *.*.* 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, - "id": 2, + "id": 1 + }, + { + "name": "no-deps", "literal": "*.*.*", - "name": "lodash", "npm": { - "name": "lodash", - "version": ">=0.0.0", + "name": "no-deps", + "version": ">=0.0.0" }, "package_id": 1, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": 1, - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, - }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: =* 1`] = ` +"{ + "format": "v2", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", + "package_index": { + "no-deps": 1, + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", - }, + "package_id": 1 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: =* 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, - "id": 2, + "id": 1 + }, + { + "name": "no-deps", "literal": "=*", - "name": "lodash", "npm": { - "name": "lodash", - "version": ">=0.0.0", + "name": "no-deps", + "version": ">=0.0.0" }, "package_id": 1, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": 1, - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, - }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: kjwoehcojrgjoj 1`] = ` +"{ + "format": "v2", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", + "package_index": { + "no-deps": 1, + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", - }, + "package_id": 1 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: kjwoehcojrgjoj 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "kjwoehcojrgjoj", "dist_tag": { - "name": "lodash", - "tag": "lodash", + "name": "no-deps", + "tag": "no-deps" }, - "id": 2, - "literal": "kjwoehcojrgjoj", - "name": "lodash", "package_id": 1, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": 1, - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, - }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: *.1.* 1`] = ` +"{ + "format": "v2", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", + "package_index": { + "no-deps": 1, + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", - }, + "package_id": 1 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: *.1.* 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, - "id": 2, + "id": 1 + }, + { + "name": "no-deps", "literal": "*.1.*", - "name": "lodash", "npm": { - "name": "lodash", - "version": ">=0.0.0", + "name": "no-deps", + "version": ">=0.0.0" }, "package_id": 1, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": 1, - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, - }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: *-pre 1`] = ` +"{ + "format": "v2", + "meta_hash": "a5d5a45555763c1040428cd33363c16438c75b23d8961e7458abe2d985fa08d1", + "package_index": { + "no-deps": 1, + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", - }, + "package_id": 1 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: *-pre 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, - "id": 2, + "id": 1 + }, + { + "name": "no-deps", "literal": "*-pre", - "name": "lodash", "npm": { - "name": "lodash", - "version": ">=0.0.0", + "name": "no-deps", + "version": ">=0.0.0" }, "package_id": 1, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "1e2d5fa6591f007aa6674495d1022868fc3b60325c4a1555315ca0e16ef31c4e", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": 1, - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, - }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: 1 1`] = ` +"{ + "format": "v2", + "meta_hash": "80ecab0f58b4fb37bae1983a06ebd81b6573433d7f92e938ffa7854f8ff15e7c", + "package_index": { + "no-deps": [ + 1, + 3 + ], + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, + { + "id": 1, + "path": "node_modules/bar/node_modules", + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: 1 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, - "id": 2, + "id": 1 + }, + { + "name": "no-deps", "literal": "1", - "name": "lodash", "npm": { - "name": "lodash", - "version": "<2.0.0 >=1.0.0", + "name": "no-deps", + "version": "<2.0.0 >=1.0.0" }, "package_id": 3, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "56c714bc8ac0cdbf731de74d216134f3ce156ab45adda065fa84e4b2ce349f4b", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ - 1, - 3, - ], - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 3, - "integrity": "sha512-F7AB8u+6d00CCgnbjWzq9fFLpzOMCgq6mPjOW4+8+dYbrnc0obRrC+IHctzfZ1KKTQxX0xo/punrlpOWcf4gpw==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.3.1.tgz", "tag": "npm", - "value": "1.3.1", + "value": "1.1.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz" }, - "scripts": {}, - }, + "dependencies": [], + "integrity": "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: 1.* 1`] = ` +"{ + "format": "v2", + "meta_hash": "80ecab0f58b4fb37bae1983a06ebd81b6573433d7f92e938ffa7854f8ff15e7c", + "package_index": { + "no-deps": [ + 1, + 3 + ], + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", - }, + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: 1.* 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, - "id": 2, + "id": 1 + }, + { + "name": "no-deps", "literal": "1.*", - "name": "lodash", "npm": { - "name": "lodash", - "version": "<2.0.0 >=1.0.0", + "name": "no-deps", + "version": "<2.0.0 >=1.0.0" }, "package_id": 3, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "56c714bc8ac0cdbf731de74d216134f3ce156ab45adda065fa84e4b2ce349f4b", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ - 1, - 3, - ], - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 3, - "integrity": "sha512-F7AB8u+6d00CCgnbjWzq9fFLpzOMCgq6mPjOW4+8+dYbrnc0obRrC+IHctzfZ1KKTQxX0xo/punrlpOWcf4gpw==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.3.1.tgz", "tag": "npm", - "value": "1.3.1", + "value": "1.1.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz" }, - "scripts": {}, - }, + "dependencies": [], + "integrity": "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: 1.1.* 1`] = ` +"{ + "format": "v2", + "meta_hash": "80ecab0f58b4fb37bae1983a06ebd81b6573433d7f92e938ffa7854f8ff15e7c", + "package_index": { + "no-deps": [ + 1, + 3 + ], + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", - }, + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: 1.1.* 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, - "id": 2, + "id": 1 + }, + { + "name": "no-deps", "literal": "1.1.*", - "name": "lodash", "npm": { - "name": "lodash", - "version": "<1.2.0 >=1.1.0", + "name": "no-deps", + "version": "<1.2.0 >=1.1.0" }, "package_id": 3, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "56ec928a6d5f1d18236abc348bc711d6cfd08ca0a068bfc9fda24e7b22bed046", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ - 1, - 3, - ], - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 3, - "integrity": "sha512-SFeNKyKPh4kvYv0yd95fwLKw4JXM45PJLsPRdA8v7/q0lBzFeK6XS8xJTl6mlhb8PbAzioMkHli1W/1g0y4XQQ==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.1.1.tgz", "tag": "npm", - "value": "1.1.1", + "value": "1.1.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz" }, - "scripts": {}, - }, + "dependencies": [], + "integrity": "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: 1.1.0 1`] = ` +"{ + "format": "v2", + "meta_hash": "80ecab0f58b4fb37bae1983a06ebd81b6573433d7f92e938ffa7854f8ff15e7c", + "package_index": { + "no-deps": [ + 1, + 3 + ], + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", - }, + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: 1.1.1 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, - "id": 2, - "literal": "1.1.1", - "name": "lodash", + "id": 1 + }, + { + "name": "no-deps", + "literal": "1.1.0", "npm": { - "name": "lodash", - "version": "==1.1.1", + "name": "no-deps", + "version": "==1.1.0" }, "package_id": 3, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "56ec928a6d5f1d18236abc348bc711d6cfd08ca0a068bfc9fda24e7b22bed046", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ - 1, - 3, - ], - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 3, - "integrity": "sha512-SFeNKyKPh4kvYv0yd95fwLKw4JXM45PJLsPRdA8v7/q0lBzFeK6XS8xJTl6mlhb8PbAzioMkHli1W/1g0y4XQQ==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.1.1.tgz", "tag": "npm", - "value": "1.1.1", + "value": "1.1.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-1.1.0.tgz" }, - "scripts": {}, - }, + "dependencies": [], + "integrity": "sha512-ebG2pipYAKINcNI3YxdsiAgFvNGp2gdRwxAKN2LYBm9+YxuH/lHH2sl+GKQTuGiNfCfNZRMHUyyLPEJD6HWm7w==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: *-pre+build 1`] = ` +"{ + "format": "v2", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", + "package_index": { + "no-deps": [ + 1, + 3 + ], + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", - }, + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: *-pre+build 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, - "id": 2, + "id": 1 + }, + { + "name": "no-deps", "literal": "*-pre+build", - "name": "lodash", "npm": { - "name": "lodash", - "version": ">=0.0.0", + "name": "no-deps", + "version": ">=0.0.0" }, "package_id": 3, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ - 1, - 3, - ], - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "tag": "npm", - "value": "4.17.21", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" }, - "scripts": {}, - }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: *+build 1`] = ` +"{ + "format": "v2", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", + "package_index": { + "no-deps": [ + 1, + 3 + ], + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", - }, + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: *+build 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, - "id": 2, + "id": 1 + }, + { + "name": "no-deps", "literal": "*+build", - "name": "lodash", "npm": { - "name": "lodash", - "version": ">=0.0.0", + "name": "no-deps", + "version": ">=0.0.0" }, "package_id": 3, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ - 1, - 3, - ], - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "tag": "npm", - "value": "4.17.21", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" }, - "scripts": {}, - }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: latest 1`] = ` +"{ + "format": "v2", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", + "package_index": { + "no-deps": [ + 1, + 3 + ], + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", - }, + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: latest 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "latest", "dist_tag": { - "name": "lodash", - "tag": "lodash", + "name": "no-deps", + "tag": "no-deps" }, - "id": 2, - "literal": "latest", - "name": "lodash", "package_id": 3, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ - 1, - 3, - ], - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "tag": "npm", - "value": "4.17.21", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" }, - "scripts": {}, - }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: 1`] = ` +"{ + "format": "v2", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", + "package_index": { + "no-deps": [ + 1, + 3 + ], + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", - }, + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on workspace without version in package.json: version: 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "", "dist_tag": { - "name": "lodash", - "tag": "lodash", + "name": "no-deps", + "tag": "no-deps" }, - "id": 2, - "literal": "", - "name": "lodash", "package_id": 3, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ - 1, - 3, - ], - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "tag": "npm", - "value": "4.17.21", + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" }, - "scripts": {}, - }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on same name as workspace and dist-tag: with version 1`] = ` +"{ + "format": "v2", + "meta_hash": "c881b2c8cf6783504861587208d2b08d131130ff006987d527987075b04aa921", + "package_index": { + "no-deps": [ + 1, + 3 + ], + "foo": 0, + "bar": 2 + }, "trees": [ { + "id": 0, + "path": "node_modules", + "depth": 0, "dependencies": { "bar": { "id": 0, - "package_id": 2, + "package_id": 2 }, - "lodash": { + "no-deps": { "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", + "package_id": 1 + } + } }, { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, - }, - "depth": 1, "id": 1, "path": "node_modules/bar/node_modules", - }, + "depth": 1, + "dependencies": { + "no-deps": { + "id": 2, + "package_id": 3 + } + } + } ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", - }, - "workspace_versions": { - "11592711315645265694": "1.0.0", - }, -} -`; - -exports[`dependency on same name as workspace and dist-tag: with version 1`] = ` -{ "dependencies": [ { - "behavior": { - "workspace": true, - }, - "id": 0, - "literal": "packages/bar", "name": "bar", - "package_id": 2, + "literal": "packages/bar", "workspace": "packages/bar", - }, - { + "package_id": 2, "behavior": { - "workspace": true, + "workspace": true }, - "id": 1, - "literal": "packages/mono", - "name": "lodash", - "package_id": 1, - "workspace": "packages/mono", + "id": 0 }, { + "name": "no-deps", + "literal": "packages/mono", + "workspace": "packages/mono", + "package_id": 1, "behavior": { - "prod": true, - "workspace": true, + "workspace": true }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "latest", "dist_tag": { - "name": "lodash", - "tag": "lodash", + "name": "no-deps", + "tag": "no-deps" }, - "id": 2, - "literal": "latest", - "name": "lodash", "package_id": 3, - }, + "behavior": { + "prod": true, + "workspace": true + }, + "id": 2 + } ], - "format": "v2", - "meta_hash": "13e05e9c7522649464f47891db2c094497e8827d4a1f6784db8ef6c066211846", - "package_index": { - "bar": 2, - "foo": 0, - "lodash": [ - 1, - 3, - ], - }, "packages": [ { - "bin": null, - "dependencies": [ - 0, - 1, - ], "id": 0, - "integrity": null, - "man_dir": "", "name": "foo", "name_hash": "14841791273925386894", - "origin": "local", "resolution": { - "resolved": "", "tag": "root", "value": "", + "resolved": "" }, - "scripts": {}, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 1, - "integrity": null, - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "workspace:packages/mono", "tag": "workspace", "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" }, - "scripts": {}, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [ - 2, - ], "id": 2, - "integrity": null, - "man_dir": "", "name": "bar", "name_hash": "11592711315645265694", - "origin": "npm", "resolution": { - "resolved": "workspace:packages/bar", "tag": "workspace", "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" }, - "scripts": {}, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} }, { - "bin": null, - "dependencies": [], "id": 3, - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "man_dir": "", - "name": "lodash", - "name_hash": "15298228331728003776", - "origin": "npm", + "name": "no-deps", + "name_hash": "5128161233225832376", "resolution": { - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "tag": "npm", - "value": "4.17.21", - }, - "scripts": {}, - }, - ], - "trees": [ - { - "dependencies": { - "bar": { - "id": 0, - "package_id": 2, - }, - "lodash": { - "id": 1, - "package_id": 1, - }, - }, - "depth": 0, - "id": 0, - "path": "node_modules", - }, - { - "dependencies": { - "lodash": { - "id": 2, - "package_id": 3, - }, + "value": "2.0.0", + "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" }, - "depth": 1, - "id": 1, - "path": "node_modules/bar/node_modules", - }, + "dependencies": [], + "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } ], "workspace_paths": { "11592711315645265694": "packages/bar", - "15298228331728003776": "packages/mono", + "5128161233225832376": "packages/mono" }, "workspace_versions": { "11592711315645265694": "1.0.0", - "15298228331728003776": "4.17.21", - }, -} + "5128161233225832376": "4.17.21" + } +}" `; diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts index 89c4a9e312036d..d2deadd510e6cf 100644 --- a/test/cli/install/bun-add.test.ts +++ b/test/cli/install/bun-add.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test"; import { access, appendFile, copyFile, mkdir, readlink, rm, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins } from "harness"; +import { bunExe, bunEnv as env, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins, readdirSorted } from "harness"; import { join, relative, resolve } from "path"; import { check_npm_auth_type, @@ -11,7 +11,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, @@ -404,6 +403,165 @@ it("should add exact version with --exact", async () => { ); await access(join(package_dir, "bun.lockb")); }); +it("should add to devDependencies with --dev", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "--dev", "BaR"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed BaR@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/BaR`, `${root_url}/BaR-0.0.2.tgz`]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "BaR"]); + expect(await readdirSorted(join(package_dir, "node_modules", "BaR"))).toEqual(["package.json"]); + expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await file(join(package_dir, "package.json")).text()).toEqual( + JSON.stringify( + { + name: "foo", + version: "0.0.1", + devDependencies: { + BaR: "^0.0.2", + }, + }, + null, + 2, + ), + ); + await access(join(package_dir, "bun.lockb")); +}); +it("should add to optionalDependencies with --optional", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "--optional", "BaR"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed BaR@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/BaR`, `${root_url}/BaR-0.0.2.tgz`]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "BaR"]); + expect(await readdirSorted(join(package_dir, "node_modules", "BaR"))).toEqual(["package.json"]); + expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await file(join(package_dir, "package.json")).text()).toEqual( + JSON.stringify( + { + name: "foo", + version: "0.0.1", + optionalDependencies: { + BaR: "^0.0.2", + }, + }, + null, + 2, + ), + ); + await access(join(package_dir, "bun.lockb")); +}); +it("should add to peerDependencies with --peer", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "--peer", "BaR"], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed BaR@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/BaR`, `${root_url}/BaR-0.0.2.tgz`]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "BaR"]); + expect(await readdirSorted(join(package_dir, "node_modules", "BaR"))).toEqual(["package.json"]); + expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await file(join(package_dir, "package.json")).text()).toEqual( + JSON.stringify( + { + name: "foo", + version: "0.0.1", + peerDependencies: { + BaR: "^0.0.2", + }, + }, + null, + 2, + ), + ); + await access(join(package_dir, "bun.lockb")); +}); it("should add exact version with install.exact", async () => { const urls: string[] = []; diff --git a/test/cli/install/bun-install-lifecycle-scripts.test.ts b/test/cli/install/bun-install-lifecycle-scripts.test.ts new file mode 100644 index 00000000000000..44bcf1bb5c0b71 --- /dev/null +++ b/test/cli/install/bun-install-lifecycle-scripts.test.ts @@ -0,0 +1,2910 @@ +import { + VerdaccioRegistry, + isLinux, + bunEnv as env, + bunExe, + assertManifestsPopulated, + readdirSorted, + isWindows, + stderrForInstall, + runBunInstall, +} from "harness"; +import { beforeAll, afterAll, beforeEach, test, expect, describe, setDefaultTimeout } from "bun:test"; +import { writeFile, exists, rm, mkdir } from "fs/promises"; +import { join, sep } from "path"; +import { spawn, file, write } from "bun"; + +var verdaccio = new VerdaccioRegistry(); +var packageDir: string; +var packageJson: string; + +beforeAll(async () => { + setDefaultTimeout(1000 * 60 * 5); + await verdaccio.start(); +}); + +afterAll(() => { + verdaccio.stop(); +}); + +beforeEach(async () => { + ({ packageDir, packageJson } = await verdaccio.createTestDir()); + env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); + env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); +}); + +// waiter thread is only a thing on Linux. +for (const forceWaiterThread of isLinux ? [false, true] : [false]) { + describe("lifecycle scripts" + (forceWaiterThread ? " (waiter thread)" : ""), async () => { + test("root package with all lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + const writeScript = async (name: string) => { + const contents = ` + import { writeFileSync, existsSync, rmSync } from "fs"; + import { join } from "path"; + + const file = join(import.meta.dir, "${name}.txt"); + + if (existsSync(file)) { + rmSync(file); + writeFileSync(file, "${name} exists!"); + } else { + writeFileSync(file, "${name}!"); + } + `; + await writeFile(join(packageDir, `${name}.js`), contents); + }; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + preinstall: `${bunExe()} preinstall.js`, + install: `${bunExe()} install.js`, + postinstall: `${bunExe()} postinstall.js`, + preprepare: `${bunExe()} preprepare.js`, + prepare: `${bunExe()} prepare.js`, + postprepare: `${bunExe()} postprepare.js`, + }, + }), + ); + + await writeScript("preinstall"); + await writeScript("install"); + await writeScript("postinstall"); + await writeScript("preprepare"); + await writeScript("prepare"); + await writeScript("postprepare"); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue(); + expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(packageDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!"); + expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!"); + expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!"); + + // add a dependency with all lifecycle scripts + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + preinstall: `${bunExe()} preinstall.js`, + install: `${bunExe()} install.js`, + postinstall: `${bunExe()} postinstall.js`, + preprepare: `${bunExe()} preprepare.js`, + prepare: `${bunExe()} prepare.js`, + postprepare: `${bunExe()} postprepare.js`, + }, + dependencies: { + "all-lifecycle-scripts": "1.0.0", + }, + trustedDependencies: ["all-lifecycle-scripts"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ all-lifecycle-scripts@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall exists!"); + expect(await file(join(packageDir, "install.txt")).text()).toBe("install exists!"); + expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall exists!"); + expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare exists!"); + expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare exists!"); + expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare exists!"); + + const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts"); + + expect(await exists(join(depDir, "preinstall.txt"))).toBeTrue(); + expect(await exists(join(depDir, "install.txt"))).toBeTrue(); + expect(await exists(join(depDir, "postinstall.txt"))).toBeTrue(); + expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); + expect(await exists(join(depDir, "prepare.txt"))).toBeTrue(); + expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); + + expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); + + await rm(join(packageDir, "preinstall.txt")); + await rm(join(packageDir, "install.txt")); + await rm(join(packageDir, "postinstall.txt")); + await rm(join(packageDir, "preprepare.txt")); + await rm(join(packageDir, "prepare.txt")); + await rm(join(packageDir, "postprepare.txt")); + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + // all at once + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ all-lifecycle-scripts@1.0.0", + "", + "1 package installed", + ]); + + expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(packageDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!"); + expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!"); + expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!"); + + expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); + }); + + test("workspace lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + workspaces: ["packages/*"], + scripts: { + preinstall: `touch preinstall.txt`, + install: `touch install.txt`, + postinstall: `touch postinstall.txt`, + preprepare: `touch preprepare.txt`, + prepare: `touch prepare.txt`, + postprepare: `touch postprepare.txt`, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); + await writeFile( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + scripts: { + preinstall: `touch preinstall.txt`, + install: `touch install.txt`, + postinstall: `touch postinstall.txt`, + preprepare: `touch preprepare.txt`, + prepare: `touch prepare.txt`, + postprepare: `touch postprepare.txt`, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); + await writeFile( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + version: "1.0.0", + scripts: { + preinstall: `touch preinstall.txt`, + install: `touch install.txt`, + postinstall: `touch postinstall.txt`, + preprepare: `touch preprepare.txt`, + prepare: `touch prepare.txt`, + postprepare: `touch postprepare.txt`, + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "postinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "preprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "packages", "pkg1", "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1", "postprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "packages", "pkg2", "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg2", "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg2", "postinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg2", "preprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "packages", "pkg2", "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg2", "postprepare.txt"))).toBeFalse(); + }); + + test("dependency lifecycle scripts run before root lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const script = '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]'; + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin-slow": "1.0.0", + }, + trustedDependencies: ["uses-what-bin-slow"], + scripts: { + install: script, + postinstall: script, + preinstall: script, + prepare: script, + postprepare: script, + preprepare: script, + }, + }), + ); + + // uses-what-bin-slow will wait one second then write a file to disk. The root package should wait for + // for this to happen before running its lifecycle scripts. + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("install a dependency with lifecycle scripts, then add to trusted dependencies and install again", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "all-lifecycle-scripts": "1.0.0", + }, + trustedDependencies: [], + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ all-lifecycle-scripts@1.0.0", + "", + "1 package installed", + "", + "Blocked 3 postinstalls. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts"); + expect(await exists(join(depDir, "preinstall.txt"))).toBeFalse(); + expect(await exists(join(depDir, "install.txt"))).toBeFalse(); + expect(await exists(join(depDir, "postinstall.txt"))).toBeFalse(); + expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); + expect(await exists(join(depDir, "prepare.txt"))).toBeTrue(); + expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); + expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); + + // add to trusted dependencies + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "all-lifecycle-scripts": "1.0.0", + }, + trustedDependencies: ["all-lifecycle-scripts"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("Checked 1 install across 2 packages (no changes)"), + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); + expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); + expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); + expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); + expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); + expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); + }); + + test("adding a package without scripts to trustedDependencies", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "what-bin": "1.0.0", + }, + trustedDependencies: ["what-bin"], + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ what-bin@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); + const what_bin_bins = !isWindows ? ["what-bin"] : ["what-bin.bunx", "what-bin.exe"]; + // prettier-ignore + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { "what-bin": "1.0.0" }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ what-bin@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); + + // add it to trusted dependencies + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "what-bin": "1.0.0", + }, + trustedDependencies: ["what-bin"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); + }); + + test("lifecycle scripts run if node_modules is deleted", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "lifecycle-postinstall": "1.0.0", + }, + trustedDependencies: ["lifecycle-postinstall"], + }), + ); + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ lifecycle-postinstall@1.0.0", + "", + // @ts-ignore + "1 package installed", + ]); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + await rm(join(packageDir, "node_modules"), { force: true, recursive: true }); + await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ lifecycle-postinstall@1.0.0", + "", + "1 package installed", + ]); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("INIT_CWD is set to the correct directory", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + install: "bun install.js", + }, + dependencies: { + "lifecycle-init-cwd": "1.0.0", + "another-init-cwd": "npm:lifecycle-init-cwd@1.0.0", + }, + trustedDependencies: ["lifecycle-init-cwd", "another-init-cwd"], + }), + ); + + await writeFile( + join(packageDir, "install.js"), + ` + const fs = require("fs"); + const path = require("path"); + + fs.writeFileSync( + path.join(__dirname, "test.txt"), + process.env.INIT_CWD || "does not exist" + ); + `, + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + const out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ another-init-cwd@1.0.0", + "+ lifecycle-init-cwd@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "test.txt")).text()).toBe(packageDir); + expect(await file(join(packageDir, "node_modules/lifecycle-init-cwd/test.txt")).text()).toBe(packageDir); + expect(await file(join(packageDir, "node_modules/another-init-cwd/test.txt")).text()).toBe(packageDir); + }); + + test("failing lifecycle script should print output", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "lifecycle-failing-postinstall": "1.0.0", + }, + trustedDependencies: ["lifecycle-failing-postinstall"], + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("hello"); + expect(await exited).toBe(1); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + const out = await new Response(stdout).text(); + expect(out).toEqual(expect.stringContaining("bun install v1.")); + }); + + test("failing root lifecycle script should print output correctly", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "fooooooooo", + version: "1.0.0", + scripts: { + preinstall: `${bunExe()} -e "throw new Error('Oops!')"`, + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + expect(await exited).toBe(1); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await Bun.readableStreamToText(stdout)).toEqual(expect.stringContaining("bun install v1.")); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("error: Oops!"); + expect(err).toContain('error: preinstall script from "fooooooooo" exited with 1'); + }); + + test("exit 0 in lifecycle scripts works", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + postinstall: "exit 0", + prepare: "exit 0", + postprepare: "exit 0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("No packages! Deleted empty lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("done"), + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("--ignore-scripts should skip lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "lifecycle-failing-postinstall": "1.0.0", + }, + trustedDependencies: ["lifecycle-failing-postinstall"], + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--ignore-scripts"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("hello"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ lifecycle-failing-postinstall@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("it should add `node-gyp rebuild` as the `install` script when `install` and `postinstall` don't exist and `binding.gyp` exists in the root of the package", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "binding-gyp-scripts": "1.5.0", + }, + trustedDependencies: ["binding-gyp-scripts"], + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ binding-gyp-scripts@1.5.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules/binding-gyp-scripts/build.node"))).toBeTrue(); + }); + + test("automatic node-gyp scripts should not run for untrusted dependencies, and should run after adding to `trustedDependencies`", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const packageJSON: any = { + name: "foo", + version: "1.0.0", + dependencies: { + "binding-gyp-scripts": "1.5.0", + }, + }; + await writeFile(packageJson, JSON.stringify(packageJSON)); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + let err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ binding-gyp-scripts@1.5.0", + "", + "2 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeFalse(); + + packageJSON.trustedDependencies = ["binding-gyp-scripts"]; + await writeFile(packageJson, JSON.stringify(packageJSON)); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeTrue(); + }); + + test("automatic node-gyp scripts work in package root", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "node-gyp": "1.5.0", + }, + }), + ); + + await writeFile(join(packageDir, "binding.gyp"), ""); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ node-gyp@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "build.node"))).toBeTrue(); + + await rm(join(packageDir, "build.node")); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "build.node"))).toBeTrue(); + }); + + test("auto node-gyp scripts work when scripts exists other than `install` and `preinstall`", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "node-gyp": "1.5.0", + }, + scripts: { + postinstall: "exit 0", + prepare: "exit 0", + postprepare: "exit 0", + }, + }), + ); + + await writeFile(join(packageDir, "binding.gyp"), ""); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ node-gyp@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "build.node"))).toBeTrue(); + }); + + for (const script of ["install", "preinstall"]) { + test(`does not add auto node-gyp script when ${script} script exists`, async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const packageJSON: any = { + name: "foo", + version: "1.0.0", + dependencies: { + "node-gyp": "1.5.0", + }, + scripts: { + [script]: "exit 0", + }, + }; + await writeFile(packageJson, JSON.stringify(packageJSON)); + await writeFile(join(packageDir, "binding.gyp"), ""); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ node-gyp@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "build.node"))).toBeFalse(); + }); + } + + test("git dependencies also run `preprepare`, `prepare`, and `postprepare` scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + let err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ lifecycle-install-test@github:dylan-conway/lifecycle-install-test#3ba6af5", + "", + "1 package installed", + "", + "Blocked 6 postinstalls. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeFalse(); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee", + }, + trustedDependencies: ["lifecycle-install-test"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeTrue(); + }); + + test("root lifecycle scripts should wait for dependency lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin-slow": "1.0.0", + }, + trustedDependencies: ["uses-what-bin-slow"], + scripts: { + install: '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]', + }, + }), + ); + + // Package `uses-what-bin-slow` has an install script that will sleep for 1 second + // before writing `what-bin.txt` to disk. The root package has an install script that + // checks if this file exists. If the root package install script does not wait for + // the other to finish, it will fail. + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uses-what-bin-slow@1.0.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + async function createPackagesWithScripts( + packagesCount: number, + scripts: Record, + ): Promise { + const dependencies: Record = {}; + const dependenciesList: string[] = []; + + for (let i = 0; i < packagesCount; i++) { + const packageName: string = "stress-test-package-" + i; + const packageVersion = "1.0." + i; + + dependencies[packageName] = "file:./" + packageName; + dependenciesList[i] = packageName; + + const packagePath = join(packageDir, packageName); + await mkdir(packagePath); + await writeFile( + join(packagePath, "package.json"), + JSON.stringify({ + name: packageName, + version: packageVersion, + scripts, + }), + ); + } + + await writeFile( + packageJson, + JSON.stringify({ + name: "stress-test", + version: "1.0.0", + dependencies, + trustedDependencies: dependenciesList, + }), + ); + + return dependenciesList; + } + + test("reach max concurrent scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const scripts = { + "preinstall": `${bunExe()} -e 'Bun.sleepSync(500)'`, + }; + + const dependenciesList = await createPackagesWithScripts(4, scripts); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--concurrent-scripts=2"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("Blocked"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + ...dependenciesList.map(dep => `+ ${dep}@${dep}`), + "", + "4 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("stress test", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const dependenciesList = await createPackagesWithScripts(500, { + "postinstall": `${bunExe()} --version`, + }); + + // the script is quick, default number for max concurrent scripts + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("Blocked"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + ...dependenciesList.map(dep => `+ ${dep}@${dep}`).sort((a, b) => a.localeCompare(b)), + "", + "500 packages installed", + ]); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("it should install and use correct binary version", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + // this should install `what-bin` in two places: + // + // - node_modules/.bin/what-bin@1.5.0 + // - node_modules/uses-what-bin/node_modules/.bin/what-bin@1.0.0 + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin": "1.0.0", + "what-bin": "1.5.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "+ what-bin@1.5.0", + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain( + "what-bin@1.5.0", + ); + expect( + await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(), + ).toContain("what-bin@1.0.0"); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "uses-what-bin": "1.5.0", + "what-bin": "1.0.0", + }, + scripts: { + install: "what-bin", + }, + trustedDependencies: ["uses-what-bin"], + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain( + "what-bin@1.0.0", + ); + expect( + await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(), + ).toContain("what-bin@1.5.0"); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + out = await new Response(stdout).text(); + err = await new Response(stderr).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.5.0"), + expect.stringContaining("+ what-bin@1.0.0"), + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("node-gyp should always be available for lifecycle scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + install: "node-gyp --version", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await new Response(stderr).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + + // if node-gyp isn't available, it would return a non-zero exit code + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + // if this test fails, `electron` might be removed from the default list + test("default trusted dependencies should work", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "electron": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + "", + "1 package installed", + ]); + expect(out).not.toContain("Blocked"); + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("default trusted dependencies should not be used of trustedDependencies is populated", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "uses-what-bin": "1.0.0", + // fake electron package because it's in the default trustedDependencies list + "electron": "1.0.0", + }, + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + // electron lifecycle scripts should run, uses-what-bin scripts should not run + var err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "uses-what-bin": "1.0.0", + "electron": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }), + ); + + // now uses-what-bin scripts should run and electron scripts should not run. + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); + }); + + test("does not run any scripts if trustedDependencies is an empty list", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + "uses-what-bin": "1.0.0", + "electron": "1.0.0", + }, + trustedDependencies: [], + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + const out = await Bun.readableStreamToText(stdout); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 2 postinstalls. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); + }); + + test("will run default trustedDependencies after install that didn't include them", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + electron: "1.0.0", + }, + trustedDependencies: ["blah"], + }), + ); + + // first install does not run electron scripts + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + var err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + var out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + "", + "1 package installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + dependencies: { + electron: "1.0.0", + }, + }), + ); + + // The electron scripts should run now because it's in default trusted dependencies. + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + }); + + describe("--trust", async () => { + test("unhoisted untrusted scripts, none at root node_modules", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + // prevents real `uses-what-bin` from hoisting to root + "uses-what-bin": "npm:a-dep@1.0.3", + }, + workspaces: ["pkg1"], + }), + ), + write( + join(packageDir, "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ), + ]); + + await runBunInstall(testEnv, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + const results = await Promise.all([ + exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin")), + exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")), + ]); + + expect(results).toEqual([true, false]); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "trust", "--all"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env: testEnv, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + + expect(await exited).toBe(0); + + expect( + await exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")), + ).toBeTrue(); + }); + const trustTests = [ + { + label: "only name", + packageJson: { + name: "foo", + }, + }, + { + label: "empty dependencies", + packageJson: { + name: "foo", + dependencies: {}, + }, + }, + { + label: "populated dependencies", + packageJson: { + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }, + }, + + { + label: "empty trustedDependencies", + packageJson: { + name: "foo", + trustedDependencies: [], + }, + }, + + { + label: "populated dependencies, empty trustedDependencies", + packageJson: { + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: [], + }, + }, + + { + label: "populated dependencies and trustedDependencies", + packageJson: { + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }, + }, + + { + label: "empty dependencies and trustedDependencies", + packageJson: { + name: "foo", + dependencies: {}, + trustedDependencies: [], + }, + }, + ]; + for (const { label, packageJson } of trustTests) { + test(label, async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile(join(packageDir, "package.json"), JSON.stringify(packageJson)); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "--trust", "uses-what-bin@1.0.0"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed uses-what-bin@1.0.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await file(join(packageDir, "package.json")).json()).toEqual({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }); + + // another install should not error with json SyntaxError + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + }); + } + describe("packages without lifecycle scripts", async () => { + test("initial install", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "--trust", "no-deps@1.0.0"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + const err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + const out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + }); + }); + test("already installed", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + }), + ); + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "no-deps"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@2.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "^2.0.0", + }, + }); + + // oops, I wanted to run the lifecycle scripts for no-deps, I'll install + // again with --trust. + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "--trust", "no-deps"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + // oh, I didn't realize no-deps doesn't have + // any lifecycle scripts. It shouldn't automatically add to + // trustedDependencies. + + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@2.0.0", + "", + expect.stringContaining("done"), + "", + ]); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "^2.0.0", + }, + }); + }); + }); + }); + + describe("updating trustedDependencies", async () => { + test("existing trustedDependencies, unchanged trustedDependencies", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + trustedDependencies: ["uses-what-bin"], + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }); + + // no changes, lockfile shouldn't be saved + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("existing trustedDependencies, removing trustedDependencies", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + trustedDependencies: ["uses-what-bin"], + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }), + ); + + // this script should not run because uses-what-bin is no longer in trustedDependencies + await rm(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"), { force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "uses-what-bin": "1.0.0", + }, + }); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + }); + + test("non-existent trustedDependencies, then adding it", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "electron": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ electron@1.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "electron": "1.0.0", + }, + }); + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + trustedDependencies: ["electron"], + dependencies: { + "electron": "1.0.0", + }, + }), + ); + + await rm(join(packageDir, "node_modules", "electron", "preinstall.txt"), { force: true }); + + // lockfile should save evenn though there are no changes to trustedDependencies due to + // the default list + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); + }); + }); + + test("node -p should work in postinstall scripts", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + postinstall: `node -p "require('fs').writeFileSync('postinstall.txt', 'postinstall')"`, + }, + }), + ); + + const originalPath = env.PATH; + env.PATH = ""; + + let { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: testEnv, + }); + + env.PATH = originalPath; + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("No packages! Deleted empty lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); + }); + + test("ensureTempNodeGypScript works", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + preinstall: "node-gyp --version", + }, + }), + ); + + const originalPath = env.PATH; + env.PATH = ""; + + let { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + env, + }); + + env.PATH = originalPath; + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("No packages! Deleted empty lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("bun pm trust and untrusted on missing package", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "uses-what-bin": "1.5.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ uses-what-bin@1.5.0"), + "", + "2 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + // remove uses-what-bin from node_modules, bun pm trust and untrusted should handle missing package + await rm(join(packageDir, "node_modules", "uses-what-bin"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "untrusted"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("bun pm untrusted"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("Found 0 untrusted dependencies with scripts"); + expect(await exited).toBe(0); + + ({ stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "trust", "uses-what-bin"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + expect(await exited).toBe(1); + + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("bun pm trust"); + expect(err).toContain("0 scripts ran"); + expect(err).toContain("uses-what-bin"); + }); + + describe("add trusted, delete, then add again", async () => { + // when we change bun install to delete dependencies from node_modules + // for both cases, we need to update this test + for (const withRm of [true, false]) { + test(withRm ? "withRm" : "withoutRm", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + "uses-what-bin": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + let err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "3 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "trust", "uses-what-bin"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("1 script ran across 1 package"); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + "uses-what-bin": "1.0.0", + }, + trustedDependencies: ["uses-what-bin"], + }); + + // now remove and install again + if (withRm) { + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "rm", "uses-what-bin"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("1 package removed"); + expect(out).toContain("uses-what-bin"); + expect(await exited).toBe(0); + } + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + let expected = withRm + ? ["", "Checked 1 install across 2 packages (no changes)"] + : ["", expect.stringContaining("1 package removed")]; + expected = [expect.stringContaining("bun install v1."), ...expected]; + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual(expected); + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin"))).toBe(!withRm); + + // add again, bun pm untrusted should report it as untrusted + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + "uses-what-bin": "1.0.0", + }, + }), + ); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expected = withRm + ? [ + "", + expect.stringContaining("+ uses-what-bin@1.0.0"), + "", + "1 package installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ] + : ["", expect.stringContaining("Checked 3 installs across 4 packages (no changes)"), ""]; + expected = [expect.stringContaining("bun install v1."), ...expected]; + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual(expected); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "untrusted"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + })); + + err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("./node_modules/uses-what-bin @1.0.0".replaceAll("/", sep)); + expect(await exited).toBe(0); + }); + } + }); + + describe.if(!forceWaiterThread || process.platform === "linux")("does not use 100% cpu", async () => { + test("install", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + preinstall: `${bunExe()} -e 'Bun.sleepSync(1000)'`, + }, + }), + ); + + const proc = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + stdin: "ignore", + env: testEnv, + }); + + expect(await proc.exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000); + }); + + // https://github.com/oven-sh/bun/issues/11252 + test.todoIf(isWindows)("bun pm trust", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const dep = isWindows ? "uses-what-bin-slow-window" : "uses-what-bin-slow"; + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + [dep]: "1.0.0", + }, + }), + ); + + var { exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env: testEnv, + }); + + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeFalse(); + + const proc = spawn({ + cmd: [bunExe(), "pm", "trust", "--all"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env: testEnv, + }); + + expect(await proc.exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeTrue(); + + expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000 * (isWindows ? 5 : 1)); + }); + }); + }); + + describe("stdout/stderr is inherited from root scripts during install", async () => { + test("without packages", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const exe = bunExe().replace(/\\/g, "\\\\"); + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + scripts: { + "preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, + "install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, + "prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(err.split(/\r?\n/)).toEqual([ + "No packages! Deleted empty lockfile", + "", + `$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, + "preinstall stderr 🍦", + `$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, + `$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, + "", + ]); + const out = await Bun.readableStreamToText(stdout); + expect(out.split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "install stdout 🚀", + "prepare stdout done ✅", + "", + expect.stringContaining("done"), + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + + test("with a package", async () => { + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + + const exe = bunExe().replace(/\\/g, "\\\\"); + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.2.3", + scripts: { + "preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, + "install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, + "prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, + }, + dependencies: { + "no-deps": "1.0.0", + }, + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + + const err = stderrForInstall(await Bun.readableStreamToText(stderr)); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(err.split(/\r?\n/)).toEqual([ + "Resolving dependencies", + expect.stringContaining("Resolved, downloaded and extracted "), + "Saved lockfile", + "", + `$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, + "preinstall stderr 🍦", + `$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, + `$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, + "", + ]); + const out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "install stdout 🚀", + "prepare stdout done ✅", + "", + expect.stringContaining("+ no-deps@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + }); +} diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts similarity index 70% rename from test/cli/install/registry/bun-install-registry.test.ts rename to test/cli/install/bun-install-registry.test.ts index 846635dfd4adcd..99a3089e502c49 100644 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -1,17 +1,14 @@ import { file, spawn, write } from "bun"; import { install_test_helpers } from "bun:internal-for-testing"; import { afterAll, beforeAll, beforeEach, describe, expect, it, setDefaultTimeout, test } from "bun:test"; -import { ChildProcess, fork } from "child_process"; import { copyFileSync, mkdirSync } from "fs"; import { cp, exists, mkdir, readlink, rm, writeFile } from "fs/promises"; import { assertManifestsPopulated, bunExe, bunEnv as env, - isLinux, isWindows, mergeWindowEnvs, - randomPort, runBunInstall, runBunUpdate, pack, @@ -25,9 +22,10 @@ import { tls, isFlaky, isMacOS, + readdirSorted, + VerdaccioRegistry, } from "harness"; -import { join, resolve, sep } from "path"; -import { readdirSorted } from "../dummy.registry"; +import { join, resolve } from "path"; const { parseLockfile } = install_test_helpers; const { iniInternals } = require("bun:internal-for-testing"); const { loadNpmrc } = iniInternals; @@ -38,8 +36,8 @@ expect.extend({ toMatchNodeModulesAt, }); -var verdaccioServer: ChildProcess; -var port: number = randomPort(); +var verdaccio: VerdaccioRegistry; +var port: number; var packageDir: string; /** packageJson = join(packageDir, "package.json"); */ var packageJson: string; @@ -47,69 +45,28 @@ var packageJson: string; let users: Record = {}; beforeAll(async () => { - console.log("STARTING VERDACCIO"); setDefaultTimeout(1000 * 60 * 5); - verdaccioServer = fork( - require.resolve("verdaccio/bin/verdaccio"), - ["-c", join(import.meta.dir, "verdaccio.yaml"), "-l", `${port}`], - { - silent: true, - // Prefer using a release build of Bun since it's faster - execPath: Bun.which("bun") || bunExe(), - }, - ); - - verdaccioServer.stderr?.on("data", data => { - console.error(`Error: ${data}`); - }); - - verdaccioServer.on("error", error => { - console.error(`Failed to start child process: ${error}`); - }); - - verdaccioServer.on("exit", (code, signal) => { - if (code !== 0) { - console.error(`Child process exited with code ${code} and signal ${signal}`); - } else { - console.log("Child process exited successfully"); - } - }); - - await new Promise(done => { - verdaccioServer.on("message", (msg: { verdaccio_started: boolean }) => { - if (msg.verdaccio_started) { - console.log("Verdaccio started"); - done(); - } - }); - }); + verdaccio = new VerdaccioRegistry(); + port = verdaccio.port; + await verdaccio.start(); }); afterAll(async () => { await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); - if (verdaccioServer) verdaccioServer.kill(); + verdaccio.stop(); }); beforeEach(async () => { - packageDir = tmpdirSync(); - packageJson = join(packageDir, "package.json"); + ({ packageDir, packageJson } = await verdaccio.createTestDir()); await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); await Bun.$`rm -rf ${import.meta.dir}/packages/private-pkg-dont-touch`.throws(false); users = {}; env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); - await writeFile( - join(packageDir, "bunfig.toml"), - ` -[install] -cache = "${join(packageDir, ".bun-cache")}" -registry = "http://localhost:${port}/" -`, - ); }); function registryUrl() { - return `http://localhost:${port}/`; + return verdaccio.registryUrl(); } /** @@ -372,6 +329,7 @@ ${iniInner.join("\n")} const ini = /* ini */ ` registry = http://localhost:${port}/ +@needs-auth:registry=http://localhost:${port}/ //localhost:${port}/:_authToken=${await generateRegistryUser("bilbo_swaggins", "verysecure")} `; @@ -381,6 +339,7 @@ registry = http://localhost:${port}/ main: "index.js", version: "1.0.0", dependencies: { + "no-deps": "1.0.0", "@needs-auth/test-pkg": "1.0.0", }, "publishConfig": { @@ -549,7 +508,7 @@ describe("certificate authority", () => { const mockRegistryFetch = function (opts?: any): (req: Request) => Promise { return async function (req: Request) { if (req.url.includes("no-deps")) { - return new Response(Bun.file(join(import.meta.dir, "packages", "no-deps", "no-deps-1.0.0.tgz"))); + return new Response(Bun.file(join(import.meta.dir, "registry", "packages", "no-deps", "no-deps-1.0.0.tgz"))); } return new Response("OK", { status: 200 }); }; @@ -1036,7 +995,7 @@ describe("publish", async () => { cache = false registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; await Promise.all([ - rm(join(import.meta.dir, "packages", "otp-pkg-1"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "otp-pkg-1"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1068,7 +1027,7 @@ describe("publish", async () => { registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; await Promise.all([ - rm(join(import.meta.dir, "packages", "otp-pkg-2"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "otp-pkg-2"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1106,7 +1065,7 @@ describe("publish", async () => { registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; await Promise.all([ - rm(join(import.meta.dir, "packages", "otp-pkg-3"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "otp-pkg-3"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1149,7 +1108,7 @@ describe("publish", async () => { registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`; await Promise.all([ - rm(join(import.meta.dir, "packages", "otp-pkg-4"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "otp-pkg-4"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1175,7 +1134,7 @@ describe("publish", async () => { test("can publish a package then install it", async () => { const bunfig = await authBunfig("basic"); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-1"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-1"), { recursive: true, force: true }), write( packageJson, JSON.stringify({ @@ -1207,7 +1166,7 @@ describe("publish", async () => { }, }; await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-2"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-2"), { recursive: true, force: true }), write(packageJson, JSON.stringify(json)), write(join(packageDir, "bunfig.toml"), bunfig), ]); @@ -1223,7 +1182,7 @@ describe("publish", async () => { expect(await exists(join(packageDir, "node_modules", "publish-pkg-2", "package.json"))).toBeTrue(); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-2"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-2"), { recursive: true, force: true }), rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }), rm(join(packageDir, "node_modules"), { recursive: true, force: true }), ]); @@ -1249,7 +1208,7 @@ describe("publish", async () => { console.log({ packageDir, publishDir }); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-bins"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-bins"), { recursive: true, force: true }), write( join(publishDir, "package.json"), JSON.stringify({ @@ -1313,7 +1272,7 @@ describe("publish", async () => { const publishDir = tmpdirSync(); const bunfig = await authBunfig("manydeps"); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-deps"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-deps"), { recursive: true, force: true }), write( join(publishDir, "package.json"), JSON.stringify( @@ -1373,7 +1332,7 @@ describe("publish", async () => { }, }; await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-3"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-3"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1398,7 +1357,7 @@ describe("publish", async () => { test("does not publish", async () => { const bunfig = await authBunfig("dryrun"); await Promise.all([ - rm(join(import.meta.dir, "packages", "dry-run-1"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "dry-run-1"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1415,12 +1374,12 @@ describe("publish", async () => { const { out, err, exitCode } = await publish(env, packageDir, "--dry-run"); expect(exitCode).toBe(0); - expect(await exists(join(import.meta.dir, "packages", "dry-run-1"))).toBeFalse(); + expect(await exists(join(verdaccio.packagesPath, "dry-run-1"))).toBeFalse(); }); test("does not publish from tarball path", async () => { const bunfig = await authBunfig("dryruntarball"); await Promise.all([ - rm(join(import.meta.dir, "packages", "dry-run-2"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "dry-run-2"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1439,7 +1398,7 @@ describe("publish", async () => { const { out, err, exitCode } = await publish(env, packageDir, "./dry-run-2-2.2.2.tgz", "--dry-run"); expect(exitCode).toBe(0); - expect(await exists(join(import.meta.dir, "packages", "dry-run-2"))).toBeFalse(); + expect(await exists(join(verdaccio.packagesPath, "dry-run-2"))).toBeFalse(); }); }); @@ -1473,7 +1432,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; test(`should run in order${arg ? " (--dry-run)" : ""}`, async () => { const bunfig = await authBunfig("lifecycle" + (arg ? "dry" : "")); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-4"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-4"), { recursive: true, force: true }), write(packageJson, JSON.stringify(json)), write(join(packageDir, "script.js"), script), write(join(packageDir, "bunfig.toml"), bunfig), @@ -1505,7 +1464,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; test("--ignore-scripts", async () => { const bunfig = await authBunfig("ignorescripts"); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-5"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-5"), { recursive: true, force: true }), write(packageJson, JSON.stringify(json)), write(join(packageDir, "script.js"), script), write(join(packageDir, "bunfig.toml"), bunfig), @@ -1530,7 +1489,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; test("attempting to publish a private package should fail", async () => { const bunfig = await authBunfig("privatepackage"); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-6"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-6"), { recursive: true, force: true }), write( packageJson, JSON.stringify({ @@ -1549,7 +1508,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; let { out, err, exitCode } = await publish(env, packageDir); expect(exitCode).toBe(1); expect(err).toContain("error: attempted to publish a private package"); - expect(await exists(join(import.meta.dir, "packages", "publish-pkg-6-6.6.6.tgz"))).toBeFalse(); + expect(await exists(join(verdaccio.packagesPath, "publish-pkg-6-6.6.6.tgz"))).toBeFalse(); // try tarball await pack(packageDir, env); @@ -1563,7 +1522,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; test("--access", async () => { const bunfig = await authBunfig("accessflag"); await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-7"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-7"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1582,7 +1541,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; ({ out, err, exitCode } = await publish(env, packageDir, "--access", "public")); expect(exitCode).toBe(0); - expect(await exists(join(import.meta.dir, "packages", "publish-pkg-7"))).toBeTrue(); + expect(await exists(join(verdaccio.packagesPath, "publish-pkg-7"))).toBeTrue(); }); for (const access of ["restricted", "public"]) { @@ -1601,7 +1560,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; }; await Promise.all([ - rm(join(import.meta.dir, "packages", "@secret", "publish-pkg-8"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "@secret", "publish-pkg-8"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write(packageJson, JSON.stringify(pkgJson)), ]); @@ -1629,7 +1588,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; }, }; await Promise.all([ - rm(join(import.meta.dir, "packages", "publish-pkg-9"), { recursive: true, force: true }), + rm(join(verdaccio.packagesPath, "publish-pkg-9"), { recursive: true, force: true }), write(join(packageDir, "bunfig.toml"), bunfig), write(packageJson, JSON.stringify(pkgJson)), ]); @@ -1896,6 +1855,121 @@ describe("text lockfile", () => { ); }); } + + test("optionalPeers", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + peerDependencies: { + "no-deps": "1.0.0", + }, + peerDependenciesMeta: { + "no-deps": { + optional: true, + }, + }, + }), + ), + ]); + + let { exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeFalse(); + const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + // another install should recognize the peer dependency as `"optional": true` + ({ exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeFalse(); + expect((await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( + firstLockfile, + ); + }); +}); + +test("--lockfile-only", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "no-deps": "^1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "package1", + dependencies: { + "two-range-deps": "1.0.0", + }, + }), + ), + ]); + + let { exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile", "--lockfile-only"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules"))).toBeFalse(); + const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( + /localhost:\d+/g, + "localhost:1234", + ); + + // nothing changes with another --lockfile-only + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--lockfile-only"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + + expect(await exited).toBe(0); + expect(await exists(join(packageDir, "node_modules"))).toBeFalse(); + expect((await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( + firstLockfile, + ); }); describe("bundledDependencies", () => { @@ -3953,6 +4027,7 @@ describe("binaries", () => { "lockfileVersion": 0, "workspaces": { "": { + "name": "fooooo", "dependencies": { "fooooo": ".", // out of date, no no-deps @@ -5271,305 +5346,419 @@ describe("hoisting", async () => { }); }); -describe("workspaces", async () => { - test("adding packages in a subdirectory of a workspace", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "root", - workspaces: ["foo"], - }), - ); - - await mkdir(join(packageDir, "folder1")); - await mkdir(join(packageDir, "foo", "folder2"), { recursive: true }); - await writeFile( - join(packageDir, "foo", "package.json"), - JSON.stringify({ - name: "foo", - }), +describe("transitive file dependencies", () => { + async function checkHoistedFiles() { + const aliasedFileDepFilesPackageJson = join( + packageDir, + "node_modules", + "aliased-file-dep", + "node_modules", + "files", + "the-files", + "package.json", ); - - // add package to root workspace from `folder1` - let { stdout, exited } = spawn({ - cmd: [bunExe(), "add", "no-deps"], - cwd: join(packageDir, "folder1"), - stdout: "pipe", - stderr: "inherit", - env, - }); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@2.0.0", - "", - "2 packages installed", + const results = await Promise.all([ + exists(join(packageDir, "node_modules", "file-dep", "node_modules", "files", "package.json")), + readdirSorted(join(packageDir, "node_modules", "missing-file-dep", "node_modules")), + exists(join(packageDir, "node_modules", "aliased-file-dep", "package.json")), + isWindows + ? file(await readlink(aliasedFileDepFilesPackageJson)).json() + : file(aliasedFileDepFilesPackageJson).json(), + exists( + join(packageDir, "node_modules", "@scoped", "file-dep", "node_modules", "@scoped", "files", "package.json"), + ), + exists( + join( + packageDir, + "node_modules", + "@another-scope", + "file-dep", + "node_modules", + "@scoped", + "files", + "package.json", + ), + ), + exists(join(packageDir, "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json")), ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await file(packageJson).json()).toEqual({ - name: "root", - workspaces: ["foo"], - dependencies: { - "no-deps": "^2.0.0", + expect(results).toEqual([ + true, + [], + true, + { + "name": "files", + "version": "1.1.1", + "dependencies": { + "no-deps": "2.0.0", + }, }, - }); - - // add package to foo from `folder2` - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "add", "what-bin"], - cwd: join(packageDir, "foo", "folder2"), - stdout: "pipe", - stderr: "inherit", - env, - })); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed what-bin@1.5.0 with binaries:", - " - what-bin", - "", - "1 package installed", + true, + true, + true, ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + } - expect(await file(join(packageDir, "foo", "package.json")).json()).toEqual({ - name: "foo", - dependencies: { - "what-bin": "^1.5.0", - }, - }); + async function checkUnhoistedFiles() { + const results = await Promise.all([ + file(join(packageDir, "node_modules", "dep-file-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "file-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "missing-file-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "aliased-file-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "@scoped", "file-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "@another-scope", "file-dep", "package.json")).json(), + file(join(packageDir, "node_modules", "self-file-dep", "package.json")).json(), - // now delete node_modules and bun.lockb and install - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); + exists(join(packageDir, "pkg1", "node_modules", "file-dep", "node_modules", "files", "package.json")), // true + readdirSorted(join(packageDir, "pkg1", "node_modules", "missing-file-dep", "node_modules")), // [] + exists(join(packageDir, "pkg1", "node_modules", "aliased-file-dep")), // false + exists( + join( + packageDir, + "pkg1", + "node_modules", + "@scoped", + "file-dep", + "node_modules", + "@scoped", + "files", + "package.json", + ), + ), + exists( + join( + packageDir, + "pkg1", + "node_modules", + "@another-scope", + "file-dep", + "node_modules", + "@scoped", + "files", + "package.json", + ), + ), + exists( + join(packageDir, "pkg1", "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json"), + ), + readdirSorted(join(packageDir, "pkg1", "node_modules")), + ]); + + const expected = [ + ...(Array(7).fill({ name: "a-dep", version: "1.0.1" }) as any), + true, + [] as string[], + false, + true, + true, + true, + ["@another-scope", "@scoped", "dep-file-dep", "file-dep", "missing-file-dep", "self-file-dep"], + ]; + + // @ts-ignore + expect(results).toEqual(expected); + } + + test("from hoisted workspace dependencies", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["pkg1"], + }), + ), + write( + join(packageDir, "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + // hoisted + "dep-file-dep": "1.0.0", + // root + "file-dep": "1.0.0", + // dangling symlink + "missing-file-dep": "1.0.0", + // aliased. has `"file-dep": "file:."` + "aliased-file-dep": "npm:file-dep@1.0.1", + // scoped + "@scoped/file-dep": "1.0.0", + // scoped with different names + "@another-scope/file-dep": "1.0.0", + // file dependency on itself + "self-file-dep": "1.0.0", + }, + }), + ), + ]); + + var { out } = await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "folder1"), - stdout: "pipe", - stderr: "inherit", - env, - })); - out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", - "+ no-deps@2.0.0", - "", - "3 packages installed", + "14 packages installed", ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "foo", "no-deps", "what-bin"]); + await checkHoistedFiles(); + expect(await exists(join(packageDir, "pkg1", "node_modules"))).toBeFalse(); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "foo", "folder2"), - stdout: "pipe", - stderr: "inherit", - env, - })); - out = await Bun.readableStreamToText(stdout); + // reinstall + ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", - "+ what-bin@1.5.0", - "", - "3 packages installed", + "14 packages installed", ]); - expect(await exited).toBe(0); + + await checkHoistedFiles(); + + ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "foo", "no-deps", "what-bin"]); - }); - test("adding packages in workspaces", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "bar": "workspace:*", - }, - }), - ); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); - await mkdir(join(packageDir, "packages", "bar"), { recursive: true }); - await mkdir(join(packageDir, "packages", "boba")); - await mkdir(join(packageDir, "packages", "pkg5")); + await checkHoistedFiles(); - await writeFile(join(packageDir, "packages", "bar", "package.json"), JSON.stringify({ name: "bar" })); - await writeFile( - join(packageDir, "packages", "boba", "package.json"), - JSON.stringify({ name: "boba", version: "1.0.0", dependencies: { "pkg5": "*" } }), - ); - await writeFile( - join(packageDir, "packages", "pkg5", "package.json"), - JSON.stringify({ - name: "pkg5", - version: "1.2.3", - dependencies: { - "bar": "workspace:*", - }, - }), - ); + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { force: true }); - let { stdout, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "inherit", - env, - }); + // install from workspace + ({ out } = await runBunInstall(env, join(packageDir, "pkg1"))); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - let out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", - "+ bar@workspace:packages/bar", + "+ @another-scope/file-dep@1.0.0", + "+ @scoped/file-dep@1.0.0", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.0", + expect.stringContaining("+ file-dep@1.0.0"), + "+ missing-file-dep@1.0.0", + "+ self-file-dep@1.0.0", "", - "3 packages installed", + "14 packages installed", ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await exists(join(packageDir, "node_modules", "bar"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "boba"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "pkg5"))).toBeTrue(); + await checkHoistedFiles(); + expect(await exists(join(packageDir, "pkg1", "node_modules"))).toBeFalse(); - // add a package to the root workspace - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "add", "no-deps"], - cwd: packageDir, - stdout: "pipe", - stderr: "inherit", - env, - })); + ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@2.0.0", + expect.stringContaining("bun install v1."), "", "1 package installed", ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - bar: "workspace:*", - "no-deps": "^2.0.0", - }, - }); + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - // add a package in a workspace - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "add", "two-range-deps"], - cwd: join(packageDir, "packages", "boba"), - stdout: "pipe", - stderr: "inherit", - env, - })); + ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), + expect.stringContaining("bun install v1."), "", - "installed two-range-deps@1.0.0", + "+ @another-scope/file-dep@1.0.0", + "+ @scoped/file-dep@1.0.0", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.0", + expect.stringContaining("+ file-dep@1.0.0"), + "+ missing-file-dep@1.0.0", + "+ self-file-dep@1.0.0", "", - "3 packages installed", + "14 packages installed", ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }); - expect(await file(join(packageDir, "packages", "boba", "package.json")).json()).toEqual({ - name: "boba", - version: "1.0.0", - dependencies: { - "pkg5": "*", - "two-range-deps": "^1.0.0", - }, - }); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "@types", - "bar", - "boba", - "no-deps", - "pkg5", - "two-range-deps", + test("from non-hoisted workspace dependencies", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["pkg1"], + // these dependencies exist to make the workspace + // dependencies non-hoisted + dependencies: { + "dep-file-dep": "npm:a-dep@1.0.1", + "file-dep": "npm:a-dep@1.0.1", + "missing-file-dep": "npm:a-dep@1.0.1", + "aliased-file-dep": "npm:a-dep@1.0.1", + "@scoped/file-dep": "npm:a-dep@1.0.1", + "@another-scope/file-dep": "npm:a-dep@1.0.1", + "self-file-dep": "npm:a-dep@1.0.1", + }, + }), + ), + write( + join(packageDir, "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + // hoisted + "dep-file-dep": "1.0.0", + // root + "file-dep": "1.0.0", + // dangling symlink + "missing-file-dep": "1.0.0", + // aliased. has `"file-dep": "file:."` + "aliased-file-dep": "npm:file-dep@1.0.1", + // scoped + "@scoped/file-dep": "1.0.0", + // scoped with different names + "@another-scope/file-dep": "1.0.0", + // file dependency on itself + "self-file-dep": "1.0.0", + }, + }), + ), ]); - // add a dependency to a workspace with the same name as another workspace - ({ stdout, exited } = spawn({ - cmd: [bunExe(), "add", "bar@0.0.7"], - cwd: join(packageDir, "packages", "boba"), - stdout: "pipe", - stderr: "inherit", - env, - })); + var { out } = await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), + expect.stringContaining("bun install v1."), "", - "installed bar@0.0.7", + "+ @another-scope/file-dep@1.0.1", + "+ @scoped/file-dep@1.0.1", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.1", + expect.stringContaining("+ file-dep@1.0.1"), + "+ missing-file-dep@1.0.1", + "+ self-file-dep@1.0.1", "", - "1 package installed", + "13 packages installed", ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await file(join(packageDir, "packages", "boba", "package.json")).json()).toEqual({ - name: "boba", - version: "1.0.0", - dependencies: { - "pkg5": "*", - "two-range-deps": "^1.0.0", - "bar": "0.0.7", - }, - }); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "@types", - "bar", - "boba", - "no-deps", - "pkg5", - "two-range-deps", - ]); - expect(await file(join(packageDir, "node_modules", "boba", "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.7", - description: "not a workspace", - }); - }); - test("it should detect duplicate workspace dependencies", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - }), - ); + await checkUnhoistedFiles(); - await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); - await writeFile(join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1" })); - await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); - await writeFile(join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "pkg1" })); + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); + + // reinstall + ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @another-scope/file-dep@1.0.1", + "+ @scoped/file-dep@1.0.1", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.1", + expect.stringContaining("+ file-dep@1.0.1"), + "+ missing-file-dep@1.0.1", + "+ self-file-dep@1.0.1", + "", + "13 packages installed", + ]); + + await checkUnhoistedFiles(); + + ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + + await checkUnhoistedFiles(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { force: true }); + + // install from workspace + ({ out } = await runBunInstall(env, join(packageDir, "pkg1"))); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @another-scope/file-dep@1.0.0", + "+ @scoped/file-dep@1.0.0", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.0", + expect.stringContaining("+ file-dep@1.0.0"), + "+ missing-file-dep@1.0.0", + "+ self-file-dep@1.0.0", + "", + "13 packages installed", + ]); + + await checkUnhoistedFiles(); + + ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); + + ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @another-scope/file-dep@1.0.0", + "+ @scoped/file-dep@1.0.0", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.0", + expect.stringContaining("+ file-dep@1.0.0"), + "+ missing-file-dep@1.0.0", + "+ self-file-dep@1.0.0", + "", + "13 packages installed", + ]); + }); + + test("from root dependencies", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + // hoisted + "dep-file-dep": "1.0.0", + // root + "file-dep": "1.0.0", + // dangling symlink + "missing-file-dep": "1.0.0", + // aliased. has `"file-dep": "file:."` + "aliased-file-dep": "npm:file-dep@1.0.1", + // scoped + "@scoped/file-dep": "1.0.0", + // scoped with different names + "@another-scope/file-dep": "1.0.0", + // file dependency on itself + "self-file-dep": "1.0.0", + }, + }), + ); - var { stderr, exited } = spawn({ + var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", @@ -5578,898 +5767,1117 @@ describe("workspaces", async () => { env, }); - var err = await new Response(stderr).text(); - expect(err).toContain('Workspace name "pkg1" already exists'); - expect(await exited).toBe(1); + var err = await Bun.readableStreamToText(stderr); + var out = await Bun.readableStreamToText(stdout); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @another-scope/file-dep@1.0.0", + "+ @scoped/file-dep@1.0.0", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.0", + expect.stringContaining("+ file-dep@1.0.0"), + "+ missing-file-dep@1.0.0", + "+ self-file-dep@1.0.0", + "", + "13 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { force: true }); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "@another-scope", + "@scoped", + "aliased-file-dep", + "dep-file-dep", + "file-dep", + "missing-file-dep", + "self-file-dep", + ]); - ({ stderr, exited } = spawn({ + await checkHoistedFiles(); + + ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "pkg1"), + cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); - err = await new Response(stderr).text(); - expect(err).toContain('Workspace name "pkg1" already exists'); - expect(await exited).toBe(1); - }); + err = await Bun.readableStreamToText(stderr); + out = await Bun.readableStreamToText(stdout); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - const versions = ["workspace:1.0.0", "workspace:*", "workspace:^1.0.0", "1.0.0", "*"]; + await checkHoistedFiles(); - for (const rootVersion of versions) { - for (const packageVersion of versions) { - test(`it should allow duplicates, root@${rootVersion}, package@${packageVersion}`, async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - workspaces: ["packages/*"], - dependencies: { - pkg2: rootVersion, - }, - }), - ); + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await Bun.readableStreamToText(stderr); + out = await Bun.readableStreamToText(stdout); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "@another-scope", + "@scoped", + "aliased-file-dep", + "dep-file-dep", + "file-dep", + "missing-file-dep", + "self-file-dep", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); + await checkHoistedFiles(); + }); + test("it should install folder dependencies with absolute paths", async () => { + async function writePackages(num: number) { + await rm(join(packageDir, `pkg0`), { recursive: true, force: true }); + for (let i = 0; i < num; i++) { + await mkdir(join(packageDir, `pkg${i}`)); await writeFile( - join(packageDir, "packages", "pkg1", "package.json"), + join(packageDir, `pkg${i}`, "package.json"), JSON.stringify({ - name: "pkg1", - version: "1.0.0", - dependencies: { - pkg2: packageVersion, - }, + name: `pkg${i}`, + version: "1.1.1", }), ); + } + } - await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "pkg2", "package.json"), - JSON.stringify({ name: "pkg2", version: "1.0.0" }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); + await writePackages(2); - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ pkg2@workspace:packages/pkg2`, - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + // without and without file protocol + "pkg0": `file:${resolve(packageDir, "pkg0").replace(/\\/g, "\\\\")}`, + "pkg1": `${resolve(packageDir, "pkg1").replace(/\\/g, "\\\\")}`, + }, + }), + ); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "pkg1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + env, + }); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + var err = await Bun.readableStreamToText(stderr); + var out = await Bun.readableStreamToText(stdout); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ pkg0@pkg0", + "+ pkg1@pkg1", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["pkg0", "pkg1"]); + expect(await file(join(packageDir, "node_modules", "pkg0", "package.json")).json()).toEqual({ + name: "pkg0", + version: "1.1.1", + }); + expect(await file(join(packageDir, "node_modules", "pkg1", "package.json")).json()).toEqual({ + name: "pkg1", + version: "1.1.1", + }); + }); +}); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "pkg1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); +test("name from manifest is scoped and url encoded", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + // `name` in the manifest for these packages is manually changed + // to use `%40` and `%2f` + "@url/encoding.2": "1.0.1", + "@url/encoding.3": "1.0.1", + }, + }), + ); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ pkg2@workspace:packages/pkg2`, - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); + const files = await Promise.all([ + file(join(packageDir, "node_modules", "@url", "encoding.2", "package.json")).json(), + file(join(packageDir, "node_modules", "@url", "encoding.3", "package.json")).json(), + ]); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - } - } + expect(files).toEqual([ + { name: "@url/encoding.2", version: "1.0.1" }, + { name: "@url/encoding.3", version: "1.0.1" }, + ]); +}); - for (const version of versions) { - test(`it should allow listing workspace as dependency of the root package version ${version}`, async () => { - await writeFile( +describe("update", () => { + test("duplicate peer dependency (one package is invalid_package_id)", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "^1.0.0", + }, + peerDependencies: { + "no-deps": "^1.0.0", + }, + }), + ); + + await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "^1.1.0", + }, + peerDependencies: { + "no-deps": "^1.0.0", + }, + }); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "1.1.0", + }); + }); + test("dist-tags", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "a-dep": "latest", + }, + }), + ); + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ + name: "a-dep", + version: "1.0.10", + }); + + // Update without args, `latest` should stay + await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "latest", + }, + }); + + // Update with `a-dep` and `--latest`, `latest` should be replaced with the installed version + await runBunUpdate(env, packageDir, ["a-dep"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "^1.0.10", + }, + }); + await runBunUpdate(env, packageDir, ["--latest"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "^1.0.10", + }, + }); + }); + test("exact versions stay exact", async () => { + const runs = [ + { version: "1.0.1", dependency: "a-dep" }, + { version: "npm:a-dep@1.0.1", dependency: "aliased" }, + ]; + for (const { version, dependency } of runs) { + await write( packageJson, JSON.stringify({ name: "foo", - workspaces: ["packages/*"], dependencies: { - "workspace-1": version, + [dependency]: version, }, }), ); + async function check(version: string) { + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - await mkdir(join(packageDir, "packages", "workspace-1"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "workspace-1", "package.json"), + expect(await file(join(packageDir, "node_modules", dependency, "package.json")).json()).toMatchObject({ + name: "a-dep", + version: version.replace(/.*@/, ""), + }); + + expect(await file(packageJson).json()).toMatchObject({ + dependencies: { + [dependency]: version, + }, + }); + } + await runBunInstall(env, packageDir); + await check(version); + + await runBunUpdate(env, packageDir); + await check(version); + + await runBunUpdate(env, packageDir, [dependency]); + await check(version); + + // this will actually update the package, but the version should remain exact + await runBunUpdate(env, packageDir, ["--latest"]); + await check(dependency === "aliased" ? "npm:a-dep@1.0.10" : "1.0.10"); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + } + }); + describe("tilde", () => { + test("without args", async () => { + await write( + packageJson, JSON.stringify({ - name: "workspace-1", - version: "1.0.0", + name: "foo", + dependencies: { + "no-deps": "~1.0.0", + }, }), ); - // install first from the root, the workspace package - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + name: "no-deps", + version: "1.0.1", }); - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("already exists"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("Duplicate dependency"); - expect(err).not.toContain('workspace dependency "workspace-1" not found'); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ workspace-1@workspace:packages/workspace-1`, - "", - "1 package installed", - ]); - expect(await exited).toBe(0); + let { out } = await runBunUpdate(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ - name: "workspace-1", - version: "1.0.0", + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "~1.0.1", + }, }); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "workspace-1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); + // another update does not change anything (previously the version would update because it was changed to `^1.0.1`) + ({ out } = await runBunUpdate(env, packageDir)); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("already exists"); - expect(err).not.toContain("Duplicate dependency"); - expect(err).not.toContain('workspace dependency "workspace-1" not found'); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), + expect(out).toEqual([ + expect.stringContaining("bun update v1."), "", "Checked 1 install across 2 packages (no changes)", ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "no-deps": "~1.0.1", + }, + }); + }); - expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ - name: "workspace-1", - version: "1.0.0", + for (const latest of [true, false]) { + test(`update no args${latest ? " --latest" : ""}`, async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "a1": "npm:no-deps@1", + "a10": "npm:no-deps@~1.0", + "a11": "npm:no-deps@^1.0", + "a12": "npm:no-deps@~1.0.1", + "a13": "npm:no-deps@^1.0.1", + "a14": "npm:no-deps@~1.1.0", + "a15": "npm:no-deps@^1.1.0", + "a2": "npm:no-deps@1.0", + "a3": "npm:no-deps@1.1", + "a4": "npm:no-deps@1.0.1", + "a5": "npm:no-deps@1.1.0", + "a6": "npm:no-deps@~1", + "a7": "npm:no-deps@^1", + "a8": "npm:no-deps@~1.1", + "a9": "npm:no-deps@^1.1", + }, + }), + ); + + if (latest) { + await runBunUpdate(env, packageDir, ["--latest"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a1": "npm:no-deps@^2.0.0", + "a10": "npm:no-deps@~2.0.0", + "a11": "npm:no-deps@^2.0.0", + "a12": "npm:no-deps@~2.0.0", + "a13": "npm:no-deps@^2.0.0", + "a14": "npm:no-deps@~2.0.0", + "a15": "npm:no-deps@^2.0.0", + "a2": "npm:no-deps@~2.0.0", + "a3": "npm:no-deps@~2.0.0", + "a4": "npm:no-deps@2.0.0", + "a5": "npm:no-deps@2.0.0", + "a6": "npm:no-deps@~2.0.0", + "a7": "npm:no-deps@^2.0.0", + "a8": "npm:no-deps@~2.0.0", + "a9": "npm:no-deps@^2.0.0", + }, + }); + } else { + await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a1": "npm:no-deps@^1.1.0", + "a10": "npm:no-deps@~1.0.1", + "a11": "npm:no-deps@^1.1.0", + "a12": "npm:no-deps@~1.0.1", + "a13": "npm:no-deps@^1.1.0", + "a14": "npm:no-deps@~1.1.0", + "a15": "npm:no-deps@^1.1.0", + "a2": "npm:no-deps@~1.0.1", + "a3": "npm:no-deps@~1.1.0", + "a4": "npm:no-deps@1.0.1", + "a5": "npm:no-deps@1.1.0", + "a6": "npm:no-deps@~1.1.0", + "a7": "npm:no-deps@^1.1.0", + "a8": "npm:no-deps@~1.1.0", + "a9": "npm:no-deps@^1.1.0", + }, + }); + } + const files = await Promise.all( + ["a1", "a10", "a11", "a12", "a13", "a14", "a15", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9"].map(alias => + file(join(packageDir, "node_modules", alias, "package.json")).json(), + ), + ); + + if (latest) { + // each version should be "2.0.0" + expect(files).toMatchObject(Array(15).fill({ version: "2.0.0" })); + } else { + expect(files).toMatchObject([ + { version: "1.1.0" }, + { version: "1.0.1" }, + { version: "1.1.0" }, + { version: "1.0.1" }, + { version: "1.1.0" }, + { version: "1.1.0" }, + { version: "1.1.0" }, + { version: "1.0.1" }, + { version: "1.1.0" }, + { version: "1.0.1" }, + { version: "1.1.0" }, + { version: "1.1.0" }, + { version: "1.1.0" }, + { version: "1.1.0" }, + { version: "1.1.0" }, + ]); + } }); + } - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); + test("with package name in args", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "a-dep": "1.0.3", + "no-deps": "~1.0.0", + }, + }), + ); - // install from workspace package then from root - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "packages", "workspace-1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("already exists"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("Duplicate dependency"); - expect(err).not.toContain('workspace dependency "workspace-1" not found'); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ - name: "workspace-1", - version: "1.0.0", + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + name: "no-deps", + version: "1.0.1", }); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); + let { out } = await runBunUpdate(env, packageDir, ["no-deps"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("already exists"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("Duplicate dependency"); - expect(err).not.toContain('workspace dependency "workspace-1" not found'); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "installed no-deps@1.0.1", + "", + expect.stringContaining("done"), "", - "Checked 1 install across 2 packages (no changes)", ]); - expect(await exited).toBe(0); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "1.0.3", + "no-deps": "~1.0.1", + }, + }); + + // update with --latest should only change the update request and keep `~` + ({ out } = await runBunUpdate(env, packageDir, ["no-deps", "--latest"])); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ - name: "workspace-1", - version: "1.0.0", + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "installed no-deps@2.0.0", + "", + "1 package installed", + ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "1.0.3", + "no-deps": "~2.0.0", + }, }); }); - } -}); + }); + describe("alises", () => { + test("update all", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "aliased-dep": "npm:no-deps@^1.0.0", + }, + }), + ); -describe("transitive file dependencies", () => { - async function checkHoistedFiles() { - const aliasedFileDepFilesPackageJson = join( - packageDir, - "node_modules", - "aliased-file-dep", - "node_modules", - "files", - "the-files", - "package.json", - ); - const results = await Promise.all([ - exists(join(packageDir, "node_modules", "file-dep", "node_modules", "files", "package.json")), - readdirSorted(join(packageDir, "node_modules", "missing-file-dep", "node_modules")), - exists(join(packageDir, "node_modules", "aliased-file-dep", "package.json")), - isWindows - ? file(await readlink(aliasedFileDepFilesPackageJson)).json() - : file(aliasedFileDepFilesPackageJson).json(), - exists( - join(packageDir, "node_modules", "@scoped", "file-dep", "node_modules", "@scoped", "files", "package.json"), - ), - exists( - join( - packageDir, - "node_modules", - "@another-scope", - "file-dep", - "node_modules", - "@scoped", - "files", - "package.json", - ), - ), - exists(join(packageDir, "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json")), - ]); + await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(results).toEqual([ - true, - [], - true, - { - "name": "files", - "version": "1.1.1", - "dependencies": { - "no-deps": "2.0.0", + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "aliased-dep": "npm:no-deps@^1.1.0", }, - }, - true, - true, - true, - ]); - } - - async function checkUnhoistedFiles() { - const results = await Promise.all([ - file(join(packageDir, "node_modules", "dep-file-dep", "package.json")).json(), - file(join(packageDir, "node_modules", "file-dep", "package.json")).json(), - file(join(packageDir, "node_modules", "missing-file-dep", "package.json")).json(), - file(join(packageDir, "node_modules", "aliased-file-dep", "package.json")).json(), - file(join(packageDir, "node_modules", "@scoped", "file-dep", "package.json")).json(), - file(join(packageDir, "node_modules", "@another-scope", "file-dep", "package.json")).json(), - file(join(packageDir, "node_modules", "self-file-dep", "package.json")).json(), - - exists(join(packageDir, "pkg1", "node_modules", "file-dep", "node_modules", "files", "package.json")), // true - readdirSorted(join(packageDir, "pkg1", "node_modules", "missing-file-dep", "node_modules")), // [] - exists(join(packageDir, "pkg1", "node_modules", "aliased-file-dep")), // false - exists( - join( - packageDir, - "pkg1", - "node_modules", - "@scoped", - "file-dep", - "node_modules", - "@scoped", - "files", - "package.json", - ), - ), - exists( - join( - packageDir, - "pkg1", - "node_modules", - "@another-scope", - "file-dep", - "node_modules", - "@scoped", - "files", - "package.json", - ), - ), - exists( - join(packageDir, "pkg1", "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json"), - ), - readdirSorted(join(packageDir, "pkg1", "node_modules")), - ]); - - const expected = [ - ...(Array(7).fill({ name: "a-dep", version: "1.0.1" }) as any), - true, - [] as string[], - false, - true, - true, - true, - ["@another-scope", "@scoped", "dep-file-dep", "file-dep", "missing-file-dep", "self-file-dep"], - ]; - - // @ts-ignore - expect(results).toEqual(expected); - } - - test("from hoisted workspace dependencies", async () => { - await Promise.all([ - write( + }); + expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ + name: "no-deps", + version: "1.1.0", + }); + }); + test("update specific aliased package", async () => { + await write( packageJson, JSON.stringify({ name: "foo", - workspaces: ["pkg1"], + dependencies: { + "aliased-dep": "npm:no-deps@^1.0.0", + }, }), - ), - write( - join(packageDir, "pkg1", "package.json"), + ); + + await runBunUpdate(env, packageDir, ["aliased-dep"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "aliased-dep": "npm:no-deps@^1.1.0", + }, + }); + expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ + name: "no-deps", + version: "1.1.0", + }); + }); + test("with pre and build tags", async () => { + await write( + packageJson, JSON.stringify({ - name: "pkg1", + name: "foo", dependencies: { - // hoisted - "dep-file-dep": "1.0.0", - // root - "file-dep": "1.0.0", - // dangling symlink - "missing-file-dep": "1.0.0", - // aliased. has `"file-dep": "file:."` - "aliased-file-dep": "npm:file-dep@1.0.1", - // scoped - "@scoped/file-dep": "1.0.0", - // scoped with different names - "@another-scope/file-dep": "1.0.0", - // file dependency on itself - "self-file-dep": "1.0.0", + "aliased-dep": "npm:prereleases-3@5.0.0-alpha.150", }, }), - ), - ]); + ); - var { out } = await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "14 packages installed", - ]); + expect(await file(packageJson).json()).toMatchObject({ + name: "foo", + dependencies: { + "aliased-dep": "npm:prereleases-3@5.0.0-alpha.150", + }, + }); - await checkHoistedFiles(); - expect(await exists(join(packageDir, "pkg1", "node_modules"))).toBeFalse(); + expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ + name: "prereleases-3", + version: "5.0.0-alpha.150", + }); - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + const { out } = await runBunUpdate(env, packageDir, ["--latest"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - // reinstall - ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "^ aliased-dep 5.0.0-alpha.150 -> 5.0.0-alpha.153", + "", + "1 package installed", + ]); + expect(await file(packageJson).json()).toMatchObject({ + name: "foo", + dependencies: { + "aliased-dep": "npm:prereleases-3@5.0.0-alpha.153", + }, + }); + }); + }); + test("--no-save will update packages in node_modules and not save to package.json", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "a-dep": "1.0.1", + }, + }), + ); + + let { out } = await runBunUpdate(env, packageDir, ["--no-save"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), + expect(out).toEqual([ + expect.stringContaining("bun update v1."), "", - "14 packages installed", + expect.stringContaining("+ a-dep@1.0.1"), + "", + "1 package installed", ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "1.0.1", + }, + }); - await checkHoistedFiles(); + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "a-dep": "^1.0.1", + }, + }), + ); - ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + ({ out } = await runBunUpdate(env, packageDir, ["--no-save"])); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + expect.stringContaining("+ a-dep@1.0.10"), "", "1 package installed", ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "^1.0.1", + }, + }); - await checkHoistedFiles(); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { force: true }); - - // install from workspace - ({ out } = await runBunInstall(env, join(packageDir, "pkg1"))); + // now save + ({ out } = await runBunUpdate(env, packageDir)); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @another-scope/file-dep@1.0.0", - "+ @scoped/file-dep@1.0.0", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.0", - expect.stringContaining("+ file-dep@1.0.0"), - "+ missing-file-dep@1.0.0", - "+ self-file-dep@1.0.0", + expect(out).toEqual([ + expect.stringContaining("bun update v1."), "", - "14 packages installed", + "Checked 1 install across 2 packages (no changes)", ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "a-dep": "^1.0.10", + }, + }); + }); + test("update won't update beyond version range unless the specified version allows it", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "dep-with-tags": "^1.0.0", + }, + }), + ); - await checkHoistedFiles(); - expect(await exists(join(packageDir, "pkg1", "node_modules"))).toBeFalse(); - - ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); + await runBunUpdate(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "dep-with-tags": "^1.0.1", + }, + }); + expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ + version: "1.0.1", + }); + // update with package name does not update beyond version range + await runBunUpdate(env, packageDir, ["dep-with-tags"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "dep-with-tags": "^1.0.1", + }, + }); + expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ + version: "1.0.1", + }); - ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); + // now update with a higher version range + await runBunUpdate(env, packageDir, ["dep-with-tags@^2.0.0"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @another-scope/file-dep@1.0.0", - "+ @scoped/file-dep@1.0.0", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.0", - expect.stringContaining("+ file-dep@1.0.0"), - "+ missing-file-dep@1.0.0", - "+ self-file-dep@1.0.0", - "", - "14 packages installed", - ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + dependencies: { + "dep-with-tags": "^2.0.1", + }, + }); + expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ + version: "2.0.1", + }); }); + test("update should update all packages in the current workspace", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "what-bin": "^1.0.0", + "uses-what-bin": "^1.0.0", + "optional-native": "^1.0.0", + "peer-deps-too": "^1.0.0", + "two-range-deps": "^1.0.0", + "one-fixed-dep": "^1.0.0", + "no-deps-bins": "^2.0.0", + "left-pad": "^1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "^2.0.0", + "dev-deps": "1.0.0", + "a-dep": "^1.0.0", + }, + }), + ); - test("from non-hoisted workspace dependencies", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["pkg1"], - // these dependencies exist to make the workspace - // dependencies non-hoisted - dependencies: { - "dep-file-dep": "npm:a-dep@1.0.1", - "file-dep": "npm:a-dep@1.0.1", - "missing-file-dep": "npm:a-dep@1.0.1", - "aliased-file-dep": "npm:a-dep@1.0.1", - "@scoped/file-dep": "npm:a-dep@1.0.1", - "@another-scope/file-dep": "npm:a-dep@1.0.1", - "self-file-dep": "npm:a-dep@1.0.1", - }, - }), - ), - write( - join(packageDir, "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - dependencies: { - // hoisted - "dep-file-dep": "1.0.0", - // root - "file-dep": "1.0.0", - // dangling symlink - "missing-file-dep": "1.0.0", - // aliased. has `"file-dep": "file:."` - "aliased-file-dep": "npm:file-dep@1.0.1", - // scoped - "@scoped/file-dep": "1.0.0", - // scoped with different names - "@another-scope/file-dep": "1.0.0", - // file dependency on itself - "self-file-dep": "1.0.0", - }, - }), - ), - ]); + const originalWorkspaceJSON = { + name: "pkg1", + version: "1.0.0", + dependencies: { + "what-bin": "^1.0.0", + "uses-what-bin": "^1.0.0", + "optional-native": "^1.0.0", + "peer-deps-too": "^1.0.0", + "two-range-deps": "^1.0.0", + "one-fixed-dep": "^1.0.0", + "no-deps-bins": "^2.0.0", + "left-pad": "^1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "^2.0.0", + "dev-deps": "1.0.0", + "a-dep": "^1.0.0", + }, + }; - var { out } = await runBunInstall(env, packageDir); + await write(join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify(originalWorkspaceJSON)); + + // initial install, update root + let { out } = await runBunUpdate(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), + expect(out).toEqual([ + expect.stringContaining("bun update v1."), "", - "+ @another-scope/file-dep@1.0.1", - "+ @scoped/file-dep@1.0.1", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.1", - expect.stringContaining("+ file-dep@1.0.1"), - "+ missing-file-dep@1.0.1", - "+ self-file-dep@1.0.1", + "+ a-dep@1.0.10", + "+ dep-loop-entry@1.0.0", + expect.stringContaining("+ dep-with-tags@2.0.1"), + "+ dev-deps@1.0.0", + "+ left-pad@1.0.0", + "+ native@1.0.0", + "+ no-deps-bins@2.0.0", + expect.stringContaining("+ one-fixed-dep@1.0.0"), + "+ optional-native@1.0.0", + "+ peer-deps-too@1.0.0", + "+ two-range-deps@1.0.0", + expect.stringContaining("+ uses-what-bin@1.5.0"), + expect.stringContaining("+ what-bin@1.5.0"), + "", + // Due to optional-native dependency, this can be either 20 or 19 packages + expect.stringMatching(/(?:20|19) packages installed/), + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", - "13 packages installed", ]); - await checkUnhoistedFiles(); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); + let lockfile = parseLockfile(packageDir); + // make sure this is valid + expect(lockfile).toMatchNodeModulesAt(packageDir); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "what-bin": "^1.5.0", + "uses-what-bin": "^1.5.0", + "optional-native": "^1.0.0", + "peer-deps-too": "^1.0.0", + "two-range-deps": "^1.0.0", + "one-fixed-dep": "^1.0.0", + "no-deps-bins": "^2.0.0", + "left-pad": "^1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "^2.0.1", + "dev-deps": "1.0.0", + "a-dep": "^1.0.10", + }, + }); + // workspace hasn't changed + expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toEqual(originalWorkspaceJSON); - // reinstall - ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + // now update the workspace, first a couple packages, then all + ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), [ + "what-bin", + "uses-what-bin", + "a-dep@1.0.5", + ])); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), + expect(out).toEqual([ + expect.stringContaining("bun update v1."), "", - "+ @another-scope/file-dep@1.0.1", - "+ @scoped/file-dep@1.0.1", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.1", - expect.stringContaining("+ file-dep@1.0.1"), - "+ missing-file-dep@1.0.1", - "+ self-file-dep@1.0.1", + "installed what-bin@1.5.0 with binaries:", + " - what-bin", + "installed uses-what-bin@1.5.0", + "installed a-dep@1.0.5", "", - "13 packages installed", + "3 packages installed", ]); + // lockfile = parseLockfile(packageDir); + // expect(lockfile).toMatchNodeModulesAt(packageDir); + expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ + dependencies: { + "what-bin": "^1.5.0", + "uses-what-bin": "^1.5.0", + "optional-native": "^1.0.0", + "peer-deps-too": "^1.0.0", + "two-range-deps": "^1.0.0", + "one-fixed-dep": "^1.0.0", + "no-deps-bins": "^2.0.0", + "left-pad": "^1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "^2.0.0", + "dev-deps": "1.0.0", - await checkUnhoistedFiles(); + // a-dep should keep caret + "a-dep": "^1.0.5", + }, + }); - ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ + name: "a-dep", + version: "1.0.10", + }); + + expect( + await file(join(packageDir, "packages", "pkg1", "node_modules", "a-dep", "package.json")).json(), + ).toMatchObject({ + name: "a-dep", + version: "1.0.5", + }); + + ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), ["a-dep@^1.0.5"])); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "installed a-dep@1.0.10", + "", + expect.stringMatching(/(\[\d+\.\d+m?s\])/), "", - "1 package installed", ]); + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ + name: "a-dep", + version: "1.0.10", + }); + expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ + dependencies: { + "what-bin": "^1.5.0", + "uses-what-bin": "^1.5.0", + "optional-native": "^1.0.0", + "peer-deps-too": "^1.0.0", + "two-range-deps": "^1.0.0", + "one-fixed-dep": "^1.0.0", + "no-deps-bins": "^2.0.0", + "left-pad": "^1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "^2.0.0", + "dev-deps": "1.0.0", + "a-dep": "^1.0.10", + }, + }); + }); + test("update different dependency groups", async () => { + for (const args of [true, false]) { + for (const group of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) { + await write( + packageJson, + JSON.stringify({ + name: "foo", + [group]: { + "a-dep": "^1.0.0", + }, + }), + ); - await checkUnhoistedFiles(); + const { out } = args ? await runBunUpdate(env, packageDir, ["a-dep"]) : await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb"), { force: true }); + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + args ? "installed a-dep@1.0.10" : expect.stringContaining("+ a-dep@1.0.10"), + "", + "1 package installed", + ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + [group]: { + "a-dep": "^1.0.10", + }, + }); - // install from workspace - ({ out } = await runBunInstall(env, join(packageDir, "pkg1"))); + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + } + } + }); + test("it should update packages from update requests", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + workspaces: ["packages/*"], + }), + ); + + await write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "a-dep": "^1.0.0", + }, + }), + ); + + await write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "pkg1": "*", + "is-number": "*", + }, + }), + ); + + await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "1.0.0", + }); + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ + version: "1.0.10", + }); + expect(await file(join(packageDir, "node_modules", "pkg1", "package.json")).json()).toMatchObject({ + version: "1.0.0", + }); + + // update no-deps, no range, no change + let { out } = await runBunUpdate(env, packageDir, ["no-deps"]); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), "", - "+ @another-scope/file-dep@1.0.0", - "+ @scoped/file-dep@1.0.0", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.0", - expect.stringContaining("+ file-dep@1.0.0"), - "+ missing-file-dep@1.0.0", - "+ self-file-dep@1.0.0", + "installed no-deps@1.0.0", + "", + expect.stringMatching(/(\[\d+\.\d+m?s\])/), "", - "13 packages installed", ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "1.0.0", + }); - await checkUnhoistedFiles(); - - ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); + // update package that doesn't exist to workspace, should add to package.json + ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), ["no-deps"])); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "installed no-deps@2.0.0", "", "1 package installed", ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "1.0.0", + }); + expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "a-dep": "^1.0.0", + "no-deps": "^2.0.0", + }, + }); - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); + // update root package.json no-deps to ^1.0.0 and update it + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "^1.0.0", + }, + workspaces: ["packages/*"], + }), + ); - ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); + ({ out } = await runBunUpdate(env, packageDir, ["no-deps"])); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), + expect(out).toEqual([ + expect.stringContaining("bun update v1."), "", - "+ @another-scope/file-dep@1.0.0", - "+ @scoped/file-dep@1.0.0", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.0", - expect.stringContaining("+ file-dep@1.0.0"), - "+ missing-file-dep@1.0.0", - "+ self-file-dep@1.0.0", + "installed no-deps@1.1.0", "", - "13 packages installed", + "1 package installed", ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "1.1.0", + }); }); - test("from root dependencies", async () => { - await writeFile( + test("--latest works with packages from arguments", async () => { + await write( packageJson, JSON.stringify({ name: "foo", - version: "1.0.0", dependencies: { - // hoisted - "dep-file-dep": "1.0.0", - // root - "file-dep": "1.0.0", - // dangling symlink - "missing-file-dep": "1.0.0", - // aliased. has `"file-dep": "file:."` - "aliased-file-dep": "npm:file-dep@1.0.1", - // scoped - "@scoped/file-dep": "1.0.0", - // scoped with different names - "@another-scope/file-dep": "1.0.0", - // file dependency on itself - "self-file-dep": "1.0.0", + "no-deps": "1.0.0", }, }), ); - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - var err = await Bun.readableStreamToText(stderr); - var out = await Bun.readableStreamToText(stdout); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @another-scope/file-dep@1.0.0", - "+ @scoped/file-dep@1.0.0", - "+ aliased-file-dep@1.0.1", - "+ dep-file-dep@1.0.0", - expect.stringContaining("+ file-dep@1.0.0"), - "+ missing-file-dep@1.0.0", - "+ self-file-dep@1.0.0", - "", - "13 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "@another-scope", - "@scoped", - "aliased-file-dep", - "dep-file-dep", - "file-dep", - "missing-file-dep", - "self-file-dep", - ]); - - await checkHoistedFiles(); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await Bun.readableStreamToText(stderr); - out = await Bun.readableStreamToText(stdout); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await checkHoistedFiles(); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - - err = await Bun.readableStreamToText(stderr); - out = await Bun.readableStreamToText(stdout); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ - "@another-scope", - "@scoped", - "aliased-file-dep", - "dep-file-dep", - "file-dep", - "missing-file-dep", - "self-file-dep", - ]); - expect(await exited).toBe(0); + await runBunUpdate(env, packageDir, ["no-deps", "--latest"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - await checkHoistedFiles(); - }); - test("it should install folder dependencies with absolute paths", async () => { - async function writePackages(num: number) { - await rm(join(packageDir, `pkg0`), { recursive: true, force: true }); - for (let i = 0; i < num; i++) { - await mkdir(join(packageDir, `pkg${i}`)); - await writeFile( - join(packageDir, `pkg${i}`, "package.json"), - JSON.stringify({ - name: `pkg${i}`, - version: "1.1.1", - }), - ); - } - } - - await writePackages(2); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - // without and without file protocol - "pkg0": `file:${resolve(packageDir, "pkg0").replace(/\\/g, "\\\\")}`, - "pkg1": `${resolve(packageDir, "pkg1").replace(/\\/g, "\\\\")}`, - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env, - }); - - var err = await Bun.readableStreamToText(stderr); - var out = await Bun.readableStreamToText(stdout); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("panic:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ pkg0@pkg0", - "+ pkg1@pkg1", - "", - "2 packages installed", + const files = await Promise.all([ + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + file(packageJson).json(), ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["pkg0", "pkg1"]); - expect(await file(join(packageDir, "node_modules", "pkg0", "package.json")).json()).toEqual({ - name: "pkg0", - version: "1.1.1", - }); - expect(await file(join(packageDir, "node_modules", "pkg1", "package.json")).json()).toEqual({ - name: "pkg1", - version: "1.1.1", - }); + expect(files).toMatchObject([{ version: "2.0.0" }, { dependencies: { "no-deps": "2.0.0" } }]); }); }); -test("name from manifest is scoped and url encoded", async () => { +test("packages dependening on each other with aliases does not infinitely loop", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { - // `name` in the manifest for these packages is manually changed - // to use `%40` and `%2f` - "@url/encoding.2": "1.0.1", - "@url/encoding.3": "1.0.1", + "alias-loop-1": "1.0.0", + "alias-loop-2": "1.0.0", }, }), ); @@ -6478,4161 +6886,342 @@ test("name from manifest is scoped and url encoded", async () => { assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const files = await Promise.all([ - file(join(packageDir, "node_modules", "@url", "encoding.2", "package.json")).json(), - file(join(packageDir, "node_modules", "@url", "encoding.3", "package.json")).json(), + file(join(packageDir, "node_modules", "alias-loop-1", "package.json")).json(), + file(join(packageDir, "node_modules", "alias-loop-2", "package.json")).json(), + file(join(packageDir, "node_modules", "alias1", "package.json")).json(), + file(join(packageDir, "node_modules", "alias2", "package.json")).json(), ]); - - expect(files).toEqual([ - { name: "@url/encoding.2", version: "1.0.1" }, - { name: "@url/encoding.3", version: "1.0.1" }, + expect(files).toMatchObject([ + { name: "alias-loop-1", version: "1.0.0" }, + { name: "alias-loop-2", version: "1.0.0" }, + { name: "alias-loop-2", version: "1.0.0" }, + { name: "alias-loop-1", version: "1.0.0" }, ]); }); -describe("update", () => { - test("duplicate peer dependency (one package is invalid_package_id)", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "^1.0.0", - }, - peerDependencies: { - "no-deps": "^1.0.0", - }, - }), - ); - - await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ +test("it should re-populate .bin folder if package is reinstalled", async () => { + await writeFile( + packageJson, + JSON.stringify({ name: "foo", dependencies: { - "no-deps": "^1.1.0", - }, - peerDependencies: { - "no-deps": "^1.0.0", + "what-bin": "1.5.0", }, - }); + }), + ); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "1.1.0", - }); + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + stdin: "pipe", + env, }); - test("dist-tags", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "a-dep": "latest", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ - name: "a-dep", - version: "1.0.10", - }); - - // Update without args, `latest` should stay - await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ what-bin@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "latest", - }, - }); + const bin = process.platform === "win32" ? "what-bin.exe" : "what-bin"; + expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( + join(packageDir, "node_modules", ".bin", bin), + ); + if (process.platform === "win32") { + expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin(join("..", "what-bin", "what-bin.js")); + } else { + expect(await file(join(packageDir, "node_modules", ".bin", bin)).text()).toContain("what-bin@1.5.0"); + } - // Update with `a-dep` and `--latest`, `latest` should be replaced with the installed version - await runBunUpdate(env, packageDir, ["a-dep"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + await rm(join(packageDir, "node_modules", ".bin"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "what-bin", "package.json"), { recursive: true, force: true }); - expect(await file(packageJson).json()).toEqual({ + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + stdin: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ what-bin@1.5.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( + join(packageDir, "node_modules", ".bin", bin), + ); + if (process.platform === "win32") { + expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin(join("..", "what-bin", "what-bin.js")); + } else { + expect(await file(join(packageDir, "node_modules", ".bin", "what-bin")).text()).toContain("what-bin@1.5.0"); + } +}); + +test("one version with binary map", async () => { + await writeFile( + packageJson, + JSON.stringify({ name: "foo", dependencies: { - "a-dep": "^1.0.10", + "map-bin": "1.0.2", }, - }); - await runBunUpdate(env, packageDir, ["--latest"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }), + ); - expect(await file(packageJson).json()).toEqual({ + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + const out = await Bun.readableStreamToText(stdout); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ map-bin@1.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toHaveBins(["map-bin", "map_bin"]); + expect(join(packageDir, "node_modules", ".bin", "map-bin")).toBeValidBin(join("..", "map-bin", "bin", "map-bin")); + expect(join(packageDir, "node_modules", ".bin", "map_bin")).toBeValidBin(join("..", "map-bin", "bin", "map-bin")); +}); + +test("multiple versions with binary map", async () => { + await writeFile( + packageJson, + JSON.stringify({ name: "foo", + version: "1.2.3", dependencies: { - "a-dep": "^1.0.10", + "map-bin-multiple": "1.0.2", }, - }); + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, }); - test("exact versions stay exact", async () => { - const runs = [ - { version: "1.0.1", dependency: "a-dep" }, - { version: "npm:a-dep@1.0.1", dependency: "aliased" }, - ]; - for (const { version, dependency } of runs) { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - [dependency]: version, - }, - }), - ); - async function check(version: string) { - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await file(join(packageDir, "node_modules", dependency, "package.json")).json()).toMatchObject({ - name: "a-dep", - version: version.replace(/.*@/, ""), - }); + const err = await Bun.readableStreamToText(stderr); + const out = await Bun.readableStreamToText(stdout); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ map-bin-multiple@1.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await file(packageJson).json()).toMatchObject({ - dependencies: { - [dependency]: version, - }, - }); - } - await runBunInstall(env, packageDir); - await check(version); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toHaveBins(["map-bin", "map_bin"]); + expect(join(packageDir, "node_modules", ".bin", "map-bin")).toBeValidBin( + join("..", "map-bin-multiple", "bin", "map-bin"), + ); + expect(join(packageDir, "node_modules", ".bin", "map_bin")).toBeValidBin( + join("..", "map-bin-multiple", "bin", "map-bin"), + ); +}); - await runBunUpdate(env, packageDir); - await check(version); +test("duplicate dependency in optionalDependencies maintains sort order", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + // `duplicate-optional` has `no-deps` as a normal dependency (1.0.0) and as an + // optional dependency (1.0.1). The optional dependency version should be installed and + // the sort order should remain the same (tested by `bun-debug bun.lockb`). + "duplicate-optional": "1.0.1", + }, + }), + ); - await runBunUpdate(env, packageDir, [dependency]); - await check(version); + await runBunInstall(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - // this will actually update the package, but the version should remain exact - await runBunUpdate(env, packageDir, ["--latest"]); - await check(dependency === "aliased" ? "npm:a-dep@1.0.10" : "1.0.10"); + const lockfile = parseLockfile(packageDir); + expect(lockfile).toMatchNodeModulesAt(packageDir); - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - } + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ + version: "1.0.1", }); - describe("tilde", () => { - test("without args", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "~1.0.0", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - name: "no-deps", - version: "1.0.1", - }); - - let { out } = await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "~1.0.1", - }, - }); + const { stdout, exited } = spawn({ + cmd: [bunExe(), "bun.lockb"], + cwd: packageDir, + stderr: "inherit", + stdout: "pipe", + env, + }); - // another update does not change anything (previously the version would update because it was changed to `^1.0.1`) - ({ out } = await runBunUpdate(env, packageDir)); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + const out = await Bun.readableStreamToText(stdout); + expect(out.replaceAll(`${port}`, "4873")).toMatchSnapshot(); + expect(await exited).toBe(0); +}); - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "~1.0.1", - }, - }); - }); - - for (const latest of [true, false]) { - test(`update no args${latest ? " --latest" : ""}`, async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "a1": "npm:no-deps@1", - "a10": "npm:no-deps@~1.0", - "a11": "npm:no-deps@^1.0", - "a12": "npm:no-deps@~1.0.1", - "a13": "npm:no-deps@^1.0.1", - "a14": "npm:no-deps@~1.1.0", - "a15": "npm:no-deps@^1.1.0", - "a2": "npm:no-deps@1.0", - "a3": "npm:no-deps@1.1", - "a4": "npm:no-deps@1.0.1", - "a5": "npm:no-deps@1.1.0", - "a6": "npm:no-deps@~1", - "a7": "npm:no-deps@^1", - "a8": "npm:no-deps@~1.1", - "a9": "npm:no-deps@^1.1", - }, - }), - ); - - if (latest) { - await runBunUpdate(env, packageDir, ["--latest"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a1": "npm:no-deps@^2.0.0", - "a10": "npm:no-deps@~2.0.0", - "a11": "npm:no-deps@^2.0.0", - "a12": "npm:no-deps@~2.0.0", - "a13": "npm:no-deps@^2.0.0", - "a14": "npm:no-deps@~2.0.0", - "a15": "npm:no-deps@^2.0.0", - "a2": "npm:no-deps@~2.0.0", - "a3": "npm:no-deps@~2.0.0", - "a4": "npm:no-deps@2.0.0", - "a5": "npm:no-deps@2.0.0", - "a6": "npm:no-deps@~2.0.0", - "a7": "npm:no-deps@^2.0.0", - "a8": "npm:no-deps@~2.0.0", - "a9": "npm:no-deps@^2.0.0", - }, - }); - } else { - await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a1": "npm:no-deps@^1.1.0", - "a10": "npm:no-deps@~1.0.1", - "a11": "npm:no-deps@^1.1.0", - "a12": "npm:no-deps@~1.0.1", - "a13": "npm:no-deps@^1.1.0", - "a14": "npm:no-deps@~1.1.0", - "a15": "npm:no-deps@^1.1.0", - "a2": "npm:no-deps@~1.0.1", - "a3": "npm:no-deps@~1.1.0", - "a4": "npm:no-deps@1.0.1", - "a5": "npm:no-deps@1.1.0", - "a6": "npm:no-deps@~1.1.0", - "a7": "npm:no-deps@^1.1.0", - "a8": "npm:no-deps@~1.1.0", - "a9": "npm:no-deps@^1.1.0", - }, - }); - } - const files = await Promise.all( - ["a1", "a10", "a11", "a12", "a13", "a14", "a15", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9"].map(alias => - file(join(packageDir, "node_modules", alias, "package.json")).json(), - ), - ); - - if (latest) { - // each version should be "2.0.0" - expect(files).toMatchObject(Array(15).fill({ version: "2.0.0" })); - } else { - expect(files).toMatchObject([ - { version: "1.1.0" }, - { version: "1.0.1" }, - { version: "1.1.0" }, - { version: "1.0.1" }, - { version: "1.1.0" }, - { version: "1.1.0" }, - { version: "1.1.0" }, - { version: "1.0.1" }, - { version: "1.1.0" }, - { version: "1.0.1" }, - { version: "1.1.0" }, - { version: "1.1.0" }, - { version: "1.1.0" }, - { version: "1.1.0" }, - { version: "1.1.0" }, - ]); - } - }); - } - - test("with package name in args", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "a-dep": "1.0.3", - "no-deps": "~1.0.0", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - name: "no-deps", - version: "1.0.1", - }); - - let { out } = await runBunUpdate(env, packageDir, ["no-deps"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed no-deps@1.0.1", - "", - expect.stringContaining("done"), - "", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "1.0.3", - "no-deps": "~1.0.1", - }, - }); - - // update with --latest should only change the update request and keep `~` - ({ out } = await runBunUpdate(env, packageDir, ["no-deps", "--latest"])); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed no-deps@2.0.0", - "", - "1 package installed", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "1.0.3", - "no-deps": "~2.0.0", - }, - }); - }); - }); - describe("alises", () => { - test("update all", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "aliased-dep": "npm:no-deps@^1.0.0", - }, - }), - ); - - await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "aliased-dep": "npm:no-deps@^1.1.0", - }, - }); - expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ - name: "no-deps", - version: "1.1.0", - }); - }); - test("update specific aliased package", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "aliased-dep": "npm:no-deps@^1.0.0", - }, - }), - ); - - await runBunUpdate(env, packageDir, ["aliased-dep"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "aliased-dep": "npm:no-deps@^1.1.0", - }, - }); - expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ - name: "no-deps", - version: "1.1.0", - }); - }); - test("with pre and build tags", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "aliased-dep": "npm:prereleases-3@5.0.0-alpha.150", - }, - }), - ); - - await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toMatchObject({ - name: "foo", - dependencies: { - "aliased-dep": "npm:prereleases-3@5.0.0-alpha.150", - }, - }); - - expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ - name: "prereleases-3", - version: "5.0.0-alpha.150", - }); - - const { out } = await runBunUpdate(env, packageDir, ["--latest"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "^ aliased-dep 5.0.0-alpha.150 -> 5.0.0-alpha.153", - "", - "1 package installed", - ]); - expect(await file(packageJson).json()).toMatchObject({ - name: "foo", - dependencies: { - "aliased-dep": "npm:prereleases-3@5.0.0-alpha.153", - }, - }); - }); - }); - test("--no-save will update packages in node_modules and not save to package.json", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "a-dep": "1.0.1", - }, - }), - ); - - let { out } = await runBunUpdate(env, packageDir, ["--no-save"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - expect.stringContaining("+ a-dep@1.0.1"), - "", - "1 package installed", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "1.0.1", - }, - }); - - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "a-dep": "^1.0.1", - }, - }), - ); - - ({ out } = await runBunUpdate(env, packageDir, ["--no-save"])); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - expect.stringContaining("+ a-dep@1.0.10"), - "", - "1 package installed", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "^1.0.1", - }, - }); - - // now save - ({ out } = await runBunUpdate(env, packageDir)); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "a-dep": "^1.0.10", - }, - }); - }); - test("update won't update beyond version range unless the specified version allows it", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "dep-with-tags": "^1.0.0", - }, - }), - ); - - await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "dep-with-tags": "^1.0.1", - }, - }); - expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ - version: "1.0.1", - }); - // update with package name does not update beyond version range - await runBunUpdate(env, packageDir, ["dep-with-tags"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "dep-with-tags": "^1.0.1", - }, - }); - expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ - version: "1.0.1", - }); - - // now update with a higher version range - await runBunUpdate(env, packageDir, ["dep-with-tags@^2.0.0"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "dep-with-tags": "^2.0.1", - }, - }); - expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ - version: "2.0.1", - }); - }); - test("update should update all packages in the current workspace", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "what-bin": "^1.0.0", - "uses-what-bin": "^1.0.0", - "optional-native": "^1.0.0", - "peer-deps-too": "^1.0.0", - "two-range-deps": "^1.0.0", - "one-fixed-dep": "^1.0.0", - "no-deps-bins": "^2.0.0", - "left-pad": "^1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "^2.0.0", - "dev-deps": "1.0.0", - "a-dep": "^1.0.0", - }, - }), - ); - - const originalWorkspaceJSON = { - name: "pkg1", - version: "1.0.0", - dependencies: { - "what-bin": "^1.0.0", - "uses-what-bin": "^1.0.0", - "optional-native": "^1.0.0", - "peer-deps-too": "^1.0.0", - "two-range-deps": "^1.0.0", - "one-fixed-dep": "^1.0.0", - "no-deps-bins": "^2.0.0", - "left-pad": "^1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "^2.0.0", - "dev-deps": "1.0.0", - "a-dep": "^1.0.0", - }, - }; - - await write(join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify(originalWorkspaceJSON)); - - // initial install, update root - let { out } = await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "+ a-dep@1.0.10", - "+ dep-loop-entry@1.0.0", - expect.stringContaining("+ dep-with-tags@2.0.1"), - "+ dev-deps@1.0.0", - "+ left-pad@1.0.0", - "+ native@1.0.0", - "+ no-deps-bins@2.0.0", - expect.stringContaining("+ one-fixed-dep@1.0.0"), - "+ optional-native@1.0.0", - "+ peer-deps-too@1.0.0", - "+ two-range-deps@1.0.0", - expect.stringContaining("+ uses-what-bin@1.5.0"), - expect.stringContaining("+ what-bin@1.5.0"), - "", - // Due to optional-native dependency, this can be either 20 or 19 packages - expect.stringMatching(/(?:20|19) packages installed/), - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - - let lockfile = parseLockfile(packageDir); - // make sure this is valid - expect(lockfile).toMatchNodeModulesAt(packageDir); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "what-bin": "^1.5.0", - "uses-what-bin": "^1.5.0", - "optional-native": "^1.0.0", - "peer-deps-too": "^1.0.0", - "two-range-deps": "^1.0.0", - "one-fixed-dep": "^1.0.0", - "no-deps-bins": "^2.0.0", - "left-pad": "^1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "^2.0.1", - "dev-deps": "1.0.0", - "a-dep": "^1.0.10", - }, - }); - // workspace hasn't changed - expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toEqual(originalWorkspaceJSON); - - // now update the workspace, first a couple packages, then all - ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), [ - "what-bin", - "uses-what-bin", - "a-dep@1.0.5", - ])); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed what-bin@1.5.0 with binaries:", - " - what-bin", - "installed uses-what-bin@1.5.0", - "installed a-dep@1.0.5", - "", - "3 packages installed", - ]); - // lockfile = parseLockfile(packageDir); - // expect(lockfile).toMatchNodeModulesAt(packageDir); - expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ - dependencies: { - "what-bin": "^1.5.0", - "uses-what-bin": "^1.5.0", - "optional-native": "^1.0.0", - "peer-deps-too": "^1.0.0", - "two-range-deps": "^1.0.0", - "one-fixed-dep": "^1.0.0", - "no-deps-bins": "^2.0.0", - "left-pad": "^1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "^2.0.0", - "dev-deps": "1.0.0", - - // a-dep should keep caret - "a-dep": "^1.0.5", - }, - }); - - expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ - name: "a-dep", - version: "1.0.10", - }); - - expect( - await file(join(packageDir, "packages", "pkg1", "node_modules", "a-dep", "package.json")).json(), - ).toMatchObject({ - name: "a-dep", - version: "1.0.5", - }); - - ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), ["a-dep@^1.0.5"])); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed a-dep@1.0.10", - "", - expect.stringMatching(/(\[\d+\.\d+m?s\])/), - "", - ]); - expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ - name: "a-dep", - version: "1.0.10", - }); - expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ - dependencies: { - "what-bin": "^1.5.0", - "uses-what-bin": "^1.5.0", - "optional-native": "^1.0.0", - "peer-deps-too": "^1.0.0", - "two-range-deps": "^1.0.0", - "one-fixed-dep": "^1.0.0", - "no-deps-bins": "^2.0.0", - "left-pad": "^1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "^2.0.0", - "dev-deps": "1.0.0", - "a-dep": "^1.0.10", - }, - }); - }); - test("update different dependency groups", async () => { - for (const args of [true, false]) { - for (const group of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) { - await write( - packageJson, - JSON.stringify({ - name: "foo", - [group]: { - "a-dep": "^1.0.0", - }, - }), - ); - - const { out } = args ? await runBunUpdate(env, packageDir, ["a-dep"]) : await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - args ? "installed a-dep@1.0.10" : expect.stringContaining("+ a-dep@1.0.10"), - "", - "1 package installed", - ]); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - [group]: { - "a-dep": "^1.0.10", - }, - }); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - } - } - }); - test("it should update packages from update requests", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - workspaces: ["packages/*"], - }), - ); - - await write( - join(packageDir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - version: "1.0.0", - dependencies: { - "a-dep": "^1.0.0", - }, - }), - ); - - await write( - join(packageDir, "packages", "pkg2", "package.json"), - JSON.stringify({ - name: "pkg2", - dependencies: { - "pkg1": "*", - "is-number": "*", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "1.0.0", - }); - expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ - version: "1.0.10", - }); - expect(await file(join(packageDir, "node_modules", "pkg1", "package.json")).json()).toMatchObject({ - version: "1.0.0", - }); - - // update no-deps, no range, no change - let { out } = await runBunUpdate(env, packageDir, ["no-deps"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed no-deps@1.0.0", - "", - expect.stringMatching(/(\[\d+\.\d+m?s\])/), - "", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "1.0.0", - }); - - // update package that doesn't exist to workspace, should add to package.json - ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), ["no-deps"])); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed no-deps@2.0.0", - "", - "1 package installed", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "1.0.0", - }); - expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ - name: "pkg1", - version: "1.0.0", - dependencies: { - "a-dep": "^1.0.0", - "no-deps": "^2.0.0", - }, - }); - - // update root package.json no-deps to ^1.0.0 and update it - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "^1.0.0", - }, - workspaces: ["packages/*"], - }), - ); - - ({ out } = await runBunUpdate(env, packageDir, ["no-deps"])); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - "installed no-deps@1.1.0", - "", - "1 package installed", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "1.1.0", - }); - }); - - test("--latest works with packages from arguments", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - }), - ); - - await runBunUpdate(env, packageDir, ["no-deps", "--latest"]); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const files = await Promise.all([ - file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), - file(packageJson).json(), - ]); - - expect(files).toMatchObject([{ version: "2.0.0" }, { dependencies: { "no-deps": "2.0.0" } }]); - }); -}); - -test("packages dependening on each other with aliases does not infinitely loop", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "alias-loop-1": "1.0.0", - "alias-loop-2": "1.0.0", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const files = await Promise.all([ - file(join(packageDir, "node_modules", "alias-loop-1", "package.json")).json(), - file(join(packageDir, "node_modules", "alias-loop-2", "package.json")).json(), - file(join(packageDir, "node_modules", "alias1", "package.json")).json(), - file(join(packageDir, "node_modules", "alias2", "package.json")).json(), - ]); - expect(files).toMatchObject([ - { name: "alias-loop-1", version: "1.0.0" }, - { name: "alias-loop-2", version: "1.0.0" }, - { name: "alias-loop-2", version: "1.0.0" }, - { name: "alias-loop-1", version: "1.0.0" }, - ]); -}); - -test("it should re-populate .bin folder if package is reinstalled", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "what-bin": "1.5.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - stdin: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ what-bin@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const bin = process.platform === "win32" ? "what-bin.exe" : "what-bin"; - expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( - join(packageDir, "node_modules", ".bin", bin), - ); - if (process.platform === "win32") { - expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin(join("..", "what-bin", "what-bin.js")); - } else { - expect(await file(join(packageDir, "node_modules", ".bin", bin)).text()).toContain("what-bin@1.5.0"); - } - - await rm(join(packageDir, "node_modules", ".bin"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "what-bin", "package.json"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - stdin: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ what-bin@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( - join(packageDir, "node_modules", ".bin", bin), - ); - if (process.platform === "win32") { - expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin(join("..", "what-bin", "what-bin.js")); - } else { - expect(await file(join(packageDir, "node_modules", ".bin", "what-bin")).text()).toContain("what-bin@1.5.0"); - } -}); - -test("one version with binary map", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "map-bin": "1.0.2", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - env, - }); - - const err = await Bun.readableStreamToText(stderr); - const out = await Bun.readableStreamToText(stdout); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ map-bin@1.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toHaveBins(["map-bin", "map_bin"]); - expect(join(packageDir, "node_modules", ".bin", "map-bin")).toBeValidBin(join("..", "map-bin", "bin", "map-bin")); - expect(join(packageDir, "node_modules", ".bin", "map_bin")).toBeValidBin(join("..", "map-bin", "bin", "map-bin")); -}); - -test("multiple versions with binary map", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "map-bin-multiple": "1.0.2", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - env, - }); - - const err = await Bun.readableStreamToText(stderr); - const out = await Bun.readableStreamToText(stdout); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ map-bin-multiple@1.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toHaveBins(["map-bin", "map_bin"]); - expect(join(packageDir, "node_modules", ".bin", "map-bin")).toBeValidBin( - join("..", "map-bin-multiple", "bin", "map-bin"), - ); - expect(join(packageDir, "node_modules", ".bin", "map_bin")).toBeValidBin( - join("..", "map-bin-multiple", "bin", "map-bin"), - ); -}); - -test("duplicate dependency in optionalDependencies maintains sort order", async () => { - await write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - // `duplicate-optional` has `no-deps` as a normal dependency (1.0.0) and as an - // optional dependency (1.0.1). The optional dependency version should be installed and - // the sort order should remain the same (tested by `bun-debug bun.lockb`). - "duplicate-optional": "1.0.1", - }, - }), - ); - - await runBunInstall(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const lockfile = parseLockfile(packageDir); - expect(lockfile).toMatchNodeModulesAt(packageDir); - - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ - version: "1.0.1", - }); - - const { stdout, exited } = spawn({ - cmd: [bunExe(), "bun.lockb"], - cwd: packageDir, - stderr: "inherit", - stdout: "pipe", - env, - }); - - const out = await Bun.readableStreamToText(stdout); - expect(out.replaceAll(`${port}`, "4873")).toMatchSnapshot(); - expect(await exited).toBe(0); -}); - -test("missing package on reinstall, some with binaries", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "fooooo", - dependencies: { - "what-bin": "1.0.0", - "uses-what-bin": "1.5.0", - "optional-native": "1.0.0", - "peer-deps-too": "1.0.0", - "two-range-deps": "1.0.0", - "one-fixed-dep": "2.0.0", - "no-deps-bins": "2.0.0", - "left-pad": "1.0.0", - "native": "1.0.0", - "dep-loop-entry": "1.0.0", - "dep-with-tags": "3.0.0", - "dev-deps": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - stdin: "pipe", - env, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ dep-loop-entry@1.0.0", - expect.stringContaining("+ dep-with-tags@3.0.0"), - "+ dev-deps@1.0.0", - "+ left-pad@1.0.0", - "+ native@1.0.0", - "+ no-deps-bins@2.0.0", - "+ one-fixed-dep@2.0.0", - "+ optional-native@1.0.0", - "+ peer-deps-too@1.0.0", - "+ two-range-deps@1.0.0", - expect.stringContaining("+ uses-what-bin@1.5.0"), - expect.stringContaining("+ what-bin@1.0.0"), - "", - "19 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules", "native"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "left-pad"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "dep-loop-entry"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "one-fixed-dep"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "peer-deps-too"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "two-range-deps", "node_modules", "no-deps"), { - recursive: true, - force: true, - }); - await rm(join(packageDir, "node_modules", "one-fixed-dep"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin"), { recursive: true, force: true }); - await rm(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin"), { - recursive: true, - force: true, - }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stderr: "pipe", - stdout: "pipe", - stdin: "pipe", - env, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ dep-loop-entry@1.0.0", - "+ left-pad@1.0.0", - "+ native@1.0.0", - "+ one-fixed-dep@2.0.0", - "+ peer-deps-too@1.0.0", - "", - "7 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "native", "package.json"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "left-pad", "package.json"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "dep-loop-entry", "package.json"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "one-fixed-dep", "package.json"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "peer-deps-too", "package.json"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "two-range-deps", "node_modules", "no-deps"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "one-fixed-dep", "package.json"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin"))).toBe(true); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin"))).toBe(true); - const bin = process.platform === "win32" ? "what-bin.exe" : "what-bin"; - expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( - join(packageDir, "node_modules", ".bin", bin), - ); - expect( - Bun.which("what-bin", { PATH: join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin") }), - ).toBe(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin", bin)); -}); - -// waiter thread is only a thing on Linux. -for (const forceWaiterThread of isLinux ? [false, true] : [false]) { - describe("lifecycle scripts" + (forceWaiterThread ? " (waiter thread)" : ""), async () => { - test("root package with all lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - const writeScript = async (name: string) => { - const contents = ` - import { writeFileSync, existsSync, rmSync } from "fs"; - import { join } from "path"; - - const file = join(import.meta.dir, "${name}.txt"); - - if (existsSync(file)) { - rmSync(file); - writeFileSync(file, "${name} exists!"); - } else { - writeFileSync(file, "${name}!"); - } - `; - await writeFile(join(packageDir, `${name}.js`), contents); - }; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - preinstall: `${bunExe()} preinstall.js`, - install: `${bunExe()} install.js`, - postinstall: `${bunExe()} postinstall.js`, - preprepare: `${bunExe()} preprepare.js`, - prepare: `${bunExe()} prepare.js`, - postprepare: `${bunExe()} postprepare.js`, - }, - }), - ); - - await writeScript("preinstall"); - await writeScript("install"); - await writeScript("postinstall"); - await writeScript("preprepare"); - await writeScript("prepare"); - await writeScript("postprepare"); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue(); - expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(packageDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!"); - expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!"); - expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!"); - - // add a dependency with all lifecycle scripts - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - preinstall: `${bunExe()} preinstall.js`, - install: `${bunExe()} install.js`, - postinstall: `${bunExe()} postinstall.js`, - preprepare: `${bunExe()} preprepare.js`, - prepare: `${bunExe()} prepare.js`, - postprepare: `${bunExe()} postprepare.js`, - }, - dependencies: { - "all-lifecycle-scripts": "1.0.0", - }, - trustedDependencies: ["all-lifecycle-scripts"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ all-lifecycle-scripts@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall exists!"); - expect(await file(join(packageDir, "install.txt")).text()).toBe("install exists!"); - expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall exists!"); - expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare exists!"); - expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare exists!"); - expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare exists!"); - - const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts"); - - expect(await exists(join(depDir, "preinstall.txt"))).toBeTrue(); - expect(await exists(join(depDir, "install.txt"))).toBeTrue(); - expect(await exists(join(depDir, "postinstall.txt"))).toBeTrue(); - expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); - expect(await exists(join(depDir, "prepare.txt"))).toBeTrue(); - expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); - - expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); - - await rm(join(packageDir, "preinstall.txt")); - await rm(join(packageDir, "install.txt")); - await rm(join(packageDir, "postinstall.txt")); - await rm(join(packageDir, "preprepare.txt")); - await rm(join(packageDir, "prepare.txt")); - await rm(join(packageDir, "postprepare.txt")); - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - // all at once - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ all-lifecycle-scripts@1.0.0", - "", - "1 package installed", - ]); - - expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(packageDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!"); - expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!"); - expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!"); - - expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); - }); - - test("workspace lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - workspaces: ["packages/*"], - scripts: { - preinstall: `touch preinstall.txt`, - install: `touch install.txt`, - postinstall: `touch postinstall.txt`, - preprepare: `touch preprepare.txt`, - prepare: `touch prepare.txt`, - postprepare: `touch postprepare.txt`, - }, - }), - ); - - await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - version: "1.0.0", - scripts: { - preinstall: `touch preinstall.txt`, - install: `touch install.txt`, - postinstall: `touch postinstall.txt`, - preprepare: `touch preprepare.txt`, - prepare: `touch prepare.txt`, - postprepare: `touch postprepare.txt`, - }, - }), - ); - - await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); - await writeFile( - join(packageDir, "packages", "pkg2", "package.json"), - JSON.stringify({ - name: "pkg2", - version: "1.0.0", - scripts: { - preinstall: `touch preinstall.txt`, - install: `touch install.txt`, - postinstall: `touch postinstall.txt`, - preprepare: `touch preprepare.txt`, - prepare: `touch prepare.txt`, - postprepare: `touch postprepare.txt`, - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).toContain("Saved lockfile"); - var out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "postinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "preprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "packages", "pkg1", "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg1", "postprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "packages", "pkg2", "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg2", "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg2", "postinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg2", "preprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "packages", "pkg2", "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "packages", "pkg2", "postprepare.txt"))).toBeFalse(); - }); - - test("dependency lifecycle scripts run before root lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const script = '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]'; - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin-slow": "1.0.0", - }, - trustedDependencies: ["uses-what-bin-slow"], - scripts: { - install: script, - postinstall: script, - preinstall: script, - prepare: script, - postprepare: script, - preprepare: script, - }, - }), - ); - - // uses-what-bin-slow will wait one second then write a file to disk. The root package should wait for - // for this to happen before running its lifecycle scripts. - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("install a dependency with lifecycle scripts, then add to trusted dependencies and install again", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "all-lifecycle-scripts": "1.0.0", - }, - trustedDependencies: [], - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ all-lifecycle-scripts@1.0.0", - "", - "1 package installed", - "", - "Blocked 3 postinstalls. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts"); - expect(await exists(join(depDir, "preinstall.txt"))).toBeFalse(); - expect(await exists(join(depDir, "install.txt"))).toBeFalse(); - expect(await exists(join(depDir, "postinstall.txt"))).toBeFalse(); - expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); - expect(await exists(join(depDir, "prepare.txt"))).toBeTrue(); - expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); - expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); - - // add to trusted dependencies - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "all-lifecycle-scripts": "1.0.0", - }, - trustedDependencies: ["all-lifecycle-scripts"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("Checked 1 install across 2 packages (no changes)"), - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!"); - expect(await file(join(depDir, "install.txt")).text()).toBe("install!"); - expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!"); - expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!"); - expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse(); - expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse(); - }); - - test("adding a package without scripts to trustedDependencies", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "what-bin": "1.0.0", - }, - trustedDependencies: ["what-bin"], - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ what-bin@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); - const what_bin_bins = !isWindows ? ["what-bin"] : ["what-bin.bunx", "what-bin.exe"]; - // prettier-ignore - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { "what-bin": "1.0.0" }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ what-bin@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); - - // add it to trusted dependencies - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "what-bin": "1.0.0", - }, - trustedDependencies: ["what-bin"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); - }); - - test("lifecycle scripts run if node_modules is deleted", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "lifecycle-postinstall": "1.0.0", - }, - trustedDependencies: ["lifecycle-postinstall"], - }), - ); - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - var err = await new Response(stderr).text(); - var out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ lifecycle-postinstall@1.0.0", - "", - // @ts-ignore - "1 package installed", - ]); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - await rm(join(packageDir, "node_modules"), { force: true, recursive: true }); - await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ lifecycle-postinstall@1.0.0", - "", - "1 package installed", - ]); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("INIT_CWD is set to the correct directory", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - install: "bun install.js", - }, - dependencies: { - "lifecycle-init-cwd": "1.0.0", - "another-init-cwd": "npm:lifecycle-init-cwd@1.0.0", - }, - trustedDependencies: ["lifecycle-init-cwd", "another-init-cwd"], - }), - ); - - await writeFile( - join(packageDir, "install.js"), - ` - const fs = require("fs"); - const path = require("path"); - - fs.writeFileSync( - path.join(__dirname, "test.txt"), - process.env.INIT_CWD || "does not exist" - ); - `, - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - const out = await new Response(stdout).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ another-init-cwd@1.0.0", - "+ lifecycle-init-cwd@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "test.txt")).text()).toBe(packageDir); - expect(await file(join(packageDir, "node_modules/lifecycle-init-cwd/test.txt")).text()).toBe(packageDir); - expect(await file(join(packageDir, "node_modules/another-init-cwd/test.txt")).text()).toBe(packageDir); - }); - - test("failing lifecycle script should print output", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "lifecycle-failing-postinstall": "1.0.0", - }, - trustedDependencies: ["lifecycle-failing-postinstall"], - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("hello"); - expect(await exited).toBe(1); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const out = await new Response(stdout).text(); - expect(out).toEqual(expect.stringContaining("bun install v1.")); - }); - - test("failing root lifecycle script should print output correctly", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "fooooooooo", - version: "1.0.0", - scripts: { - preinstall: `${bunExe()} -e "throw new Error('Oops!')"`, - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - expect(await exited).toBe(1); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await Bun.readableStreamToText(stdout)).toEqual(expect.stringContaining("bun install v1.")); - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("error: Oops!"); - expect(err).toContain('error: preinstall script from "fooooooooo" exited with 1'); - }); - - test("exit 0 in lifecycle scripts works", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - postinstall: "exit 0", - prepare: "exit 0", - postprepare: "exit 0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("No packages! Deleted empty lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("done"), - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("--ignore-scripts should skip lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "lifecycle-failing-postinstall": "1.0.0", - }, - trustedDependencies: ["lifecycle-failing-postinstall"], - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--ignore-scripts"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("hello"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ lifecycle-failing-postinstall@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it should add `node-gyp rebuild` as the `install` script when `install` and `postinstall` don't exist and `binding.gyp` exists in the root of the package", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "binding-gyp-scripts": "1.5.0", - }, - trustedDependencies: ["binding-gyp-scripts"], - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ binding-gyp-scripts@1.5.0", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules/binding-gyp-scripts/build.node"))).toBeTrue(); - }); - - test("automatic node-gyp scripts should not run for untrusted dependencies, and should run after adding to `trustedDependencies`", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const packageJSON: any = { - name: "foo", - version: "1.0.0", - dependencies: { - "binding-gyp-scripts": "1.5.0", - }, - }; - await writeFile(packageJson, JSON.stringify(packageJSON)); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - let err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ binding-gyp-scripts@1.5.0", - "", - "2 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeFalse(); - - packageJSON.trustedDependencies = ["binding-gyp-scripts"]; - await writeFile(packageJson, JSON.stringify(packageJSON)); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeTrue(); - }); - - test("automatic node-gyp scripts work in package root", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "node-gyp": "1.5.0", - }, - }), - ); - - await writeFile(join(packageDir, "binding.gyp"), ""); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ node-gyp@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "build.node"))).toBeTrue(); - - await rm(join(packageDir, "build.node")); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "build.node"))).toBeTrue(); - }); - - test("auto node-gyp scripts work when scripts exists other than `install` and `preinstall`", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "node-gyp": "1.5.0", - }, - scripts: { - postinstall: "exit 0", - prepare: "exit 0", - postprepare: "exit 0", - }, - }), - ); - - await writeFile(join(packageDir, "binding.gyp"), ""); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ node-gyp@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "build.node"))).toBeTrue(); - }); - - for (const script of ["install", "preinstall"]) { - test(`does not add auto node-gyp script when ${script} script exists`, async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const packageJSON: any = { - name: "foo", - version: "1.0.0", - dependencies: { - "node-gyp": "1.5.0", - }, - scripts: { - [script]: "exit 0", - }, - }; - await writeFile(packageJson, JSON.stringify(packageJSON)); - await writeFile(join(packageDir, "binding.gyp"), ""); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ node-gyp@1.5.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "build.node"))).toBeFalse(); - }); - } - - test("git dependencies also run `preprepare`, `prepare`, and `postprepare` scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - let err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ lifecycle-install-test@github:dylan-conway/lifecycle-install-test#3ba6af5", - "", - "1 package installed", - "", - "Blocked 6 postinstalls. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeFalse(); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee", - }, - trustedDependencies: ["lifecycle-install-test"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeTrue(); - }); - - test("root lifecycle scripts should wait for dependency lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin-slow": "1.0.0", - }, - trustedDependencies: ["uses-what-bin-slow"], - scripts: { - install: '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]', - }, - }), - ); - - // Package `uses-what-bin-slow` has an install script that will sleep for 1 second - // before writing `what-bin.txt` to disk. The root package has an install script that - // checks if this file exists. If the root package install script does not wait for - // the other to finish, it will fail. - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uses-what-bin-slow@1.0.0", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - async function createPackagesWithScripts( - packagesCount: number, - scripts: Record, - ): Promise { - const dependencies: Record = {}; - const dependenciesList = []; - - for (let i = 0; i < packagesCount; i++) { - const packageName: string = "stress-test-package-" + i; - const packageVersion = "1.0." + i; - - dependencies[packageName] = "file:./" + packageName; - dependenciesList[i] = packageName; - - const packagePath = join(packageDir, packageName); - await mkdir(packagePath); - await writeFile( - join(packagePath, "package.json"), - JSON.stringify({ - name: packageName, - version: packageVersion, - scripts, - }), - ); - } - - await writeFile( - packageJson, - JSON.stringify({ - name: "stress-test", - version: "1.0.0", - dependencies, - trustedDependencies: dependenciesList, - }), - ); - - return dependenciesList; - } - - test("reach max concurrent scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const scripts = { - "preinstall": `${bunExe()} -e 'Bun.sleepSync(500)'`, - }; - - const dependenciesList = await createPackagesWithScripts(4, scripts); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--concurrent-scripts=2"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await Bun.readableStreamToText(stdout); - expect(out).not.toContain("Blocked"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - ...dependenciesList.map(dep => `+ ${dep}@${dep}`), - "", - "4 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("stress test", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const dependenciesList = await createPackagesWithScripts(500, { - "postinstall": `${bunExe()} --version`, - }); - - // the script is quick, default number for max concurrent scripts - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await Bun.readableStreamToText(stdout); - expect(out).not.toContain("Blocked"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - ...dependenciesList.map(dep => `+ ${dep}@${dep}`).sort((a, b) => a.localeCompare(b)), - "", - "500 packages installed", - ]); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("it should install and use correct binary version", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - // this should install `what-bin` in two places: - // - // - node_modules/.bin/what-bin@1.5.0 - // - node_modules/uses-what-bin/node_modules/.bin/what-bin@1.0.0 - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin": "1.0.0", - "what-bin": "1.5.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - var out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "+ what-bin@1.5.0", - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain( - "what-bin@1.5.0", - ); - expect( - await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(), - ).toContain("what-bin@1.0.0"); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "uses-what-bin": "1.5.0", - "what-bin": "1.0.0", - }, - scripts: { - install: "what-bin", - }, - trustedDependencies: ["uses-what-bin"], - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain( - "what-bin@1.0.0", - ); - expect( - await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(), - ).toContain("what-bin@1.5.0"); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - out = await new Response(stdout).text(); - err = await new Response(stderr).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.5.0"), - expect.stringContaining("+ what-bin@1.0.0"), - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("node-gyp should always be available for lifecycle scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - install: "node-gyp --version", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await new Response(stderr).text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - - // if node-gyp isn't available, it would return a non-zero exit code - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - // if this test fails, `electron` might be removed from the default list - test("default trusted dependencies should work", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "electron": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - "", - "1 package installed", - ]); - expect(out).not.toContain("Blocked"); - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("default trusted dependencies should not be used of trustedDependencies is populated", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "uses-what-bin": "1.0.0", - // fake electron package because it's in the default trustedDependencies list - "electron": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - // electron lifecycle scripts should run, uses-what-bin scripts should not run - var err = await new Response(stderr).text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - var out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "uses-what-bin": "1.0.0", - "electron": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }), - ); - - // now uses-what-bin scripts should run and electron scripts should not run. - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); - }); - - test("does not run any scripts if trustedDependencies is an empty list", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - "uses-what-bin": "1.0.0", - "electron": "1.0.0", - }, - trustedDependencies: [], - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - const out = await Bun.readableStreamToText(stdout); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 2 postinstalls. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); - }); - - test("will run default trustedDependencies after install that didn't include them", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - electron: "1.0.0", - }, - trustedDependencies: ["blah"], - }), - ); - - // first install does not run electron scripts - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - var err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - var out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - "", - "1 package installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse(); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - dependencies: { - electron: "1.0.0", - }, - }), - ); - - // The electron scripts should run now because it's in default trusted dependencies. - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - }); - - describe("--trust", async () => { - test("unhoisted untrusted scripts, none at root node_modules", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - // prevents real `uses-what-bin` from hoisting to root - "uses-what-bin": "npm:a-dep@1.0.3", - }, - workspaces: ["pkg1"], - }), - ), - write( - join(packageDir, "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ), - ]); - - await runBunInstall(testEnv, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - const results = await Promise.all([ - exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin")), - exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")), - ]); - - expect(results).toEqual([true, false]); - - const { stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trust", "--all"], - cwd: packageDir, - stdout: "ignore", - stderr: "pipe", - env: testEnv, - }); - - const err = await Bun.readableStreamToText(stderr); - expect(err).not.toContain("error:"); - - expect(await exited).toBe(0); - - expect( - await exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")), - ).toBeTrue(); - }); - const trustTests = [ - { - label: "only name", - packageJson: { - name: "foo", - }, - }, - { - label: "empty dependencies", - packageJson: { - name: "foo", - dependencies: {}, - }, - }, - { - label: "populated dependencies", - packageJson: { - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }, - }, - - { - label: "empty trustedDependencies", - packageJson: { - name: "foo", - trustedDependencies: [], - }, - }, - - { - label: "populated dependencies, empty trustedDependencies", - packageJson: { - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: [], - }, - }, - - { - label: "populated dependencies and trustedDependencies", - packageJson: { - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }, - }, - - { - label: "empty dependencies and trustedDependencies", - packageJson: { - name: "foo", - dependencies: {}, - trustedDependencies: [], - }, - }, - ]; - for (const { label, packageJson } of trustTests) { - test(label, async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile(join(packageDir, "package.json"), JSON.stringify(packageJson)); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "--trust", "uses-what-bin@1.0.0"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed uses-what-bin@1.0.0", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await file(join(packageDir, "package.json")).json()).toEqual({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }); - - // another install should not error with json SyntaxError - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - }); - } - describe("packages without lifecycle scripts", async () => { - test("initial install", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "--trust", "no-deps@1.0.0"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - const err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - const out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - }); - }); - test("already installed", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - }), - ); - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "no-deps"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@2.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "^2.0.0", - }, - }); - - // oops, I wanted to run the lifecycle scripts for no-deps, I'll install - // again with --trust. - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "--trust", "no-deps"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - // oh, I didn't realize no-deps doesn't have - // any lifecycle scripts. It shouldn't automatically add to - // trustedDependencies. - - err = await Bun.readableStreamToText(stderr); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed no-deps@2.0.0", - "", - expect.stringContaining("done"), - "", - ]); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "^2.0.0", - }, - }); - }); - }); - }); - - describe("updating trustedDependencies", async () => { - test("existing trustedDependencies, unchanged trustedDependencies", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - trustedDependencies: ["uses-what-bin"], - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }); - - // no changes, lockfile shouldn't be saved - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("existing trustedDependencies, removing trustedDependencies", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - trustedDependencies: ["uses-what-bin"], - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }), - ); - - // this script should not run because uses-what-bin is no longer in trustedDependencies - await rm(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"), { force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 2 installs across 3 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "uses-what-bin": "1.0.0", - }, - }); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - }); - - test("non-existent trustedDependencies, then adding it", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "electron": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ electron@1.0.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "electron": "1.0.0", - }, - }); - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - trustedDependencies: ["electron"], - dependencies: { - "electron": "1.0.0", - }, - }), - ); - - await rm(join(packageDir, "node_modules", "electron", "preinstall.txt"), { force: true }); - - // lockfile should save evenn though there are no changes to trustedDependencies due to - // the default list - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue(); - }); - }); - - test("node -p should work in postinstall scripts", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - postinstall: `node -p "require('fs').writeFileSync('postinstall.txt', 'postinstall')"`, - }, - }), - ); - - const originalPath = env.PATH; - env.PATH = ""; - - let { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: testEnv, - }); - - env.PATH = originalPath; - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("No packages! Deleted empty lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); - }); - - test("ensureTempNodeGypScript works", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - preinstall: "node-gyp --version", - }, - }), - ); - - const originalPath = env.PATH; - env.PATH = ""; - - let { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - stdin: "ignore", - env, - }); - - env.PATH = originalPath; - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("No packages! Deleted empty lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - - test("bun pm trust and untrusted on missing package", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "uses-what-bin": "1.5.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ uses-what-bin@1.5.0"), - "", - "2 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - // remove uses-what-bin from node_modules, bun pm trust and untrusted should handle missing package - await rm(join(packageDir, "node_modules", "uses-what-bin"), { recursive: true, force: true }); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "untrusted"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("bun pm untrusted"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("Found 0 untrusted dependencies with scripts"); - expect(await exited).toBe(0); - - ({ stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trust", "uses-what-bin"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - expect(await exited).toBe(1); - - err = await Bun.readableStreamToText(stderr); - expect(err).toContain("bun pm trust"); - expect(err).toContain("0 scripts ran"); - expect(err).toContain("uses-what-bin"); - }); - - describe("add trusted, delete, then add again", async () => { - // when we change bun install to delete dependencies from node_modules - // for both cases, we need to update this test - for (const withRm of [true, false]) { - test(withRm ? "withRm" : "withoutRm", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - "uses-what-bin": "1.0.0", - }, - }), - ); - - let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - let err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - let out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "3 packages installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trust", "uses-what-bin"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("1 script ran across 1 package"); - expect(await exited).toBe(0); - - expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); - expect(await file(packageJson).json()).toEqual({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - "uses-what-bin": "1.0.0", - }, - trustedDependencies: ["uses-what-bin"], - }); - - // now remove and install again - if (withRm) { - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "rm", "uses-what-bin"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("1 package removed"); - expect(out).toContain("uses-what-bin"); - expect(await exited).toBe(0); - } - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - let expected = withRm - ? ["", "Checked 1 install across 2 packages (no changes)"] - : ["", expect.stringContaining("1 package removed")]; - expected = [expect.stringContaining("bun install v1."), ...expected]; - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual(expected); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "uses-what-bin"))).toBe(!withRm); - - // add again, bun pm untrusted should report it as untrusted - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - "uses-what-bin": "1.0.0", - }, - }), - ); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expected = withRm - ? [ - "", - expect.stringContaining("+ uses-what-bin@1.0.0"), - "", - "1 package installed", - "", - "Blocked 1 postinstall. Run `bun pm untrusted` for details.", - "", - ] - : ["", expect.stringContaining("Checked 3 installs across 4 packages (no changes)"), ""]; - expected = [expect.stringContaining("bun install v1."), ...expected]; - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual(expected); - - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "untrusted"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - })); - - err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - out = await Bun.readableStreamToText(stdout); - expect(out).toContain("./node_modules/uses-what-bin @1.0.0".replaceAll("/", sep)); - expect(await exited).toBe(0); - }); - } - }); - - describe.if(!forceWaiterThread || process.platform === "linux")("does not use 100% cpu", async () => { - test("install", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - preinstall: `${bunExe()} -e 'Bun.sleepSync(1000)'`, - }, - }), - ); - - const proc = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - stdin: "ignore", - env: testEnv, - }); - - expect(await proc.exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000); - }); - - // https://github.com/oven-sh/bun/issues/11252 - test.todoIf(isWindows)("bun pm trust", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const dep = isWindows ? "uses-what-bin-slow-window" : "uses-what-bin-slow"; - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - [dep]: "1.0.0", - }, - }), - ); - - var { exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env: testEnv, - }); - - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeFalse(); - - const proc = spawn({ - cmd: [bunExe(), "pm", "trust", "--all"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env: testEnv, - }); - - expect(await proc.exited).toBe(0); - - expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeTrue(); +test("missing package on reinstall, some with binaries", async () => { + await writeFile( + packageJson, + JSON.stringify({ + name: "fooooo", + dependencies: { + "what-bin": "1.0.0", + "uses-what-bin": "1.5.0", + "optional-native": "1.0.0", + "peer-deps-too": "1.0.0", + "two-range-deps": "1.0.0", + "one-fixed-dep": "2.0.0", + "no-deps-bins": "2.0.0", + "left-pad": "1.0.0", + "native": "1.0.0", + "dep-loop-entry": "1.0.0", + "dep-with-tags": "3.0.0", + "dev-deps": "1.0.0", + }, + }), + ); - expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000 * (isWindows ? 5 : 1)); - }); - }); + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + stdin: "pipe", + env, }); - describe("stdout/stderr is inherited from root scripts during install", async () => { - test("without packages", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - - const exe = bunExe().replace(/\\/g, "\\\\"); - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - scripts: { - "preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, - "install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, - "prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - - const err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(err.split(/\r?\n/)).toEqual([ - "No packages! Deleted empty lockfile", - "", - `$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, - "preinstall stderr 🍦", - `$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, - `$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, - "", - ]); - const out = await Bun.readableStreamToText(stdout); - expect(out.split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "install stdout 🚀", - "prepare stdout done ✅", - "", - expect.stringContaining("done"), - "", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ dep-loop-entry@1.0.0", + expect.stringContaining("+ dep-with-tags@3.0.0"), + "+ dev-deps@1.0.0", + "+ left-pad@1.0.0", + "+ native@1.0.0", + "+ no-deps-bins@2.0.0", + "+ one-fixed-dep@2.0.0", + "+ optional-native@1.0.0", + "+ peer-deps-too@1.0.0", + "+ two-range-deps@1.0.0", + expect.stringContaining("+ uses-what-bin@1.5.0"), + expect.stringContaining("+ what-bin@1.0.0"), + "", + "19 packages installed", + "", + "Blocked 1 postinstall. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - test("with a package", async () => { - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + await rm(join(packageDir, "node_modules", "native"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "left-pad"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "dep-loop-entry"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "one-fixed-dep"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "peer-deps-too"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "two-range-deps", "node_modules", "no-deps"), { + recursive: true, + force: true, + }); + await rm(join(packageDir, "node_modules", "one-fixed-dep"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin"), { + recursive: true, + force: true, + }); - const exe = bunExe().replace(/\\/g, "\\\\"); - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.2.3", - scripts: { - "preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, - "install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, - "prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, - }, - dependencies: { - "no-deps": "1.0.0", - }, - }), - ); + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + stdin: "pipe", + env, + })); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ dep-loop-entry@1.0.0", + "+ left-pad@1.0.0", + "+ native@1.0.0", + "+ one-fixed-dep@2.0.0", + "+ peer-deps-too@1.0.0", + "", + "7 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - const err = stderrForInstall(await Bun.readableStreamToText(stderr)); - expect(err).not.toContain("error:"); - expect(err).not.toContain("warn:"); - expect(err.split(/\r?\n/)).toEqual([ - "Resolving dependencies", - expect.stringContaining("Resolved, downloaded and extracted "), - "Saved lockfile", - "", - `$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`, - "preinstall stderr 🍦", - `$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`, - `$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`, - "", - ]); - const out = await Bun.readableStreamToText(stdout); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "install stdout 🚀", - "prepare stdout done ✅", - "", - expect.stringContaining("+ no-deps@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - }); - }); -} + expect(await exists(join(packageDir, "node_modules", "native", "package.json"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "left-pad", "package.json"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "dep-loop-entry", "package.json"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "one-fixed-dep", "package.json"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "peer-deps-too", "package.json"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "two-range-deps", "node_modules", "no-deps"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "one-fixed-dep", "package.json"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin"))).toBe(true); + expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin"))).toBe(true); + const bin = process.platform === "win32" ? "what-bin.exe" : "what-bin"; + expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( + join(packageDir, "node_modules", ".bin", bin), + ); + expect( + Bun.which("what-bin", { PATH: join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin") }), + ).toBe(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin", bin)); +}); describe("pm trust", async () => { test("--default", async () => { @@ -13086,7 +9675,7 @@ it("$npm_command is accurate during publish", async () => { }), ); await write(join(packageDir, "bunfig.toml"), await authBunfig("npm_command")); - await rm(join(import.meta.dir, "packages", "publish-pkg-10"), { recursive: true, force: true }); + await rm(join(verdaccio.packagesPath, "publish-pkg-10"), { recursive: true, force: true }); let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); expect(err).toBe(`$ echo $npm_command\n`); expect(out.split("\n")).toEqual([ @@ -13125,7 +9714,7 @@ it("$npm_lifecycle_event is accurate during publish", async () => { `, ); await write(join(packageDir, "bunfig.toml"), await authBunfig("npm_lifecycle_event")); - await rm(join(import.meta.dir, "packages", "publish-pkg-11"), { recursive: true, force: true }); + await rm(join(verdaccio.packagesPath, "publish-pkg-11"), { recursive: true, force: true }); let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); expect(err).toBe(`$ echo 2 $npm_lifecycle_event\n$ echo 3 $npm_lifecycle_event\n`); expect(out.split("\n")).toEqual([ diff --git a/test/cli/install/bun-install-retry.test.ts b/test/cli/install/bun-install-retry.test.ts index 842691bb0b9eef..cbba8e2b370f81 100644 --- a/test/cli/install/bun-install-retry.test.ts +++ b/test/cli/install/bun-install-retry.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test"; import { access, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins } from "harness"; +import { bunExe, bunEnv as env, tmpdirSync, toBeValidBin, toBeWorkspaceLink, toHaveBins, readdirSorted } from "harness"; import { join } from "path"; import { dummyAfterAll, @@ -10,7 +10,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index 70addfbf375a7d..fb4a1d7c40e2ff 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -23,6 +23,7 @@ import { runBunInstall, isWindows, textLockfile, + readdirSorted, } from "harness"; import { join, sep, resolve } from "path"; import { @@ -32,7 +33,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/cli/install/bun-link.test.ts b/test/cli/install/bun-link.test.ts index 20d2be64394ba9..68f5160faadd14 100644 --- a/test/cli/install/bun-link.test.ts +++ b/test/cli/install/bun-link.test.ts @@ -1,16 +1,18 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { access, mkdir, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, runBunInstall, tmpdirSync, toBeValidBin, toHaveBins, stderrForInstall } from "harness"; -import { basename, join } from "path"; import { - dummyAfterAll, - dummyAfterEach, - dummyBeforeAll, - dummyBeforeEach, - package_dir, + bunExe, + bunEnv as env, + runBunInstall, + tmpdirSync, + toBeValidBin, + toHaveBins, + stderrForInstall, readdirSorted, -} from "./dummy.registry"; +} from "harness"; +import { basename, join } from "path"; +import { dummyAfterAll, dummyAfterEach, dummyBeforeAll, dummyBeforeEach, package_dir } from "./dummy.registry"; beforeAll(dummyBeforeAll); afterAll(dummyAfterAll); diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts new file mode 100644 index 00000000000000..f60725be5f1236 --- /dev/null +++ b/test/cli/install/bun-lock.test.ts @@ -0,0 +1,85 @@ +import { spawn, write, file } from "bun"; +import { expect, it } from "bun:test"; +import { access, copyFile, open, writeFile } from "fs/promises"; +import { bunExe, bunEnv as env, isWindows, tmpdirSync } from "harness"; +import { join } from "path"; + +it("should write plaintext lockfiles", async () => { + const package_dir = tmpdirSync(); + + // copy bar-0.0.2.tgz to package_dir + await copyFile(join(__dirname, "bar-0.0.2.tgz"), join(package_dir, "bar-0.0.2.tgz")); + + // Create a simple package.json + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "test-package", + version: "1.0.0", + dependencies: { + "dummy-package": "file:./bar-0.0.2.tgz", + }, + }), + ); + + // Run 'bun install' to generate the lockfile + const installResult = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: package_dir, + env, + }); + await installResult.exited; + + // Ensure the lockfile was created + await access(join(package_dir, "bun.lock")); + + // Assert that the lockfile has the correct permissions + const file = await open(join(package_dir, "bun.lock"), "r"); + const stat = await file.stat(); + + // in unix, 0o644 == 33188 + let mode = 33188; + // ..but windows is different + if (isWindows) { + mode = 33206; + } + expect(stat.mode).toBe(mode); + + expect(await file.readFile({ encoding: "utf8" })).toMatchSnapshot(); +}); + +// won't work on windows, " is not a valid character in a filename +it.skipIf(isWindows)("should escape names", async () => { + const packageDir = tmpdirSync(); + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "quote-in-dependency-name", + workspaces: ["packages/*"], + }), + ), + write(join(packageDir, "packages", '"', "package.json"), JSON.stringify({ name: '"' })), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + '"': "*", + }, + }), + ), + ]); + + const { exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + + expect(await file(join(packageDir, "bun.lock")).text()).toMatchSnapshot(); +}); diff --git a/test/cli/install/bun-lockb.test.ts b/test/cli/install/bun-lockb.test.ts index b97bc1759a9a3d..cf0ab704efa6cd 100644 --- a/test/cli/install/bun-lockb.test.ts +++ b/test/cli/install/bun-lockb.test.ts @@ -1,7 +1,7 @@ import { spawn } from "bun"; import { expect, it } from "bun:test"; -import { access, copyFile, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, tmpdirSync } from "harness"; +import { access, copyFile, open, writeFile } from "fs/promises"; +import { bunExe, bunEnv as env, isWindows, tmpdirSync } from "harness"; import { join } from "path"; it("should not print anything to stderr when running bun.lockb", async () => { @@ -33,6 +33,18 @@ it("should not print anything to stderr when running bun.lockb", async () => { // Ensure the lockfile was created await access(join(package_dir, "bun.lockb")); + // Assert that the lockfile has the correct permissions + const file = await open(join(package_dir, "bun.lockb"), "r"); + const stat = await file.stat(); + + // in unix, 0o755 == 33261 + let mode = 33261; + // ..but windows is different + if(isWindows) { + mode = 33206; + } + expect(stat.mode).toBe(mode); + // create a .env await writeFile(join(package_dir, ".env"), "FOO=bar"); diff --git a/test/cli/install/bun-pm.test.ts b/test/cli/install/bun-pm.test.ts index d1a6042f96fe06..8a49dcaf16fb10 100644 --- a/test/cli/install/bun-pm.test.ts +++ b/test/cli/install/bun-pm.test.ts @@ -1,7 +1,7 @@ import { spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { exists, mkdir, writeFile } from "fs/promises"; -import { bunEnv, bunExe, bunEnv as env, tmpdirSync } from "harness"; +import { bunEnv, bunExe, bunEnv as env, tmpdirSync, readdirSorted } from "harness"; import { cpSync } from "node:fs"; import { join } from "path"; import { @@ -11,7 +11,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/cli/install/bun-run.test.ts b/test/cli/install/bun-run.test.ts index 99293d6013cbcb..3b363ba90fb490 100644 --- a/test/cli/install/bun-run.test.ts +++ b/test/cli/install/bun-run.test.ts @@ -1,9 +1,17 @@ import { file, spawn, spawnSync } from "bun"; import { beforeEach, describe, expect, it } from "bun:test"; import { exists, mkdir, rm, writeFile } from "fs/promises"; -import { bunEnv, bunExe, bunEnv as env, isWindows, tempDirWithFiles, tmpdirSync, stderrForInstall } from "harness"; +import { + bunEnv, + bunExe, + bunEnv as env, + isWindows, + tempDirWithFiles, + tmpdirSync, + stderrForInstall, + readdirSorted, +} from "harness"; import { join } from "path"; -import { readdirSorted } from "./dummy.registry"; let run_dir: string; @@ -591,22 +599,53 @@ it("should pass arguments correctly in scripts", async () => { } }); -it("should run with bun instead of npm even with leading spaces", async () => { - const dir = tempDirWithFiles("test", { - "package.json": JSON.stringify({ - workspaces: ["a", "b"], - scripts: { "root_script": " npm run other_script ", "other_script": " echo hi " }, - }), - }); - { - const { stdout, stderr, exitCode } = spawnSync({ - cmd: [bunExe(), "run", "root_script"], - cwd: dir, - env: bunEnv, - }); +const cases = [ + ["yarn run", "run"], + ["yarn add", "passthrough"], + ["yarn audit", "passthrough"], + ["yarn -abcd run", "passthrough"], + ["yarn info", "passthrough"], + ["yarn generate-lock-entry", "passthrough"], + ["yarn", "run"], + ["npm run", "run"], + ["npx", "x"], + ["pnpm run", "run"], + ["pnpm dlx", "x"], + ["pnpx", "x"], +]; +describe("should handle run case", () => { + for (const ccase of cases) { + it(ccase[0], async () => { + const dir = tempDirWithFiles("test", { + "package.json": JSON.stringify({ + scripts: { + "root_script": ` ${ccase[0]} target_script% `, + "target_script%": " echo target_script ", + }, + }), + }); + { + const { stdout, stderr, exitCode } = spawnSync({ + cmd: [bunExe(), "root_script"], + cwd: dir, + env: bunEnv, + }); - expect(stderr.toString()).toMatch(/\$ bun(-debug)? run other_script \n\$ echo hi \n/); - expect(stdout.toString()).toEndWith("hi\n"); - expect(exitCode).toBe(0); + if (ccase[1] === "run") { + expect(stderr.toString()).toMatch( + /^\$ bun(-debug)? run target_script% \n\$ echo target_script \n/, + ); + expect(stdout.toString()).toEndWith("target_script\n"); + expect(exitCode).toBe(0); + } else if (ccase[1] === "x") { + expect(stderr.toString()).toMatch( + /^\$ bun(-debug)? x target_script% \nerror: unrecognised dependency format: target_script%/, + ); + expect(exitCode).toBe(1); + } else { + expect(stderr.toString()).toStartWith(`$ ${ccase[0]} target_script% \n`); + } + } + }); } }); diff --git a/test/cli/install/bun-update.test.ts b/test/cli/install/bun-update.test.ts index 2ecbbb9daa4984..e81f65184a4c1c 100644 --- a/test/cli/install/bun-update.test.ts +++ b/test/cli/install/bun-update.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { access, readFile, rm, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, toBeValidBin, toHaveBins } from "harness"; +import { bunExe, bunEnv as env, toBeValidBin, toHaveBins, readdirSorted } from "harness"; import { join } from "path"; import { dummyAfterAll, @@ -10,7 +10,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 7cf4d49d37e316..a72ece22806f4b 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -1,31 +1,41 @@ -import { file, write } from "bun"; +import { file, write, spawn } from "bun"; import { install_test_helpers } from "bun:internal-for-testing"; -import { beforeEach, describe, expect, test } from "bun:test"; +import { beforeEach, describe, expect, test, beforeAll, afterAll } from "bun:test"; import { mkdirSync, rmSync, writeFileSync } from "fs"; -import { cp } from "fs/promises"; -import { bunExe, bunEnv as env, runBunInstall, tmpdirSync, toMatchNodeModulesAt } from "harness"; +import { cp, mkdir, rm, exists } from "fs/promises"; +import { + bunExe, + bunEnv as env, + runBunInstall, + toMatchNodeModulesAt, + assertManifestsPopulated, + VerdaccioRegistry, + readdirSorted, +} from "harness"; import { join } from "path"; const { parseLockfile } = install_test_helpers; expect.extend({ toMatchNodeModulesAt }); -var testCounter: number = 0; - // not necessary, but verdaccio will be added to this file in the near future -var port: number = 4873; + +var verdaccio: VerdaccioRegistry; var packageDir: string; +var packageJson: string; + +beforeAll(async () => { + verdaccio = new VerdaccioRegistry(); + await verdaccio.start(); +}); -beforeEach(() => { - packageDir = tmpdirSync(); +afterAll(() => { + verdaccio.stop(); +}); + +beforeEach(async () => { + ({ packageDir, packageJson } = await verdaccio.createTestDir()); env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); - writeFileSync( - join(packageDir, "bunfig.toml"), - ` -[install] -cache = false -`, - ); }); test("dependency on workspace without version in package.json", async () => { @@ -41,7 +51,7 @@ test("dependency on workspace without version in package.json", async () => { write( join(packageDir, "packages", "mono", "package.json"), JSON.stringify({ - name: "lodash", + name: "no-deps", }), ), ]); @@ -60,7 +70,7 @@ test("dependency on workspace without version in package.json", async () => { "1", "1.*", "1.1.*", - "1.1.1", + "1.1.0", "*-pre+build", "*+build", "latest", // dist-tag exists, should choose package from npm @@ -74,7 +84,7 @@ test("dependency on workspace without version in package.json", async () => { name: "bar", version: "1.0.0", dependencies: { - lodash: version, + "no-deps": version, }, }), ); @@ -82,7 +92,9 @@ test("dependency on workspace without version in package.json", async () => { const { out } = await runBunInstall(env, packageDir); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); - expect(lockfile).toMatchSnapshot(`version: ${version}`); + expect( + JSON.stringify(lockfile, null, 2).replaceAll(/http:\/\/localhost:\d+/g, "http://localhost:1234"), + ).toMatchSnapshot(`version: ${version}`); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", @@ -101,7 +113,7 @@ test("dependency on workspace without version in package.json", async () => { name: "bar", version: "1.0.0", dependencies: { - lodash: version, + "no-deps": version, }, }), ); @@ -109,7 +121,9 @@ test("dependency on workspace without version in package.json", async () => { const { out } = await runBunInstall(env, packageDir); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); - expect(lockfile).toMatchSnapshot(`version: ${version}`); + expect( + JSON.stringify(lockfile, null, 2).replaceAll(/http:\/\/localhost:\d+/g, "http://localhost:1234"), + ).toMatchSnapshot(`version: ${version}`); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", @@ -134,7 +148,7 @@ test("dependency on same name as workspace and dist-tag", async () => { write( join(packageDir, "packages", "mono", "package.json"), JSON.stringify({ - name: "lodash", + name: "no-deps", version: "4.17.21", }), ), @@ -145,7 +159,7 @@ test("dependency on same name as workspace and dist-tag", async () => { name: "bar", version: "1.0.0", dependencies: { - lodash: "latest", + "no-deps": "latest", }, }), ), @@ -153,7 +167,9 @@ test("dependency on same name as workspace and dist-tag", async () => { const { out } = await runBunInstall(env, packageDir); const lockfile = parseLockfile(packageDir); - expect(lockfile).toMatchSnapshot("with version"); + expect( + JSON.stringify(lockfile, null, 2).replaceAll(/http:\/\/localhost:\d+/g, "http://localhost:1234"), + ).toMatchSnapshot("with version"); expect(lockfile).toMatchNodeModulesAt(packageDir); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), @@ -365,8 +381,6 @@ describe("workspace aliases", async () => { ), ]); - console.log({ packageDir }); - await runBunInstall(env, packageDir); const files = await Promise.all( ["a0", "a1", "a2", "a3", "a4", "a5"].map(name => @@ -658,3 +672,957 @@ test("$npm_package_config_ works in root in subpackage", async () => { expect(await new Response(p.stderr).text()).toBe(`$ echo $npm_package_config_foo $npm_package_config_qux\n`); expect(await new Response(p.stdout).text()).toBe(`tab\n`); }); + +test("adding packages in a subdirectory of a workspace", async () => { + await write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["foo"], + }), + ); + + await mkdir(join(packageDir, "folder1")); + await mkdir(join(packageDir, "foo", "folder2"), { recursive: true }); + await write( + join(packageDir, "foo", "package.json"), + JSON.stringify({ + name: "foo", + }), + ); + + // add package to root workspace from `folder1` + let { stdout, exited } = spawn({ + cmd: [bunExe(), "add", "no-deps"], + cwd: join(packageDir, "folder1"), + stdout: "pipe", + stderr: "inherit", + env, + }); + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@2.0.0", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "root", + workspaces: ["foo"], + dependencies: { + "no-deps": "^2.0.0", + }, + }); + + // add package to foo from `folder2` + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "add", "what-bin"], + cwd: join(packageDir, "foo", "folder2"), + stdout: "pipe", + stderr: "inherit", + env, + })); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed what-bin@1.5.0 with binaries:", + " - what-bin", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "foo", "package.json")).json()).toEqual({ + name: "foo", + dependencies: { + "what-bin": "^1.5.0", + }, + }); + + // now delete node_modules and bun.lockb and install + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "folder1"), + stdout: "pipe", + stderr: "inherit", + env, + })); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ no-deps@2.0.0", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "foo", "no-deps", "what-bin"]); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb")); + + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "foo", "folder2"), + stdout: "pipe", + stderr: "inherit", + env, + })); + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ what-bin@1.5.0", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "foo", "no-deps", "what-bin"]); +}); +test("adding packages in workspaces", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "bar": "workspace:*", + }, + }), + ); + + await mkdir(join(packageDir, "packages", "bar"), { recursive: true }); + await mkdir(join(packageDir, "packages", "boba")); + await mkdir(join(packageDir, "packages", "pkg5")); + + await write(join(packageDir, "packages", "bar", "package.json"), JSON.stringify({ name: "bar" })); + await write( + join(packageDir, "packages", "boba", "package.json"), + JSON.stringify({ name: "boba", version: "1.0.0", dependencies: { "pkg5": "*" } }), + ); + await write( + join(packageDir, "packages", "pkg5", "package.json"), + JSON.stringify({ + name: "pkg5", + version: "1.2.3", + dependencies: { + "bar": "workspace:*", + }, + }), + ); + + let { stdout, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "inherit", + env, + }); + + let out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@workspace:packages/bar", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await exists(join(packageDir, "node_modules", "bar"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "boba"))).toBeTrue(); + expect(await exists(join(packageDir, "node_modules", "pkg5"))).toBeTrue(); + + // add a package to the root workspace + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "add", "no-deps"], + cwd: packageDir, + stdout: "pipe", + stderr: "inherit", + env, + })); + + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed no-deps@2.0.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(packageJson).json()).toEqual({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + bar: "workspace:*", + "no-deps": "^2.0.0", + }, + }); + + // add a package in a workspace + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "add", "two-range-deps"], + cwd: join(packageDir, "packages", "boba"), + stdout: "pipe", + stderr: "inherit", + env, + })); + + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed two-range-deps@1.0.0", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "packages", "boba", "package.json")).json()).toEqual({ + name: "boba", + version: "1.0.0", + dependencies: { + "pkg5": "*", + "two-range-deps": "^1.0.0", + }, + }); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "@types", + "bar", + "boba", + "no-deps", + "pkg5", + "two-range-deps", + ]); + + // add a dependency to a workspace with the same name as another workspace + ({ stdout, exited } = spawn({ + cmd: [bunExe(), "add", "bar@0.0.7"], + cwd: join(packageDir, "packages", "boba"), + stdout: "pipe", + stderr: "inherit", + env, + })); + + out = await Bun.readableStreamToText(stdout); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed bar@0.0.7", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "packages", "boba", "package.json")).json()).toEqual({ + name: "boba", + version: "1.0.0", + dependencies: { + "pkg5": "*", + "two-range-deps": "^1.0.0", + "bar": "0.0.7", + }, + }); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ + "@types", + "bar", + "boba", + "no-deps", + "pkg5", + "two-range-deps", + ]); + expect(await file(join(packageDir, "node_modules", "boba", "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.7", + description: "not a workspace", + }); +}); +test("it should detect duplicate workspace dependencies", async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + }), + ); + + await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); + await write(join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1" })); + await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); + await write(join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "pkg1" })); + + var { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + expect(err).toContain('Workspace name "pkg1" already exists'); + expect(await exited).toBe(1); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { force: true }); + + ({ stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "pkg1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + expect(err).toContain('Workspace name "pkg1" already exists'); + expect(await exited).toBe(1); +}); + +const versions = ["workspace:1.0.0", "workspace:*", "workspace:^1.0.0", "1.0.0", "*"]; + +for (const rootVersion of versions) { + for (const packageVersion of versions) { + test(`it should allow duplicates, root@${rootVersion}, package@${packageVersion}`, async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + workspaces: ["packages/*"], + dependencies: { + pkg2: rootVersion, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true }); + await write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + pkg2: packageVersion, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true }); + await write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ name: "pkg2", version: "1.0.0" }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ pkg2@workspace:packages/pkg2`, + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "pkg1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "pkg1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ pkg2@workspace:packages/pkg2`, + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 2 installs across 3 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + }); + } +} + +for (const version of versions) { + test(`it should allow listing workspace as dependency of the root package version ${version}`, async () => { + await write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "workspace-1": version, + }, + }), + ); + + await mkdir(join(packageDir, "packages", "workspace-1"), { recursive: true }); + await write( + join(packageDir, "packages", "workspace-1", "package.json"), + JSON.stringify({ + name: "workspace-1", + version: "1.0.0", + }), + ); + // install first from the root, the workspace package + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("already exists"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("Duplicate dependency"); + expect(err).not.toContain('workspace dependency "workspace-1" not found'); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ workspace-1@workspace:packages/workspace-1`, + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ + name: "workspace-1", + version: "1.0.0", + }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "workspace-1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("already exists"); + expect(err).not.toContain("Duplicate dependency"); + expect(err).not.toContain('workspace dependency "workspace-1" not found'); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ + name: "workspace-1", + version: "1.0.0", + }); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); + + // install from workspace package then from root + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "packages", "workspace-1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("already exists"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("Duplicate dependency"); + expect(err).not.toContain('workspace dependency "workspace-1" not found'); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ + name: "workspace-1", + version: "1.0.0", + }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + + err = await new Response(stderr).text(); + out = await new Response(stdout).text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("already exists"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("Duplicate dependency"); + expect(err).not.toContain('workspace dependency "workspace-1" not found'); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl()); + + expect(await file(join(packageDir, "node_modules", "workspace-1", "package.json")).json()).toEqual({ + name: "workspace-1", + version: "1.0.0", + }); + }); +} + +describe("install --filter", () => { + test("does not run root scripts if root is filtered out", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + scripts: { + postinstall: `${bunExe()} root.js`, + }, + }), + ), + write(join(packageDir, "root.js"), `require("fs").writeFileSync("root.txt", "")`), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + scripts: { + postinstall: `${bunExe()} pkg1.js`, + }, + }), + ), + write(join(packageDir, "packages", "pkg1", "pkg1.js"), `require("fs").writeFileSync("pkg1.txt", "")`), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + }); + + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "root.txt"))).toBeFalse(); + expect(await exists(join(packageDir, "packages", "pkg1", "pkg1.txt"))).toBeTrue(); + + await rm(join(packageDir, "packages", "pkg1", "pkg1.txt")); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "root"], + cwd: packageDir, + stdout: "ignore", + stderr: "ignore", + env, + })); + + expect(await exited).toBe(0); + + expect(await exists(join(packageDir, "root.txt"))).toBeTrue(); + expect(await exists(join(packageDir, "packages", "pkg1.txt"))).toBeFalse(); + }); + + test("basic", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps")), + ]), + ).toEqual([false, false]); + + // add workspace + await write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps")), + ]), + ).toEqual([false, true]); + }); + + test("all but one or two", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!pkg2", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + exists(join(packageDir, "node_modules", "pkg2")), + ]), + ).toEqual([true, { name: "no-deps", version: "2.0.0" }, false]); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + // exclude the root by name + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!root"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps")), + exists(join(packageDir, "node_modules", "pkg1")), + exists(join(packageDir, "node_modules", "pkg2")), + ]), + ).toEqual([false, true, true, true]); + }); + + test("matched workspace depends on filtered workspace", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "pkg1": "1.0.0", + }, + }), + ), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + exists(join(packageDir, "node_modules", "pkg1")), + exists(join(packageDir, "node_modules", "pkg2")), + ]), + ).toEqual([true, { name: "no-deps", version: "2.0.0" }, true, true]); + }); + + test("filter with a path", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "path-pattern", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + ]); + + async function checkRoot() { + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps", "package.json")), + exists(join(packageDir, "node_modules", "pkg1")), + ]), + ).toEqual([true, false, false]); + } + + async function checkWorkspace() { + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + exists(join(packageDir, "node_modules", "pkg1")), + ]), + ).toEqual([false, { name: "no-deps", version: "2.0.0" }, true]); + } + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "./packages/pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + await checkWorkspace(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "./packages/*"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkWorkspace(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!./packages/pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkRoot(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!./packages/*"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkRoot(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!./"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkWorkspace(); + }); +}); diff --git a/test/cli/install/bunx.test.ts b/test/cli/install/bunx.test.ts index 87a26b0c7b15e2..81efa8318af649 100644 --- a/test/cli/install/bunx.test.ts +++ b/test/cli/install/bunx.test.ts @@ -1,11 +1,10 @@ import { spawn } from "bun"; import { beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test"; import { rm, writeFile } from "fs/promises"; -import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness"; +import { bunEnv, bunExe, isWindows, tmpdirSync, readdirSorted } from "harness"; import { readdirSync } from "node:fs"; import { tmpdir } from "os"; import { join, resolve } from "path"; -import { readdirSorted } from "./dummy.registry"; let x_dir: string; let current_tmpdir: string; diff --git a/test/cli/install/dummy.registry.ts b/test/cli/install/dummy.registry.ts index 060b50a0fe915c..f83f719542cc62 100644 --- a/test/cli/install/dummy.registry.ts +++ b/test/cli/install/dummy.registry.ts @@ -87,12 +87,6 @@ export function dummyRegistry(urls: string[], info: any = { "0.0.2": {} }, numbe return _handler; } -export async function readdirSorted(path: PathLike): Promise { - const results = await readdir(path); - results.sort(); - return results; -} - export function setHandler(newHandler: Handler) { handler = newHandler; } diff --git a/test/cli/install/registry/missing-directory-bin-1.1.1.tgz b/test/cli/install/missing-directory-bin-1.1.1.tgz similarity index 100% rename from test/cli/install/registry/missing-directory-bin-1.1.1.tgz rename to test/cli/install/missing-directory-bin-1.1.1.tgz diff --git a/test/cli/run/filter-workspace.test.ts b/test/cli/run/filter-workspace.test.ts index 2d5b4f4fe9b097..8a6b064a774d72 100644 --- a/test/cli/run/filter-workspace.test.ts +++ b/test/cli/run/filter-workspace.test.ts @@ -110,7 +110,7 @@ function runInCwdSuccess({ cmd.push("--filter", p); } } else { - cmd.push("--filter", pattern); + cmd.push("-F", pattern); } for (const c of command) { diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index d4d2e83a194ebf..cc9f72a03c8efc 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -577,7 +577,7 @@ describe("bun test", () => { GITHUB_ACTIONS: "true", }, }); - expect(stderr).toMatch(/::error title=error: Oops!::/); + expect(stderr).toMatch(/::error file=.*,line=\d+,col=\d+,title=error: Oops!::/m); }); test("should annotate a test timeout", () => { const stderr = runTest({ diff --git a/test/cli/test/expectations.test.ts b/test/cli/test/expectations.test.ts new file mode 100644 index 00000000000000..20698874daf454 --- /dev/null +++ b/test/cli/test/expectations.test.ts @@ -0,0 +1,11 @@ +describe(".toThrow()", () => { + it(".toThrow() behaves the same as .toThrow('')", () => { + expect(() => { + throw new Error("test"); + }).toThrow(); + + expect(() => { + throw new Error("test"); + }).toThrow(""); + }); +}); diff --git a/test/harness.ts b/test/harness.ts index bbdc48006f666d..5fe4cf1a18727a 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1,8 +1,9 @@ -import { gc as bunGC, sleepSync, spawnSync, unsafe, which } from "bun"; +import { gc as bunGC, sleepSync, spawnSync, unsafe, which, write } from "bun"; import { heapStats } from "bun:jsc"; +import { fork, ChildProcess } from "child_process"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { readFile, readlink, writeFile } from "fs/promises"; -import fs, { closeSync, openSync } from "node:fs"; +import { readFile, readlink, writeFile, readdir, rm } from "fs/promises"; +import fs, { closeSync, openSync, rmSync } from "node:fs"; import os from "node:os"; import { dirname, isAbsolute, join } from "path"; import detectLibc from "detect-libc"; @@ -1388,9 +1389,9 @@ Object.defineProperty(globalThis, "gc", { configurable: true, }); -export function waitForFileToExist(path: string, interval: number) { +export function waitForFileToExist(path: string, interval_ms: number) { while (!fs.existsSync(path)) { - sleepSync(interval); + sleepSync(interval_ms); } } @@ -1434,3 +1435,82 @@ export function textLockfile(version: number, pkgs: any): string { ...pkgs, }); } + +export class VerdaccioRegistry { + port: number; + process: ChildProcess | undefined; + configPath: string; + packagesPath: string; + + constructor(opts?: { configPath?: string; packagesPath?: string; verbose?: boolean }) { + this.port = randomPort(); + this.configPath = opts?.configPath ?? join(import.meta.dir, "cli", "install", "registry", "verdaccio.yaml"); + this.packagesPath = opts?.packagesPath ?? join(import.meta.dir, "cli", "install", "registry", "packages"); + } + + async start(silent: boolean = true) { + await rm(join(dirname(this.configPath), "htpasswd"), { force: true }); + this.process = fork(require.resolve("verdaccio/bin/verdaccio"), ["-c", this.configPath, "-l", `${this.port}`], { + silent, + // Prefer using a release build of Bun since it's faster + execPath: Bun.which("bun") || bunExe(), + }); + + this.process.stderr?.on("data", data => { + console.error(`[verdaccio] stderr: ${data}`); + }); + + const started = Promise.withResolvers(); + + this.process.on("error", error => { + console.error(`Failed to start verdaccio: ${error}`); + started.reject(error); + }); + + this.process.on("exit", (code, signal) => { + if (code !== 0) { + console.error(`Verdaccio exited with code ${code} and signal ${signal}`); + } else { + console.log("Verdaccio exited successfully"); + } + }); + + this.process.on("message", (message: { verdaccio_started: boolean }) => { + if (message.verdaccio_started) { + started.resolve(); + } + }); + + await started.promise; + } + + registryUrl() { + return `http://localhost:${this.port}/`; + } + + stop() { + rmSync(join(dirname(this.configPath), "htpasswd"), { force: true }); + this.process?.kill(); + } + + async createTestDir() { + const packageDir = tmpdirSync(); + const packageJson = join(packageDir, "package.json"); + await write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = "${join(packageDir, ".bun-cache")}" + registry = "${this.registryUrl()}" + `, + ); + + return { packageDir, packageJson }; + } +} + +export async function readdirSorted(path: string): Promise { + const results = await readdir(path); + results.sort(); + return results; +} diff --git a/test/internal/fifo.test.ts b/test/internal/fifo.test.ts new file mode 100644 index 00000000000000..9efd777dc71703 --- /dev/null +++ b/test/internal/fifo.test.ts @@ -0,0 +1,245 @@ +import { Dequeue } from "bun:internal-for-testing"; +import { describe, expect, test, it, beforeAll, beforeEach } from "bun:test"; + +/** + * Implements the same API as {@link Dequeue} but uses a simple list as the + * backing store. + * + * Used to check expected behavior. + */ +class DequeueList { + private _list: T[]; + + constructor() { + this._list = []; + } + + size(): number { + return this._list.length; + } + + isEmpty(): boolean { + return this.size() == 0; + } + + isNotEmpty(): boolean { + return this.size() > 0; + } + + shift(): T | undefined { + return this._list.shift(); + } + + peek(): T | undefined { + return this._list[0]; + } + + push(item: T): void { + this._list.push(item); + } + + toArray(fullCopy: boolean): T[] { + return fullCopy ? this._list.slice() : this._list; + } + + clear(): void { + this._list = []; + } +} + +describe("Given an empty queue", () => { + let queue: Dequeue; + + beforeEach(() => { + queue = new Dequeue(); + }); + + it("has a size of 0", () => { + expect(queue.size()).toBe(0); + }); + + it("is empty", () => { + expect(queue.isEmpty()).toBe(true); + expect(queue.isNotEmpty()).toBe(false); + }); + + it("shift() returns undefined", () => { + expect(queue.shift()).toBe(undefined); + expect(queue.size()).toBe(0); + }); + + it("has an initial capacity of 4", () => { + expect(queue._list.length).toBe(4); + expect(queue._capacityMask).toBe(3); + }); + + it("toArray() returns an empty array", () => { + expect(queue.toArray()).toEqual([]); + }); + + describe("When an element is pushed", () => { + beforeEach(() => { + queue.push(42); + }); + + it("has a size of 1", () => { + expect(queue.size()).toBe(1); + }); + + it("can be peeked without removing it", () => { + expect(queue.peek()).toBe(42); + expect(queue.size()).toBe(1); + }); + + it("is not empty", () => { + expect(queue.isEmpty()).toBe(false); + expect(queue.isNotEmpty()).toBe(true); + }); + + it("can be shifted out", () => { + const el = queue.shift(); + expect(el).toBe(42); + expect(queue.size()).toBe(0); + expect(queue.isEmpty()).toBe(true); + }); + }); // +}); // + +describe("grow boundary conditions", () => { + describe.each([3, 4, 16])("when %d items are pushed", n => { + let queue: Dequeue; + + beforeEach(() => { + queue = new Dequeue(); + for (let i = 0; i < n; i++) { + queue.push(i); + } + }); + + it(`has a size of ${n}`, () => { + expect(queue.size()).toBe(n); + }); + + it("is not empty", () => { + expect(queue.isEmpty()).toBe(false); + expect(queue.isNotEmpty()).toBe(true); + }); + + it(`can shift() ${n} times`, () => { + for (let i = 0; i < n; i++) { + expect(queue.peek()).toBe(i); + expect(queue.shift()).toBe(i); + } + expect(queue.size()).toBe(0); + expect(queue.shift()).toBe(undefined); + }); + + it("toArray() returns [0..n-1]", () => { + // same as repeated push() but only allocates once + var expected = new Array(n); + for (let i = 0; i < n; i++) { + expected[i] = i; + } + expect(queue.toArray()).toEqual(expected); + }); + }); +}); // + +describe("adding and removing items", () => { + let queue: Dequeue; + let expected: DequeueList; + + describe("when 10k items are pushed", () => { + beforeEach(() => { + queue = new Dequeue(); + expected = new DequeueList(); + + for (let i = 0; i < 10_000; i++) { + queue.push(i); + expected.push(i); + } + }); + + it("has a size of 10000", () => { + expect(queue.size()).toBe(10_000); + expect(expected.size()).toBe(10_000); + }); + + describe("when 10 items are shifted", () => { + beforeEach(() => { + for (let i = 0; i < 10; i++) { + expect(queue.shift()).toBe(expected.shift()); + } + }); + + it("has a size of 9990", () => { + expect(queue.size()).toBe(9990); + expect(expected.size()).toBe(9990); + }); + }); + }); // + + describe("when 1k items are pushed, then removed", () => { + beforeEach(() => { + queue = new Dequeue(); + expected = new DequeueList(); + + for (let i = 0; i < 1_000; i++) { + queue.push(i); + expected.push(i); + } + expect(queue.size()).toBe(1_000); + + while (queue.isNotEmpty()) { + expect(queue.shift()).toBe(expected.shift()); + } + }); + + it("is now empty", () => { + expect(queue.size()).toBe(0); + expect(queue.isEmpty()).toBeTrue(); + expect(queue.isNotEmpty()).toBeFalse(); + }); + + it("when new items are added, the backing list is resized", () => { + for (let i = 0; i < 10_000; i++) { + queue.push(i); + expected.push(i); + expect(queue.size()).toBe(expected.size()); + expect(queue.peek()).toBe(expected.peek()); + expect(queue.isEmpty()).toBeFalse(); + expect(queue.isNotEmpty()).toBeTrue(); + } + }); + }); // + + it("pushing and shifting a lot of items affects the size and backing list correctly", () => { + queue = new Dequeue(); + expected = new DequeueList(); + + for (let i = 0; i < 15_000; i++) { + queue.push(i); + expected.push(i); + expect(queue.size()).toBe(expected.size()); + expect(queue.peek()).toBe(expected.peek()); + expect(queue.isEmpty()).toBeFalse(); + expect(queue.isNotEmpty()).toBeTrue(); + } + + // shift() shrinks the backing array when tail > 10,000 and the list is + // shrunk too far (tail <= list.length >>> 2) + for (let i = 0; i < 10_000; i++) { + expect(queue.shift()).toBe(expected.shift()); + expect(queue.size()).toBe(expected.size()); + } + + for (let i = 0; i < 5_000; i++) { + queue.push(i); + expected.push(i); + expect(queue.size()).toBe(expected.size()); + expect(queue.peek()).toBe(expected.peek()); + expect(queue.isEmpty()).toBeFalse(); + expect(queue.isNotEmpty()).toBeTrue(); + } + }); // +}); // diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 2e4405893a4234..5c85ae7b07d998 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -2,9 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { describe, expect, test } from "bun:test"; +import { describe, test } from "bun:test"; import "harness"; -import path from "path"; import { attrTest, cssTest, indoc, minify_test, minifyTest, prefix_test } from "./util"; describe("css tests", () => { diff --git a/test/js/bun/ffi/ffi.test.js b/test/js/bun/ffi/ffi.test.js index 39782240bfadb6..93f1abbfc19dd9 100644 --- a/test/js/bun/ffi/ffi.test.js +++ b/test/js/bun/ffi/ffi.test.js @@ -927,14 +927,6 @@ const libSymbols = { returns: "int", args: ["ptr", "ptr", "usize"], }, - pthread_attr_getguardsize: { - returns: "int", - args: ["ptr", "ptr"], - }, - pthread_attr_setguardsize: { - returns: "int", - args: ["ptr", "usize"], - }, login_tty: { returns: "int", args: ["int"], diff --git a/test/js/bun/glob/match.test.ts b/test/js/bun/glob/match.test.ts index c09f8b7cd0ea5e..9a98d44c403b79 100644 --- a/test/js/bun/glob/match.test.ts +++ b/test/js/bun/glob/match.test.ts @@ -634,6 +634,13 @@ describe("Glob.match", () => { expect(new Glob("[^a-c]*").match("BewAre")).toBeTrue(); }); + test("square braces", () => { + expect(new Glob("src/*.[tj]s").match("src/foo.js")).toBeTrue(); + expect(new Glob("src/*.[tj]s").match("src/foo.ts")).toBeTrue(); + expect(new Glob("foo/ba[rz].md").match("foo/bar.md")).toBeTrue(); + expect(new Glob("foo/ba[rz].md").match("foo/baz.md")).toBeTrue(); + }); + test("bash wildmatch", () => { expect(new Glob("a[]-]b").match("aab")).toBeFalse(); expect(new Glob("[ten]").match("ten")).toBeFalse(); diff --git a/test/js/bun/globals.test.js b/test/js/bun/globals.test.js index 554f9d7b42033c..e109d04fd2e986 100644 --- a/test/js/bun/globals.test.js +++ b/test/js/bun/globals.test.js @@ -196,3 +196,57 @@ it("errors thrown by native code should be TypeError", async () => { expect(() => Bun.dns.prefetch()).toThrowError(TypeError); expect(async () => await fetch("http://localhost", { body: "123" })).toThrowError(TypeError); }); + +describe("globalThis.gc", () => { + /** + * @param {string} expr + * @param {string[]} args + * @returns {string} + */ + const runAndPrint = (expr, ...args) => { + const result = Bun.spawnSync([bunExe(), ...args, "--print", expr], { + env: bunEnv, + }); + if (!result.success) throw new Error(result.stderr.toString("utf8")); + return result.stdout.toString("utf8").trim(); + }; + + describe("when --expose-gc is not passed", () => { + it("globalThis.gc === undefined", () => { + expect(runAndPrint("typeof globalThis.gc")).toEqual("undefined"); + }); + it(".gc does not take up a property slot", () => { + expect(runAndPrint("'gc' in globalThis")).toEqual("false"); + }); + }); + + describe("when --expose-gc is passed", () => { + it("is a function", () => { + expect(runAndPrint("typeof globalThis.gc", "--expose-gc")).toEqual("function"); + }); + + it("gc is the same as globalThis.gc", () => { + expect(runAndPrint("gc === globalThis.gc", "--expose-gc")).toEqual("true"); + }); + + it("cleans up memory", () => { + const src = /* js */ ` + let arr = [] + for (let i = 0; i < 100; i++) { + arr.push(new Array(100_000)); + } + arr.length = 0; + + const before = process.memoryUsage().heapUsed; + globalThis.gc(); + const after = process.memoryUsage().heapUsed; + return before - after; + `; + const expr = /* js */ `(function() { ${src} })()`; + + const delta = Number.parseInt(runAndPrint(expr, "--expose-gc")); + expect(delta).not.toBeNaN(); + expect(delta).toBeGreaterThanOrEqual(0); + }); + }); +}); diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts index de430f36e39b1c..1c49bc8bff9853 100644 --- a/test/js/bun/http/bun-server.test.ts +++ b/test/js/bun/http/bun-server.test.ts @@ -1,6 +1,6 @@ import type { Server, ServerWebSocket, Socket } from "bun"; import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, rejectUnauthorizedScope } from "harness"; +import { bunEnv, bunExe, rejectUnauthorizedScope, tempDirWithFiles } from "harness"; import path from "path"; describe("Server", () => { @@ -317,8 +317,7 @@ describe("Server", () => { } }); - - test('server should return a body for a OPTIONS Request', async () => { + test("server should return a body for a OPTIONS Request", async () => { using server = Bun.serve({ port: 0, fetch(req) { @@ -327,16 +326,17 @@ describe("Server", () => { }); { const url = `http://${server.hostname}:${server.port}/`; - const response = await fetch(new Request(url, { - method: 'OPTIONS', - })); + const response = await fetch( + new Request(url, { + method: "OPTIONS", + }), + ); expect(await response.text()).toBe("Hello World!"); expect(response.status).toBe(200); expect(response.url).toBe(url); } }); - test("abort signal on server with stream", async () => { { let signalOnServer = false; @@ -456,7 +456,7 @@ describe("Server", () => { env: bunEnv, stderr: "pipe", }); - expect(stderr.toString('utf-8')).toBeEmpty(); + expect(stderr.toString("utf-8")).toBeEmpty(); expect(exitCode).toBe(0); }); }); @@ -768,3 +768,282 @@ test.skip("should be able to stream huge amounts of data", async () => { expect(written).toBe(CONTENT_LENGTH); expect(received).toBe(CONTENT_LENGTH); }, 30_000); + +describe("HEAD requests #15355", () => { + test("should be able to make HEAD requests with content-length or transfer-encoding (async)", async () => { + using server = Bun.serve({ + port: 0, + async fetch(req) { + await Bun.sleep(1); + if (req.method === "HEAD") { + if (req.url.endsWith("/content-length")) { + return new Response(null, { + headers: { + "Content-Length": "11", + }, + }); + } + return new Response(null, { + headers: { + "Transfer-Encoding": "chunked", + }, + }); + } + if (req.url.endsWith("/content-length")) { + return new Response("Hello World"); + } + return new Response(async function* () { + yield "Hello"; + await Bun.sleep(1); + yield " "; + await Bun.sleep(1); + yield "World"; + }); + }, + }); + + { + const response = await fetch(server.url + "/content-length"); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe("Hello World"); + } + { + const response = await fetch(server.url + "/chunked"); + expect(response.status).toBe(200); + expect(response.headers.get("transfer-encoding")).toBe("chunked"); + expect(await response.text()).toBe("Hello World"); + } + + { + const response = await fetch(server.url + "/content-length", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe(""); + } + { + const response = await fetch(server.url + "/chunked", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("transfer-encoding")).toBe("chunked"); + expect(await response.text()).toBe(""); + } + }); + + test("should be able to make HEAD requests with content-length or transfer-encoding (sync)", async () => { + using server = Bun.serve({ + port: 0, + fetch(req) { + if (req.method === "HEAD") { + if (req.url.endsWith("/content-length")) { + return new Response(null, { + headers: { + "Content-Length": "11", + }, + }); + } + return new Response(null, { + headers: { + "Transfer-Encoding": "chunked", + }, + }); + } + if (req.url.endsWith("/content-length")) { + return new Response("Hello World"); + } + return new Response(async function* () { + yield "Hello"; + await Bun.sleep(1); + yield " "; + await Bun.sleep(1); + yield "World"; + }); + }, + }); + + { + const response = await fetch(server.url + "/content-length"); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe("Hello World"); + } + { + const response = await fetch(server.url + "/chunked"); + expect(response.status).toBe(200); + expect(response.headers.get("transfer-encoding")).toBe("chunked"); + expect(await response.text()).toBe("Hello World"); + } + + { + const response = await fetch(server.url + "/content-length", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe(""); + } + { + const response = await fetch(server.url + "/chunked", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("transfer-encoding")).toBe("chunked"); + expect(await response.text()).toBe(""); + } + }); + + test("should fallback to the body if content-length is missing in the headers", async () => { + using server = Bun.serve({ + port: 0, + fetch(req) { + if (req.url.endsWith("/content-length")) { + return new Response("Hello World", { + headers: { + "Content-Type": "text/plain", + "X-Bun-Test": "1", + }, + }); + } + + if (req.url.endsWith("/chunked")) { + return new Response( + async function* () { + yield "Hello"; + await Bun.sleep(1); + yield " "; + await Bun.sleep(1); + yield "World"; + }, + { + headers: { + "Content-Type": "text/plain", + "X-Bun-Test": "1", + }, + }, + ); + } + + return new Response(null, { + headers: { + "Content-Type": "text/plain", + "X-Bun-Test": "1", + }, + }); + }, + }); + { + const response = await fetch(server.url + "/content-length", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(response.headers.get("x-bun-test")).toBe("1"); + expect(await response.text()).toBe(""); + } + { + const response = await fetch(server.url + "/chunked", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("transfer-encoding")).toBe("chunked"); + expect(response.headers.get("x-bun-test")).toBe("1"); + expect(await response.text()).toBe(""); + } + { + const response = await fetch(server.url + "/null", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("0"); + expect(response.headers.get("x-bun-test")).toBe("1"); + expect(await response.text()).toBe(""); + } + }); + + test("HEAD requests should not have body", async () => { + const dir = tempDirWithFiles("fsr", { + "hello": "Hello World", + }); + + const filename = path.join(dir, "hello"); + using server = Bun.serve({ + port: 0, + fetch(req) { + if (req.url.endsWith("/file")) { + return new Response(Bun.file(filename)); + } + return new Response("Hello World"); + }, + }); + + { + const response = await fetch(server.url); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe("Hello World"); + } + { + const response = await fetch(server.url + "/file"); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe("Hello World"); + } + + function doHead(server: Server, path: string): Promise<{ headers: string; body: string }> { + const { promise, resolve } = Promise.withResolvers(); + // use node net to make a HEAD request + const net = require("net"); + const url = new URL(server.url); + const socket = net.createConnection(url.port, url.hostname); + socket.write(`HEAD ${path} HTTP/1.1\r\nHost: ${url.hostname}:${url.port}\r\n\r\n`); + let body = ""; + let headers = ""; + socket.on("data", data => { + body += data.toString(); + if (!headers) { + const headerIndex = body.indexOf("\r\n\r\n"); + if (headerIndex !== -1) { + headers = body.slice(0, headerIndex); + body = body.slice(headerIndex + 4); + + setTimeout(() => { + // wait to see if we get extra data + resolve({ headers, body }); + socket.destroy(); + }, 100); + } + } + }); + return promise as Promise<{ headers: string; body: string }>; + } + { + const response = await fetch(server.url, { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe(""); + } + { + const response = await fetch(server.url + "/file", { + method: "HEAD", + }); + expect(response.status).toBe(200); + expect(response.headers.get("content-length")).toBe("11"); + expect(await response.text()).toBe(""); + } + { + const { headers, body } = await doHead(server, "/"); + expect(headers.toLowerCase()).toContain("content-length: 11"); + expect(body).toBe(""); + } + { + const { headers, body } = await doHead(server, "/file"); + expect(headers.toLowerCase()).toContain("content-length: 11"); + expect(body).toBe(""); + } + }); +}); diff --git a/test/js/bun/http/serve-body-leak.test.ts b/test/js/bun/http/serve-body-leak.test.ts index ed40ed810d5112..510a00a07853f1 100644 --- a/test/js/bun/http/serve-body-leak.test.ts +++ b/test/js/bun/http/serve-body-leak.test.ts @@ -1,6 +1,6 @@ import type { Subprocess } from "bun"; import { afterEach, beforeEach, expect, it } from "bun:test"; -import { bunEnv, bunExe, isDebug, isFlaky, isLinux } from "harness"; +import { bunEnv, bunExe, isDebug, isFlaky, isLinux, isWindows } from "harness"; import { join } from "path"; const payload = Buffer.alloc(512 * 1024, "1").toString("utf-8"); // decent size payload to test memory leak @@ -149,7 +149,7 @@ for (const test_info of [ ["should not leak memory when streaming the body and echoing it back", callStreamingEcho, false, 64], ] as const) { const [testName, fn, skip, maxMemoryGrowth] = test_info; - it.todoIf(skip)( + it.todoIf(skip || isFlaky && isWindows)( testName, async () => { const { url, process } = await getURL(); diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index e3735148f3339f..693ce9e808d37b 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -220,7 +220,8 @@ it("should reject on connection error, calling both connectError() and rejecting expect(socket).toBeDefined(); expect(socket.data).toBe(data); expect(error).toBeDefined(); - expect(error.name).toBe("ECONNREFUSED"); + expect(error.name).toBe("Error"); + expect(error.code).toBe("ECONNREFUSED"); expect(error.message).toBe("Failed to connect"); }, data() { @@ -246,7 +247,8 @@ it("should reject on connection error, calling both connectError() and rejecting () => done(new Error("Promise should reject instead")), err => { expect(err).toBeDefined(); - expect(err.name).toBe("ECONNREFUSED"); + expect(err.name).toBe("Error"); + expect(err.code).toBe("ECONNREFUSED"); expect(err.message).toBe("Failed to connect"); done(); @@ -293,7 +295,7 @@ it("should handle connection error", done => { expect(socket).toBeDefined(); expect(socket.data).toBe(data); expect(error).toBeDefined(); - expect(error.name).toBe("ECONNREFUSED"); + expect(error.name).toBe("Error"); expect(error.message).toBe("Failed to connect"); expect((error as any).code).toBe("ECONNREFUSED"); done(); @@ -595,6 +597,7 @@ it("should not call drain before handshake", async () => { }); it("upgradeTLS handles errors", async () => { using server = Bun.serve({ + port: 0, tls, async fetch(req) { return new Response("Hello World"); @@ -699,6 +702,7 @@ it("upgradeTLS handles errors", async () => { }); it("should be able to upgrade to TLS", async () => { using server = Bun.serve({ + port: 0, tls, async fetch(req) { return new Response("Hello World"); diff --git a/test/js/bun/resolve/resolve.test.ts b/test/js/bun/resolve/resolve.test.ts index d2fa6fbb97f561..969449ffd01e82 100644 --- a/test/js/bun/resolve/resolve.test.ts +++ b/test/js/bun/resolve/resolve.test.ts @@ -1,12 +1,8 @@ import { it, expect } from "bun:test"; import { mkdirSync, writeFileSync } from "fs"; -import { join } from "path"; +import { join, sep } from "path"; import { bunExe, bunEnv, tempDirWithFiles, isWindows } from "harness"; import { pathToFileURL } from "bun"; -import { expect, it } from "bun:test"; -import { mkdirSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, tempDirWithFiles } from "harness"; -import { join, sep } from "path"; it("spawn test file", () => { writePackageJSONImportsFixture(); diff --git a/test/js/bun/s3/s3-insecure.test.ts b/test/js/bun/s3/s3-insecure.test.ts new file mode 100644 index 00000000000000..d757fff77b2362 --- /dev/null +++ b/test/js/bun/s3/s3-insecure.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "bun:test"; +import { S3Client } from "bun"; + +describe("s3", async () => { + it("should not fail to connect when endpoint is http and not https", async () => { + using server = Bun.serve({ + port: 0, + async fetch(req) { + return new Response("<>lol!", { + headers: { + "Content-Type": "text/plain", + }, + status: 400, + }); + }, + }); + + const s3 = new S3Client({ + accessKeyId: "test", + secretAccessKey: "test", + endpoint: server.url.href, + bucket: "test", + }); + + const file = s3.file("hello.txt"); + let err; + try { + await file.text(); + } catch (e) { + err = e; + } + // Test we don't get ConnectionRefused + expect(err.code!).toBe("UnknownError"); + }); +}); diff --git a/test/js/bun/s3/s3.leak.test.ts b/test/js/bun/s3/s3.leak.test.ts index 9b25c622cbd246..4f814707223c45 100644 --- a/test/js/bun/s3/s3.leak.test.ts +++ b/test/js/bun/s3/s3.leak.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "bun:test"; import { bunExe, bunEnv, getSecret, tempDirWithFiles } from "harness"; -import type { S3FileOptions } from "bun"; +import type { S3Options } from "bun"; import path from "path"; -const s3Options: S3FileOptions = { +const s3Options: S3Options = { accessKeyId: getSecret("S3_R2_ACCESS_KEY"), secretAccessKey: getSecret("S3_R2_SECRET_KEY"), endpoint: getSecret("S3_R2_ENDPOINT"), diff --git a/test/js/bun/s3/s3.test.ts b/test/js/bun/s3/s3.test.ts index 7ac336fc6c96c6..a99103198076e8 100644 --- a/test/js/bun/s3/s3.test.ts +++ b/test/js/bun/s3/s3.test.ts @@ -1,617 +1,1060 @@ import { describe, expect, it, beforeAll, afterAll } from "bun:test"; -import { bunExe, bunEnv, getSecret, tempDirWithFiles } from "harness"; +import { bunExe, bunEnv, getSecret, tempDirWithFiles, isLinux } from "harness"; import { randomUUID } from "crypto"; -import { S3, s3, file } from "bun"; -import type { S3File, S3FileOptions } from "bun"; +import { S3Client, s3, file, which } from "bun"; +const S3 = (...args) => new S3Client(...args); +import child_process from "child_process"; +import type { S3Options } from "bun"; import path from "path"; -const s3Options: S3FileOptions = { - accessKeyId: getSecret("S3_R2_ACCESS_KEY"), - secretAccessKey: getSecret("S3_R2_SECRET_KEY"), - endpoint: getSecret("S3_R2_ENDPOINT"), -}; -const S3Bucket = getSecret("S3_R2_BUCKET"); +const dockerCLI = which("docker") as string; +function isDockerEnabled(): boolean { + if (!dockerCLI) { + return false; + } -function makePayLoadFrom(text: string, size: number): string { - while (Buffer.byteLength(text) < size) { - text += text; + try { + const info = child_process.execSync(`${dockerCLI} info`, { stdio: ["ignore", "pipe", "inherit"] }); + return info.toString().indexOf("Server Version:") !== -1; + } catch (error) { + return false; } - return text.slice(0, size); } -// 10 MiB big enough to Multipart upload in more than one part -const bigPayload = makePayLoadFrom("Bun is the best runtime ever", 10 * 1024 * 1024); -const bigishPayload = makePayLoadFrom("Bun is the best runtime ever", 1 * 1024 * 1024); - -describe.skipIf(!s3Options.accessKeyId)("s3", () => { - for (let bucketInName of [true, false]) { - describe("fetch", () => { - describe(bucketInName ? "bucket in path" : "bucket in options", () => { - var tmp_filename: string; - const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; - beforeAll(async () => { - tmp_filename = bucketInName ? `s3://${S3Bucket}/${randomUUID()}` : `s3://${randomUUID()}`; - const result = await fetch(tmp_filename, { - method: "PUT", - body: "Hello Bun!", - s3: options, - }); - expect(result.status).toBe(200); - }); +const allCredentials = [ + { + accessKeyId: getSecret("S3_R2_ACCESS_KEY"), + secretAccessKey: getSecret("S3_R2_SECRET_KEY"), + endpoint: getSecret("S3_R2_ENDPOINT"), + bucket: getSecret("S3_R2_BUCKET"), + service: "R2" as string, + }, +]; - afterAll(async () => { - const result = await fetch(tmp_filename, { - method: "DELETE", - s3: options, - }); - expect(result.status).toBe(204); - }); +// TODO: figure out why minio is not creating a bucket on Linux, works on macOS and windows +if (isDockerEnabled() && !isLinux) { + const minio_dir = tempDirWithFiles("minio", {}); + const result = child_process.spawnSync( + "docker", + [ + "run", + "-d", + "--name", + "minio", + "-p", + "9000:9000", + "-p", + "9001:9001", + "-e", + "MINIO_ROOT_USER=minioadmin", + "-e", + "MINIO_ROOT_PASSWORD=minioadmin", + "-v", + `${minio_dir}:/data`, + "minio/minio", + "server", + "--console-address", + ":9001", + "/data", + ], + { + stdio: ["ignore", "pipe", "pipe"], + }, + ); - it("should download file via fetch GET", async () => { - const result = await fetch(tmp_filename, { s3: options }); - expect(result.status).toBe(200); - expect(await result.text()).toBe("Hello Bun!"); - }); + if (result.error) { + if (!result.error.message.includes('The container name "/minio" is already in use by container')) + throw result.error; + } + // wait for minio to be ready + await Bun.sleep(1_000); + + /// create a bucket + child_process.spawnSync(dockerCLI, [`exec`, `minio`, `mc`, `mb`, `http://localhost:9000/buntest`], { + stdio: "ignore", + }); + + allCredentials.push({ + endpoint: "http://localhost:9000", // MinIO endpoint + accessKeyId: "minioadmin", + secretAccessKey: "minioadmin", + bucket: "buntest", + service: "MinIO" as string, + }); +} +for (let credentials of allCredentials) { + describe(`${credentials.service}`, () => { + const s3Options: S3Options = { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + endpoint: credentials.endpoint, + }; + + const S3Bucket = credentials.bucket; + + function makePayLoadFrom(text: string, size: number): string { + while (Buffer.byteLength(text) < size) { + text += text; + } + return text.slice(0, size); + } + + // 10 MiB big enough to Multipart upload in more than one part + const bigPayload = makePayLoadFrom("Bun is the best runtime ever", 10 * 1024 * 1024); + const bigishPayload = makePayLoadFrom("Bun is the best runtime ever", 1 * 1024 * 1024); + + describe.skipIf(!s3Options.accessKeyId)("s3", () => { + for (let bucketInName of [true, false]) { + describe("fetch", () => { + describe(bucketInName ? "bucket in path" : "bucket in options", () => { + var tmp_filename: string; + const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; + beforeAll(async () => { + tmp_filename = bucketInName ? `s3://${S3Bucket}/${randomUUID()}` : `s3://${randomUUID()}`; + const result = await fetch(tmp_filename, { + method: "PUT", + body: "Hello Bun!", + s3: options, + }); + expect(result.status).toBe(200); + }); + + afterAll(async () => { + const result = await fetch(tmp_filename, { + method: "DELETE", + s3: options, + }); + expect(result.status).toBe(204); + }); + + it("should download file via fetch GET", async () => { + const result = await fetch(tmp_filename, { s3: options }); + expect(result.status).toBe(200); + expect(await result.text()).toBe("Hello Bun!"); + }); + + it("should download range", async () => { + const result = await fetch(tmp_filename, { + headers: { "range": "bytes=6-10" }, + s3: options, + }); + expect(result.status).toBe(206); + expect(await result.text()).toBe("Bun!"); + }); + + it("should check if a key exists or content-length", async () => { + const result = await fetch(tmp_filename, { + method: "HEAD", + s3: options, + }); + expect(result.status).toBe(200); // 404 if do not exists + expect(result.headers.get("content-length")).toBe("10"); // content-length + }); + + it("should check if a key does not exist", async () => { + const result = await fetch(tmp_filename + "-does-not-exist", { s3: options }); + expect(result.status).toBe(404); + }); + + it("should be able to set content-type", async () => { + { + const result = await fetch(tmp_filename, { + method: "PUT", + body: "Hello Bun!", + headers: { + "Content-Type": "application/json", + }, + s3: options, + }); + expect(result.status).toBe(200); + const response = await fetch(tmp_filename, { s3: options }); + expect(response.headers.get("content-type")).toStartWith("application/json"); + } + { + const result = await fetch(tmp_filename, { + method: "PUT", + body: "Hello Bun!", + headers: { + "Content-Type": "text/plain", + }, + s3: options, + }); + expect(result.status).toBe(200); + const response = await fetch(tmp_filename, { s3: options }); + expect(response.headers.get("content-type")).toStartWith("text/plain"); + } + }); - it("should download range", async () => { - const result = await fetch(tmp_filename, { - headers: { "range": "bytes=6-10" }, - s3: options, + it("should be able to upload large files", async () => { + // 10 MiB big enough to Multipart upload in more than one part + const buffer = Buffer.alloc(1 * 1024 * 1024, "a"); + { + await fetch(tmp_filename, { + method: "PUT", + body: async function* () { + for (let i = 0; i < 10; i++) { + await Bun.sleep(10); + yield buffer; + } + }, + s3: options, + }).then(res => res.text()); + + const result = await fetch(tmp_filename, { method: "HEAD", s3: options }); + expect(result.status).toBe(200); + expect(result.headers.get("content-length")).toBe((buffer.byteLength * 10).toString()); + } + }, 20_000); }); - expect(result.status).toBe(206); - expect(await result.text()).toBe("Bun!"); }); - it("should check if a key exists or content-length", async () => { - const result = await fetch(tmp_filename, { - method: "HEAD", - s3: options, + describe("Bun.S3Client", () => { + describe(bucketInName ? "bucket in path" : "bucket in options", () => { + const tmp_filename = bucketInName ? `${S3Bucket}/${randomUUID()}` : `${randomUUID()}`; + const options = bucketInName ? null : { bucket: S3Bucket }; + + var bucket = S3(s3Options); + beforeAll(async () => { + const file = bucket.file(tmp_filename, options); + await file.write("Hello Bun!"); + }); + + afterAll(async () => { + const file = bucket.file(tmp_filename, options); + await file.unlink(); + }); + + it("should download file via Bun.s3().text()", async () => { + const file = bucket.file(tmp_filename, options); + const text = await file.text(); + expect(text).toBe("Hello Bun!"); + }); + + it("should download range", async () => { + const file = bucket.file(tmp_filename, options); + const text = await file.slice(6, 10).text(); + expect(text).toBe("Bun!"); + }); + + it("should check if a key exists or content-length", async () => { + const file = bucket.file(tmp_filename, options); + const exists = await file.exists(); + expect(exists).toBe(true); + const stat = await file.stat(); + expect(stat.size).toBe(10); + }); + + it("should check if a key does not exist", async () => { + const file = bucket.file(tmp_filename + "-does-not-exist", options); + const exists = await file.exists(); + expect(exists).toBe(false); + }); + + it("should be able to set content-type", async () => { + { + const s3file = bucket.file(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/css" }); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/css"); + } + { + const s3file = bucket.file(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/plain" }); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/plain"); + } + + { + const s3file = bucket.file(tmp_filename, options); + const writer = s3file.writer({ type: "application/json" }); + writer.write("Hello Bun!"); + await writer.end(); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("application/json"); + } + + { + await bucket.write(tmp_filename, "Hello Bun!", { ...options, type: "application/xml" }); + const response = await fetch(bucket.file(tmp_filename, options).presign()); + expect(response.headers.get("content-type")).toStartWith("application/xml"); + } + }); + + it("should be able to upload large files using bucket.write + readable Request", async () => { + { + await bucket.write( + tmp_filename, + new Request("https://example.com", { + method: "PUT", + body: async function* () { + for (let i = 0; i < 10; i++) { + if (i % 5 === 0) { + await Bun.sleep(10); + } + yield bigishPayload; + } + }, + }), + options, + ); + expect(await bucket.size(tmp_filename, options)).toBe(Buffer.byteLength(bigishPayload) * 10); + } + }, 10_000); + + it("should be able to upload large files in one go using bucket.write", async () => { + { + await bucket.write(tmp_filename, bigPayload, options); + expect(await bucket.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); + expect(await bucket.file(tmp_filename, options).text()).toBe(bigPayload); + } + }, 10_000); + + it("should be able to upload large files in one go using S3File.write", async () => { + { + const s3File = bucket.file(tmp_filename, options); + await s3File.write(bigPayload); + const stat = await s3File.stat(); + expect(stat.size).toBe(Buffer.byteLength(bigPayload)); + expect(await s3File.text()).toBe(bigPayload); + } + }, 10_000); }); - expect(result.status).toBe(200); // 404 if do not exists - expect(result.headers.get("content-length")).toBe("10"); // content-length }); - it("should check if a key does not exist", async () => { - const result = await fetch(tmp_filename + "-does-not-exist", { s3: options }); - expect(result.status).toBe(404); - }); + describe("Bun.file", () => { + describe(bucketInName ? "bucket in path" : "bucket in options", () => { + const tmp_filename = bucketInName ? `s3://${S3Bucket}/${randomUUID()}` : `s3://${randomUUID()}`; + const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; + beforeAll(async () => { + const s3file = file(tmp_filename, options); + await s3file.write("Hello Bun!"); + }); - it("should be able to set content-type", async () => { - { - const result = await fetch(tmp_filename, { - method: "PUT", - body: "Hello Bun!", - headers: { - "Content-Type": "application/json", - }, - s3: options, - }); - expect(result.status).toBe(200); - const response = await fetch(tmp_filename, { s3: options }); - expect(response.headers.get("content-type")).toStartWith("application/json"); - } - { - const result = await fetch(tmp_filename, { - method: "PUT", - body: "Hello Bun!", - headers: { - "Content-Type": "text/plain", - }, - s3: options, - }); - expect(result.status).toBe(200); - const response = await fetch(tmp_filename, { s3: options }); - expect(response.headers.get("content-type")).toStartWith("text/plain"); - } - }); + afterAll(async () => { + const s3file = file(tmp_filename, options); + await s3file.unlink(); + }); - it("should be able to upload large files", async () => { - // 10 MiB big enough to Multipart upload in more than one part - const buffer = Buffer.alloc(1 * 1024 * 1024, "a"); - { - await fetch(tmp_filename, { - method: "PUT", - body: async function* () { - for (let i = 0; i < 10; i++) { - await Bun.sleep(10); - yield buffer; - } - }, - s3: options, - }).then(res => res.text()); + it("should download file via Bun.file().text()", async () => { + const s3file = file(tmp_filename, options); + const text = await s3file.text(); + expect(text).toBe("Hello Bun!"); + }); - const result = await fetch(tmp_filename, { method: "HEAD", s3: options }); - expect(result.status).toBe(200); - expect(result.headers.get("content-length")).toBe((buffer.byteLength * 10).toString()); - } - }, 10_000); - }); - }); + it("should download range", async () => { + const s3file = file(tmp_filename, options); + const text = await s3file.slice(6, 10).text(); + expect(text).toBe("Bun!"); + }); - describe("Bun.S3", () => { - describe(bucketInName ? "bucket in path" : "bucket in options", () => { - const tmp_filename = bucketInName ? `${S3Bucket}/${randomUUID()}` : `${randomUUID()}`; - const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; - beforeAll(async () => { - const file = new S3(tmp_filename, options); - await file.write("Hello Bun!"); - }); + it("should check if a key exists or content-length", async () => { + const s3file = file(tmp_filename, options); + const exists = await s3file.exists(); + expect(exists).toBe(true); + const stat = await s3file.stat(); + expect(stat.size).toBe(10); + }); - afterAll(async () => { - const file = new S3(tmp_filename, options); - await file.unlink(); - }); + it("should check if a key does not exist", async () => { + const s3file = file(tmp_filename + "-does-not-exist", options); + const exists = await s3file.exists(); + expect(exists).toBe(false); + }); - it("should download file via Bun.s3().text()", async () => { - const file = new S3(tmp_filename, options); - const text = await file.text(); - expect(text).toBe("Hello Bun!"); - }); + it("should be able to set content-type", async () => { + { + const s3file = file(tmp_filename, { ...options, type: "text/css" }); + await s3file.write("Hello Bun!"); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/css"); + } + { + const s3file = file(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/plain" }); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/plain"); + } - it("should download range", async () => { - const file = new S3(tmp_filename, options); - const text = await file.slice(6, 10).text(); - expect(text).toBe("Bun!"); - }); + { + const s3file = file(tmp_filename, options); + const writer = s3file.writer({ type: "application/json" }); + writer.write("Hello Bun!"); + await writer.end(); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("application/json"); + } + }); - it("should check if a key exists or content-length", async () => { - const file = new S3(tmp_filename, options); - const exists = await file.exists(); - expect(exists).toBe(true); - const contentLength = await file.size; - expect(contentLength).toBe(10); - }); + it("should be able to upload large files in one go using Bun.write", async () => { + { + await Bun.write(file(tmp_filename, options), bigPayload); + expect(await S3Client.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); + expect(await file(tmp_filename, options).text()).toEqual(bigPayload); + } + }, 15_000); - it("should check if a key does not exist", async () => { - const file = new S3(tmp_filename + "-does-not-exist", options); - const exists = await file.exists(); - expect(exists).toBe(false); + it("should be able to upload large files in one go using S3File.write", async () => { + { + const s3File = file(tmp_filename, options); + await s3File.write(bigPayload); + expect(s3File.size).toBeNaN(); + expect(await s3File.text()).toBe(bigPayload); + } + }, 10_000); + }); }); - it("should be able to set content-type", async () => { - { - const s3file = new S3(tmp_filename, { ...options, type: "text/css" }); - await s3file.write("Hello Bun!"); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("text/css"); - } - { - const s3file = new S3(tmp_filename, options); - await s3file.write("Hello Bun!", { type: "text/plain" }); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("text/plain"); - } + describe("Bun.s3", () => { + describe(bucketInName ? "bucket in path" : "bucket in options", () => { + const tmp_filename = bucketInName ? `${S3Bucket}/${randomUUID()}` : `${randomUUID()}`; + const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; + beforeAll(async () => { + const s3file = s3(tmp_filename, options); + await s3file.write("Hello Bun!"); + }); - { - const s3file = new S3(tmp_filename, options); - const writer = s3file.writer({ type: "application/json" }); - writer.write("Hello Bun!"); - await writer.end(); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("application/json"); - } + afterAll(async () => { + const s3file = s3(tmp_filename, options); + await s3file.unlink(); + }); - { - await S3.upload(tmp_filename, "Hello Bun!", { ...options, type: "application/xml" }); - const response = await fetch(s3(tmp_filename, options).presign()); - expect(response.headers.get("content-type")).toStartWith("application/xml"); - } - }); + it("should download file via Bun.s3().text()", async () => { + const s3file = s3(tmp_filename, options); + const text = await s3file.text(); + expect(text).toBe("Hello Bun!"); + }); - it("should be able to upload large files using S3.upload + readable Request", async () => { - { - await S3.upload( - tmp_filename, - new Request("https://example.com", { - method: "PUT", - body: async function* () { - for (let i = 0; i < 10; i++) { - if (i % 5 === 0) { - await Bun.sleep(10); - } - yield bigishPayload; - } - }, - }), - options, - ); - expect(await S3.size(tmp_filename, options)).toBe(Buffer.byteLength(bigishPayload) * 10); - } - }, 10_000); + it("should download range", async () => { + const s3file = s3(tmp_filename, options); + const text = await s3file.slice(6, 10).text(); + expect(text).toBe("Bun!"); + }); - it("should be able to upload large files in one go using S3.upload", async () => { - { - await S3.upload(tmp_filename, bigPayload, options); - expect(await S3.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); - expect(await new S3(tmp_filename, options).text()).toBe(bigPayload); - } - }, 10_000); + it("should check if a key exists or content-length", async () => { + const s3file = s3(tmp_filename, options); + const exists = await s3file.exists(); + expect(exists).toBe(true); + expect(s3file.size).toBeNaN(); + const stat = await s3file.stat(); + expect(stat.size).toBe(10); + expect(stat.etag).toBeDefined(); - it("should be able to upload large files in one go using S3File.write", async () => { - { - const s3File = new S3(tmp_filename, options); - await s3File.write(bigPayload); - expect(await s3File.size).toBe(Buffer.byteLength(bigPayload)); - expect(await s3File.text()).toBe(bigPayload); - } - }, 10_000); - }); - }); + expect(stat.lastModified).toBeDefined(); + }); - describe("Bun.file", () => { - describe(bucketInName ? "bucket in path" : "bucket in options", () => { - const tmp_filename = bucketInName ? `s3://${S3Bucket}/${randomUUID()}` : `s3://${randomUUID()}`; - const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; - beforeAll(async () => { - const s3file = file(tmp_filename, options); - await s3file.write("Hello Bun!"); - }); + it("should check if a key does not exist", async () => { + const s3file = s3(tmp_filename + "-does-not-exist", options); + const exists = await s3file.exists(); + expect(exists).toBe(false); + }); - afterAll(async () => { - const s3file = file(tmp_filename, options); - await s3file.unlink(); - }); + it("presign url", async () => { + const s3file = s3(tmp_filename, options); + const response = await fetch(s3file.presign()); + expect(response.status).toBe(200); + expect(await response.text()).toBe("Hello Bun!"); + }); - it("should download file via Bun.file().text()", async () => { - const s3file = file(tmp_filename, options); - const text = await s3file.text(); - expect(text).toBe("Hello Bun!"); - }); + it("should be able to set content-type", async () => { + { + const s3file = s3(tmp_filename, { ...options, type: "text/css" }); + await s3file.write("Hello Bun!"); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/css"); + } + { + const s3file = s3(tmp_filename, options); + await s3file.write("Hello Bun!", { type: "text/plain" }); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("text/plain"); + } - it("should download range", async () => { - const s3file = file(tmp_filename, options); - const text = await s3file.slice(6, 10).text(); - expect(text).toBe("Bun!"); - }); + { + const s3file = s3(tmp_filename, options); + const writer = s3file.writer({ type: "application/json" }); + writer.write("Hello Bun!"); + await writer.end(); + const response = await fetch(s3file.presign()); + expect(response.headers.get("content-type")).toStartWith("application/json"); + } + }); - it("should check if a key exists or content-length", async () => { - const s3file = file(tmp_filename, options); - const exists = await s3file.exists(); - expect(exists).toBe(true); - const contentLength = await s3file.size; - expect(contentLength).toBe(10); - }); + it("should be able to upload large files in one go using Bun.write", async () => { + { + const s3file = s3(tmp_filename, options); + await Bun.write(s3file, bigPayload); + const stat = await s3file.stat(); + expect(stat.size).toBe(Buffer.byteLength(bigPayload)); + expect(stat.etag).toBeDefined(); - it("should check if a key does not exist", async () => { - const s3file = file(tmp_filename + "-does-not-exist", options); - const exists = await s3file.exists(); - expect(exists).toBe(false); - }); + expect(stat.lastModified).toBeDefined(); + expect(await s3file.text()).toBe(bigPayload); + } + }, 10_000); + + it("should be able to upload large files in one go using S3File.write", async () => { + { + const s3File = s3(tmp_filename, options); + await s3File.write(bigPayload); + const stat = await s3File.stat(); + expect(stat.size).toBe(Buffer.byteLength(bigPayload)); + expect(stat.etag).toBeDefined(); + + expect(stat.lastModified).toBeDefined(); - it("should be able to set content-type", async () => { + expect(await s3File.text()).toBe(bigPayload); + } + }, 10_000); + + describe("readable stream", () => { + afterAll(async () => { + await Promise.all([ + s3(tmp_filename + "-readable-stream", options).unlink(), + s3(tmp_filename + "-readable-stream-big", options).unlink(), + ]); + }); + it("should work with small files", async () => { + const s3file = s3(tmp_filename + "-readable-stream", options); + await s3file.write("Hello Bun!"); + const stream = s3file.stream(); + const reader = stream.getReader(); + let bytes = 0; + let chunks: Array = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + bytes += value?.length ?? 0; + + if (value) chunks.push(value as Buffer); + } + expect(bytes).toBe(10); + expect(Buffer.concat(chunks)).toEqual(Buffer.from("Hello Bun!")); + }); + it("should work with large files ", async () => { + const s3file = s3(tmp_filename + "-readable-stream-big", options); + await s3file.write(bigishPayload); + const stream = s3file.stream(); + const reader = stream.getReader(); + let bytes = 0; + let chunks: Array = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + bytes += value?.length ?? 0; + if (value) chunks.push(value as Buffer); + } + expect(bytes).toBe(Buffer.byteLength(bigishPayload)); + expect(Buffer.concat(chunks).toString()).toBe(bigishPayload); + }, 30_000); + }); + }); + }); + } + describe("special characters", () => { + it("should allow special characters in the path", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + const s3file = s3(`🌈🦄${randomUUID()}.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.exists(); + await s3file.unlink(); + expect().pass(); + }); + it("should allow forward slashes in the path", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + const s3file = s3(`${randomUUID()}/test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.exists(); + await s3file.unlink(); + expect().pass(); + }); + it("should allow backslashes in the path", async () => { + const options = { ...s3Options, bucket: S3Bucket }; + const s3file = s3(`${randomUUID()}\\test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.exists(); + await s3file.unlink(); + expect().pass(); + }); + it("should allow starting with slashs and backslashes", async () => { + const options = { ...s3Options, bucket: S3Bucket }; { - const s3file = file(tmp_filename, { ...options, type: "text/css" }); + const s3file = s3(`/${randomUUID()}test.txt`, options); await s3file.write("Hello Bun!"); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("text/css"); - } - { - const s3file = file(tmp_filename, options); - await s3file.write("Hello Bun!", { type: "text/plain" }); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("text/plain"); + await s3file.unlink(); } - { - const s3file = file(tmp_filename, options); - const writer = s3file.writer({ type: "application/json" }); - writer.write("Hello Bun!"); - await writer.end(); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("application/json"); + const s3file = s3(`\\${randomUUID()}test.txt`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); } + expect().pass(); }); - it("should be able to upload large files in one go using Bun.write", async () => { + it("should allow ending with slashs and backslashes", async () => { + const options = { ...s3Options, bucket: S3Bucket }; { - await Bun.write(file(tmp_filename, options), bigPayload); - expect(await S3.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); - expect(await file(tmp_filename, options).text()).toEqual(bigPayload); + const s3file = s3(`${randomUUID()}/`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); } - }, 15_000); - - it("should be able to upload large files in one go using S3File.write", async () => { { - const s3File = file(tmp_filename, options); - await s3File.write(bigPayload); - expect(await s3File.size).toBe(Buffer.byteLength(bigPayload)); - expect(await s3File.text()).toBe(bigPayload); + const s3file = s3(`${randomUUID()}\\`, options); + await s3file.write("Hello Bun!"); + await s3file.unlink(); } - }, 10_000); + expect().pass(); + }); }); - }); - describe("Bun.s3", () => { - describe(bucketInName ? "bucket in path" : "bucket in options", () => { - const tmp_filename = bucketInName ? `${S3Bucket}/${randomUUID()}` : `${randomUUID()}`; - const options = bucketInName ? s3Options : { ...s3Options, bucket: S3Bucket }; - beforeAll(async () => { - const s3file = s3(tmp_filename, options); - await s3file.write("Hello Bun!"); + describe("static methods", () => { + it("its defined", () => { + expect(S3Client).toBeDefined(); + expect(S3Client.write).toBeDefined(); + expect(S3Client.file).toBeDefined(); + expect(S3Client.stat).toBeDefined(); + expect(S3Client.unlink).toBeDefined(); + expect(S3Client.exists).toBeDefined(); + expect(S3Client.presign).toBeDefined(); + expect(S3Client.size).toBeDefined(); + expect(S3Client.delete).toBeDefined(); + }); + it("should work", async () => { + const filename = randomUUID() + ".txt"; + await S3Client.write(filename, "Hello Bun!", { ...s3Options, bucket: S3Bucket }); + expect(await S3Client.file(filename, { ...s3Options, bucket: S3Bucket }).text()).toBe("Hello Bun!"); + const stat = await S3Client.stat(filename, { ...s3Options, bucket: S3Bucket }); + expect(stat.size).toBe(10); + expect(stat.etag).toBeString(); + expect(stat.lastModified).toBeValidDate(); + expect(stat.type).toBe("text/plain;charset=utf-8"); + const url = S3Client.presign(filename, { ...s3Options, bucket: S3Bucket }); + expect(url).toBeDefined(); + const response = await fetch(url); + expect(response.status).toBe(200); + expect(await response.text()).toBe("Hello Bun!"); + await S3Client.unlink(filename, { ...s3Options, bucket: S3Bucket }); + expect().pass(); + }); + }); + describe("errors", () => { + it("Bun.write(s3file, file) should throw if the file does not exist", async () => { + try { + await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), file("./do-not-exist.txt")); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ENOENT"); + expect(e?.path).toBe("./do-not-exist.txt"); + expect(e?.syscall).toBe("open"); + } }); - afterAll(async () => { - const s3file = s3(tmp_filename, options); - await s3file.unlink(); + it("Bun.write(s3file, file) should work with empty file", async () => { + const dir = tempDirWithFiles("fsr", { + "hello.txt": "", + }); + await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), file(path.join(dir, "hello.txt"))); + }); + it("Bun.write(s3file, file) should throw if the file does not exist", async () => { + try { + await Bun.write( + s3("test.txt", { ...s3Options, bucket: S3Bucket }), + s3("do-not-exist.txt", { ...s3Options, bucket: S3Bucket }), + ); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("NoSuchKey"); + expect(e?.path).toBe("do-not-exist.txt"); + expect(e?.name).toBe("S3Error"); + } + }); + it("Bun.write(s3file, file) should throw if the file does not exist", async () => { + try { + await Bun.write( + s3("test.txt", { ...s3Options, bucket: S3Bucket }), + s3("do-not-exist.txt", { ...s3Options, bucket: "does-not-exists" }), + ); + expect.unreachable(); + } catch (e: any) { + expect(["AccessDenied", "NoSuchBucket"]).toContain(e?.code); + expect(e?.path).toBe("do-not-exist.txt"); + expect(e?.name).toBe("S3Error"); + } + }); + it("should error if bucket is missing", async () => { + try { + await Bun.write(s3("test.txt", s3Options), "Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_PATH"); + expect(e?.name).toBe("S3Error"); + } }); - it("should download file via Bun.s3().text()", async () => { - const s3file = s3(tmp_filename, options); - const text = await s3file.text(); - expect(text).toBe("Hello Bun!"); + it("should error if bucket is missing on payload", async () => { + try { + await Bun.write(s3("test.txt", { ...s3Options, bucket: S3Bucket }), s3("test2.txt", s3Options)); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_PATH"); + expect(e?.path).toBe("test2.txt"); + expect(e?.name).toBe("S3Error"); + } }); - it("should download range", async () => { - const s3file = s3(tmp_filename, options); - const text = await s3file.slice(6, 10).text(); - expect(text).toBe("Bun!"); + it("should error when invalid method", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + const s3file = fn("method-test", { + ...s3Options, + bucket: S3Bucket, + }); + + try { + await s3file.presign({ method: "OPTIONS" }); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_METHOD"); + } + }), + ); }); - it("should check if a key exists or content-length", async () => { - const s3file = s3(tmp_filename, options); - const exists = await s3file.exists(); - expect(exists).toBe(true); - const contentLength = await s3file.size; - expect(contentLength).toBe(10); + it("should error when path is too long", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + try { + const s3file = fn("test" + "a".repeat(4096), { + ...s3Options, + bucket: S3Bucket, + }); + + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(["ENAMETOOLONG", "ERR_S3_INVALID_PATH"]).toContain(e?.code); + } + }), + ); }); + }); + describe("credentials", () => { + it("should error with invalid access key id", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + accessKeyId: "invalid", + }); - it("should check if a key does not exist", async () => { - const s3file = s3(tmp_filename + "-does-not-exist", options); - const exists = await s3file.exists(); - expect(exists).toBe(false); + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(["InvalidAccessKeyId", "InvalidArgument"]).toContain(e?.code); + } + }), + ); + }); + it("should error with invalid secret key id", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + secretAccessKey: "invalid", + }); + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(["SignatureDoesNotMatch", "AccessDenied"]).toContain(e?.code); + } + }), + ); }); - it("presign url", async () => { - const s3file = s3(tmp_filename, options); - const response = await fetch(s3file.presign()); - expect(response.status).toBe(200); - expect(await response.text()).toBe("Hello Bun!"); + it("should error with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + try { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + endpoint: "🙂.🥯", + }); + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_INVALID_ARG_TYPE"); + } + }), + ); + }); + it("should error with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + try { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, // credentials and endpoint dont match + endpoint: "s3.us-west-1.amazonaws.com", + }); + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("PermanentRedirect"); + } + }), + ); + }); + it("should error with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + try { + const s3file = fn("s3://bucket/credentials-test", { + ...s3Options, + endpoint: "..asd.@%&&&%%", + }); + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_INVALID_ARG_TYPE"); + } + }), + ); }); - it("should be able to set content-type", async () => { - { - const s3file = s3(tmp_filename, { ...options, type: "text/css" }); - await s3file.write("Hello Bun!"); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("text/css"); - } - { - const s3file = s3(tmp_filename, options); - await s3file.write("Hello Bun!", { type: "text/plain" }); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("text/plain"); - } + it("should error with invalid bucket", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + const s3file = fn("s3://credentials-test", { + ...s3Options, + bucket: "invalid", + }); - { - const s3file = s3(tmp_filename, options); - const writer = s3file.writer({ type: "application/json" }); - writer.write("Hello Bun!"); - await writer.end(); - const response = await fetch(s3file.presign()); - expect(response.headers.get("content-type")).toStartWith("application/json"); - } + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(["AccessDenied", "NoSuchBucket"]).toContain(e?.code); + expect(e?.name).toBe("S3Error"); + } + }), + ); }); - it("should be able to upload large files in one go using S3.upload", async () => { - { - await S3.upload(s3(tmp_filename, options), bigPayload); - expect(await S3.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); - } - }, 10_000); - - it("should be able to upload large files in one go using Bun.write", async () => { - { - await Bun.write(s3(tmp_filename, options), bigPayload); - expect(await S3.size(tmp_filename, options)).toBe(Buffer.byteLength(bigPayload)); - expect(await s3(tmp_filename, options).text()).toBe(bigPayload); - } - }, 10_000); + it("should error when missing credentials", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path), file].map(async fn => { + const s3file = fn("s3://credentials-test", { + bucket: "invalid", + }); - it("should be able to upload large files in one go using S3File.write", async () => { - { - const s3File = s3(tmp_filename, options); - await s3File.write(bigPayload); - expect(await s3File.size).toBe(Buffer.byteLength(bigPayload)); - expect(await s3File.text()).toBe(bigPayload); - } - }, 10_000); - - describe("readable stream", () => { - afterAll(async () => { - await Promise.all([ - s3(tmp_filename + "-readable-stream", options).unlink(), - s3(tmp_filename + "-readable-stream-big", options).unlink(), - ]); - }); - it("should work with small files", async () => { - const s3file = s3(tmp_filename + "-readable-stream", options); - await s3file.write("Hello Bun!"); - const stream = s3file.stream(); - const reader = stream.getReader(); - let bytes = 0; - let chunks: Array = []; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - bytes += value?.length ?? 0; - - if (value) chunks.push(value as Buffer); - } - expect(bytes).toBe(10); - expect(Buffer.concat(chunks)).toEqual(Buffer.from("Hello Bun!")); - }); - it("should work with large files ", async () => { - const s3file = s3(tmp_filename + "-readable-stream-big", options); - await s3file.write(bigishPayload); - const stream = s3file.stream(); - const reader = stream.getReader(); - let bytes = 0; - let chunks: Array = []; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - bytes += value?.length ?? 0; - if (value) chunks.push(value as Buffer); - } - expect(bytes).toBe(Buffer.byteLength(bigishPayload)); - expect(Buffer.concat(chunks).toString()).toBe(bigishPayload); - }, 30_000); + try { + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_MISSING_CREDENTIALS"); + } + }), + ); }); - }); - }); - } + it("should error when presign missing credentials", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + const s3file = fn("method-test", { + bucket: S3Bucket, + }); - describe("credentials", () => { - it("should error with invalid access key id", async () => { - [s3, (...args) => new S3(...args), file].forEach(fn => { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - accessKeyId: "invalid", + try { + await s3file.presign(); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_MISSING_CREDENTIALS"); + } + }), + ); }); - expect(s3file.write("Hello Bun!")).rejects.toThrow(); - }); - }); - it("should error with invalid secret key id", async () => { - [s3, (...args) => new S3(...args), file].forEach(fn => { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - secretAccessKey: "invalid", - }); - expect(s3file.write("Hello Bun!")).rejects.toThrow(); - }); - }); - it("should error with invalid endpoint", async () => { - [s3, (...args) => new S3(...args), file].forEach(fn => { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - endpoint: "🙂.🥯", - }); - expect(s3file.write("Hello Bun!")).rejects.toThrow(); - }); - }); + it("should error when presign with invalid endpoint", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + let options = { ...s3Options, bucket: S3Bucket }; + options.endpoint = Buffer.alloc(1024, "a").toString(); - it("should error with invalid endpoint", async () => { - [s3, (...args) => new S3(...args), file].forEach(fn => { - const s3file = fn("s3://bucket/credentials-test", { - ...s3Options, - endpoint: "..asd.@%&&&%%", + try { + const s3file = fn(randomUUID(), options); + + await s3file.write("Hello Bun!"); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_ENDPOINT"); + } + }), + ); }); - expect(s3file.write("Hello Bun!")).rejects.toThrow(); - }); - }); + it("should error when presign with invalid token", async () => { + await Promise.all( + [s3, (path, ...args) => S3(...args).file(path)].map(async fn => { + let options = { ...s3Options, bucket: S3Bucket }; + options.sessionToken = Buffer.alloc(4096, "a").toString(); - it("should error with invalid bucket", async () => { - [s3, (...args) => new S3(...args), file].forEach(fn => { - const s3file = fn("s3://credentials-test", { - ...s3Options, - bucket: "invalid", + try { + const s3file = fn(randomUUID(), options); + await s3file.presign(); + expect.unreachable(); + } catch (e: any) { + expect(e?.code).toBe("ERR_S3_INVALID_SESSION_TOKEN"); + } + }), + ); }); - expect(s3file.write("Hello Bun!")).rejects.toThrow(); }); - }); - }); - describe("S3 static methods", () => { - describe("presign", () => { - it("should work", async () => { - const s3file = s3("s3://bucket/credentials-test", s3Options); - const url = s3file.presign(); - expect(url).toBeDefined(); - expect(url.includes("X-Amz-Expires=86400")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); - it("should work with expires", async () => { - const s3file = s3("s3://bucket/credentials-test", s3Options); - const url = s3file.presign({ - expiresIn: 10, - }); - expect(url).toBeDefined(); - expect(url.includes("X-Amz-Expires=10")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); + describe("S3 static methods", () => { + describe("presign", () => { + it("should work", async () => { + const s3file = s3("s3://bucket/credentials-test", s3Options); + const url = s3file.presign(); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=86400")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("default endpoint and region should work", async () => { + let options = { ...s3Options }; + options.endpoint = undefined; + options.region = undefined; + const s3file = s3("s3://bucket/credentials-test", options); + const url = s3file.presign(); + expect(url).toBeDefined(); + expect(url.includes("https://s3.us-east-1.amazonaws.com")).toBe(true); + expect(url.includes("X-Amz-Expires=86400")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("default endpoint + region should work", async () => { + let options = { ...s3Options }; + options.endpoint = undefined; + options.region = "us-west-1"; + const s3file = s3("s3://bucket/credentials-test", options); + const url = s3file.presign(); + expect(url).toBeDefined(); + expect(url.includes("https://s3.us-west-1.amazonaws.com")).toBe(true); + expect(url.includes("X-Amz-Expires=86400")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("should work with expires", async () => { + const s3file = s3("s3://bucket/credentials-test", s3Options); + const url = s3file.presign({ + expiresIn: 10, + }); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + it("should work with acl", async () => { + const s3file = s3("s3://bucket/credentials-test", s3Options); + const url = s3file.presign({ + expiresIn: 10, + acl: "public-read", + }); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Acl=public-read")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); - it("S3.presign should work", async () => { - const url = S3.presign("s3://bucket/credentials-test", { - ...s3Options, - expiresIn: 10, - }); - expect(url).toBeDefined(); - expect(url.includes("X-Amz-Expires=10")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); + it("s3().presign() should work", async () => { + const url = s3("s3://bucket/credentials-test", s3Options).presign({ + expiresIn: 10, + }); + expect(url).toBeDefined(); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); - it("S3.presign endpoint should work", async () => { - const url = S3.presign("s3://bucket/credentials-test", { - ...s3Options, - expiresIn: 10, - endpoint: "https://s3.bun.sh", - }); - expect(url).toBeDefined(); - expect(url.includes("https://s3.bun.sh")).toBe(true); - expect(url.includes("X-Amz-Expires=10")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); + it("s3().presign() endpoint should work", async () => { + const url = s3("s3://bucket/credentials-test", s3Options).presign({ + expiresIn: 10, + endpoint: "https://s3.bun.sh", + }); + expect(url).toBeDefined(); + expect(url.includes("https://s3.bun.sh")).toBe(true); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); - it("S3.presign endpoint should work", async () => { - const url = S3.presign("s3://folder/credentials-test", { - ...s3Options, - expiresIn: 10, - bucket: "my-bucket", - }); - expect(url).toBeDefined(); - expect(url.includes("my-bucket")).toBe(true); - expect(url.includes("X-Amz-Expires=10")).toBe(true); - expect(url.includes("X-Amz-Date")).toBe(true); - expect(url.includes("X-Amz-Signature")).toBe(true); - expect(url.includes("X-Amz-Credential")).toBe(true); - expect(url.includes("X-Amz-Algorithm")).toBe(true); - expect(url.includes("X-Amz-SignedHeaders")).toBe(true); - }); - }); + it("s3().presign() endpoint should work", async () => { + const url = s3("s3://folder/credentials-test", s3Options).presign({ + expiresIn: 10, + bucket: "my-bucket", + }); + expect(url).toBeDefined(); + expect(url.includes("my-bucket")).toBe(true); + expect(url.includes("X-Amz-Expires=10")).toBe(true); + expect(url.includes("X-Amz-Date")).toBe(true); + expect(url.includes("X-Amz-Signature")).toBe(true); + expect(url.includes("X-Amz-Credential")).toBe(true); + expect(url.includes("X-Amz-Algorithm")).toBe(true); + expect(url.includes("X-Amz-SignedHeaders")).toBe(true); + }); + }); - it("exists, upload, size, unlink should work", async () => { - const filename = randomUUID(); - const fullPath = `s3://${S3Bucket}/${filename}`; - expect(await S3.exists(fullPath, s3Options)).toBe(false); + it("exists, write, size, unlink should work", async () => { + const fullPath = randomUUID(); + const bucket = S3({ + ...s3Options, + bucket: S3Bucket, + }); + expect(await bucket.exists(fullPath)).toBe(false); - await S3.upload(fullPath, "bun", s3Options); - expect(await S3.exists(fullPath, s3Options)).toBe(true); - expect(await S3.size(fullPath, s3Options)).toBe(3); - await S3.unlink(fullPath, s3Options); - expect(await S3.exists(fullPath, s3Options)).toBe(false); - }); + await bucket.write(fullPath, "bun"); + expect(await bucket.exists(fullPath)).toBe(true); + expect(await bucket.size(fullPath)).toBe(3); + await bucket.unlink(fullPath); + expect(await bucket.exists(fullPath)).toBe(false); + }); + + it("should be able to upload a slice", async () => { + const filename = randomUUID(); + const fullPath = `s3://${S3Bucket}/${filename}`; + const s3file = s3(fullPath, s3Options); + await s3file.write("Hello Bun!"); + const slice = s3file.slice(6, 10); + expect(await slice.text()).toBe("Bun!"); + expect(await s3file.text()).toBe("Hello Bun!"); - it("should be able to upload a slice", async () => { - const filename = randomUUID(); - const fullPath = `s3://${S3Bucket}/${filename}`; - const s3file = s3(fullPath, s3Options); - await s3file.write("Hello Bun!"); - const slice = s3file.slice(6, 10); - expect(await slice.text()).toBe("Bun!"); - expect(await s3file.text()).toBe("Hello Bun!"); - - await S3.upload(fullPath, slice, s3Options); - const text = await s3file.text(); - expect(text).toBe("Bun!"); - await s3file.unlink(); + await s3file.write(slice); + const text = await s3file.text(); + expect(text).toBe("Bun!"); + await s3file.unlink(); + }); + }); }); }); -}); +} diff --git a/test/js/bun/spawn/spawn-env.test.ts b/test/js/bun/spawn/spawn-env.test.ts new file mode 100644 index 00000000000000..5d2e34cc0eed13 --- /dev/null +++ b/test/js/bun/spawn/spawn-env.test.ts @@ -0,0 +1,24 @@ +import { test, expect } from "bun:test"; +import { spawn } from "bun"; +import { bunExe } from "harness"; + +test("spawn env", async () => { + const env = {}; + Object.defineProperty(env, "LOL", { + get() { + throw new Error("Bad!!"); + }, + configurable: false, + enumerable: true, + }); + + // This was the minimum to reliably cause a crash in Bun < v1.1.42 + for (let i = 0; i < 1024 * 10; i++) { + try { + const result = spawn({ + env, + cmd: [bunExe(), "-e", "console.log(process.env.LOL)"], + }); + } catch (e) {} + } +}); diff --git a/test/js/bun/spawn/spawn-path.test.ts b/test/js/bun/spawn/spawn-path.test.ts new file mode 100644 index 00000000000000..d47876c33e07e2 --- /dev/null +++ b/test/js/bun/spawn/spawn-path.test.ts @@ -0,0 +1,26 @@ +import { test, expect } from "bun:test"; +import { chmodSync } from "fs"; +import { isWindows, tempDirWithFiles, bunEnv } from "harness"; +import path from "path"; + +test.skipIf(isWindows)("spawn uses PATH from env if present", async () => { + const tmpDir = await tempDirWithFiles("spawn-path", { + "test-script": `#!/usr/bin/env bash +echo "hello from script"`, + }); + + chmodSync(path.join(tmpDir, "test-script"), 0o777); + + const proc = Bun.spawn(["test-script"], { + env: { + ...bunEnv, + PATH: tmpDir + ":" + bunEnv.PATH, + }, + }); + + const output = await new Response(proc.stdout).text(); + expect(output.trim()).toBe("hello from script"); + + const status = await proc.exited; + expect(status).toBe(0); +}); diff --git a/test/js/bun/test/__snapshots__/test-test.test.ts.snap b/test/js/bun/test/__snapshots__/test-test.test.ts.snap index aad0206a46e6b4..d07cc41e5add0e 100644 --- a/test/js/bun/test/__snapshots__/test-test.test.ts.snap +++ b/test/js/bun/test/__snapshots__/test-test.test.ts.snap @@ -13,7 +13,7 @@ my-test.test.js: 5 | throw new Error('## stage beforeAll ##'); ^ error: ## stage beforeAll ## - at /my-test.test.js:5:11 + at (/my-test.test.js:5:11) ------------------------------- (pass) my-test @@ -35,7 +35,7 @@ my-test.test.js: 5 | throw new Error('## stage beforeEach ##'); ^ error: ## stage beforeEach ## - at /my-test.test.js:5:11 + at (/my-test.test.js:5:11) (fail) my-test 0 pass @@ -58,7 +58,7 @@ my-test.test.js: 5 | throw new Error('## stage afterEach ##'); ^ error: ## stage afterEach ## - at /my-test.test.js:5:11 + at (/my-test.test.js:5:11) ------------------------------- @@ -83,7 +83,7 @@ my-test.test.js: 5 | throw new Error('## stage afterAll ##'); ^ error: ## stage afterAll ## - at /my-test.test.js:5:11 + at (/my-test.test.js:5:11) ------------------------------- @@ -107,7 +107,7 @@ my-test.test.js: 5 | throw new Error('## stage describe ##'); ^ error: ## stage describe ## - at /my-test.test.js:5:11 + at (/my-test.test.js:5:11) at /my-test.test.js:3:1 ------------------------------- diff --git a/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts b/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts index fc014b9faf818d..d211fd4c19ebb3 100644 --- a/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts +++ b/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts @@ -533,7 +533,7 @@ describe("inline snapshots", () => { (\r ${v("", bad, '`"12"`')})\r ; - expect("13").toMatchInlineSnapshot(${v("", bad, '`"13"`')}); expect("14").toMatchInlineSnapshot(${v("", bad, '`"14"`')}); expect("15").toMatchInlineSnapshot(${v("", bad, '`"15"`')}); + expect("13").toMatchInlineSnapshot(${v("", bad, '`"13"`')}); expect("14").toMatchInlineSnapshot(${v("", bad, '`"14"`')}); expect("15").toMatchInlineSnapshot(${v("", bad, '`"15"`')}); expect({a: new Date()}).toMatchInlineSnapshot({a: expect.any(Date)}${v("", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); expect({a: new Date()}).toMatchInlineSnapshot({a: expect.any(Date)}${v(",", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); expect({a: new Date()}).toMatchInlineSnapshot({a: expect.any(Date)\n}${v("", ', "bad"', ', `\n{\n "a": Any,\n}\n`')}); diff --git a/test/js/bun/test/stack.test.ts b/test/js/bun/test/stack.test.ts index dd32267d49091b..2a55d5b0e15974 100644 --- a/test/js/bun/test/stack.test.ts +++ b/test/js/bun/test/stack.test.ts @@ -113,10 +113,11 @@ test("throwing inside an error suppresses the error and continues printing prope const { stderr, exitCode } = result; - expect(stderr.toString().trim()).toStartWith(`ENOENT: No such file or directory + expect(stderr.toString().trim()).toStartWith(`error: No such file or directory path: "this-file-path-is-bad", syscall: "open", errno: -2, + code: "ENOENT", `); expect(exitCode).toBe(1); }); diff --git a/test/js/bun/typescript/type-export.test.ts b/test/js/bun/typescript/type-export.test.ts new file mode 100644 index 00000000000000..2f44ac4577196e --- /dev/null +++ b/test/js/bun/typescript/type-export.test.ts @@ -0,0 +1,174 @@ +import { describe, test, expect } from "bun:test" with { todo: "true" }; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; + +/* +Potential solutions: +- Option 1: Make a fake export `export const my_string = undefined;` and make sure it is not enumerable +- Option 2: In b.ts, make javascriptcore skip re-exporting something if it is not found rather than SyntaxErroring + - this won't work because in the import {} export {} case, the error will be on the import +*/ + +const a_file = ` + export type my_string = "1"; + + export type my_value = "2"; + export const my_value = "2"; + + export const my_only = "3"; +`; +const a_no_value = ` + export type my_string = "1"; + export type my_value = "2"; + export const my_only = "3"; +`; +const a_with_value = ` + export type my_string = "1"; + export const my_value = "2"; +`; +const b_files = [ + { + name: "export from", + value: `export { my_string, my_value, my_only } from "./a.ts";`, + }, + { + name: "import then export", + value: ` + import { my_string, my_value, my_only } from "./a.ts"; + export { my_string, my_value, my_only }; + `, + }, + { + name: "export star", + value: `export * from "./a.ts";`, + }, + { + name: "export merge", + value: `export * from "./a_no_value.ts"; export * from "./a_with_value.ts"`, + }, +]; +const c_files = [ + { name: "require", value: `console.log(JSON.stringify(require("./b")));` }, + { name: "import star", value: `import * as b from "./b"; console.log(JSON.stringify(b));` }, + { name: "await import", value: `console.log(JSON.stringify(await import("./b")));` }, + { + name: "import individual", + value: ` + import { my_string, my_value, my_only } from "./b"; + console.log(JSON.stringify({ my_only, my_value })); + `, + }, +]; +for (const b_file of b_files) { + describe(`re-export with ${b_file.name}`, () => { + for (const c_file of c_files) { + describe(`import with ${c_file.name}`, () => { + const dir = tempDirWithFiles("type-export", { + "a.ts": a_file, + "b.ts": b_file.value, + "c.ts": c_file.value, + + "a_no_value.ts": a_no_value, + "a_with_value.ts": a_with_value, + }); + + const runAndVerify = (filename: string) => { + const result = Bun.spawnSync({ + cmd: [bunExe(), "run", filename], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "inherit"], + }); + + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout.toString().trim())).toEqual({ my_value: "2", my_only: "3" }); + }; + + test.todoIf(b_file.name !== "export star" && b_file.name !== "export merge")("run", () => { + runAndVerify("c.ts"); + }); + + test("build", async () => { + const result = Bun.spawnSync({ + cmd: [bunExe(), "build", "--target=bun", "--outfile", "bundle.js", "c.ts"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "inherit", "inherit"], + }); + + expect(result.exitCode).toBe(0); + runAndVerify("bundle.js"); + }); + }); + } + }); +} + +test("import not found", () => { + const dir = tempDirWithFiles("type-export", { + "a.ts": `export const a = 25; export const c = "hello";`, + "b.ts": /*js*/ ` + import { a, b, c } from "./a"; + console.log(a, b, c); + `, + }); + + const result = Bun.spawnSync({ + cmd: [bunExe(), "run", "b.ts"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + + expect(result.stderr?.toString().trim()).toContain("SyntaxError: Export named 'b' not found in module"); + expect({ + exitCode: result.exitCode, + stdout: result.stdout?.toString().trim(), + }).toEqual({ + exitCode: 1, + stdout: "", + }); +}); + +describe("through export merge", () => { + // this isn't allowed, even in typescript (tsc emits "Duplicate identifier 'value'.") + for (const fmt of ["js", "ts"]) { + describe(fmt, () => { + for (const [name, mode] of [ + ["through", "export {value} from './b'; export {value} from './c';"], + ["direct", "export {value} from './b'; export const value = 'abc';"], + ["direct2", "export const value = 'abc'; export {value};"], + ["ns", "export * as value from './c'; export * as value from './c';"], + ]) { + describe(name, () => { + const dir = tempDirWithFiles("type-import", { + ["main." + fmt]: "import {value} from './a'; console.log(value);", + ["a." + fmt]: mode, + ["b." + fmt]: fmt === "ts" ? "export type value = 'b';" : "", + ["c." + fmt]: "export const value = 'c';", + }); + for (const file of ["main." + fmt, "a." + fmt]) { + test(file, () => { + const result = Bun.spawnSync({ + cmd: [bunExe(), file], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + expect(result.stderr?.toString().trim()).toInclude( + file === "a." + fmt + ? 'error: Multiple exports with the same name "value"\n' // bun's syntax error + : "SyntaxError: Cannot export a duplicate name 'value'.\n", // jsc's syntax error + ); + expect(result.exitCode).toBe(1); + }); + } + }); + } + }); + } +}); + +// TODO: +// - check ownkeys from a star import +// - check commonjs cases +// - what happens with `export * from ./a; export * from ./b` where a and b have different definitions of the same name? diff --git a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap index a6a949433dfe1a..eff7103964ba8f 100644 --- a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap +++ b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap @@ -2,7 +2,7 @@ exports[`error.cause 1`] = ` "1 | import { expect, test } from "bun:test"; -2 | +2 | 3 | test("error.cause", () => { 4 | const err = new Error("error 1"); 5 | const err2 = new Error("error 2", { cause: err }); @@ -11,7 +11,7 @@ error: error 2 at [dir]/inspect-error.test.js:5:16 1 | import { expect, test } from "bun:test"; -2 | +2 | 3 | test("error.cause", () => { 4 | const err = new Error("error 1"); ^ @@ -24,7 +24,7 @@ exports[`Error 1`] = ` " 9 | .replaceAll("//", "/"), 10 | ).toMatchSnapshot(); 11 | }); -12 | +12 | 13 | test("Error", () => { 14 | const err = new Error("my message"); ^ @@ -65,7 +65,7 @@ exports[`Error inside minified file (color) 1`] = ` 23 | arguments);c=b;c.s=1;return c.v=g}catch(h){throw g=b,g.s=2,g.v=h,h;}}}; 24 | exports.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+a+".");var f=C({},a.props),d=a.key,e=a.ref,g=a._owner;if(null!=b){void 0!==b.ref&&(e=b.ref,g=K.current);void 0!==b.key&&(d=""+b.key);if(a.type&&a.type.defaultProps)var h=a.type.defaultProps;for(k in b)J.call(b,k)&&!L.hasOwnProperty(k)&&(f[k]=void 0===b[k]&&void 0!==h?h[k]:b[k])}var k=arguments.length-2;if(1===k)f.children=c;else if(1 { 5 | const err2 = new Error("error 2", { cause: err }); ^ error: error 2 - at [dir]/inspect-error.test.js:5:16 + at ([dir]/inspect-error.test.js:5:16) 1 | import { expect, test, describe, jest } from "bun:test"; 2 | @@ -23,7 +23,7 @@ error: error 2 4 | const err = new Error("error 1"); ^ error: error 1 - at [dir]/inspect-error.test.js:4:15 + at ([dir]/inspect-error.test.js:4:15) " `); }); @@ -43,7 +43,7 @@ test("Error", () => { 32 | const err = new Error("my message"); ^ error: my message - at [dir]/inspect-error.test.js:32:15 + at ([dir]/inspect-error.test.js:32:15) " `); }); @@ -118,9 +118,9 @@ test("Error inside minified file (no color) ", () => { 26 | exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};expo error: error inside long minified file! - at [dir]/inspect-error-fixture.min.js:26:2846 - at [dir]/inspect-error-fixture.min.js:26:2890 - at [dir]/inspect-error.test.js:102:5" + at ([dir]/inspect-error-fixture.min.js:26:2846) + at ([dir]/inspect-error-fixture.min.js:26:2890) + at ([dir]/inspect-error.test.js:102:5)" `); } }); @@ -149,9 +149,9 @@ test("Error inside minified file (color) ", () => { 26 | exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};expo | ... truncated error: error inside long minified file! - at [dir]/inspect-error-fixture.min.js:26:2846 - at [dir]/inspect-error-fixture.min.js:26:2890 - at [dir]/inspect-error.test.js:130:5" + at ([dir]/inspect-error-fixture.min.js:26:2846) + at ([dir]/inspect-error-fixture.min.js:26:2890) + at ([dir]/inspect-error.test.js:130:5)" `); } }); diff --git a/test/js/bun/util/v8-heap-snapshot.test.ts b/test/js/bun/util/v8-heap-snapshot.test.ts new file mode 100644 index 00000000000000..5125b26667ac1d --- /dev/null +++ b/test/js/bun/util/v8-heap-snapshot.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from "bun:test"; +import { tempDirWithFiles } from "harness"; +import { join } from "node:path"; +import * as v8 from "v8"; +import * as v8HeapSnapshot from "v8-heapsnapshot"; + +test("v8 heap snapshot", async () => { + const snapshot = Bun.generateHeapSnapshot("v8"); + // Sanity check: run the validations from this library + const parsed = await v8HeapSnapshot.parseSnapshot(JSON.parse(snapshot)); + + // Loop over all edges and nodes as another sanity check. + for (const edge of parsed.edges) { + if (!edge.to) { + throw new Error("Edge has no 'to' property"); + } + } + for (const node of parsed.nodes) { + if (!node) { + throw new Error("Node is undefined"); + } + } + + expect(parsed.nodes.length).toBeGreaterThan(0); + expect(parsed.edges.length).toBeGreaterThan(0); +}); + +test("v8.getHeapSnapshot()", async () => { + const snapshot = v8.getHeapSnapshot(); + let chunks = []; + for await (const chunk of snapshot) { + expect(chunk.byteLength).toBeGreaterThan(0); + chunks.push(chunk); + } + expect(chunks.length).toBeGreaterThan(0); +}); + +test("v8.writeHeapSnapshot()", async () => { + const path = v8.writeHeapSnapshot(); + expect(path).toBeDefined(); + expect(path).toContain("Heap-"); + + const snapshot = await Bun.file(path).json(); + expect(await v8HeapSnapshot.parseSnapshot(snapshot)).toBeDefined(); +}); + +test("v8.writeHeapSnapshot() with path", async () => { + const dir = tempDirWithFiles("v8-heap-snapshot", { + "test.heapsnapshot": "", + }); + + const path = join(dir, "test.heapsnapshot"); + v8.writeHeapSnapshot(path); + + const snapshot = await Bun.file(path).json(); + expect(await v8HeapSnapshot.parseSnapshot(snapshot)).toBeDefined(); +}); diff --git a/test/js/node/assert/assert-doesNotMatch.test.cjs b/test/js/node/assert/assert-doesNotMatch.test.cjs index 14ffd2eae22ae6..136ea095db0caf 100644 --- a/test/js/node/assert/assert-doesNotMatch.test.cjs +++ b/test/js/node/assert/assert-doesNotMatch.test.cjs @@ -6,7 +6,7 @@ test("doesNotMatch does not throw when not matching", () => { test("doesNotMatch throws when argument is not string", () => { expect(() => assert.doesNotMatch(123, /pass/)).toThrow( - 'The "actual" argument must be of type string. Received type number', + 'The "string" argument must be of type string. Received type number', ); }); diff --git a/test/js/node/assert/assert-match.test.cjs b/test/js/node/assert/assert-match.test.cjs index 4eb097e357c4bd..29f2d117ebba7c 100644 --- a/test/js/node/assert/assert-match.test.cjs +++ b/test/js/node/assert/assert-match.test.cjs @@ -5,7 +5,7 @@ test("match does not throw when matching", () => { }); test("match throws when argument is not string", () => { - expect(() => assert.match(123, /pass/)).toThrow('The "actual" argument must be of type string. Received type number'); + expect(() => assert.match(123, /pass/)).toThrow('The "string" argument must be of type string. Received type number'); }); test("match throws when not matching", () => { diff --git a/test/js/node/bunfig.toml b/test/js/node/bunfig.toml index cac7f387d5cc4a..946890e448ec49 100644 --- a/test/js/node/bunfig.toml +++ b/test/js/node/bunfig.toml @@ -1 +1,2 @@ -preload = ["./harness.ts"] +[test] +preload = ["./harness.ts", "../../preload.ts"] diff --git a/test/js/node/child_process/child_process-node.test.js b/test/js/node/child_process/child_process-node.test.js index 1d4a7a430557a6..ff4699e1e14fc8 100644 --- a/test/js/node/child_process/child_process-node.test.js +++ b/test/js/node/child_process/child_process-node.test.js @@ -659,7 +659,7 @@ describe("fork", () => { code: "ERR_INVALID_ARG_TYPE", name: "TypeError", message: expect.stringContaining( - `The "modulePath" argument must be of type string, Buffer, or URL. Received `, + `The "modulePath" argument must be of type string, Buffer or URL. Received `, ), }), ); diff --git a/test/js/node/child_process/child_process.test.ts b/test/js/node/child_process/child_process.test.ts index a259c6897da066..961f9634d36cd5 100644 --- a/test/js/node/child_process/child_process.test.ts +++ b/test/js/node/child_process/child_process.test.ts @@ -195,8 +195,8 @@ describe("spawn()", () => { it("should allow us to set env", async () => { async function getChildEnv(env: any): Promise { const result: string = await new Promise(resolve => { - const child = spawn(bunExe(), ["-e", "process.stdout.write(JSON.stringify(process.env))"], { env }); - child.stdout.on("data", data => { + const child = spawn(bunExe(), ["-e", "process.stderr.write(JSON.stringify(process.env))"], { env }); + child.stderr.on("data", data => { resolve(data.toString()); }); }); @@ -231,6 +231,7 @@ describe("spawn()", () => { { argv0: bun, stdio: ["inherit", "pipe", "inherit"], + env: bunEnv, }, ); delete process.env.NO_COLOR; diff --git a/test/js/node/dns/node-dns.test.js b/test/js/node/dns/node-dns.test.js index be19e9436e3c04..48977c9ef3634e 100644 --- a/test/js/node/dns/node-dns.test.js +++ b/test/js/node/dns/node-dns.test.js @@ -220,8 +220,6 @@ test("dns.resolveNs (empty string) ", () => { dns.resolveNs("", (err, results) => { try { expect(err).toBeNull(); - console.log("resolveNs:", results); - expect(results instanceof Array).toBe(true); // root servers expect(results.sort()).toStrictEqual( @@ -254,7 +252,6 @@ test("dns.resolvePtr (ptr.socketify.dev)", () => { dns.resolvePtr("ptr.socketify.dev", (err, results) => { try { expect(err).toBeNull(); - console.log("resolvePtr:", results); expect(results instanceof Array).toBe(true); expect(results[0]).toBe("bun.sh"); resolve(); @@ -270,7 +267,6 @@ test("dns.resolveCname (cname.socketify.dev)", () => { dns.resolveCname("cname.socketify.dev", (err, results) => { try { expect(err).toBeNull(); - console.log("resolveCname:", results); expect(results instanceof Array).toBe(true); expect(results[0]).toBe("bun.sh"); resolve(); @@ -427,7 +423,7 @@ describe("test invalid arguments", () => { }).toThrow("Expected address to be a non-empty string for 'lookupService'."); expect(() => { dns.lookupService("google.com", 443, (err, hostname, service) => {}); - }).toThrow("Expected address to be a invalid address for 'lookupService'."); + }).toThrow('The "address" argument is invalid. Received google.com'); }); }); @@ -486,7 +482,7 @@ describe("dns.lookupService", () => { ["1.1.1.1", 80, ["one.one.one.one", "http"]], ["1.1.1.1", 443, ["one.one.one.one", "https"]], ])("promises.lookupService(%s, %d)", async (address, port, expected) => { - const [hostname, service] = await dns.promises.lookupService(address, port); + const { hostname, service } = await dns.promises.lookupService(address, port); expect(hostname).toStrictEqual(expected[0]); expect(service).toStrictEqual(expected[1]); }); diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index d4f52071e52d04..86fc06de3080d7 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -1115,7 +1115,8 @@ it("readdirSync throws when given a file path", () => { readdirSync(import.meta.path); throw new Error("should not get here"); } catch (exception: any) { - expect(exception.name).toBe("ENOTDIR"); + expect(exception.name).toBe("Error"); + expect(exception.code).toBe("ENOTDIR"); } }); @@ -1126,7 +1127,8 @@ it("readdirSync throws when given a path that doesn't exist", () => { } catch (exception: any) { // the correct error to return in this case is actually ENOENT (which we do on windows), // but on posix we return ENOTDIR - expect(exception.name).toMatch(/ENOTDIR|ENOENT/); + expect(exception.name).toBe("Error"); + expect(exception.code).toMatch(/ENOTDIR|ENOENT/); } }); @@ -1135,7 +1137,8 @@ it("readdirSync throws when given a file path with trailing slash", () => { readdirSync(import.meta.path + "/"); throw new Error("should not get here"); } catch (exception: any) { - expect(exception.name).toBe("ENOTDIR"); + expect(exception.name).toBe("Error"); + expect(exception.code).toBe("ENOTDIR"); } }); diff --git a/test/js/node/harness.ts b/test/js/node/harness.ts index f723a749ac1e9e..c13b3a5afc4ce2 100644 --- a/test/js/node/harness.ts +++ b/test/js/node/harness.ts @@ -1,14 +1,15 @@ /** * @note this file patches `node:test` via the require cache. */ -import {AnyFunction} from "bun"; -import {hideFromStackTrace} from "harness"; +import { AnyFunction } from "bun"; +import os from "node:os"; +import { hideFromStackTrace } from "harness"; import assertNode from "node:assert"; type DoneCb = (err?: Error) => any; function noop() {} export function createTest(path: string) { - const {expect, test, it, describe, beforeAll, afterAll, beforeEach, afterEach, mock} = Bun.jest(path); + const { expect, test, it, describe, beforeAll, afterAll, beforeEach, afterEach, mock } = Bun.jest(path); hideFromStackTrace(expect); @@ -204,11 +205,11 @@ export function createTest(path: string) { let completed = 0; const globalTimer = globalTimeout ? (timers.push( - setTimeout(() => { - console.log("Global Timeout"); - done(new Error("Timed out!")); - }, globalTimeout), - ), + setTimeout(() => { + console.log("Global Timeout"); + done(new Error("Timed out!")); + }, globalTimeout), + ), timers[timers.length - 1]) : undefined; function createDoneCb(timeout?: number) { @@ -216,11 +217,11 @@ export function createTest(path: string) { const timer = timeout !== undefined ? (timers.push( - setTimeout(() => { - console.log("Timeout"); - done(new Error("Timed out!")); - }, timeout), - ), + setTimeout(() => { + console.log("Timeout"); + done(new Error("Timed out!")); + }, timeout), + ), timers[timers.length - 1]) : timeout; return (result?: Error) => { @@ -266,9 +267,9 @@ declare namespace Bun { function jest(path: string): typeof import("bun:test"); } -if (Bun.main.includes("node/test/parallel")) { +const normalized = os.platform() === "win32" ? Bun.main.replaceAll("\\", "/") : Bun.main; +if (normalized.includes("node/test/parallel")) { function createMockNodeTestModule() { - interface TestError extends Error { testStack: string[]; } @@ -279,8 +280,8 @@ if (Bun.main.includes("node/test/parallel")) { successes: number; addFailure(err: unknown): TestError; recordSuccess(): void; - } - const contexts: Record = {} + }; + const contexts: Record = {}; // @ts-ignore let activeSuite: Context = undefined; @@ -305,13 +306,13 @@ if (Bun.main.includes("node/test/parallel")) { const fullname = this.testStack.join(" > "); console.log("✅ Test passed:", fullname); this.successes++; - } - } + }, + }; } function getContext() { - const key: string = Bun.main;// module.parent?.filename ?? require.main?.filename ?? __filename; - return activeSuite = (contexts[key] ??= createContext(key)); + const key: string = Bun.main; // module.parent?.filename ?? require.main?.filename ?? __filename; + return (activeSuite = contexts[key] ??= createContext(key)); } async function test(label: string | Function, fn?: Function | undefined) { @@ -333,7 +334,7 @@ if (Bun.main.includes("node/test/parallel")) { } function describe(labelOrFn: string | Function, maybeFn?: Function) { - const [label, fn] = (typeof labelOrFn == "function" ? [labelOrFn.name, labelOrFn] : [labelOrFn, maybeFn]); + const [label, fn] = typeof labelOrFn == "function" ? [labelOrFn.name, labelOrFn] : [labelOrFn, maybeFn]; if (typeof fn !== "function") throw new TypeError("Second argument to describe() must be a function."); getContext().testStack.push(label); @@ -341,7 +342,7 @@ if (Bun.main.includes("node/test/parallel")) { fn(); } catch (e) { getContext().addFailure(e); - throw e + throw e; } finally { getContext().testStack.pop(); } @@ -352,14 +353,12 @@ if (Bun.main.includes("node/test/parallel")) { if (failures > 0) { throw new Error(`${failures} tests failed.`); } - } return { test, describe, - } - + }; } require.cache["node:test"] ??= { diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js index 9c44c9656e461a..ca76ce322a96c9 100644 --- a/test/js/node/module/node-module-module.test.js +++ b/test/js/node/module/node-module-module.test.js @@ -135,3 +135,7 @@ test("Module._resolveLookupPaths", () => { expect(Module._resolveLookupPaths("./bar", { paths: ["a"] })).toEqual(["."]); expect(Module._resolveLookupPaths("bar", { paths: ["a"] })).toEqual(["a"]); }); + +test("Module.findSourceMap doesn't throw", () => { + expect(Module.findSourceMap("foo")).toEqual(undefined); +}); diff --git a/test/js/node/net/node-net-server.test.ts b/test/js/node/net/node-net-server.test.ts index 70034749ed75a4..0572567901611a 100644 --- a/test/js/node/net/node-net-server.test.ts +++ b/test/js/node/net/node-net-server.test.ts @@ -285,7 +285,8 @@ describe("net.createServer listen", () => { expect(err).not.toBeNull(); expect(err!.message).toBe("Failed to connect"); - expect(err!.name).toBe("ECONNREFUSED"); + expect(err!.name).toBe("Error"); + expect(err!.code).toBe("ECONNREFUSED"); server.close(); done(); diff --git a/test/js/node/path/matches-glob.test.ts b/test/js/node/path/matches-glob.test.ts new file mode 100644 index 00000000000000..8802be251bc2d4 --- /dev/null +++ b/test/js/node/path/matches-glob.test.ts @@ -0,0 +1,78 @@ +import path from "path"; + +describe("path.matchesGlob(path, glob)", () => { + const stringLikeObject = { + toString() { + return "hi"; + }, + }; + + it.each([ + // line break + null, + undefined, + 123, + stringLikeObject, + Symbol("hi"), + ])("throws if `path` is not a string", (notAString: any) => { + expect(() => path.matchesGlob(notAString, "*")).toThrow(TypeError); + }); + + it.each([ + // line break + null, + undefined, + 123, + stringLikeObject, + Symbol("hi"), + ])("throws if `glob` is not a string", (notAString: any) => { + expect(() => path.matchesGlob("hi", notAString)).toThrow(TypeError); + }); +}); + +describe("path.posix.matchesGlob(path, glob)", () => { + it.each([ + // line break + ["foo.js", "*.js"], + ["foo.js", "*.[tj]s"], + ["foo.ts", "*.[tj]s"], + ["foo.js", "**/*.js"], + ["src/bar/foo.js", "**/*.js"], + ["foo/bar/baz", "foo/[bcr]ar/baz"], + ])("path '%s' matches pattern '%s'", (pathname, glob) => { + expect(path.posix.matchesGlob(pathname, glob)).toBeTrue(); + }); + it.each([ + // line break + ["foo.js", "*.ts"], + ["src/foo.js", "*.js"], + ["foo.js", "src/*.js"], + ["foo/bar", "*"], + ])("path '%s' does not match pattern '%s'", (pathname, glob) => { + expect(path.posix.matchesGlob(pathname, glob)).toBeFalse(); + }); +}); + +describe("path.win32.matchesGlob(path, glob)", () => { + it.each([ + // line break + ["foo.js", "*.js"], + ["foo.js", "*.[tj]s"], + ["foo.ts", "*.[tj]s"], + ["foo.js", "**\\*.js"], + ["src\\bar\\foo.js", "**\\*.js"], + ["src\\bar\\foo.js", "**/*.js"], + ["foo\\bar\\baz", "foo\\[bcr]ar\\baz"], + ["foo\\bar\\baz", "foo/[bcr]ar/baz"], + ])("path '%s' matches gattern '%s'", (pathname, glob) => { + expect(path.win32.matchesGlob(pathname, glob)).toBeTrue(); + }); + it.each([ + // line break + ["foo.js", "*.ts"], + ["foo.js", "src\\*.js"], + ["foo/bar", "*"], + ])("path '%s' does not match pattern '%s'", (pathname, glob) => { + expect(path.win32.matchesGlob(pathname, glob)).toBeFalse(); + }); +}); diff --git a/test/js/node/process/call-constructor.test.js b/test/js/node/process/call-constructor.test.js new file mode 100644 index 00000000000000..7522966572836a --- /dev/null +++ b/test/js/node/process/call-constructor.test.js @@ -0,0 +1,11 @@ +import { expect, test } from "bun:test"; +import process from "process"; + +test("the constructor of process can be called", () => { + let obj = process.constructor.call({ ...process }); + expect(Object.getPrototypeOf(obj)).toEqual(Object.getPrototypeOf(process)); +}); + +test("#14346", () => { + process.__proto__.constructor.call({}); +}); diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index 70555508479ae0..965105f56bf5a9 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -2,7 +2,7 @@ import { spawnSync, which } from "bun"; import { describe, expect, it } from "bun:test"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness"; -import { basename, join, resolve } from "path"; +import path, { basename, join, resolve } from "path"; import { familySync } from "detect-libc"; expect.extend({ @@ -236,12 +236,16 @@ it("process.uptime()", () => { }); it("process.umask()", () => { - let notNumbers = [265n, "string", true, false, null, {}, [], () => {}, Symbol("symbol"), BigInt(1)]; - for (let notNumber of notNumbers) { - expect(() => { - process.umask(notNumber); - }).toThrow('The "mask" argument must be of type number'); - } + expect(() => process.umask(265n)).toThrow('The "mask" argument must be of type number. Received type bigint (265n)'); + expect(() => process.umask("string")).toThrow(`The argument 'mask' must be a 32-bit unsigned integer or an octal string. Received "string"`); // prettier-ignore + expect(() => process.umask(true)).toThrow('The "mask" argument must be of type number. Received type boolean (true)'); + expect(() => process.umask(false)).toThrow('The "mask" argument must be of type number. Received type boolean (false)'); // prettier-ignore + expect(() => process.umask(null)).toThrow('The "mask" argument must be of type number. Received null'); + expect(() => process.umask({})).toThrow('The "mask" argument must be of type number. Received an instance of Object'); + expect(() => process.umask([])).toThrow('The "mask" argument must be of type number. Received an instance of Array'); + expect(() => process.umask(() => {})).toThrow('The "mask" argument must be of type number. Received function '); + expect(() => process.umask(Symbol("symbol"))).toThrow('The "mask" argument must be of type number. Received type symbol (Symbol(symbol))'); // prettier-ignore + expect(() => process.umask(BigInt(1))).toThrow('The "mask" argument must be of type number. Received type bigint (1n)'); // prettier-ignore let rangeErrors = [NaN, -1.4, Infinity, -Infinity, -1, 1.3, 4294967296]; for (let rangeError of rangeErrors) { @@ -310,20 +314,6 @@ it("process.config", () => { }); }); -it("process.emitWarning", () => { - process.emitWarning("-- Testing process.emitWarning --"); - var called = 0; - process.on("warning", err => { - called++; - expect(err.message).toBe("-- Testing process.on('warning') --"); - }); - process.emitWarning("-- Testing process.on('warning') --"); - expect(called).toBe(1); - expect(process.off("warning")).toBe(process); - process.emitWarning("-- Testing process.on('warning') --"); - expect(called).toBe(1); -}); - it("process.execArgv", () => { expect(process.execArgv instanceof Array).toBe(true); }); @@ -342,11 +332,21 @@ it("process.argv in testing", () => { describe("process.exitCode", () => { it("validates int", () => { - expect(() => (process.exitCode = "potato")).toThrow(`exitCode must be an integer`); - expect(() => (process.exitCode = 1.2)).toThrow("exitCode must be an integer"); - expect(() => (process.exitCode = NaN)).toThrow("exitCode must be an integer"); - expect(() => (process.exitCode = Infinity)).toThrow("exitCode must be an integer"); - expect(() => (process.exitCode = -Infinity)).toThrow("exitCode must be an integer"); + expect(() => (process.exitCode = "potato")).toThrow( + `The "code" argument must be of type number. Received type string ("potato")`, + ); + expect(() => (process.exitCode = 1.2)).toThrow( + `The value of \"code\" is out of range. It must be an integer. Received 1.2`, + ); + expect(() => (process.exitCode = NaN)).toThrow( + `The value of \"code\" is out of range. It must be an integer. Received NaN`, + ); + expect(() => (process.exitCode = Infinity)).toThrow( + `The value of \"code\" is out of range. It must be an integer. Received Infinity`, + ); + expect(() => (process.exitCode = -Infinity)).toThrow( + `The value of \"code\" is out of range. It must be an integer. Received -Infinity`, + ); }); it("works with implicit process.exit", () => { @@ -458,13 +458,13 @@ describe("process.cpuUsage", () => { user: -1, system: 100, }), - ).toThrow("The 'user' property must be a number between 0 and 2^53"); + ).toThrow("The property 'prevValue.user' is invalid. Received -1"); expect(() => process.cpuUsage({ user: 100, system: -1, }), - ).toThrow("The 'system' property must be a number between 0 and 2^53"); + ).toThrow("The property 'prevValue.system' is invalid. Received -1"); }); // Skipped on Windows because it seems UV returns { user: 15000, system: 0 } constantly @@ -684,13 +684,7 @@ it("dlopen accepts file: URLs", () => { }); it("process.constrainedMemory()", () => { - if (process.platform === "linux") { - // On Linux, it returns 0 if the kernel doesn't support it - expect(process.constrainedMemory() >= 0).toBe(true); - } else { - // On unsupported platforms, it returns undefined - expect(process.constrainedMemory()).toBeUndefined(); - } + expect(process.constrainedMemory() >= 0).toBe(true); }); it("process.report", () => { diff --git a/test/js/node/readline/readline.node.test.ts b/test/js/node/readline/readline.node.test.ts index caa38dcfa593e0..fecce0f34d21d1 100644 --- a/test/js/node/readline/readline.node.test.ts +++ b/test/js/node/readline/readline.node.test.ts @@ -306,15 +306,15 @@ describe("readline.cursorTo()", () => { // Verify that cursorTo() throws if x or y is NaN. assert.throws(() => { readline.cursorTo(writable, NaN); - }, /ERR_INVALID_ARG_VALUE/); + }, "ERR_INVALID_ARG_VALUE"); assert.throws(() => { readline.cursorTo(writable, 1, NaN); - }, /ERR_INVALID_ARG_VALUE/); + }, "ERR_INVALID_ARG_VALUE"); assert.throws(() => { readline.cursorTo(writable, NaN, NaN); - }, /ERR_INVALID_ARG_VALUE/); + }, "ERR_INVALID_ARG_VALUE"); }); }); diff --git a/test/js/node/test/common/dns.js b/test/js/node/test/common/dns.js index d854c73629a07c..8fa264dc2c1c77 100644 --- a/test/js/node/test/common/dns.js +++ b/test/js/node/test/common/dns.js @@ -15,6 +15,7 @@ const types = { TXT: 16, ANY: 255, CAA: 257, + SRV: 33, }; const classes = { diff --git a/test/js/node/test/common/index.js b/test/js/node/test/common/index.js index 6b5d1079ffad7b..9c283bb4d4730d 100644 --- a/test/js/node/test/common/index.js +++ b/test/js/node/test/common/index.js @@ -107,7 +107,7 @@ function parseTestFlags(filename = process.argv[1]) { // `worker_threads`) and child processes. // If the binary was built without-ssl then the crypto flags are // invalid (bad option). The test itself should handle this case. -if (process.argv.length === 2 && +if ((process.argv.length === 2 || process.argv.length === 3) && !process.env.NODE_SKIP_FLAG_CHECK && isMainThread && hasCrypto && @@ -132,11 +132,9 @@ if (process.argv.length === 2 && const options = { encoding: 'utf8', stdio: 'inherit' }; const result = spawnSync(process.execPath, args, options); if (result.signal) { - process.kill(0, result.signal); + process.kill(process.pid, result.signal); } else { - // Ensure we don't call the "exit" callbacks, as that will cause the - // test to fail when it may have passed in the child process. - process.kill(process.pid, result.status); + process.exit(result.status); } } } @@ -900,6 +898,7 @@ function invalidArgTypeHelper(input) { let inspected = inspect(input, { colors: false }); if (inspected.length > 28) { inspected = `${inspected.slice(inspected, 0, 25)}...`; } + if (inspected.startsWith("'") && inspected.endsWith("'")) inspected = `"${inspected.slice(1, inspected.length - 1)}"`; // BUN: util.inspect uses ' but bun uses " for strings return ` Received type ${typeof input} (${inspected})`; } @@ -1218,5 +1217,3 @@ module.exports = new Proxy(common, { return obj[prop]; }, }); - - diff --git a/test/js/node/test/parallel/needs-test/README.md b/test/js/node/test/parallel/needs-test/README.md new file mode 100644 index 00000000000000..821ae16ee3cebb --- /dev/null +++ b/test/js/node/test/parallel/needs-test/README.md @@ -0,0 +1,8 @@ +A good deal of parallel test cases can be run directly via `bun `. +However, some newer cases use `node:test`. + +Files in this directory need to be run with `bun test `. The +`node:test` module is shimmed via a require cache hack in +`test/js/node/harness.js` to use `bun:test`. Note that our test runner +(`scripts/runner.node.mjs`) checks for `needs-test` in the names of test files, +so don't rename this folder without updating that code. diff --git a/test/js/node/test/parallel/needs-test/test-assert.js b/test/js/node/test/parallel/needs-test/test-assert.js new file mode 100644 index 00000000000000..d16194b57f3f5e --- /dev/null +++ b/test/js/node/test/parallel/needs-test/test-assert.js @@ -0,0 +1,1601 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const assert = require('node:assert'); + +require('../../../harness'); + +const {invalidArgTypeHelper} = require('../../common'); +const {inspect} = require('util'); +const {test} = require('node:test'); +const vm = require('vm'); +// const { createTest } = require('node-harness'); +// const { test } = createTest(__filename); + +// Disable colored output to prevent color codes from breaking assertion +// message comparisons. This should only be an issue when process.stdout +// is a TTY. +if (process.stdout.isTTY) { + process.env.NODE_DISABLE_COLORS = '1'; +} + +const strictEqualMessageStart = 'Expected values to be strictly equal:\n'; +const start = 'Expected values to be strictly deep-equal:'; +const actExp = '+ actual - expected'; + +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-restricted-properties */ + +test('some basics', () => { + assert.ok(assert.AssertionError.prototype instanceof Error, + 'assert.AssertionError instanceof Error'); + + assert.throws(() => assert(false), assert.AssertionError, 'ok(false)'); + assert.throws(() => assert.ok(false), assert.AssertionError, 'ok(false)'); + assert(true); + assert('test', 'ok(\'test\')'); + assert.ok(true); + assert.ok('test'); + assert.throws(() => assert.equal(true, false), + assert.AssertionError, 'equal(true, false)'); + assert.equal(null, null); + assert.equal(undefined, undefined); + assert.equal(null, undefined); + assert.equal(true, true); + assert.equal(2, '2'); + assert.notEqual(true, false); + assert.notStrictEqual(2, '2'); +}); + +test('Throw message if the message is instanceof Error', () => { + let threw = false; + try { + assert.ok(false, new Error('ok(false)')); + } catch (e) { + threw = true; + assert.ok(e instanceof Error); + } + assert.ok(threw, 'Error: ok(false)'); +}); + +test('Errors created in different contexts are handled as any other custom error', () => { + assert('createContext' in vm, 'vm.createContext is available'); + const context = vm.createContext(); + const error = vm.runInContext('new SyntaxError("custom error")', context); + + assert.throws(() => assert(false, error), { + message: 'custom error', + name: 'SyntaxError' + }); +}); + +test('assert.throws()', () => { + assert.throws(() => assert.notEqual(true, true), + assert.AssertionError, 'notEqual(true, true)'); + + assert.throws(() => assert.strictEqual(2, '2'), + assert.AssertionError, 'strictEqual(2, \'2\')'); + + assert.throws(() => assert.strictEqual(null, undefined), + assert.AssertionError, 'strictEqual(null, undefined)'); + + assert.throws( + () => assert.notStrictEqual(2, 2), + { + message: 'Expected "actual" to be strictly unequal to: 2', + name: 'AssertionError' + } + ); + + assert.throws( + () => assert.notStrictEqual('a '.repeat(30), 'a '.repeat(30)), + { + message: 'Expected "actual" to be strictly unequal to:\n\n' + + `'${'a '.repeat(30)}'`, + name: 'AssertionError' + } + ); + + assert.throws( + () => assert.notEqual(1, 1), + { + message: '1 != 1', + operator: '!=' + } + ); + + // Testing the throwing. + function thrower(errorConstructor) { + throw new errorConstructor({}); + } + + // The basic calls work. + assert.throws(() => thrower(assert.AssertionError), assert.AssertionError, 'message'); + assert.throws(() => thrower(assert.AssertionError), assert.AssertionError); + assert.throws(() => thrower(assert.AssertionError)); + + // If not passing an error, catch all. + assert.throws(() => thrower(TypeError)); + + // When passing a type, only catch errors of the appropriate type. + assert.throws( + () => assert.throws(() => thrower(TypeError), assert.AssertionError), + { + generatedMessage: true, + actual: new TypeError({}), + expected: assert.AssertionError, + code: 'ERR_ASSERTION', + name: 'AssertionError', + operator: 'throws', + message: 'The error is expected to be an instance of "AssertionError". ' + + 'Received "TypeError"\n\nError message:\n\n[object Object]' + } + ); + + // doesNotThrow should pass through all errors. + { + let threw = false; + try { + assert.doesNotThrow(() => thrower(TypeError), assert.AssertionError); + } catch (e) { + threw = true; + assert.ok(e instanceof TypeError); + } + assert(threw, 'assert.doesNotThrow with an explicit error is eating extra errors'); + } + + // Key difference is that throwing our correct error makes an assertion error. + { + let threw = false; + try { + assert.doesNotThrow(() => thrower(TypeError), TypeError); + } catch (e) { + threw = true; + assert.ok(e instanceof assert.AssertionError); + assert.ok(!e.stack.includes('at Function.doesNotThrow')); + } + assert.ok(threw, 'assert.doesNotThrow is not catching type matching errors'); + } + + assert.throws( + () => assert.doesNotThrow(() => thrower(Error), 'user message'), + { + name: 'AssertionError', + code: 'ERR_ASSERTION', + operator: 'doesNotThrow', + message: 'Got unwanted exception: user message\n' + + 'Actual message: "[object Object]"' + } + ); + + assert.throws( + () => assert.doesNotThrow(() => thrower(Error)), + { + code: 'ERR_ASSERTION', + message: 'Got unwanted exception.\nActual message: "[object Object]"' + } + ); + + assert.throws( + () => assert.doesNotThrow(() => thrower(Error), /\[[a-z]{6}\s[A-z]{6}\]/g, 'user message'), + { + name: 'AssertionError', + code: 'ERR_ASSERTION', + operator: 'doesNotThrow', + message: 'Got unwanted exception: user message\n' + + 'Actual message: "[object Object]"' + } + ); + + // Make sure that validating using constructor really works. + { + let threw = false; + try { + assert.throws( + () => { + throw ({}); // eslint-disable-line no-throw-literal + }, + Array + ); + } catch { + threw = true; + } + assert.ok(threw, 'wrong constructor validation'); + } + + // Use a RegExp to validate the error message. + { + assert.throws(() => thrower(TypeError), /\[object Object\]/); + + const symbol = Symbol('foo'); + assert.throws(() => { + throw symbol; + }, /foo/); + + assert.throws(() => { + assert.throws(() => { + throw symbol; + }, /abc/); + }, { + message: 'The input did not match the regular expression /abc/. ' + + "Input:\n\n'Symbol(foo)'\n", + code: 'ERR_ASSERTION', + operator: 'throws', + actual: symbol, + expected: /abc/ + }); + } + + // Use a fn to validate the error object. + assert.throws(() => thrower(TypeError), (err) => { + if ((err instanceof TypeError) && /\[object Object\]/.test(err)) { + return true; + } + }); + + // https://github.com/nodejs/node/issues/3188 + { + let actual; + assert.throws( + () => { + const ES6Error = class extends Error {}; + const AnotherErrorType = class extends Error {}; + + assert.throws(() => { + actual = new AnotherErrorType('foo'); + throw actual; + }, ES6Error); + }, + (err) => { + assert.strictEqual( + err.message, + 'The error is expected to be an instance of "ES6Error". ' + + 'Received "AnotherErrorType"\n\nError message:\n\nfoo' + ); + assert.strictEqual(err.actual, actual); + return true; + } + ); + } + + assert.throws( + () => assert.strictEqual(new Error('foo'), new Error('foobar')), + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'Expected "actual" to be reference-equal to "expected":\n' + + '+ actual - expected\n' + + '\n' + + '+ [Error: foo]\n' + + '- [Error: foobar]\n' + } + ); +}); + +test('Check messages from assert.throws()', () => { + const noop = () => {}; + assert.throws( + () => {assert.throws((noop));}, + { + code: 'ERR_ASSERTION', + message: 'Missing expected exception.', + operator: 'throws', + actual: undefined, + expected: undefined + }); + + assert.throws( + () => {assert.throws(noop, TypeError);}, + { + code: 'ERR_ASSERTION', + message: 'Missing expected exception (TypeError).', + actual: undefined, + expected: TypeError + }); + + assert.throws( + () => {assert.throws(noop, 'fhqwhgads');}, + { + code: 'ERR_ASSERTION', + message: 'Missing expected exception: fhqwhgads', + actual: undefined, + expected: undefined + }); + + assert.throws( + () => {assert.throws(noop, TypeError, 'fhqwhgads');}, + { + code: 'ERR_ASSERTION', + message: 'Missing expected exception (TypeError): fhqwhgads', + actual: undefined, + expected: TypeError + }); + + let threw = false; + try { + assert.throws(noop); + } catch (e) { + threw = true; + assert.ok(e instanceof assert.AssertionError); + assert.ok(!e.stack.includes('at Function.throws')); + } + assert.ok(threw); +}); + +test('Test assertion messages', () => { + const circular = {y: 1}; + circular.x = circular; + + function testAssertionMessage(actual, expected, msg) { + assert.throws( + () => assert.strictEqual(actual, ''), + { + generatedMessage: true, + message: msg || `Expected values to be strictly equal:\n\n${expected} !== ''\n` + } + ); + } + + function testLongAssertionMessage(actual, expected) { + testAssertionMessage(actual, expected, 'Expected values to be strictly equal:\n' + + '+ actual - expected\n' + + '\n' + + `+ ${expected}\n` + + "- ''\n"); + } + + function testShortAssertionMessage(actual, expected) { + testAssertionMessage(actual, expected, strictEqualMessageStart + `\n${inspect(actual)} !== ''\n`); + } + + testShortAssertionMessage(null, 'null'); + testShortAssertionMessage(true, 'true'); + testShortAssertionMessage(false, 'false'); + testShortAssertionMessage(100, '100'); + testShortAssertionMessage(NaN, 'NaN'); + testShortAssertionMessage(Infinity, 'Infinity'); + testShortAssertionMessage('a', '\'a\''); + testShortAssertionMessage('foo', '\'foo\''); + testShortAssertionMessage(0, '0'); + testShortAssertionMessage(Symbol(), 'Symbol()'); + testShortAssertionMessage(undefined, 'undefined'); + testShortAssertionMessage(-Infinity, '-Infinity'); + testShortAssertionMessage([], '[]'); + testShortAssertionMessage({}, '{}'); + testAssertionMessage(/a/, '/a/'); + testAssertionMessage(/abc/gim, '/abc/gim'); + testLongAssertionMessage(function f() {}, '[Function: f]'); + testLongAssertionMessage(function () {}, '[Function (anonymous)]'); + + assert.throws( + () => assert.strictEqual([1, 2, 3], ''), + { + message: 'Expected values to be strictly equal:\n' + + '+ actual - expected\n' + + '\n' + + '+ [\n' + + '+ 1,\n' + + '+ 2,\n' + + '+ 3\n' + + '+ ]\n' + + "- ''\n", + generatedMessage: true + } + ); + + assert.throws( + () => assert.strictEqual(circular, ''), + { + message: 'Expected values to be strictly equal:\n' + + '+ actual - expected\n' + + '\n' + + '+ {\n' + + '+ x: [Circular *1],\n' + + '+ y: 1\n' + + '+ }\n' + + "- ''\n", + generatedMessage: true + } + ); + + assert.throws( + () => assert.strictEqual({a: undefined, b: null}, ''), + { + message: 'Expected values to be strictly equal:\n' + + '+ actual - expected\n' + + '\n' + + '+ {\n' + + '+ a: undefined,\n' + + '+ b: null\n' + + '+ }\n' + + "- ''\n", + generatedMessage: true + } + ); + + assert.throws( + () => assert.strictEqual({a: NaN, b: Infinity, c: -Infinity}, ''), + { + message: 'Expected values to be strictly equal:\n' + + '+ actual - expected\n' + + '\n' + + '+ {\n' + + '+ a: NaN,\n' + + '+ b: Infinity,\n' + + '+ c: -Infinity\n' + + '+ }\n' + + "- ''\n", + generatedMessage: true + } + ); + + // https://github.com/nodejs/node-v0.x-archive/issues/5292 + assert.throws( + () => assert.strictEqual(1, 2), + { + message: 'Expected values to be strictly equal:\n\n1 !== 2\n', + generatedMessage: true + } + ); + + assert.throws( + () => assert.strictEqual(1, 2, 'oh no'), + { + message: 'oh no\n\n1 !== 2\n', + generatedMessage: false + } + ); +}); + +test('Custom errors', () => { + let threw = false; + const rangeError = new RangeError('my range'); + + // Verify custom errors. + try { + assert.strictEqual(1, 2, rangeError); + } catch (e) { + assert.strictEqual(e, rangeError); + threw = true; + assert.ok(e instanceof RangeError, 'Incorrect error type thrown'); + } + assert.ok(threw); + threw = false; + + // Verify AssertionError is the result from doesNotThrow with custom Error. + try { + assert.doesNotThrow(() => { + throw new TypeError('wrong type'); + }, TypeError, rangeError); + } catch (e) { + threw = true; + // assert.ok(e.message.includes(rangeError.message)); + assert.ok(e.actual instanceof TypeError); + assert.equal(e.expected, TypeError); + assert.ok(e instanceof assert.AssertionError); + assert.ok(!e.stack.includes('doesNotThrow'), e); + } + assert.ok(threw); +}); + +test('Verify that throws() and doesNotThrow() throw on non-functions', () => { + const testBlockTypeError = (method, fn) => { + assert.throws( + () => method(fn), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + // message: 'The "fn" argument must be of type function.' + + // invalidArgTypeHelper(fn) + } + ); + }; + + testBlockTypeError(assert.throws, 'string'); + testBlockTypeError(assert.doesNotThrow, 'string'); + testBlockTypeError(assert.throws, 1); + testBlockTypeError(assert.doesNotThrow, 1); + testBlockTypeError(assert.throws, true); + testBlockTypeError(assert.doesNotThrow, true); + testBlockTypeError(assert.throws, false); + testBlockTypeError(assert.doesNotThrow, false); + testBlockTypeError(assert.throws, []); + testBlockTypeError(assert.doesNotThrow, []); + testBlockTypeError(assert.throws, {}); + testBlockTypeError(assert.doesNotThrow, {}); + testBlockTypeError(assert.throws, /foo/); + testBlockTypeError(assert.doesNotThrow, /foo/); + testBlockTypeError(assert.throws, null); + testBlockTypeError(assert.doesNotThrow, null); + testBlockTypeError(assert.throws, undefined); + testBlockTypeError(assert.doesNotThrow, undefined); +}); + +test('https://github.com/nodejs/node/issues/3275', () => { + // eslint-disable-next-line no-throw-literal + assert.throws(() => {throw 'error';}, (err) => err === 'error'); + assert.throws(() => {throw new Error();}, (err) => err instanceof Error); +}); + +test('Long values should be truncated for display', () => { + assert.throws(() => { + assert.strictEqual('A'.repeat(1000), ''); + }, (err) => { + assert.strictEqual(err.code, 'ERR_ASSERTION'); + assert.strictEqual(err.message, + `${strictEqualMessageStart}+ actual - expected\n\n` + + `+ '${'A'.repeat(1000)}'\n- ''\n`); + assert.strictEqual(err.actual.length, 1000); + assert.ok(inspect(err).includes(`actual: '${'A'.repeat(488)}...'`)); + return true; + }); +}); + +test('Output that extends beyond 10 lines should also be truncated for display', () => { + const multilineString = 'fhqwhgads\n'.repeat(15); + assert.throws(() => { + assert.strictEqual(multilineString, ''); + }, (err) => { + assert.strictEqual(err.code, 'ERR_ASSERTION'); + assert.strictEqual(err.message.split('\n').length, 21); + assert.strictEqual(err.actual.split('\n').length, 16); + assert.ok(inspect(err).includes( + "actual: 'fhqwhgads\\n' +\n" + + " 'fhqwhgads\\n' +\n".repeat(9) + + " '...'")); + return true; + }); +}); + +test('Bad args to AssertionError constructor should throw TypeError.', () => { + const args = [1, true, false, '', null, Infinity, Symbol('test'), undefined]; + for (const input of args) { + assert.throws( + () => new assert.AssertionError(input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options" argument must be of type object.' + + invalidArgTypeHelper(input) + }); + } +}); + +test('NaN is handled correctly', () => { + assert.equal(NaN, NaN); + assert.throws( + () => assert.notEqual(NaN, NaN), + assert.AssertionError + ); +}); + +test('Test strict assert', () => { + const {strict} = require('assert'); + + strict.throws(() => strict.equal(1, true), strict.AssertionError); + strict.notEqual(0, false); + strict.throws(() => strict.deepEqual(1, true), strict.AssertionError); + strict.notDeepEqual(0, false); + strict.equal(strict.strict, strict.strict.strict); + strict.equal(strict.equal, strict.strictEqual); + strict.equal(strict.deepEqual, strict.deepStrictEqual); + strict.equal(strict.notEqual, strict.notStrictEqual); + strict.equal(strict.notDeepEqual, strict.notDeepStrictEqual); + strict.equal(Object.keys(strict).length, Object.keys(assert).length); + strict(7); + strict.throws( + () => strict(...[]), + { + message: 'No value argument passed to `assert.ok()`', + name: 'AssertionError', + generatedMessage: true + } + ); + strict.throws( + () => assert(), + { + message: 'No value argument passed to `assert.ok()`', + name: 'AssertionError' + } + ); + + // Test setting the limit to zero and that assert.strict works properly. + const tmpLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + strict.throws( + () => { + strict.ok( + typeof 123 === 'string' + ); + }, + { + code: 'ERR_ASSERTION', + constructor: strict.AssertionError, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // "strict.ok(\n typeof 123 === 'string'\n )\n" + } + ); + Error.stackTraceLimit = tmpLimit; + + // Test error diffs. + let message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '\n' + + ' [\n' + + ' [\n' + + ' [\n' + + ' 1,\n' + + ' 2,\n' + + '+ 3\n' + + "- '3'\n" + + ' ]\n' + + ' ],\n' + + ' 4,\n' + + ' 5\n' + + ' ]\n'; + strict.throws( + () => strict.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]), + {message}); + + message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '... Skipped lines\n' + + '\n' + + ' [\n' + + ' 1,\n' + + ' 1,\n' + + ' 1,\n' + + ' 0,\n' + + '...\n' + + ' 1,\n' + + '+ 1\n' + + ' ]\n'; + strict.throws( + () => strict.deepEqual( + [1, 1, 1, 0, 1, 1, 1, 1], + [1, 1, 1, 0, 1, 1, 1]), + {message}); + + message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '\n' + + ' [\n' + + ' 1,\n' + + ' 2,\n' + + ' 3,\n' + + ' 4,\n' + + ' 5,\n' + + '+ 6,\n' + + '- 9,\n' + + ' 7\n' + + ' ]\n'; + + assert.throws( + () => assert.deepStrictEqual([1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 9, 7]), + {message} + ); + + message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '\n' + + ' [\n' + + ' 1,\n' + + ' 2,\n' + + ' 3,\n' + + ' 4,\n' + + ' 5,\n' + + ' 6,\n' + + '+ 7,\n' + + '- 9,\n' + + ' 8\n' + + ' ]\n'; + + assert.throws( + () => assert.deepStrictEqual([1, 2, 3, 4, 5, 6, 7, 8], [1, 2, 3, 4, 5, 6, 9, 8]), + {message} + ); + + message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '... Skipped lines\n' + + '\n' + + ' [\n' + + ' 1,\n' + + ' 2,\n' + + ' 3,\n' + + ' 4,\n' + + '...\n' + + ' 7,\n' + + '+ 8,\n' + + '- 0,\n' + + ' 9\n' + + ' ]\n'; + + assert.throws( + () => assert.deepStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 2, 3, 4, 5, 6, 7, 0, 9]), + {message} + ); + + message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '\n' + + ' [\n' + + ' 1,\n' + + '+ 2,\n' + + ' 1,\n' + + ' 1,\n' + + '- 1,\n' + + ' 0,\n' + + ' 1,\n' + + '+ 1\n' + + ' ]\n'; + strict.throws( + () => strict.deepEqual( + [1, 2, 1, 1, 0, 1, 1], + [1, 1, 1, 1, 0, 1]), + {message}); + + message = [ + start, + actExp, + '', + '+ [', + '+ 1,', + '+ 2,', + '+ 1', + '+ ]', + '- undefined\n', + ].join('\n'); + strict.throws( + () => strict.deepEqual([1, 2, 1], undefined), + {message}); + + message = [ + start, + actExp, + '', + ' [', + '+ 1,', + ' 2,', + ' 1', + ' ]\n', + ].join('\n'); + strict.throws( + () => strict.deepEqual([1, 2, 1], [2, 1]), + {message}); + + message = 'Expected values to be strictly deep-equal:\n' + + '+ actual - expected\n' + + '\n' + + ' [\n' + + '+ 1,\n'.repeat(10) + + '+ 3\n' + + '- 2,\n'.repeat(11) + + '- 4,\n' + + '- 4,\n' + + '- 4\n' + + ' ]\n'; + strict.throws( + () => strict.deepEqual([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4]), + {message}); + + const obj1 = {}; + const obj2 = {loop: 'forever'}; + obj2[inspect.custom] = () => '{}'; + // No infinite loop and no custom inspect. + strict.throws(() => strict.deepEqual(obj1, obj2), { + message: `${start}\n` + + `${actExp}\n` + + '\n' + + '+ {}\n' + + '- {\n' + + '- [Symbol(nodejs.util.inspect.custom)]: [Function (anonymous)],\n' + + "- loop: 'forever'\n" + + '- }\n' + }); + + // notDeepEqual tests + strict.throws( + () => strict.notDeepEqual([1], [1]), + { + message: 'Expected "actual" not to be strictly deep-equal to:\n\n' + + '[\n 1\n]\n' + } + ); + + message = 'Expected "actual" not to be strictly deep-equal to:' + + `\n\n[${'\n 1,'.repeat(45)}\n...\n`; + const data = Array(51).fill(1); + strict.throws( + () => strict.notDeepEqual(data, data), + {message}); + +}); + +test('Additional asserts', () => { + assert.throws( + () => assert.ok(null), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + generatedMessage: true, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'assert.ok(null)\n' + } + ); + assert.throws( + () => { + // This test case checks if `try` left brace without a line break + // before the assertion causes any wrong assertion message. + // Therefore, don't reformat the following code. + // Refs: https://github.com/nodejs/node/issues/30872 + try { + assert.ok(0); // eslint-disable-line no-useless-catch, @stylistic/js/brace-style + } catch (err) { + throw err; + } + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + generatedMessage: true, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'assert.ok(0)\n' + } + ); + assert.throws( + () => { + try { + throw new Error(); + // This test case checks if `catch` left brace without a line break + // before the assertion causes any wrong assertion message. + // Therefore, don't reformat the following code. + // Refs: https://github.com/nodejs/node/issues/30872 + } catch (err) {assert.ok(0);} // eslint-disable-line no-unused-vars + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + generatedMessage: true, + message: '0 == true' + } + ); + assert.throws( + () => { + // This test case checks if `function` left brace without a line break + // before the assertion causes any wrong assertion message. + // Therefore, don't reformat the following code. + // Refs: https://github.com/nodejs/node/issues/30872 + function test() { + assert.ok(0); // eslint-disable-line @stylistic/js/brace-style + } + test(); + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + generatedMessage: true, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'assert.ok(0)\n' + } + ); + assert.throws( + () => assert(typeof 123n === 'string'), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + generatedMessage: true, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // "assert(typeof 123n === 'string')\n" + } + ); + + assert.throws( + () => assert(false, Symbol('foo')), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + generatedMessage: false, + message: 'Symbol(foo)' + } + ); + + assert.throws( + () => { + assert.strictEqual((() => 'string')(), 123 instanceof + Buffer); + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + message: 'Expected values to be strictly equal:\n\n\'string\' !== false\n' + } + ); + + assert.throws( + () => { + assert.strictEqual((() => 'string')(), 123 instanceof + Buffer); + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + message: 'Expected values to be strictly equal:\n\n\'string\' !== false\n' + } + ); + + /* eslint-disable @stylistic/js/indent */ + assert.throws(() => { + assert.strictEqual(( + () => 'string')(), 123 instanceof + Buffer); + }, { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + message: 'Expected values to be strictly equal:\n\n\'string\' !== false\n' + } + ); + /* eslint-enable @stylistic/js/indent */ + + assert.throws( + () => { + assert(true); assert(null, undefined); + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'assert(null, undefined)\n' + } + ); + + assert.throws( + () => { + assert + .ok(null, undefined); + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'ok(null, undefined)\n' + } + ); + + assert.throws( + // eslint-disable-next-line dot-notation, @stylistic/js/quotes + () => assert['ok']["apply"](null, [0]), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'assert[\'ok\']["apply"](null, [0])\n' + } + ); + + assert.throws( + () => { + const wrapper = (fn, value) => fn(value); + wrapper(assert, false); + }, + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + // message: 'The expression evaluated to a falsy value:\n\n fn(value)\n' + } + ); + + assert.throws( + () => assert.ok.call(null, 0), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + // message: 'The expression evaluated to a falsy value:\n\n ' + + // 'assert.ok.call(null, 0)\n', + generatedMessage: true + } + ); + + assert.throws( + () => assert.ok.call(null, 0, 'test'), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + message: 'test', + generatedMessage: false + } + ); + + // Works in eval. + assert.throws( + () => new Function('assert', 'assert(1 === 2);')(assert), + { + code: 'ERR_ASSERTION', + constructor: assert.AssertionError, + message: 'false == true' + } + ); + assert.throws( + () => eval('console.log("FOO");\nassert.ok(1 === 2);'), + { + code: 'ERR_ASSERTION', + message: 'false == true' + } + ); + + assert.throws( + () => assert.throws(() => {}, 'Error message', 'message'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + // message: 'The "error" argument must be of type Object, Error, Function or RegExp. Received: "Error message"', + message: 'The "error" argument must be of type Object, Error, Function or RegExp.' + invalidArgTypeHelper('Error message'), + } + ); + + const inputs = [1, false, Symbol()]; + for (const input of inputs) { + assert.throws( + () => assert.throws(() => {}, input), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "error" argument must be of type Object, Error, Function or RegExp.' + invalidArgTypeHelper(input) + + } + ); + } +}); + +test('Throws accepts objects', () => { + assert.throws(() => { + // eslint-disable-next-line no-constant-binary-expression + assert.ok((() => Boolean('' === false))()); + }, { + code: 'ERR_ASSERTION', + // message: 'The expression evaluated to a falsy value:\n\n' + + // " assert.ok((() => Boolean('\\u0001' === false))())\n" + }); + + const errFn = () => { + const err = new TypeError('Wrong value'); + err.code = 404; + throw err; + }; + const errObj = { + name: 'TypeError', + message: 'Wrong value' + }; + assert.throws(errFn, errObj); + + errObj.code = 404; + assert.throws(errFn, errObj); + + // Fail in case a expected property is undefined and not existent on the + // error. + errObj.foo = undefined; + assert.throws( + () => assert.throws(errFn, errObj), + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: `${start}\n${actExp}\n\n` + + ' Comparison {\n' + + ' code: 404,\n' + + '- foo: undefined,\n' + + " message: 'Wrong value',\n" + + " name: 'TypeError'\n" + + ' }\n' + } + ); + + // Show multiple wrong properties at the same time. + errObj.code = '404'; + assert.throws( + () => assert.throws(errFn, errObj), + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: `${start}\n${actExp}\n\n` + + ' Comparison {\n' + + '+ code: 404,\n' + + "- code: '404',\n" + + '- foo: undefined,\n' + + " message: 'Wrong value',\n" + + " name: 'TypeError'\n" + + ' }\n' + } + ); + + assert.throws( + () => assert.throws(() => {throw new Error();}, {foo: 'bar'}, 'foobar'), + { + constructor: assert.AssertionError, + code: 'ERR_ASSERTION', + message: 'foobar' + } + ); + + assert.throws( + () => assert.doesNotThrow(() => {throw new Error();}, {foo: 'bar'}), + { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "expected" argument must be of type Function or ' + + 'RegExp.' + invalidArgTypeHelper({foo: 'bar'}) + } + ); + + assert.throws(() => {throw new Error('e');}, new Error('e')); + assert.throws( + () => assert.throws(() => {throw new TypeError('e');}, new Error('e')), + { + name: 'AssertionError', + code: 'ERR_ASSERTION', + message: `${start}\n${actExp}\n\n` + + ' Comparison {\n' + + " message: 'e',\n" + + "+ name: 'TypeError'\n" + + "- name: 'Error'\n" + + ' }\n' + } + ); + assert.throws( + () => assert.throws(() => {throw new Error('foo');}, new Error('')), + { + name: 'AssertionError', + code: 'ERR_ASSERTION', + generatedMessage: true, + message: `${start}\n${actExp}\n\n` + + ' Comparison {\n' + + "+ message: 'foo',\n" + + "- message: '',\n" + + " name: 'Error'\n" + + ' }\n' + } + ); + + // eslint-disable-next-line no-throw-literal + assert.throws(() => {throw undefined;}, /undefined/); + assert.throws( + // eslint-disable-next-line no-throw-literal + () => assert.doesNotThrow(() => {throw undefined;}), + { + name: 'AssertionError', + code: 'ERR_ASSERTION', + message: 'Got unwanted exception.\nActual message: "undefined"' + } + ); +}); + +test('Additional assert', () => { + assert.throws( + () => assert.throws(() => {throw new Error();}, {}), + { + message: "The argument 'error' may not be an empty object. Received {}", + code: 'ERR_INVALID_ARG_VALUE' + } + ); + + assert.throws( + () => assert.throws( + // eslint-disable-next-line no-throw-literal + () => {throw 'foo';}, + 'foo' + ), + { + code: 'ERR_AMBIGUOUS_ARGUMENT', + message: 'The "error/message" argument is ambiguous. ' + + 'The error "foo" is identical to the message.' + } + ); + + assert.throws( + () => assert.throws( + () => {throw new TypeError('foo');}, + 'foo' + ), + { + code: 'ERR_AMBIGUOUS_ARGUMENT', + message: 'The "error/message" argument is ambiguous. ' + + 'The error message "foo" is identical to the message.' + } + ); + + // Should not throw. + assert.throws(() => {throw null;}, 'foo'); // eslint-disable-line no-throw-literal + + assert.throws( + () => assert.strictEqual([], []), + { + message: 'Values have same structure but are not reference-equal:\n\n[]\n' + } + ); + + { + const args = (function () {return arguments;})('a'); + assert.throws( + () => assert.strictEqual(args, {0: 'a'}), + { + message: 'Expected "actual" to be reference-equal to "expected":\n' + + '+ actual - expected\n\n' + + "+ [Arguments] {\n- {\n '0': 'a'\n }\n" + } + ); + } + + assert.throws( + () => {throw new TypeError('foobar');}, + { + message: /foo/, + name: /^TypeError$/ + } + ); + + assert.throws( + () => assert.throws( + () => {throw new TypeError('foobar');}, + { + message: /fooa/, + name: /^TypeError$/ + } + ), + { + message: `${start}\n${actExp}\n\n` + + ' Comparison {\n' + + "+ message: 'foobar',\n" + + '- message: /fooa/,\n' + + " name: 'TypeError'\n" + + ' }\n' + } + ); + + { + let actual = null; + const expected = {message: 'foo'}; + assert.throws( + () => assert.throws( + () => {throw actual;}, + expected + ), + { + operator: 'throws', + actual, + expected, + generatedMessage: true, + message: `${start}\n${actExp}\n\n` + + '+ null\n' + + '- {\n' + + "- message: 'foo'\n" + + '- }\n' + } + ); + + actual = 'foobar'; + const message = 'message'; + assert.throws( + () => assert.throws( + () => {throw actual;}, + {message: 'foobar'}, + message + ), + { + actual, + message: "message\n+ actual - expected\n\n+ 'foobar'\n- {\n- message: 'foobar'\n- }\n", + operator: 'throws', + generatedMessage: false + } + ); + } + + // Indicate where the strings diverge. + assert.throws( + () => assert.strictEqual('test test', 'test foobar'), + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'Expected values to be strictly equal:\n' + + '+ actual - expected\n' + + '\n' + + "+ 'test test'\n" + + "- 'test foobar'\n" + + ' ^\n', + } + ); + + // Check for reference-equal objects in `notStrictEqual()` + assert.throws( + () => { + const obj = {}; + assert.notStrictEqual(obj, obj); + }, + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'Expected "actual" not to be reference-equal to "expected": {}' + } + ); + + assert.throws( + () => { + const obj = {a: true}; + assert.notStrictEqual(obj, obj); + }, + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'Expected "actual" not to be reference-equal to "expected":\n\n' + + '{\n a: true\n}\n' + } + ); + + assert.throws( + () => { + assert.deepStrictEqual({a: true}, {a: false}, 'custom message'); + }, + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'custom message\n+ actual - expected\n\n {\n+ a: true\n- a: false\n }\n' + } + ); + + { + let threw = false; + try { + assert.deepStrictEqual(Array(100).fill(1), 'foobar'); + } catch (err) { + threw = true; + assert.match(inspect(err), /actual: \[Array],\n {2}expected: 'foobar',/); + } + assert(threw); + } + + assert.throws( + () => assert.equal(1), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.deepEqual(/a/), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.notEqual(null), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.notDeepEqual('test'), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.strictEqual({}), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.deepStrictEqual(Symbol()), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.notStrictEqual(5n), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.notDeepStrictEqual(undefined), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.strictEqual(), + {code: 'ERR_MISSING_ARGS'} + ); + + assert.throws( + () => assert.deepStrictEqual(), + {code: 'ERR_MISSING_ARGS'} + ); + + // Verify that `stackStartFunction` works as alternative to `stackStartFn`. + { + (function hidden() { + const err = new assert.AssertionError({ + actual: 'foo', + operator: 'strictEqual', + stackStartFunction: hidden + }); + const err2 = new assert.AssertionError({ + actual: 'foo', + operator: 'strictEqual', + stackStartFn: hidden + }); + assert(!err.stack.includes('hidden')); + assert(!err2.stack.includes('hidden')); + })(); + } + + assert.throws( + () => assert.throws(() => {throw Symbol('foo');}, RangeError), + { + message: 'The error is expected to be an instance of "RangeError". ' + + 'Received "Symbol(foo)"' + } + ); + + assert.throws( + // eslint-disable-next-line no-throw-literal + () => assert.throws(() => {throw [1, 2];}, RangeError), + { + message: 'The error is expected to be an instance of "RangeError". ' + + 'Received "[Array]"' + } + ); + + { + const err = new TypeError('foo'); + const validate = (() => () => ({a: true, b: [1, 2, 3]}))(); + assert.throws( + () => assert.throws(() => {throw err;}, validate), + { + message: 'The validation function is expected to ' + + `return "true". Received ${inspect(validate())}\n\nCaught ` + + `error:\n\n${err}`, + code: 'ERR_ASSERTION', + actual: err, + expected: validate, + name: 'AssertionError', + operator: 'throws', + } + ); + } + + assert.throws( + () => { + const script = new vm.Script('new RangeError("foobar");'); + const context = vm.createContext(); + const err = script.runInContext(context); + assert.throws(() => {throw err;}, RangeError); + }, + { + message: 'The error is expected to be an instance of "RangeError". ' + + 'Received an error with identical name but a different ' + + 'prototype.\n\nError message:\n\nfoobar' + } + ); + + // Multiple assert.match() tests. + { + assert.throws( + () => assert.match(/abc/, 'string'), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "regexp" argument must be of type RegExp.' + + invalidArgTypeHelper('string') + } + ); + assert.throws( + () => assert.match('string', /abc/), + { + actual: 'string', + expected: /abc/, + operator: 'match', + message: 'The input did not match the regular expression /abc/. ' + + "Input:\n\n'string'\n", + generatedMessage: true + } + ); + assert.throws( + () => assert.match('string', /abc/, 'foobar'), + { + actual: 'string', + expected: /abc/, + operator: 'match', + message: 'foobar', + generatedMessage: false + } + ); + const errorMessage = new RangeError('foobar'); + assert.throws( + () => assert.match('string', /abc/, errorMessage), + errorMessage + ); + assert.throws( + () => assert.match({abc: 123}, /abc/), + { + actual: {abc: 123}, + expected: /abc/, + operator: 'match', + message: 'The "string" argument must be of type string. ' + + // NOTE: invalidArgTypeHelper just says "received instanceof Object", + // as does message formatters in ErrorCode.cpp. we may want to change that in the future. + // invalidArgTypeHelper({abc: 123}), + 'Received type object ({ abc: 123 })', + generatedMessage: true + } + ); + assert.match('I will pass', /pass$/); + } + + // Multiple assert.doesNotMatch() tests. + { + assert.throws( + () => assert.doesNotMatch(/abc/, 'string'), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "regexp" argument must be of type RegExp.' + + invalidArgTypeHelper('string') + } + ); + assert.throws( + () => assert.doesNotMatch('string', /string/), + { + actual: 'string', + expected: /string/, + operator: 'doesNotMatch', + message: 'The input was expected to not match the regular expression ' + + "/string/. Input:\n\n'string'\n", + generatedMessage: true + } + ); + assert.throws( + () => assert.doesNotMatch('string', /string/, 'foobar'), + { + actual: 'string', + expected: /string/, + operator: 'doesNotMatch', + message: 'foobar', + generatedMessage: false + } + ); + const errorMessage = new RangeError('foobar'); + assert.throws( + () => assert.doesNotMatch('string', /string/, errorMessage), + errorMessage + ); + assert.throws( + () => assert.doesNotMatch({abc: 123}, /abc/), + { + actual: {abc: 123}, + expected: /abc/, + operator: 'doesNotMatch', + message: 'The "string" argument must be of type string. ' + + 'Received type object ({ abc: 123 })', + generatedMessage: true + } + ); + assert.doesNotMatch('I will pass', /different$/); + } +}); + +test('assert/strict exists', () => { + assert.strictEqual(require('assert/strict'), assert.strict); +}); + +/* eslint-enable no-restricted-syntax */ +/* eslint-enable no-restricted-properties */ + + diff --git a/test/js/node/test/parallel/test-child-process-exec-timeout-not-expired.js b/test/js/node/test/parallel/test-child-process-exec-timeout-not-expired.js index fb0af5fa8f59d5..7c8dd3661a5897 100644 --- a/test/js/node/test/parallel/test-child-process-exec-timeout-not-expired.js +++ b/test/js/node/test/parallel/test-child-process-exec-timeout-not-expired.js @@ -27,8 +27,8 @@ const cmd = `"${process.execPath}" "${__filename}" child`; cp.exec(cmd, { timeout: kTimeoutNotSupposedToExpire }, common.mustSucceed((stdout, stderr) => { - assert.strictEqual(stdout.trim(), 'child stdout'); - assert.strictEqual(stderr.trim(), 'child stderr'); + assert.strict(stdout.trim().includes('child stdout')); + assert.strict(stderr.trim().includes('child stderr')); })); cleanupStaleProcess(__filename); diff --git a/test/js/node/test/parallel/test-child-process-spawnsync-input.js b/test/js/node/test/parallel/test-child-process-spawnsync-input.js index 62ae476ae17caa..4b4549ff55dae7 100644 --- a/test/js/node/test/parallel/test-child-process-spawnsync-input.js +++ b/test/js/node/test/parallel/test-child-process-spawnsync-input.js @@ -48,7 +48,9 @@ function checkSpawnSyncRet(ret) { function verifyBufOutput(ret) { checkSpawnSyncRet(ret); + assert.deepStrictEqual(ret.stdout.toString('utf8'), msgOutBuf.toString('utf8')); assert.deepStrictEqual(ret.stdout, msgOutBuf); + assert.deepStrictEqual(ret.stderr.toString('utf8'), msgErrBuf.toString('utf8')); assert.deepStrictEqual(ret.stderr, msgErrBuf); } diff --git a/test/js/node/test/parallel/test-child-process-stdio.js b/test/js/node/test/parallel/test-child-process-stdio.js new file mode 100644 index 00000000000000..15c2770aa29d1a --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdio.js @@ -0,0 +1,77 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { spawn } = require('child_process'); + +// Test stdio piping. +{ + const child = spawn(...common.pwdCommand, { stdio: ['pipe'] }); + assert.notStrictEqual(child.stdout, null); + assert.notStrictEqual(child.stderr, null); +} + +// Test stdio ignoring. +{ + const child = spawn(...common.pwdCommand, { stdio: 'ignore' }); + assert.strictEqual(child.stdout, null); + assert.strictEqual(child.stderr, null); +} + +// Asset options invariance. +{ + const options = { stdio: 'ignore' }; + spawn(...common.pwdCommand, options); + assert.deepStrictEqual(options, { stdio: 'ignore' }); +} + +// Test stdout buffering. +{ + let output = ''; + const child = spawn(...common.pwdCommand); + + child.stdout.setEncoding('utf8'); + child.stdout.on('data', function(s) { + output += s; + }); + + child.on('exit', common.mustCall(function(code) { + assert.strictEqual(code, 0); + })); + + child.on('close', common.mustCall(function() { + assert.strictEqual(output.length > 1, true); + assert.strictEqual(output[output.length - 1], '\n'); + })); +} + +// Assert only one IPC pipe allowed. +assert.throws( + () => { + spawn( + ...common.pwdCommand, + { stdio: ['pipe', 'pipe', 'pipe', 'ipc', 'ipc'] } + ); + }, + { code: 'ERR_IPC_ONE_PIPE', name: 'Error' } +); diff --git a/test/js/node/test/parallel/test-console-tty-colors.js b/test/js/node/test/parallel/test-console-tty-colors.js index 969fb53a239883..63ff42935bd4dc 100644 --- a/test/js/node/test/parallel/test-console-tty-colors.js +++ b/test/js/node/test/parallel/test-console-tty-colors.js @@ -60,7 +60,21 @@ check(false, false, false); write: common.mustNotCall() }); - [0, 'true', null, {}, [], () => {}].forEach((colorMode) => { + assert.throws( + () => { + new Console({ + stdout: stream, + ignoreErrors: false, + colorMode: 'true' + }); + }, + { + message: `The argument 'colorMode' must be one of: 'auto', true, false. Received "true"`, + code: 'ERR_INVALID_ARG_VALUE' + } + ); + + [0, null, {}, [], () => {}].forEach((colorMode) => { const received = util.inspect(colorMode); assert.throws( () => { diff --git a/test/js/node/test/parallel/test-dns-cancel-reverse-lookup.js b/test/js/node/test/parallel/test-dns-cancel-reverse-lookup.js new file mode 100644 index 00000000000000..e0cb4d1854b4ab --- /dev/null +++ b/test/js/node/test/parallel/test-dns-cancel-reverse-lookup.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const { Resolver } = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); + +const server = dgram.createSocket('udp4'); +const resolver = new Resolver(); + +server.bind(0, common.mustCall(() => { + resolver.setServers([`127.0.0.1:${server.address().port}`]); + resolver.reverse('123.45.67.89', common.mustCall((err, res) => { + assert.strictEqual(err.code, 'ECANCELLED'); + assert.strictEqual(err.syscall, 'getHostByAddr'); + assert.strictEqual(err.hostname, '123.45.67.89'); + server.close(); + })); +})); + +server.on('message', common.mustCall((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, '89.67.45.123.in-addr.arpa'); + + // Do not send a reply. + resolver.cancel(); +})); diff --git a/test/js/node/test/parallel/test-dns-channel-cancel-promise.js b/test/js/node/test/parallel/test-dns-channel-cancel-promise.js new file mode 100644 index 00000000000000..6dee3e6a778687 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-channel-cancel-promise.js @@ -0,0 +1,59 @@ +'use strict'; +const common = require('../common'); +const { promises: dnsPromises } = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); + +const server = dgram.createSocket('udp4'); +const resolver = new dnsPromises.Resolver(); + +server.bind(0, common.mustCall(async () => { + resolver.setServers([`127.0.0.1:${server.address().port}`]); + + // Single promise + { + server.once('message', () => { + resolver.cancel(); + }); + + const hostname = 'example0.org'; + + await assert.rejects( + resolver.resolve4(hostname), + { + code: 'ECANCELLED', + syscall: 'queryA', + hostname + } + ); + } + + // Multiple promises + { + server.once('message', () => { + resolver.cancel(); + }); + + const assertions = []; + const assertionCount = 10; + + for (let i = 1; i <= assertionCount; i++) { + const hostname = `example${i}.org`; + + assertions.push( + assert.rejects( + resolver.resolve4(hostname), + { + code: 'ECANCELLED', + syscall: 'queryA', + hostname: hostname + } + ) + ); + } + + await Promise.all(assertions); + } + + server.close(); +})); diff --git a/test/js/node/test/parallel/test-dns-channel-cancel.js b/test/js/node/test/parallel/test-dns-channel-cancel.js new file mode 100644 index 00000000000000..405b31e4cc1913 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-channel-cancel.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../common'); +const { Resolver } = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); + +const server = dgram.createSocket('udp4'); +const resolver = new Resolver(); + +const desiredQueries = 11; +let finishedQueries = 0; + +server.bind(0, common.mustCall(async () => { + resolver.setServers([`127.0.0.1:${server.address().port}`]); + + const callback = common.mustCall((err, res) => { + assert.strictEqual(err.code, 'ECANCELLED'); + assert.strictEqual(err.syscall, 'queryA'); + assert.strictEqual(err.hostname, `example${finishedQueries}.org`); + + finishedQueries++; + if (finishedQueries === desiredQueries) { + server.close(); + } + }, desiredQueries); + + const next = (...args) => { + callback(...args); + + server.once('message', () => { + resolver.cancel(); + }); + + // Multiple queries + for (let i = 1; i < desiredQueries; i++) { + resolver.resolve4(`example${i}.org`, callback); + } + }; + + server.once('message', () => { + resolver.cancel(); + }); + + // Single query + resolver.resolve4('example0.org', next); +})); diff --git a/test/js/node/test/parallel/test-dns-channel-timeout.js b/test/js/node/test/parallel/test-dns-channel-timeout.js new file mode 100644 index 00000000000000..153d7ad907b8d5 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-channel-timeout.js @@ -0,0 +1,61 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const dns = require('dns'); + +if (typeof Bun !== 'undefined') { + if (process.platform === 'win32' && require('harness').isCI) { + // TODO(@heimskr): This test mysteriously takes forever in Windows in CI + // possibly due to UDP keeping the event loop alive longer than it should. + process.exit(0); + } +} + +for (const ctor of [dns.Resolver, dns.promises.Resolver]) { + for (const timeout of [null, true, false, '', '2']) { + assert.throws(() => new ctor({ timeout }), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + } + + for (const timeout of [-2, 4.2, 2 ** 31]) { + assert.throws(() => new ctor({ timeout }), { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + }); + } + + for (const timeout of [-1, 0, 1]) new ctor({ timeout }); // OK +} + +for (const timeout of [0, 1, 2]) { + const server = dgram.createSocket('udp4'); + server.bind(0, '127.0.0.1', common.mustCall(() => { + const resolver = new dns.Resolver({ timeout }); + resolver.setServers([`127.0.0.1:${server.address().port}`]); + resolver.resolve4('nodejs.org', common.mustCall((err) => { + assert.throws(() => { throw err; }, { + code: 'ETIMEOUT', + name: /^(DNSException|Error)$/, + }); + server.close(); + })); + })); +} + +for (const timeout of [0, 1, 2]) { + const server = dgram.createSocket('udp4'); + server.bind(0, '127.0.0.1', common.mustCall(() => { + const resolver = new dns.promises.Resolver({ timeout }); + resolver.setServers([`127.0.0.1:${server.address().port}`]); + resolver.resolve4('nodejs.org').catch(common.mustCall((err) => { + assert.throws(() => { throw err; }, { + code: 'ETIMEOUT', + name: /^(DNSException|Error)$/, + }); + server.close(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-dns-default-order-ipv4.js b/test/js/node/test/parallel/test-dns-default-order-ipv4.js new file mode 100644 index 00000000000000..ea3deec4f7e875 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-default-order-ipv4.js @@ -0,0 +1,51 @@ +// Flags: --dns-result-order=ipv4first +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); + +// Test that --dns-result-order=ipv4first works as expected. + +if (!process.execArgv.includes("--dns-result-order=ipv4first")) { + process.exit(0); +} + +const originalLookup = Bun.dns.lookup; +const calls = []; +Bun.dns.lookup = common.mustCallAtLeast((...args) => { + calls.push(args); + return originalLookup(...args); +}, 1); + +const dns = require('dns'); +const dnsPromises = dns.promises; + +// We want to test the parameter of ipv4first only so that we +// ignore possible errors here. +function allowFailed(fn) { + return fn.catch((_err) => { + // + }); +} + +(async () => { + let callsLength = 0; + const checkParameter = (expected) => { + assert.strictEqual(calls.length, callsLength + 1); + const { order } = calls[callsLength][1]; + assert.strictEqual(order, expected); + callsLength += 1; + }; + + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv4first'); + + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv4first'); + + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv4first'); + + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv4first'); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-default-order-ipv6.js b/test/js/node/test/parallel/test-dns-default-order-ipv6.js new file mode 100644 index 00000000000000..aeb2dc2b2ac1f4 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-default-order-ipv6.js @@ -0,0 +1,51 @@ +// Flags: --dns-result-order=ipv6first +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); + +// Test that --dns-result-order=ipv6first works as expected. + +if (!process.execArgv.includes("--dns-result-order=ipv6first")) { + process.exit(0); +} + +const originalLookup = Bun.dns.lookup; +const calls = []; +Bun.dns.lookup = common.mustCallAtLeast((...args) => { + calls.push(args); + return originalLookup(...args); +}, 1); + +const dns = require('dns'); +const dnsPromises = dns.promises; + +// We want to test the parameter of ipv6first only so that we +// ignore possible errors here. +function allowFailed(fn) { + return fn.catch((_err) => { + // + }); +} + +(async () => { + let callsLength = 0; + const checkParameter = (expected) => { + assert.strictEqual(calls.length, callsLength + 1); + const { order } = calls[callsLength][1]; + assert.strictEqual(order, expected); + callsLength += 1; + }; + + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv6first'); + + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv6first'); + + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv6first'); + + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv6first'); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-default-order-verbatim.js b/test/js/node/test/parallel/test-dns-default-order-verbatim.js new file mode 100644 index 00000000000000..562250ca4842f5 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-default-order-verbatim.js @@ -0,0 +1,55 @@ +// Flags: --dns-result-order=verbatim +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); + +const originalLookup = Bun.dns.lookup; +const calls = []; +Bun.dns.lookup = common.mustCallAtLeast((...args) => { + calls.push(args); + return originalLookup(...args); +}, 1); + +const dns = require('dns'); +const dnsPromises = dns.promises; + +// We want to test the parameter of verbatim only so that we +// ignore possible errors here. +function allowFailed(fn) { + return fn.catch((_err) => { + // + }); +} + +(async () => { + let callsLength = 0; + const checkParameter = (expected) => { + assert.strictEqual(calls.length, callsLength + 1); + const { order } = calls[callsLength][1]; + assert.strictEqual(order, expected); + callsLength += 1; + }; + + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter("verbatim"); + + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter("verbatim"); + + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter("verbatim"); + + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter("verbatim"); + + await allowFailed( + promisify(dns.lookup)('example.org', { order: 'ipv4first' }) + ); + checkParameter("ipv4first"); + + await allowFailed( + promisify(dns.lookup)('example.org', { order: 'ipv6first' }) + ); + checkParameter("ipv6first"); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-get-server.js b/test/js/node/test/parallel/test-dns-get-server.js new file mode 100644 index 00000000000000..3ce6a45ac7d897 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-get-server.js @@ -0,0 +1,11 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { Resolver } = require('dns'); + +const resolver = new Resolver(); +assert(resolver.getServers().length > 0); + +resolver._handle.getServers = common.mustCall(); +assert.strictEqual(resolver.getServers().length, 0); diff --git a/test/js/node/test/parallel/test-dns-lookup-promises-options-deprecated.js b/test/js/node/test/parallel/test-dns-lookup-promises-options-deprecated.js new file mode 100644 index 00000000000000..934796198bbdcf --- /dev/null +++ b/test/js/node/test/parallel/test-dns-lookup-promises-options-deprecated.js @@ -0,0 +1,44 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +Bun.dns.lookup = hostname => { + throw Object.assign(new Error('Out of memory'), { + name: 'DNSException', + code: 'ENOMEM', + syscall: 'getaddrinfo', + hostname, + }); +}; + +// This test ensures that dns.lookup issues a DeprecationWarning +// when invalid options type is given + +const dnsPromises = require('dns/promises'); + +common.expectWarning({ + // 'internal/test/binding': [ + // 'These APIs are for internal testing only. Do not use them.', + // ], +}); + +assert.throws(() => { + dnsPromises.lookup('127.0.0.1', { hints: '-1' }); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { hints: -1 }), + { code: 'ERR_INVALID_ARG_VALUE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { family: '6' }), + { code: 'ERR_INVALID_ARG_VALUE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { all: 'true' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { verbatim: 'true' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { order: 'true' }), + { code: 'ERR_INVALID_ARG_VALUE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', '6'), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => dnsPromises.lookup('localhost'), + { code: 'ENOMEM' }); diff --git a/test/js/node/test/parallel/test-dns-lookup.js b/test/js/node/test/parallel/test-dns-lookup.js new file mode 100644 index 00000000000000..bef563df608762 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-lookup.js @@ -0,0 +1,223 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// Stub `getaddrinfo` to *always* error. This has to be done before we load the +// `dns` module to guarantee that the `dns` module uses the stub. +if (typeof Bun === "undefined") { + const { internalBinding } = require('internal/test/binding'); + const cares = internalBinding('cares_wrap'); + cares.getaddrinfo = () => internalBinding('uv').UV_ENOMEM; +} else { + Bun.dns.lookup = (hostname) => Promise.reject(Object.assign(new Error('Out of memory'), { code: 'ENOMEM', hostname })); +} + +const dns = require('dns'); +const dnsPromises = dns.promises; + +{ + const err = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "hostname" argument must be of type string\. Received( type number|: "number")/ + }; + + assert.throws(() => dns.lookup(1, {}), err); + assert.throws(() => dnsPromises.lookup(1, {}), err); +} + +// This also verifies different expectWarning notations. +common.expectWarning({ + // For 'internal/test/binding' module. + ...(typeof Bun === "undefined"? { + 'internal/test/binding': [ + 'These APIs are for internal testing only. Do not use them.', + ] + } : {}), + // For calling `dns.lookup` with falsy `hostname`. + 'DeprecationWarning': { + DEP0118: 'The provided hostname "false" is not a valid ' + + 'hostname, and is supported in the dns module solely for compatibility.' + } +}); + +assert.throws(() => { + dns.lookup(false, 'cb'); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +assert.throws(() => { + dns.lookup(false, 'options', 'cb'); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +{ + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /The argument 'hints' is invalid\. Received:? 100/ + }; + const options = { + hints: 100, + family: 0, + all: false + }; + + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +} + +{ + const family = 20; + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /^The (property 'options.family' must be one of: 0, 4, 6|argument 'family' must be one of 0, 4 or 6)\. Received:? 20$/ + }; + const options = { + hints: 0, + family, + all: false + }; + + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +} + +[1, 0n, 1n, '', '0', Symbol(), true, false, {}, [], () => {}] + .forEach((family) => { + const err = { code: 'ERR_INVALID_ARG_VALUE' }; + const options = { family }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); + }); +[0n, 1n, '', '0', Symbol(), true, false].forEach((family) => { + const err = { code: 'ERR_INVALID_ARG_TYPE' }; + assert.throws(() => { dnsPromises.lookup(false, family); }, err); + assert.throws(() => { + dns.lookup(false, family, common.mustNotCall()); + }, err); +}); +assert.throws(() => dnsPromises.lookup(false, () => {}), + { code: 'ERR_INVALID_ARG_TYPE' }); + +[0n, 1n, '', '0', Symbol(), true, false, {}, [], () => {}].forEach((hints) => { + const err = { code: 'ERR_INVALID_ARG_TYPE' }; + const options = { hints }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +}); + +[0, 1, 0n, 1n, '', '0', Symbol(), {}, [], () => {}].forEach((all) => { + const err = { code: 'ERR_INVALID_ARG_TYPE' }; + const options = { all }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +}); + +[0, 1, 0n, 1n, '', '0', Symbol(), {}, [], () => {}].forEach((verbatim) => { + const err = { code: 'ERR_INVALID_ARG_TYPE' }; + const options = { verbatim }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +}); + +[0, 1, 0n, 1n, '', '0', Symbol(), {}, [], () => {}].forEach((order) => { + const err = { code: 'ERR_INVALID_ARG_VALUE' }; + const options = { order }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +}); + +(async function() { + let res; + + res = await dnsPromises.lookup(false, { + hints: 0, + family: 0, + all: true + }); + assert.deepStrictEqual(res, []); + + res = await dnsPromises.lookup('127.0.0.1', { + hints: 0, + family: 4, + all: true + }); + assert.deepStrictEqual(res, [{ address: '127.0.0.1', family: 4 }]); + + res = await dnsPromises.lookup('127.0.0.1', { + hints: 0, + family: 4, + all: false + }); + assert.deepStrictEqual(res, { address: '127.0.0.1', family: 4 }); +})().then(common.mustCall()); + +dns.lookup(false, { + hints: 0, + family: 0, + all: true +}, common.mustSucceed((result, addressType) => { + assert.deepStrictEqual(result, []); + assert.strictEqual(addressType, undefined); +})); + +dns.lookup('127.0.0.1', { + hints: 0, + family: 4, + all: true +}, common.mustSucceed((result, addressType) => { + assert.deepStrictEqual(result, [{ + address: '127.0.0.1', + family: 4 + }]); + assert.strictEqual(addressType, undefined); +})); + +dns.lookup('127.0.0.1', { + hints: 0, + family: 4, + all: false +}, common.mustSucceed((result, addressType) => { + assert.strictEqual(result, '127.0.0.1'); + assert.strictEqual(addressType, 4); +})); + +let tickValue = 0; + +// Should fail due to stub. +dns.lookup('example.com', common.mustCall((error, result, addressType) => { + assert(error); + assert.strictEqual(tickValue, 1); + assert.strictEqual(error.code, 'ENOMEM'); + const descriptor = Object.getOwnPropertyDescriptor(error, 'message'); + // The error message should be non-enumerable. + assert.strictEqual(descriptor.enumerable, false); +})); + +// Make sure that the error callback is called on next tick. +tickValue = 1; + +// Should fail due to stub. +assert.rejects(dnsPromises.lookup('example.com'), + { code: 'ENOMEM', hostname: 'example.com' }).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-lookupService-promises.js b/test/js/node/test/parallel/test-dns-lookupService-promises.js new file mode 100644 index 00000000000000..f4053d484da8a4 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-lookupService-promises.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN + +const assert = require('assert'); +const dnsPromises = require('dns').promises; + +dnsPromises.lookupService('127.0.0.1', 22).then(common.mustCall((result) => { + assert(['ssh', '22'].includes(result.service)); + assert.strictEqual(typeof result.hostname, 'string'); + assert.notStrictEqual(result.hostname.length, 0); +})); + +// Use an IP from the RFC 5737 test range to cause an error. +// Refs: https://tools.ietf.org/html/rfc5737 +assert.rejects( + () => dnsPromises.lookupService('192.0.2.1', 22), + { code: /^(?:ENOTFOUND|EAI_AGAIN)$/ } +).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-lookupService.js b/test/js/node/test/parallel/test-dns-lookupService.js new file mode 100644 index 00000000000000..e4e48de8bbd31e --- /dev/null +++ b/test/js/node/test/parallel/test-dns-lookupService.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// Stub `getnameinfo` to *always* error. +Bun.dns.lookupService = (addr, port) => { + throw Object.assign(new Error(`getnameinfo ENOENT ${addr}`), {code: 'ENOENT', syscall: 'getnameinfo'}); +}; + +const dns = require('dns'); + +assert.throws( + () => dns.lookupService('127.0.0.1', 80, common.mustNotCall()), + { + code: 'ENOENT', + message: 'getnameinfo ENOENT 127.0.0.1', + syscall: 'getnameinfo' + } +); + +assert.rejects( + dns.promises.lookupService('127.0.0.1', 80), + { + code: 'ENOENT', + message: 'getnameinfo ENOENT 127.0.0.1', + syscall: 'getnameinfo' + } +).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-multi-channel.js b/test/js/node/test/parallel/test-dns-multi-channel.js new file mode 100644 index 00000000000000..026ef44e339e85 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-multi-channel.js @@ -0,0 +1,52 @@ +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const { Resolver } = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); + +const servers = [ + { + socket: dgram.createSocket('udp4'), + reply: { type: 'A', address: '1.2.3.4', ttl: 123, domain: 'example.org' } + }, + { + socket: dgram.createSocket('udp4'), + reply: { type: 'A', address: '5.6.7.8', ttl: 123, domain: 'example.org' } + }, +]; + +let waiting = servers.length; +for (const { socket, reply } of servers) { + socket.on('message', common.mustCall((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, 'example.org'); + + socket.send(dnstools.writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: [reply], + }), port, address); + })); + + socket.bind(0, common.mustCall(() => { + if (--waiting === 0) ready(); + })); +} + + +function ready() { + const resolvers = servers.map((server) => ({ + server, + resolver: new Resolver() + })); + + for (const { server: { socket, reply }, resolver } of resolvers) { + resolver.setServers([`127.0.0.1:${socket.address().port}`]); + resolver.resolve4('example.org', common.mustSucceed((res) => { + assert.deepStrictEqual(res, [reply.address]); + socket.close(); + })); + } +} diff --git a/test/js/node/test/parallel/test-dns-promises-exists.js b/test/js/node/test/parallel/test-dns-promises-exists.js new file mode 100644 index 00000000000000..d88ecefaa985ca --- /dev/null +++ b/test/js/node/test/parallel/test-dns-promises-exists.js @@ -0,0 +1,33 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const dnsPromises = require('dns/promises'); +const dns = require('dns'); + +assert.strictEqual(dnsPromises, dns.promises); + +assert.strictEqual(dnsPromises.NODATA, dns.NODATA); +assert.strictEqual(dnsPromises.FORMERR, dns.FORMERR); +assert.strictEqual(dnsPromises.SERVFAIL, dns.SERVFAIL); +assert.strictEqual(dnsPromises.NOTFOUND, dns.NOTFOUND); +assert.strictEqual(dnsPromises.NOTIMP, dns.NOTIMP); +assert.strictEqual(dnsPromises.REFUSED, dns.REFUSED); +assert.strictEqual(dnsPromises.BADQUERY, dns.BADQUERY); +assert.strictEqual(dnsPromises.BADNAME, dns.BADNAME); +assert.strictEqual(dnsPromises.BADFAMILY, dns.BADFAMILY); +assert.strictEqual(dnsPromises.BADRESP, dns.BADRESP); +assert.strictEqual(dnsPromises.CONNREFUSED, dns.CONNREFUSED); +assert.strictEqual(dnsPromises.TIMEOUT, dns.TIMEOUT); +assert.strictEqual(dnsPromises.EOF, dns.EOF); +assert.strictEqual(dnsPromises.FILE, dns.FILE); +assert.strictEqual(dnsPromises.NOMEM, dns.NOMEM); +assert.strictEqual(dnsPromises.DESTRUCTION, dns.DESTRUCTION); +assert.strictEqual(dnsPromises.BADSTR, dns.BADSTR); +assert.strictEqual(dnsPromises.BADFLAGS, dns.BADFLAGS); +assert.strictEqual(dnsPromises.NONAME, dns.NONAME); +assert.strictEqual(dnsPromises.BADHINTS, dns.BADHINTS); +assert.strictEqual(dnsPromises.NOTINITIALIZED, dns.NOTINITIALIZED); +assert.strictEqual(dnsPromises.LOADIPHLPAPI, dns.LOADIPHLPAPI); +assert.strictEqual(dnsPromises.ADDRGETNETWORKPARAMS, dns.ADDRGETNETWORKPARAMS); +assert.strictEqual(dnsPromises.CANCELLED, dns.CANCELLED); diff --git a/test/js/node/test/parallel/test-dns-resolve-promises.js b/test/js/node/test/parallel/test-dns-resolve-promises.js new file mode 100644 index 00000000000000..b9965614acfb03 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-resolve-promises.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dnsPromises = require('dns').promises; + +Bun.dns.resolve = (hostname, rrtype) => Promise.reject({code: 'EPERM', syscall: 'query' + rrtype[0].toUpperCase() + rrtype.substr(1), hostname}); + +assert.rejects( + dnsPromises.resolve('example.org'), + { + code: 'EPERM', + syscall: 'queryA', + hostname: 'example.org' + } +).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-resolveany-bad-ancount.js b/test/js/node/test/parallel/test-dns-resolveany-bad-ancount.js new file mode 100644 index 00000000000000..88369a87f8e077 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-resolveany-bad-ancount.js @@ -0,0 +1,55 @@ +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const dns = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); +const dnsPromises = dns.promises; + +const server = dgram.createSocket('udp4'); +const resolver = new dns.Resolver({ timeout: 100, tries: 1 }); +const resolverPromises = new dnsPromises.Resolver({ timeout: 100, tries: 1 }); + +server.on('message', common.mustCallAtLeast((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + + assert.strictEqual(domain, 'example.org'); + + const buf = dnstools.writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: { type: 'A', address: '1.2.3.4', ttl: 123, domain }, + }); + // Overwrite the # of answers with 2, which is incorrect. The response is + // discarded in c-ares >= 1.21.0. This is the reason why a small timeout is + // used in the `Resolver` constructor. See + // https://github.com/nodejs/node/pull/50743#issue-1994909204 + buf.writeUInt16LE(2, 6); + server.send(buf, port, address); +}, 2)); + +server.bind(0, common.mustCall(async () => { + const address = server.address(); + resolver.setServers([`127.0.0.1:${address.port}`]); + resolverPromises.setServers([`127.0.0.1:${address.port}`]); + + resolverPromises.resolveAny('example.org') + .then(common.mustNotCall()) + .catch(common.expectsError({ + // May return EBADRESP or ETIMEOUT + code: /^(?:EBADRESP|ETIMEOUT)$/, + syscall: 'queryAny', + hostname: 'example.org' + })); + + resolver.resolveAny('example.org', common.mustCall((err) => { + assert.notStrictEqual(err.code, 'SUCCESS'); + assert.strictEqual(err.syscall, 'queryAny'); + assert.strictEqual(err.hostname, 'example.org'); + const descriptor = Object.getOwnPropertyDescriptor(err, 'message'); + // The error message should be non-enumerable. + assert.strictEqual(descriptor.enumerable, false); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-dns-resolveany.js b/test/js/node/test/parallel/test-dns-resolveany.js new file mode 100644 index 00000000000000..f64dbfc93e2da8 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-resolveany.js @@ -0,0 +1,69 @@ +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const dns = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); +const dnsPromises = dns.promises; + +const answers = [ + { type: 'A', address: '1.2.3.4', ttl: 123 }, + { type: 'AAAA', address: '::42', ttl: 123 }, + { type: 'MX', priority: 42, exchange: 'foobar.com', ttl: 124 }, + { type: 'NS', value: 'foobar.org', ttl: 457 }, + { type: 'TXT', entries: [ 'v=spf1 ~all xyz\0foo' ] }, + { type: 'PTR', value: 'baz.org', ttl: 987 }, + { + type: 'SOA', + nsname: 'ns1.example.com', + hostmaster: 'admin.example.com', + serial: 156696742, + refresh: 900, + retry: 900, + expire: 1800, + minttl: 60 + }, + { + type: 'CAA', + critical: 128, + issue: 'platynum.ch' + }, +]; + +const server = dgram.createSocket('udp4'); + +server.on('message', common.mustCall((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, 'example.org'); + + server.send(dnstools.writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: answers.map((answer) => Object.assign({ domain }, answer)), + }), port, address); +}, 2)); + +server.bind(0, common.mustCall(async () => { + const address = server.address(); + dns.setServers([`127.0.0.1:${address.port}`]); + + validateResults(await dnsPromises.resolveAny('example.org')); + + dns.resolveAny('example.org', common.mustSucceed((res) => { + validateResults(res); + server.close(); + })); +})); + +function validateResults(res) { + // TTL values are only provided for A and AAAA entries. + assert.deepStrictEqual(res.map(maybeRedactTTL), answers.map(maybeRedactTTL)); +} + +function maybeRedactTTL(r) { + const ret = { ...r }; + if (!['A', 'AAAA'].includes(r.type)) + delete ret.ttl; + return ret; +} diff --git a/test/js/node/test/parallel/test-dns-resolvens-typeerror.js b/test/js/node/test/parallel/test-dns-resolvens-typeerror.js new file mode 100644 index 00000000000000..c1eea2ddeb64f9 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-resolvens-typeerror.js @@ -0,0 +1,55 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); + +// This test ensures `dns.resolveNs()` does not raise a C++-land assertion error +// and throw a JavaScript TypeError instead. +// Issue https://github.com/nodejs/node-v0.x-archive/issues/7070 + +const assert = require('assert'); +const dns = require('dns'); +const dnsPromises = dns.promises; + +assert.throws( + () => dnsPromises.resolveNs([]), // bad name + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^(The "(host)?name" argument must be of type string|Expected hostname to be a string)/ + } +); +assert.throws( + () => dns.resolveNs([]), // bad name + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^(The "(host)?name" argument must be of type string|Expected hostname to be a string)/ + } +); +assert.throws( + () => dns.resolveNs(''), // bad callback + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } +); diff --git a/test/js/node/test/parallel/test-dns-set-default-order.js b/test/js/node/test/parallel/test-dns-set-default-order.js new file mode 100644 index 00000000000000..47bc08e6b17dfb --- /dev/null +++ b/test/js/node/test/parallel/test-dns-set-default-order.js @@ -0,0 +1,108 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); + +// Test that `dns.setDefaultResultOrder()` and +// `dns.promises.setDefaultResultOrder()` work as expected. + +const originalLookup = Bun.dns.lookup; +const calls = []; +Bun.dns.lookup = common.mustCallAtLeast((...args) => { + calls.push(args); + return originalLookup(...args); +}, 1); + +const dns = require('dns'); +const dnsPromises = dns.promises; + +// We want to test the parameter of order only so that we +// ignore possible errors here. +function allowFailed(fn) { + return fn.catch((_err) => { + // + }); +} + +assert.throws(() => dns.setDefaultResultOrder('my_order'), { + code: 'ERR_INVALID_ARG_VALUE', +}); +assert.throws(() => dns.promises.setDefaultResultOrder('my_order'), { + code: 'ERR_INVALID_ARG_VALUE', +}); +assert.throws(() => dns.setDefaultResultOrder(4), { + code: 'ERR_INVALID_ARG_VALUE', +}); +assert.throws(() => dns.promises.setDefaultResultOrder(4), { + code: 'ERR_INVALID_ARG_VALUE', +}); + +(async () => { + let callsLength = 0; + const checkParameter = (expected) => { + assert.strictEqual(calls.length, callsLength + 1); + const { order } = calls[callsLength][1]; + assert.strictEqual(order, expected); + callsLength += 1; + }; + + dns.setDefaultResultOrder('verbatim'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('verbatim'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('verbatim'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('verbatim'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('verbatim'); + + dns.setDefaultResultOrder('ipv4first'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv4first'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv4first'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv4first'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv4first'); + + dns.setDefaultResultOrder('ipv6first'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv6first'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv6first'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv6first'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv6first'); + + dns.promises.setDefaultResultOrder('verbatim'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('verbatim'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('verbatim'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('verbatim'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('verbatim'); + + dns.promises.setDefaultResultOrder('ipv4first'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv4first'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv4first'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv4first'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv4first'); + + dns.promises.setDefaultResultOrder('ipv6first'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv6first'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv6first'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv6first'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv6first'); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-setlocaladdress.js b/test/js/node/test/parallel/test-dns-setlocaladdress.js new file mode 100644 index 00000000000000..25bece328f4a74 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-setlocaladdress.js @@ -0,0 +1,40 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const dns = require('dns'); +const resolver = new dns.Resolver(); +const promiseResolver = new dns.promises.Resolver(); + +// Verifies that setLocalAddress succeeds with IPv4 and IPv6 addresses +{ + resolver.setLocalAddress('127.0.0.1'); + resolver.setLocalAddress('::1'); + resolver.setLocalAddress('127.0.0.1', '::1'); + promiseResolver.setLocalAddress('127.0.0.1', '::1'); +} + +// Verify that setLocalAddress throws if called with an invalid address +{ + assert.throws(() => { + resolver.setLocalAddress('127.0.0.1', '127.0.0.1'); + }, Error); + assert.throws(() => { + resolver.setLocalAddress('::1', '::1'); + }, Error); + assert.throws(() => { + resolver.setLocalAddress('bad'); + }, Error); + assert.throws(() => { + resolver.setLocalAddress(123); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => { + resolver.setLocalAddress('127.0.0.1', 42); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => { + resolver.setLocalAddress(); + }, Error); + assert.throws(() => { + promiseResolver.setLocalAddress(); + }, Error); +} diff --git a/test/js/node/test/parallel/test-dns-setserver-when-querying.js b/test/js/node/test/parallel/test-dns-setserver-when-querying.js new file mode 100644 index 00000000000000..1a002df4986669 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-setserver-when-querying.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const dns = require('dns'); + +const localhost = [ '127.0.0.1' ]; + +{ + // Fix https://github.com/nodejs/node/issues/14734 + + { + const resolver = new dns.Resolver(); + resolver.resolve('localhost', common.mustCall()); + + assert.throws(resolver.setServers.bind(resolver, localhost), { + code: 'ERR_DNS_SET_SERVERS_FAILED', + message: /[Tt]here are pending queries/ + }); + } + + { + dns.resolve('localhost', common.mustCall()); + + // should not throw + dns.setServers(localhost); + } +} diff --git a/test/js/node/test/parallel/test-dns-setservers-type-check.js b/test/js/node/test/parallel/test-dns-setservers-type-check.js new file mode 100644 index 00000000000000..7a19dc5eb067d2 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-setservers-type-check.js @@ -0,0 +1,117 @@ +'use strict'; +const common = require('../common'); +const { addresses } = require('../common/internet'); +const assert = require('assert'); +const dns = require('dns'); +const resolver = new dns.promises.Resolver(); +const dnsPromises = dns.promises; +const promiseResolver = new dns.promises.Resolver(); + +{ + [ + null, + undefined, + Number(addresses.DNS4_SERVER), + addresses.DNS4_SERVER, + { + address: addresses.DNS4_SERVER + }, + ].forEach((val) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "servers" argument must be an instance of Array\./ + }; + assert.throws( + () => { + dns.setServers(val); + }, errObj + ); + assert.throws( + () => { + resolver.setServers(val); + }, errObj + ); + assert.throws( + () => { + dnsPromises.setServers(val); + }, errObj + ); + assert.throws( + () => { + promiseResolver.setServers(val); + }, errObj + ); + }); +} + +{ + [ + [null], + [undefined], + [Number(addresses.DNS4_SERVER)], + [ + { + address: addresses.DNS4_SERVER + }, + ], + ].forEach((val) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "servers\[0\]" argument must be of type string\./ + }; + assert.throws( + () => { + dns.setServers(val); + }, errObj + ); + assert.throws( + () => { + resolver.setServers(val); + }, errObj + ); + assert.throws( + () => { + dnsPromises.setServers(val); + }, errObj + ); + assert.throws( + () => { + promiseResolver.setServers(val); + }, errObj + ); + }); +} + +// This test for 'dns/promises' +{ + const { + setServers + } = require('dns/promises'); + + // This should not throw any error. + (async () => { + setServers([ '127.0.0.1' ]); + })().then(common.mustCall()); + + [ + [null], + [undefined], + [Number(addresses.DNS4_SERVER)], + [ + { + address: addresses.DNS4_SERVER + }, + ], + ].forEach((val) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "servers\[0\]" argument must be of type string\./ + }; + assert.throws(() => { + setServers(val); + }, errObj); + }); +} diff --git a/test/js/node/test/parallel/test-dns.js b/test/js/node/test/parallel/test-dns.js new file mode 100644 index 00000000000000..8c2b0f8e480ea0 --- /dev/null +++ b/test/js/node/test/parallel/test-dns.js @@ -0,0 +1,461 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const assert = require('assert'); + +const dns = require('dns'); +const dnsPromises = dns.promises; +const dgram = require('dgram'); + +const existing = dns.getServers(); +assert(existing.length > 0); + +// Verify that setServers() handles arrays with holes and other oddities +{ + const servers = []; + + servers[0] = '127.0.0.1'; + servers[2] = '0.0.0.0'; + dns.setServers(servers); + + assert.deepStrictEqual(dns.getServers(), ['127.0.0.1', '0.0.0.0']); +} + +{ + const servers = ['127.0.0.1', '192.168.1.1']; + + servers[3] = '127.1.0.1'; + servers[4] = '127.1.0.1'; + servers[5] = '127.1.1.1'; + + Object.defineProperty(servers, 2, { + enumerable: true, + get: () => { + servers.length = 3; + return '0.0.0.0'; + } + }); + + dns.setServers(servers); + assert.deepStrictEqual(dns.getServers(), [ + '127.0.0.1', + '192.168.1.1', + '0.0.0.0', + ]); +} + +{ + // Various invalidities, all of which should throw a clean error. + const invalidServers = [ + ' ', + '\n', + '\0', + '1'.repeat(3 * 4), + // Check for REDOS issues. + ':'.repeat(100000), + '['.repeat(100000), + '['.repeat(100000) + ']'.repeat(100000) + 'a', + ]; + invalidServers.forEach((serv) => { + assert.throws( + () => { + dns.setServers([serv]); + }, + { + name: 'TypeError', + code: 'ERR_INVALID_IP_ADDRESS' + } + ); + }); +} + +const goog = [ + '8.8.8.8', + '8.8.4.4', +]; +dns.setServers(goog); +assert.deepStrictEqual(dns.getServers(), goog); +assert.throws(() => dns.setServers(['foobar']), { + code: 'ERR_INVALID_IP_ADDRESS', + name: 'TypeError', + message: 'Invalid IP address: foobar' +}); +assert.throws(() => dns.setServers(['127.0.0.1:va']), { + code: 'ERR_INVALID_IP_ADDRESS', + name: 'TypeError', + message: 'Invalid IP address: 127.0.0.1:va' +}); +assert.deepStrictEqual(dns.getServers(), goog); + +const goog6 = [ + '2001:4860:4860::8888', + '2001:4860:4860::8844', +]; +dns.setServers(goog6); +assert.deepStrictEqual(dns.getServers(), goog6); + +goog6.push('4.4.4.4'); +dns.setServers(goog6); +assert.deepStrictEqual(dns.getServers(), goog6); + +const ports = [ + '4.4.4.4:53', + '[2001:4860:4860::8888]:53', + '103.238.225.181:666', + '[fe80::483a:5aff:fee6:1f04]:666', + '[fe80::483a:5aff:fee6:1f04]', +]; +const portsExpected = [ + '4.4.4.4', + '2001:4860:4860::8888', + '103.238.225.181:666', + '[fe80::483a:5aff:fee6:1f04]:666', + 'fe80::483a:5aff:fee6:1f04', +]; +dns.setServers(ports); +assert.deepStrictEqual(dns.getServers(), portsExpected); + +dns.setServers([]); +assert.deepStrictEqual(dns.getServers(), []); + +{ + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "rrtype" argument must be of type string\. Received( an instance of Array|: \[\]|: "object")$/ + }; + assert.throws(() => { + dns.resolve('example.com', [], common.mustNotCall()); + }, errObj); + assert.throws(() => { + dnsPromises.resolve('example.com', []); + }, errObj); +} +{ + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "(host)?name" argument must be of type string\. Received:? undefined$/ + }; + assert.throws(() => { + dnsPromises.resolve(); + }, errObj); +} + +// dns.lookup should accept only falsey and string values +{ + const errorReg = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "hostname" argument must be of type string\. Received:? .*|^Expected hostname to be a string/ + }; + + assert.throws(() => dns.lookup({}, common.mustNotCall()), errorReg); + + assert.throws(() => dns.lookup([], common.mustNotCall()), errorReg); + + assert.throws(() => dns.lookup(true, common.mustNotCall()), errorReg); + + assert.throws(() => dns.lookup(1, common.mustNotCall()), errorReg); + + assert.throws(() => dns.lookup(common.mustNotCall(), common.mustNotCall()), + errorReg); + + assert.throws(() => dnsPromises.lookup({}), errorReg); + assert.throws(() => dnsPromises.lookup([]), errorReg); + assert.throws(() => dnsPromises.lookup(true), errorReg); + assert.throws(() => dnsPromises.lookup(1), errorReg); + assert.throws(() => dnsPromises.lookup(common.mustNotCall()), errorReg); +} + +// dns.lookup should accept falsey values +{ + const checkCallback = (err, address, family) => { + assert.ifError(err); + assert.strictEqual(address, null); + assert.strictEqual(family, 4); + }; + + ['', null, undefined, 0, NaN].forEach(async (value) => { + const res = await dnsPromises.lookup(value); + assert.deepStrictEqual(res, { address: null, family: 4 }); + dns.lookup(value, common.mustCall(checkCallback)); + }); +} + +{ + // Make sure that dns.lookup throws if hints does not represent a valid flag. + // (dns.V4MAPPED | dns.ADDRCONFIG | dns.ALL) + 1 is invalid because: + // - it's different from dns.V4MAPPED and dns.ADDRCONFIG and dns.ALL. + // - it's different from any subset of them bitwise ored. + // - it's different from 0. + // - it's an odd number different than 1, and thus is invalid, because + // flags are either === 1 or even. + const hints = (dns.V4MAPPED | dns.ADDRCONFIG | dns.ALL) + 1; + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /The (argument 'hints'|"hints" option) is invalid\. Received:? \d+/ + }; + + assert.throws(() => { + dnsPromises.lookup('nodejs.org', { hints }); + }, err); + assert.throws(() => { + dns.lookup('nodejs.org', { hints }, common.mustNotCall()); + }, err); +} + +assert.throws(() => dns.lookup('nodejs.org'), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +assert.throws(() => dns.lookup('nodejs.org', 4), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +assert.throws(() => dns.lookup('', { + family: 'nodejs.org', + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL, +}), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +dns.lookup('', { family: 4, hints: 0 }, common.mustCall()); + +dns.lookup('', { + family: 6, + hints: dns.ADDRCONFIG +}, common.mustCall()); + +dns.lookup('', { hints: dns.V4MAPPED }, common.mustCall()); + +dns.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED +}, common.mustCall()); + +dns.lookup('', { + hints: dns.ALL +}, common.mustCall()); + +dns.lookup('', { + hints: dns.V4MAPPED | dns.ALL +}, common.mustCall()); + +dns.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL +}, common.mustCall()); + +dns.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL, + family: 'IPv4' +}, common.mustCall()); + +dns.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL, + family: 'IPv6' +}, common.mustCall()); + +(async function() { + await dnsPromises.lookup('', { family: 4, hints: 0 }); + await dnsPromises.lookup('', { family: 6, hints: dns.ADDRCONFIG }); + await dnsPromises.lookup('', { hints: dns.V4MAPPED }); + await dnsPromises.lookup('', { hints: dns.ADDRCONFIG | dns.V4MAPPED }); + await dnsPromises.lookup('', { hints: dns.ALL }); + await dnsPromises.lookup('', { hints: dns.V4MAPPED | dns.ALL }); + await dnsPromises.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL + }); + await dnsPromises.lookup('', { order: 'verbatim' }); +})().then(common.mustCall()); + +{ + const err = { + code: 'ERR_MISSING_ARGS', + name: 'TypeError', + message: 'The "address", "port", and "callback" arguments must be ' + + 'specified' + }; + + assert.throws(() => dns.lookupService('0.0.0.0'), err); + err.message = 'The "address" and "port" arguments must be specified'; + assert.throws(() => dnsPromises.lookupService('0.0.0.0'), err); +} + +{ + const invalidAddress = 'fasdfdsaf'; + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /The (argument 'address'|"address" argument) is invalid\. Received/ + }; + + assert.throws(() => { + dnsPromises.lookupService(invalidAddress, 0); + }, err); + + assert.throws(() => { + dns.lookupService(invalidAddress, 0, common.mustNotCall()); + }, err); +} + +const portErr = (port) => { + const err = { + code: 'ERR_SOCKET_BAD_PORT', + name: 'RangeError' + }; + + assert.throws(() => { + dnsPromises.lookupService('0.0.0.0', port); + }, err); + + assert.throws(() => { + dns.lookupService('0.0.0.0', port, common.mustNotCall()); + }, err); +}; +[null, undefined, 65538, 'test', NaN, Infinity, Symbol(), 0n, true, false, '', () => {}, {}].forEach(portErr); + +assert.throws(() => { + dns.lookupService('0.0.0.0', 80, null); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +{ + dns.resolveMx('foo.onion', function(err) { + assert.strictEqual(err.code, 'ENOTFOUND'); + assert.strictEqual(err.syscall, 'queryMx'); + assert.strictEqual(err.hostname, 'foo.onion'); + assert.strictEqual(err.message, 'queryMx ENOTFOUND foo.onion'); + }); +} + +{ + const cases = [ + // { method: 'resolveAny', + // answers: [ + // { type: 'A', address: '1.2.3.4', ttl: 0 }, + // { type: 'AAAA', address: '::42', ttl: 0 }, + // { type: 'MX', priority: 42, exchange: 'foobar.com', ttl: 0 }, + // { type: 'NS', value: 'foobar.org', ttl: 0 }, + // { type: 'PTR', value: 'baz.org', ttl: 0 }, + // { + // type: 'SOA', + // nsname: 'ns1.example.com', + // hostmaster: 'admin.example.com', + // serial: 3210987654, + // refresh: 900, + // retry: 900, + // expire: 1800, + // minttl: 3333333333 + // }, + // ] }, + + { method: 'resolve4', + options: { ttl: true }, + answers: [ { type: 'A', address: '1.2.3.4', ttl: 0 } ] }, + + { method: 'resolve6', + options: { ttl: true }, + answers: [ { type: 'AAAA', address: '::42', ttl: 0 } ] }, + + { method: 'resolveSoa', + answers: [ + { + type: 'SOA', + nsname: 'ns1.example.com', + hostmaster: 'admin.example.com', + serial: 3210987654, + refresh: 900, + retry: 900, + expire: 1800, + minttl: 3333333333 + }, + ] }, + ]; + + const server = dgram.createSocket('udp4'); + + server.on('message', common.mustCallAtLeast((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, 'example.org'); + + server.send(dnstools.writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: cases[0].answers.map( + (answer) => Object.assign({ domain }, answer) + ), + }), port, address); + }, cases.length * 2 - 1)); + + server.bind(0, common.mustCall(() => { + const address = server.address(); + dns.setServers([`127.0.0.1:${address.port}`]); + + function validateResults(res) { + if (!Array.isArray(res)) + res = [res]; + + assert.deepStrictEqual(res.map(tweakEntry), + cases[0].answers.map(tweakEntry)); + } + + function tweakEntry(r) { + const ret = { ...r }; + + const { method } = cases[0]; + + // TTL values are only provided for A and AAAA entries. + if (!['A', 'AAAA'].includes(ret.type) && !/^resolve(4|6)?$/.test(method)) + delete ret.ttl; + + if (method !== 'resolveAny') + delete ret.type; + + return ret; + } + + (async function nextCase() { + if (cases.length === 0) + return server.close(); + + const { method, options } = cases[0]; + + validateResults(await dnsPromises[method]('example.org', options)); + + dns[method]('example.org', ...(options? [options] : []), common.mustSucceed((res) => { + validateResults(res); + cases.shift(); + nextCase(); + })); + })().then(common.mustCall()); + + })); +} diff --git a/test/js/node/test/parallel/test-path-glob.js b/test/js/node/test/parallel/test-path-glob.js new file mode 100644 index 00000000000000..47647e12784e5a --- /dev/null +++ b/test/js/node/test/parallel/test-path-glob.js @@ -0,0 +1,44 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const path = require('path'); + +const globs = { + win32: [ + ['foo\\bar\\baz', 'foo\\[bcr]ar\\baz', true], // Matches 'bar' or 'car' in 'foo\\bar' + ['foo\\bar\\baz', 'foo\\[!bcr]ar\\baz', false], // Matches anything except 'bar' or 'car' in 'foo\\bar' + ['foo\\bar\\baz', 'foo\\[bc-r]ar\\baz', true], // Matches 'bar' or 'car' using range in 'foo\\bar' + ['foo\\bar\\baz', 'foo\\*\\!bar\\*\\baz', false], // Matches anything with 'foo' and 'baz' but not 'bar' in between + ['foo\\bar1\\baz', 'foo\\bar[0-9]\\baz', true], // Matches 'bar' followed by any digit in 'foo\\bar1' + ['foo\\bar5\\baz', 'foo\\bar[0-9]\\baz', true], // Matches 'bar' followed by any digit in 'foo\\bar5' + ['foo\\barx\\baz', 'foo\\bar[a-z]\\baz', true], // Matches 'bar' followed by any lowercase letter in 'foo\\barx' + ['foo\\bar\\baz\\boo', 'foo\\[bc-r]ar\\baz\\*', true], // Matches 'bar' or 'car' in 'foo\\bar' + ['foo\\bar\\baz', 'foo/**', true], // Matches anything in 'foo' + ['foo\\bar\\baz', '*', false], // No match + ], + posix: [ + ['foo/bar/baz', 'foo/[bcr]ar/baz', true], // Matches 'bar' or 'car' in 'foo/bar' + ['foo/bar/baz', 'foo/[!bcr]ar/baz', false], // Matches anything except 'bar' or 'car' in 'foo/bar' + ['foo/bar/baz', 'foo/[bc-r]ar/baz', true], // Matches 'bar' or 'car' using range in 'foo/bar' + ['foo/bar/baz', 'foo/*/!bar/*/baz', false], // Matches anything with 'foo' and 'baz' but not 'bar' in between + ['foo/bar1/baz', 'foo/bar[0-9]/baz', true], // Matches 'bar' followed by any digit in 'foo/bar1' + ['foo/bar5/baz', 'foo/bar[0-9]/baz', true], // Matches 'bar' followed by any digit in 'foo/bar5' + ['foo/barx/baz', 'foo/bar[a-z]/baz', true], // Matches 'bar' followed by any lowercase letter in 'foo/barx' + ['foo/bar/baz/boo', 'foo/[bc-r]ar/baz/*', true], // Matches 'bar' or 'car' in 'foo/bar' + ['foo/bar/baz', 'foo/**', true], // Matches anything in 'foo' + ['foo/bar/baz', '*', false], // No match + ], +}; + + +for (const [platform, platformGlobs] of Object.entries(globs)) { + for (const [pathStr, glob, expected] of platformGlobs) { + const actual = path[platform].matchesGlob(pathStr, glob); + assert.strictEqual(actual, expected, `Expected ${pathStr} to ` + (expected ? '' : 'not ') + `match ${glob} on ${platform}`); + } +} + +// Test for non-string input +assert.throws(() => path.matchesGlob(123, 'foo/bar/baz'), /.*must be of type string.*/); +assert.throws(() => path.matchesGlob('foo/bar/baz', 123), /.*must be of type string.*/); diff --git a/test/js/node/test/parallel/test-process-assert.js b/test/js/node/test/parallel/test-process-assert.js new file mode 100644 index 00000000000000..f740d3d70c7c0f --- /dev/null +++ b/test/js/node/test/parallel/test-process-assert.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +assert.strictEqual(process.assert(1, 'error'), undefined); +assert.throws(() => { + process.assert(undefined, 'errorMessage'); +}, { + code: 'ERR_ASSERTION', + name: 'Error', + message: 'errorMessage' +}); +assert.throws(() => { + process.assert(false); +}, { + code: 'ERR_ASSERTION', + name: 'Error', + message: 'assertion error' +}); diff --git a/test/js/node/test/parallel/test-process-available-memory.js b/test/js/node/test/parallel/test-process-available-memory.js new file mode 100644 index 00000000000000..67de5b5e0bb000 --- /dev/null +++ b/test/js/node/test/parallel/test-process-available-memory.js @@ -0,0 +1,5 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const availableMemory = process.availableMemory(); +assert(typeof availableMemory, 'number'); diff --git a/test/js/node/test/parallel/test-process-beforeexit-throw-exit.js b/test/js/node/test/parallel/test-process-beforeexit-throw-exit.js new file mode 100644 index 00000000000000..6e9d764be90baa --- /dev/null +++ b/test/js/node/test/parallel/test-process-beforeexit-throw-exit.js @@ -0,0 +1,12 @@ +'use strict'; +const common = require('../common'); +common.skipIfWorker(); + +// Test that 'exit' is emitted if 'beforeExit' throws. + +process.on('exit', common.mustCall(() => { + process.exitCode = 0; +})); +process.on('beforeExit', common.mustCall(() => { + throw new Error(); +})); diff --git a/test/js/node/test/parallel/test-process-beforeexit.js b/test/js/node/test/parallel/test-process-beforeexit.js new file mode 100644 index 00000000000000..e04b756cade8bc --- /dev/null +++ b/test/js/node/test/parallel/test-process-beforeexit.js @@ -0,0 +1,81 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const net = require('net'); + +process.once('beforeExit', common.mustCall(tryImmediate)); + +function tryImmediate() { + setImmediate(common.mustCall(() => { + process.once('beforeExit', common.mustCall(tryTimer)); + })); +} + +function tryTimer() { + setTimeout(common.mustCall(() => { + process.once('beforeExit', common.mustCall(tryListen)); + }), 1); +} + +function tryListen() { + net.createServer() + .listen(0) + .on('listening', common.mustCall(function() { + this.close(); + process.once('beforeExit', common.mustCall(tryRepeatedTimer)); + })); +} + +// Test that a function invoked from the beforeExit handler can use a timer +// to keep the event loop open, which can use another timer to keep the event +// loop open, etc. +// +// After N times, call function `tryNextTick` to test behaviors of the +// `process.nextTick`. +function tryRepeatedTimer() { + const N = 5; + let n = 0; + const repeatedTimer = common.mustCall(function() { + if (++n < N) + setTimeout(repeatedTimer, 1); + else // n == N + process.once('beforeExit', common.mustCall(tryNextTickSetImmediate)); + }, N); + setTimeout(repeatedTimer, 1); +} + +// Test if the callback of `process.nextTick` can be invoked. +function tryNextTickSetImmediate() { + process.nextTick(common.mustCall(function() { + setImmediate(common.mustCall(() => { + process.once('beforeExit', common.mustCall(tryNextTick)); + })); + })); +} + +// Test that `process.nextTick` won't keep the event loop running by itself. +function tryNextTick() { + process.nextTick(common.mustCall(function() { + process.once('beforeExit', common.mustNotCall()); + })); +} diff --git a/test/js/node/test/parallel/test-process-binding-util.js b/test/js/node/test/parallel/test-process-binding-util.js new file mode 100644 index 00000000000000..a834676e05be45 --- /dev/null +++ b/test/js/node/test/parallel/test-process-binding-util.js @@ -0,0 +1,58 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const util = require('util'); + +const utilBinding = process.binding('util'); +assert.deepStrictEqual( + Object.keys(utilBinding).sort(), + [ + 'isAnyArrayBuffer', + 'isArgumentsObject', + 'isArrayBuffer', + 'isArrayBufferView', + 'isAsyncFunction', + 'isBigInt64Array', + 'isBigIntObject', + 'isBigUint64Array', + 'isBooleanObject', + 'isBoxedPrimitive', + 'isCryptoKey', + 'isDataView', + 'isDate', + 'isEventTarget', + 'isExternal', + 'isFloat16Array', + 'isFloat32Array', + 'isFloat64Array', + 'isGeneratorFunction', + 'isGeneratorObject', + 'isInt16Array', + 'isInt32Array', + 'isInt8Array', + 'isKeyObject', + 'isMap', + 'isMapIterator', + 'isModuleNamespaceObject', + 'isNativeError', + 'isNumberObject', + 'isPromise', + 'isProxy', + 'isRegExp', + 'isSet', + 'isSetIterator', + 'isSharedArrayBuffer', + 'isStringObject', + 'isSymbolObject', + 'isTypedArray', + 'isUint16Array', + 'isUint32Array', + 'isUint8Array', + 'isUint8ClampedArray', + 'isWeakMap', + 'isWeakSet', + ]); + +for (const k of Object.keys(utilBinding)) { + assert.strictEqual(utilBinding[k], util.types[k]); +} diff --git a/test/js/node/test/parallel/test-process-chdir-errormessage.js b/test/js/node/test/parallel/test-process-chdir-errormessage.js new file mode 100644 index 00000000000000..16cdf4aa1deaf3 --- /dev/null +++ b/test/js/node/test/parallel/test-process-chdir-errormessage.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); +const assert = require('assert'); + +assert.throws( + () => { + process.chdir('does-not-exist'); + }, + { + name: 'Error', + code: 'ENOENT', + // message: /ENOENT: No such file or directory, chdir .+ -> 'does-not-exist'/, + path: process.cwd(), + syscall: 'chdir', + dest: 'does-not-exist' + } +); diff --git a/test/js/node/test/parallel/test-process-chdir.js b/test/js/node/test/parallel/test-process-chdir.js new file mode 100644 index 00000000000000..ee59df853b24ce --- /dev/null +++ b/test/js/node/test/parallel/test-process-chdir.js @@ -0,0 +1,44 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + +const tmpdir = require('../common/tmpdir'); + +process.chdir('..'); +assert.notStrictEqual(process.cwd(), __dirname); +process.chdir(__dirname); +assert.strictEqual(process.cwd(), __dirname); + +let dirName; +if (process.versions.icu) { + // ICU is available, use characters that could possibly be decomposed + dirName = 'weird \uc3a4\uc3ab\uc3af characters \u00e1\u00e2\u00e3'; +} else { + // ICU is unavailable, use characters that can't be decomposed + dirName = 'weird \ud83d\udc04 characters \ud83d\udc05'; +} +const dir = tmpdir.resolve(dirName); + +// Make sure that the tmp directory is clean +tmpdir.refresh(); + +fs.mkdirSync(dir); +process.chdir(dir); +assert.strictEqual(process.cwd().normalize(), dir.normalize()); + +process.chdir('..'); +assert.strictEqual(process.cwd().normalize(), + path.resolve(tmpdir.path).normalize()); + +const err = { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "directory" argument must be of type string/ +}; +assert.throws(function() { process.chdir({}); }, err); +assert.throws(function() { process.chdir(); }, err); diff --git a/test/js/node/test/parallel/test-process-config.js b/test/js/node/test/parallel/test-process-config.js new file mode 100644 index 00000000000000..20ebc36a996385 --- /dev/null +++ b/test/js/node/test/parallel/test-process-config.js @@ -0,0 +1,69 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); + +// Checks that the internal process.config is equivalent to the config.gypi file +// created when we run configure. + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +// Check for existence of `process.config`. +assert(Object.hasOwn(process, 'config')); + +// Ensure that `process.config` is an Object. +assert.strictEqual(Object(process.config), process.config); + +// Ensure that you can't change config values +assert.throws(() => { process.config.variables = 42; }, TypeError); + +const configPath = path.resolve(__dirname, '..', '..', 'config.gypi'); + +if (!fs.existsSync(configPath)) { + common.skip('config.gypi does not exist.'); +} + +let config = fs.readFileSync(configPath, 'utf8'); + +// Clean up comment at the first line. +config = config.split('\n').slice(1).join('\n'); +config = config.replace(/"/g, '\\"'); +config = config.replace(/'/g, '"'); +config = JSON.parse(config, (key, value) => { + if (value === 'true') return true; + if (value === 'false') return false; + return value; +}); + +try { + assert.deepStrictEqual(config, process.config); +} catch (e) { + // If the assert fails, it only shows 3 lines. We need all the output to + // compare. + console.log('config:', config); + console.log('process.config:', process.config); + + throw e; +} diff --git a/test/js/node/test/parallel/test-process-constrained-memory.js b/test/js/node/test/parallel/test-process-constrained-memory.js new file mode 100644 index 00000000000000..03f99b166f72ca --- /dev/null +++ b/test/js/node/test/parallel/test-process-constrained-memory.js @@ -0,0 +1,6 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const constrainedMemory = process.constrainedMemory(); +assert.strictEqual(typeof constrainedMemory, 'number'); diff --git a/test/js/node/test/parallel/test-process-cpuUsage.js b/test/js/node/test/parallel/test-process-cpuUsage.js new file mode 100644 index 00000000000000..f1580d5f092b72 --- /dev/null +++ b/test/js/node/test/parallel/test-process-cpuUsage.js @@ -0,0 +1,118 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const result = process.cpuUsage(); + +// Validate the result of calling with no previous value argument. +validateResult(result); + +// Validate the result of calling with a previous value argument. +validateResult(process.cpuUsage(result)); + +// Ensure the results are >= the previous. +let thisUsage; +let lastUsage = process.cpuUsage(); +for (let i = 0; i < 10; i++) { + thisUsage = process.cpuUsage(); + validateResult(thisUsage); + assert(thisUsage.user >= lastUsage.user); + assert(thisUsage.system >= lastUsage.system); + lastUsage = thisUsage; +} + +// Ensure that the diffs are >= 0. +let startUsage; +let diffUsage; +for (let i = 0; i < 10; i++) { + startUsage = process.cpuUsage(); + diffUsage = process.cpuUsage(startUsage); + validateResult(startUsage); + validateResult(diffUsage); + assert(diffUsage.user >= 0); + assert(diffUsage.system >= 0); +} + +// Ensure that an invalid shape for the previous value argument throws an error. +assert.throws( + () => process.cpuUsage(1), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "prevValue" argument must be of type object. ' + + 'Received type number (1)' + } +); + +// Check invalid types. +[ + {}, + { user: 'a' }, + { user: null, system: 'c' }, +].forEach((value) => { + assert.throws( + () => process.cpuUsage(value), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "prevValue.user" property must be of type number.' + + common.invalidArgTypeHelper(value.user) + } + ); +}); + +[ + { user: 3, system: 'b' }, + { user: 3, system: null }, +].forEach((value) => { + assert.throws( + () => process.cpuUsage(value), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "prevValue.system" property must be of type number.' + + common.invalidArgTypeHelper(value.system) + } + ); +}); + +// Check invalid values. +[ + { user: -1, system: 2 }, + { user: Number.POSITIVE_INFINITY, system: 4 }, +].forEach((value) => { + assert.throws( + () => process.cpuUsage(value), + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'RangeError', + message: "The property 'prevValue.user' is invalid. " + + `Received ${value.user}`, + } + ); +}); + +[ + { user: 3, system: -2 }, + { user: 5, system: Number.NEGATIVE_INFINITY }, +].forEach((value) => { + assert.throws( + () => process.cpuUsage(value), + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'RangeError', + message: "The property 'prevValue.system' is invalid. " + + `Received ${value.system}`, + } + ); +}); + +// Ensure that the return value is the expected shape. +function validateResult(result) { + assert.notStrictEqual(result, null); + + assert(Number.isFinite(result.user)); + assert(Number.isFinite(result.system)); + + assert(result.user >= 0); + assert(result.system >= 0); +} diff --git a/test/js/node/test/parallel/test-process-dlopen-error-message-crash.js b/test/js/node/test/parallel/test-process-dlopen-error-message-crash.js new file mode 100644 index 00000000000000..cc93e01abd81df --- /dev/null +++ b/test/js/node/test/parallel/test-process-dlopen-error-message-crash.js @@ -0,0 +1,47 @@ +'use strict'; + +// This is a regression test for some scenarios in which node would pass +// unsanitized user input to a printf-like formatting function when dlopen +// fails, potentially crashing the process. + +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const assert = require('assert'); +const fs = require('fs'); + +// This error message should not be passed to a printf-like function. +assert.throws(() => { + process.dlopen({ exports: {} }, 'foo-%s.node'); +}, ({ name, code, message }) => { + assert.strictEqual(name, 'Error'); + assert.strictEqual(code, 'ERR_DLOPEN_FAILED'); + if (!common.isAIX && !common.isIBMi) { + assert.match(message, /foo-%s\.node/); + } + return true; +}); + +const notBindingDir = 'test/addons/not-a-binding'; +const notBindingPath = `${notBindingDir}/build/Release/binding.node`; +const strangeBindingPath = `${tmpdir.path}/binding-%s.node`; +// Ensure that the addon directory exists, but skip the remainder of the test if +// the addon has not been compiled. +// fs.accessSync(notBindingDir); +// try { +// fs.copyFileSync(notBindingPath, strangeBindingPath); +// } catch (err) { +// if (err.code !== 'ENOENT') throw err; +// common.skip(`addon not found: ${notBindingPath}`); +// } + +// This error message should also not be passed to a printf-like function. +assert.throws(() => { + process.dlopen({ exports: {} }, strangeBindingPath); +}, { + name: 'Error', + code: 'ERR_DLOPEN_FAILED', + message: /binding-%s\.node/ +}); diff --git a/test/js/node/test/parallel/test-process-emitwarning.js b/test/js/node/test/parallel/test-process-emitwarning.js new file mode 100644 index 00000000000000..e1c7473f8aad3d --- /dev/null +++ b/test/js/node/test/parallel/test-process-emitwarning.js @@ -0,0 +1,81 @@ +// Flags: --no-warnings +// The flag suppresses stderr output but the warning event will still emit +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const testMsg = 'A Warning'; +const testCode = 'CODE001'; +const testDetail = 'Some detail'; +const testType = 'CustomWarning'; + +process.on('warning', common.mustCall((warning) => { + assert(warning); + assert.match(warning.name, /^(?:Warning|CustomWarning)/); + assert.strictEqual(warning.message, testMsg); + if (warning.code) assert.strictEqual(warning.code, testCode); + if (warning.detail) assert.strictEqual(warning.detail, testDetail); +}, 15)); + +class CustomWarning extends Error { + constructor() { + super(); + this.name = testType; + this.message = testMsg; + this.code = testCode; + Error.captureStackTrace(this, CustomWarning); + } +} + +[ + [testMsg], + [testMsg, testType], + [testMsg, CustomWarning], + [testMsg, testType, CustomWarning], + [testMsg, testType, testCode], + [testMsg, { type: testType }], + [testMsg, { type: testType, code: testCode }], + [testMsg, { type: testType, code: testCode, detail: testDetail }], + [new CustomWarning()], + // Detail will be ignored for the following. No errors thrown + [testMsg, { type: testType, code: testCode, detail: true }], + [testMsg, { type: testType, code: testCode, detail: [] }], + [testMsg, { type: testType, code: testCode, detail: null }], + [testMsg, { type: testType, code: testCode, detail: 1 }], +].forEach((args) => { + process.emitWarning(...args); +}); + +const warningNoToString = new CustomWarning(); +warningNoToString.toString = null; +process.emitWarning(warningNoToString); + +const warningThrowToString = new CustomWarning(); +warningThrowToString.toString = function() { + throw new Error('invalid toString'); +}; +process.emitWarning(warningThrowToString); + +// TypeError is thrown on invalid input +[ + [1], + [{}], + [true], + [[]], + ['', '', {}], + ['', 1], + ['', '', 1], + ['', true], + ['', '', true], + ['', []], + ['', '', []], + [], + [undefined, 'foo', 'bar'], + [undefined], +].forEach((args) => { + assert.throws( + () => process.emitWarning(...args), + { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' } + ); +}); diff --git a/test/js/node/test/parallel/test-process-euid-egid.js b/test/js/node/test/parallel/test-process-euid-egid.js new file mode 100644 index 00000000000000..06854ba3f574fe --- /dev/null +++ b/test/js/node/test/parallel/test-process-euid-egid.js @@ -0,0 +1,70 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +if (common.isWindows) { + assert.strictEqual(process.geteuid, undefined); + assert.strictEqual(process.getegid, undefined); + assert.strictEqual(process.seteuid, undefined); + assert.strictEqual(process.setegid, undefined); + return; +} + +if (!common.isMainThread) + return; + +assert.throws(() => { + process.seteuid({}); +}, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "id" argument must be of type number or string. ' + + 'Received an instance of Object' +}); + +assert.throws(() => { + process.seteuid('fhqwhgadshgnsdhjsdbkhsdabkfabkveyb'); +}, { + code: 'ERR_UNKNOWN_CREDENTIAL', + message: 'User identifier does not exist: fhqwhgadshgnsdhjsdbkhsdabkfabkveyb' +}); + +// IBMi does not support below operations. +if (common.isIBMi) + return; + +// If we're not running as super user... +if (process.getuid() !== 0) { + // Should not throw. + process.getegid(); + process.geteuid(); + + assert.throws(() => { + process.setegid('nobody'); + }, /(?:EPERM: .+|Group identifier does not exist: nobody)$/); + + assert.throws(() => { + process.seteuid('nobody'); + }, /(?:EPERM: .+|User identifier does not exist: nobody)$/); + + return; +} + +// If we are running as super user... +const oldgid = process.getegid(); +try { + process.setegid('nobody'); +} catch (err) { + if (err.message !== 'Group identifier does not exist: nobody') { + throw err; + } else { + process.setegid('nogroup'); + } +} +const newgid = process.getegid(); +assert.notStrictEqual(newgid, oldgid); + +const olduid = process.geteuid(); +process.seteuid('nobody'); +const newuid = process.geteuid(); +assert.notStrictEqual(newuid, olduid); diff --git a/test/js/node/test/parallel/test-process-exception-capture-errors.js b/test/js/node/test/parallel/test-process-exception-capture-errors.js new file mode 100644 index 00000000000000..8eb825267cf336 --- /dev/null +++ b/test/js/node/test/parallel/test-process-exception-capture-errors.js @@ -0,0 +1,24 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +assert.throws( + () => process.setUncaughtExceptionCaptureCallback(42), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "fn" argument must be of type function or null. ' + + 'Received type number (42)' + } +); + +process.setUncaughtExceptionCaptureCallback(common.mustNotCall()); + +assert.throws( + () => process.setUncaughtExceptionCaptureCallback(common.mustNotCall()), + { + code: 'ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET', + name: 'Error', + message: /setupUncaughtExceptionCapture.*called while a capture callback/ + } +); diff --git a/test/js/node/test/parallel/test-process-exit-code-validation.js b/test/js/node/test/parallel/test-process-exit-code-validation.js new file mode 100644 index 00000000000000..59934fa31dcdab --- /dev/null +++ b/test/js/node/test/parallel/test-process-exit-code-validation.js @@ -0,0 +1,145 @@ +'use strict'; + +require('../common'); + +const invalids = [ + { + code: '', + expected: 1, + pattern: 'Received type string \\(""\\)$', + }, + { + code: '1 one', + expected: 1, + pattern: 'Received type string \\("1 one"\\)$', + }, + { + code: 'two', + expected: 1, + pattern: 'Received type string \\("two"\\)$', + }, + { + code: {}, + expected: 1, + pattern: 'Received an instance of Object$', + }, + { + code: [], + expected: 1, + pattern: 'Received an instance of Array$', + }, + { + code: true, + expected: 1, + pattern: 'Received type boolean \\(true\\)$', + }, + { + code: false, + expected: 1, + pattern: 'Received type boolean \\(false\\)$', + }, + { + code: 2n, + expected: 1, + pattern: 'Received type bigint \\(2n\\)$', + }, + { + code: 2.1, + expected: 1, + pattern: 'Received 2.1$', + }, + { + code: Infinity, + expected: 1, + pattern: 'Received Infinity$', + }, + { + code: NaN, + expected: 1, + pattern: 'Received NaN$', + }, +]; +const valids = [ + { + code: 1, + expected: 1, + }, + { + code: '2', + expected: 2, + }, + { + code: undefined, + expected: 0, + }, + { + code: null, + expected: 0, + }, + { + code: 0, + expected: 0, + }, + { + code: '0', + expected: 0, + }, +]; +const args = [...invalids, ...valids]; + +if (process.argv[2] === undefined) { + const { spawnSync } = require('node:child_process'); + const { inspect, debuglog } = require('node:util'); + const { throws, strictEqual } = require('node:assert'); + + const debug = debuglog('test'); + const node = process.execPath; + const test = (index, useProcessExitCode) => { + const { status: code } = spawnSync(node, [ + __filename, + index, + useProcessExitCode, + ]); + console.log(`actual: ${code}, ${args[index].expected} ${index} ${!!useProcessExitCode} ${args[index].code}`); + debug(`actual: ${code}, ${inspect(args[index])} ${!!useProcessExitCode}`); + strictEqual( + code, + args[index].expected, + `actual: ${code}, ${inspect(args[index])}` + ); + }; + + // Check process.exitCode + for (const arg of invalids) { + debug(`invaild code: ${inspect(arg.code)}`); + throws(() => (process.exitCode = arg.code), new RegExp(arg.pattern)); + } + for (const arg of valids) { + debug(`vaild code: ${inspect(arg.code)}`); + process.exitCode = arg.code; + } + + throws(() => { + delete process.exitCode; + // }, /Cannot delete property 'exitCode' of #/); + }, /Unable to delete property./); + process.exitCode = 0; + + // Check process.exit([code]) + for (const index of args.keys()) { + test(index); + test(index, true); + } +} else { + const index = parseInt(process.argv[2]); + const useProcessExitCode = process.argv[3] !== 'undefined'; + if (Number.isNaN(index)) { + return process.exit(100); + } + + if (useProcessExitCode) { + process.exitCode = args[index].code; + } else { + process.exit(args[index].code); + } +} diff --git a/test/js/node/test/parallel/test-process-hrtime.js b/test/js/node/test/parallel/test-process-hrtime.js new file mode 100644 index 00000000000000..34ef514aac309b --- /dev/null +++ b/test/js/node/test/parallel/test-process-hrtime.js @@ -0,0 +1,74 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); + +// The default behavior, return an Array "tuple" of numbers +const tuple = process.hrtime(); + +// Validate the default behavior +validateTuple(tuple); + +// Validate that passing an existing tuple returns another valid tuple +validateTuple(process.hrtime(tuple)); + +// Test that only an Array may be passed to process.hrtime() +assert.throws(() => { + process.hrtime(1); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "time" argument must be an instance of Array. Received type ' + + 'number (1)' +}); +assert.throws(() => { + process.hrtime([]); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "time" is out of range. It must be 2. Received 0' +}); +assert.throws(() => { + process.hrtime([1]); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "time" is out of range. It must be 2. Received 1' +}); +assert.throws(() => { + process.hrtime([1, 2, 3]); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "time" is out of range. It must be 2. Received 3' +}); + +function validateTuple(tuple) { + assert(Array.isArray(tuple)); + assert.strictEqual(tuple.length, 2); + assert(Number.isInteger(tuple[0])); + assert(Number.isInteger(tuple[1])); +} + +const diff = process.hrtime([0, 1e9 - 1]); +assert(diff[1] >= 0); // https://github.com/nodejs/node/issues/4751 diff --git a/test/js/node/test/parallel/test-process-kill-pid.js b/test/js/node/test/parallel/test-process-kill-pid.js new file mode 100644 index 00000000000000..1fa1d6c2ab4211 --- /dev/null +++ b/test/js/node/test/parallel/test-process-kill-pid.js @@ -0,0 +1,116 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN +const assert = require('assert'); + +// Test variants of pid +// +// null: TypeError +// undefined: TypeError +// +// 'SIGTERM': TypeError +// +// String(process.pid): TypeError +// +// Nan, Infinity, -Infinity: TypeError +// +// 0, String(0): our group process +// +// process.pid, String(process.pid): ourself + +assert.throws(() => process.kill('SIGTERM'), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "pid" argument must be of type number. Received type string ("SIGTERM")' +}); + +[null, undefined, NaN, Infinity, -Infinity].forEach((val) => { + assert.throws(() => process.kill(val), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "pid" argument must be of type number.' + + common.invalidArgTypeHelper(val) + }); +}); + +// Test that kill throws an error for unknown signal names +assert.throws(() => process.kill(0, 'test'), { + code: 'ERR_UNKNOWN_SIGNAL', + name: 'TypeError', + message: 'Unknown signal: test' +}); + +// Test that kill throws an error for invalid signal numbers +assert.throws(() => process.kill(0, 987), { + code: 'EINVAL', + name: 'SystemError', + message: 'kill() failed: EINVAL: Invalid argument' +}); + +// Test kill argument processing in valid cases. +// +// Monkey patch _kill so that we don't actually send any signals, particularly +// that we don't kill our process group, or try to actually send ANY signals on +// windows, which doesn't support them. +function kill(tryPid, trySig, expectPid, expectSig) { + let getPid; + let getSig; + const origKill = process._kill; + process._kill = function(pid, sig) { + getPid = pid; + getSig = sig; + + // un-monkey patch process._kill + process._kill = origKill; + }; + + process.kill(tryPid, trySig); + + assert.strictEqual(getPid.toString(), expectPid.toString()); + assert.strictEqual(getSig, expectSig); +} + +// Note that SIGHUP and SIGTERM map to 1 and 15 respectively, even on Windows +// (for Windows, libuv maps 1 and 15 to the correct behavior). + +kill(0, 'SIGHUP', 0, 1); +kill(0, undefined, 0, 15); +kill('0', 'SIGHUP', 0, 1); +kill('0', undefined, 0, 15); + +// Confirm that numeric signal arguments are supported + +kill(0, 1, 0, 1); +kill(0, 15, 0, 15); + +// Negative numbers are meaningful on unix +kill(-1, 'SIGHUP', -1, 1); +kill(-1, undefined, -1, 15); +kill('-1', 'SIGHUP', -1, 1); +kill('-1', undefined, -1, 15); + +kill(process.pid, 'SIGHUP', process.pid, 1); +kill(process.pid, undefined, process.pid, 15); +kill(String(process.pid), 'SIGHUP', process.pid, 1); +kill(String(process.pid), undefined, process.pid, 15); diff --git a/test/js/node/test/parallel/test-process-no-deprecation.js b/test/js/node/test/parallel/test-process-no-deprecation.js new file mode 100644 index 00000000000000..bcda99de25069d --- /dev/null +++ b/test/js/node/test/parallel/test-process-no-deprecation.js @@ -0,0 +1,32 @@ +'use strict'; +// Flags: --no-warnings + +// The --no-warnings flag only suppresses writing the warning to stderr, not the +// emission of the corresponding event. This test file can be run without it. + +const common = require('../common'); +process.noDeprecation = true; + +const assert = require('assert'); + +function listener() { + assert.fail('received unexpected warning'); +} + +process.addListener('warning', listener); + +process.emitWarning('Something is deprecated.', 'DeprecationWarning'); + +// The warning would be emitted in the next tick, so continue after that. +process.nextTick(common.mustCall(() => { + // Check that deprecations can be re-enabled. + process.noDeprecation = false; + process.removeListener('warning', listener); + + process.addListener('warning', common.mustCall((warning) => { + assert.strictEqual(warning.name, 'DeprecationWarning'); + assert.strictEqual(warning.message, 'Something else is deprecated.'); + })); + + process.emitWarning('Something else is deprecated.', 'DeprecationWarning'); +})); diff --git a/test/js/node/test/parallel/test-process-really-exit.js b/test/js/node/test/parallel/test-process-really-exit.js new file mode 100644 index 00000000000000..8445d220ca88b7 --- /dev/null +++ b/test/js/node/test/parallel/test-process-really-exit.js @@ -0,0 +1,17 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +// Ensure that the reallyExit hook is executed. +// see: https://github.com/nodejs/node/issues/25650 +if (process.argv[2] === 'subprocess') { + process.reallyExit = function() { + console.info('really exited'); + }; + process.exit(); +} else { + const { spawnSync } = require('child_process'); + const out = spawnSync(process.execPath, [__filename, 'subprocess']); + const observed = out.output[1].toString('utf8').trim(); + assert.strictEqual(observed, 'really exited'); +} diff --git a/test/js/node/test/parallel/test-process-release.js b/test/js/node/test/parallel/test-process-release.js new file mode 100644 index 00000000000000..98a089a8f9ef5a --- /dev/null +++ b/test/js/node/test/parallel/test-process-release.js @@ -0,0 +1,32 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const versionParts = process.versions.node.split('.'); + +assert.strictEqual(process.release.name, 'node'); + +// It's expected that future LTS release lines will have additional +// branches in here +if (versionParts[0] === '4' && versionParts[1] >= 2) { + assert.strictEqual(process.release.lts, 'Argon'); +} else if (versionParts[0] === '6' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Boron'); +} else if (versionParts[0] === '8' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Carbon'); +} else if (versionParts[0] === '10' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Dubnium'); +} else if (versionParts[0] === '12' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Erbium'); +} else if (versionParts[0] === '14' && versionParts[1] >= 15) { + assert.strictEqual(process.release.lts, 'Fermium'); +} else if (versionParts[0] === '16' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Gallium'); +} else if (versionParts[0] === '18' && versionParts[1] >= 12) { + assert.strictEqual(process.release.lts, 'Hydrogen'); +} else if (versionParts[0] === '20' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Iron'); +} else { + assert.strictEqual(process.release.lts, undefined); +} diff --git a/test/js/node/test/parallel/test-process-setgroups.js b/test/js/node/test/parallel/test-process-setgroups.js new file mode 100644 index 00000000000000..c26b5dbaf1cfc0 --- /dev/null +++ b/test/js/node/test/parallel/test-process-setgroups.js @@ -0,0 +1,55 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +if (common.isWindows) { + assert.strictEqual(process.setgroups, undefined); + return; +} + +if (!common.isMainThread) + return; + +assert.throws( + () => { + process.setgroups(); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "groups" argument must be an instance of Array. ' + + 'Received undefined' + } +); + +assert.throws( + () => { + process.setgroups([1, -1]); + }, + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + } +); + +[undefined, null, true, {}, [], () => {}].forEach((val) => { + assert.throws( + () => { + process.setgroups([val]); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "groups[0]" argument must be ' + + 'of type number or string.' + + common.invalidArgTypeHelper(val) + } + ); +}); + +assert.throws(() => { + process.setgroups([1, 'fhqwhgadshgnsdhjsdbkhsdabkfabkveyb']); +}, { + code: 'ERR_UNKNOWN_CREDENTIAL', + message: 'Group identifier does not exist: fhqwhgadshgnsdhjsdbkhsdabkfabkveyb' +}); diff --git a/test/js/node/test/parallel/test-process-title-cli.js b/test/js/node/test/parallel/test-process-title-cli.js new file mode 100644 index 00000000000000..98b3da003f77c6 --- /dev/null +++ b/test/js/node/test/parallel/test-process-title-cli.js @@ -0,0 +1,17 @@ +// Flags: --title=foo +'use strict'; + +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN + +if (common.isSunOS) + common.skip(`Unsupported platform [${process.platform}]`); + +if (common.isIBMi) + common.skip('Unsupported platform IBMi'); + +const assert = require('assert'); + +// Verifies that the --title=foo command line flag set the process +// title on startup. +assert.strictEqual(process.title, 'foo'); diff --git a/test/js/node/test/parallel/test-process-uid-gid.js b/test/js/node/test/parallel/test-process-uid-gid.js new file mode 100644 index 00000000000000..0e8e0e89a0b89a --- /dev/null +++ b/test/js/node/test/parallel/test-process-uid-gid.js @@ -0,0 +1,100 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +const assert = require('assert'); + +if (common.isWindows) { + // uid/gid functions are POSIX only. + assert.strictEqual(process.getuid, undefined); + assert.strictEqual(process.getgid, undefined); + assert.strictEqual(process.setuid, undefined); + assert.strictEqual(process.setgid, undefined); + return; +} + +if (!common.isMainThread) + return; + +assert.throws(() => { + process.setuid({}); +}, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "id" argument must be of type ' + + 'number or string. Received an instance of Object' +}); + +assert.throws(() => { + process.setuid('fhqwhgadshgnsdhjsdbkhsdabkfabkveyb'); +}, { + code: 'ERR_UNKNOWN_CREDENTIAL', + message: 'User identifier does not exist: fhqwhgadshgnsdhjsdbkhsdabkfabkveyb' +}); + +// Passing -0 shouldn't crash the process +// Refs: https://github.com/nodejs/node/issues/32750 +// And neither should values exceeding 2 ** 31 - 1. +for (const id of [-0, 2 ** 31, 2 ** 32 - 1]) { + for (const fn of [process.setuid, process.setuid, process.setgid, process.setegid]) { + try { fn(id); } catch { + // Continue regardless of error. + } + } +} + +// If we're not running as super user... +if (process.getuid() !== 0) { + // Should not throw. + process.getgid(); + process.getuid(); + + assert.throws( + () => { process.setgid('nobody'); }, + /(?:EPERM: .+|Group identifier does not exist: nobody)$/ + ); + + assert.throws( + () => { process.setuid('nobody'); }, + /(?:EPERM: .+|User identifier does not exist: nobody)$/ + ); + return; +} + +// If we are running as super user... +const oldgid = process.getgid(); +try { + process.setgid('nobody'); +} catch (err) { + if (err.code !== 'ERR_UNKNOWN_CREDENTIAL') { + throw err; + } + process.setgid('nogroup'); +} + +const newgid = process.getgid(); +assert.notStrictEqual(newgid, oldgid); + +const olduid = process.getuid(); +process.setuid('nobody'); +const newuid = process.getuid(); +assert.notStrictEqual(newuid, olduid); diff --git a/test/js/node/test/parallel/test-process-umask-mask.js b/test/js/node/test/parallel/test-process-umask-mask.js new file mode 100644 index 00000000000000..d599379761fd40 --- /dev/null +++ b/test/js/node/test/parallel/test-process-umask-mask.js @@ -0,0 +1,32 @@ +'use strict'; + +// This tests that the lower bits of mode > 0o777 still works in +// process.umask() + +const common = require('../common'); +const assert = require('assert'); + +if (!common.isMainThread) + common.skip('Setting process.umask is not supported in Workers'); + +let mask; + +if (common.isWindows) { + mask = 0o600; +} else { + mask = 0o664; +} + +const maskToIgnore = 0o10000; + +const old = process.umask(); + +function test(input, output) { + process.umask(input); + assert.strictEqual(process.umask(), output); + + process.umask(old); +} + +test(mask | maskToIgnore, mask); +test((mask | maskToIgnore).toString(8), mask); diff --git a/test/js/node/test/parallel/test-process-umask.js b/test/js/node/test/parallel/test-process-umask.js new file mode 100644 index 00000000000000..e90955f394df4e --- /dev/null +++ b/test/js/node/test/parallel/test-process-umask.js @@ -0,0 +1,65 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +if (!common.isMainThread) { + assert.strictEqual(typeof process.umask(), 'number'); + assert.throws(() => { + process.umask('0664'); + }, { code: 'ERR_WORKER_UNSUPPORTED_OPERATION' }); + + common.skip('Setting process.umask is not supported in Workers'); +} + +// Note in Windows one can only set the "user" bits. +let mask; +if (common.isWindows) { + mask = '0600'; +} else { + mask = '0664'; +} + +const old = process.umask(mask); + +assert.strictEqual(process.umask(old), parseInt(mask, 8)); + +// Confirm reading the umask does not modify it. +// 1. If the test fails, this call will succeed, but the mask will be set to 0 +assert.strictEqual(process.umask(), old); +// 2. If the test fails, process.umask() will return 0 +assert.strictEqual(process.umask(), old); + +assert.throws(() => { + process.umask({}); +}, { + code: 'ERR_INVALID_ARG_TYPE', +}); + +['123x', 'abc', '999'].forEach((value) => { + assert.throws(() => { + process.umask(value); + }, { + code: 'ERR_INVALID_ARG_VALUE', + }); +}); diff --git a/test/js/node/test/parallel/test-process-warning.js b/test/js/node/test/parallel/test-process-warning.js new file mode 100644 index 00000000000000..c1fbbf775fb45e --- /dev/null +++ b/test/js/node/test/parallel/test-process-warning.js @@ -0,0 +1,68 @@ +'use strict'; + +const common = require('../common'); +const { + hijackStderr, + restoreStderr +} = require('../common/hijackstdio'); +const assert = require('assert'); + +function test1() { + // Output is skipped if the argument to the 'warning' event is + // not an Error object. + hijackStderr(common.mustNotCall('stderr.write must not be called')); + process.emit('warning', 'test'); + setImmediate(test2); +} + +function test2() { + // Output is skipped if it's a deprecation warning and + // process.noDeprecation = true + process.noDeprecation = true; + process.emitWarning('test', 'DeprecationWarning'); + process.noDeprecation = false; + setImmediate(test3); +} + +function test3() { + restoreStderr(); + // Type defaults to warning when the second argument is an object + process.emitWarning('test', {}); + process.once('warning', common.mustCall((warning) => { + assert.strictEqual(warning.name, 'Warning'); + })); + setImmediate(test4); +} + +function test4() { + // process.emitWarning will throw when process.throwDeprecation is true + // and type is `DeprecationWarning`. + process.throwDeprecation = true; + process.once('uncaughtException', (err) => { + assert.match(err.toString(), /^DeprecationWarning: test$/); + }); + try { + process.emitWarning('test', 'DeprecationWarning'); + } catch { + assert.fail('Unreachable'); + } + process.throwDeprecation = false; + setImmediate(test5); +} + +function test5() { + // Setting toString to a non-function should not cause an error + const err = new Error('test'); + err.toString = 1; + process.emitWarning(err); + setImmediate(test6); +} + +function test6() { + process.emitWarning('test', { detail: 'foo' }); + process.on('warning', (warning) => { + assert.strictEqual(warning.detail, 'foo'); + }); +} + +test1(); diff --git a/test/js/node/test/parallel/test-queue-microtask-uncaught-asynchooks.js b/test/js/node/test/parallel/test-queue-microtask-uncaught-asynchooks.js deleted file mode 100644 index 35b3d9fa309af9..00000000000000 --- a/test/js/node/test/parallel/test-queue-microtask-uncaught-asynchooks.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; -const common = require('../common'); -const assert = require('assert'); -const async_hooks = require('async_hooks'); - -// Regression test for https://github.com/nodejs/node/issues/30080: -// An uncaught exception inside a queueMicrotask callback should not lead -// to multiple after() calls for it. - -let µtaskId; -const events = []; - -async_hooks.createHook({ - init(id, type, triggerId, resource) { - if (type === 'Microtask') { - µtaskId = id; - events.push('init'); - } - }, - before(id) { - if (id === µtaskId) events.push('before'); - }, - after(id) { - if (id === µtaskId) events.push('after'); - }, - destroy(id) { - if (id === µtaskId) events.push('destroy'); - } -}).enable(); - -queueMicrotask(() => { throw new Error(); }); - -process.on('uncaughtException', common.mustCall()); -process.on('exit', () => { - assert.deepStrictEqual(events, ['init', 'after', 'before', 'destroy']); -}); diff --git a/test/js/node/v8/capture-stack-trace.test.js b/test/js/node/v8/capture-stack-trace.test.js index 69dcf9307fa928..814aee3ab3bd51 100644 --- a/test/js/node/v8/capture-stack-trace.test.js +++ b/test/js/node/v8/capture-stack-trace.test.js @@ -1,6 +1,6 @@ import { nativeFrameForTesting } from "bun:internal-for-testing"; -import { afterEach, expect, test } from "bun:test"; import { noInline } from "bun:jsc"; +import { afterEach, expect, mock, test } from "bun:test"; const origPrepareStackTrace = Error.prepareStackTrace; afterEach(() => { Error.prepareStackTrace = origPrepareStackTrace; @@ -697,3 +697,30 @@ test("Error.prepareStackTrace propagates exceptions", () => { ]), ).toThrow("hi"); }); + +test("CallFrame.p.getScriptNameOrSourceURL inside eval", () => { + let prevPrepareStackTrace = Error.prepareStackTrace; + const prepare = mock((e, s) => { + expect(s[0].getScriptNameOrSourceURL()).toBe("https://zombo.com/welcome-to-zombo.js"); + expect(s[1].getScriptNameOrSourceURL()).toBe("https://zombo.com/welcome-to-zombo.js"); + expect(s[2].getScriptNameOrSourceURL()).toBe("[native code]"); + expect(s[3].getScriptNameOrSourceURL()).toBe(import.meta.path); + expect(s[4].getScriptNameOrSourceURL()).toBe(import.meta.path); + }); + Error.prepareStackTrace = prepare; + let evalScript = `(function() { + throw new Error("bad error!"); + })() //# sourceURL=https://zombo.com/welcome-to-zombo.js`; + + try { + function insideAFunction() { + eval(evalScript); + } + insideAFunction(); + } catch (e) { + e.stack; + } + Error.prepareStackTrace = prevPrepareStackTrace; + + expect(prepare).toHaveBeenCalledTimes(1); +}); diff --git a/test/js/node/vm/happy-dom-vm-16277.test.ts b/test/js/node/vm/happy-dom-vm-16277.test.ts new file mode 100644 index 00000000000000..022a11e21bd156 --- /dev/null +++ b/test/js/node/vm/happy-dom-vm-16277.test.ts @@ -0,0 +1,27 @@ +import { test, expect } from "bun:test"; +import { Window } from "happy-dom"; +test("reproduction", async (): Promise => { + expect.assertions(1); + for (let i: number = 0; i < 2; ++i) { + // TODO: have a reproduction of this that doesn't depend on a 10 MB file. + const response: Response = new Response(` + + + + + +`); + const window: Window = new Window({ url: "http://youtube.com" }); + const localStorage = window.localStorage; + global.window = window; + global.document = window.document; + localStorage.clear(); + document.body.innerHTML = await response.text(); + } + + // This test passes by simply not crashing. + expect().pass(); +}); diff --git a/test/js/node/vm/vm.test.ts b/test/js/node/vm/vm.test.ts index f0a66ec2e905ee..38a626895e6b77 100644 --- a/test/js/node/vm/vm.test.ts +++ b/test/js/node/vm/vm.test.ts @@ -453,3 +453,74 @@ resp.text().then((a) => { delete URL.prototype.ok; } }); + +test("can get sourceURL from eval inside node:vm", () => { + try { + runInNewContext( + ` +throw new Error("hello"); +//# sourceURL=hellohello.js +`, + {}, + ); + } catch (e: any) { + var err: Error = e; + } + + expect(err!.stack!.replaceAll("\r\n", "\n").replaceAll(import.meta.path, "")).toMatchInlineSnapshot(` +"Error: hello + at hellohello.js:2:16 + at runInNewContext (unknown) + at (:459:5)" +`); +}); + +test("can get sourceURL inside node:vm", () => { + const err = runInNewContext( + ` + +function hello() { + return Bun.inspect(new Error("hello")); +} + +hello(); + +//# sourceURL=hellohello.js +`, + { Bun }, + ); + + expect(err.replaceAll("\r\n", "\n").replaceAll(import.meta.path, "")).toMatchInlineSnapshot(` +"4 | return Bun.inspect(new Error("hello")); + ^ +error: hello + at hello (hellohello.js:4:24) + at hellohello.js:7:6 + at (:479:15) +" +`); +}); + +test("eval sourceURL is correct", () => { + const err = eval( + ` + +function hello() { + return Bun.inspect(new Error("hello")); +} + +hello(); + +//# sourceURL=hellohello.js +`, + ); + expect(err.replaceAll("\r\n", "\n").replaceAll(import.meta.path, "")).toMatchInlineSnapshot(` +"4 | return Bun.inspect(new Error("hello")); + ^ +error: hello + at hello (hellohello.js:4:24) + at eval (hellohello.js:7:6) + at (:505:15) +" +`); +}); diff --git a/test/js/sql/sql.test.ts b/test/js/sql/sql.test.ts index 92fd82931bd592..ce681484fe6004 100644 --- a/test/js/sql/sql.test.ts +++ b/test/js/sql/sql.test.ts @@ -4,7 +4,8 @@ import { $ } from "bun"; import { bunExe, isCI, withoutAggressiveGC } from "harness"; import path from "path"; -if (!isCI) { +const hasPsql = Bun.which("psql"); +if (!isCI && hasPsql) { require("./bootstrap.js"); // macOS location: /opt/homebrew/var/postgresql@14/pg_hba.conf @@ -157,6 +158,62 @@ if (!isCI) { expect(error.code).toBe(`ERR_POSTGRES_LIFETIME_TIMEOUT`); }); + // Last one wins. + test("Handles duplicate string column names", async () => { + const result = await sql`select 1 as x, 2 as x, 3 as x`; + expect(result).toEqual([{ x: 3 }]); + }); + + test("Handles numeric column names", async () => { + // deliberately out of order + const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 0 as "0"`; + expect(result).toEqual([{ "1": 1, "2": 2, "3": 3, "0": 0 }]); + + expect(Object.keys(result[0])).toEqual(["0", "1", "2", "3"]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + + // Last one wins. + test("Handles duplicate numeric column names", async () => { + const result = await sql`select 1 as "1", 2 as "1", 3 as "1"`; + expect(result).toEqual([{ "1": 3 }]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + + test("Handles mixed column names", async () => { + const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 4 as x`; + expect(result).toEqual([{ "1": 1, "2": 2, "3": 3, x: 4 }]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + + test("Handles mixed column names with duplicates", async () => { + const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 4 as "1", 1 as x, 2 as x`; + expect(result).toEqual([{ "1": 4, "2": 2, "3": 3, x: 2 }]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + + // Named columns are inserted first, but they appear from JS as last. + expect(Object.keys(result[0])).toEqual(["1", "2", "3", "x"]); + }); + + test("Handles mixed column names with duplicates at the end", async () => { + const result = await sql`select 1 as "1", 2 as "2", 3 as "3", 4 as "1", 1 as x, 2 as x, 3 as x, 4 as "y"`; + expect(result).toEqual([{ "1": 4, "2": 2, "3": 3, x: 3, y: 4 }]); + + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + + test("Handles mixed column names with duplicates at the start", async () => { + const result = await sql`select 1 as "1", 2 as "1", 3 as "2", 4 as "3", 1 as x, 2 as x, 3 as x`; + expect(result).toEqual([{ "1": 2, "2": 3, "3": 4, x: 3 }]); + // Sanity check: ensure iterating through the properties doesn't crash. + Bun.inspect(result); + }); + test("Uses default database without slash", async () => { const sql = postgres("postgres://localhost"); expect(sql.options.username).toBe(sql.options.database); diff --git a/test/js/sql/tls-sql.test.ts b/test/js/sql/tls-sql.test.ts index 2bc99bd3ad2d40..78bd4d0daaec0f 100644 --- a/test/js/sql/tls-sql.test.ts +++ b/test/js/sql/tls-sql.test.ts @@ -4,20 +4,22 @@ import { sql as SQL } from "bun"; const TLS_POSTGRES_DATABASE_URL = getSecret("TLS_POSTGRES_DATABASE_URL"); -test("tls (explicit)", async () => { - const sql = new SQL({ - url: TLS_POSTGRES_DATABASE_URL!, - tls: true, - adapter: "postgresql", - }); +if (TLS_POSTGRES_DATABASE_URL) { + test("tls (explicit)", async () => { + const sql = new SQL({ + url: TLS_POSTGRES_DATABASE_URL!, + tls: true, + adapter: "postgresql", + }); - const [{ one, two }] = await sql`SELECT 1 as one, '2' as two`; - expect(one).toBe(1); - expect(two).toBe("2"); -}); + const [{ one, two }] = await sql`SELECT 1 as one, '2' as two`; + expect(one).toBe(1); + expect(two).toBe("2"); + }); -test("tls (implicit)", async () => { - const [{ one, two }] = await SQL`SELECT 1 as one, '2' as two`; - expect(one).toBe(1); - expect(two).toBe("2"); -}); + test("tls (implicit)", async () => { + const [{ one, two }] = await SQL`SELECT 1 as one, '2' as two`; + expect(one).toBe(1); + expect(two).toBe("2"); + }); +} diff --git a/test/js/third_party/@electric-sql/pglite/pglite.test.ts b/test/js/third_party/@electric-sql/pglite/pglite.test.ts new file mode 100644 index 00000000000000..45423d5a7443b2 --- /dev/null +++ b/test/js/third_party/@electric-sql/pglite/pglite.test.ts @@ -0,0 +1,19 @@ +import { PGlite } from "@electric-sql/pglite"; + +describe("pglite", () => { + it("can initialize successfully", async () => { + const db = new PGlite(); + expect(await db.query("SELECT version()")).toEqual({ + rows: [ + { + version: + // since pglite is wasm, there is only one binary for all platforms. it always thinks it + // is x86_64-pc-linux-gnu. + "PostgreSQL 16.4 on x86_64-pc-linux-gnu, compiled by emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.72 (437140d149d9c977ffc8b09dbaf9b0f5a02db190), 32-bit", + }, + ], + fields: [{ name: "version", dataTypeID: 25 }], + affectedRows: 0, + }); + }); +}); diff --git a/test/js/web/fetch/fetch-gzip.test.ts b/test/js/web/fetch/fetch-gzip.test.ts index 83028ae12bac47..8f162b004a8f00 100644 --- a/test/js/web/fetch/fetch-gzip.test.ts +++ b/test/js/web/fetch/fetch-gzip.test.ts @@ -210,8 +210,7 @@ it("fetch() with a gzip response works (multiple chunks, TCP server)", async don await write("\r\n"); socket.flush(); - }, - drain(socket) {}, + } }, }); await 1; diff --git a/test/js/web/fetch/fetch.stream.test.ts b/test/js/web/fetch/fetch.stream.test.ts index 21a72ede533627..b3414f453bf96d 100644 --- a/test/js/web/fetch/fetch.stream.test.ts +++ b/test/js/web/fetch/fetch.stream.test.ts @@ -1209,12 +1209,15 @@ describe("fetch() with streaming", () => { expect(buffer.toString("utf8")).toBe("unreachable"); } catch (err) { if (compression === "br") { - expect((err as Error).name).toBe("BrotliDecompressionError"); + expect((err as Error).name).toBe("Error"); + expect((err as Error).code).toBe("BrotliDecompressionError"); } else if (compression === "deflate-libdeflate") { // Since the compressed data is different, the error ends up different. - expect((err as Error).name).toBe("ShortRead"); + expect((err as Error).name).toBe("Error"); + expect((err as Error).code).toBe("ShortRead"); } else { - expect((err as Error).name).toBe("ZlibError"); + expect((err as Error).name).toBe("Error"); + expect((err as Error).code).toBe("ZlibError"); } } } @@ -1306,7 +1309,8 @@ describe("fetch() with streaming", () => { gcTick(false); expect(buffer.toString("utf8")).toBe("unreachable"); } catch (err) { - expect((err as Error).name).toBe("ConnectionClosed"); + expect((err as Error).name).toBe("Error"); + expect((err as Error).code).toBe("ConnectionClosed"); } } }); diff --git a/test/js/web/streams/streams.test.js b/test/js/web/streams/streams.test.js index 1caa6eb7aecc02..b8636ccf9f8ac8 100644 --- a/test/js/web/streams/streams.test.js +++ b/test/js/web/streams/streams.test.js @@ -7,7 +7,7 @@ import { readableStreamToText, } from "bun"; import { describe, expect, it, test } from "bun:test"; -import { tmpdirSync, isWindows, isMacOS } from "harness"; +import { tmpdirSync, isWindows, isMacOS, bunEnv } from "harness"; import { mkfifo } from "mkfifo"; import { createReadStream, realpathSync, unlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -445,6 +445,7 @@ it.todoIf(isWindows || isMacOS)("Bun.file() read text from pipe", async () => { stdout: "pipe", stdin: null, env: { + ...bunEnv, FIFO_TEST: large, }, }); diff --git a/test/js/web/timers/performance-entries.test.ts b/test/js/web/timers/performance-entries.test.ts new file mode 100644 index 00000000000000..bfc297cc1a84ce --- /dev/null +++ b/test/js/web/timers/performance-entries.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from "bun:test"; +import { estimateShallowMemoryUsageOf } from "bun:jsc"; + +test("memory usage of Performance", () => { + const initial = estimateShallowMemoryUsageOf(performance); + for (let i = 0; i < 1024; i++) { + performance.mark(`mark-${i}`); + } + const final = estimateShallowMemoryUsageOf(performance); + + for (let i = 1; i < 1024; i++) { + performance.measure(`measure-${i}`, `mark-${i}`, `mark-${i - 1}`); + } + const final2 = estimateShallowMemoryUsageOf(performance); + expect(final2).toBeGreaterThan(final); + expect(final).toBeGreaterThan(initial); +}); diff --git a/test/napi/napi-app/main.cpp b/test/napi/napi-app/main.cpp index 64b4450c3d5bfb..5e99b118d07425 100644 --- a/test/napi/napi-app/main.cpp +++ b/test/napi/napi-app/main.cpp @@ -1076,6 +1076,61 @@ static napi_value create_weird_bigints(const Napi::CallbackInfo &info) { return array; } +// Call Node-API functions in ways that result in different error handling +// (erroneous call, valid call, or valid call while an exception is pending) and +// log information from napi_get_last_error_info +static napi_value test_extended_error_messages(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + const napi_extended_error_info *error; + + // this function is implemented in C++ + // error because the result pointer is null + printf("erroneous napi_create_double returned code %d\n", + napi_create_double(env, 1.0, nullptr)); + NODE_API_CALL(env, napi_get_last_error_info(env, &error)); + printf("erroneous napi_create_double info: code = %d, message = %s\n", + error->error_code, error->error_message); + + // this function should succeed and the success should overwrite the error + // from the last call + napi_value js_number; + printf("successful napi_create_double returned code %d\n", + napi_create_double(env, 5.0, &js_number)); + NODE_API_CALL(env, napi_get_last_error_info(env, &error)); + printf("successful napi_create_double info: code = %d, message = %s\n", + error->error_code, + error->error_message ? error->error_message : "(null)"); + + // this function is implemented in zig + // error because the value is not an array + unsigned int len; + printf("erroneous napi_get_array_length returned code %d\n", + napi_get_array_length(env, js_number, &len)); + NODE_API_CALL(env, napi_get_last_error_info(env, &error)); + printf("erroneous napi_get_array_length info: code = %d, message = %s\n", + error->error_code, error->error_message); + + // throw an exception + NODE_API_CALL(env, napi_throw_type_error(env, nullptr, "oops!")); + // nothing is wrong with this call by itself, but it should return + // napi_pending_exception without doing anything because an exception is + // pending + napi_value coerced_string; + printf("napi_coerce_to_string with pending exception returned code %d\n", + napi_coerce_to_string(env, js_number, &coerced_string)); + NODE_API_CALL(env, napi_get_last_error_info(env, &error)); + printf( + "napi_coerce_to_string with pending exception info: code = %d, message = " + "%s\n", + error->error_code, error->error_message); + + // clear the exception + napi_value exception; + NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &exception)); + + return ok(env); +} + Napi::Value RunCallback(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); // this function is invoked without the GC callback @@ -1147,6 +1202,8 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports1) { exports.Set("bigint_to_64_null", Napi::Function::New(env, bigint_to_64_null)); exports.Set("create_weird_bigints", Napi::Function::New(env, create_weird_bigints)); + exports.Set("test_extended_error_messages", + Napi::Function::New(env, test_extended_error_messages)); napitests::register_wrap_tests(env, exports); diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index b00513cb861a86..1030c4eb86c561 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -378,6 +378,12 @@ describe("napi", () => { checkSameOutput("test_create_bigint_words", []); }); }); + + describe("napi_get_last_error_info", () => { + it("returns information from the most recent call", () => { + checkSameOutput("test_extended_error_messages", []); + }); + }); }); function checkSameOutput(test: string, args: any[] | string) { diff --git a/test/package.json b/test/package.json index efe5c43799cdff..9c9d51d496c4a6 100644 --- a/test/package.json +++ b/test/package.json @@ -11,6 +11,7 @@ "dependencies": { "@azure/service-bus": "7.9.4", "@duckdb/node-api": "1.1.3-alpha.7", + "@electric-sql/pglite": "0.2.15", "@grpc/grpc-js": "1.12.0", "@grpc/proto-loader": "0.7.10", "@napi-rs/canvas": "0.1.65", @@ -29,6 +30,7 @@ "express": "4.18.2", "fast-glob": "3.3.1", "filenamify": "6.0.0", + "happy-dom": "16.5.3", "http2-wrapper": "2.2.1", "https-proxy-agent": "7.0.5", "iconv-lite": "0.6.3", @@ -67,6 +69,7 @@ "svelte": "5.4.0", "typescript": "5.0.2", "undici": "5.20.0", + "v8-heapsnapshot": "1.3.1", "verdaccio": "6.0.0", "vitest": "0.32.2", "webpack": "5.88.0", diff --git a/test/regression/issue/08093.test.ts b/test/regression/issue/08093.test.ts index 280d0ec4df8c0d..4d32dab6b777b9 100644 --- a/test/regression/issue/08093.test.ts +++ b/test/regression/issue/08093.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { access, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env } from "harness"; +import { bunExe, bunEnv as env, readdirSorted } from "harness"; import { join } from "path"; import { dummyAfterAll, @@ -10,7 +10,6 @@ import { dummyBeforeEach, dummyRegistry, package_dir, - readdirSorted, requested, root_url, setHandler, diff --git a/test/snippets/react-context-value-func.tsx b/test/snippets/react-context-value-func.tsx index 800ad428d7c134..c693717466c499 100644 --- a/test/snippets/react-context-value-func.tsx +++ b/test/snippets/react-context-value-func.tsx @@ -10,7 +10,7 @@ const ContextProvider = ({ children }) => { return {children(foo)}; }; -const ContextValue = ({}) => ( +const ContextValue = () => ( {foo => { if (foo) { diff --git a/test/v8/v8.test.ts b/test/v8/v8.test.ts index 1f4ff131ea9637..d2a3a468ba4c1d 100644 --- a/test/v8/v8.test.ts +++ b/test/v8/v8.test.ts @@ -1,6 +1,6 @@ import { spawn, spawnSync } from "bun"; import { beforeAll, describe, expect, it } from "bun:test"; -import { bunEnv, bunExe, tmpdirSync, isWindows, isMusl, isBroken } from "harness"; +import { bunEnv, bunExe, tmpdirSync, isWindows, isMusl, isBroken, nodeExe } from "harness"; import assert from "node:assert"; import fs from "node:fs/promises"; import { join, basename } from "path"; @@ -38,7 +38,7 @@ const directories = { }; async function install(srcDir: string, tmpDir: string, runtime: Runtime): Promise { - await fs.cp(srcDir, tmpDir, { recursive: true }); + await fs.cp(srcDir, tmpDir, { recursive: true, force: true }); const install = spawn({ cmd: [bunExe(), "install", "--ignore-scripts"], cwd: tmpDir, @@ -47,9 +47,9 @@ async function install(srcDir: string, tmpDir: string, runtime: Runtime): Promis stdout: "inherit", stderr: "inherit", }); - await install.exited; - if (install.exitCode != 0) { - throw new Error("build failed"); + const exitCode = await install.exited; + if (exitCode !== 0) { + throw new Error(`install failed: ${exitCode}`); } } @@ -63,20 +63,24 @@ async function build( cmd: runtime == Runtime.bun ? [bunExe(), "x", "--bun", "node-gyp", "rebuild", buildMode == BuildMode.debug ? "--debug" : "--release"] - : ["npx", "node-gyp", "rebuild", "--release"], // for node.js we don't bother with debug mode + : [bunExe(), "x", "node-gyp", "rebuild", "--release"], // for node.js we don't bother with debug mode cwd: tmpDir, env: bunEnv, stdin: "inherit", stdout: "pipe", stderr: "pipe", }); - await build.exited; - const out = await new Response(build.stdout).text(); - const err = await new Response(build.stderr).text(); - if (build.exitCode != 0) { + const [exitCode, out, err] = await Promise.all([ + build.exited, + new Response(build.stdout).text(), + new Response(build.stderr).text(), + ]); + if (exitCode !== 0) { console.error(err); - throw new Error("build failed"); + console.log(out); + throw new Error(`build failed: ${exitCode}`); } + return { out, err, @@ -112,89 +116,89 @@ describe.todoIf(isBroken && isMusl)("node:v8", () => { }); describe("module lifecycle", () => { - it("can call a basic native function", () => { - checkSameOutput("test_v8_native_call", []); + it("can call a basic native function", async () => { + await checkSameOutput("test_v8_native_call", []); }); }); describe("primitives", () => { - it("can create and distinguish between null, undefined, true, and false", () => { - checkSameOutput("test_v8_primitives", []); + it("can create and distinguish between null, undefined, true, and false", async () => { + await checkSameOutput("test_v8_primitives", []); }); }); describe("Number", () => { - it("can create small integer", () => { - checkSameOutput("test_v8_number_int", []); + it("can create small integer", async () => { + await checkSameOutput("test_v8_number_int", []); }); // non-i32 v8::Number is not implemented yet - it("can create large integer", () => { - checkSameOutput("test_v8_number_large_int", []); + it("can create large integer", async () => { + await checkSameOutput("test_v8_number_large_int", []); }); - it("can create fraction", () => { - checkSameOutput("test_v8_number_fraction", []); + it("can create fraction", async () => { + await checkSameOutput("test_v8_number_fraction", []); }); }); describe("String", () => { - it("can create and read back strings with only ASCII characters", () => { - checkSameOutput("test_v8_string_ascii", []); + it("can create and read back strings with only ASCII characters", async () => { + await checkSameOutput("test_v8_string_ascii", []); }); // non-ASCII strings are not implemented yet - it("can create and read back strings with UTF-8 characters", () => { - checkSameOutput("test_v8_string_utf8", []); + it("can create and read back strings with UTF-8 characters", async () => { + await checkSameOutput("test_v8_string_utf8", []); }); - it("handles replacement correctly in strings with invalid UTF-8 sequences", () => { - checkSameOutput("test_v8_string_invalid_utf8", []); + it("handles replacement correctly in strings with invalid UTF-8 sequences", async () => { + await checkSameOutput("test_v8_string_invalid_utf8", []); }); - it("can create strings from null-terminated Latin-1 data", () => { - checkSameOutput("test_v8_string_latin1", []); + it("can create strings from null-terminated Latin-1 data", async () => { + await checkSameOutput("test_v8_string_latin1", []); }); describe("WriteUtf8", () => { - it("truncates the string correctly", () => { - checkSameOutput("test_v8_string_write_utf8", []); + it("truncates the string correctly", async () => { + await checkSameOutput("test_v8_string_write_utf8", []); }); }); }); describe("External", () => { - it("can create an external and read back the correct value", () => { - checkSameOutput("test_v8_external", []); + it("can create an external and read back the correct value", async () => { + await checkSameOutput("test_v8_external", []); }); }); describe("Object", () => { - it("can create an object and set properties", () => { - checkSameOutput("test_v8_object", []); + it("can create an object and set properties", async () => { + await checkSameOutput("test_v8_object", []); }); }); describe("Array", () => { // v8::Array::New is broken as it still tries to reinterpret locals as JSValues - it.skip("can create an array from a C array of Locals", () => { - checkSameOutput("test_v8_array_new", []); + it.skip("can create an array from a C array of Locals", async () => { + await checkSameOutput("test_v8_array_new", []); }); }); describe("ObjectTemplate", () => { - it("creates objects with internal fields", () => { - checkSameOutput("test_v8_object_template", []); + it("creates objects with internal fields", async () => { + await checkSameOutput("test_v8_object_template", []); }); }); describe("FunctionTemplate", () => { - it("keeps the data parameter alive", () => { - checkSameOutput("test_v8_function_template", []); + it("keeps the data parameter alive", async () => { + await checkSameOutput("test_v8_function_template", []); }); }); describe("Function", () => { - it("correctly receives all its arguments from JS", () => { - checkSameOutput("print_values_from_js", [5.0, true, null, false, "meow", {}]); - checkSameOutput("print_native_function", []); + it("correctly receives all its arguments from JS", async () => { + await checkSameOutput("print_values_from_js", [5.0, true, null, false, "async meow", {}]); + await checkSameOutput("print_native_function", []); }); - it("correctly receives the this value from JS", () => { - checkSameOutput("call_function_with_weird_this_values", []); + it("correctly receives the this value from JS", async () => { + await checkSameOutput("call_function_with_weird_this_values", []); }); }); @@ -213,44 +217,56 @@ describe.todoIf(isBroken && isMusl)("node:v8", () => { }); describe("Global", () => { - it("can create, modify, and read the value from global handles", () => { - checkSameOutput("test_v8_global", []); + it("can create, modify, and read the value from global handles", async () => { + await checkSameOutput("test_v8_global", []); }); }); describe("HandleScope", () => { - it("can hold a lot of locals", () => { - checkSameOutput("test_many_v8_locals", []); + it("can hold a lot of locals", async () => { + await checkSameOutput("test_many_v8_locals", []); }); - it("keeps GC objects alive", () => { - checkSameOutput("test_handle_scope_gc", []); + it("keeps GC objects alive", async () => { + await checkSameOutput("test_handle_scope_gc", []); }, 10000); }); describe("EscapableHandleScope", () => { - it("keeps handles alive in the outer scope", () => { - checkSameOutput("test_v8_escapable_handle_scope", []); + it("keeps handles alive in the outer scope", async () => { + await checkSameOutput("test_v8_escapable_handle_scope", []); }); }); describe("uv_os_getpid", () => { - it.skipIf(isWindows)("returns the same result as getpid on POSIX", () => { - checkSameOutput("test_uv_os_getpid", []); + it.skipIf(isWindows)("returns the same result as getpid on POSIX", async () => { + await checkSameOutput("test_uv_os_getpid", []); }); }); describe("uv_os_getppid", () => { - it.skipIf(isWindows)("returns the same result as getppid on POSIX", () => { - checkSameOutput("test_uv_os_getppid", []); + it.skipIf(isWindows)("returns the same result as getppid on POSIX", async () => { + await checkSameOutput("test_uv_os_getppid", []); }); }); }); -function checkSameOutput(testName: string, args: any[], thisValue?: any) { - const nodeResult = runOn(Runtime.node, BuildMode.release, testName, args, thisValue).trim(); - let bunReleaseResult = runOn(Runtime.bun, BuildMode.release, testName, args, thisValue); - let bunDebugResult = runOn(Runtime.bun, BuildMode.debug, testName, args, thisValue); - +async function checkSameOutput(testName: string, args: any[], thisValue?: any) { + const [nodeResultResolution, bunReleaseResultResolution, bunDebugResultResolution] = await Promise.allSettled([ + runOn(Runtime.node, BuildMode.release, testName, args, thisValue), + runOn(Runtime.bun, BuildMode.release, testName, args, thisValue), + runOn(Runtime.bun, BuildMode.debug, testName, args, thisValue), + ]); + const errors = [nodeResultResolution, bunReleaseResultResolution, bunDebugResultResolution] + .filter(r => r.status === "rejected") + .map(r => r.reason); + if (errors.length > 0) { + throw new AggregateError(errors); + } + let [nodeResult, bunReleaseResult, bunDebugResult] = [ + nodeResultResolution, + bunReleaseResultResolution, + bunDebugResultResolution, + ].map(r => (r as any).value); // remove all debug logs bunReleaseResult = bunReleaseResult.replaceAll(/^\[\w+\].+$/gm, "").trim(); bunDebugResult = bunDebugResult.replaceAll(/^\[\w+\].+$/gm, "").trim(); @@ -262,7 +278,7 @@ function checkSameOutput(testName: string, args: any[], thisValue?: any) { return nodeResult; } -function runOn(runtime: Runtime, buildMode: BuildMode, testName: string, jsArgs: any[], thisValue?: any) { +async function runOn(runtime: Runtime, buildMode: BuildMode, testName: string, jsArgs: any[], thisValue?: any) { if (runtime == Runtime.node) { assert(buildMode == BuildMode.release); } @@ -272,7 +288,7 @@ function runOn(runtime: Runtime, buildMode: BuildMode, testName: string, jsArgs: : buildMode == BuildMode.debug ? directories.bunDebug : directories.bunRelease; - const exe = runtime == Runtime.node ? "node" : bunExe(); + const exe = runtime == Runtime.node ? (nodeExe() ?? "node") : bunExe(); const cmd = [ exe, @@ -286,16 +302,21 @@ function runOn(runtime: Runtime, buildMode: BuildMode, testName: string, jsArgs: cmd.push("debug"); } - const exec = spawnSync({ + const proc = spawn({ cmd, cwd: baseDir, env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], }); - const errs = exec.stderr.toString(); + const [exitCode, out, err] = await Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); const crashMsg = `test ${testName} crashed under ${Runtime[runtime]} in ${BuildMode[buildMode]} mode`; - if (errs !== "") { - throw new Error(`${crashMsg}: ${errs}`); + if (exitCode !== 0) { + throw new Error(`${crashMsg}: ${err}\n${out}`.trim()); } - expect(exec.success, crashMsg).toBeTrue(); - return exec.stdout.toString(); + expect(exitCode, crashMsg).toBe(0); + return out.trim(); }