mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 08:58:52 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeeef5aaf0 | ||
|
|
f9b966c13f | ||
|
|
d2ad4da1a0 | ||
|
|
eb4ef364f2 | ||
|
|
d0e2679fb5 | ||
|
|
764437eb6d | ||
|
|
824655e1cb | ||
|
|
21b2d5c3a5 | ||
|
|
10815a7d43 | ||
|
|
f839640c17 | ||
|
|
557e912d9a | ||
|
|
1e75a978e5 | ||
|
|
95e8c24db1 | ||
|
|
755f41fe2a | ||
|
|
9aabe4eea1 | ||
|
|
90f3bf2796 | ||
|
|
16b4bf341a | ||
|
|
1480889205 | ||
|
|
f269432d90 | ||
|
|
73b3fb7b0f |
@@ -35,7 +35,7 @@ To configure which port and hostname the server will listen on:
|
||||
|
||||
```ts
|
||||
Bun.serve({
|
||||
port: 8080, // defaults to $PORT, then 3000
|
||||
port: 8080, // defaults to $BUN_PORT, $PORT, $NODE_PORT otherwise 3000
|
||||
hostname: "mydomain.com", // defaults to "0.0.0.0"
|
||||
fetch(req) {
|
||||
return new Response(`404!`);
|
||||
@@ -43,6 +43,17 @@ Bun.serve({
|
||||
});
|
||||
```
|
||||
|
||||
To listen on a [unix domain socket](https://en.wikipedia.org/wiki/Unix_domain_socket):
|
||||
|
||||
```ts
|
||||
Bun.serve({
|
||||
unix: "/tmp/my-socket.sock", // path to socket
|
||||
fetch(req) {
|
||||
return new Response(`404!`);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
To activate development mode, set `development: true`. By default, development mode is _enabled_ unless `NODE_ENV` is `production`.
|
||||
@@ -78,7 +89,7 @@ Bun.serve({
|
||||
```
|
||||
|
||||
{% callout %}
|
||||
**Note** — Full debugger support is planned.
|
||||
[Learn more about debugging in Bun](https://bun.sh/docs/runtime/debugger)
|
||||
{% /callout %}
|
||||
|
||||
The call to `Bun.serve` returns a `Server` object. To stop the server, call the `.stop()` method.
|
||||
|
||||
@@ -31,3 +31,30 @@ All imported files and packages are bundled into the executable, along with a co
|
||||
- `--publicPath`
|
||||
|
||||
{% /callout %}
|
||||
|
||||
## Embedding files
|
||||
|
||||
Standalone executables support embedding files.
|
||||
|
||||
To embed files into an executable with `bun build --compile`, import the file in your code
|
||||
|
||||
```js
|
||||
// this becomes an internal file path
|
||||
import icon from "./icon.png";
|
||||
|
||||
import { file } from "bun";
|
||||
|
||||
export default {
|
||||
fetch(req) {
|
||||
return new Response(file(icon));
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
You may need to specify a `--loader` for it to be treated as a `"file"` loader (so you get back a file path).
|
||||
|
||||
Embedded files can be read using `Bun.file`'s functions or the Node.js `fs.readFile` function (in `"node:fs"`).
|
||||
|
||||
## Minification
|
||||
|
||||
To trim down the size of the executable a little, pass `--minify` to `bun build --compile`. This uses Bun's minifier to reduce the code size. Overall though, Bun's binary is still way too big and we need to make it smaller.
|
||||
|
||||
35
docs/guides/runtime/timezone.md
Normal file
35
docs/guides/runtime/timezone.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Set a time zone in Bun
|
||||
---
|
||||
|
||||
Bun supports programmatically setting a default time zone for the lifetime of the `bun` process. To do set, set the value of the `TZ` environment variable to a [valid timezone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
|
||||
|
||||
{% callout %}
|
||||
When running a file with `bun`, the timezone defaults to your system's configured local time zone.
|
||||
|
||||
When running tests with `bun test`, the timezone is set to `UTC` to make tests more deterministic.
|
||||
{% /callout %}
|
||||
|
||||
```ts
|
||||
process.env.TZ = "America/New_York";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Alternatively, this can be set from the command line when running a Bun command.
|
||||
|
||||
```sh
|
||||
$ TZ=America/New_York bun run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Once `TZ` is set, any `Date` instances will have that time zone. By default all dates use your system's configured time zone.
|
||||
|
||||
```ts
|
||||
new Date().getHours(); // => 18
|
||||
|
||||
process.env.TZ = "America/New_York";
|
||||
|
||||
new Date().getHours(); // => 21
|
||||
```
|
||||
23
docs/guides/test/bail.md
Normal file
23
docs/guides/test/bail.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Bail early with the Bun test runner
|
||||
---
|
||||
|
||||
Use the `--bail` flag to bail on a test run after a single failure. This is useful for aborting as soon as possible in a continuous integration environment.
|
||||
|
||||
```sh
|
||||
# re-run each test 10 times
|
||||
$ bun test --bail
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
To bail after a certain threshold of failures, optionally specify a number after the flag.
|
||||
|
||||
```sh
|
||||
# bail after 10 failures
|
||||
$ bun test --bail 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See [Docs > Test runner](/docs/cli/test) for complete documentation of `bun test`.
|
||||
58
docs/guides/test/coverage-threshold.md
Normal file
58
docs/guides/test/coverage-threshold.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: Set a code coverage threshold with the Bun test runner
|
||||
---
|
||||
|
||||
Bun's test runner supports built-in code coverage reporting via the `--coverage` flag.
|
||||
|
||||
```sh
|
||||
$ bun test --coverage
|
||||
|
||||
test.test.ts:
|
||||
✓ math > add [0.71ms]
|
||||
✓ math > multiply [0.03ms]
|
||||
✓ random [0.13ms]
|
||||
-------------|---------|---------|-------------------
|
||||
File | % Funcs | % Lines | Uncovered Line #s
|
||||
-------------|---------|---------|-------------------
|
||||
All files | 66.67 | 77.78 |
|
||||
math.ts | 50.00 | 66.67 |
|
||||
random.ts | 50.00 | 66.67 |
|
||||
-------------|---------|---------|-------------------
|
||||
|
||||
3 pass
|
||||
0 fail
|
||||
3 expect() calls
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
To set a minimum coverage threshold, add the following line to your `bunfig.toml`. This requires that 90% of your codebase is covered by tests.
|
||||
|
||||
```toml
|
||||
[test]
|
||||
# to require 90% line-level and function-level coverage
|
||||
coverageThreshold = 0.9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
If your test suite does not meet this threshold, `bun test` will exit with a non-zero exit code to signal a failure.
|
||||
|
||||
```sh
|
||||
$ bun test --coverage
|
||||
<test output>
|
||||
$ echo $?
|
||||
1 # this is the exit code of the previous command
|
||||
```
|
||||
|
||||
Different thresholds can be set for line-level and function-level coverage.
|
||||
|
||||
```toml
|
||||
[test]
|
||||
# to set different thresholds for lines and functions
|
||||
coverageThreshold = { line = 0.5, function = 0.7 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See [Docs > Test runner > Coverage](/docs/test/coverage) for complete documentation on code coverage reporting in Bun.
|
||||
44
docs/guides/test/coverage.md
Normal file
44
docs/guides/test/coverage.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: Generate code coverage reports with the Bun test runner
|
||||
---
|
||||
|
||||
Bun's test runner supports built-in _code coverage reporting_. This makes it easy to see how much of the codebase is covered by tests, and find areas that are not currently well-tested.
|
||||
|
||||
---
|
||||
|
||||
Pass the `--coverage` flag to `bun test` to enable this feature. This will print a coverage report after the test run.
|
||||
|
||||
The coverage report lists the source files that were executed during the test run, the percentage of functions and lines that were executed, and the line ranges that were not executed during the run.
|
||||
|
||||
```sh
|
||||
$ bun test --coverage
|
||||
|
||||
test.test.ts:
|
||||
✓ math > add [0.71ms]
|
||||
✓ math > multiply [0.03ms]
|
||||
✓ random [0.13ms]
|
||||
-------------|---------|---------|-------------------
|
||||
File | % Funcs | % Lines | Uncovered Line #s
|
||||
-------------|---------|---------|-------------------
|
||||
All files | 66.67 | 77.78 |
|
||||
math.ts | 50.00 | 66.67 |
|
||||
random.ts | 50.00 | 66.67 |
|
||||
-------------|---------|---------|-------------------
|
||||
|
||||
3 pass
|
||||
0 fail
|
||||
3 expect() calls
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
To always enable coverage reporting by default, add the following line to your `bunfig.toml`:
|
||||
|
||||
```toml
|
||||
[test]
|
||||
coverage = true # always enable coverage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Refer to [Docs > Test runner > Coverage](/docs/test/coverage) for complete documentation on code coverage reporting in Bun.
|
||||
68
docs/guides/test/happy-dom.md
Normal file
68
docs/guides/test/happy-dom.md
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: Write browser DOM tests with Bun and happy-dom
|
||||
---
|
||||
|
||||
You can write and run browser tests with Bun's test runner in conjunction with [Happy DOM](https://github.com/capricorn86/happy-dom). Happy DOM implements mocked versions of browser APIs like `document` and `location`.
|
||||
|
||||
---
|
||||
|
||||
To get started, install `happy-dom`.
|
||||
|
||||
```sh
|
||||
$ bun add -d @happy-dom/global-registrator
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
This module exports a "registrator" that will adds the mocked browser APIs to the global scope.
|
||||
|
||||
```ts#happydom.ts
|
||||
import { GlobalRegistrator } from "@happy-dom/global-registrator";
|
||||
|
||||
GlobalRegistrator.register();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
We need to make sure this file is executed before any of our test files. That's a job for Bun's built-in _preload_ functionality. Create a `bunfig.toml` file in the root of your project (if it doesn't already exist) and add the following lines.
|
||||
|
||||
The `./happydom.ts` file should contain the registration code above.
|
||||
|
||||
```toml#bunfig.toml
|
||||
[test]
|
||||
preload = "./happydom.ts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Now running `bun test` inside our project will automatically execute `happydom.ts` first. We can start writing tests that use browser APIs.
|
||||
|
||||
```ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("set button text", () => {
|
||||
document.body.innerHTML = `<button>My button</button>`;
|
||||
const button = document.querySelector("button");
|
||||
expect(button?.innerText).toEqual("My button");
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
With Happy DOM propertly configured, this test runs as expected.
|
||||
|
||||
```sh
|
||||
$ bun test
|
||||
|
||||
dom.test.ts:
|
||||
✓ set button text [0.82ms]
|
||||
|
||||
1 pass
|
||||
0 fail
|
||||
1 expect() calls
|
||||
Ran 1 tests across 1 files. 1 total [125.00ms]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Refer to the [Happy DOM repo](https://github.com/capricorn86/happy-dom) and [Docs > Test runner > DOM](/docs/test/dom) for complete documentation on writing browser tests with Bun.
|
||||
4
docs/guides/test/index.json
Normal file
4
docs/guides/test/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Test runner",
|
||||
"description": "A collection of guides for writing, running, and configuring tests in Bun"
|
||||
}
|
||||
48
docs/guides/test/mock-clock.md
Normal file
48
docs/guides/test/mock-clock.md
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: Set the system time in Bun's test runner
|
||||
---
|
||||
|
||||
Bun's test runner supports setting the system time programmatically with the `setSystemTime` function.
|
||||
|
||||
```ts
|
||||
import { test, expect, beforeAll, setSystemTime } from "bun:test";
|
||||
|
||||
test("party like it's 1999", () => {
|
||||
const date = new Date("1999-01-01T00:00:00.000Z");
|
||||
setSystemTime(date); // it's now January 1, 1999
|
||||
|
||||
const now = new Date();
|
||||
expect(now.getFullYear()).toBe(1999);
|
||||
expect(now.getMonth()).toBe(0);
|
||||
expect(now.getDate()).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
The `setSystemTime` function is commonly used on conjunction with [Lifecycle Hooks](/docs/test/lifecycle) to configure a testing environment with a determinstic "fake clock".
|
||||
|
||||
```ts
|
||||
import { test, expect, beforeAll, setSystemTime } from "bun:test";
|
||||
|
||||
beforeAll(() => {
|
||||
const date = new Date("1999-01-01T00:00:00.000Z");
|
||||
setSystemTime(date); // it's now January 1, 1999
|
||||
});
|
||||
|
||||
// tests...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
To reset the system clock to the actual time, call `setSystemTime` with no arguments.
|
||||
|
||||
```ts
|
||||
import { test, expect, beforeAll, setSystemTime } from "bun:test";
|
||||
|
||||
setSystemTime(); // reset to actual time
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See [Docs > Test Runner > Date and time](/docs/test/time) for complete documentation on mocking with the Bun test runner.
|
||||
68
docs/guides/test/mock-functions.md
Normal file
68
docs/guides/test/mock-functions.md
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: Mock functions in `bun test`
|
||||
---
|
||||
|
||||
Create mocks with the `mock` function from `bun:test`.
|
||||
|
||||
```ts
|
||||
import { test, expect, mock } from "bun:test";
|
||||
|
||||
const random = mock(() => Math.random());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
The mock function can accept arguments.
|
||||
|
||||
```ts
|
||||
import { test, expect, mock } from "bun:test";
|
||||
|
||||
const random = mock((multiplier: number) => multiplier * Math.random());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
The result of `mock()` is a new function that's been decorated with some additional properties.
|
||||
|
||||
```ts
|
||||
import { mock } from "bun:test";
|
||||
|
||||
const random = mock((multiplier: number) => multiplier * Math.random());
|
||||
|
||||
random(2);
|
||||
random(10);
|
||||
|
||||
random.mock.calls;
|
||||
// [[ 2 ], [ 10 ]]
|
||||
|
||||
random.mock.results;
|
||||
// [
|
||||
// { type: "return", value: 0.6533907460954099 },
|
||||
// { type: "return", value: 0.6452713933037312 }
|
||||
// ]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
These extra properties make it possible to write `expect` assertions about usage of the mock function, including how many times it was called, the arguments, and the return values.
|
||||
|
||||
```ts
|
||||
import { test, mock } from "bun:test";
|
||||
|
||||
const random = mock((multiplier: number) => multiplier * Math.random());
|
||||
|
||||
test("random", async () => {
|
||||
const a = random(1);
|
||||
const b = random(2);
|
||||
const c = random(3);
|
||||
|
||||
expect(random).toHaveBeenCalled();
|
||||
expect(random).toHaveBeenCalledTimes(3);
|
||||
expect(random.mock.args).toEqual([[1], [2], [3]]);
|
||||
expect(random.mock.results[0]).toEqual({ type: "return", value: a });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See [Docs > Test Runner > Mocks](/docs/test/mocks) for complete documentation on mocking with the Bun test runner.
|
||||
14
docs/guides/test/rerun-each.md
Normal file
14
docs/guides/test/rerun-each.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Re-run tests multiple times with the Bun test runner
|
||||
---
|
||||
|
||||
Use the `--rerun-each` flag to re-run every test multiple times with the Bun test runner. This is useful for finding flaky or non-deterministic tests.
|
||||
|
||||
```sh
|
||||
# re-run each test 10 times
|
||||
$ bun test --rerun-each 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See [Docs > Test runner](/docs/cli/test) for complete documentation of `bun test`.
|
||||
99
docs/guides/test/run-tests.md
Normal file
99
docs/guides/test/run-tests.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: Run your tests with the Bun test runner
|
||||
---
|
||||
|
||||
Bun has a built-in test runner with a Jest-like `expect` API. To use it, run `bun test` from your project directory. The test runner will search for all files in the directory that match the following patterns:
|
||||
|
||||
- `*.test.{js|jsx|ts|tsx}`
|
||||
- `*_test.{js|jsx|ts|tsx}`
|
||||
- `*.spec.{js|jsx|ts|tsx}`
|
||||
- `*_spec.{js|jsx|ts|tsx}`
|
||||
|
||||
```sh
|
||||
$ bun test
|
||||
bun test v0.8.0 (9c68abdb)
|
||||
|
||||
test.test.js:
|
||||
✓ add [0.87ms]
|
||||
✓ multiply [0.02ms]
|
||||
|
||||
test2.test.js:
|
||||
✓ add [0.72ms]
|
||||
✓ multiply [0.01ms]
|
||||
|
||||
test3.test.js:
|
||||
✓ add [0.54ms]
|
||||
✓ multiply [0.01ms]
|
||||
|
||||
6 pass
|
||||
0 fail
|
||||
6 expect() calls
|
||||
Ran 6 tests across 3 files. [9.00ms]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
To only run certain test files, pass a positional argument to `bun test`. The runner will only execute files that contain that argument in their path.
|
||||
|
||||
```sh
|
||||
$ bun test test3
|
||||
bun test v0.8.0 (9c68abdb)
|
||||
|
||||
test3.test.js:
|
||||
✓ add [1.40ms]
|
||||
✓ multiply [0.03ms]
|
||||
|
||||
2 pass
|
||||
0 fail
|
||||
2 expect() calls
|
||||
Ran 2 tests across 1 files. [15.00ms]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
All tests have a name, defined as the first parameter to the `test` function. Tests can also be inside a `describe` block.
|
||||
|
||||
```ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("add", () => {
|
||||
expect(2 + 2).toEqual(4);
|
||||
});
|
||||
|
||||
test("multiply", () => {
|
||||
expect(2 * 2).toEqual(4);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
To filter which tests are executed by name, use the `-t`/`--test-name-pattern` flag.
|
||||
|
||||
Adding `-t add` will only run tests with "add" in the name. This flag also checks the name of the test suite (the first parameter to `describe`).
|
||||
|
||||
```sh
|
||||
$ bun test -t add
|
||||
bun test v0.8.0 (9c68abdb)
|
||||
|
||||
test.test.js:
|
||||
✓ add [1.79ms]
|
||||
» multiply
|
||||
|
||||
test2.test.js:
|
||||
✓ add [2.30ms]
|
||||
» multiply
|
||||
|
||||
test3.test.js:
|
||||
✓ add [0.32ms]
|
||||
» multiply
|
||||
|
||||
3 pass
|
||||
3 skip
|
||||
0 fail
|
||||
3 expect() calls
|
||||
Ran 6 tests across 3 files. [59.00ms]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See [Docs > Test Runner](/docs/cli/test) for complete documentation on the test runner.
|
||||
37
docs/guides/test/skip-tests.md
Normal file
37
docs/guides/test/skip-tests.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: Skip tests with the Bun test runner
|
||||
---
|
||||
|
||||
To skip a test with the Bun test runner, use the `test.skip` function.
|
||||
|
||||
```ts-diff
|
||||
test.skip("unimplemented feature", ()=>{
|
||||
expect(Bun.isAwesome()).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Running `bun test` will not execute this test. It will be marked as skipped in the terminal output.
|
||||
|
||||
```sh
|
||||
$ bun test
|
||||
|
||||
test.test.ts:
|
||||
✓ add [0.03ms]
|
||||
✓ multiply [0.02ms]
|
||||
» unimplemented feature
|
||||
|
||||
2 pass
|
||||
1 skip
|
||||
0 fail
|
||||
2 expect() calls
|
||||
Ran 3 tests across 1 files. [74.00ms]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See also:
|
||||
|
||||
- [Mark a test as a todo](/guides/test/todo-tests)
|
||||
- [Docs > Test runner > Writing tests](/docs/test/writings-tests)
|
||||
99
docs/guides/test/snapshot.md
Normal file
99
docs/guides/test/snapshot.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: Use snapshot testing in `bun test`
|
||||
---
|
||||
|
||||
Bun's test runner supports Jest-style snapshot testing via `.toMatchSnapshot()`.
|
||||
|
||||
{% callout %}
|
||||
The `.toMatchInlineSnapshot()` method is not yet supported.
|
||||
{% /callout %}
|
||||
|
||||
```ts#snap.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("snapshot", () => {
|
||||
expect({ foo: "bar" }).toMatchSnapshot();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
The first time this test is executed, Bun will evaluate the value passed into `expect()` (`{ foo: "bar" }`) and write it to disk in a directory called `__snapshots__` that lives alongside the test file.
|
||||
|
||||
```sh
|
||||
$ bun test test/snap
|
||||
bun test v0.8.0 (9c68abdb)
|
||||
|
||||
test/snap.test.ts:
|
||||
✓ snapshot [1.48ms]
|
||||
|
||||
1 pass
|
||||
0 fail
|
||||
snapshots: +1 added # note: the snapshot is created automatically the first run
|
||||
1 expect() calls
|
||||
Ran 1 tests across 1 files. [82.00ms]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
The `__snapshots__` directory contains a `.snap` file for each test file in the directory.
|
||||
|
||||
```txt
|
||||
test
|
||||
├── __snapshots__
|
||||
│ └── snap.test.ts.snap
|
||||
└── snap.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
The `snap.test.ts.snap` file is a JavaScript file that exports a serialized version of the value passed into `expect()`. The `{foo: "bar"}` object has been serialized to JSON.
|
||||
|
||||
```js
|
||||
// Bun Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`snapshot 1`] = `
|
||||
{
|
||||
"foo": "bar",
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Later, when this test file is executed again, Bun will read the snapshot file and compare it to the value passed into `expect()`. If the values are different, the test will fail.
|
||||
|
||||
```sh
|
||||
$ bun test
|
||||
bun test v0.8.0 (9c68abdb)
|
||||
|
||||
test/snap.test.ts:
|
||||
✓ snapshot [1.05ms]
|
||||
|
||||
1 pass
|
||||
0 fail
|
||||
1 snapshots, 1 expect() calls
|
||||
Ran 1 tests across 1 files. [101.00ms]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
To update snapshots, use the `--update-snapshots` flag.
|
||||
|
||||
```sh
|
||||
$ bun test --update-snapshots
|
||||
bun test v0.8.0 (9c68abdb)
|
||||
|
||||
test/snap.test.ts:
|
||||
✓ snapshot [0.86ms]
|
||||
|
||||
1 pass
|
||||
0 fail
|
||||
snapshots: +1 added # the snapshot was regenerated
|
||||
1 expect() calls
|
||||
Ran 1 tests across 1 files. [102.00ms]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See [Docs > Test Runner > Snapshots](/docs/test/mocks) for complete documentation on mocking with the Bun test runner.
|
||||
46
docs/guides/test/spy-on.md
Normal file
46
docs/guides/test/spy-on.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: Spy on methods in `bun test`
|
||||
---
|
||||
|
||||
Use the `spyOn` utility to track method calls with Bun's test runner.
|
||||
|
||||
```ts
|
||||
import { test, expect, spyOn } from "bun:test";
|
||||
|
||||
const leo = {
|
||||
name: "Leonard",
|
||||
sayHi(thing: string) {
|
||||
console.log(`Sup I'm ${this.name} and I like ${thing}`);
|
||||
},
|
||||
};
|
||||
|
||||
const spy = spyOn(leo, "sayHi");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Once the spy is created, it can be used to write `expect` assertions relating to method calls.
|
||||
|
||||
```ts-diff
|
||||
import { test, expect, spyOn } from "bun:test";
|
||||
|
||||
const leo = {
|
||||
name: "Leonardo",
|
||||
sayHi(thing: string) {
|
||||
console.log(`Sup, I'm ${this.name} and I like ${thing}`);
|
||||
},
|
||||
};
|
||||
|
||||
const spy = spyOn(leo, "sayHi");
|
||||
|
||||
+ test("turtles", ()=>{
|
||||
+ expect(spy).toHaveBeenCalledTimes(0);
|
||||
+ leo.sayHi("pizza");
|
||||
+ expect(spy).toHaveBeenCalledTimes(0);
|
||||
+ expect(spy.mock.calls).toEqual([[ "pizza" ]]);
|
||||
+ })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See [Docs > Test Runner > Mocks](/docs/test/mocks) for complete documentation on mocking with the Bun test runner.
|
||||
15
docs/guides/test/timeout.md
Normal file
15
docs/guides/test/timeout.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: Set a per-test timeout with the Bun test runner
|
||||
---
|
||||
|
||||
Use the `--timeout` flag to set a timeout for each test in milliseconds. If any test exceeds this timeout, it will be marked as failed.
|
||||
|
||||
The default timeout is `5000` (5 seconds).
|
||||
|
||||
```sh
|
||||
$ bun test --timeout 3000 # 3 seconds
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See [Docs > Test runner](/docs/cli/test) for complete documentation of `bun test`.
|
||||
60
docs/guides/test/todo-tests.md
Normal file
60
docs/guides/test/todo-tests.md
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: Mark a test as a "todo" with the Bun test runner
|
||||
---
|
||||
|
||||
To remind yourself to write a test later, use the `test.todo` function. There's no need to provide a test implementation.
|
||||
|
||||
```ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
// write this later
|
||||
test.todo("unimplemented feature");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Optionally, you can provide a test implementation.
|
||||
|
||||
```ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test.todo("unimplemented feature", () => {
|
||||
expect(Bun.isAwesome()).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
The output of `bun test` indicates how many `todo` tests were encountered.
|
||||
|
||||
```sh
|
||||
$ bun test
|
||||
|
||||
test.test.ts:
|
||||
✓ add [0.03ms]
|
||||
✓ multiply [0.02ms]
|
||||
✎ unimplemented feature
|
||||
|
||||
2 pass
|
||||
1 todo
|
||||
0 fail
|
||||
2 expect() calls
|
||||
Ran 3 tests across 1 files. [74.00ms]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Note that `todo` tests _are executed_ by the test runner! They are _expected to fail_; if a todo test passes, the `bun test` run will return a non-zero exit code to signal the failure.
|
||||
|
||||
```sh
|
||||
$ bun test
|
||||
$ echo $?
|
||||
1 # this is the exit code of the previous command
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See also:
|
||||
|
||||
- [Skip a test](/guides/test/skip-tests)
|
||||
- [Docs > Test runner > Writing tests](/docs/test/writings-tests)
|
||||
50
docs/guides/test/update-snapshots.md
Normal file
50
docs/guides/test/update-snapshots.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: Update snapshots in `bun test`
|
||||
---
|
||||
|
||||
Bun's test runner supports Jest-style snapshot testing via `.toMatchSnapshot()`.
|
||||
|
||||
{% callout %}
|
||||
The `.toMatchInlineSnapshot()` method is not yet supported.
|
||||
{% /callout %}
|
||||
|
||||
```ts#snap.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("snapshot", () => {
|
||||
expect({ foo: "bar" }).toMatchSnapshot();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
The first time this test is executed, Bun will write a snapshot file to disk in a directory called `__snapshots__` that lives alongside the test file.
|
||||
|
||||
```txt
|
||||
test
|
||||
├── __snapshots__
|
||||
│ └── snap.test.ts.snap
|
||||
└── snap.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
To regenerate snapshots, use the `--update-snapshots` flag.
|
||||
|
||||
```sh
|
||||
$ bun test --update-snapshots
|
||||
bun test v0.8.0 (9c68abdb)
|
||||
|
||||
test/snap.test.ts:
|
||||
✓ snapshot [0.86ms]
|
||||
|
||||
1 pass
|
||||
0 fail
|
||||
snapshots: +1 added # the snapshot was regenerated
|
||||
1 expect() calls
|
||||
Ran 1 tests across 1 files. [102.00ms]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See [Docs > Test Runner > Snapshots](/docs/test/mocks) for complete documentation on mocking with the Bun test runner.
|
||||
19
docs/guides/test/watch-mode.md
Normal file
19
docs/guides/test/watch-mode.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Run tests in watch mode with Bun
|
||||
---
|
||||
|
||||
Use the `--watch` flag to run your tests in watch mode.
|
||||
|
||||
```sh
|
||||
$ bun test --watch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
This will restart the running Bun process whenever a file change is detected. It's fast. In this example, the editor is configured to save the file on every keystroke.
|
||||
|
||||
{% image src="https://github.com/oven-sh/bun/assets/3084745/dc49a36e-ba82-416f-b960-1c883a924248" caption="Running tests in watch mode in Bun" /%}
|
||||
|
||||
---
|
||||
|
||||
See [Docs > Test Runner](/docs/cli/test) for complete documentation on the test runner.
|
||||
@@ -5,10 +5,11 @@
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"mitata": "^0.1.3",
|
||||
"peechy": "latest",
|
||||
"peechy": "0.4.34",
|
||||
"prettier": "^2.4.1",
|
||||
"react": "next",
|
||||
"react-dom": "next",
|
||||
"source-map-js": "^1.0.2",
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"private": true,
|
||||
@@ -17,7 +18,7 @@
|
||||
"build-fallback": "esbuild --target=esnext --bundle src/fallback.ts --format=iife --platform=browser --minify > src/fallback.out.js",
|
||||
"postinstall": "bash .scripts/postinstall.sh",
|
||||
"typecheck": "tsc --noEmit && cd test && bun run typecheck",
|
||||
"fmt": "prettier --write --cache './{src,test,bench}/**/*.{mjs,ts,tsx,js,jsx}'",
|
||||
"fmt": "prettier --write --cache './{src,test,bench,packages/{bun-inspector-*,bun-vscode,bun-debug-adapter-protocol}}/**/*.{mjs,ts,tsx,js,jsx}'",
|
||||
"lint": "eslint './**/*.d.ts' --cache",
|
||||
"lint:fix": "eslint './**/*.d.ts' --cache --fix"
|
||||
},
|
||||
|
||||
2
packages/bun-debug-adapter-protocol/.gitattributes
vendored
Normal file
2
packages/bun-debug-adapter-protocol/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
protocol/*/protocol.json linguist-generated=true
|
||||
protocol/*/index.d.ts linguist-generated=true
|
||||
1
packages/bun-debug-adapter-protocol/.gitignore
vendored
Normal file
1
packages/bun-debug-adapter-protocol/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
protocol/*.json
|
||||
3
packages/bun-debug-adapter-protocol/README.md
Normal file
3
packages/bun-debug-adapter-protocol/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# bun-debug-adapter-protocol
|
||||
|
||||
https://microsoft.github.io/debug-adapter-protocol/overview
|
||||
BIN
packages/bun-debug-adapter-protocol/bun.lockb
Executable file
BIN
packages/bun-debug-adapter-protocol/bun.lockb
Executable file
Binary file not shown.
3
packages/bun-debug-adapter-protocol/index.ts
Normal file
3
packages/bun-debug-adapter-protocol/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type * from "./src/protocol";
|
||||
export * from "./src/debugger/adapter";
|
||||
export * from "./src/debugger/signal";
|
||||
8
packages/bun-debug-adapter-protocol/package.json
Normal file
8
packages/bun-debug-adapter-protocol/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "bun-debug-adapter-protocol",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.4",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
}
|
||||
176
packages/bun-debug-adapter-protocol/scripts/generate-protocol.ts
Normal file
176
packages/bun-debug-adapter-protocol/scripts/generate-protocol.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import type { Protocol, Type } from "../src/protocol/schema";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
run().catch(console.error);
|
||||
|
||||
async function run() {
|
||||
const cwd = new URL("../protocol/", import.meta.url);
|
||||
const runner = "Bun" in globalThis ? "bunx" : "npx";
|
||||
const write = (name: string, data: string) => {
|
||||
const path = new URL(name, cwd);
|
||||
writeFileSync(path, data);
|
||||
spawnSync(runner, ["prettier", "--write", path.pathname], { cwd, stdio: "ignore" });
|
||||
};
|
||||
const schema: Protocol = await download(
|
||||
"https://microsoft.github.io/debug-adapter-protocol/debugAdapterProtocol.json",
|
||||
);
|
||||
write("protocol.json", JSON.stringify(schema));
|
||||
const types = formatProtocol(schema);
|
||||
write("index.d.ts", `// GENERATED - DO NOT EDIT\n${types}`);
|
||||
}
|
||||
|
||||
function formatProtocol(protocol: Protocol, extraTs?: string): string {
|
||||
const { definitions } = protocol;
|
||||
const requestMap = new Map();
|
||||
const responseMap = new Map();
|
||||
const eventMap = new Map();
|
||||
let body = `export namespace DAP {`;
|
||||
loop: for (const [key, definition] of Object.entries(definitions)) {
|
||||
if (/[a-z]+Request$/i.test(key)) {
|
||||
continue;
|
||||
}
|
||||
if (/[a-z]+Arguments$/i.test(key)) {
|
||||
const name = key.replace(/(Request)?Arguments$/, "");
|
||||
const requestName = `${name}Request`;
|
||||
requestMap.set(toMethod(name), requestName);
|
||||
body += formatType(definition, requestName);
|
||||
continue;
|
||||
}
|
||||
if ("allOf" in definition) {
|
||||
const { allOf } = definition;
|
||||
for (const type of allOf) {
|
||||
if (type.type !== "object") {
|
||||
continue;
|
||||
}
|
||||
const { description, properties = {} } = type;
|
||||
if (/[a-z]+Event$/i.test(key)) {
|
||||
const { event, body: type = {} } = properties;
|
||||
if (!event || !("enum" in event)) {
|
||||
continue;
|
||||
}
|
||||
const [eventKey] = event.enum ?? [];
|
||||
eventMap.set(eventKey, key);
|
||||
const eventType: Type = {
|
||||
type: "object",
|
||||
description,
|
||||
...type,
|
||||
};
|
||||
body += formatType(eventType, key);
|
||||
continue loop;
|
||||
}
|
||||
if (/[a-z]+Response$/i.test(key)) {
|
||||
const { body: type = {} } = properties;
|
||||
const bodyType: Type = {
|
||||
type: "object",
|
||||
description,
|
||||
...type,
|
||||
};
|
||||
const name = key.replace(/Response$/, "");
|
||||
responseMap.set(toMethod(name), key);
|
||||
body += formatType(bodyType, key);
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
body += formatType(definition, key);
|
||||
}
|
||||
for (const [key, name] of responseMap) {
|
||||
if (requestMap.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const requestName = `${name.replace(/Response$/, "")}Request`;
|
||||
requestMap.set(key, requestName);
|
||||
body += formatType({ type: "object", properties: {} }, requestName);
|
||||
}
|
||||
body += formatMapType("RequestMap", requestMap);
|
||||
body += formatMapType("ResponseMap", responseMap);
|
||||
body += formatMapType("EventMap", eventMap);
|
||||
if (extraTs) {
|
||||
body += extraTs;
|
||||
}
|
||||
return body + "};";
|
||||
}
|
||||
|
||||
function formatMapType(key: string, typeMap: Map<string, string>): string {
|
||||
const type: Type = {
|
||||
type: "object",
|
||||
required: [...typeMap.keys()],
|
||||
properties: Object.fromEntries([...typeMap.entries()].map(([key, value]) => [key, { $ref: value }])),
|
||||
};
|
||||
return formatType(type, key);
|
||||
}
|
||||
|
||||
function formatType(type: Type, key?: string): string {
|
||||
const { description, type: kind } = type;
|
||||
let body = "";
|
||||
if (key) {
|
||||
if (description) {
|
||||
body += `\n${toComment(description)}\n`;
|
||||
}
|
||||
body += `export type ${key}=`;
|
||||
}
|
||||
if (kind === "boolean") {
|
||||
body += "boolean";
|
||||
} else if (kind === "number" || kind === "integer") {
|
||||
body += "number";
|
||||
} else if (kind === "string") {
|
||||
const { enum: choices } = type;
|
||||
if (choices) {
|
||||
body += choices.map(value => `"${value}"`).join("|");
|
||||
} else {
|
||||
body += "string";
|
||||
}
|
||||
} else if (kind === "array") {
|
||||
const { items } = type;
|
||||
const itemType = items ? formatType(items) : "unknown";
|
||||
body += `${itemType}[]`;
|
||||
} else if (kind === "object") {
|
||||
const { properties, required } = type;
|
||||
if (!properties || Object.keys(properties).length === 0) {
|
||||
body += "{}";
|
||||
} else {
|
||||
body += "{";
|
||||
for (const [key, { description, ...type }] of Object.entries(properties)) {
|
||||
if (description) {
|
||||
body += `\n${toComment(description)}`;
|
||||
}
|
||||
const delimit = required?.includes(key) ? ":" : "?:";
|
||||
body += `\n${key}${delimit}${formatType(type)};`;
|
||||
}
|
||||
body += "}";
|
||||
}
|
||||
} else if ("$ref" in type) {
|
||||
const { $ref: ref } = type;
|
||||
body += ref.split("/").pop() || "unknown";
|
||||
} else if ("allOf" in type) {
|
||||
const { allOf } = type;
|
||||
body += allOf.map(type => formatType(type)).join("&");
|
||||
} else {
|
||||
body += "unknown";
|
||||
}
|
||||
if (key) {
|
||||
body += ";";
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function toMethod(name: string): string {
|
||||
return `${name.substring(0, 1).toLowerCase()}${name.substring(1)}`;
|
||||
}
|
||||
|
||||
function toComment(description?: string): string {
|
||||
if (!description) {
|
||||
return "";
|
||||
}
|
||||
const lines = ["/**", ...description.split("\n").map(line => ` * ${line.trim()}`), "*/"];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function download<T>(url: string | URL): Promise<T> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download ${url}: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
1764
packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
Normal file
1764
packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
Normal file
File diff suppressed because it is too large
Load Diff
271
packages/bun-debug-adapter-protocol/src/debugger/capabilities.ts
Normal file
271
packages/bun-debug-adapter-protocol/src/debugger/capabilities.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import type { DAP } from "../protocol";
|
||||
|
||||
const capabilities: DAP.Capabilities = {
|
||||
/**
|
||||
* The debug adapter supports the `configurationDone` request.
|
||||
* @see configurationDone
|
||||
*/
|
||||
supportsConfigurationDoneRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports function breakpoints using the `setFunctionBreakpoints` request.
|
||||
* @see setFunctionBreakpoints
|
||||
*/
|
||||
supportsFunctionBreakpoints: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports conditional breakpoints.
|
||||
* @see setBreakpoints
|
||||
* @see setInstructionBreakpoints
|
||||
* @see setFunctionBreakpoints
|
||||
* @see setExceptionBreakpoints
|
||||
* @see setDataBreakpoints
|
||||
*/
|
||||
supportsConditionalBreakpoints: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports breakpoints that break execution after a specified number of hits.
|
||||
* @see setBreakpoints
|
||||
* @see setInstructionBreakpoints
|
||||
* @see setFunctionBreakpoints
|
||||
* @see setExceptionBreakpoints
|
||||
* @see setDataBreakpoints
|
||||
*/
|
||||
supportsHitConditionalBreakpoints: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports a (side effect free) `evaluate` request for data hovers.
|
||||
* @see evaluate
|
||||
*/
|
||||
supportsEvaluateForHovers: true,
|
||||
|
||||
/**
|
||||
* Available exception filter options for the `setExceptionBreakpoints` request.
|
||||
* @see setExceptionBreakpoints
|
||||
*/
|
||||
exceptionBreakpointFilters: [
|
||||
{
|
||||
filter: "all",
|
||||
label: "Caught Exceptions",
|
||||
default: false,
|
||||
supportsCondition: true,
|
||||
description: "Breaks on all throw errors, even if they're caught later.",
|
||||
conditionDescription: `error.name == "CustomError"`,
|
||||
},
|
||||
{
|
||||
filter: "uncaught",
|
||||
label: "Uncaught Exceptions",
|
||||
default: false,
|
||||
supportsCondition: true,
|
||||
description: "Breaks only on errors or promise rejections that are not handled.",
|
||||
conditionDescription: `error.name == "CustomError"`,
|
||||
},
|
||||
],
|
||||
|
||||
/**
|
||||
* The debug adapter supports stepping back via the `stepBack` and `reverseContinue` requests.
|
||||
* @see stepBack
|
||||
* @see reverseContinue
|
||||
*/
|
||||
supportsStepBack: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports setting a variable to a value.
|
||||
* @see setVariable
|
||||
*/
|
||||
supportsSetVariable: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports restarting a frame.
|
||||
* @see restartFrame
|
||||
*/
|
||||
supportsRestartFrame: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `gotoTargets` request.
|
||||
* @see gotoTargets
|
||||
*/
|
||||
supportsGotoTargetsRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `stepInTargets` request.
|
||||
* @see stepInTargets
|
||||
*/
|
||||
supportsStepInTargetsRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `completions` request.
|
||||
* @see completions
|
||||
*/
|
||||
supportsCompletionsRequest: false,
|
||||
|
||||
/**
|
||||
* The set of characters that should trigger completion in a REPL.
|
||||
* If not specified, the UI should assume the `.` character.
|
||||
* @see completions
|
||||
*/
|
||||
completionTriggerCharacters: [".", "[", '"', "'"],
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `modules` request.
|
||||
* @see modules
|
||||
*/
|
||||
supportsModulesRequest: false,
|
||||
|
||||
/**
|
||||
* The set of additional module information exposed by the debug adapter.
|
||||
* @see modules
|
||||
*/
|
||||
additionalModuleColumns: [],
|
||||
|
||||
/**
|
||||
* Checksum algorithms supported by the debug adapter.
|
||||
*/
|
||||
supportedChecksumAlgorithms: [],
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `restart` request.
|
||||
* In this case a client should not implement `restart` by terminating
|
||||
* and relaunching the adapter but by calling the `restart` request.
|
||||
* @see restart
|
||||
*/
|
||||
supportsRestartRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports `exceptionOptions` on the `setExceptionBreakpoints` request.
|
||||
* @see setExceptionBreakpoints
|
||||
*/
|
||||
supportsExceptionOptions: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports a `format` attribute on the `stackTrace`, `variables`, and `evaluate` requests.
|
||||
* @see stackTrace
|
||||
* @see variables
|
||||
* @see evaluate
|
||||
*/
|
||||
supportsValueFormattingOptions: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `exceptionInfo` request.
|
||||
* @see exceptionInfo
|
||||
*/
|
||||
supportsExceptionInfoRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `terminateDebuggee` attribute on the `disconnect` request.
|
||||
* @see disconnect
|
||||
*/
|
||||
supportTerminateDebuggee: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `suspendDebuggee` attribute on the `disconnect` request.
|
||||
* @see disconnect
|
||||
*/
|
||||
supportSuspendDebuggee: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the delayed loading of parts of the stack,
|
||||
* which requires that both the `startFrame` and `levels` arguments and
|
||||
* the `totalFrames` result of the `stackTrace` request are supported.
|
||||
* @see stackTrace
|
||||
*/
|
||||
supportsDelayedStackTraceLoading: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `loadedSources` request.
|
||||
* @see loadedSources
|
||||
*/
|
||||
supportsLoadedSourcesRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports log points by interpreting the `logMessage` attribute of the `SourceBreakpoint`.
|
||||
* @see setBreakpoints
|
||||
*/
|
||||
supportsLogPoints: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `terminateThreads` request.
|
||||
* @see terminateThreads
|
||||
*/
|
||||
supportsTerminateThreadsRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `setExpression` request.
|
||||
* @see setExpression
|
||||
*/
|
||||
supportsSetExpression: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `terminate` request.
|
||||
* @see terminate
|
||||
*/
|
||||
supportsTerminateRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports data breakpoints.
|
||||
* @see setDataBreakpoints
|
||||
*/
|
||||
supportsDataBreakpoints: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `readMemory` request.
|
||||
* @see readMemory
|
||||
*/
|
||||
supportsReadMemoryRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `writeMemory` request.
|
||||
* @see writeMemory
|
||||
*/
|
||||
supportsWriteMemoryRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `disassemble` request.
|
||||
* @see disassemble
|
||||
*/
|
||||
supportsDisassembleRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `cancel` request.
|
||||
* @see cancel
|
||||
*/
|
||||
supportsCancelRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `breakpointLocations` request.
|
||||
* @see breakpointLocations
|
||||
*/
|
||||
supportsBreakpointLocationsRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `clipboard` context value in the `evaluate` request.
|
||||
* @see evaluate
|
||||
*/
|
||||
supportsClipboardContext: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports stepping granularities (argument `granularity`) for the stepping requests.
|
||||
* @see stepIn
|
||||
*/
|
||||
supportsSteppingGranularity: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports adding breakpoints based on instruction references.
|
||||
* @see setInstructionBreakpoints
|
||||
*/
|
||||
supportsInstructionBreakpoints: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports `filterOptions` as an argument on the `setExceptionBreakpoints` request.
|
||||
* @see setExceptionBreakpoints
|
||||
*/
|
||||
supportsExceptionFilterOptions: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `singleThread` property on the execution requests
|
||||
* (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, `stepBack`).
|
||||
*/
|
||||
supportsSingleThreadExecutionRequests: false,
|
||||
};
|
||||
|
||||
export default capabilities;
|
||||
@@ -0,0 +1,36 @@
|
||||
"use strict";
|
||||
export default {
|
||||
fetch(request) {
|
||||
const animal = getAnimal(request.url);
|
||||
const voice = animal.talk();
|
||||
return new Response(voice);
|
||||
},
|
||||
};
|
||||
function getAnimal(query) {
|
||||
switch (query.split("/").pop()) {
|
||||
case "dog":
|
||||
return new Dog();
|
||||
case "cat":
|
||||
return new Cat();
|
||||
}
|
||||
return new Bird();
|
||||
}
|
||||
class Dog {
|
||||
name = "dog";
|
||||
talk() {
|
||||
return "woof";
|
||||
}
|
||||
}
|
||||
class Cat {
|
||||
name = "cat";
|
||||
talk() {
|
||||
return "meow";
|
||||
}
|
||||
}
|
||||
class Bird {
|
||||
name = "bird";
|
||||
talk() {
|
||||
return "chirp";
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsicGFja2FnZXMvYnVuLWRlYnVnLWFkYXB0ZXItcHJvdG9jb2wvZGVidWdnZXIvZml4dHVyZXMvd2l0aC1zb3VyY2VtYXAudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImV4cG9ydCBkZWZhdWx0IHtcbiAgZmV0Y2gocmVxdWVzdDogUmVxdWVzdCk6IFJlc3BvbnNlIHtcbiAgICBjb25zdCBhbmltYWwgPSBnZXRBbmltYWwocmVxdWVzdC51cmwpO1xuICAgIGNvbnN0IHZvaWNlID0gYW5pbWFsLnRhbGsoKTtcbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKHZvaWNlKTtcbiAgfSxcbn07XG5cbmZ1bmN0aW9uIGdldEFuaW1hbChxdWVyeTogc3RyaW5nKTogQW5pbWFsIHtcbiAgc3dpdGNoIChxdWVyeS5zcGxpdChcIi9cIikucG9wKCkpIHtcbiAgICBjYXNlIFwiZG9nXCI6XG4gICAgICByZXR1cm4gbmV3IERvZygpO1xuICAgIGNhc2UgXCJjYXRcIjpcbiAgICAgIHJldHVybiBuZXcgQ2F0KCk7XG4gIH1cbiAgcmV0dXJuIG5ldyBCaXJkKCk7XG59XG5cbmludGVyZmFjZSBBbmltYWwge1xuICByZWFkb25seSBuYW1lOiBzdHJpbmc7XG4gIHRhbGsoKTogc3RyaW5nO1xufVxuXG5jbGFzcyBEb2cgaW1wbGVtZW50cyBBbmltYWwge1xuICBuYW1lID0gXCJkb2dcIjtcblxuICB0YWxrKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIFwid29vZlwiO1xuICB9XG59XG5cbmNsYXNzIENhdCBpbXBsZW1lbnRzIEFuaW1hbCB7XG4gIG5hbWUgPSBcImNhdFwiO1xuXG4gIHRhbGsoKTogc3RyaW5nIHtcbiAgICByZXR1cm4gXCJtZW93XCI7XG4gIH1cbn1cblxuY2xhc3MgQmlyZCBpbXBsZW1lbnRzIEFuaW1hbCB7XG4gIG5hbWUgPSBcImJpcmRcIjtcblxuICB0YWxrKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIFwiY2hpcnBcIjtcbiAgfVxufVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFBLGVBQWU7QUFBQSxFQUNiLE1BQU0sU0FBNEI7QUFDaEMsVUFBTSxTQUFTLFVBQVUsUUFBUSxHQUFHO0FBQ3BDLFVBQU0sUUFBUSxPQUFPLEtBQUs7QUFDMUIsV0FBTyxJQUFJLFNBQVMsS0FBSztBQUFBLEVBQzNCO0FBQ0Y7QUFFQSxTQUFTLFVBQVUsT0FBdUI7QUFDeEMsVUFBUSxNQUFNLE1BQU0sR0FBRyxFQUFFLElBQUksR0FBRztBQUFBLElBQzlCLEtBQUs7QUFDSCxhQUFPLElBQUksSUFBSTtBQUFBLElBQ2pCLEtBQUs7QUFDSCxhQUFPLElBQUksSUFBSTtBQUFBLEVBQ25CO0FBQ0EsU0FBTyxJQUFJLEtBQUs7QUFDbEI7QUFPQSxNQUFNLElBQXNCO0FBQUEsRUFDMUIsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7QUFFQSxNQUFNLElBQXNCO0FBQUEsRUFDMUIsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7QUFFQSxNQUFNLEtBQXVCO0FBQUEsRUFDM0IsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7IiwKICAibmFtZXMiOiBbXQp9Cg==
|
||||
@@ -0,0 +1,46 @@
|
||||
export default {
|
||||
fetch(request: Request): Response {
|
||||
const animal = getAnimal(request.url);
|
||||
const voice = animal.talk();
|
||||
return new Response(voice);
|
||||
},
|
||||
};
|
||||
|
||||
function getAnimal(query: string): Animal {
|
||||
switch (query.split("/").pop()) {
|
||||
case "dog":
|
||||
return new Dog();
|
||||
case "cat":
|
||||
return new Cat();
|
||||
}
|
||||
return new Bird();
|
||||
}
|
||||
|
||||
interface Animal {
|
||||
readonly name: string;
|
||||
talk(): string;
|
||||
}
|
||||
|
||||
class Dog implements Animal {
|
||||
name = "dog";
|
||||
|
||||
talk(): string {
|
||||
return "woof";
|
||||
}
|
||||
}
|
||||
|
||||
class Cat implements Animal {
|
||||
name = "cat";
|
||||
|
||||
talk(): string {
|
||||
return "meow";
|
||||
}
|
||||
}
|
||||
|
||||
class Bird implements Animal {
|
||||
name = "bird";
|
||||
|
||||
talk(): string {
|
||||
return "chirp";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
fetch(request) {
|
||||
return new Response(a());
|
||||
},
|
||||
};
|
||||
|
||||
function a() {
|
||||
return b();
|
||||
}
|
||||
|
||||
function b() {
|
||||
return c();
|
||||
}
|
||||
|
||||
function c() {
|
||||
function d() {
|
||||
return "hello";
|
||||
}
|
||||
return d();
|
||||
}
|
||||
87
packages/bun-debug-adapter-protocol/src/debugger/signal.ts
Normal file
87
packages/bun-debug-adapter-protocol/src/debugger/signal.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { Server } from "node:net";
|
||||
import { createServer } from "node:net";
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
const isDebug = process.env.NODE_ENV === "development";
|
||||
|
||||
export type UnixSignalEventMap = {
|
||||
"Signal.listening": [string];
|
||||
"Signal.error": [Error];
|
||||
"Signal.received": [string];
|
||||
"Signal.closed": [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts a server that listens for signals on a UNIX domain socket.
|
||||
*/
|
||||
export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
|
||||
#path: string;
|
||||
#server: Server;
|
||||
#ready: Promise<void>;
|
||||
|
||||
constructor(path?: string) {
|
||||
super();
|
||||
this.#path = path ? parseUnixPath(path) : randomUnixPath();
|
||||
this.#server = createServer();
|
||||
this.#server.on("listening", () => this.emit("Signal.listening", this.#path));
|
||||
this.#server.on("error", error => this.emit("Signal.error", error));
|
||||
this.#server.on("close", () => this.emit("Signal.closed"));
|
||||
this.#server.on("connection", socket => {
|
||||
socket.on("data", data => {
|
||||
this.emit("Signal.received", data.toString());
|
||||
});
|
||||
});
|
||||
this.#ready = new Promise((resolve, reject) => {
|
||||
this.#server.on("listening", resolve);
|
||||
this.#server.on("error", reject);
|
||||
});
|
||||
this.#server.listen(this.#path);
|
||||
}
|
||||
|
||||
emit<E extends keyof UnixSignalEventMap>(event: E, ...args: UnixSignalEventMap[E]): boolean {
|
||||
if (isDebug) {
|
||||
console.log(event, ...args);
|
||||
}
|
||||
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* The path to the UNIX domain socket.
|
||||
*/
|
||||
get url(): string {
|
||||
return `unix://${this.#path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves when the server is listening or rejects if an error occurs.
|
||||
*/
|
||||
get ready(): Promise<void> {
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the server.
|
||||
*/
|
||||
close(): void {
|
||||
this.#server.close();
|
||||
}
|
||||
}
|
||||
|
||||
function randomUnixPath(): string {
|
||||
return join(tmpdir(), `${Math.random().toString(36).slice(2)}.sock`);
|
||||
}
|
||||
|
||||
function parseUnixPath(path: string): string {
|
||||
if (path.startsWith("/")) {
|
||||
return path;
|
||||
}
|
||||
try {
|
||||
const { pathname } = new URL(path);
|
||||
return pathname;
|
||||
} catch {
|
||||
throw new Error(`Invalid UNIX path: ${path}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { SourceMap } from "./sourcemap";
|
||||
|
||||
test("works without source map", () => {
|
||||
const sourceMap = getSourceMap("without-sourcemap.js");
|
||||
expect(sourceMap.generatedLocation({ line: 7 })).toEqual({ line: 7, column: 0, verified: true });
|
||||
expect(sourceMap.generatedLocation({ line: 7, column: 2 })).toEqual({ line: 7, column: 2, verified: true });
|
||||
expect(sourceMap.originalLocation({ line: 11 })).toEqual({ line: 11, column: 0, verified: true });
|
||||
expect(sourceMap.originalLocation({ line: 11, column: 2 })).toEqual({ line: 11, column: 2, verified: true });
|
||||
});
|
||||
|
||||
test("works with source map", () => {
|
||||
const sourceMap = getSourceMap("with-sourcemap.js");
|
||||
// FIXME: Columns don't appear to be accurate for `generatedLocation`
|
||||
expect(sourceMap.generatedLocation({ line: 3 })).toMatchObject({ line: 4, verified: true });
|
||||
expect(sourceMap.generatedLocation({ line: 27 })).toMatchObject({ line: 20, verified: true });
|
||||
expect(sourceMap.originalLocation({ line: 32 })).toEqual({ line: 43, column: 4, verified: true });
|
||||
expect(sourceMap.originalLocation({ line: 13 })).toEqual({ line: 13, column: 6, verified: true });
|
||||
});
|
||||
|
||||
function getSourceMap(filename: string): SourceMap {
|
||||
const { pathname } = new URL(`./fixtures/${filename}`, import.meta.url);
|
||||
const source = readFileSync(pathname, "utf-8");
|
||||
const match = source.match(/\/\/# sourceMappingURL=(.*)$/m);
|
||||
if (match) {
|
||||
const [, url] = match;
|
||||
return SourceMap(url);
|
||||
}
|
||||
return SourceMap();
|
||||
}
|
||||
193
packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts
Normal file
193
packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import type { LineRange, MappedPosition } from "source-map-js";
|
||||
import { SourceMapConsumer } from "source-map-js";
|
||||
|
||||
export type LocationRequest = {
|
||||
line?: number;
|
||||
column?: number;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export type Location = {
|
||||
line: number; // 0-based
|
||||
column: number; // 0-based
|
||||
} & (
|
||||
| {
|
||||
verified: true;
|
||||
}
|
||||
| {
|
||||
verified?: false;
|
||||
message?: string;
|
||||
}
|
||||
);
|
||||
|
||||
export interface SourceMap {
|
||||
generatedLocation(request: LocationRequest): Location;
|
||||
originalLocation(request: LocationRequest): Location;
|
||||
}
|
||||
|
||||
class ActualSourceMap implements SourceMap {
|
||||
#sourceMap: SourceMapConsumer;
|
||||
#sources: string[];
|
||||
|
||||
constructor(sourceMap: SourceMapConsumer) {
|
||||
this.#sourceMap = sourceMap;
|
||||
this.#sources = (sourceMap as any)._absoluteSources;
|
||||
}
|
||||
|
||||
#getSource(url?: string): string {
|
||||
const sources = this.#sources;
|
||||
if (!sources.length) {
|
||||
return "";
|
||||
}
|
||||
if (sources.length === 1 || !url) {
|
||||
return sources[0];
|
||||
}
|
||||
for (const source of sources) {
|
||||
if (url.endsWith(source)) {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
generatedLocation(request: LocationRequest): Location {
|
||||
const { line, column, url } = request;
|
||||
|
||||
let lineRange: LineRange;
|
||||
try {
|
||||
const source = this.#getSource(url);
|
||||
lineRange = this.#sourceMap.generatedPositionFor({
|
||||
line: lineTo1BasedLine(line),
|
||||
column: columnToColumn(column),
|
||||
source,
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
column: columnToColumn(column),
|
||||
verified: false,
|
||||
message: unknownToError(error),
|
||||
};
|
||||
}
|
||||
|
||||
if (!locationIsValid(lineRange)) {
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
column: columnToColumn(column),
|
||||
verified: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { line: gline, column: gcolumn } = lineRange;
|
||||
return {
|
||||
line: lineToLine(gline),
|
||||
column: columnToColumn(gcolumn),
|
||||
verified: true,
|
||||
};
|
||||
}
|
||||
|
||||
originalLocation(request: LocationRequest): Location {
|
||||
const { line, column } = request;
|
||||
|
||||
let mappedPosition: MappedPosition;
|
||||
try {
|
||||
mappedPosition = this.#sourceMap.originalPositionFor({
|
||||
line: lineTo1BasedLine(line),
|
||||
column: columnToColumn(column),
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
column: columnToColumn(column),
|
||||
verified: false,
|
||||
message: unknownToError(error),
|
||||
};
|
||||
}
|
||||
|
||||
if (!locationIsValid(mappedPosition)) {
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
column: columnToColumn(column),
|
||||
verified: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { line: oline, column: ocolumn } = mappedPosition;
|
||||
return {
|
||||
line: lineTo0BasedLine(oline),
|
||||
column: columnToColumn(ocolumn),
|
||||
verified: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class NoopSourceMap implements SourceMap {
|
||||
generatedLocation(request: LocationRequest): Location {
|
||||
const { line, column } = request;
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
column: columnToColumn(column),
|
||||
verified: true,
|
||||
};
|
||||
}
|
||||
|
||||
originalLocation(request: LocationRequest): Location {
|
||||
const { line, column } = request;
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
column: columnToColumn(column),
|
||||
verified: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSourceMap = new NoopSourceMap();
|
||||
|
||||
export function SourceMap(url?: string): SourceMap {
|
||||
if (!url || !url.startsWith("data:")) {
|
||||
return defaultSourceMap;
|
||||
}
|
||||
try {
|
||||
const [_, base64] = url.split(",", 2);
|
||||
const decoded = Buffer.from(base64, "base64url").toString("utf8");
|
||||
const schema = JSON.parse(decoded);
|
||||
const sourceMap = new SourceMapConsumer(schema);
|
||||
return new ActualSourceMap(sourceMap);
|
||||
} catch (error) {
|
||||
console.warn("Failed to parse source map URL", url);
|
||||
}
|
||||
return defaultSourceMap;
|
||||
}
|
||||
|
||||
function lineTo1BasedLine(line?: number): number {
|
||||
return numberIsValid(line) ? line + 1 : 1;
|
||||
}
|
||||
|
||||
function lineTo0BasedLine(line?: number): number {
|
||||
return numberIsValid(line) ? line - 1 : 0;
|
||||
}
|
||||
|
||||
function lineToLine(line?: number): number {
|
||||
return numberIsValid(line) ? line : 0;
|
||||
}
|
||||
|
||||
function columnToColumn(column?: number): number {
|
||||
return numberIsValid(column) ? column : 0;
|
||||
}
|
||||
|
||||
function locationIsValid(location: Location): location is Location {
|
||||
const { line, column } = location;
|
||||
return numberIsValid(line) && numberIsValid(column);
|
||||
}
|
||||
|
||||
function numberIsValid(number?: number): number is number {
|
||||
return typeof number === "number" && isFinite(number) && number >= 0;
|
||||
}
|
||||
|
||||
function unknownToError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
const { message } = error;
|
||||
return message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
2696
packages/bun-debug-adapter-protocol/src/protocol/index.d.ts
vendored
Normal file
2696
packages/bun-debug-adapter-protocol/src/protocol/index.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3761
packages/bun-debug-adapter-protocol/src/protocol/protocol.json
Normal file
3761
packages/bun-debug-adapter-protocol/src/protocol/protocol.json
Normal file
File diff suppressed because it is too large
Load Diff
37
packages/bun-debug-adapter-protocol/src/protocol/schema.d.ts
vendored
Normal file
37
packages/bun-debug-adapter-protocol/src/protocol/schema.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
export type Protocol = {
|
||||
$schema: string;
|
||||
title: string;
|
||||
description: string;
|
||||
type: "object";
|
||||
definitions: Record<string, Type>;
|
||||
};
|
||||
|
||||
export type Type = {
|
||||
description?: string;
|
||||
} & (
|
||||
| {
|
||||
type: "number" | "integer" | "boolean";
|
||||
}
|
||||
| {
|
||||
type: "string";
|
||||
enum?: string[];
|
||||
enumDescriptions?: string[];
|
||||
}
|
||||
| {
|
||||
type: "object";
|
||||
properties?: Record<string, Type>;
|
||||
required?: string[];
|
||||
}
|
||||
| {
|
||||
type: "array";
|
||||
items?: Type;
|
||||
}
|
||||
| {
|
||||
type?: undefined;
|
||||
$ref: string;
|
||||
}
|
||||
| {
|
||||
type?: undefined;
|
||||
allOf: Type[];
|
||||
}
|
||||
);
|
||||
21
packages/bun-debug-adapter-protocol/tsconfig.json
Normal file
21
packages/bun-debug-adapter-protocol/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "nodenext",
|
||||
"moduleDetection": "force",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSourceMap": true,
|
||||
"allowJs": true,
|
||||
"outDir": "dist",
|
||||
},
|
||||
"include": ["src", "scripts", "../bun-types/index.d.ts", "../bun-inspector-protocol/src"]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "bun-ecosystem-ci",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"globby": "^13.1.3"
|
||||
@@ -11,4 +12,4 @@
|
||||
"format": "prettier --write src",
|
||||
"test": "bun run src/runner.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# web-inspector-bun
|
||||
# bun-devtools-frontend
|
||||
|
||||
This is the WebKit Web Inspector bundled as standalone assets.
|
||||
|
||||
@@ -17,7 +17,12 @@ try {
|
||||
.on("script", {
|
||||
element(element) {
|
||||
const src = element.getAttribute("src");
|
||||
if (src && !src?.includes("External") && !src?.includes("WebKitAdditions")) {
|
||||
if (
|
||||
src &&
|
||||
!src?.includes("External") &&
|
||||
!src?.includes("WebKitAdditions") &&
|
||||
!src.includes("DOMUtilities.js")
|
||||
) {
|
||||
if (scriptsToBundle.length === 0) {
|
||||
element.replace("<script>var WI = {};\n</script>", { html: true });
|
||||
} else {
|
||||
@@ -35,7 +40,40 @@ try {
|
||||
})
|
||||
.on("head", {
|
||||
element(element) {
|
||||
element.prepend(` <base href="/" /> `, { html: true });
|
||||
element.prepend(
|
||||
`
|
||||
<script type="text/javascript">
|
||||
if (!Element.prototype.scrollIntoViewIfNeeded) {
|
||||
Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) {
|
||||
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
|
||||
|
||||
var parent = this.parentNode,
|
||||
parentComputedStyle = window.getComputedStyle(parent, null),
|
||||
parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
|
||||
parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
|
||||
overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
|
||||
overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
|
||||
overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
|
||||
overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
|
||||
alignWithTop = overTop && !overBottom;
|
||||
|
||||
if ((overTop || overBottom) && centerIfNeeded) {
|
||||
parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
|
||||
}
|
||||
|
||||
if ((overLeft || overRight) && centerIfNeeded) {
|
||||
parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2;
|
||||
}
|
||||
|
||||
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
|
||||
this.scrollIntoView(alignWithTop);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<base href="/" /> `,
|
||||
{ html: true },
|
||||
);
|
||||
|
||||
element.append(
|
||||
`
|
||||
@@ -46,7 +84,7 @@ try {
|
||||
</style>
|
||||
<script src="${jsReplacementId}"></script>
|
||||
|
||||
<script>
|
||||
<script type="text/javascript">
|
||||
WI.sharedApp = new WI.AppController;
|
||||
WI.sharedApp.initialize();
|
||||
</script>`,
|
||||
@@ -71,6 +109,9 @@ try {
|
||||
const javascript = scriptsToBundle.map(a => `import '${join(basePath, a)}';`).join("\n") + "\n";
|
||||
// const css = stylesToBundle.map(a => `@import "${join(basePath, a)}";`).join("\n") + "\n";
|
||||
await Bun.write(join(import.meta.dir, "out/manifest.js"), javascript);
|
||||
mkdirSync("out/WebKitAdditions/WebInspectorUI/", { recursive: true });
|
||||
await Bun.write(join(import.meta.dir, "out/WebKitAdditions/WebInspectorUI/WebInspectorUIAdditions.js"), "");
|
||||
await Bun.write(join(import.meta.dir, "out/WebKitAdditions/WebInspectorUI/WebInspectorUIAdditions.css"), "");
|
||||
// await Bun.write(join(import.meta.dir, "manifest.css"), css);
|
||||
const jsBundle = await Bun.build({
|
||||
entrypoints: [join(import.meta.dir, "out/manifest.js")],
|
||||
20
packages/bun-inspector-frontend/tsconfig.json
Normal file
20
packages/bun-inspector-frontend/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "nodenext",
|
||||
"moduleDetection": "force",
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSourceMap": true,
|
||||
"allowJs": true,
|
||||
"noImplicitAny": false,
|
||||
"outDir": "dist",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [".", "../bun-types/index.d.ts"]
|
||||
}
|
||||
2
packages/bun-inspector-protocol/.gitattributes
vendored
Normal file
2
packages/bun-inspector-protocol/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
protocol/*/protocol.json linguist-generated=true
|
||||
protocol/*/index.d.ts linguist-generated=true
|
||||
2
packages/bun-inspector-protocol/.gitignore
vendored
Normal file
2
packages/bun-inspector-protocol/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
protocol/*.json
|
||||
protocol/v8
|
||||
1
packages/bun-inspector-protocol/README.md
Normal file
1
packages/bun-inspector-protocol/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# bun-inspector-protocol
|
||||
BIN
packages/bun-inspector-protocol/bun.lockb
Executable file
BIN
packages/bun-inspector-protocol/bun.lockb
Executable file
Binary file not shown.
4
packages/bun-inspector-protocol/index.ts
Normal file
4
packages/bun-inspector-protocol/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type * from "./src/protocol";
|
||||
export type * from "./src/inspector";
|
||||
export * from "./src/util/preview";
|
||||
export * from "./src/inspector/websocket";
|
||||
7
packages/bun-inspector-protocol/package.json
Normal file
7
packages/bun-inspector-protocol/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "bun-inspector-protocol",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
}
|
||||
202
packages/bun-inspector-protocol/scripts/generate-protocol.ts
Normal file
202
packages/bun-inspector-protocol/scripts/generate-protocol.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import type { Protocol, Domain, Property } from "../src/protocol/schema";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
run().catch(console.error);
|
||||
|
||||
async function run() {
|
||||
const cwd = new URL("../protocol/", import.meta.url);
|
||||
const runner = "Bun" in globalThis ? "bunx" : "npx";
|
||||
const write = (name: string, data: string) => {
|
||||
const path = new URL(name, cwd);
|
||||
writeFileSync(path, data);
|
||||
spawnSync(runner, ["prettier", "--write", path.pathname], { cwd, stdio: "ignore" });
|
||||
};
|
||||
const base = readFileSync(new URL("protocol.d.ts", cwd), "utf-8");
|
||||
const baseNoComments = base.replace(/\/\/.*/g, "");
|
||||
const jsc = await downloadJsc();
|
||||
write("jsc/protocol.json", JSON.stringify(jsc));
|
||||
write("jsc/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(jsc, baseNoComments));
|
||||
const v8 = await downloadV8();
|
||||
write("v8/protocol.json", JSON.stringify(v8));
|
||||
write("v8/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(v8, baseNoComments));
|
||||
}
|
||||
|
||||
function formatProtocol(protocol: Protocol, extraTs?: string): string {
|
||||
const { name, domains } = protocol;
|
||||
const eventMap = new Map();
|
||||
const commandMap = new Map();
|
||||
let body = `export namespace ${name} {`;
|
||||
for (const { domain, types = [], events = [], commands = [] } of domains) {
|
||||
body += `export namespace ${domain} {`;
|
||||
for (const type of types) {
|
||||
body += formatProperty(type);
|
||||
}
|
||||
for (const { name, description, parameters = [] } of events) {
|
||||
const symbol = `${domain}.${name}`;
|
||||
const title = toTitle(name);
|
||||
eventMap.set(symbol, `${domain}.${title}`);
|
||||
body += formatProperty({
|
||||
id: `${title}Event`,
|
||||
type: "object",
|
||||
description: `${description}\n@event \`${symbol}\``,
|
||||
properties: parameters,
|
||||
});
|
||||
}
|
||||
for (const { name, description, parameters = [], returns = [] } of commands) {
|
||||
const symbol = `${domain}.${name}`;
|
||||
const title = toTitle(name);
|
||||
commandMap.set(symbol, `${domain}.${title}`);
|
||||
body += formatProperty({
|
||||
id: `${title}Request`,
|
||||
type: "object",
|
||||
description: `${description}\n@request \`${symbol}\``,
|
||||
properties: parameters,
|
||||
});
|
||||
body += formatProperty({
|
||||
id: `${title}Response`,
|
||||
type: "object",
|
||||
description: `${description}\n@response \`${symbol}\``,
|
||||
properties: returns,
|
||||
});
|
||||
}
|
||||
body += "};";
|
||||
}
|
||||
for (const type of ["Event", "Request", "Response"]) {
|
||||
const sourceMap = type === "Event" ? eventMap : commandMap;
|
||||
body += formatProperty({
|
||||
id: `${type}Map`,
|
||||
type: "object",
|
||||
properties: [...sourceMap.entries()].map(([name, title]) => ({
|
||||
name: `"${name}"`,
|
||||
type: undefined,
|
||||
$ref: `${title}${type}`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
if (extraTs) {
|
||||
body += extraTs;
|
||||
}
|
||||
return body + "};";
|
||||
}
|
||||
|
||||
function formatProperty(property: Property): string {
|
||||
const { id, description, type, optional } = property;
|
||||
let body = "";
|
||||
if (id) {
|
||||
if (description) {
|
||||
body += `\n${toComment(description)}\n`;
|
||||
}
|
||||
body += `export type ${id}=`;
|
||||
}
|
||||
if (type === "boolean") {
|
||||
body += "boolean";
|
||||
} else if (type === "number" || type === "integer") {
|
||||
body += "number";
|
||||
} else if (type === "string") {
|
||||
const { enum: choices } = property;
|
||||
if (choices) {
|
||||
body += choices.map(value => `"${value}"`).join("|");
|
||||
} else {
|
||||
body += "string";
|
||||
}
|
||||
} else if (type === "array") {
|
||||
const { items } = property;
|
||||
const itemType = items ? formatProperty(items) : "unknown";
|
||||
body += `${itemType}[]`;
|
||||
} else if (type === "object") {
|
||||
const { properties } = property;
|
||||
if (!properties) {
|
||||
body += "Record<string, unknown>";
|
||||
} else if (properties.length === 0) {
|
||||
body += "{}";
|
||||
} else {
|
||||
body += "{";
|
||||
for (const { name, description, ...property } of properties) {
|
||||
if (description) {
|
||||
body += `\n${toComment(description)}`;
|
||||
}
|
||||
const delimit = property.optional ? "?:" : ":";
|
||||
body += `\n${name}${delimit}${formatProperty({ ...property, id: undefined })};`;
|
||||
}
|
||||
body += "}";
|
||||
}
|
||||
} else if ("$ref" in property) {
|
||||
body += property.$ref;
|
||||
} else {
|
||||
body += "unknown";
|
||||
}
|
||||
if (optional) {
|
||||
body += "|undefined";
|
||||
}
|
||||
if (id) {
|
||||
body += ";";
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://github.com/ChromeDevTools/devtools-protocol/tree/master/json
|
||||
*/
|
||||
async function downloadV8(): Promise<Protocol> {
|
||||
const baseUrl = "https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json";
|
||||
const domains = ["Runtime", "Console", "Debugger", "Memory", "HeapProfiler", "Profiler", "Network", "Inspector"];
|
||||
return Promise.all([
|
||||
download<Protocol>(`${baseUrl}/js_protocol.json`),
|
||||
download<Protocol>(`${baseUrl}/browser_protocol.json`),
|
||||
]).then(([js, browser]) => ({
|
||||
name: "V8",
|
||||
version: js.version,
|
||||
domains: [...js.domains, ...browser.domains]
|
||||
.filter(domain => !domains.includes(domain.domain))
|
||||
.sort((a, b) => a.domain.localeCompare(b.domain)),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://github.com/WebKit/WebKit/tree/main/Source/JavaScriptCore/inspector/protocol
|
||||
*/
|
||||
async function downloadJsc(): Promise<Protocol> {
|
||||
const baseUrl = "https://raw.githubusercontent.com/WebKit/WebKit/main/Source/JavaScriptCore/inspector/protocol";
|
||||
const domains = [
|
||||
"Runtime",
|
||||
"Console",
|
||||
"Debugger",
|
||||
"Heap",
|
||||
"ScriptProfiler",
|
||||
"CPUProfiler",
|
||||
"GenericTypes",
|
||||
"Network",
|
||||
"Inspector",
|
||||
];
|
||||
return {
|
||||
name: "JSC",
|
||||
version: {
|
||||
major: 1,
|
||||
minor: 3,
|
||||
},
|
||||
domains: await Promise.all(domains.map(domain => download<Domain>(`${baseUrl}/${domain}.json`))).then(domains =>
|
||||
domains.sort((a, b) => a.domain.localeCompare(b.domain)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function download<V>(url: string): Promise<V> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status}: ${url}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function toTitle(name: string): string {
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
}
|
||||
|
||||
function toComment(description?: string): string {
|
||||
if (!description) {
|
||||
return "";
|
||||
}
|
||||
const lines = ["/**", ...description.split("\n").map(line => ` * ${line.trim()}`), "*/"];
|
||||
return lines.join("\n");
|
||||
}
|
||||
40
packages/bun-inspector-protocol/src/inspector/index.d.ts
vendored
Normal file
40
packages/bun-inspector-protocol/src/inspector/index.d.ts
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { EventEmitter } from "node:events";
|
||||
import type { JSC } from "../protocol";
|
||||
|
||||
export type InspectorEventMap = {
|
||||
[E in keyof JSC.EventMap]: [JSC.EventMap[E]];
|
||||
} & {
|
||||
"Inspector.connecting": [string];
|
||||
"Inspector.connected": [];
|
||||
"Inspector.disconnected": [Error | undefined];
|
||||
"Inspector.error": [Error];
|
||||
"Inspector.pendingRequest": [JSC.Request];
|
||||
"Inspector.request": [JSC.Request];
|
||||
"Inspector.response": [JSC.Response];
|
||||
"Inspector.event": [JSC.Event];
|
||||
};
|
||||
|
||||
/**
|
||||
* A client that can send and receive messages to/from a debugger.
|
||||
*/
|
||||
export interface Inspector extends EventEmitter<InspectorEventMap> {
|
||||
/**
|
||||
* Starts the inspector.
|
||||
*/
|
||||
start(...args: unknown[]): Promise<boolean>;
|
||||
/**
|
||||
* Sends a request to the debugger.
|
||||
*/
|
||||
send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
|
||||
method: M,
|
||||
params?: JSC.RequestMap[M],
|
||||
): Promise<JSC.ResponseMap[M]>;
|
||||
/**
|
||||
* If the inspector is closed.
|
||||
*/
|
||||
get closed(): boolean;
|
||||
/**
|
||||
* Closes the inspector.
|
||||
*/
|
||||
close(...args: unknown[]): void;
|
||||
}
|
||||
239
packages/bun-inspector-protocol/src/inspector/websocket.ts
Normal file
239
packages/bun-inspector-protocol/src/inspector/websocket.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import type { Inspector, InspectorEventMap } from ".";
|
||||
import type { JSC } from "../protocol";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { WebSocket } from "ws";
|
||||
|
||||
/**
|
||||
* An inspector that communicates with a debugger over a WebSocket.
|
||||
*/
|
||||
export class WebSocketInspector extends EventEmitter<InspectorEventMap> implements Inspector {
|
||||
#url?: string;
|
||||
#webSocket?: WebSocket;
|
||||
#ready: Promise<boolean> | undefined;
|
||||
#requestId: number;
|
||||
#pendingRequests: JSC.Request[];
|
||||
#pendingResponses: Map<number, (result: unknown) => void>;
|
||||
|
||||
constructor(url?: string | URL) {
|
||||
super();
|
||||
this.#url = url ? String(url) : undefined;
|
||||
this.#requestId = 1;
|
||||
this.#pendingRequests = [];
|
||||
this.#pendingResponses = new Map();
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
return this.#url!;
|
||||
}
|
||||
|
||||
async start(url?: string | URL): Promise<boolean> {
|
||||
if (url) {
|
||||
this.#url = String(url);
|
||||
}
|
||||
|
||||
if (!this.#url) {
|
||||
this.emit("Inspector.error", new Error("Inspector needs a URL, but none was provided"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.#connect(this.#url);
|
||||
}
|
||||
|
||||
async #connect(url: string): Promise<boolean> {
|
||||
if (this.#ready) {
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
this.close(1001, "Restarting...");
|
||||
this.emit("Inspector.connecting", url);
|
||||
|
||||
let webSocket: WebSocket;
|
||||
try {
|
||||
// @ts-expect-error: Support both Bun and Node.js version of `headers`.
|
||||
webSocket = new WebSocket(url, {
|
||||
headers: {
|
||||
"Ref-Event-Loop": "1",
|
||||
},
|
||||
finishRequest: (request: import("http").ClientRequest) => {
|
||||
request.setHeader("Ref-Event-Loop", "1");
|
||||
request.end();
|
||||
},
|
||||
});
|
||||
} catch (cause) {
|
||||
this.#close(unknownToError(cause));
|
||||
return false;
|
||||
}
|
||||
|
||||
webSocket.addEventListener("open", () => {
|
||||
this.emit("Inspector.connected");
|
||||
|
||||
for (const request of this.#pendingRequests) {
|
||||
if (this.#send(request)) {
|
||||
this.emit("Inspector.request", request);
|
||||
}
|
||||
}
|
||||
|
||||
this.#pendingRequests.length = 0;
|
||||
});
|
||||
|
||||
webSocket.addEventListener("message", ({ data }) => {
|
||||
if (typeof data === "string") {
|
||||
this.#accept(data);
|
||||
}
|
||||
});
|
||||
|
||||
webSocket.addEventListener("error", event => {
|
||||
this.#close(unknownToError(event));
|
||||
});
|
||||
|
||||
webSocket.addEventListener("unexpected-response", () => {
|
||||
this.#close(new Error("WebSocket upgrade failed"));
|
||||
});
|
||||
|
||||
webSocket.addEventListener("close", ({ code, reason }) => {
|
||||
if (code === 1001 || code === 1006) {
|
||||
this.#close();
|
||||
return;
|
||||
}
|
||||
this.#close(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd()));
|
||||
});
|
||||
|
||||
this.#webSocket = webSocket;
|
||||
|
||||
const ready = new Promise<boolean>(resolve => {
|
||||
webSocket.addEventListener("open", () => resolve(true));
|
||||
webSocket.addEventListener("close", () => resolve(false));
|
||||
webSocket.addEventListener("error", () => resolve(false));
|
||||
}).finally(() => {
|
||||
this.#ready = undefined;
|
||||
});
|
||||
|
||||
this.#ready = ready;
|
||||
|
||||
return ready;
|
||||
}
|
||||
|
||||
send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
|
||||
method: M,
|
||||
params?: JSC.RequestMap[M] | undefined,
|
||||
): Promise<JSC.ResponseMap[M]> {
|
||||
const id = this.#requestId++;
|
||||
const request = {
|
||||
id,
|
||||
method,
|
||||
params: params ?? {},
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const done = (result: any) => {
|
||||
this.#pendingResponses.delete(id);
|
||||
if (result instanceof Error) {
|
||||
reject(result);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
|
||||
this.#pendingResponses.set(id, done);
|
||||
if (this.#send(request)) {
|
||||
this.emit("Inspector.request", request);
|
||||
} else {
|
||||
this.emit("Inspector.pendingRequest", request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#send(request: JSC.Request): boolean {
|
||||
if (this.#webSocket) {
|
||||
const { readyState } = this.#webSocket!;
|
||||
if (readyState === WebSocket.OPEN) {
|
||||
this.#webSocket.send(JSON.stringify(request));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.#pendingRequests.includes(request)) {
|
||||
this.#pendingRequests.push(request);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#accept(message: string): void {
|
||||
let data: JSC.Event | JSC.Response;
|
||||
try {
|
||||
data = JSON.parse(message);
|
||||
} catch (cause) {
|
||||
this.emit("Inspector.error", new Error(`Failed to parse message: ${message}`, { cause }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!("id" in data)) {
|
||||
this.emit("Inspector.event", data);
|
||||
const { method, params } = data;
|
||||
this.emit(method, params);
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit("Inspector.response", data);
|
||||
|
||||
const { id } = data;
|
||||
const resolve = this.#pendingResponses.get(id);
|
||||
if (!resolve) {
|
||||
this.emit("Inspector.error", new Error(`Failed to find matching request for ID: ${id}`));
|
||||
return;
|
||||
}
|
||||
|
||||
this.#pendingResponses.delete(id);
|
||||
if ("error" in data) {
|
||||
const { error } = data;
|
||||
const { message } = error;
|
||||
resolve(new Error(message));
|
||||
} else {
|
||||
const { result } = data;
|
||||
resolve(result);
|
||||
}
|
||||
}
|
||||
|
||||
get closed(): boolean {
|
||||
if (!this.#webSocket) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { readyState } = this.#webSocket;
|
||||
switch (readyState) {
|
||||
case WebSocket.CLOSED:
|
||||
case WebSocket.CLOSING:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
close(code?: number, reason?: string): void {
|
||||
this.#webSocket?.close(code ?? 1001, reason);
|
||||
}
|
||||
|
||||
#close(error?: Error): void {
|
||||
for (const resolve of this.#pendingResponses.values()) {
|
||||
resolve(error ?? new Error("WebSocket closed"));
|
||||
}
|
||||
this.#pendingResponses.clear();
|
||||
if (error) {
|
||||
this.emit("Inspector.error", error);
|
||||
}
|
||||
this.emit("Inspector.disconnected", error);
|
||||
}
|
||||
}
|
||||
|
||||
function unknownToError(input: unknown): Error {
|
||||
if (input instanceof Error) {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (typeof input === "object" && input !== null && "message" in input) {
|
||||
const { message } = input;
|
||||
return new Error(`${message}`);
|
||||
}
|
||||
|
||||
return new Error(`${input}`);
|
||||
}
|
||||
1
packages/bun-inspector-protocol/src/protocol/index.d.ts
vendored
Normal file
1
packages/bun-inspector-protocol/src/protocol/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export type { JSC } from "./jsc";
|
||||
3695
packages/bun-inspector-protocol/src/protocol/jsc/index.d.ts
vendored
Normal file
3695
packages/bun-inspector-protocol/src/protocol/jsc/index.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3114
packages/bun-inspector-protocol/src/protocol/jsc/protocol.json
Normal file
3114
packages/bun-inspector-protocol/src/protocol/jsc/protocol.json
Normal file
File diff suppressed because it is too large
Load Diff
28
packages/bun-inspector-protocol/src/protocol/protocol.d.ts
vendored
Normal file
28
packages/bun-inspector-protocol/src/protocol/protocol.d.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
// @ts-nocheck
|
||||
// The content of this file is included in each generated protocol file.
|
||||
|
||||
export type Event<T extends keyof EventMap = keyof EventMap> = {
|
||||
readonly method: T;
|
||||
readonly params: EventMap[T];
|
||||
};
|
||||
|
||||
export type Request<T extends keyof RequestMap = keyof RequestMap> = {
|
||||
readonly id: number;
|
||||
readonly method: T;
|
||||
readonly params: RequestMap[T];
|
||||
};
|
||||
|
||||
export type Response<T extends keyof ResponseMap = keyof ResponseMap> = {
|
||||
readonly id: number;
|
||||
} & (
|
||||
| {
|
||||
readonly method?: T;
|
||||
readonly result: ResponseMap[T];
|
||||
}
|
||||
| {
|
||||
readonly error: {
|
||||
readonly code?: string;
|
||||
readonly message: string;
|
||||
};
|
||||
}
|
||||
);
|
||||
58
packages/bun-inspector-protocol/src/protocol/schema.d.ts
vendored
Normal file
58
packages/bun-inspector-protocol/src/protocol/schema.d.ts
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
// Represents the schema of the protocol.json file.
|
||||
|
||||
export type Protocol = {
|
||||
readonly name: string;
|
||||
readonly version: {
|
||||
readonly major: number;
|
||||
readonly minor: number;
|
||||
};
|
||||
readonly domains: readonly Domain[];
|
||||
};
|
||||
|
||||
export type Domain = {
|
||||
readonly domain: string;
|
||||
readonly dependencies?: readonly string[];
|
||||
readonly types: readonly Property[];
|
||||
readonly commands?: readonly Command[];
|
||||
readonly events?: readonly Event[];
|
||||
};
|
||||
|
||||
export type Command = {
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly parameters?: readonly Property[];
|
||||
readonly returns?: readonly Property[];
|
||||
};
|
||||
|
||||
export type Event = {
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly parameters: readonly Property[];
|
||||
};
|
||||
|
||||
export type Property = {
|
||||
readonly id?: string;
|
||||
readonly name?: string;
|
||||
readonly description?: string;
|
||||
readonly optional?: boolean;
|
||||
} & (
|
||||
| {
|
||||
readonly type: "array";
|
||||
readonly items?: Property;
|
||||
}
|
||||
| {
|
||||
readonly type: "object";
|
||||
readonly properties?: readonly Property[];
|
||||
}
|
||||
| {
|
||||
readonly type: "string";
|
||||
readonly enum?: readonly string[];
|
||||
}
|
||||
| {
|
||||
readonly type: "boolean" | "number" | "integer";
|
||||
}
|
||||
| {
|
||||
readonly type: undefined;
|
||||
readonly $ref: string;
|
||||
}
|
||||
);
|
||||
17428
packages/bun-inspector-protocol/src/protocol/v8/index.d.ts
vendored
Normal file
17428
packages/bun-inspector-protocol/src/protocol/v8/index.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
14136
packages/bun-inspector-protocol/src/protocol/v8/protocol.json
Normal file
14136
packages/bun-inspector-protocol/src/protocol/v8/protocol.json
Normal file
File diff suppressed because it is too large
Load Diff
113
packages/bun-inspector-protocol/src/util/preview.ts
Normal file
113
packages/bun-inspector-protocol/src/util/preview.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { JSC } from "../protocol";
|
||||
|
||||
export function remoteObjectToString(remoteObject: JSC.Runtime.RemoteObject, topLevel?: boolean): string {
|
||||
const { type, subtype, value, description, className, preview } = remoteObject;
|
||||
switch (type) {
|
||||
case "undefined":
|
||||
return "undefined";
|
||||
case "boolean":
|
||||
case "number":
|
||||
return description ?? JSON.stringify(value);
|
||||
case "string":
|
||||
if (topLevel) {
|
||||
return String(value ?? description);
|
||||
}
|
||||
return JSON.stringify(value ?? description);
|
||||
case "symbol":
|
||||
case "bigint":
|
||||
return description!;
|
||||
case "function":
|
||||
return description!.replace("function", "ƒ") || "ƒ";
|
||||
}
|
||||
switch (subtype) {
|
||||
case "null":
|
||||
return "null";
|
||||
case "regexp":
|
||||
case "date":
|
||||
case "error":
|
||||
return description!;
|
||||
}
|
||||
if (preview) {
|
||||
return objectPreviewToString(preview);
|
||||
}
|
||||
if (className) {
|
||||
return className;
|
||||
}
|
||||
return description || "Object";
|
||||
}
|
||||
|
||||
export function objectPreviewToString(objectPreview: JSC.Runtime.ObjectPreview): string {
|
||||
const { type, subtype, entries, properties, overflow, description, size } = objectPreview;
|
||||
if (type !== "object") {
|
||||
return remoteObjectToString(objectPreview);
|
||||
}
|
||||
let items: string[];
|
||||
if (entries) {
|
||||
items = entries.map(entryPreviewToString).sort();
|
||||
} else if (properties) {
|
||||
if (isIndexed(subtype)) {
|
||||
items = properties.map(indexedPropertyPreviewToString).sort();
|
||||
} else {
|
||||
items = properties.map(namedPropertyPreviewToString).sort();
|
||||
}
|
||||
} else {
|
||||
items = ["…"];
|
||||
}
|
||||
if (overflow) {
|
||||
items.push("…");
|
||||
}
|
||||
let label: string;
|
||||
if (description === "Object") {
|
||||
label = "";
|
||||
} else if (size === undefined) {
|
||||
label = description!;
|
||||
} else {
|
||||
label = `${description}(${size})`;
|
||||
}
|
||||
if (!items.length) {
|
||||
return label || "{}";
|
||||
}
|
||||
if (label) {
|
||||
label += " ";
|
||||
}
|
||||
if (isIndexed(subtype)) {
|
||||
return `${label}[${items.join(", ")}]`;
|
||||
}
|
||||
return `${label}{${items.join(", ")}}`;
|
||||
}
|
||||
|
||||
function propertyPreviewToString(propertyPreview: JSC.Runtime.PropertyPreview): string {
|
||||
const { type, value, ...preview } = propertyPreview;
|
||||
if (type === "accessor") {
|
||||
return "ƒ";
|
||||
}
|
||||
return remoteObjectToString({ ...preview, type, description: value });
|
||||
}
|
||||
|
||||
function entryPreviewToString(entryPreview: JSC.Runtime.EntryPreview): string {
|
||||
const { key, value } = entryPreview;
|
||||
if (key) {
|
||||
return `${objectPreviewToString(key)} => ${objectPreviewToString(value)}`;
|
||||
}
|
||||
return objectPreviewToString(value);
|
||||
}
|
||||
|
||||
function namedPropertyPreviewToString(propertyPreview: JSC.Runtime.PropertyPreview): string {
|
||||
const { name, valuePreview } = propertyPreview;
|
||||
if (valuePreview) {
|
||||
return `${name}: ${objectPreviewToString(valuePreview)}`;
|
||||
}
|
||||
return `${name}: ${propertyPreviewToString(propertyPreview)}`;
|
||||
}
|
||||
|
||||
function indexedPropertyPreviewToString(propertyPreview: JSC.Runtime.PropertyPreview): string {
|
||||
const { valuePreview } = propertyPreview;
|
||||
if (valuePreview) {
|
||||
return objectPreviewToString(valuePreview);
|
||||
}
|
||||
return propertyPreviewToString(propertyPreview);
|
||||
}
|
||||
|
||||
function isIndexed(type?: JSC.Runtime.RemoteObject["subtype"]): boolean {
|
||||
return type === "array" || type === "set" || type === "weakset";
|
||||
}
|
||||
190
packages/bun-inspector-protocol/test/inspector/websocket.test.ts
Normal file
190
packages/bun-inspector-protocol/test/inspector/websocket.test.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { describe, test, expect, mock, beforeAll, afterAll } from "bun:test";
|
||||
import { WebSocketInspector } from "../../src/inspector/websocket";
|
||||
import type { Server } from "bun";
|
||||
import { serve } from "bun";
|
||||
|
||||
let server: Server;
|
||||
let url: URL;
|
||||
|
||||
describe("WebSocketInspector", () => {
|
||||
test("fails without a URL", () => {
|
||||
const ws = new WebSocketInspector();
|
||||
const fn = mock(error => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
ws.on("Inspector.error", fn);
|
||||
expect(ws.start()).resolves.toBeFalse();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("fails with invalid URL", () => {
|
||||
const ws = new WebSocketInspector("notaurl");
|
||||
const fn = mock(error => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
ws.on("Inspector.error", fn);
|
||||
expect(ws.start()).resolves.toBeFalse();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("fails with valid URL but no server", () => {
|
||||
const ws = new WebSocketInspector("ws://localhost:0/doesnotexist/");
|
||||
const fn = mock(error => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
ws.on("Inspector.error", fn);
|
||||
expect(ws.start()).resolves.toBeFalse();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("fails with invalid upgrade response", () => {
|
||||
const ws = new WebSocketInspector(new URL("/", url));
|
||||
const fn = mock(error => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
ws.on("Inspector.error", fn);
|
||||
expect(ws.start()).resolves.toBeFalse();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("can connect to a server", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn = mock(() => {
|
||||
expect(ws.closed).toBe(false);
|
||||
});
|
||||
ws.on("Inspector.connected", fn);
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("can disconnect from a server", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn = mock(() => {
|
||||
expect(ws.closed).toBeTrue();
|
||||
});
|
||||
ws.on("Inspector.disconnected", fn);
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
ws.close();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("can connect to a server multiple times", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn0 = mock(() => {
|
||||
expect(ws.closed).toBeFalse();
|
||||
});
|
||||
ws.on("Inspector.connected", fn0);
|
||||
const fn1 = mock(() => {
|
||||
expect(ws.closed).toBeTrue();
|
||||
});
|
||||
ws.on("Inspector.disconnected", fn1);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
ws.close();
|
||||
}
|
||||
expect(fn0).toHaveBeenCalledTimes(3);
|
||||
expect(fn1).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("can send a request", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn0 = mock(request => {
|
||||
expect(request).toStrictEqual({
|
||||
id: 1,
|
||||
method: "Debugger.setPauseOnAssertions",
|
||||
params: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.request", fn0);
|
||||
const fn1 = mock(response => {
|
||||
expect(response).toStrictEqual({
|
||||
id: 1,
|
||||
result: {
|
||||
ok: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.response", fn1);
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
expect(ws.send("Debugger.setPauseOnAssertions", { enabled: true })).resolves.toMatchObject({ ok: true });
|
||||
expect(fn0).toHaveBeenCalled();
|
||||
expect(fn1).toHaveBeenCalled();
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("can send a request before connecting", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn0 = mock(request => {
|
||||
expect(request).toStrictEqual({
|
||||
id: 1,
|
||||
method: "Runtime.enable",
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.pendingRequest", fn0);
|
||||
ws.on("Inspector.request", fn0);
|
||||
const fn1 = mock(response => {
|
||||
expect(response).toStrictEqual({
|
||||
id: 1,
|
||||
result: {
|
||||
ok: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.response", fn1);
|
||||
const request = ws.send("Runtime.enable");
|
||||
expect(ws.start()).resolves.toBe(true);
|
||||
expect(request).resolves.toMatchObject({ ok: true });
|
||||
expect(fn0).toHaveBeenCalledTimes(2);
|
||||
expect(fn1).toHaveBeenCalled();
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("can receive an event", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn = mock(event => {
|
||||
expect(event).toStrictEqual({
|
||||
method: "Debugger.scriptParsed",
|
||||
params: {
|
||||
scriptId: "1",
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.event", fn);
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
expect(ws.send("Debugger.enable")).resolves.toMatchObject({ ok: true });
|
||||
expect(fn).toHaveBeenCalled();
|
||||
ws.close();
|
||||
});
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
server = serve({
|
||||
port: 0,
|
||||
fetch(request, server) {
|
||||
if (request.url.endsWith("/ws") && server.upgrade(request)) {
|
||||
return;
|
||||
}
|
||||
return new Response();
|
||||
},
|
||||
websocket: {
|
||||
message(ws, message) {
|
||||
const { id, method } = JSON.parse(String(message));
|
||||
ws.send(JSON.stringify({ id, result: { ok: true } }));
|
||||
|
||||
if (method === "Debugger.enable") {
|
||||
ws.send(JSON.stringify({ method: "Debugger.scriptParsed", params: { scriptId: "1" } }));
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
const { hostname, port } = server;
|
||||
url = new URL(`ws://${hostname}:${port}/ws`);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server?.stop(true);
|
||||
});
|
||||
18
packages/bun-inspector-protocol/tsconfig.json
Normal file
18
packages/bun-inspector-protocol/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSourceMap": true,
|
||||
"allowJs": true,
|
||||
"outDir": "dist",
|
||||
},
|
||||
"include": [".", "../bun-types/index.d.ts"]
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"name": "bun-lambda",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"bun-types": "^0.7.0",
|
||||
"jszip": "^3.10.1",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "bun-release-action",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"aws4fetch": "^1.0.17",
|
||||
|
||||
227
packages/bun-types/bun.d.ts
vendored
227
packages/bun-types/bun.d.ts
vendored
@@ -73,8 +73,12 @@ declare module "bun" {
|
||||
export type Serve<WebSocketDataType = undefined> =
|
||||
| ServeOptions
|
||||
| TLSServeOptions
|
||||
| UnixServeOptions
|
||||
| UnixTLSServeOptions
|
||||
| WebSocketServeOptions<WebSocketDataType>
|
||||
| TLSWebSocketServeOptions<WebSocketDataType>;
|
||||
| TLSWebSocketServeOptions<WebSocketDataType>
|
||||
| UnixWebSocketServeOptions<WebSocketDataType>
|
||||
| UnixTLSWebSocketServeOptions<WebSocketDataType>;
|
||||
|
||||
/**
|
||||
* Start a fast HTTP server.
|
||||
@@ -112,13 +116,7 @@ declare module "bun" {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function serve<T>(
|
||||
options:
|
||||
| ServeOptions
|
||||
| TLSServeOptions
|
||||
| WebSocketServeOptions<T>
|
||||
| TLSWebSocketServeOptions<T>,
|
||||
): Server;
|
||||
export function serve<T>(options: Serve<T>): Server;
|
||||
|
||||
/**
|
||||
* Synchronously resolve a `moduleId` as though it were imported from `parent`
|
||||
@@ -1783,6 +1781,49 @@ declare module "bun" {
|
||||
};
|
||||
|
||||
interface GenericServeOptions {
|
||||
/**
|
||||
*
|
||||
* What URI should be used to make {@link Request.url} absolute?
|
||||
*
|
||||
* By default, looks at {@link hostname}, {@link port}, and whether or not SSL is enabled to generate one
|
||||
*
|
||||
* @example
|
||||
*```js
|
||||
* "http://my-app.com"
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
*```js
|
||||
* "https://wongmjane.com/"
|
||||
* ```
|
||||
*
|
||||
* This should be the public, absolute URL – include the protocol and {@link hostname}. If the port isn't 80 or 443, then include the {@link port} too.
|
||||
*
|
||||
* @example
|
||||
* "http://localhost:3000"
|
||||
*/
|
||||
// baseURI?: string;
|
||||
|
||||
/**
|
||||
* What is the maximum size of a request body? (in bytes)
|
||||
* @default 1024 * 1024 * 128 // 128MB
|
||||
*/
|
||||
maxRequestBodySize?: number;
|
||||
|
||||
/**
|
||||
* Render contextual errors? This enables bun's error page
|
||||
* @default process.env.NODE_ENV !== 'production'
|
||||
*/
|
||||
development?: boolean;
|
||||
|
||||
error?: (
|
||||
this: Server,
|
||||
request: Errorlike,
|
||||
) => Response | Promise<Response> | undefined | void | Promise<undefined>;
|
||||
}
|
||||
|
||||
export type AnyFunction = (..._: any[]) => any;
|
||||
export interface ServeOptions extends GenericServeOptions {
|
||||
/**
|
||||
* What port should the server listen on?
|
||||
* @default process.env.PORT || "3000"
|
||||
@@ -1810,48 +1851,11 @@ declare module "bun" {
|
||||
hostname?: string;
|
||||
|
||||
/**
|
||||
* What URI should be used to make {@link Request.url} absolute?
|
||||
*
|
||||
* By default, looks at {@link hostname}, {@link port}, and whether or not SSL is enabled to generate one
|
||||
*
|
||||
* @example
|
||||
*```js
|
||||
* "http://my-app.com"
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
*```js
|
||||
* "https://wongmjane.com/"
|
||||
* ```
|
||||
*
|
||||
* This should be the public, absolute URL – include the protocol and {@link hostname}. If the port isn't 80 or 443, then include the {@link port} too.
|
||||
*
|
||||
* @example
|
||||
* "http://localhost:3000"
|
||||
*
|
||||
* If set, the HTTP server will listen on a unix socket instead of a port.
|
||||
* (Cannot be used with hostname+port)
|
||||
*/
|
||||
// baseURI?: string;
|
||||
unix?: never;
|
||||
|
||||
/**
|
||||
* What is the maximum size of a request body? (in bytes)
|
||||
* @default 1024 * 1024 * 128 // 128MB
|
||||
*/
|
||||
maxRequestBodySize?: number;
|
||||
|
||||
/**
|
||||
* Render contextual errors? This enables bun's error page
|
||||
* @default process.env.NODE_ENV !== 'production'
|
||||
*/
|
||||
development?: boolean;
|
||||
|
||||
error?: (
|
||||
this: Server,
|
||||
request: Errorlike,
|
||||
) => Response | Promise<Response> | undefined | void | Promise<undefined>;
|
||||
}
|
||||
|
||||
export type AnyFunction = (..._: any[]) => any;
|
||||
export interface ServeOptions extends GenericServeOptions {
|
||||
/**
|
||||
* Handle HTTP requests
|
||||
*
|
||||
@@ -1865,8 +1869,113 @@ declare module "bun" {
|
||||
): Response | Promise<Response>;
|
||||
}
|
||||
|
||||
export interface UnixServeOptions extends GenericServeOptions {
|
||||
/**
|
||||
* If set, the HTTP server will listen on a unix socket instead of a port.
|
||||
* (Cannot be used with hostname+port)
|
||||
*/
|
||||
unix: string;
|
||||
/**
|
||||
* Handle HTTP requests
|
||||
*
|
||||
* Respond to {@link Request} objects with a {@link Response} object.
|
||||
*/
|
||||
fetch(
|
||||
this: Server,
|
||||
request: Request,
|
||||
server: Server,
|
||||
): Response | Promise<Response>;
|
||||
}
|
||||
|
||||
export interface WebSocketServeOptions<WebSocketDataType = undefined>
|
||||
extends GenericServeOptions {
|
||||
/**
|
||||
* What port should the server listen on?
|
||||
* @default process.env.PORT || "3000"
|
||||
*/
|
||||
port?: string | number;
|
||||
|
||||
/**
|
||||
* What hostname should the server listen on?
|
||||
*
|
||||
* @default
|
||||
* ```js
|
||||
* "0.0.0.0" // listen on all interfaces
|
||||
* ```
|
||||
* @example
|
||||
* ```js
|
||||
* "127.0.0.1" // Only listen locally
|
||||
* ```
|
||||
* @example
|
||||
* ```js
|
||||
* "remix.run" // Only listen on remix.run
|
||||
* ````
|
||||
*
|
||||
* note: hostname should not include a {@link port}
|
||||
*/
|
||||
hostname?: string;
|
||||
|
||||
/**
|
||||
* Enable websockets with {@link Bun.serve}
|
||||
*
|
||||
* For simpler type safety, see {@link Bun.websocket}
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*import { serve } from "bun";
|
||||
*serve({
|
||||
* websocket: {
|
||||
* open: (ws) => {
|
||||
* console.log("Client connected");
|
||||
* },
|
||||
* message: (ws, message) => {
|
||||
* console.log("Client sent message", message);
|
||||
* },
|
||||
* close: (ws) => {
|
||||
* console.log("Client disconnected");
|
||||
* },
|
||||
* },
|
||||
* fetch(req, server) {
|
||||
* const url = new URL(req.url);
|
||||
* if (url.pathname === "/chat") {
|
||||
* const upgraded = server.upgrade(req);
|
||||
* if (!upgraded) {
|
||||
* return new Response("Upgrade failed", { status: 400 });
|
||||
* }
|
||||
* }
|
||||
* return new Response("Hello World");
|
||||
* },
|
||||
*});
|
||||
*```
|
||||
* Upgrade a {@link Request} to a {@link ServerWebSocket} via {@link Server.upgrade}
|
||||
*
|
||||
* Pass `data` in @{link Server.upgrade} to attach data to the {@link ServerWebSocket.data} property
|
||||
*
|
||||
*
|
||||
*/
|
||||
websocket: WebSocketHandler<WebSocketDataType>;
|
||||
|
||||
/**
|
||||
* Handle HTTP requests or upgrade them to a {@link ServerWebSocket}
|
||||
*
|
||||
* Respond to {@link Request} objects with a {@link Response} object.
|
||||
*
|
||||
*/
|
||||
fetch(
|
||||
this: Server,
|
||||
request: Request,
|
||||
server: Server,
|
||||
): Response | undefined | Promise<Response | undefined>;
|
||||
}
|
||||
|
||||
export interface UnixWebSocketServeOptions<WebSocketDataType = undefined>
|
||||
extends GenericServeOptions {
|
||||
/**
|
||||
* If set, the HTTP server will listen on a unix socket instead of a port.
|
||||
* (Cannot be used with hostname+port)
|
||||
*/
|
||||
unix: string;
|
||||
|
||||
/**
|
||||
* Enable websockets with {@link Bun.serve}
|
||||
*
|
||||
@@ -1923,6 +2032,17 @@ declare module "bun" {
|
||||
export interface TLSWebSocketServeOptions<WebSocketDataType = undefined>
|
||||
extends WebSocketServeOptions<WebSocketDataType>,
|
||||
TLSOptions {
|
||||
unix?: never;
|
||||
tls?: TLSOptions;
|
||||
}
|
||||
export interface UnixTLSWebSocketServeOptions<WebSocketDataType = undefined>
|
||||
extends UnixWebSocketServeOptions<WebSocketDataType>,
|
||||
TLSOptions {
|
||||
/**
|
||||
* If set, the HTTP server will listen on a unix socket instead of a port.
|
||||
* (Cannot be used with hostname+port)
|
||||
*/
|
||||
unix: string;
|
||||
tls?: TLSOptions;
|
||||
}
|
||||
export interface Errorlike extends Error {
|
||||
@@ -2039,6 +2159,16 @@ declare module "bun" {
|
||||
tls?: TLSOptions;
|
||||
}
|
||||
|
||||
export interface UnixTLSServeOptions extends UnixServeOptions, TLSOptions {
|
||||
/**
|
||||
* The keys are [SNI](https://en.wikipedia.org/wiki/Server_Name_Indication) hostnames.
|
||||
* The values are SSL options objects.
|
||||
*/
|
||||
serverNames?: Record<string, TLSOptions>;
|
||||
|
||||
tls?: TLSOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP & HTTPS Server
|
||||
*
|
||||
@@ -2049,7 +2179,6 @@ declare module "bun" {
|
||||
* avoid starting and stopping the server often (unless it's a new instance of bun).
|
||||
*
|
||||
* Powered by a fork of [uWebSockets](https://github.com/uNetworking/uWebSockets). Thank you @alexhultman.
|
||||
*
|
||||
*/
|
||||
export interface Server {
|
||||
/**
|
||||
|
||||
@@ -103,3 +103,60 @@ Bun.serve({
|
||||
},
|
||||
websocket: { message() {} },
|
||||
});
|
||||
|
||||
Bun.serve({
|
||||
unix: "/tmp/bun.sock",
|
||||
fetch() {
|
||||
return new Response();
|
||||
},
|
||||
});
|
||||
|
||||
Bun.serve({
|
||||
unix: "/tmp/bun.sock",
|
||||
fetch(req, server) {
|
||||
server.upgrade(req);
|
||||
if (Math.random() > 0.5) return undefined;
|
||||
return new Response();
|
||||
},
|
||||
websocket: { message() {} },
|
||||
});
|
||||
|
||||
Bun.serve({
|
||||
unix: "/tmp/bun.sock",
|
||||
fetch() {
|
||||
return new Response();
|
||||
},
|
||||
tls: {},
|
||||
});
|
||||
|
||||
Bun.serve({
|
||||
unix: "/tmp/bun.sock",
|
||||
fetch(req, server) {
|
||||
server.upgrade(req);
|
||||
if (Math.random() > 0.5) return undefined;
|
||||
return new Response();
|
||||
},
|
||||
websocket: { message() {} },
|
||||
tls: {},
|
||||
});
|
||||
|
||||
// Bun.serve({
|
||||
// unix: "/tmp/bun.sock",
|
||||
// // @ts-expect-error
|
||||
// port: 1234,
|
||||
// fetch() {
|
||||
// return new Response();
|
||||
// },
|
||||
// });
|
||||
|
||||
// Bun.serve({
|
||||
// unix: "/tmp/bun.sock",
|
||||
// // @ts-expect-error
|
||||
// port: 1234,
|
||||
// fetch(req, server) {
|
||||
// server.upgrade(req);
|
||||
// if (Math.random() > 0.5) return undefined;
|
||||
// return new Response();
|
||||
// },
|
||||
// websocket: { message() {} },
|
||||
// });
|
||||
|
||||
3
packages/bun-vscode/.gitignore
vendored
Normal file
3
packages/bun-vscode/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
extension
|
||||
example/.vscode
|
||||
15
packages/bun-vscode/.vscode/launch.json
vendored
15
packages/bun-vscode/.vscode/launch.json
vendored
@@ -5,25 +5,28 @@
|
||||
"name": "Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}/example"
|
||||
],
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/example"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "Build (watch)"
|
||||
"preLaunchTask": "Build"
|
||||
},
|
||||
{
|
||||
"name": "Extension (web)",
|
||||
"type": "extensionHost",
|
||||
"debugWebWorkerHost": true,
|
||||
"request": "launch",
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--extensionDevelopmentKind=web",
|
||||
"${workspaceFolder}/example"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "Build (watch)"
|
||||
"preLaunchTask": "Build"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
4
packages/bun-vscode/.vscode/settings.json
vendored
4
packages/bun-vscode/.vscode/settings.json
vendored
@@ -5,5 +5,5 @@
|
||||
"search.exclude": {
|
||||
"out": true // set this to false to include "out" folder in search results
|
||||
},
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
}
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
}
|
||||
|
||||
11
packages/bun-vscode/.vscode/tasks.json
vendored
11
packages/bun-vscode/.vscode/tasks.json
vendored
@@ -4,13 +4,8 @@
|
||||
{
|
||||
"label": "Build",
|
||||
"type": "shell",
|
||||
"command": "bun run build"
|
||||
},
|
||||
{
|
||||
"label": "Build (watch)",
|
||||
"type": "shell",
|
||||
"command": "bun run build:watch",
|
||||
"isBackground": true
|
||||
"command": "bun run build",
|
||||
"problemMatcher": "$esbuild"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
0
packages/bun-vscode/LICENSE
Normal file
0
packages/bun-vscode/LICENSE
Normal file
@@ -1 +1,22 @@
|
||||
# Debug Adapter Protocol for Bun
|
||||
# Bun for Visual Studio Code
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
<img align="right" src="https://user-images.githubusercontent.com/709451/182802334-d9c42afe-f35d-4a7b-86ea-9985f73f20c3.png" height="150px" style="float: right; padding: 30px;">
|
||||
|
||||
This extension adds support for using [Bun](https://bun.sh/) with Visual Studio Code. Bun is an all-in-one toolkit for JavaScript and TypeScript apps.
|
||||
|
||||
At its core is the _Bun runtime_, a fast JavaScript runtime designed as a drop-in replacement for Node.js. It's written in Zig and powered by JavaScriptCore under the hood, dramatically reducing startup times and memory usage.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://bun.sh/docs">Documentation</a>
|
||||
<span> • </span>
|
||||
<a href="https://discord.com/invite/CXdq2DP29u">Discord</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/oven-sh/bun/issues/new">Issues</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/oven-sh/bun/issues/159">Roadmap</a>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
* Off-by-one for debug lines
|
||||
* Formatting values in console (some code is wired up)
|
||||
* Play button on debugger actually starting Bun
|
||||
* bun debug or --inspect command added to Bun, not need Bun.serve
|
||||
* Breakpoint actually setting
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Binary file not shown.
19
packages/bun-vscode/example/.vscode/launch.json
vendored
19
packages/bun-vscode/example/.vscode/launch.json
vendored
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "launch",
|
||||
"name": "Debug",
|
||||
"program": "${workspaceFolder}/example.js",
|
||||
"stopOnEntry": true
|
||||
},
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"name": "Attach",
|
||||
"program": "${workspaceFolder}/example.js",
|
||||
"stopOnEntry": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -1,63 +0,0 @@
|
||||
// @bun
|
||||
const express = import.meta.require("express");
|
||||
const app = express();
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
app
|
||||
.get("/", (req, res) => {
|
||||
console.log("I am logging a request!");
|
||||
readFile(import.meta.path, "utf-8").then(data => {
|
||||
console.log(data.length);
|
||||
debugger;
|
||||
res.send("hello world");
|
||||
});
|
||||
})
|
||||
.listen(3000);
|
||||
|
||||
const va = 1;
|
||||
let vb = 2;
|
||||
var vc = 3;
|
||||
|
||||
function fa() {
|
||||
fb();
|
||||
}
|
||||
|
||||
function fb() {
|
||||
fc();
|
||||
}
|
||||
|
||||
function fc() {
|
||||
fd();
|
||||
}
|
||||
|
||||
function fd() {
|
||||
let map = new Map([
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[3, 4],
|
||||
]);
|
||||
let set = new Set([1, 2, 3, 4, 5]);
|
||||
let arr = [1, 2, 3, 4, 5];
|
||||
let obj = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
};
|
||||
function fd1() {
|
||||
let date = new Date();
|
||||
console.log(new Error().stack);
|
||||
debugger;
|
||||
console.log(date);
|
||||
}
|
||||
fd1();
|
||||
}
|
||||
|
||||
Bun.serve({
|
||||
port: 9229,
|
||||
inspector: true,
|
||||
development: true,
|
||||
fetch(request, server) {
|
||||
console.log(request);
|
||||
return new Response(request.url);
|
||||
},
|
||||
});
|
||||
11
packages/bun-vscode/example/example.test.ts
Normal file
11
packages/bun-vscode/example/example.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
|
||||
describe("example", () => {
|
||||
test("it works", () => {
|
||||
expect(1).toBe(1);
|
||||
expect(1).not.toBe(2);
|
||||
expect(() => {
|
||||
throw new Error("error");
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
34
packages/bun-vscode/example/example.ts
Normal file
34
packages/bun-vscode/example/example.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
a(request);
|
||||
const coolThing: CoolThing = new SuperCoolThing();
|
||||
coolThing.doCoolThing();
|
||||
debugger;
|
||||
return new Response("BAI BAI");
|
||||
},
|
||||
};
|
||||
|
||||
// a
|
||||
function a(request: Request): void {
|
||||
b(request);
|
||||
}
|
||||
|
||||
// b
|
||||
function b(request: Request): void {
|
||||
c(request);
|
||||
}
|
||||
|
||||
// c
|
||||
function c(request: Request) {
|
||||
console.log(request);
|
||||
}
|
||||
|
||||
interface CoolThing {
|
||||
doCoolThing(): void;
|
||||
}
|
||||
|
||||
class SuperCoolThing implements CoolThing {
|
||||
doCoolThing(): void {
|
||||
console.log("BLAH BLAH");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "example",
|
||||
"dependencies": {
|
||||
"elysia": "^0.6.3",
|
||||
"express": "^4.18.2",
|
||||
@@ -7,5 +9,11 @@
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"mime"
|
||||
]
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"bun-types": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "preserve",
|
||||
@@ -1,65 +1,99 @@
|
||||
{
|
||||
"name": "bun-vscode",
|
||||
"version": "0.0.1",
|
||||
"main": "./dist/extension.js",
|
||||
"author": "oven",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/oven-sh/bun"
|
||||
},
|
||||
"main": "dist/extension.js",
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.81.0",
|
||||
"@vscode/debugadapter": "^1.56.0",
|
||||
"@vscode/debugadapter-testsupport": "^1.56.0",
|
||||
"@vscode/vsce": "^2.20.1",
|
||||
"bun-types": "^0.7.3",
|
||||
"esbuild": "^0.19.2",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onLanguage:javascript",
|
||||
"onLanguage:javascriptreact",
|
||||
"onLanguage:typescript",
|
||||
"onLanguage:typescriptreact",
|
||||
"workspaceContains:**/.lockb",
|
||||
"onDebugResolve:bun",
|
||||
"onDebugDynamicConfigurations:bun",
|
||||
"onCommand:extension.bun.getProgramName"
|
||||
"onDebugDynamicConfigurations:bun"
|
||||
],
|
||||
"browser": "./dist/web-extension.js",
|
||||
"browser": "dist/web-extension.js",
|
||||
"bugs": {
|
||||
"url": "https://github.com/oven-sh/bun/issues"
|
||||
},
|
||||
"capabilities": {
|
||||
"untrustedWorkspaces": {
|
||||
"supported": false,
|
||||
"description": "This extension needs to be able to run your code using Bun."
|
||||
}
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
"Debuggers"
|
||||
"Debuggers",
|
||||
"Testing"
|
||||
],
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "Bun",
|
||||
"properties": {
|
||||
"bun.path": {
|
||||
"type": "string",
|
||||
"description": "A path to the `bun` executable. By default, the extension looks for `bun` in the `PATH`, but if set, it will use the specified path instead.",
|
||||
"scope": "window",
|
||||
"default": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "extension.bun.runFile",
|
||||
"title": "Run Bun",
|
||||
"shortTitle": "Run",
|
||||
"category": "Bun",
|
||||
"enablement": "!inDebugMode",
|
||||
"icon": "$(play)"
|
||||
},
|
||||
{
|
||||
"command": "extension.bun.debugFile",
|
||||
"title": "Debug Bun",
|
||||
"shortTitle": "Debug",
|
||||
"category": "Bun",
|
||||
"enablement": "!inDebugMode",
|
||||
"icon": "$(debug-alt)"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"editor/title/run": [
|
||||
{
|
||||
"command": "extension.bun.runEditorContents",
|
||||
"when": "resourceLangId == javascript",
|
||||
"command": "extension.bun.runFile",
|
||||
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "extension.bun.debugEditorContents",
|
||||
"when": "resourceLangId == javascript",
|
||||
"command": "extension.bun.debugFile",
|
||||
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact",
|
||||
"group": "navigation@2"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "extension.bun.debugEditorContents",
|
||||
"when": "resourceLangId == javascript"
|
||||
"command": "extension.bun.runFile",
|
||||
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact"
|
||||
},
|
||||
{
|
||||
"command": "extension.bun.runEditorContents",
|
||||
"when": "resourceLangId == javascript"
|
||||
"command": "extension.bun.debugFile",
|
||||
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "extension.bun.debugEditorContents",
|
||||
"title": "Debug File",
|
||||
"category": "Bun Debug",
|
||||
"enablement": "!inDebugMode",
|
||||
"icon": "$(debug-alt)"
|
||||
},
|
||||
{
|
||||
"command": "extension.bun.runEditorContents",
|
||||
"title": "Run File",
|
||||
"category": "Bun Debug",
|
||||
"enablement": "!inDebugMode",
|
||||
"icon": "$(play)"
|
||||
}
|
||||
],
|
||||
"breakpoints": [
|
||||
{
|
||||
"language": "javascript"
|
||||
@@ -85,73 +119,71 @@
|
||||
"typescriptreact"
|
||||
],
|
||||
"runtime": "node",
|
||||
"program": "./dist/adapter.js",
|
||||
"program": "dist/adapter.js",
|
||||
"configurationAttributes": {
|
||||
"launch": {
|
||||
"required": [
|
||||
"program"
|
||||
],
|
||||
"properties": {
|
||||
"runtime": {
|
||||
"type": "string",
|
||||
"description": "The path to Bun.",
|
||||
"default": "bun"
|
||||
},
|
||||
"program": {
|
||||
"type": "string",
|
||||
"description": "The file to run and debug.",
|
||||
"default": "${workspaceFolder}/${command:AskForProgramName}"
|
||||
"description": "The file to debug.",
|
||||
"default": "${file}"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "The working directory.",
|
||||
"default": "${workspaceFolder}"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"description": "The arguments passed to Bun.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"description": "The environment variables passed to Bun.",
|
||||
"default": {}
|
||||
},
|
||||
"inheritEnv": {
|
||||
"type": "boolean",
|
||||
"description": "If environment variables should be inherited from the parent process.",
|
||||
"default": true
|
||||
},
|
||||
"watch": {
|
||||
"type": ["boolean", "string"],
|
||||
"description": "If the process should be restarted when files change.",
|
||||
"enum": [
|
||||
true,
|
||||
false,
|
||||
"hot"
|
||||
],
|
||||
"default": false
|
||||
},
|
||||
"debug": {
|
||||
"type": "boolean",
|
||||
"description": "If the process should be started in debug mode.",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"attach": {
|
||||
"properties": {
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"description": "The port to attach and debug.",
|
||||
"default": 6499
|
||||
},
|
||||
"hostname": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The hostname to attach and debug.",
|
||||
"default": "localhost"
|
||||
"description": "The URL of the Bun process to attach to."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "launch",
|
||||
"name": "Bun: Debug",
|
||||
"program": "${workspaceFolder}/${command:AskForProgramName}"
|
||||
},
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"name": "Bun: Attach"
|
||||
}
|
||||
],
|
||||
"configurationSnippets": [
|
||||
{
|
||||
"label": "Bun: Debug",
|
||||
"description": "A new configuration for 'debugging' a Bun process.",
|
||||
"body": {
|
||||
"type": "bun",
|
||||
"request": "launch",
|
||||
"name": "Ask for file name",
|
||||
"program": "^\"\\${workspaceFolder}/\\${command:AskForProgramName}\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Bun: Attach",
|
||||
"description": "A new configuration for 'attaching' to a running Bun process.",
|
||||
"body": {
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"name": "Attach to Bun",
|
||||
"port": 6499,
|
||||
"hostname": "localhost"
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": {
|
||||
"AskForProgramName": "extension.bun.getProgramName"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -165,15 +197,15 @@
|
||||
".lockb"
|
||||
],
|
||||
"icon": {
|
||||
"dark": "./src/assets/icon-small.png",
|
||||
"light": "./src/assets/icon-small.png"
|
||||
"dark": "assets/icon-small.png",
|
||||
"light": "assets/icon-small.png"
|
||||
}
|
||||
}
|
||||
],
|
||||
"jsonValidation": [
|
||||
{
|
||||
"fileMatch": "package.json",
|
||||
"url": "./src/resources/package.json"
|
||||
"url": "assets/package.json"
|
||||
}
|
||||
],
|
||||
"customEditors": [
|
||||
@@ -189,26 +221,38 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Visual Studio Code extension for Bun.",
|
||||
"description": "The Visual Studio Code extension for Bun.",
|
||||
"displayName": "Bun",
|
||||
"engines": {
|
||||
"vscode": "^1.66.0"
|
||||
"vscode": "^1.81.0"
|
||||
},
|
||||
"extensionKind": [
|
||||
"workspace"
|
||||
],
|
||||
"galleryBanner": {
|
||||
"color": "#C80000",
|
||||
"theme": "dark"
|
||||
},
|
||||
"icon": "./src/assets/icon.png",
|
||||
"homepage": "https://bun.sh/",
|
||||
"icon": "assets/icon.png",
|
||||
"keywords": [
|
||||
"bun",
|
||||
"node.js",
|
||||
"javascript",
|
||||
"typescript",
|
||||
"vscode"
|
||||
],
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"publisher": "oven",
|
||||
"scripts": {
|
||||
"build": "bunx esbuild src/extension.ts src/web-extension.ts --bundle --external:vscode --outdir=dist --platform=node --format=cjs",
|
||||
"build:watch": "bunx esbuild --watch src/extension.ts src/web-extension.ts --bundle --external:vscode --outdir=dist --platform=node --format=cjs"
|
||||
"build": "node scripts/build.mjs",
|
||||
"test": "node scripts/test.mjs"
|
||||
},
|
||||
"workspaceTrust": {
|
||||
"request": "never"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
"workspaces": [
|
||||
"../bun-debug-adapter-protocol",
|
||||
"../bun-inspector-protocol"
|
||||
]
|
||||
}
|
||||
|
||||
29
packages/bun-vscode/scripts/build.mjs
Normal file
29
packages/bun-vscode/scripts/build.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
import { buildSync } from "esbuild";
|
||||
import { rmSync, mkdirSync, cpSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const { pathname } = new URL("..", import.meta.url);
|
||||
process.chdir(pathname);
|
||||
|
||||
buildSync({
|
||||
entryPoints: ["src/extension.ts", "src/web-extension.ts"],
|
||||
outdir: "dist",
|
||||
bundle: true,
|
||||
external: ["vscode"],
|
||||
platform: "node",
|
||||
format: "cjs",
|
||||
});
|
||||
|
||||
rmSync("extension", { recursive: true, force: true });
|
||||
mkdirSync("extension", { recursive: true });
|
||||
cpSync("dist", "extension/dist", { recursive: true });
|
||||
cpSync("assets", "extension/assets", { recursive: true });
|
||||
cpSync("README.md", "extension/README.md");
|
||||
cpSync("LICENSE", "extension/LICENSE");
|
||||
cpSync("package.json", "extension/package.json");
|
||||
|
||||
const cmd = process.isBun ? "bunx" : "npx";
|
||||
spawnSync(cmd, ["vsce", "package"], {
|
||||
cwd: "extension",
|
||||
stdio: "inherit",
|
||||
});
|
||||
21
packages/bun-vscode/scripts/test.mjs
Normal file
21
packages/bun-vscode/scripts/test.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
import { readdirSync } from "node:fs";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const { pathname } = new URL("..", import.meta.url);
|
||||
process.chdir(pathname);
|
||||
|
||||
let path;
|
||||
for (const filename of readdirSync("extension")) {
|
||||
if (filename.endsWith(".vsix")) {
|
||||
path = `extension/${filename}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
throw new Error("No .vsix file found");
|
||||
}
|
||||
|
||||
spawn("code", ["--new-window", `--install-extension=${path}`, `--extensionDevelopmentPath=${pathname}`, "example"], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
@@ -1,118 +0,0 @@
|
||||
import * as vscode from "vscode";
|
||||
import { CancellationToken, DebugConfiguration, ProviderResult, WorkspaceFolder } from "vscode";
|
||||
import { DAPAdapter } from "./dap";
|
||||
import lockfile from "./lockfile";
|
||||
|
||||
export function activateBunDebug(context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) {
|
||||
lockfile(context);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("extension.bun.runEditorContents", (resource: vscode.Uri) => {
|
||||
let targetResource = resource;
|
||||
if (!targetResource && vscode.window.activeTextEditor) {
|
||||
targetResource = vscode.window.activeTextEditor.document.uri;
|
||||
}
|
||||
if (targetResource) {
|
||||
vscode.debug.startDebugging(
|
||||
undefined,
|
||||
{
|
||||
type: "bun",
|
||||
name: "Run File",
|
||||
request: "launch",
|
||||
program: targetResource.fsPath,
|
||||
},
|
||||
{ noDebug: true },
|
||||
);
|
||||
}
|
||||
}),
|
||||
vscode.commands.registerCommand("extension.bun.debugEditorContents", (resource: vscode.Uri) => {
|
||||
let targetResource = resource;
|
||||
if (!targetResource && vscode.window.activeTextEditor) {
|
||||
targetResource = vscode.window.activeTextEditor.document.uri;
|
||||
}
|
||||
if (targetResource) {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
type: "bun",
|
||||
name: "Debug File",
|
||||
request: "launch",
|
||||
program: targetResource.fsPath,
|
||||
stopOnEntry: true,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("extension.bun.getProgramName", config => {
|
||||
return vscode.window.showInputBox({
|
||||
placeHolder: "Please enter the name of a file in the workspace folder",
|
||||
value: "src/index.js",
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const provider = new BunConfigurationProvider();
|
||||
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("bun", provider));
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.debug.registerDebugConfigurationProvider(
|
||||
"bun",
|
||||
{
|
||||
provideDebugConfigurations(folder: WorkspaceFolder | undefined): ProviderResult<DebugConfiguration[]> {
|
||||
return [
|
||||
{
|
||||
name: "Launch",
|
||||
request: "launch",
|
||||
type: "bun",
|
||||
program: "${file}",
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
vscode.DebugConfigurationProviderTriggerKind.Dynamic,
|
||||
),
|
||||
);
|
||||
|
||||
if (!factory) {
|
||||
factory = new InlineDebugAdapterFactory();
|
||||
}
|
||||
context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("bun", factory));
|
||||
if ("dispose" in factory) {
|
||||
// @ts-expect-error ???
|
||||
context.subscriptions.push(factory);
|
||||
}
|
||||
}
|
||||
|
||||
class BunConfigurationProvider implements vscode.DebugConfigurationProvider {
|
||||
resolveDebugConfiguration(
|
||||
folder: WorkspaceFolder | undefined,
|
||||
config: DebugConfiguration,
|
||||
token?: CancellationToken,
|
||||
): ProviderResult<DebugConfiguration> {
|
||||
// if launch.json is missing or empty
|
||||
if (!config.type && !config.request && !config.name) {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor && editor.document.languageId === "javascript") {
|
||||
config.type = "bun";
|
||||
config.name = "Launch";
|
||||
config.request = "launch";
|
||||
config.program = "${file}";
|
||||
config.stopOnEntry = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.program) {
|
||||
return vscode.window.showInformationMessage("Cannot find a program to debug").then(_ => {
|
||||
return undefined; // abort launch
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
|
||||
createDebugAdapterDescriptor(_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
|
||||
return new vscode.DebugAdapterInlineImplementation(new DAPAdapter(_session));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,10 @@
|
||||
import * as vscode from "vscode";
|
||||
import { activateBunDebug } from "./activate";
|
||||
|
||||
const runMode: "external" | "server" | "namedPipeServer" | "inline" = "inline";
|
||||
import activateLockfile from "./features/lockfile";
|
||||
import activateDebug from "./features/debug";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
if (runMode === "inline") {
|
||||
activateBunDebug(context);
|
||||
return;
|
||||
}
|
||||
throw new Error(`This extension does not support '${runMode}' mode.`);
|
||||
activateLockfile(context);
|
||||
activateDebug(context);
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
// No-op
|
||||
}
|
||||
export function deactivate() {}
|
||||
|
||||
186
packages/bun-vscode/src/features/debug.ts
Normal file
186
packages/bun-vscode/src/features/debug.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import * as vscode from "vscode";
|
||||
import type { CancellationToken, DebugConfiguration, ProviderResult, WorkspaceFolder } from "vscode";
|
||||
import type { DAP } from "../../../bun-debug-adapter-protocol";
|
||||
import { DebugAdapter, UnixSignal } from "../../../bun-debug-adapter-protocol";
|
||||
import { DebugSession } from "@vscode/debugadapter";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const debugConfiguration: vscode.DebugConfiguration = {
|
||||
type: "bun",
|
||||
request: "launch",
|
||||
name: "Debug Bun",
|
||||
program: "${file}",
|
||||
watch: false,
|
||||
};
|
||||
|
||||
const runConfiguration: vscode.DebugConfiguration = {
|
||||
type: "bun",
|
||||
request: "launch",
|
||||
name: "Run Bun",
|
||||
program: "${file}",
|
||||
debug: false,
|
||||
watch: false,
|
||||
};
|
||||
|
||||
const attachConfiguration: vscode.DebugConfiguration = {
|
||||
type: "bun",
|
||||
request: "attach",
|
||||
name: "Attach Bun",
|
||||
url: "ws://localhost:6499/",
|
||||
};
|
||||
|
||||
let channels: Record<string, vscode.OutputChannel> = {};
|
||||
let terminal: TerminalDebugSession | undefined;
|
||||
|
||||
export default function (context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("extension.bun.runFile", RunFileCommand),
|
||||
vscode.commands.registerCommand("extension.bun.debugFile", DebugFileCommand),
|
||||
vscode.debug.registerDebugConfigurationProvider(
|
||||
"bun",
|
||||
new DebugConfigurationProvider(),
|
||||
vscode.DebugConfigurationProviderTriggerKind.Initial,
|
||||
),
|
||||
vscode.debug.registerDebugConfigurationProvider(
|
||||
"bun",
|
||||
new DebugConfigurationProvider(),
|
||||
vscode.DebugConfigurationProviderTriggerKind.Dynamic,
|
||||
),
|
||||
vscode.debug.registerDebugAdapterDescriptorFactory("bun", factory ?? new InlineDebugAdapterFactory()),
|
||||
(channels["dap"] = vscode.window.createOutputChannel("Debug Adapter Protocol (Bun)")),
|
||||
(channels["jsc"] = vscode.window.createOutputChannel("JavaScript Inspector (Bun)")),
|
||||
(channels["console"] = vscode.window.createOutputChannel("Console (Bun)")),
|
||||
(terminal = new TerminalDebugSession()),
|
||||
);
|
||||
}
|
||||
|
||||
function RunFileCommand(resource?: vscode.Uri): void {
|
||||
const path = getCurrentPath(resource);
|
||||
if (path) {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
...runConfiguration,
|
||||
noDebug: true,
|
||||
program: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function DebugFileCommand(resource?: vscode.Uri): void {
|
||||
const path = getCurrentPath(resource);
|
||||
if (path) {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
...debugConfiguration,
|
||||
program: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class DebugConfigurationProvider implements vscode.DebugConfigurationProvider {
|
||||
provideDebugConfigurations(folder: WorkspaceFolder | undefined): ProviderResult<DebugConfiguration[]> {
|
||||
return [debugConfiguration, runConfiguration, attachConfiguration];
|
||||
}
|
||||
|
||||
resolveDebugConfiguration(
|
||||
folder: WorkspaceFolder | undefined,
|
||||
config: DebugConfiguration,
|
||||
token?: CancellationToken,
|
||||
): ProviderResult<DebugConfiguration> {
|
||||
let target: DebugConfiguration;
|
||||
|
||||
const { request } = config;
|
||||
if (request === "attach") {
|
||||
target = attachConfiguration;
|
||||
} else {
|
||||
target = debugConfiguration;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(target)) {
|
||||
if (config[key] === undefined) {
|
||||
config[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
|
||||
createDebugAdapterDescriptor(session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
|
||||
const { configuration } = session;
|
||||
const { request, url } = configuration;
|
||||
|
||||
if (request === "attach" && url === terminal?.adapter.url) {
|
||||
return new vscode.DebugAdapterInlineImplementation(terminal);
|
||||
}
|
||||
|
||||
const adapter = new FileDebugSession(session.id);
|
||||
return new vscode.DebugAdapterInlineImplementation(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
class FileDebugSession extends DebugSession {
|
||||
readonly adapter: DebugAdapter;
|
||||
readonly signal: UnixSignal;
|
||||
|
||||
constructor(sessionId?: string) {
|
||||
super();
|
||||
const uniqueId = sessionId ?? Math.random().toString(36).slice(2);
|
||||
this.adapter = new DebugAdapter(`ws+unix://${tmpdir()}/${uniqueId}.sock`);
|
||||
this.adapter.on("Adapter.response", response => this.sendResponse(response));
|
||||
this.adapter.on("Adapter.event", event => this.sendEvent(event));
|
||||
this.signal = new UnixSignal();
|
||||
}
|
||||
|
||||
handleMessage(message: DAP.Event | DAP.Request | DAP.Response): void {
|
||||
const { type } = message;
|
||||
if (type === "request") {
|
||||
this.adapter.emit("Adapter.request", message);
|
||||
} else {
|
||||
throw new Error(`Not supported: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.adapter.close();
|
||||
}
|
||||
}
|
||||
|
||||
class TerminalDebugSession extends FileDebugSession {
|
||||
readonly terminal: vscode.Terminal;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.terminal = vscode.window.createTerminal({
|
||||
name: "Bun Terminal",
|
||||
env: {
|
||||
"BUN_INSPECT": `1${this.adapter.url}`,
|
||||
"BUN_INSPECT_NOTIFY": `${this.signal.url}`,
|
||||
},
|
||||
isTransient: true,
|
||||
iconPath: new vscode.ThemeIcon("debug-console"),
|
||||
});
|
||||
this.terminal.show();
|
||||
this.signal.on("Signal.received", () => {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
...attachConfiguration,
|
||||
url: this.adapter.url,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isJavaScript(languageId: string): boolean {
|
||||
return (
|
||||
languageId === "javascript" ||
|
||||
languageId === "javascriptreact" ||
|
||||
languageId === "typescript" ||
|
||||
languageId === "typescriptreact"
|
||||
);
|
||||
}
|
||||
|
||||
function getCurrentPath(target?: vscode.Uri): string | undefined {
|
||||
if (!target && vscode.window.activeTextEditor) {
|
||||
target = vscode.window.activeTextEditor.document.uri;
|
||||
}
|
||||
return target?.fsPath;
|
||||
}
|
||||
@@ -52,10 +52,10 @@ function previewLockfile(uri: vscode.Uri, token?: vscode.CancellationToken): Pro
|
||||
process.stderr.on("data", (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
process.on("error", (error) => {
|
||||
process.on("error", error => {
|
||||
reject(error);
|
||||
});
|
||||
process.on("exit", (code) => {
|
||||
process.on("exit", code => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
@@ -65,19 +65,15 @@ function previewLockfile(uri: vscode.Uri, token?: vscode.CancellationToken): Pro
|
||||
});
|
||||
}
|
||||
|
||||
export default function(context: vscode.ExtensionContext): void {
|
||||
export default function (context: vscode.ExtensionContext): void {
|
||||
const viewType = "bun.lockb";
|
||||
const provider = new BunLockfileEditorProvider(context);
|
||||
|
||||
vscode.window.registerCustomEditorProvider(
|
||||
viewType,
|
||||
provider,
|
||||
{
|
||||
supportsMultipleEditorsPerDocument: true,
|
||||
webviewOptions: {
|
||||
enableFindWidget: true,
|
||||
retainContextWhenHidden: true,
|
||||
},
|
||||
|
||||
vscode.window.registerCustomEditorProvider(viewType, provider, {
|
||||
supportsMultipleEditorsPerDocument: true,
|
||||
webviewOptions: {
|
||||
enableFindWidget: true,
|
||||
retainContextWhenHidden: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
import { Socket, createConnection } from "node:net";
|
||||
import { inspect } from "node:util";
|
||||
import type { JSC } from "../types/jsc";
|
||||
export type { JSC };
|
||||
|
||||
export type JSCClientOptions = {
|
||||
url: string | URL;
|
||||
retry?: boolean;
|
||||
onEvent?: (event: JSC.Event) => void;
|
||||
onRequest?: (request: JSC.Request) => void;
|
||||
onResponse?: (response: JSC.Response) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onClose?: (code: number, reason: string) => void;
|
||||
};
|
||||
const headerInvalidNumber = 2147483646;
|
||||
|
||||
// We use non-printable characters to separate messages in the stream.
|
||||
// These should never appear in textual messages.
|
||||
|
||||
// These are non-sequential so that code which just counts up from 0 doesn't accidentally parse them as messages.
|
||||
// 0x12 0x11 0x13 0x14 as a little-endian 32-bit unsigned integer
|
||||
const headerPrefix = "\x14\x13\x11\x12";
|
||||
|
||||
// 0x14 0x12 0x13 0x11 as a little-endian 32-bit unsigned integer
|
||||
const headerSuffixString = "\x11\x13\x12\x14";
|
||||
|
||||
const headerSuffixInt = Buffer.from(headerSuffixString).readInt32LE(0);
|
||||
const headerPrefixInt = Buffer.from(headerPrefix).readInt32LE(0);
|
||||
|
||||
const messageLengthBuffer = new ArrayBuffer(12);
|
||||
const messageLengthDataView = new DataView(messageLengthBuffer);
|
||||
messageLengthDataView.setInt32(0, headerPrefixInt, true);
|
||||
messageLengthDataView.setInt32(8, headerSuffixInt, true);
|
||||
|
||||
function writeJSONMessageToBuffer(message: any) {
|
||||
const asString = JSON.stringify(message);
|
||||
const byteLength = Buffer.byteLength(asString, "utf8");
|
||||
const buffer = Buffer.allocUnsafe(12 + byteLength);
|
||||
buffer.writeInt32LE(headerPrefixInt, 0);
|
||||
buffer.writeInt32LE(byteLength, 4);
|
||||
buffer.writeInt32LE(headerSuffixInt, 8);
|
||||
if (buffer.write(asString, 12, byteLength, "utf8") !== byteLength) {
|
||||
throw new Error("Failed to write message to buffer");
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
let currentMessageLength = 0;
|
||||
const DEBUGGING = true;
|
||||
function extractMessageLengthAndOffsetFromBytes(buffer: Buffer, offset: number) {
|
||||
const bufferLength = buffer.length;
|
||||
while (offset < bufferLength) {
|
||||
const headerStart = buffer.indexOf(headerPrefix, offset, "binary");
|
||||
if (headerStart === -1) {
|
||||
if (DEBUGGING) {
|
||||
console.error("No header found in buffer of length " + bufferLength + " starting at offset " + offset);
|
||||
}
|
||||
return headerInvalidNumber;
|
||||
}
|
||||
|
||||
// [headerPrefix (4), byteLength (4), headerSuffix (4)]
|
||||
if (bufferLength <= headerStart + 12) {
|
||||
if (DEBUGGING) {
|
||||
console.error(
|
||||
"Not enough bytes for header in buffer of length " + bufferLength + " starting at offset " + offset,
|
||||
);
|
||||
}
|
||||
return headerInvalidNumber;
|
||||
}
|
||||
|
||||
const prefix = buffer.readInt32LE(headerStart);
|
||||
const byteLengthInt = buffer.readInt32LE(headerStart + 4);
|
||||
const suffix = buffer.readInt32LE(headerStart + 8);
|
||||
|
||||
if (prefix !== headerPrefixInt || suffix !== headerSuffixInt) {
|
||||
offset = headerStart + 1;
|
||||
currentMessageLength = 0;
|
||||
|
||||
if (DEBUGGING) {
|
||||
console.error(
|
||||
"Invalid header in buffer of length " + bufferLength + " starting at offset " + offset + ": " + prefix,
|
||||
byteLengthInt,
|
||||
suffix,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (byteLengthInt < 0) {
|
||||
if (DEBUGGING) {
|
||||
console.error(
|
||||
"Invalid byteLength in buffer of length " + bufferLength + " starting at offset " + offset + ": " + prefix,
|
||||
byteLengthInt,
|
||||
suffix,
|
||||
);
|
||||
}
|
||||
|
||||
return headerInvalidNumber;
|
||||
}
|
||||
|
||||
if (byteLengthInt === 0) {
|
||||
// Ignore 0-length messages
|
||||
// Shouldn't happen in practice
|
||||
offset = headerStart + 12;
|
||||
currentMessageLength = 0;
|
||||
|
||||
if (DEBUGGING) {
|
||||
console.error(
|
||||
"Ignoring 0-length message in buffer of length " + bufferLength + " starting at offset " + offset,
|
||||
);
|
||||
console.error({
|
||||
buffer: buffer,
|
||||
string: buffer.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
currentMessageLength = byteLengthInt;
|
||||
|
||||
return headerStart + 12;
|
||||
}
|
||||
|
||||
if (DEBUGGING) {
|
||||
if (bufferLength > 0)
|
||||
console.error("Header not found in buffer of length " + bufferLength + " starting at offset " + offset);
|
||||
}
|
||||
|
||||
return headerInvalidNumber;
|
||||
}
|
||||
|
||||
class StreamingReader {
|
||||
pendingBuffer: Buffer;
|
||||
|
||||
constructor() {
|
||||
this.pendingBuffer = Buffer.alloc(0);
|
||||
}
|
||||
|
||||
*onMessage(chunk: Buffer) {
|
||||
let buffer: Buffer;
|
||||
if (this.pendingBuffer.length > 0) {
|
||||
this.pendingBuffer = buffer = Buffer.concat([this.pendingBuffer, chunk]);
|
||||
} else {
|
||||
this.pendingBuffer = buffer = chunk;
|
||||
}
|
||||
|
||||
currentMessageLength = 0;
|
||||
|
||||
for (
|
||||
let offset = extractMessageLengthAndOffsetFromBytes(buffer, 0);
|
||||
buffer.length > 0 && offset !== headerInvalidNumber;
|
||||
currentMessageLength = 0, offset = extractMessageLengthAndOffsetFromBytes(buffer, 0)
|
||||
) {
|
||||
const messageLength = currentMessageLength;
|
||||
const start = offset;
|
||||
const end = start + messageLength;
|
||||
offset = end;
|
||||
const messageChunk = buffer.slice(start, end);
|
||||
this.pendingBuffer = buffer = buffer.slice(offset);
|
||||
yield messageChunk.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class JSCClient {
|
||||
#options: JSCClientOptions;
|
||||
#requestId: number;
|
||||
#pendingMessages: Buffer[];
|
||||
#pendingRequests: Map<number, (result: unknown) => void>;
|
||||
#socket: Socket;
|
||||
#ready?: Promise<void>;
|
||||
#reader = new StreamingReader();
|
||||
signal?: AbortSignal;
|
||||
|
||||
constructor(options: JSCClientOptions) {
|
||||
this.#options = options;
|
||||
this.#socket = undefined;
|
||||
this.#requestId = 1;
|
||||
|
||||
this.#pendingMessages = [];
|
||||
this.#pendingRequests = new Map();
|
||||
}
|
||||
|
||||
get ready(): Promise<void> {
|
||||
if (!this.#ready) {
|
||||
this.#ready = this.#connect();
|
||||
}
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
#connect(): Promise<void> {
|
||||
const { url, retry, onError, onResponse, onEvent, onClose } = this.#options;
|
||||
let [host, port] = typeof url === "string" ? url.split(":") : [url.hostname, url.port];
|
||||
if (port == null) {
|
||||
if (host == null) {
|
||||
host = "localhost";
|
||||
port = "9229";
|
||||
} else {
|
||||
port = "9229";
|
||||
}
|
||||
}
|
||||
|
||||
if (host == null) {
|
||||
host = "localhost";
|
||||
}
|
||||
var resolve,
|
||||
reject,
|
||||
promise = new Promise<void>((r1, r2) => {
|
||||
resolve = r1;
|
||||
reject = r2;
|
||||
}),
|
||||
socket: Socket;
|
||||
let didConnect = false;
|
||||
|
||||
this.#socket = socket = createConnection(
|
||||
{
|
||||
host,
|
||||
port: Number(port),
|
||||
},
|
||||
() => {
|
||||
for (const message of this.#pendingMessages) {
|
||||
this.#send(message);
|
||||
}
|
||||
this.#pendingMessages.length = 0;
|
||||
didConnect = true;
|
||||
resolve();
|
||||
},
|
||||
)
|
||||
.once("error", e => {
|
||||
const error = new Error(`Socket error: ${e?.message || e}`);
|
||||
reject(error);
|
||||
})
|
||||
.on("data", buffer => {
|
||||
for (const message of this.#reader.onMessage(buffer)) {
|
||||
let received: JSC.Event | JSC.Response;
|
||||
try {
|
||||
received = JSON.parse(message);
|
||||
} catch {
|
||||
const error = new Error(`Invalid WebSocket data: ${inspect(message)}`);
|
||||
onError?.(error);
|
||||
return;
|
||||
}
|
||||
console.log({ received });
|
||||
if ("id" in received) {
|
||||
onResponse?.(received);
|
||||
if ("error" in received) {
|
||||
const { message, code = "?" } = received.error;
|
||||
const error = new Error(`${message} [code: ${code}]`);
|
||||
error.code = code;
|
||||
onError?.(error);
|
||||
this.#pendingRequests.get(received.id)?.(error);
|
||||
} else {
|
||||
this.#pendingRequests.get(received.id)?.(received.result);
|
||||
}
|
||||
} else {
|
||||
onEvent?.(received);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on("close", hadError => {
|
||||
if (didConnect) {
|
||||
onClose?.(hadError ? 1 : 0, "Socket closed");
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
#send(message: any): void {
|
||||
const socket = this.#socket;
|
||||
const framed = writeJSONMessageToBuffer(message);
|
||||
if (socket && !socket.connecting) {
|
||||
socket.write(framed);
|
||||
} else {
|
||||
this.#pendingMessages.push(framed);
|
||||
}
|
||||
}
|
||||
|
||||
async fetch<T extends keyof JSC.RequestMap>(
|
||||
method: T,
|
||||
params?: JSC.Request<T>["params"],
|
||||
): Promise<JSC.ResponseMap[T]> {
|
||||
const request: JSC.Request<T> = {
|
||||
id: this.#requestId++,
|
||||
method,
|
||||
params,
|
||||
};
|
||||
this.#options.onRequest?.(request);
|
||||
return new Promise((resolve, reject) => {
|
||||
const done = (result: Error | JSC.ResponseMap[T]) => {
|
||||
this.#pendingRequests.delete(request.id);
|
||||
if (result instanceof Error) {
|
||||
reject(result);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
this.#pendingRequests.set(request.id, done);
|
||||
this.#send(request);
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.#socket) this.#socket.end();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import * as vscode from "vscode";
|
||||
import { activateBunDebug } from "./activate";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
activateBunDebug(context);
|
||||
}
|
||||
export function activate(context: vscode.ExtensionContext) {}
|
||||
|
||||
export function deactivate() {
|
||||
// No-op
|
||||
}
|
||||
export function deactivate() {}
|
||||
|
||||
10
packages/bun-vscode/test/fixtures/echo.ts
vendored
10
packages/bun-vscode/test/fixtures/echo.ts
vendored
@@ -1,10 +0,0 @@
|
||||
import { serve } from "bun";
|
||||
|
||||
serve({
|
||||
port: 9229,
|
||||
development: true,
|
||||
fetch(request, server) {
|
||||
return new Response(`Hello, ${request.url}!`);
|
||||
},
|
||||
inspector: true,
|
||||
});
|
||||
@@ -1,123 +0,0 @@
|
||||
import { beforeAll, afterAll, describe, test, expect, mock } from "bun:test";
|
||||
import { Worker } from "node:worker_threads";
|
||||
import { JSCClient, type JSC } from "../src/jsc";
|
||||
|
||||
let worker: Worker;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { pathname } = new URL("./fixtures/echo.ts", import.meta.url);
|
||||
worker = new Worker(pathname, { smol: true });
|
||||
while (true) {
|
||||
try {
|
||||
await fetch("http://localhost:9229/");
|
||||
break;
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
worker?.terminate();
|
||||
});
|
||||
|
||||
describe("JSCClient", () => {
|
||||
const onRequest = mock((request: JSC.Request) => {
|
||||
expect(request).toBeInstanceOf(Object);
|
||||
expect(request.id).toBeNumber();
|
||||
expect(request.method).toBeString();
|
||||
if (request.params) {
|
||||
expect(request.params).toBeInstanceOf(Object);
|
||||
} else {
|
||||
expect(request).toBeUndefined();
|
||||
}
|
||||
});
|
||||
const onResponse = mock((response: JSC.Response) => {
|
||||
expect(response).toBeInstanceOf(Object);
|
||||
expect(response.id).toBeNumber();
|
||||
if ("result" in response) {
|
||||
expect(response.result).toBeInstanceOf(Object);
|
||||
} else {
|
||||
expect(response.error).toBeInstanceOf(Object);
|
||||
expect(response.error.message).toBeString();
|
||||
}
|
||||
});
|
||||
const onEvent = mock((event: JSC.Event) => {
|
||||
expect(event).toBeInstanceOf(Object);
|
||||
expect(event.method).toBeString();
|
||||
if (event.params) {
|
||||
expect(event.params).toBeInstanceOf(Object);
|
||||
} else {
|
||||
expect(event).toBeUndefined();
|
||||
}
|
||||
});
|
||||
const onError = mock((error: Error) => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
const client = new JSCClient({
|
||||
url: "ws://localhost:9229/bun:inspect",
|
||||
onRequest,
|
||||
onResponse,
|
||||
onEvent,
|
||||
onError,
|
||||
});
|
||||
test("can connect", () => {
|
||||
expect(client.ready).resolves.toBeUndefined();
|
||||
});
|
||||
test("can send a request", () => {
|
||||
expect(client.fetch("Runtime.evaluate", { expression: "1 + 1" })).resolves.toStrictEqual({
|
||||
result: {
|
||||
type: "number",
|
||||
value: 2,
|
||||
description: "2",
|
||||
},
|
||||
wasThrown: false,
|
||||
});
|
||||
expect(onRequest).toHaveBeenCalled();
|
||||
expect(onRequest.mock.lastCall[0]).toStrictEqual({
|
||||
id: 1,
|
||||
method: "Runtime.evaluate",
|
||||
params: { expression: "1 + 1" },
|
||||
});
|
||||
expect(onResponse).toHaveBeenCalled();
|
||||
expect(onResponse.mock.lastCall[0]).toMatchObject({
|
||||
id: 1,
|
||||
result: {
|
||||
result: {
|
||||
type: "number",
|
||||
value: 2,
|
||||
description: "2",
|
||||
},
|
||||
wasThrown: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
test("can send an invalid request", () => {
|
||||
expect(
|
||||
client.fetch("Runtime.awaitPromise", {
|
||||
promiseObjectId: "this-does-not-exist",
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
name: "Error",
|
||||
message: expect.stringMatching(/promiseObjectId/),
|
||||
});
|
||||
expect(onRequest).toHaveBeenCalled();
|
||||
expect(onRequest.mock.lastCall[0]).toStrictEqual({
|
||||
id: 2,
|
||||
method: "Runtime.awaitPromise",
|
||||
params: {
|
||||
promiseObjectId: "this-does-not-exist",
|
||||
},
|
||||
});
|
||||
expect(onResponse).toHaveBeenCalled();
|
||||
expect(onResponse.mock.lastCall[0]).toMatchObject({
|
||||
id: 2,
|
||||
error: {
|
||||
code: expect.any(Number),
|
||||
message: expect.stringMatching(/promiseObjectId/),
|
||||
},
|
||||
});
|
||||
expect(onError).toHaveBeenCalled();
|
||||
});
|
||||
test("can disconnect", () => {
|
||||
expect(() => client.close()).not.toThrow();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user