Merge branch 'main' into dylan/nohoist

This commit is contained in:
Dylan Conway
2025-11-12 20:22:30 -08:00
committed by GitHub
38 changed files with 958 additions and 540 deletions

View File

@@ -133,6 +133,20 @@ RUN ARCH=$(if [ "$TARGETARCH" = "arm64" ]; then echo "arm64"; else echo "amd64";
RUN mkdir -p /var/cache/buildkite-agent /var/log/buildkite-agent /var/run/buildkite-agent /etc/buildkite-agent /var/lib/buildkite-agent/cache/bun
# The following is necessary to configure buildkite to use a stable
# checkout directory. sccache hashes absolute paths into its cache keys,
# so if buildkite uses a different checkout path each time (which it does
# by default), sccache will be useless.
RUN mkdir -p -m 755 /var/lib/buildkite-agent/hooks && \
cat <<'EOF' > /var/lib/buildkite-agent/hooks/environment
#!/bin/sh
set -efu
export BUILDKITE_BUILD_CHECKOUT_PATH=/var/lib/buildkite-agent/build
EOF
RUN chmod 744 /var/lib/buildkite-agent/hooks/environment
COPY ../*/agent.mjs /var/bun/scripts/
ENV BUN_INSTALL_CACHE=/var/lib/buildkite-agent/cache/bun

View File

@@ -9,3 +9,6 @@ test/snippets
test/js/node/test
test/napi/node-napi-tests
bun.lock
# the output codeblocks need to stay minified
docs/bundler/minifier.mdx

View File

@@ -230,7 +230,7 @@ bun upgrade --canary
- Ecosystem
- [Use React and JSX](https://bun.com/guides/ecosystem/react)
- [Use EdgeDB with Bun](https://bun.com/guides/ecosystem/edgedb)
- [Use Gel with Bun](https://bun.com/guides/ecosystem/gel)
- [Use Prisma with Bun](https://bun.com/guides/ecosystem/prisma)
- [Add Sentry to a Bun app](https://bun.com/guides/ecosystem/sentry)
- [Create a Discord bot](https://bun.com/guides/ecosystem/discordjs)

View File

@@ -10,12 +10,8 @@ set(SCCACHE_SHARED_CACHE_BUCKET "bun-build-sccache-store")
function(check_aws_credentials OUT_VAR)
# Install dependencies first
execute_process(
COMMAND
${BUN_EXECUTABLE}
install
--frozen-lockfile
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}/scripts/build-cache
COMMAND ${BUN_EXECUTABLE} install --frozen-lockfile
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/scripts/build-cache
RESULT_VARIABLE INSTALL_EXIT_CODE
OUTPUT_VARIABLE INSTALL_OUTPUT
ERROR_VARIABLE INSTALL_ERROR
@@ -106,26 +102,22 @@ function(sccache_configure_environment_developer)
endif()
endfunction()
function(sccache_configure)
find_command(VARIABLE SCCACHE_PROGRAM COMMAND sccache REQUIRED ${CI})
if(NOT SCCACHE_PROGRAM)
message(WARNING "sccache not found. Your builds will be slower.")
return()
endif()
find_command(VARIABLE SCCACHE_PROGRAM COMMAND sccache REQUIRED ${CI})
if(NOT SCCACHE_PROGRAM)
message(WARNING "sccache not found. Your builds will be slower.")
return()
endif()
set(SCCACHE_ARGS CMAKE_C_COMPILER_LAUNCHER CMAKE_CXX_COMPILER_LAUNCHER)
foreach(arg ${SCCACHE_ARGS})
setx(${arg} ${SCCACHE_PROGRAM})
list(APPEND CMAKE_ARGS -D${arg}=${${arg}})
endforeach()
set(SCCACHE_ARGS CMAKE_C_COMPILER_LAUNCHER CMAKE_CXX_COMPILER_LAUNCHER)
foreach(arg ${SCCACHE_ARGS})
setx(${arg} ${SCCACHE_PROGRAM})
list(APPEND CMAKE_ARGS -D${arg}=${${arg}})
endforeach()
setenv(SCCACHE_LOG "info")
setenv(SCCACHE_LOG "info")
if (CI)
sccache_configure_environment_ci()
else()
sccache_configure_environment_developer()
endif()
endfunction()
sccache_configure()
if (CI)
sccache_configure_environment_ci()
else()
sccache_configure_environment_developer()
endif()

View File

@@ -34,7 +34,7 @@ By default, Bun's CSS bundler targets the following browsers:
The CSS Nesting specification allows you to write more concise and intuitive stylesheets by nesting selectors inside one another. Instead of repeating parent selectors across your CSS file, you can write child styles directly within their parent blocks.
```css title="styles.css" icon="file-code"
```scss title="styles.css" icon="file-code"
/* With nesting */
.card {
background: white;
@@ -100,7 +100,7 @@ This compiles to:
The `color-mix()` function gives you an easy way to blend two colors together according to a specified ratio in a chosen color space. This powerful feature lets you create color variations without manually calculating the resulting values.
```css title="styles.css" icon="file-code"
```scss title="styles.css" icon="file-code"
.button {
/* Mix blue and red in the RGB color space with a 30/70 proportion */
background-color: color-mix(in srgb, blue 30%, red);

View File

@@ -231,23 +231,67 @@ const myPlugin: BunPlugin = {
### onResolve
<Tabs>
<Tab title="options">- 🟢 `filter` - 🟢 `namespace`</Tab>
<Tab title="options">
- 🟢 `filter`
- 🟢 `namespace`
</Tab>
<Tab title="arguments">
- 🟢 `path` - 🟢 `importer` - 🔴 `namespace` - 🔴 `resolveDir` - 🔴 `kind` - 🔴 `pluginData`
- 🟢 `path`
- 🟢 `importer`
- 🔴 `namespace`
- 🔴 `resolveDir`
- 🔴 `kind`
- 🔴 `pluginData`
</Tab>
<Tab title="results">
- 🟢 `namespace` - 🟢 `path` - 🔴 `errors` - 🔴 `external` - 🔴 `pluginData` - 🔴 `pluginName` - 🔴 `sideEffects` -
🔴 `suffix` - 🔴 `warnings` - 🔴 `watchDirs` - 🔴 `watchFiles`
- 🟢 `namespace`
- 🟢 `path`
- 🔴 `errors`
- 🔴 `external`
- 🔴 `pluginData`
- 🔴 `pluginName`
- 🔴 `sideEffects`
- 🔴 `suffix`
- 🔴 `warnings`
- 🔴 `watchDirs`
- 🔴 `watchFiles`
</Tab>
</Tabs>
### onLoad
<Tabs>
<Tab title="options">- 🟢 `filter` - 🟢 `namespace`</Tab>
<Tab title="arguments">- 🟢 `path` - 🔴 `namespace` - 🔴 `suffix` - 🔴 `pluginData`</Tab>
<Tab title="options">
- 🟢 `filter`
- 🟢 `namespace`
</Tab>
<Tab title="arguments">
- 🟢 `path`
- 🔴 `namespace`
- 🔴 `suffix`
- 🔴 `pluginData`
</Tab>
<Tab title="results">
- 🟢 `contents` - 🟢 `loader` - 🔴 `errors` - 🔴 `pluginData` - 🔴 `pluginName` - 🔴 `resolveDir` - 🔴 `warnings` -
🔴 `watchDirs` - 🔴 `watchFiles`
- 🟢 `contents`
- 🟢 `loader`
- 🔴 `errors`
- 🔴 `pluginData`
- 🔴 `pluginName`
- 🔴 `resolveDir`
- 🔴 `warnings`
- 🔴 `watchDirs`
- 🔴 `watchFiles`
</Tab>
</Tabs>

View File

@@ -160,8 +160,12 @@ Like the Bun runtime, the bundler supports an array of file types out of the box
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `.js` `.jsx` `.cjs` `.mjs` `.mts` `.cts` `.ts` `.tsx` | Uses Bun's built-in transpiler to parse the file and transpile TypeScript/JSX syntax to vanilla JavaScript. The bundler executes a set of default transforms including dead code elimination and tree shaking. At the moment Bun does not attempt to down-convert syntax; if you use recently ECMAScript syntax, that will be reflected in the bundled code. |
| `.json` | JSON files are parsed and inlined into the bundle as a JavaScript object.<br/><br/>`js<br/>import pkg from "./package.json";<br/>pkg.name; // => "my-package"<br/>` |
| `.jsonc` | JSON with comments. Files are parsed and inlined into the bundle as a JavaScript object.<br/><br/>`js<br/>import config from "./config.jsonc";<br/>config.name; // => "my-config"<br/>` |
| `.toml` | TOML files are parsed and inlined into the bundle as a JavaScript object.<br/><br/>`js<br/>import config from "./bunfig.toml";<br/>config.logLevel; // => "debug"<br/>` |
| `.yaml` `.yml` | YAML files are parsed and inlined into the bundle as a JavaScript object.<br/><br/>`js<br/>import config from "./config.yaml";<br/>config.name; // => "my-app"<br/>` |
| `.txt` | The contents of the text file are read and inlined into the bundle as a string.<br/><br/>`js<br/>import contents from "./file.txt";<br/>console.log(contents); // => "Hello, world!"<br/>` |
| `.html` | HTML files are processed and any referenced assets (scripts, stylesheets, images) are bundled. |
| `.css` | CSS files are bundled together into a single `.css` file in the output directory. |
| `.node` `.wasm` | These files are supported by the Bun runtime, but during bundling they are treated as assets. |
### Assets
@@ -1462,7 +1466,21 @@ interface BuildArtifact extends Blob {
sourcemap: BuildArtifact | null;
}
type Loader = "js" | "jsx" | "ts" | "tsx" | "json" | "toml" | "file" | "napi" | "wasm" | "text";
type Loader =
| "js"
| "jsx"
| "ts"
| "tsx"
| "css"
| "json"
| "jsonc"
| "toml"
| "yaml"
| "text"
| "file"
| "napi"
| "wasm"
| "html";
interface BuildOutput {
outputs: BuildArtifact[];

View File

@@ -7,7 +7,7 @@ The Bun bundler implements a set of default loaders out of the box.
> As a rule of thumb: **the bundler and the runtime both support the same set of file types out of the box.**
`.js` `.cjs` `.mjs` `.mts` `.cts` `.ts` `.tsx` `.jsx` `.toml` `.json` `.txt` `.wasm` `.node` `.html`
`.js` `.cjs` `.mjs` `.mts` `.cts` `.ts` `.tsx` `.jsx` `.css` `.json` `.jsonc` `.toml` `.yaml` `.yml` `.txt` `.wasm` `.node` `.html` `.sh`
Bun uses the file extension to determine which built-in loader should be used to parse the file. Every loader has a name, such as `js`, `tsx`, or `json`. These names are used when building plugins that extend Bun with custom loaders.
@@ -85,7 +85,7 @@ If a `.json` file is passed as an entrypoint to the bundler, it will be converte
}
```
```ts Output
```js Output
export default {
name: "John Doe",
age: 35,
@@ -97,7 +97,33 @@ export default {
---
### toml
### `jsonc`
**JSON with Comments loader.** Default for `.jsonc`.
JSONC (JSON with Comments) files can be directly imported. Bun will parse them, stripping out comments and trailing commas.
```js
import config from "./config.jsonc";
console.log(config);
```
During bundling, the parsed JSONC is inlined into the bundle as a JavaScript object, identical to the `json` loader.
```js
var config = {
option: "value",
};
```
<Note>
Bun automatically uses the `jsonc` loader for `tsconfig.json`, `jsconfig.json`, `package.json`, and `bun.lock` files,
allowing comments and trailing commas in these files.
</Note>
---
### `toml`
**TOML loader.** Default for `.toml`.
@@ -131,7 +157,7 @@ age = 35
email = "johndoe@example.com"
```
```ts Output
```js Output
export default {
name: "John Doe",
age: 35,
@@ -143,7 +169,53 @@ export default {
---
### text
### `yaml`
**YAML loader.** Default for `.yaml` and `.yml`.
YAML files can be directly imported. Bun will parse them with its fast native YAML parser.
```js
import config from "./config.yaml";
console.log(config);
// via import attribute:
import data from "./data.txt" with { type: "yaml" };
```
During bundling, the parsed YAML is inlined into the bundle as a JavaScript object.
```js
var config = {
name: "my-app",
version: "1.0.0",
// ...other fields
};
```
If a `.yaml` or `.yml` file is passed as an entrypoint, it will be converted to a `.js` module that `export default`s the parsed object.
<CodeGroup>
```yaml Input
name: John Doe
age: 35
email: johndoe@example.com
```
```js Output
export default {
name: "John Doe",
age: 35,
email: "johndoe@example.com",
};
```
</CodeGroup>
---
### `text`
**Text loader.** Default for `.txt`.
@@ -173,7 +245,7 @@ If a `.txt` file is passed as an entrypoint, it will be converted to a `.js` mod
Hello, world!
```
```ts Output
```js Output
export default "Hello, world!";
```
@@ -181,7 +253,7 @@ export default "Hello, world!";
---
### napi
### `napi`
**Native addon loader.** Default for `.node`.
@@ -196,7 +268,7 @@ console.log(addon);
---
### sqlite
### `sqlite`
**SQLite loader.** Requires `with { "type": "sqlite" }` import attribute.
@@ -226,7 +298,9 @@ Otherwise, the database to embed is copied into the `outdir` with a hashed filen
---
### html
### `html`
**HTML loader.** Default for `.html`.
The `html` loader processes HTML files and bundles any referenced assets. It will:
@@ -301,7 +375,27 @@ The `html` loader behaves differently depending on how it's used:
---
### sh
### `css`
**CSS loader.** Default for `.css`.
CSS files can be directly imported. The bundler will parse and bundle CSS files, handling `@import` statements and `url()` references.
```js
import "./styles.css";
```
During bundling, all imported CSS files are bundled together into a single `.css` file in the output directory.
```css
.my-class {
background: url("./image.png");
}
```
---
### `sh`
**Bun Shell loader.** Default for `.sh` files.
@@ -313,7 +407,7 @@ bun run ./script.sh
---
### file
### `file`
**File loader.** Default for all unrecognized file types.

File diff suppressed because it is too large Load Diff

View File

@@ -42,7 +42,21 @@ type PluginBuilder = {
config: BuildConfig;
};
type Loader = "js" | "jsx" | "ts" | "tsx" | "css" | "json" | "toml";
type Loader =
| "js"
| "jsx"
| "ts"
| "tsx"
| "json"
| "jsonc"
| "toml"
| "yaml"
| "file"
| "napi"
| "wasm"
| "text"
| "css"
| "html";
```
## Usage

View File

@@ -354,7 +354,7 @@
"/guides/ecosystem/discordjs",
"/guides/ecosystem/docker",
"/guides/ecosystem/drizzle",
"/guides/ecosystem/edgedb",
"/guides/ecosystem/gel",
"/guides/ecosystem/elysia",
"/guides/ecosystem/express",
"/guides/ecosystem/hono",

View File

@@ -132,9 +132,7 @@ CMD ["bun", "index.ts"]
<Note>
Make sure that the start command corresponds to your application's entry point. This can also be `CMD ["bun", "run", "start"]` if you have a start script in your `package.json`.
This image installs dependencies and runs your app with Bun inside a container. If your app doesn't have dependencies, you can omit the `RUN bun install --production --frozen-lockfile` line.
This image installs dependencies and runs your app with Bun inside a container. If your app doesn't have dependencies, you can omit the `RUN bun install --production --frozen-lockfile` line.
This image installs dependencies and runs your app with Bun inside a container. If your app doesn't have dependencies, you can omit the `RUN bun install --production --frozen-lockfile` line.
</Note>
@@ -159,8 +157,7 @@ Make sure you're in the directory containing your `Dockerfile`, then deploy dire
<Note>
Update the `--region` flag to your preferred region. You can also omit this flag to get an interactive prompt to
select a region. Update the `--region` flag to your preferred region. You can also omit this flag to get an
interactive prompt to select a region.
select a region.
</Note>
```bash terminal icon="terminal"

View File

@@ -1,23 +1,27 @@
---
title: Use EdgeDB with Bun
sidebarTitle: EdgeDB with Bun
title: Use Gel with Bun
sidebarTitle: Gel with Bun
mode: center
---
EdgeDB is a graph-relational database powered by Postgres under the hood. It provides a declarative schema language, migrations system, and object-oriented query language, in addition to supporting raw SQL queries. It solves the object-relational mapping problem at the database layer, eliminating the need for an ORM library in your application code.
Gel (formerly EdgeDB) is a graph-relational database powered by Postgres under the hood. It provides a declarative schema language, migrations system, and object-oriented query language, in addition to supporting raw SQL queries. It solves the object-relational mapping problem at the database layer, eliminating the need for an ORM library in your application code.
---
First, [install EdgeDB](https://www.edgedb.com/install) if you haven't already.
First, [install Gel](https://docs.geldata.com/learn/installation) if you haven't already.
<CodeGroup>
```sh Linux/macOS terminal icon="terminal"
curl --proto '=https' --tlsv1.2 -sSf https://sh.edgedb.com | sh
curl https://www.geldata.com/sh --proto "=https" -sSf1 | sh
```
```sh Windows terminal icon="windows"
iwr https://ps1.edgedb.com -useb | iex
irm https://www.geldata.com/ps1 | iex
```
```sh Homebrew terminal icon="terminal"
brew install geldata/tap/gel-cli
```
</CodeGroup>
@@ -34,35 +38,35 @@ bun init -y
---
We'll use the EdgeDB CLI to initialize an EdgeDB instance for our project. This creates an `edgedb.toml` file in our project root.
We'll use the Gel CLI to initialize a Gel instance for our project. This creates a `gel.toml` file in our project root.
```sh terminal icon="terminal"
edgedb project init
gel project init
```
```txt
No `edgedb.toml` found in `/Users/colinmcd94/Documents/bun/fun/examples/my-edgedb-app` or above
No `gel.toml` found in `/Users/colinmcd94/Documents/bun/fun/examples/my-gel-app` or above
Do you want to initialize a new project? [Y/n]
> Y
Specify the name of EdgeDB instance to use with this project [default: my_edgedb_app]:
> my_edgedb_app
Checking EdgeDB versions...
Specify the version of EdgeDB to use with this project [default: x.y]:
Specify the name of Gel instance to use with this project [default: my_gel_app]:
> my_gel_app
Checking Gel versions...
Specify the version of Gel to use with this project [default: x.y]:
> x.y
┌─────────────────────┬────────────────────────────────────────────────────────────────────────
│ Project directory │ /Users/colinmcd94/Documents/bun/fun/examples/my-edgedb-app
│ Project config │ /Users/colinmcd94/Documents/bun/fun/examples/my-edgedb-app/edgedb.toml
│ Schema dir (empty) │ /Users/colinmcd94/Documents/bun/fun/examples/my-edgedb-app/dbschema
│ Installation method │ portable package
│ Version │ x.y+6d5921b
│ Instance name │ my_edgedb_app
└─────────────────────┴────────────────────────────────────────────────────────────────────────
┌─────────────────────┬──────────────────────────────────────────────────────────────────┐
│ Project directory │ /Users/colinmcd94/Documents/bun/fun/examples/my-gel-app
│ Project config │ /Users/colinmcd94/Documents/bun/fun/examples/my-gel-app/gel.toml│
│ Schema dir (empty) │ /Users/colinmcd94/Documents/bun/fun/examples/my-gel-app/dbschema│
│ Installation method │ portable package │
│ Version │ x.y+6d5921b │
│ Instance name │ my_gel_app │
└─────────────────────┴──────────────────────────────────────────────────────────────────┘
Version x.y+6d5921b is already downloaded
Initializing EdgeDB instance...
Initializing Gel instance...
Applying migrations...
Everything is up to date. Revision initial
Project initialized.
To connect to my_edgedb_app, run `edgedb`
To connect to my_gel_app, run `gel`
```
---
@@ -70,8 +74,8 @@ To connect to my_edgedb_app, run `edgedb`
To see if the database is running, let's open a REPL and run a simple query.
```sh terminal icon="terminal"
edgedb
edgedb> select 1 + 1;
gel
gel> select 1 + 1;
```
```txt
@@ -81,12 +85,12 @@ edgedb> select 1 + 1;
Then run `\quit` to exit the REPL.
```sh terminal icon="terminal"
edgedb> \quit
gel> \quit
```
---
With the project initialized, we can define a schema. The `edgedb project init` command already created a `dbschema/default.esdl` file to contain our schema.
With the project initialized, we can define a schema. The `gel project init` command already created a `dbschema/default.esdl` file to contain our schema.
```txt File Tree icon="folder-tree"
dbschema
@@ -112,15 +116,15 @@ module default {
Then generate and apply an initial migration.
```sh terminal icon="terminal"
edgedb migration create
gel migration create
```
```txt
Created /Users/colinmcd94/Documents/bun/fun/examples/my-edgedb-app/dbschema/migrations/00001.edgeql, id: m1uwekrn4ni4qs7ul7hfar4xemm5kkxlpswolcoyqj3xdhweomwjrq
Created /Users/colinmcd94/Documents/bun/fun/examples/my-gel-app/dbschema/migrations/00001.edgeql, id: m1uwekrn4ni4qs7ul7hfar4xemm5kkxlpswolcoyqj3xdhweomwjrq
```
```sh terminal icon="terminal"
edgedb migrate
gel migrate
```
```txt
@@ -129,11 +133,11 @@ Applied m1uwekrn4ni4qs7ul7hfar4xemm5kkxlpswolcoyqj3xdhweomwjrq (00001.edgeql)
---
With our schema applied, let's execute some queries using EdgeDB's JavaScript client library. We'll install the client library and EdgeDB's codegen CLI, and create a `seed.ts`.file.
With our schema applied, let's execute some queries using Gel's JavaScript client library. We'll install the client library and Gel's codegen CLI, and create a `seed.ts`.file.
```sh terminal icon="terminal"
bun add edgedb
bun add -D @edgedb/generate
bun add gel
bun add -D @gel/generate
touch seed.ts
```
@@ -144,7 +148,7 @@ Paste the following code into `seed.ts`.
The client auto-connects to the database. We insert a couple movies using the `.execute()` method. We will use EdgeQL's `for` expression to turn this bulk insert into a single optimized query.
```ts seed.ts icon="/icons/typescript.svg"
import { createClient } from "edgedb";
import { createClient } from "gel";
const client = createClient();
@@ -184,10 +188,10 @@ Seeding complete.
---
EdgeDB implements a number of code generation tools for TypeScript. To query our newly seeded database in a typesafe way, we'll use `@edgedb/generate` to code-generate the EdgeQL query builder.
Gel implements a number of code generation tools for TypeScript. To query our newly seeded database in a typesafe way, we'll use `@gel/generate` to code-generate the EdgeQL query builder.
```sh terminal icon="terminal"
bunx @edgedb/generate edgeql-js
bunx @gel/generate edgeql-js
```
```txt
@@ -213,7 +217,7 @@ the query builder directory? The following line will be added:
In `index.ts`, we can import the generated query builder from `./dbschema/edgeql-js` and write a simple select query.
```ts index.ts icon="/icons/typescript.svg"
import { createClient } from "edgedb";
import { createClient } from "gel";
import e from "./dbschema/edgeql-js";
const client = createClient();
@@ -254,4 +258,4 @@ bun run index.ts
---
For complete documentation, refer to the [EdgeDB docs](https://www.edgedb.com/docs).
For complete documentation, refer to the [Gel docs](https://docs.geldata.com/).

View File

@@ -64,10 +64,10 @@ Later, when this test file is executed again, Bun will read the snapshot file an
```sh terminal icon="terminal"
bun test
bun test v1.3.2 (9c68abdb)
```
```txt
bun test v1.3.2 (9c68abdb)
test/snap.test.ts:
✓ snapshot [1.05ms]
@@ -83,10 +83,10 @@ To update snapshots, use the `--update-snapshots` flag.
```sh terminal icon="terminal"
bun test --update-snapshots
bun test v1.3.2 (9c68abdb)
```
```txt
bun test v1.3.2 (9c68abdb)
test/snap.test.ts:
✓ snapshot [0.86ms]

View File

@@ -303,7 +303,7 @@ scripts[test:watch] # bracket for special chars
Examples:
```bash terminal icon="terminal"
# set
# get
bun pm pkg get name # single property
bun pm pkg get name version # multiple properties
bun pm pkg get # entire package.json

View File

@@ -7,8 +7,7 @@ Bun supports [`workspaces`](https://docs.npmjs.com/cli/v9/using-npm/workspaces?v
It's common for a monorepo to have the following structure:
```
tree
```txt File Tree icon="folder-tree"
<root>
├── README.md
├── bun.lock

View File

@@ -401,6 +401,7 @@ Environment variable: `BUN_INSTALL_BIN`
```toml title="bunfig.toml" icon="settings"
# where globally-installed package bins are linked
[install]
globalBinDir = "~/.bun/bin"
```

View File

@@ -276,7 +276,7 @@ await Bun.build({
## Path re-mapping
In the spirit of treating TypeScript as a first-class citizen, the Bun runtime will re-map import paths according to the [`compilerOptions.paths`](https://www.typescriptlang.org/tsconfig#paths) field in `tsconfig.json`. This is a major divergence from Node.js, which doesn't support any form of import path re-mapping.
Bun supports import path re-mapping through TypeScript's [`compilerOptions.paths`](https://www.typescriptlang.org/tsconfig#paths) in `tsconfig.json`, which works well with editors. If you aren't a TypeScript user, you can achieve the same behavior by using a [`jsconfig.json`](https://code.visualstudio.com/docs/languages/jsconfig) in your project root.
```json tsconfig.json icon="file-json"
{
@@ -289,7 +289,16 @@ In the spirit of treating TypeScript as a first-class citizen, the Bun runtime w
}
```
If you aren't a TypeScript user, you can create a [`jsconfig.json`](https://code.visualstudio.com/docs/languages/jsconfig) in your project root to achieve the same behavior.
Bun also supports [Node.js-style subpath imports](https://nodejs.org/api/packages.html#subpath-imports) in `package.json`, where mapped paths must start with `#`. This approach doesnt work as well with editors, but both options can be used together.
```json package.json icon="file-json"
{
"imports": {
"#config": "./config.ts", // map specifier to file
"#components/*": "./components/*" // wildcard matching
}
}
```
<Accordion title="Low-level details of CommonJS interop in Bun">

View File

@@ -42,7 +42,21 @@ type PluginBuilder = {
config: BuildConfig;
};
type Loader = "js" | "jsx" | "ts" | "tsx" | "css" | "json" | "toml";
type Loader =
| "js"
| "jsx"
| "ts"
| "tsx"
| "json"
| "jsonc"
| "toml"
| "yaml"
| "file"
| "napi"
| "wasm"
| "text"
| "css"
| "html";
```
## Usage

View File

@@ -697,7 +697,7 @@ To list some or all (up to 1,000) objects in a bucket, you can use the `S3Client
```ts s3.ts icon="/icons/typescript.svg" highlight={12, 15-20, 24-29}
import { S3Client } from "bun";
const credentials = { ... }
const credentials = {
accessKeyId: "your-access-key",
secretAccessKey: "your-secret-key",
bucket: "my-bucket",

View File

@@ -123,7 +123,7 @@ export const GuidesList = () => {
title: "Ecosystem",
icon: "puzzle",
items: [
{ title: "Use EdgeDB with Bun", href: "/guides/ecosystem/edgedb" },
{ title: "Use Gel with Bun", href: "/guides/ecosystem/gel" },
{ title: "Use Prisma ORM with Bun", href: "/guides/ecosystem/prisma" },
{ title: "Use Prisma Postgres with Bun", href: "/guides/ecosystem/prisma-postgres" },
{ title: "Create a Discord bot", href: "/guides/ecosystem/discordjs" },

View File

@@ -4041,7 +4041,21 @@ declare module "bun" {
| "browser";
/** https://bun.com/docs/bundler/loaders */
type Loader = "js" | "jsx" | "ts" | "tsx" | "json" | "toml" | "file" | "napi" | "wasm" | "text" | "css" | "html";
type Loader =
| "js"
| "jsx"
| "ts"
| "tsx"
| "json"
| "jsonc"
| "toml"
| "yaml"
| "file"
| "napi"
| "wasm"
| "text"
| "css"
| "html";
interface PluginConstraints {
/**
@@ -6430,6 +6444,16 @@ declare module "bun" {
/** @see https://bun.com/docs/install/catalogs */
catalogs?: Record<string, Record<string, string>>;
/**
* `0` / `undefined` for projects created before v1.3.2, `1` for projects created after.
*
* ---
* Right now this only changes the default [install linker strategy](https://bun.com/docs/pm/cli/install#isolated-installs):
* - With `0`, the linker is hoisted.
* - With `1`, the linker is isolated for workspaces and hoisted for single-package projects.
*/
configVersion?: 0 | 1;
/**
* ```
* INFO = { prod/dev/optional/peer dependencies, os, cpu, libc (TODO), bin, binDir }

View File

@@ -635,7 +635,7 @@ ssl_on_writable(struct us_internal_ssl_socket_t *s) {
(struct us_internal_ssl_socket_context_t *)us_socket_context(0, &s->s);
// if this one fails to write data, it sets ssl_read_wants_write again
s = (struct us_internal_ssl_socket_t *)context->sc.on_data(&s->s, 0,
s = (struct us_internal_ssl_socket_t *)context->sc.on_data(&s->s, "",
0); // cast here!
}
// Do not call on_writable if the socket is closed.

View File

@@ -1392,6 +1392,10 @@ create_buildkite_user() {
create_file "$file"
done
# The following is necessary to configure buildkite to use a stable
# checkout directory. sccache hashes absolute paths into its cache keys,
# so if buildkite uses a different checkout path each time (which it does
# by default), sccache will be useless.
local opts=$-
set -ef

View File

@@ -104,29 +104,6 @@ pub const ErrorKind = enum(u8) {
js_aggregate,
};
pub fn initFromJs(dev: *DevServer, owner: Owner, value: JSValue) !SerializedFailure {
{
_ = value;
@panic("TODO");
}
// Avoid small re-allocations without requesting so much from the heap
var sfb = std.heap.stackFallback(65536, dev.allocator());
var payload = std.array_list.Managed(u8).initCapacity(sfb.get(), 65536) catch
unreachable; // enough space
const w = payload.writer();
try w.writeInt(u32, @bitCast(owner.encode()), .little);
// try writeJsValue(value);
// Avoid-recloning if it is was moved to the hap
const data = if (payload.items.ptr == &sfb.buffer)
try dev.allocator().dupe(u8, payload.items)
else
payload.items;
return .{ .data = data };
}
pub fn initFromLog(
dev: *DevServer,
owner: Owner,

View File

@@ -273,7 +273,11 @@ pub const Entry = struct {
.none => {
// NOTE: It is too late to compute the line count since the bundled text may
// have been freed already. For example, a HMR chunk is never persisted.
@panic("Missing internal precomputed line count.");
// We could return an error here but what would be a better behavior for renderJSON and renderMappings?
// This is a dev server, crashing is not a good DX, we could fail the request but that's not a good DX either.
if (bun.Environment.enable_logs) {
mapLog("Skipping source map entry with missing line count at index {d}", .{i});
}
},
};
}

View File

@@ -1357,6 +1357,10 @@ pub fn NewSocket(comptime ssl: bool) type {
return .js_undefined;
}
pub fn getFD(this: *This, _: *jsc.JSGlobalObject) JSValue {
return this.socket.fd().toJSWithoutMakingLibUVOwned();
}
pub fn getBytesWritten(this: *This, _: *jsc.JSGlobalObject) JSValue {
return jsc.JSValue.jsNumber(this.bytes_written + this.buffered_data_for_node_net.len);
}

View File

@@ -523,6 +523,19 @@ pub fn getPort(this: *Listener, _: *jsc.JSGlobalObject) JSValue {
return JSValue.jsNumber(this.connection.host.port);
}
pub fn getFD(this: *Listener, _: *jsc.JSGlobalObject) JSValue {
switch (this.listener) {
.uws => |uws_listener| {
switch (this.ssl) {
inline else => |ssl| {
return uws_listener.socket(ssl).fd().toJSWithoutMakingLibUVOwned();
},
}
},
else => return JSValue.jsNumber(-1),
}
}
pub fn ref(this: *Listener, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
const this_value = callframe.this();
if (this.listener == .none) return .js_undefined;

View File

@@ -21,6 +21,7 @@ function generate(ssl) {
fn: "pauseFromJS",
length: 0,
},
getTLSFinishedMessage: {
fn: "getTLSFinishedMessage",
length: 0,
@@ -91,6 +92,9 @@ function generate(ssl) {
bytesWritten: {
getter: "getBytesWritten",
},
fd: {
getter: "getFD",
},
setNoDelay: {
fn: "setNoDelay",
length: 1,
@@ -263,7 +267,9 @@ export default [
fn: "unref",
length: 0,
},
fd: {
getter: "getFD",
},
port: {
getter: "getPort",
},

View File

@@ -1053,9 +1053,6 @@ pub const RunCommand = struct {
bun.copy(u8, path_buf[dir_slice.len..], base);
path_buf[dir_slice.len + base.len] = 0;
const slice = path_buf[0 .. dir_slice.len + base.len :0];
if (Environment.isWindows) {
@panic("TODO");
}
if (!(bun.sys.isExecutableFilePath(slice))) continue;
// we need to dupe because the string pay point to a pointer that only exists in the current scope
_ = try results.getOrPut(this_transpiler.fs.filename_store.append(@TypeOf(base), base) catch continue);

View File

@@ -11,6 +11,9 @@ pub const ListenSocket = opaque {
pub fn getSocket(this: *ListenSocket) *uws.us_socket_t {
return @ptrCast(this);
}
pub fn socket(this: *ListenSocket, comptime is_ssl: bool) uws.NewSocketHandler(is_ssl) {
return uws.NewSocketHandler(is_ssl).from(this.getSocket());
}
};
const c = struct {

View File

@@ -183,7 +183,7 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type {
const res = Fields.onData(
getValue(socket),
TLSSocket.from(socket),
buf.?[0..@as(usize, @intCast(len))],
if (buf) |data_ptr| data_ptr[0..@as(usize, @intCast(len))] else "",
);
if (@TypeOf(res) != void) res catch |err| switch (err) {
error.JSTerminated => return null, // TODO: declare throw scope
@@ -295,16 +295,29 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type {
}
pub inline fn fd(this: ThisSocket) bun.FileDescriptor {
if (comptime is_ssl) {
@compileError("SSL sockets do not have a file descriptor accessible this way");
}
const socket = this.socket.get() orelse return bun.invalid_fd;
// on windows uSockets exposes SOCKET
return if (comptime Environment.isWindows)
.fromNative(@ptrCast(socket.getNativeHandle(is_ssl).?))
else
.fromNative(@intCast(@intFromPtr(socket.getNativeHandle(is_ssl))));
if (comptime is_ssl) {
if (socket.getNativeHandle(is_ssl)) |handle| {
const ssl_ptr: *BoringSSL.SSL = @as(*BoringSSL.SSL, @ptrCast(handle));
const fd_value = BoringSSL.SSL_get_fd(ssl_ptr);
if (fd_value == -1) {
return bun.invalid_fd;
}
return if (Environment.isWindows)
.fromNative(@ptrFromInt(@as(usize, @intCast(fd_value))))
else
.fromNative(fd_value);
}
return bun.invalid_fd;
}
if (socket.getNativeHandle(is_ssl)) |handle| {
// on windows uSockets exposes SOCKET
return if (comptime Environment.isWindows)
.fromNative(@ptrCast(handle))
else
.fromNative(@intCast(@intFromPtr(handle)));
}
return bun.invalid_fd;
}
pub fn markNeedsMoreForSendfile(this: ThisSocket) void {
@@ -731,7 +744,7 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type {
const res = Fields.onData(
getValue(socket),
SocketHandlerType.from(socket),
buf.?[0..@as(usize, @intCast(len))],
if (buf) |data_ptr| data_ptr[0..@as(usize, @intCast(len))] else "",
);
if (@TypeOf(res) != void) res catch |err| switch (err) {
error.JSTerminated => return null, // TODO: declare throw scope
@@ -897,7 +910,7 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type {
const res = Fields.onData(
getValue(socket),
ThisSocket.from(socket),
buf.?[0..@as(usize, @intCast(len))],
if (buf) |data_ptr| data_ptr[0..@as(usize, @intCast(len))] else "",
);
if (@TypeOf(res) != void) res catch |err| switch (err) {
error.JSTerminated => return null, // TODO: declare throw scope

View File

@@ -18,7 +18,7 @@ pub const us_socket_t = opaque {
if (ip_addr) |ip| {
bun.assert(ip.len < max_i32);
_ = c.us_socket_open(ssl, this, @intFromBool(is_client), ip.ptr, @intCast(ip.len));
_ = c.us_socket_open(ssl, this, @intFromBool(is_client), ip.ptr, @intCast(@min(ip.len, std.math.maxInt(i32))));
} else {
_ = c.us_socket_open(ssl, this, @intFromBool(is_client), null, 0);
}
@@ -66,7 +66,7 @@ pub const us_socket_t = opaque {
/// Returned slice is a view into `buf`.
pub fn localAddress(this: *us_socket_t, ssl: bool, buf: []u8) ![]const u8 {
var length: i32 = @intCast(buf.len);
var length: i32 = @intCast(@min(buf.len, std.math.maxInt(i32)));
c.us_socket_local_address(@intFromBool(ssl), this, buf.ptr, &length);
if (length < 0) {
@@ -81,7 +81,7 @@ pub const us_socket_t = opaque {
/// Returned slice is a view into `buf`. On error, `errno` should be set
pub fn remoteAddress(this: *us_socket_t, ssl: bool, buf: []u8) ![]const u8 {
var length: i32 = @intCast(buf.len);
var length: i32 = @intCast(@min(buf.len, std.math.maxInt(i32)));
c.us_socket_remote_address(@intFromBool(ssl), this, buf.ptr, &length);
if (length < 0) {
@@ -95,11 +95,11 @@ pub const us_socket_t = opaque {
}
pub fn setTimeout(this: *us_socket_t, ssl: bool, seconds: u32) void {
c.us_socket_timeout(@intFromBool(ssl), this, @intCast(seconds));
c.us_socket_timeout(@intFromBool(ssl), this, seconds);
}
pub fn setLongTimeout(this: *us_socket_t, ssl: bool, minutes: u32) void {
c.us_socket_long_timeout(@intFromBool(ssl), this, @intCast(minutes));
c.us_socket_long_timeout(@intFromBool(ssl), this, minutes);
}
pub fn setNodelay(this: *us_socket_t, enabled: bool) void {
@@ -109,7 +109,7 @@ pub const us_socket_t = opaque {
/// Returns error code. `0` on success. error codes depend on platform an
/// configured event loop.
pub fn setKeepalive(this: *us_socket_t, enabled: bool, delay: u32) i32 {
return c.us_socket_keepalive(this, @intFromBool(enabled), @intCast(delay));
return c.us_socket_keepalive(this, @intFromBool(enabled), delay);
}
pub fn getNativeHandle(this: *us_socket_t, ssl: bool) ?*anyopaque {
@@ -127,14 +127,14 @@ pub const us_socket_t = opaque {
}
pub fn write(this: *us_socket_t, ssl: bool, data: []const u8) i32 {
const rc = c.us_socket_write(@intFromBool(ssl), this, data.ptr, @intCast(data.len));
const rc = c.us_socket_write(@intFromBool(ssl), this, data.ptr, @intCast(@min(data.len, std.math.maxInt(i32))));
debug("us_socket_write({p}, {d}) = {d}", .{ this, data.len, rc });
return rc;
}
pub fn writeFd(this: *us_socket_t, data: []const u8, file_descriptor: bun.FD) i32 {
if (bun.Environment.isWindows) @compileError("TODO: implement writeFd on Windows");
const rc = c.us_socket_ipc_write_fd(this, data.ptr, @intCast(data.len), file_descriptor.native());
const rc = c.us_socket_ipc_write_fd(this, data.ptr, @intCast(@min(data.len, std.math.maxInt(i32))), file_descriptor.native());
debug("us_socket_ipc_write_fd({p}, {d}, {d}) = {d}", .{ this, data.len, file_descriptor.native(), rc });
return rc;
}
@@ -147,7 +147,7 @@ pub const us_socket_t = opaque {
pub fn rawWrite(this: *us_socket_t, ssl: bool, data: []const u8) i32 {
debug("us_socket_raw_write({p}, {d})", .{ this, data.len });
return c.us_socket_raw_write(@intFromBool(ssl), this, data.ptr, @intCast(data.len));
return c.us_socket_raw_write(@intFromBool(ssl), this, data.ptr, @intCast(@min(data.len, std.math.maxInt(i32))));
}
pub fn flush(this: *us_socket_t, ssl: bool) void {

View File

@@ -346,6 +346,9 @@ pub const FD = packed struct(backing_int) {
/// After calling, the input file descriptor is no longer valid and must not be used.
/// If an error is thrown, the file descriptor is cleaned up for you.
pub fn toJS(any_fd: FD, global: *jsc.JSGlobalObject) JSValue {
if (!any_fd.isValid()) {
return JSValue.jsNumberFromInt32(-1);
}
const uv_owned_fd = any_fd.makeLibUVOwned() catch {
any_fd.close();
return global.throwValue((jsc.SystemError{
@@ -356,6 +359,24 @@ pub const FD = packed struct(backing_int) {
return JSValue.jsNumberFromInt32(uv_owned_fd.uv());
}
/// Convert an FD to a JavaScript number without transferring ownership to libuv.
/// Unlike toJS(), this does not call makeLibUVOwned() on Windows, so the caller
/// retains ownership and must close the FD themselves.
/// Returns -1 for invalid file descriptors.
/// On Windows: returns Uint64 for system handles, Int32 for uv file descriptors.
pub fn toJSWithoutMakingLibUVOwned(any_fd: FD) JSValue {
if (!any_fd.isValid()) {
return JSValue.jsNumberFromInt32(-1);
}
if (Environment.isWindows) {
return switch (any_fd.kind) {
.system => JSValue.jsNumberFromUint64(@intCast(any_fd.value.as_system)),
.uv => JSValue.jsNumberFromInt32(any_fd.value.as_uv),
};
}
return JSValue.jsNumberFromInt32(any_fd.value.as_system);
}
pub const Stdio = enum(u8) {
std_in = 0,
std_out = 1,

View File

@@ -3430,29 +3430,23 @@ pub fn isExecutableFileOSPath(path: bun.OSPathSliceZ) bool {
if (comptime Environment.isWindows) {
// Rationale: `GetBinaryTypeW` does not work on .cmd files.
// Windows does not have executable permission like posix does, instead we
// can just look at the file extension to determine executable status.
@compileError("Do not use isExecutableFilePath on Windows");
// SaferiIsExecutableFileType works on .cmd files.
// The following file name extensions are examples of executable file types. This is not a complete list.
// .bat
// .cmd
// .com
// .exe
// .js
// .lnk
// .pif
// .pl
// .shs
// .url
// .vbs
// The security policy Microsoft Management Console (MMC) snap-in (Secpol.msc) controls which extensions are considered executable file types.
// var out: windows.DWORD = 0;
// const rc = kernel32.GetBinaryTypeW(path, &out);
// const result = if (rc == windows.FALSE)
// false
// else switch (out) {
// kernel32.SCS_32BIT_BINARY,
// kernel32.SCS_64BIT_BINARY,
// kernel32.SCS_DOS_BINARY,
// kernel32.SCS_OS216_BINARY,
// kernel32.SCS_PIF_BINARY,
// kernel32.SCS_POSIX_BINARY,
// => true,
// else => false,
// };
// log("GetBinaryTypeW({f}) = {d}. isExecutable={}", .{ bun.fmt.utf16(path), out, result });
// return result;
// we pass false to include .exe files (see https://learn.microsoft.com/en-us/windows/win32/api/winsafer/nf-winsafer-saferiisexecutablefiletype)
return bun.windows.SaferiIsExecutableFileType(path, w.FALSE) != w.FALSE;
}
@compileError("TODO: isExecutablePath");

View File

@@ -150,7 +150,7 @@ pub extern "kernel32" fn SetCurrentDirectoryW(
) callconv(.winapi) win32.BOOL;
pub const SetCurrentDirectory = SetCurrentDirectoryW;
pub extern "ntdll" fn RtlNtStatusToDosError(win32.NTSTATUS) callconv(.winapi) Win32Error;
pub extern "advapi32" fn SaferiIsExecutableFileType(szFullPathname: win32.LPCWSTR, bFromShellExecute: win32.BOOLEAN) callconv(.winapi) win32.BOOL;
// This was originally copied from Zig's standard library
/// Codes are from https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d
pub const Win32Error = enum(u16) {

View File

@@ -59,7 +59,16 @@ describe("bun", () => {
);
});
});
describe("getcompletes", () => {
test("getcompletes should not panic and should not be empty", () => {
const { stdout, exitCode } = spawnSync({
cmd: [bunExe(), "getcompletes"],
env: bunEnv,
});
expect(exitCode).toBe(0);
expect(stdout.toString()).not.toBeEmpty();
});
});
describe("test command line arguments", () => {
test("test --config, issue #4128", () => {
const path = `${tmpdir()}/bunfig-${Date.now()}.toml`;

View File

@@ -0,0 +1,166 @@
// https://github.com/oven-sh/bun/pull/24575
// Tests that socket._handle.fd property is available
import { expect, test } from "bun:test";
import net from "node:net";
import tls from "node:tls";
test("socket._handle.fd should be accessible on TCP sockets", async () => {
const { promise, resolve, reject } = Promise.withResolvers<void>();
let serverFd: number | undefined;
let clientFd: number | undefined;
const server = net.createServer(socket => {
// Server-side socket should have _handle.fd
expect(socket._handle).toBeDefined();
expect(socket._handle.fd).toBeTypeOf("number");
expect(socket._handle.fd).toBeGreaterThan(0);
serverFd = socket._handle.fd;
socket.end(`server fd: ${socket._handle.fd}`);
});
server.listen(0, "127.0.0.1", () => {
const client = net.connect({
host: "127.0.0.1",
port: (server.address() as any).port,
});
client.on("connect", () => {
// Client-side socket should have _handle.fd
expect(client._handle).toBeDefined();
expect(client._handle.fd).toBeTypeOf("number");
expect(client._handle.fd).toBeGreaterThan(0);
clientFd = client._handle.fd;
});
client.on("data", data => {
const response = data.toString();
expect(response).toStartWith("server fd: ");
// Verify we got valid fds
expect(serverFd).toBeTypeOf("number");
expect(clientFd).toBeTypeOf("number");
expect(serverFd).toBeGreaterThan(0);
expect(clientFd).toBeGreaterThan(0);
// Server and client should have different fds
expect(serverFd).not.toBe(clientFd);
server.close();
resolve();
});
client.on("error", reject);
});
server.on("error", reject);
await promise;
});
test("socket._handle.fd should remain consistent during connection lifetime", async () => {
const { promise, resolve, reject } = Promise.withResolvers<void>();
const server = net.createServer(socket => {
const initialFd = socket._handle.fd;
// Send multiple messages to ensure fd doesn't change
socket.write("message1\n");
expect(socket._handle.fd).toBe(initialFd);
socket.write("message2\n");
expect(socket._handle.fd).toBe(initialFd);
socket.end("message3\n");
expect(socket._handle.fd).toBe(initialFd);
});
server.listen(0, "127.0.0.1", () => {
const client = net.connect({
host: "127.0.0.1",
port: (server.address() as any).port,
});
let initialClientFd: number;
let buffer = "";
client.on("connect", () => {
initialClientFd = client._handle.fd;
expect(initialClientFd).toBeGreaterThan(0);
});
client.on("data", data => {
buffer += data.toString();
// Fd should remain consistent across multiple data events
expect(client._handle.fd).toBe(initialClientFd);
});
client.on("end", () => {
// Verify we received all messages
expect(buffer).toBe("message1\nmessage2\nmessage3\n");
server.close();
resolve();
});
client.on("error", reject);
});
server.on("error", reject);
await promise;
});
test("socket._handle.fd should be accessible on TLS sockets", async () => {
const { tls: tlsCert } = await import("harness");
const { promise, resolve, reject } = Promise.withResolvers<void>();
let serverFd: number | undefined;
let clientFd: number | undefined;
const server = tls.createServer(tlsCert, socket => {
// Server-side TLS socket should have _handle.fd
expect(socket._handle).toBeDefined();
expect(socket._handle.fd).toBeTypeOf("number");
// TLS sockets should have a valid fd (may be -1 on some platforms/states)
expect(typeof socket._handle.fd).toBe("number");
serverFd = socket._handle.fd;
socket.end(`server fd: ${socket._handle.fd}`);
});
server.listen(0, "127.0.0.1", () => {
const client = tls.connect({
host: "127.0.0.1",
port: (server.address() as any).port,
rejectUnauthorized: false,
});
client.on("secureConnect", () => {
// Client-side TLS socket should have _handle.fd
expect(client._handle).toBeDefined();
expect(client._handle.fd).toBeTypeOf("number");
// TLS sockets should have a valid fd (may be -1 on some platforms/states)
expect(typeof client._handle.fd).toBe("number");
clientFd = client._handle.fd;
});
client.on("data", data => {
const response = data.toString();
expect(response).toMatch(/server fd: -?\d+/);
// Verify we got valid fds (number type, even if -1)
expect(serverFd).toBeTypeOf("number");
expect(clientFd).toBeTypeOf("number");
server.close();
resolve();
});
client.on("error", reject);
});
server.on("error", reject);
await promise;
});