mirror of
https://github.com/oven-sh/bun
synced 2026-02-24 10:37:20 +01:00
Compare commits
113 Commits
nektro-pat
...
jarred/fet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c989e4780c | ||
|
|
d2acb2eac0 | ||
|
|
de7eafbdd1 | ||
|
|
4114986c3e | ||
|
|
8aa451c2dc | ||
|
|
497cef9759 | ||
|
|
dd57b95546 | ||
|
|
ea7c4986d7 | ||
|
|
6c7edf2dbe | ||
|
|
bf2f153f5c | ||
|
|
f64a4c4ace | ||
|
|
0216431c98 | ||
|
|
ae289c4858 | ||
|
|
5d1609fe5c | ||
|
|
471fe7b886 | ||
|
|
08222eda71 | ||
|
|
6f8c5959d0 | ||
|
|
40d5e745c9 | ||
|
|
225bfd54fa | ||
|
|
a6ca8c40d4 | ||
|
|
b52ad226a5 | ||
|
|
5f8f805db9 | ||
|
|
37c98bebd6 | ||
|
|
bd01df19c1 | ||
|
|
7fd16ebffa | ||
|
|
1bb211df56 | ||
|
|
bdd0b89f16 | ||
|
|
841f593b12 | ||
|
|
3afd19c73c | ||
|
|
b6a231add3 | ||
|
|
ca86bae5d5 | ||
|
|
215fdb4697 | ||
|
|
578bdf1cd6 | ||
|
|
cf2fa30639 | ||
|
|
5b3c58bdf5 | ||
|
|
0d6d4faa51 | ||
|
|
5e4642295a | ||
|
|
68f026b3cd | ||
|
|
5e9563833d | ||
|
|
6dd44cbeda | ||
|
|
a9ce4d40c2 | ||
|
|
663f00b62b | ||
|
|
f21fffd1bf | ||
|
|
d92d8dc886 | ||
|
|
6d127ba3f4 | ||
|
|
c3d9e8c7af | ||
|
|
c25e744837 | ||
|
|
dc01a5d6a8 | ||
|
|
c434b2c191 | ||
|
|
8ca0eb831d | ||
|
|
b19f13f5c4 | ||
|
|
bb3d570ad0 | ||
|
|
a6f37b398c | ||
|
|
39af2a0a56 | ||
|
|
7f6bb30877 | ||
|
|
812288eb72 | ||
|
|
9cbe1ec300 | ||
|
|
4f8c1c9124 | ||
|
|
468a392fd5 | ||
|
|
f61f03fae3 | ||
|
|
a468d09064 | ||
|
|
898feb886f | ||
|
|
c5cd0e4575 | ||
|
|
f4a0fe40aa | ||
|
|
2d2e329ee3 | ||
|
|
618d2cb3ac | ||
|
|
6c915fc1d0 | ||
|
|
aa60ab3b65 | ||
|
|
f855ae8618 | ||
|
|
514a47cb54 | ||
|
|
1a1cf0a4d7 | ||
|
|
9fbe64619b | ||
|
|
642e0ba73c | ||
|
|
19d7a5fe53 | ||
|
|
c04a2d1dfc | ||
|
|
82cb82d828 | ||
|
|
4ae982be4e | ||
|
|
2d65063571 | ||
|
|
746cf2cf01 | ||
|
|
9c1fde0132 | ||
|
|
f8f76a6fe0 | ||
|
|
4117af6e46 | ||
|
|
5bcaf32ba3 | ||
|
|
d01bfb5aa2 | ||
|
|
78b495aff5 | ||
|
|
6adb3954fe | ||
|
|
b152fbefcd | ||
|
|
8c0c97a273 | ||
|
|
95fcee8b76 | ||
|
|
c3f63bcdc4 | ||
|
|
2283ed098f | ||
|
|
43dcb8fce1 | ||
|
|
0eb6a4c55e | ||
|
|
144db9ca52 | ||
|
|
a6a4ca1e49 | ||
|
|
314b4d9b44 | ||
|
|
0e3e33072b | ||
|
|
3681aa9f0a | ||
|
|
c9d0fd51a9 | ||
|
|
4fe8b71437 | ||
|
|
1efab7f42d | ||
|
|
61a3f08595 | ||
|
|
363595fd31 | ||
|
|
173f67d81e | ||
|
|
05d5ab7489 | ||
|
|
b7bd5a4cf5 | ||
|
|
ab4da13785 | ||
|
|
ab3cb68f66 | ||
|
|
795f14c1d1 | ||
|
|
708ed00705 | ||
|
|
ff4eccc3b4 | ||
|
|
ededc168cf | ||
|
|
46c750fc12 |
@@ -14,6 +14,7 @@ import {
|
||||
getChangedFiles,
|
||||
getCommit,
|
||||
getCommitMessage,
|
||||
getEnv,
|
||||
getLastSuccessfulBuild,
|
||||
getMainBranch,
|
||||
getTargetBranch,
|
||||
@@ -233,8 +234,8 @@ function getPipeline(options) {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isUsingNewAgent = platform => {
|
||||
const { os, distro } = platform;
|
||||
if (os === "linux" && distro === "alpine") {
|
||||
const { os } = platform;
|
||||
if (os === "linux") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -303,15 +304,23 @@ function getPipeline(options) {
|
||||
* @param {Target} target
|
||||
* @returns {Agent}
|
||||
*/
|
||||
const getZigAgent = target => {
|
||||
const { abi, arch } = target;
|
||||
// if (abi === "musl") {
|
||||
// const instanceType = arch === "aarch64" ? "c8g.large" : "c7i.large";
|
||||
// return getEmphemeralAgent("v2", target, instanceType);
|
||||
// }
|
||||
const getZigAgent = platform => {
|
||||
const { arch } = platform;
|
||||
const instanceType = arch === "aarch64" ? "c8g.2xlarge" : "c7i.2xlarge";
|
||||
return {
|
||||
queue: "build-zig",
|
||||
robobun: true,
|
||||
robobun2: true,
|
||||
os: "linux",
|
||||
arch,
|
||||
distro: "debian",
|
||||
release: "11",
|
||||
"image-name": `linux-${arch}-debian-11-v5`, // v5 is not on main yet
|
||||
"instance-type": instanceType,
|
||||
};
|
||||
// TODO: Temporarily disable due to configuration
|
||||
// return {
|
||||
// queue: "build-zig",
|
||||
// };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -358,6 +367,21 @@ function getPipeline(options) {
|
||||
* @link https://buildkite.com/docs/pipelines/command-step
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Platform} platform
|
||||
* @param {string} [step]
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const getDependsOn = (platform, step) => {
|
||||
if (imagePlatforms.has(getImageKey(platform))) {
|
||||
const key = `${getImageKey(platform)}-build-image`;
|
||||
if (key !== step) {
|
||||
return [key];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Platform} platform
|
||||
* @returns {Step}
|
||||
@@ -374,81 +398,85 @@ function getPipeline(options) {
|
||||
env: {
|
||||
DEBUG: "1",
|
||||
},
|
||||
retry: getRetry(),
|
||||
command: `node ./scripts/machine.mjs ${action} --ci --cloud=aws --os=${os} --arch=${arch} --distro=${distro} --distro-version=${release}`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Target} target
|
||||
* @param {Platform} platform
|
||||
* @returns {Step}
|
||||
*/
|
||||
const getBuildVendorStep = target => {
|
||||
const getBuildVendorStep = platform => {
|
||||
return {
|
||||
key: `${getTargetKey(target)}-build-vendor`,
|
||||
label: `${getTargetLabel(target)} - build-vendor`,
|
||||
agents: getBuildAgent(target),
|
||||
key: `${getTargetKey(platform)}-build-vendor`,
|
||||
label: `${getTargetLabel(platform)} - build-vendor`,
|
||||
depends_on: getDependsOn(platform),
|
||||
agents: getBuildAgent(platform),
|
||||
retry: getRetry(),
|
||||
cancel_on_build_failing: isMergeQueue(),
|
||||
env: getBuildEnv(target),
|
||||
env: getBuildEnv(platform),
|
||||
command: "bun run build:ci --target dependencies",
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Target} target
|
||||
* @param {Platform} platform
|
||||
* @returns {Step}
|
||||
*/
|
||||
const getBuildCppStep = target => {
|
||||
const getBuildCppStep = platform => {
|
||||
return {
|
||||
key: `${getTargetKey(target)}-build-cpp`,
|
||||
label: `${getTargetLabel(target)} - build-cpp`,
|
||||
agents: getBuildAgent(target),
|
||||
key: `${getTargetKey(platform)}-build-cpp`,
|
||||
label: `${getTargetLabel(platform)} - build-cpp`,
|
||||
depends_on: getDependsOn(platform),
|
||||
agents: getBuildAgent(platform),
|
||||
retry: getRetry(),
|
||||
cancel_on_build_failing: isMergeQueue(),
|
||||
env: {
|
||||
BUN_CPP_ONLY: "ON",
|
||||
...getBuildEnv(target),
|
||||
...getBuildEnv(platform),
|
||||
},
|
||||
command: "bun run build:ci --target bun",
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Target} target
|
||||
* @param {Platform} platform
|
||||
* @returns {Step}
|
||||
*/
|
||||
const getBuildZigStep = target => {
|
||||
const toolchain = getBuildToolchain(target);
|
||||
const getBuildZigStep = platform => {
|
||||
const toolchain = getBuildToolchain(platform);
|
||||
return {
|
||||
key: `${getTargetKey(target)}-build-zig`,
|
||||
label: `${getTargetLabel(target)} - build-zig`,
|
||||
agents: getZigAgent(target),
|
||||
retry: getRetry(1), // FIXME: Sometimes zig build hangs, so we need to retry once
|
||||
key: `${getTargetKey(platform)}-build-zig`,
|
||||
label: `${getTargetLabel(platform)} - build-zig`,
|
||||
depends_on: getDependsOn(platform),
|
||||
agents: getZigAgent(platform),
|
||||
retry: getRetry(),
|
||||
cancel_on_build_failing: isMergeQueue(),
|
||||
env: getBuildEnv(target),
|
||||
env: getBuildEnv(platform),
|
||||
command: `bun run build:ci --target bun-zig --toolchain ${toolchain}`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Target} target
|
||||
* @param {Platform} platform
|
||||
* @returns {Step}
|
||||
*/
|
||||
const getBuildBunStep = target => {
|
||||
const getBuildBunStep = platform => {
|
||||
return {
|
||||
key: `${getTargetKey(target)}-build-bun`,
|
||||
label: `${getTargetLabel(target)} - build-bun`,
|
||||
key: `${getTargetKey(platform)}-build-bun`,
|
||||
label: `${getTargetLabel(platform)} - build-bun`,
|
||||
depends_on: [
|
||||
`${getTargetKey(target)}-build-vendor`,
|
||||
`${getTargetKey(target)}-build-cpp`,
|
||||
`${getTargetKey(target)}-build-zig`,
|
||||
`${getTargetKey(platform)}-build-vendor`,
|
||||
`${getTargetKey(platform)}-build-cpp`,
|
||||
`${getTargetKey(platform)}-build-zig`,
|
||||
],
|
||||
agents: getBuildAgent(target),
|
||||
agents: getBuildAgent(platform),
|
||||
retry: getRetry(),
|
||||
cancel_on_build_failing: isMergeQueue(),
|
||||
env: {
|
||||
BUN_LINK_ONLY: "ON",
|
||||
...getBuildEnv(target),
|
||||
...getBuildEnv(platform),
|
||||
},
|
||||
command: "bun run build:ci --target bun",
|
||||
};
|
||||
@@ -472,8 +500,8 @@ function getPipeline(options) {
|
||||
} else {
|
||||
parallelism = 10;
|
||||
}
|
||||
let depends;
|
||||
let env;
|
||||
let depends = [];
|
||||
if (buildId) {
|
||||
env = {
|
||||
BUILDKITE_ARTIFACT_BUILD_ID: buildId,
|
||||
@@ -487,14 +515,20 @@ function getPipeline(options) {
|
||||
// Because of this, we don't know if the run was fatal, or soft-failed.
|
||||
retry = getRetry(1);
|
||||
}
|
||||
let soft_fail;
|
||||
if (isMainBranch()) {
|
||||
soft_fail = true;
|
||||
} else {
|
||||
soft_fail = [{ exit_status: 2 }];
|
||||
}
|
||||
return {
|
||||
key: `${getPlatformKey(platform)}-test-bun`,
|
||||
label: `${getPlatformLabel(platform)} - test-bun`,
|
||||
depends_on: depends,
|
||||
depends_on: [...depends, ...getDependsOn(platform)],
|
||||
agents: getTestAgent(platform),
|
||||
retry,
|
||||
cancel_on_build_failing: isMergeQueue(),
|
||||
soft_fail: isMainBranch(),
|
||||
soft_fail,
|
||||
parallelism,
|
||||
command,
|
||||
env,
|
||||
@@ -530,35 +564,20 @@ function getPipeline(options) {
|
||||
{ os: "darwin", arch: "x64", release: "14" },
|
||||
{ os: "darwin", arch: "x64", release: "13" },
|
||||
{ os: "linux", arch: "aarch64", distro: "debian", release: "12" },
|
||||
// { os: "linux", arch: "aarch64", distro: "debian", release: "11" },
|
||||
// { os: "linux", arch: "aarch64", distro: "debian", release: "10" },
|
||||
{ os: "linux", arch: "aarch64", distro: "debian", release: "11" },
|
||||
{ os: "linux", arch: "x64", distro: "debian", release: "12" },
|
||||
// { os: "linux", arch: "x64", distro: "debian", release: "11" },
|
||||
// { os: "linux", arch: "x64", distro: "debian", release: "10" },
|
||||
{ os: "linux", arch: "x64", distro: "debian", release: "11" },
|
||||
{ os: "linux", arch: "x64", baseline: true, distro: "debian", release: "12" },
|
||||
// { os: "linux", arch: "x64", baseline: true, distro: "debian", release: "11" },
|
||||
// { os: "linux", arch: "x64", baseline: true, distro: "debian", release: "10" },
|
||||
// { os: "linux", arch: "aarch64", distro: "ubuntu", release: "24.04" },
|
||||
{ os: "linux", arch: "x64", baseline: true, distro: "debian", release: "11" },
|
||||
{ os: "linux", arch: "aarch64", distro: "ubuntu", release: "22.04" },
|
||||
{ os: "linux", arch: "aarch64", distro: "ubuntu", release: "20.04" },
|
||||
// { os: "linux", arch: "x64", distro: "ubuntu", release: "24.04" },
|
||||
{ os: "linux", arch: "x64", distro: "ubuntu", release: "22.04" },
|
||||
{ os: "linux", arch: "x64", distro: "ubuntu", release: "20.04" },
|
||||
// { os: "linux", arch: "x64", baseline: true, distro: "ubuntu", release: "24.04" },
|
||||
{ os: "linux", arch: "x64", baseline: true, distro: "ubuntu", release: "22.04" },
|
||||
{ os: "linux", arch: "x64", baseline: true, distro: "ubuntu", release: "20.04" },
|
||||
// { os: "linux", arch: "aarch64", distro: "amazonlinux", release: "2023" },
|
||||
// { os: "linux", arch: "aarch64", distro: "amazonlinux", release: "2" },
|
||||
// { os: "linux", arch: "x64", distro: "amazonlinux", release: "2023" },
|
||||
// { os: "linux", arch: "x64", distro: "amazonlinux", release: "2" },
|
||||
// { os: "linux", arch: "x64", baseline: true, distro: "amazonlinux", release: "2023" },
|
||||
// { os: "linux", arch: "x64", baseline: true, distro: "amazonlinux", release: "2" },
|
||||
{ os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.20" },
|
||||
// { os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.17" },
|
||||
{ os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.20" },
|
||||
// { os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.17" },
|
||||
{ os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.20" },
|
||||
// { os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.17" },
|
||||
{ os: "windows", arch: "x64", release: "2019" },
|
||||
{ os: "windows", arch: "x64", baseline: true, release: "2019" },
|
||||
];
|
||||
@@ -614,15 +633,6 @@ function getPipeline(options) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (imagePlatforms.has(getImageKey(platform))) {
|
||||
for (const step of platformSteps) {
|
||||
if (step.agents?.["image-name"]) {
|
||||
step.depends_on ??= [];
|
||||
step.depends_on.push(`${getImageKey(platform)}-build-image`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
steps.push({
|
||||
key: getTargetKey(platform),
|
||||
group: getTargetLabel(platform),
|
||||
@@ -662,14 +672,18 @@ async function main() {
|
||||
}
|
||||
|
||||
let changedFiles;
|
||||
let changedFilesBranch;
|
||||
if (!isFork() && !isMainBranch()) {
|
||||
console.log("Checking changed files...");
|
||||
const baseRef = lastBuild?.commit_id || getTargetBranch() || getMainBranch();
|
||||
const targetRef = getTargetBranch();
|
||||
console.log(" - Target Ref:", targetRef);
|
||||
const baseRef = lastBuild?.commit_id || targetRef || getMainBranch();
|
||||
console.log(" - Base Ref:", baseRef);
|
||||
const headRef = getCommit();
|
||||
console.log(" - Head Ref:", headRef);
|
||||
|
||||
changedFiles = await getChangedFiles(undefined, baseRef, headRef);
|
||||
changedFilesBranch = await getChangedFiles(undefined, targetRef, headRef);
|
||||
if (changedFiles) {
|
||||
if (changedFiles.length) {
|
||||
changedFiles.forEach(filename => console.log(` - ${filename}`));
|
||||
@@ -694,7 +708,7 @@ async function main() {
|
||||
forceBuild = true;
|
||||
}
|
||||
for (const coref of [".buildkite/ci.mjs", "scripts/utils.mjs", "scripts/bootstrap.sh", "scripts/machine.mjs"]) {
|
||||
if (changedFiles && changedFiles.includes(coref)) {
|
||||
if (changedFilesBranch && changedFilesBranch.includes(coref)) {
|
||||
console.log(" - Yes, because the list of changed files contains:", coref);
|
||||
forceBuild = true;
|
||||
ciFileChanged = true;
|
||||
@@ -784,7 +798,10 @@ async function main() {
|
||||
|
||||
console.log("Checking if build is a named release...");
|
||||
let buildRelease;
|
||||
{
|
||||
if (/^(1|true|on|yes)$/i.test(getEnv("RELEASE", false))) {
|
||||
console.log(" - Yes, because RELEASE environment variable is set");
|
||||
buildRelease = true;
|
||||
} else {
|
||||
const message = getCommitMessage();
|
||||
const match = /\[(release|release build|build release)\]/i.exec(message);
|
||||
if (match) {
|
||||
|
||||
92
.github/workflows/update-cares.yml
vendored
Normal file
92
.github/workflows/update-cares.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: Update c-ares
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 4 * * 0"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check c-ares version
|
||||
id: check-version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Extract the commit hash from the line after COMMIT
|
||||
CURRENT_VERSION=$(awk '/[[:space:]]*COMMIT[[:space:]]*$/{getline; gsub(/^[[:space:]]+|[[:space:]]+$/,"",$0); print}' cmake/targets/BuildCares.cmake)
|
||||
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "Error: Could not find COMMIT line in BuildCares.cmake"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate that it looks like a git hash
|
||||
if ! [[ $CURRENT_VERSION =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Error: Invalid git hash format in BuildCares.cmake"
|
||||
echo "Found: $CURRENT_VERSION"
|
||||
echo "Expected: 40 character hexadecimal string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
LATEST_RELEASE=$(curl -sL https://api.github.com/repos/c-ares/c-ares/releases/latest)
|
||||
if [ -z "$LATEST_RELEASE" ]; then
|
||||
echo "Error: Failed to fetch latest release from GitHub API"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LATEST_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tag_name')
|
||||
if [ -z "$LATEST_TAG" ] || [ "$LATEST_TAG" = "null" ]; then
|
||||
echo "Error: Could not extract tag name from GitHub API response"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LATEST_SHA=$(curl -sL "https://api.github.com/repos/c-ares/c-ares/git/ref/tags/$LATEST_TAG" | jq -r '.object.sha')
|
||||
if [ -z "$LATEST_SHA" ] || [ "$LATEST_SHA" = "null" ]; then
|
||||
echo "Error: Could not fetch SHA for tag $LATEST_TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ $LATEST_SHA =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Error: Invalid SHA format received from GitHub"
|
||||
echo "Found: $LATEST_SHA"
|
||||
echo "Expected: 40 character hexadecimal string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "latest=$LATEST_SHA" >> $GITHUB_OUTPUT
|
||||
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update version if needed
|
||||
if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Handle multi-line format where COMMIT and its value are on separate lines
|
||||
sed -i -E '/[[:space:]]*COMMIT[[:space:]]*$/{n;s/[[:space:]]*([0-9a-f]+)[[:space:]]*$/ ${{ steps.check-version.outputs.latest }}/}' cmake/targets/BuildCares.cmake
|
||||
|
||||
- name: Create Pull Request
|
||||
if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
add-paths: |
|
||||
cmake/targets/BuildCares.cmake
|
||||
commit-message: "deps: update c-ares to ${{ steps.check-version.outputs.tag }} (${{ steps.check-version.outputs.latest }})"
|
||||
title: "deps: update c-ares to ${{ steps.check-version.outputs.tag }}"
|
||||
delete-branch: true
|
||||
branch: deps/update-cares-${{ github.run_number }}
|
||||
body: |
|
||||
## What does this PR do?
|
||||
|
||||
Updates c-ares to version ${{ steps.check-version.outputs.tag }}
|
||||
|
||||
Auto-updated by [this workflow](https://github.com/oven-sh/bun/actions/workflows/update-cares.yml)
|
||||
92
.github/workflows/update-libarchive.yml
vendored
Normal file
92
.github/workflows/update-libarchive.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: Update libarchive
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 3 * * 0"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check libarchive version
|
||||
id: check-version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Extract the commit hash from the line after COMMIT
|
||||
CURRENT_VERSION=$(awk '/[[:space:]]*COMMIT[[:space:]]*$/{getline; gsub(/^[[:space:]]+|[[:space:]]+$/,"",$0); print}' cmake/targets/BuildLibArchive.cmake)
|
||||
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "Error: Could not find COMMIT line in BuildLibArchive.cmake"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate that it looks like a git hash
|
||||
if ! [[ $CURRENT_VERSION =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Error: Invalid git hash format in BuildLibArchive.cmake"
|
||||
echo "Found: $CURRENT_VERSION"
|
||||
echo "Expected: 40 character hexadecimal string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
LATEST_RELEASE=$(curl -sL https://api.github.com/repos/libarchive/libarchive/releases/latest)
|
||||
if [ -z "$LATEST_RELEASE" ]; then
|
||||
echo "Error: Failed to fetch latest release from GitHub API"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LATEST_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tag_name')
|
||||
if [ -z "$LATEST_TAG" ] || [ "$LATEST_TAG" = "null" ]; then
|
||||
echo "Error: Could not extract tag name from GitHub API response"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LATEST_SHA=$(curl -sL "https://api.github.com/repos/libarchive/libarchive/git/ref/tags/$LATEST_TAG" | jq -r '.object.sha')
|
||||
if [ -z "$LATEST_SHA" ] || [ "$LATEST_SHA" = "null" ]; then
|
||||
echo "Error: Could not fetch SHA for tag $LATEST_TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ $LATEST_SHA =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Error: Invalid SHA format received from GitHub"
|
||||
echo "Found: $LATEST_SHA"
|
||||
echo "Expected: 40 character hexadecimal string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "latest=$LATEST_SHA" >> $GITHUB_OUTPUT
|
||||
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update version if needed
|
||||
if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Handle multi-line format where COMMIT and its value are on separate lines
|
||||
sed -i -E '/[[:space:]]*COMMIT[[:space:]]*$/{n;s/[[:space:]]*([0-9a-f]+)[[:space:]]*$/ ${{ steps.check-version.outputs.latest }}/}' cmake/targets/BuildLibArchive.cmake
|
||||
|
||||
- name: Create Pull Request
|
||||
if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
add-paths: |
|
||||
cmake/targets/BuildLibArchive.cmake
|
||||
commit-message: "deps: update libarchive to ${{ steps.check-version.outputs.tag }} (${{ steps.check-version.outputs.latest }})"
|
||||
title: "deps: update libarchive to ${{ steps.check-version.outputs.tag }}"
|
||||
delete-branch: true
|
||||
branch: deps/update-libarchive-${{ github.run_number }}
|
||||
body: |
|
||||
## What does this PR do?
|
||||
|
||||
Updates libarchive to version ${{ steps.check-version.outputs.tag }}
|
||||
|
||||
Auto-updated by [this workflow](https://github.com/oven-sh/bun/actions/workflows/update-libarchive.yml)
|
||||
92
.github/workflows/update-libdeflate.yml
vendored
Normal file
92
.github/workflows/update-libdeflate.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: Update libdeflate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 2 * * 0"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check libdeflate version
|
||||
id: check-version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Extract the commit hash from the line after COMMIT
|
||||
CURRENT_VERSION=$(awk '/[[:space:]]*COMMIT[[:space:]]*$/{getline; gsub(/^[[:space:]]+|[[:space:]]+$/,"",$0); print}' cmake/targets/BuildLibDeflate.cmake)
|
||||
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "Error: Could not find COMMIT line in BuildLibDeflate.cmake"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate that it looks like a git hash
|
||||
if ! [[ $CURRENT_VERSION =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Error: Invalid git hash format in BuildLibDeflate.cmake"
|
||||
echo "Found: $CURRENT_VERSION"
|
||||
echo "Expected: 40 character hexadecimal string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
LATEST_RELEASE=$(curl -sL https://api.github.com/repos/ebiggers/libdeflate/releases/latest)
|
||||
if [ -z "$LATEST_RELEASE" ]; then
|
||||
echo "Error: Failed to fetch latest release from GitHub API"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LATEST_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tag_name')
|
||||
if [ -z "$LATEST_TAG" ] || [ "$LATEST_TAG" = "null" ]; then
|
||||
echo "Error: Could not extract tag name from GitHub API response"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LATEST_SHA=$(curl -sL "https://api.github.com/repos/ebiggers/libdeflate/git/ref/tags/$LATEST_TAG" | jq -r '.object.sha')
|
||||
if [ -z "$LATEST_SHA" ] || [ "$LATEST_SHA" = "null" ]; then
|
||||
echo "Error: Could not fetch SHA for tag $LATEST_TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ $LATEST_SHA =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Error: Invalid SHA format received from GitHub"
|
||||
echo "Found: $LATEST_SHA"
|
||||
echo "Expected: 40 character hexadecimal string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "latest=$LATEST_SHA" >> $GITHUB_OUTPUT
|
||||
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update version if needed
|
||||
if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Handle multi-line format where COMMIT and its value are on separate lines
|
||||
sed -i -E '/[[:space:]]*COMMIT[[:space:]]*$/{n;s/[[:space:]]*([0-9a-f]+)[[:space:]]*$/ ${{ steps.check-version.outputs.latest }}/}' cmake/targets/BuildLibDeflate.cmake
|
||||
|
||||
- name: Create Pull Request
|
||||
if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
add-paths: |
|
||||
cmake/targets/BuildLibDeflate.cmake
|
||||
commit-message: "deps: update libdeflate to ${{ steps.check-version.outputs.tag }} (${{ steps.check-version.outputs.latest }})"
|
||||
title: "deps: update libdeflate to ${{ steps.check-version.outputs.tag }}"
|
||||
delete-branch: true
|
||||
branch: deps/update-libdeflate-${{ github.run_number }}
|
||||
body: |
|
||||
## What does this PR do?
|
||||
|
||||
Updates libdeflate to version ${{ steps.check-version.outputs.tag }}
|
||||
|
||||
Auto-updated by [this workflow](https://github.com/oven-sh/bun/actions/workflows/update-libdeflate.yml)
|
||||
92
.github/workflows/update-lolhtml.yml
vendored
Normal file
92
.github/workflows/update-lolhtml.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: Update lolhtml
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 1 * * 0"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check lolhtml version
|
||||
id: check-version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Extract the commit hash from the line after COMMIT
|
||||
CURRENT_VERSION=$(awk '/[[:space:]]*COMMIT[[:space:]]*$/{getline; gsub(/^[[:space:]]+|[[:space:]]+$/,"",$0); print}' cmake/targets/BuildLolHtml.cmake)
|
||||
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "Error: Could not find COMMIT line in BuildLolHtml.cmake"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate that it looks like a git hash
|
||||
if ! [[ $CURRENT_VERSION =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Error: Invalid git hash format in BuildLolHtml.cmake"
|
||||
echo "Found: $CURRENT_VERSION"
|
||||
echo "Expected: 40 character hexadecimal string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
LATEST_RELEASE=$(curl -sL https://api.github.com/repos/cloudflare/lol-html/releases/latest)
|
||||
if [ -z "$LATEST_RELEASE" ]; then
|
||||
echo "Error: Failed to fetch latest release from GitHub API"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LATEST_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tag_name')
|
||||
if [ -z "$LATEST_TAG" ] || [ "$LATEST_TAG" = "null" ]; then
|
||||
echo "Error: Could not extract tag name from GitHub API response"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LATEST_SHA=$(curl -sL "https://api.github.com/repos/cloudflare/lol-html/git/ref/tags/$LATEST_TAG" | jq -r '.object.sha')
|
||||
if [ -z "$LATEST_SHA" ] || [ "$LATEST_SHA" = "null" ]; then
|
||||
echo "Error: Could not fetch SHA for tag $LATEST_TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ $LATEST_SHA =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Error: Invalid SHA format received from GitHub"
|
||||
echo "Found: $LATEST_SHA"
|
||||
echo "Expected: 40 character hexadecimal string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "latest=$LATEST_SHA" >> $GITHUB_OUTPUT
|
||||
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update version if needed
|
||||
if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Handle multi-line format where COMMIT and its value are on separate lines
|
||||
sed -i -E '/[[:space:]]*COMMIT[[:space:]]*$/{n;s/[[:space:]]*([0-9a-f]+)[[:space:]]*$/ ${{ steps.check-version.outputs.latest }}/}' cmake/targets/BuildLolHtml.cmake
|
||||
|
||||
- name: Create Pull Request
|
||||
if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
add-paths: |
|
||||
cmake/targets/BuildLolHtml.cmake
|
||||
commit-message: "deps: update lolhtml to ${{ steps.check-version.outputs.tag }} (${{ steps.check-version.outputs.latest }})"
|
||||
title: "deps: update lolhtml to ${{ steps.check-version.outputs.tag }}"
|
||||
delete-branch: true
|
||||
branch: deps/update-lolhtml-${{ github.run_number }}
|
||||
body: |
|
||||
## What does this PR do?
|
||||
|
||||
Updates lolhtml to version ${{ steps.check-version.outputs.tag }}
|
||||
|
||||
Auto-updated by [this workflow](https://github.com/oven-sh/bun/actions/workflows/update-lolhtml.yml)
|
||||
92
.github/workflows/update-lshpack.yml
vendored
Normal file
92
.github/workflows/update-lshpack.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: Update lshpack
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 5 * * 0"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check lshpack version
|
||||
id: check-version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Extract the commit hash from the line after COMMIT
|
||||
CURRENT_VERSION=$(awk '/[[:space:]]*COMMIT[[:space:]]*$/{getline; gsub(/^[[:space:]]+|[[:space:]]+$/,"",$0); print}' cmake/targets/BuildLshpack.cmake)
|
||||
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "Error: Could not find COMMIT line in BuildLshpack.cmake"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate that it looks like a git hash
|
||||
if ! [[ $CURRENT_VERSION =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Error: Invalid git hash format in BuildLshpack.cmake"
|
||||
echo "Found: $CURRENT_VERSION"
|
||||
echo "Expected: 40 character hexadecimal string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
LATEST_RELEASE=$(curl -sL https://api.github.com/repos/litespeedtech/ls-hpack/releases/latest)
|
||||
if [ -z "$LATEST_RELEASE" ]; then
|
||||
echo "Error: Failed to fetch latest release from GitHub API"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LATEST_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tag_name')
|
||||
if [ -z "$LATEST_TAG" ] || [ "$LATEST_TAG" = "null" ]; then
|
||||
echo "Error: Could not extract tag name from GitHub API response"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LATEST_SHA=$(curl -sL "https://api.github.com/repos/litespeedtech/ls-hpack/git/ref/tags/$LATEST_TAG" | jq -r '.object.sha')
|
||||
if [ -z "$LATEST_SHA" ] || [ "$LATEST_SHA" = "null" ]; then
|
||||
echo "Error: Could not fetch SHA for tag $LATEST_TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ $LATEST_SHA =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "Error: Invalid SHA format received from GitHub"
|
||||
echo "Found: $LATEST_SHA"
|
||||
echo "Expected: 40 character hexadecimal string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "latest=$LATEST_SHA" >> $GITHUB_OUTPUT
|
||||
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update version if needed
|
||||
if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Handle multi-line format where COMMIT and its value are on separate lines
|
||||
sed -i -E '/[[:space:]]*COMMIT[[:space:]]*$/{n;s/[[:space:]]*([0-9a-f]+)[[:space:]]*$/ ${{ steps.check-version.outputs.latest }}/}' cmake/targets/BuildLshpack.cmake
|
||||
|
||||
- name: Create Pull Request
|
||||
if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
add-paths: |
|
||||
cmake/targets/BuildLshpack.cmake
|
||||
commit-message: "deps: update lshpack to ${{ steps.check-version.outputs.tag }} (${{ steps.check-version.outputs.latest }})"
|
||||
title: "deps: update lshpack to ${{ steps.check-version.outputs.tag }}"
|
||||
delete-branch: true
|
||||
branch: deps/update-lshpack-${{ github.run_number }}
|
||||
body: |
|
||||
## What does this PR do?
|
||||
|
||||
Updates lshpack to version ${{ steps.check-version.outputs.tag }}
|
||||
|
||||
Auto-updated by [this workflow](https://github.com/oven-sh/bun/actions/workflows/update-lshpack.yml)
|
||||
109
.github/workflows/update-sqlite3.yml
vendored
Normal file
109
.github/workflows/update-sqlite3.yml
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
name: Update SQLite3
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 6 * * 0" # Run weekly
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check SQLite version
|
||||
id: check-version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Get current version from the header file using SQLITE_VERSION_NUMBER
|
||||
CURRENT_VERSION_NUM=$(grep -o '#define SQLITE_VERSION_NUMBER [0-9]\+' src/bun.js/bindings/sqlite/sqlite3_local.h | awk '{print $3}' | tr -d '\n\r')
|
||||
if [ -z "$CURRENT_VERSION_NUM" ]; then
|
||||
echo "Error: Could not find SQLITE_VERSION_NUMBER in sqlite3_local.h"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Convert numeric version to semantic version for display
|
||||
CURRENT_MAJOR=$((CURRENT_VERSION_NUM / 1000000))
|
||||
CURRENT_MINOR=$((($CURRENT_VERSION_NUM / 1000) % 1000))
|
||||
CURRENT_PATCH=$((CURRENT_VERSION_NUM % 1000))
|
||||
CURRENT_VERSION="$CURRENT_MAJOR.$CURRENT_MINOR.$CURRENT_PATCH"
|
||||
|
||||
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "current_num=$CURRENT_VERSION_NUM" >> $GITHUB_OUTPUT
|
||||
|
||||
# Fetch SQLite download page
|
||||
DOWNLOAD_PAGE=$(curl -sL https://sqlite.org/download.html)
|
||||
if [ -z "$DOWNLOAD_PAGE" ]; then
|
||||
echo "Error: Failed to fetch SQLite download page"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract latest version and year from the amalgamation link
|
||||
LATEST_INFO=$(echo "$DOWNLOAD_PAGE" | grep -o 'sqlite-amalgamation-[0-9]\{7\}.zip' | head -n1)
|
||||
LATEST_YEAR=$(echo "$DOWNLOAD_PAGE" | grep -o '[0-9]\{4\}/sqlite-amalgamation-[0-9]\{7\}.zip' | head -n1 | cut -d'/' -f1 | tr -d '\n\r')
|
||||
LATEST_VERSION_NUM=$(echo "$LATEST_INFO" | grep -o '[0-9]\{7\}' | tr -d '\n\r')
|
||||
|
||||
if [ -z "$LATEST_VERSION_NUM" ] || [ -z "$LATEST_YEAR" ]; then
|
||||
echo "Error: Could not extract latest version info"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Convert numeric version to semantic version for display
|
||||
LATEST_MAJOR=$((10#$LATEST_VERSION_NUM / 1000000))
|
||||
LATEST_MINOR=$((($LATEST_VERSION_NUM / 1000) % 1000))
|
||||
LATEST_PATCH=$((10#$LATEST_VERSION_NUM % 1000))
|
||||
LATEST_VERSION="$LATEST_MAJOR.$LATEST_MINOR.$LATEST_PATCH"
|
||||
|
||||
echo "latest=$LATEST_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "latest_year=$LATEST_YEAR" >> $GITHUB_OUTPUT
|
||||
echo "latest_num=$LATEST_VERSION_NUM" >> $GITHUB_OUTPUT
|
||||
|
||||
# Debug output
|
||||
echo "Current version: $CURRENT_VERSION ($CURRENT_VERSION_NUM)"
|
||||
echo "Latest version: $LATEST_VERSION ($LATEST_VERSION_NUM)"
|
||||
|
||||
- name: Update SQLite if needed
|
||||
if: success() && steps.check-version.outputs.current_num < steps.check-version.outputs.latest_num
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
cd $TEMP_DIR
|
||||
|
||||
echo "Downloading from: https://sqlite.org/${{ steps.check-version.outputs.latest_year }}/sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}.zip"
|
||||
|
||||
# Download and extract latest version
|
||||
wget "https://sqlite.org/${{ steps.check-version.outputs.latest_year }}/sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}.zip"
|
||||
unzip "sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}.zip"
|
||||
cd "sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}"
|
||||
|
||||
# Add header comment and copy files
|
||||
echo "// clang-format off" > $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3.c
|
||||
cat sqlite3.c >> $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3.c
|
||||
|
||||
echo "// clang-format off" > $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3_local.h
|
||||
cat sqlite3.h >> $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3_local.h
|
||||
|
||||
- name: Create Pull Request
|
||||
if: success() && steps.check-version.outputs.current_num < steps.check-version.outputs.latest_num
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
add-paths: |
|
||||
src/bun.js/bindings/sqlite/sqlite3.c
|
||||
src/bun.js/bindings/sqlite/sqlite3_local.h
|
||||
commit-message: "deps: update sqlite to ${{ steps.check-version.outputs.latest }}"
|
||||
title: "deps: update sqlite to ${{ steps.check-version.outputs.latest }}"
|
||||
delete-branch: true
|
||||
branch: deps/update-sqlite-${{ steps.check-version.outputs.latest }}
|
||||
body: |
|
||||
## What does this PR do?
|
||||
|
||||
Updates SQLite to version ${{ steps.check-version.outputs.latest }}
|
||||
|
||||
Auto-updated by [this workflow](https://github.com/oven-sh/bun/actions/workflows/update-sqlite3.yml)
|
||||
19
.gitignore
vendored
19
.gitignore
vendored
@@ -26,6 +26,7 @@
|
||||
*.db
|
||||
*.dmg
|
||||
*.dSYM
|
||||
*.generated.ts
|
||||
*.jsb
|
||||
*.lib
|
||||
*.log
|
||||
@@ -53,8 +54,8 @@
|
||||
/test-report.md
|
||||
/test.js
|
||||
/test.ts
|
||||
/testdir
|
||||
/test.zig
|
||||
/testdir
|
||||
build
|
||||
build.ninja
|
||||
bun-binary
|
||||
@@ -111,8 +112,10 @@ pnpm-lock.yaml
|
||||
profile.json
|
||||
README.md.template
|
||||
release/
|
||||
scripts/env.local
|
||||
sign.*.json
|
||||
sign.json
|
||||
src/bake/generated.ts
|
||||
src/bun.js/bindings-obj
|
||||
src/bun.js/bindings/GeneratedJS2Native.zig
|
||||
src/bun.js/debug-bindings-obj
|
||||
@@ -131,6 +134,7 @@ src/runtime.version
|
||||
src/tests.zig
|
||||
test.txt
|
||||
test/js/bun/glob/fixtures
|
||||
test/node.js/upstream
|
||||
tsconfig.tsbuildinfo
|
||||
txt.js
|
||||
x64
|
||||
@@ -142,6 +146,9 @@ test/node.js/upstream
|
||||
scripts/env.local
|
||||
*.generated.ts
|
||||
src/bake/generated.ts
|
||||
test/cli/install/registry/packages/publish-pkg-*
|
||||
test/cli/install/registry/packages/@secret/publish-pkg-8
|
||||
test/js/third_party/prisma/prisma/sqlite/dev.db-journal
|
||||
|
||||
# Dependencies
|
||||
/vendor
|
||||
@@ -149,22 +156,24 @@ src/bake/generated.ts
|
||||
# Dependencies (before CMake)
|
||||
# These can be removed in the far future
|
||||
/src/bun.js/WebKit
|
||||
/src/deps/WebKit
|
||||
/src/deps/boringssl
|
||||
/src/deps/brotli
|
||||
/src/deps/c*ares
|
||||
/src/deps/lol*html
|
||||
/src/deps/libarchive
|
||||
/src/deps/libdeflate
|
||||
/src/deps/libuv
|
||||
/src/deps/lol*html
|
||||
/src/deps/ls*hpack
|
||||
/src/deps/mimalloc
|
||||
/src/deps/picohttpparser
|
||||
/src/deps/tinycc
|
||||
/src/deps/zstd
|
||||
/src/deps/zlib
|
||||
/src/deps/WebKit
|
||||
/src/deps/zig
|
||||
/src/deps/zlib
|
||||
/src/deps/zstd
|
||||
|
||||
# Generated files
|
||||
|
||||
.buildkite/ci.yml
|
||||
*.sock
|
||||
scratch*.{js,ts,tsx,cjs,mjs}
|
||||
3
.vscode/launch.json
generated
vendored
3
.vscode/launch.json
generated
vendored
@@ -224,8 +224,11 @@
|
||||
"cwd": "${fileDirname}",
|
||||
"env": {
|
||||
"FORCE_COLOR": "1",
|
||||
// "BUN_DEBUG_DEBUGGER": "1",
|
||||
// "BUN_DEBUG_INTERNAL_DEBUGGER": "1",
|
||||
"BUN_DEBUG_QUIET_LOGS": "1",
|
||||
"BUN_GARBAGE_COLLECTOR_LEVEL": "2",
|
||||
// "BUN_INSPECT": "ws+unix:///var/folders/jk/8fzl9l5119598vsqrmphsw7m0000gn/T/tl15npi7qtf.sock?report=1",
|
||||
},
|
||||
"console": "internalConsole",
|
||||
// Don't pause when the GC runs while the debugger is open.
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -78,7 +78,7 @@
|
||||
"prettier.prettierPath": "./node_modules/prettier",
|
||||
|
||||
// TypeScript
|
||||
"typescript.tsdk": "${workspaceFolder}/node_modules/typescript/lib",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
},
|
||||
|
||||
91
.vscode/tasks.json
vendored
91
.vscode/tasks.json
vendored
@@ -2,50 +2,57 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "process",
|
||||
"label": "Install Dependencies",
|
||||
"command": "scripts/all-dependencies.sh",
|
||||
"windows": {
|
||||
"command": "scripts/all-dependencies.ps1",
|
||||
},
|
||||
"icon": {
|
||||
"id": "arrow-down",
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "process",
|
||||
"label": "Setup Environment",
|
||||
"dependsOn": ["Install Dependencies"],
|
||||
"command": "scripts/setup.sh",
|
||||
"windows": {
|
||||
"command": "scripts/setup.ps1",
|
||||
},
|
||||
"icon": {
|
||||
"id": "check",
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "process",
|
||||
"label": "Build Bun",
|
||||
"dependsOn": ["Setup Environment"],
|
||||
"command": "bun",
|
||||
"args": ["run", "build"],
|
||||
"icon": {
|
||||
"id": "gear",
|
||||
"type": "shell",
|
||||
"command": "bun run build",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true,
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
},
|
||||
"isBuildCommand": true,
|
||||
"runOptions": {
|
||||
"instanceLimit": 1,
|
||||
"reevaluateOnRerun": true,
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "zig",
|
||||
"fileLocation": ["relative", "${workspaceFolder}"],
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.+?):(\\d+):(\\d+): (error|warning|note): (.+)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"message": 5,
|
||||
},
|
||||
{
|
||||
"regexp": "^\\s+(.+)$",
|
||||
"message": 1,
|
||||
"loop": true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"owner": "clang",
|
||||
"fileLocation": ["relative", "${workspaceFolder}"],
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^([^:]+):(\\d+):(\\d+):\\s+(warning|error|note|remark):\\s+(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"message": 5,
|
||||
},
|
||||
{
|
||||
"regexp": "^\\s*(.*)$",
|
||||
"message": 1,
|
||||
"loop": true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
"clear": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Configuring a development environment for Bun can take 10-30 minutes depending on your internet connection and computer speed. You will need ~10GB of free disk space for the repository and build artifacts.
|
||||
|
||||
If you are using Windows, please refer to [this guide](/docs/project/building-windows)
|
||||
If you are using Windows, please refer to [this guide](/docs/project/building-windows.md)
|
||||
|
||||
{% details summary="For Ubuntu users" %}
|
||||
TL;DR: Ubuntu 22.04 is suggested.
|
||||
|
||||
@@ -136,16 +136,6 @@ else()
|
||||
set(WARNING WARNING)
|
||||
endif()
|
||||
|
||||
if(LINUX)
|
||||
if(EXISTS "/etc/alpine-release")
|
||||
set(DEFAULT_ABI "musl")
|
||||
else()
|
||||
set(DEFAULT_ABI "gnu")
|
||||
endif()
|
||||
|
||||
optionx(ABI "musl|gnu" "The ABI to use (e.g. musl, gnu)" DEFAULT ${DEFAULT_ABI})
|
||||
endif()
|
||||
|
||||
# TODO: This causes flaky zig builds in CI, so temporarily disable it.
|
||||
# if(CI)
|
||||
# set(DEFAULT_VENDOR_PATH ${CACHE_PATH}/vendor)
|
||||
|
||||
@@ -10,7 +10,6 @@ optionx(GITHUB_ACTIONS BOOL "If GitHub Actions is enabled" DEFAULT OFF)
|
||||
|
||||
if(BUILDKITE)
|
||||
optionx(BUILDKITE_COMMIT STRING "The commit hash")
|
||||
optionx(BUILDKITE_MESSAGE STRING "The commit message")
|
||||
endif()
|
||||
|
||||
optionx(CMAKE_BUILD_TYPE "Debug|Release|RelWithDebInfo|MinSizeRel" "The build type to use" REQUIRED)
|
||||
@@ -49,6 +48,16 @@ else()
|
||||
message(FATAL_ERROR "Unsupported architecture: ${CMAKE_SYSTEM_PROCESSOR}")
|
||||
endif()
|
||||
|
||||
if(LINUX)
|
||||
if(EXISTS "/etc/alpine-release")
|
||||
set(DEFAULT_ABI "musl")
|
||||
else()
|
||||
set(DEFAULT_ABI "gnu")
|
||||
endif()
|
||||
|
||||
optionx(ABI "musl|gnu" "The ABI to use (e.g. musl, gnu)" DEFAULT ${DEFAULT_ABI})
|
||||
endif()
|
||||
|
||||
if(ARCH STREQUAL "x64")
|
||||
optionx(ENABLE_BASELINE BOOL "If baseline features should be used for older CPUs (e.g. disables AVX, AVX2)" DEFAULT OFF)
|
||||
endif()
|
||||
@@ -56,14 +65,7 @@ endif()
|
||||
optionx(ENABLE_LOGS BOOL "If debug logs should be enabled" DEFAULT ${DEBUG})
|
||||
optionx(ENABLE_ASSERTIONS BOOL "If debug assertions should be enabled" DEFAULT ${DEBUG})
|
||||
|
||||
if(BUILDKITE_MESSAGE AND BUILDKITE_MESSAGE MATCHES "\\[release build\\]")
|
||||
message(STATUS "Switched to release build, since commit message contains: \"[release build]\"")
|
||||
set(DEFAULT_CANARY OFF)
|
||||
else()
|
||||
set(DEFAULT_CANARY ON)
|
||||
endif()
|
||||
|
||||
optionx(ENABLE_CANARY BOOL "If canary features should be enabled" DEFAULT ${DEFAULT_CANARY})
|
||||
optionx(ENABLE_CANARY BOOL "If canary features should be enabled" DEFAULT ON)
|
||||
|
||||
if(ENABLE_CANARY AND BUILDKITE)
|
||||
execute_process(
|
||||
|
||||
@@ -1163,7 +1163,7 @@ if(NOT BUN_CPP_ONLY)
|
||||
|
||||
if(CI)
|
||||
set(bunTriplet bun-${OS}-${ARCH})
|
||||
if(ABI STREQUAL "musl")
|
||||
if(LINUX AND ABI STREQUAL "musl")
|
||||
set(bunTriplet ${bunTriplet}-musl)
|
||||
endif()
|
||||
if(ENABLE_BASELINE)
|
||||
|
||||
@@ -4,7 +4,7 @@ register_repository(
|
||||
REPOSITORY
|
||||
c-ares/c-ares
|
||||
COMMIT
|
||||
d1722e6e8acaf10eb73fa995798a9cd421d9f85e
|
||||
41ee334af3e3d0027dca5e477855d0244936bd49
|
||||
)
|
||||
|
||||
register_cmake_command(
|
||||
|
||||
@@ -4,7 +4,7 @@ register_repository(
|
||||
REPOSITORY
|
||||
ebiggers/libdeflate
|
||||
COMMIT
|
||||
dc76454a39e7e83b68c3704b6e3784654f8d5ac5
|
||||
9d624d1d8ba82c690d6d6be1d0a961fc5a983ea4
|
||||
)
|
||||
|
||||
register_cmake_command(
|
||||
|
||||
@@ -4,7 +4,7 @@ register_repository(
|
||||
REPOSITORY
|
||||
cloudflare/lol-html
|
||||
COMMIT
|
||||
8d4c273ded322193d017042d1f48df2766b0f88b
|
||||
4f8becea13a0021c8b71abd2dcc5899384973b66
|
||||
)
|
||||
|
||||
set(LOLHTML_CWD ${VENDOR_PATH}/lolhtml/c-api)
|
||||
|
||||
@@ -4,7 +4,7 @@ register_repository(
|
||||
REPOSITORY
|
||||
litespeedtech/ls-hpack
|
||||
COMMIT
|
||||
3d0f1fc1d6e66a642e7a98c55deb38aa986eb4b0
|
||||
32e96f10593c7cb8553cd8c9c12721100ae9e924
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
|
||||
@@ -4,7 +4,7 @@ if(NOT ENABLE_LLVM)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(CMAKE_HOST_WIN32 OR CMAKE_HOST_APPLE OR ABI STREQUAL "musl")
|
||||
if(CMAKE_HOST_WIN32 OR CMAKE_HOST_APPLE OR EXISTS "/etc/alpine-release")
|
||||
set(DEFAULT_LLVM_VERSION "18.1.8")
|
||||
else()
|
||||
set(DEFAULT_LLVM_VERSION "16.0.6")
|
||||
|
||||
@@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use")
|
||||
option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading")
|
||||
|
||||
if(NOT WEBKIT_VERSION)
|
||||
set(WEBKIT_VERSION 3bc4abf2d5875baf500b4687ef869987f6d19e00)
|
||||
set(WEBKIT_VERSION 8f9ae4f01a047c666ef548864294e01df731d4ea)
|
||||
endif()
|
||||
|
||||
if(WEBKIT_LOCAL)
|
||||
@@ -63,7 +63,7 @@ else()
|
||||
message(FATAL_ERROR "Unsupported architecture: ${CMAKE_SYSTEM_PROCESSOR}")
|
||||
endif()
|
||||
|
||||
if(ABI STREQUAL "musl")
|
||||
if(LINUX AND ABI STREQUAL "musl")
|
||||
set(WEBKIT_SUFFIX "-musl")
|
||||
endif()
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ To instead throw an error when a parameter is missing and allow binding without
|
||||
import { Database } from "bun:sqlite";
|
||||
|
||||
const strict = new Database(
|
||||
":memory:",
|
||||
":memory:",
|
||||
{ strict: true }
|
||||
);
|
||||
|
||||
@@ -177,7 +177,7 @@ const query = db.prepare("SELECT * FROM foo WHERE bar = ?");
|
||||
|
||||
## WAL mode
|
||||
|
||||
SQLite supports [write-ahead log mode](https://www.sqlite.org/wal.html) (WAL) which dramatically improves performance, especially in situations with many concurrent writes. It's broadly recommended to enable WAL mode for most typical applications.
|
||||
SQLite supports [write-ahead log mode](https://www.sqlite.org/wal.html) (WAL) which dramatically improves performance, especially in situations with many concurrent readers and a single writer. It's broadly recommended to enable WAL mode for most typical applications.
|
||||
|
||||
To enable WAL mode, run this pragma query at the beginning of your application:
|
||||
|
||||
|
||||
@@ -75,8 +75,10 @@ jobs:
|
||||
name: build-app
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
- name: Install dependencies # (assuming your project has dependencies)
|
||||
run: bun install # You can use npm/yarn/pnpm instead if you prefer
|
||||
- name: Run tests
|
||||
@@ -124,7 +126,7 @@ Use the `--bail` flag to abort the test run early after a pre-determined number
|
||||
$ bun test --bail
|
||||
|
||||
# bail after 10 failure
|
||||
$ bun test --bail 10
|
||||
$ bun test --bail=10
|
||||
```
|
||||
|
||||
## Watch mode
|
||||
|
||||
@@ -14,7 +14,7 @@ To bail after a certain threshold of failures, optionally specify a number after
|
||||
|
||||
```sh
|
||||
# bail after 10 failures
|
||||
$ bun test --bail 10
|
||||
$ bun test --bail=10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -57,7 +57,7 @@ Replace `bail` in your Jest config with the `--bail` CLI flag.
|
||||
``` -->
|
||||
|
||||
```sh
|
||||
$ bun test --bail 3
|
||||
$ bun test --bail=3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -73,8 +73,7 @@ There are also image variants for different operating systems.
|
||||
$ docker pull oven/bun:debian
|
||||
$ docker pull oven/bun:slim
|
||||
$ docker pull oven/bun:distroless
|
||||
# alpine not recommended until #918 is fixed
|
||||
# $ docker pull oven/bun:alpine
|
||||
$ docker pull oven/bun:alpine
|
||||
```
|
||||
|
||||
## Checking installation
|
||||
@@ -190,14 +189,19 @@ For convenience, here are download links for the latest version:
|
||||
|
||||
- [`bun-linux-x64.zip`](https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip)
|
||||
- [`bun-linux-x64-baseline.zip`](https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64-baseline.zip)
|
||||
- [`bun-linux-x64-musl.zip`](https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64-musl.zip)
|
||||
- [`bun-linux-x64-musl-baseline.zip`](https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64-musl-baseline.zip)
|
||||
- [`bun-windows-x64.zip`](https://github.com/oven-sh/bun/releases/latest/download/bun-windows-x64.zip)
|
||||
- [`bun-windows-x64-baseline.zip`](https://github.com/oven-sh/bun/releases/latest/download/bun-windows-x64-baseline.zip)
|
||||
- [`bun-darwin-aarch64.zip`](https://github.com/oven-sh/bun/releases/latest/download/bun-darwin-aarch64.zip)
|
||||
- [`bun-linux-aarch64.zip`](https://github.com/oven-sh/bun/releases/latest/download/bun-linux-aarch64.zip)
|
||||
- [`bun-linux-aarch64-musl.zip`](https://github.com/oven-sh/bun/releases/latest/download/bun-linux-aarch64-musl.zip)
|
||||
- [`bun-darwin-x64.zip`](https://github.com/oven-sh/bun/releases/latest/download/bun-darwin-x64.zip)
|
||||
- [`bun-darwin-x64-baseline.zip`](https://github.com/oven-sh/bun/releases/latest/download/bun-darwin-x64-baseline.zip)
|
||||
|
||||
The `baseline` binaries are built for older CPUs which may not support AVX2 instructions. If you run into an "Illegal Instruction" error when running Bun, try using the `baseline` binaries instead. Bun's install scripts automatically choose the correct binary for your system which helps avoid this issue. Baseline builds are slower than regular builds, so use them only if necessary.
|
||||
The `musl` binaries are built for distributions that do not ship with the glibc libraries by default, instead relying on musl. The two most popular distros are Void Linux and Alpine Linux, with the latter is used heavily in Docker containers. If you encounter an error like the following: `bun: /lib/x86_64-linux-gnu/libm.so.6: version GLIBC_2.29' not found (required by bun)`, try using the musl binary. Bun's install script automatically chooses the correct binary for your system.
|
||||
|
||||
The `baseline` binaries are built for older CPUs which may not support AVX2 instructions. If you run into an "Illegal Instruction" error when running Bun, try using the `baseline` binaries instead. Bun's install scripts automatically chooses the correct binary for your system which helps avoid this issue. Baseline builds are slower than regular builds, so use them only if necessary.
|
||||
|
||||
<!--
|
||||
## Native
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
## Troubleshooting
|
||||
|
||||
### Bun not running on an M1 (or Apple Silicon)
|
||||
|
||||
If you see a message like this
|
||||
|
||||
> [1] 28447 killed bun create next ./test
|
||||
|
||||
It most likely means you’re running Bun’s x64 version on Apple Silicon. This happens if Bun is running via Rosetta. Rosetta is unable to emulate AVX2 instructions, which Bun indirectly uses.
|
||||
|
||||
The fix is to ensure you installed a version of Bun built for Apple Silicon.
|
||||
|
||||
### error: Unexpected
|
||||
|
||||
If you see an error like this:
|
||||
|
||||

|
||||
|
||||
It usually means the max number of open file descriptors is being explicitly set to a low number. By default, Bun requests the max number of file descriptors available (which on macOS, is something like 32,000). But, if you previously ran into ulimit issues with, e.g., Chokidar, someone on The Internet may have advised you to run `ulimit -n 8192`.
|
||||
|
||||
That advice unfortunately **lowers** the hard limit to `8192`. This can be a problem in large repositories or projects with lots of dependencies. Chokidar (and other watchers) don’t seem to call `setrlimit`, which means they’re reliant on the (much lower) soft limit.
|
||||
|
||||
To fix this issue:
|
||||
|
||||
1. Remove any scripts that call `ulimit -n` and restart your shell.
|
||||
2. Try again, and if the error still occurs, try setting `ulimit -n` to an absurdly high number, such as `ulimit -n 2147483646`
|
||||
3. Try again, and if that still doesn’t fix it, open an issue
|
||||
|
||||
### Unzip is required
|
||||
|
||||
Unzip is required to install Bun on Linux. You can use one of the following commands to install `unzip`:
|
||||
|
||||
#### Debian / Ubuntu / Mint
|
||||
|
||||
```sh
|
||||
$ sudo apt install unzip
|
||||
```
|
||||
|
||||
#### RedHat / CentOS / Fedora
|
||||
|
||||
```sh
|
||||
$ sudo dnf install unzip
|
||||
```
|
||||
|
||||
#### Arch / Manjaro
|
||||
|
||||
```sh
|
||||
$ sudo pacman -S unzip
|
||||
```
|
||||
|
||||
#### OpenSUSE
|
||||
|
||||
```sh
|
||||
$ sudo zypper install unzip
|
||||
```
|
||||
|
||||
### bun install is stuck
|
||||
|
||||
Please run `bun install --verbose 2> logs.txt` and send them to me in Bun's discord. If you're on Linux, it would also be helpful if you run `sudo perf trace bun install --silent` and attach the logs.
|
||||
|
||||
### Uninstalling
|
||||
|
||||
Bun's binary and install cache is located in `~/.bun` by default. To uninstall bun, delete this directory and edit your shell config (`.bashrc`, `.zshrc`, or similar) to remove `~/.bun/bin` from the `$PATH` variable.
|
||||
|
||||
```sh
|
||||
$ rm -rf ~/.bun # make sure to remove ~/.bun/bin from $PATH
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "bun",
|
||||
"version": "1.1.35",
|
||||
"version": "1.1.39",
|
||||
"workspaces": [
|
||||
"./packages/bun-types"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "bun-debug-adapter-protocol",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.4",
|
||||
"source-map-js": "^1.0.2"
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { InspectorEventMap } from "../../../bun-inspector-protocol/src/inspector";
|
||||
import type { JSC } from "../../../bun-inspector-protocol/src/protocol";
|
||||
import type { DAP } from "../protocol";
|
||||
// @ts-ignore
|
||||
import { ChildProcess, spawn } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { AddressInfo, createServer } from "node:net";
|
||||
import { AddressInfo, createServer, Socket } from "node:net";
|
||||
import * as path from "node:path";
|
||||
import { remoteObjectToString, WebSocketInspector } from "../../../bun-inspector-protocol/index";
|
||||
import { randomUnixPath, TCPSocketSignal, UnixSignal } from "./signal";
|
||||
import { Location, SourceMap } from "./sourcemap";
|
||||
import { remoteObjectToString, WebSocketInspector } from "../../../bun-inspector-protocol/index.ts";
|
||||
import type { Inspector, InspectorEventMap } from "../../../bun-inspector-protocol/src/inspector/index.d.ts";
|
||||
import { NodeSocketInspector } from "../../../bun-inspector-protocol/src/inspector/node-socket.ts";
|
||||
import type { JSC } from "../../../bun-inspector-protocol/src/protocol/index.d.ts";
|
||||
import type { DAP } from "../protocol/index.d.ts";
|
||||
import { randomUnixPath, TCPSocketSignal, UnixSignal } from "./signal.ts";
|
||||
import { Location, SourceMap } from "./sourcemap.ts";
|
||||
|
||||
export async function getAvailablePort(): Promise<number> {
|
||||
const server = createServer();
|
||||
server.listen(0);
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(resolve => {
|
||||
server.on("listening", () => {
|
||||
const { port } = server.address() as AddressInfo;
|
||||
server.close(() => {
|
||||
@@ -105,7 +105,18 @@ const capabilities: DAP.Capabilities = {
|
||||
|
||||
type InitializeRequest = DAP.InitializeRequest & {
|
||||
supportsConfigurationDoneRequest?: boolean;
|
||||
};
|
||||
enableControlFlowProfiler?: boolean;
|
||||
enableDebugger?: boolean;
|
||||
} & (
|
||||
| {
|
||||
enableLifecycleAgentReporter?: false;
|
||||
sendImmediatePreventExit?: false;
|
||||
}
|
||||
| {
|
||||
enableLifecycleAgentReporter: true;
|
||||
sendImmediatePreventExit?: boolean;
|
||||
}
|
||||
);
|
||||
|
||||
type LaunchRequest = DAP.LaunchRequest & {
|
||||
runtime?: string;
|
||||
@@ -231,10 +242,14 @@ function normalizeSourcePath(sourcePath: string, untitledDocPath?: string, bunEv
|
||||
return path.normalize(sourcePath);
|
||||
}
|
||||
|
||||
export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements IDebugAdapter {
|
||||
export abstract class BaseDebugAdapter<T extends Inspector = Inspector>
|
||||
extends EventEmitter<DebugAdapterEventMap>
|
||||
implements IDebugAdapter
|
||||
{
|
||||
protected readonly inspector: T;
|
||||
protected options?: DebuggerOptions;
|
||||
|
||||
#threadId: number;
|
||||
#inspector: WebSocketInspector;
|
||||
#process?: ChildProcess;
|
||||
#sourceId: number;
|
||||
#pendingSources: Map<string, ((source: Source) => void)[]>;
|
||||
#sources: Map<string | number, Source>;
|
||||
@@ -247,20 +262,21 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
#targets: Map<number, Target>;
|
||||
#variableId: number;
|
||||
#variables: Map<number, Variable>;
|
||||
#initialized?: InitializeRequest;
|
||||
#options?: DebuggerOptions;
|
||||
#untitledDocPath?: string;
|
||||
#bunEvalPath?: string;
|
||||
#initialized?: InitializeRequest;
|
||||
|
||||
constructor(url?: string | URL, untitledDocPath?: string, bunEvalPath?: string) {
|
||||
protected constructor(inspector: T, untitledDocPath?: string, bunEvalPath?: string) {
|
||||
super();
|
||||
this.#untitledDocPath = untitledDocPath;
|
||||
this.#bunEvalPath = bunEvalPath;
|
||||
this.#threadId = threadId++;
|
||||
this.#inspector = new WebSocketInspector(url);
|
||||
const emit = this.#inspector.emit.bind(this.#inspector);
|
||||
this.#inspector.emit = (event, ...args) => {
|
||||
this.inspector = inspector;
|
||||
const emit = this.inspector.emit.bind(this.inspector);
|
||||
this.inspector.emit = (event, ...args) => {
|
||||
let sent = false;
|
||||
sent ||= emit(event, ...args);
|
||||
sent ||= this.emit(event, ...(args as any));
|
||||
sent ||= this.emit(event as keyof JSC.EventMap, ...(args as any));
|
||||
return sent;
|
||||
};
|
||||
this.#sourceId = 1;
|
||||
@@ -274,26 +290,27 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
this.#targets = new Map();
|
||||
this.#variableId = 1;
|
||||
this.#variables = new Map();
|
||||
this.#untitledDocPath = untitledDocPath;
|
||||
this.#bunEvalPath = bunEvalPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the inspector url.
|
||||
* Gets the inspector url. This is deprecated and exists for compat.
|
||||
* @deprecated You should get the inspector directly (with .getInspector()), and if it's a WebSocketInspector you can access `.url` direclty.
|
||||
*/
|
||||
get url(): string {
|
||||
return this.#inspector.url;
|
||||
// This code has been migrated from a time when the inspector was always a WebSocketInspector.
|
||||
if (this.inspector instanceof WebSocketInspector) {
|
||||
return this.inspector.url;
|
||||
}
|
||||
|
||||
throw new Error("Inspector does not offer a URL");
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the inspector.
|
||||
* @param url the inspector url
|
||||
* @returns if the inspector was able to connect
|
||||
*/
|
||||
start(url?: string): Promise<boolean> {
|
||||
return this.#attach({ url });
|
||||
public getInspector() {
|
||||
return this.inspector;
|
||||
}
|
||||
|
||||
abstract start(...args: unknown[]): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Sends a request to the JavaScript inspector.
|
||||
* @param method the method name
|
||||
@@ -306,7 +323,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
* console.log(result.value); // 2
|
||||
*/
|
||||
async send<M extends keyof JSC.ResponseMap>(method: M, params?: JSC.RequestMap[M]): Promise<JSC.ResponseMap[M]> {
|
||||
return this.#inspector.send(method, params);
|
||||
return this.inspector.send(method, params);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,7 +364,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
return sent;
|
||||
}
|
||||
|
||||
#emit<E extends keyof DAP.EventMap>(event: E, body?: DAP.EventMap[E]): void {
|
||||
protected emitAdapterEvent<E extends keyof DAP.EventMap>(event: E, body?: DAP.EventMap[E]): void {
|
||||
this.emit("Adapter.event", {
|
||||
type: "event",
|
||||
seq: 0,
|
||||
@@ -359,7 +376,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
#emitAfterResponse<E extends keyof DAP.EventMap>(event: E, body?: DAP.EventMap[E]): void {
|
||||
this.once("Adapter.response", () => {
|
||||
process.nextTick(() => {
|
||||
this.#emit(event, body);
|
||||
this.emitAdapterEvent(event, body);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -437,19 +454,37 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
this.emit(`Adapter.${name}` as keyof DebugAdapterEventMap, body);
|
||||
}
|
||||
|
||||
initialize(request: InitializeRequest): DAP.InitializeResponse {
|
||||
public initialize(request: InitializeRequest): DAP.InitializeResponse {
|
||||
this.#initialized = request;
|
||||
|
||||
this.send("Inspector.enable");
|
||||
this.send("Runtime.enable");
|
||||
this.send("Console.enable");
|
||||
this.send("Debugger.enable").catch(error => {
|
||||
const { message } = unknownToError(error);
|
||||
if (message !== "Debugger domain already enabled") {
|
||||
throw error;
|
||||
|
||||
if (request.enableControlFlowProfiler) {
|
||||
this.send("Runtime.enableControlFlowProfiler");
|
||||
}
|
||||
|
||||
if (request.enableLifecycleAgentReporter) {
|
||||
this.send("LifecycleReporter.enable");
|
||||
|
||||
if (request.sendImmediatePreventExit) {
|
||||
this.send("LifecycleReporter.preventExit");
|
||||
}
|
||||
});
|
||||
this.send("Debugger.setAsyncStackTraceDepth", { depth: 200 });
|
||||
}
|
||||
|
||||
// use !== false because by default if unspecified we want to enable the debugger
|
||||
// and this option didn't exist beforehand, so we can't make it non-optional
|
||||
if (request.enableDebugger !== false) {
|
||||
this.send("Debugger.enable").catch(error => {
|
||||
const { message } = unknownToError(error);
|
||||
if (message !== "Debugger domain already enabled") {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
this.send("Debugger.setAsyncStackTraceDepth", { depth: 200 });
|
||||
}
|
||||
|
||||
const { clientID, supportsConfigurationDoneRequest } = request;
|
||||
if (!supportsConfigurationDoneRequest && clientID !== "vscode") {
|
||||
@@ -463,248 +498,20 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
configurationDone(): void {
|
||||
// If the client requested that `noDebug` mode be enabled,
|
||||
// then we need to disable all breakpoints and pause on statements.
|
||||
const active = !this.#options?.noDebug;
|
||||
const active = !this.options?.noDebug;
|
||||
this.send("Debugger.setBreakpointsActive", { active });
|
||||
|
||||
// Tell the debugger that its ready to start execution.
|
||||
this.send("Inspector.initialized");
|
||||
}
|
||||
|
||||
async launch(request: DAP.LaunchRequest): Promise<void> {
|
||||
this.#options = { ...request, type: "launch" };
|
||||
|
||||
try {
|
||||
await this.#launch(request);
|
||||
} catch (error) {
|
||||
// Some clients, like VSCode, will show a system-level popup when a `launch` request fails.
|
||||
// Instead, we want to show the error as a sidebar notification.
|
||||
const { message } = unknownToError(error);
|
||||
this.#emit("output", {
|
||||
category: "stderr",
|
||||
output: `Failed to start debugger.\n${message}`,
|
||||
});
|
||||
this.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
async #launch(request: LaunchRequest): Promise<void> {
|
||||
const {
|
||||
runtime = "bun",
|
||||
runtimeArgs = [],
|
||||
program,
|
||||
args = [],
|
||||
cwd,
|
||||
env = {},
|
||||
strictEnv = false,
|
||||
watchMode = false,
|
||||
stopOnEntry = false,
|
||||
__skipValidation = false,
|
||||
stdin,
|
||||
} = request;
|
||||
|
||||
if (!__skipValidation && !program) {
|
||||
throw new Error("No program specified");
|
||||
}
|
||||
|
||||
const processArgs = [...runtimeArgs];
|
||||
|
||||
if (program === "-" && stdin) {
|
||||
processArgs.push("--eval", stdin);
|
||||
} else if (program) {
|
||||
processArgs.push(program);
|
||||
}
|
||||
|
||||
processArgs.push(...args);
|
||||
|
||||
if (program && isTestJavaScript(program) && !runtimeArgs.includes("test")) {
|
||||
processArgs.unshift("test");
|
||||
}
|
||||
|
||||
if (watchMode && !runtimeArgs.includes("--watch") && !runtimeArgs.includes("--hot")) {
|
||||
processArgs.unshift(watchMode === "hot" ? "--hot" : "--watch");
|
||||
}
|
||||
|
||||
const processEnv = strictEnv
|
||||
? {
|
||||
...env,
|
||||
}
|
||||
: {
|
||||
...process.env,
|
||||
...env,
|
||||
};
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
// we're on unix
|
||||
const url = `ws+unix://${randomUnixPath()}`;
|
||||
const signal = new UnixSignal();
|
||||
|
||||
signal.on("Signal.received", () => {
|
||||
this.#attach({ url });
|
||||
});
|
||||
|
||||
this.once("Adapter.terminated", () => {
|
||||
signal.close();
|
||||
});
|
||||
|
||||
const query = stopOnEntry ? "break=1" : "wait=1";
|
||||
processEnv["BUN_INSPECT"] = `${url}?${query}`;
|
||||
processEnv["BUN_INSPECT_NOTIFY"] = signal.url;
|
||||
|
||||
// This is probably not correct, but it's the best we can do for now.
|
||||
processEnv["FORCE_COLOR"] = "1";
|
||||
processEnv["BUN_QUIET_DEBUG_LOGS"] = "1";
|
||||
processEnv["BUN_DEBUG_QUIET_LOGS"] = "1";
|
||||
|
||||
const started = await this.#spawn({
|
||||
command: runtime,
|
||||
args: processArgs,
|
||||
env: processEnv,
|
||||
cwd,
|
||||
isDebugee: true,
|
||||
});
|
||||
|
||||
if (!started) {
|
||||
throw new Error("Program could not be started.");
|
||||
}
|
||||
} else {
|
||||
// we're on windows
|
||||
// Create TCPSocketSignal
|
||||
const url = `ws://127.0.0.1:${await getAvailablePort()}/${getRandomId()}`; // 127.0.0.1 so it resolves correctly on windows
|
||||
const signal = new TCPSocketSignal(await getAvailablePort());
|
||||
|
||||
signal.on("Signal.received", async () => {
|
||||
this.#attach({ url });
|
||||
});
|
||||
|
||||
this.once("Adapter.terminated", () => {
|
||||
signal.close();
|
||||
});
|
||||
|
||||
const query = stopOnEntry ? "break=1" : "wait=1";
|
||||
processEnv["BUN_INSPECT"] = `${url}?${query}`;
|
||||
processEnv["BUN_INSPECT_NOTIFY"] = signal.url; // 127.0.0.1 so it resolves correctly on windows
|
||||
|
||||
// This is probably not correct, but it's the best we can do for now.
|
||||
processEnv["FORCE_COLOR"] = "1";
|
||||
processEnv["BUN_QUIET_DEBUG_LOGS"] = "1";
|
||||
processEnv["BUN_DEBUG_QUIET_LOGS"] = "1";
|
||||
|
||||
const started = await this.#spawn({
|
||||
command: runtime,
|
||||
args: processArgs,
|
||||
env: processEnv,
|
||||
cwd,
|
||||
isDebugee: true,
|
||||
});
|
||||
|
||||
if (!started) {
|
||||
throw new Error("Program could not be started.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #spawn(options: {
|
||||
command: string;
|
||||
args?: string[];
|
||||
cwd?: string;
|
||||
env?: Record<string, string | undefined>;
|
||||
isDebugee?: boolean;
|
||||
}): Promise<boolean> {
|
||||
const { command, args = [], cwd, env, isDebugee } = options;
|
||||
const request = { command, args, cwd, env };
|
||||
this.emit("Process.requested", request);
|
||||
|
||||
let subprocess: ChildProcess;
|
||||
try {
|
||||
subprocess = spawn(command, args, {
|
||||
...request,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
} catch (cause) {
|
||||
this.emit("Process.exited", new Error("Failed to spawn process", { cause }), null);
|
||||
return false;
|
||||
}
|
||||
|
||||
subprocess.on("spawn", () => {
|
||||
this.emit("Process.spawned", subprocess);
|
||||
|
||||
if (isDebugee) {
|
||||
this.#process = subprocess;
|
||||
this.#emit("process", {
|
||||
name: `${command} ${args.join(" ")}`,
|
||||
systemProcessId: subprocess.pid,
|
||||
isLocalProcess: true,
|
||||
startMethod: "launch",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
subprocess.on("exit", (code, signal) => {
|
||||
this.emit("Process.exited", code, signal);
|
||||
|
||||
if (isDebugee) {
|
||||
this.#process = undefined;
|
||||
this.#emit("exited", {
|
||||
exitCode: code ?? -1,
|
||||
});
|
||||
this.#emit("terminated");
|
||||
}
|
||||
});
|
||||
|
||||
subprocess.stdout?.on("data", data => {
|
||||
this.emit("Process.stdout", data.toString());
|
||||
});
|
||||
|
||||
subprocess.stderr?.on("data", data => {
|
||||
this.emit("Process.stderr", data.toString());
|
||||
});
|
||||
|
||||
return new Promise(resolve => {
|
||||
subprocess.on("spawn", () => resolve(true));
|
||||
subprocess.on("exit", () => resolve(false));
|
||||
subprocess.on("error", () => resolve(false));
|
||||
});
|
||||
}
|
||||
|
||||
async attach(request: AttachRequest): Promise<void> {
|
||||
this.#options = { ...request, type: "attach" };
|
||||
|
||||
try {
|
||||
await this.#attach(request);
|
||||
} catch (error) {
|
||||
// Some clients, like VSCode, will show a system-level popup when a `launch` request fails.
|
||||
// Instead, we want to show the error as a sidebar notification.
|
||||
const { message } = unknownToError(error);
|
||||
this.#emit("output", {
|
||||
category: "stderr",
|
||||
output: `Failed to start debugger.\n${message}`,
|
||||
});
|
||||
this.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
async #attach(request: AttachRequest): Promise<boolean> {
|
||||
const { url } = request;
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const ok = await this.#inspector.start(url);
|
||||
if (ok) {
|
||||
return true;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100 * i));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
// Required so all implementations have a method that .terminate() always calls.
|
||||
// This is useful because we don't want any implementors to forget
|
||||
protected abstract exitJSProcess(): void;
|
||||
|
||||
terminate(): void {
|
||||
if (!this.#process?.kill()) {
|
||||
this.#evaluate({
|
||||
expression: "process.exit(0)",
|
||||
});
|
||||
}
|
||||
|
||||
this.#emit("terminated");
|
||||
this.exitJSProcess();
|
||||
this.emitAdapterEvent("terminated");
|
||||
}
|
||||
|
||||
disconnect(request: DAP.DisconnectRequest): void {
|
||||
@@ -1077,7 +884,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
}
|
||||
|
||||
for (const breakpoint of breakpoints) {
|
||||
this.#emit("breakpoint", {
|
||||
this.emitAdapterEvent("breakpoint", {
|
||||
reason: "removed",
|
||||
breakpoint,
|
||||
});
|
||||
@@ -1316,7 +1123,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
const callFrameId = this.#getCallFrameId(frameId);
|
||||
const objectGroup = callFrameId ? "debugger" : context;
|
||||
|
||||
const { result, wasThrown } = await this.#evaluate({
|
||||
const { result, wasThrown } = await this.evaluateInternal({
|
||||
expression,
|
||||
objectGroup,
|
||||
callFrameId,
|
||||
@@ -1337,7 +1144,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
};
|
||||
}
|
||||
|
||||
async #evaluate(options: {
|
||||
protected async evaluateInternal(options: {
|
||||
expression: string;
|
||||
objectGroup?: string;
|
||||
callFrameId?: string;
|
||||
@@ -1361,7 +1168,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
const callFrameId = this.#getCallFrameId(frameId);
|
||||
|
||||
const { expression, hint } = completionToExpression(text);
|
||||
const { result, wasThrown } = await this.#evaluate({
|
||||
const { result, wasThrown } = await this.evaluateInternal({
|
||||
expression: expression || "this",
|
||||
callFrameId,
|
||||
objectGroup: "repl",
|
||||
@@ -1393,33 +1200,29 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
}
|
||||
|
||||
["Inspector.connected"](): void {
|
||||
this.#emit("output", {
|
||||
this.emitAdapterEvent("output", {
|
||||
category: "debug console",
|
||||
output: "Debugger attached.\n",
|
||||
});
|
||||
|
||||
this.#emit("initialized");
|
||||
this.emitAdapterEvent("initialized");
|
||||
}
|
||||
|
||||
async ["Inspector.disconnected"](error?: Error): Promise<void> {
|
||||
this.#emit("output", {
|
||||
this.emitAdapterEvent("output", {
|
||||
category: "debug console",
|
||||
output: "Debugger detached.\n",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const { message } = error;
|
||||
this.#emit("output", {
|
||||
this.emitAdapterEvent("output", {
|
||||
category: "stderr",
|
||||
output: `${message}\n`,
|
||||
});
|
||||
}
|
||||
|
||||
this.#reset();
|
||||
|
||||
if (this.#process?.exitCode !== null) {
|
||||
this.#emit("terminated");
|
||||
}
|
||||
this.resetInternal();
|
||||
}
|
||||
|
||||
async ["Debugger.scriptParsed"](event: JSC.Debugger.ScriptParsedEvent): Promise<void> {
|
||||
@@ -1470,7 +1273,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
return;
|
||||
}
|
||||
|
||||
this.#emit("output", {
|
||||
this.emitAdapterEvent("output", {
|
||||
category: "stderr",
|
||||
output: errorMessage,
|
||||
line: this.#lineFrom0BasedLine(errorLine),
|
||||
@@ -1498,7 +1301,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
const breakpoint = breakpoints[i];
|
||||
const oldBreakpoint = oldBreakpoints[i];
|
||||
|
||||
this.#emit("breakpoint", {
|
||||
this.emitAdapterEvent("breakpoint", {
|
||||
reason: "changed",
|
||||
breakpoint: {
|
||||
...breakpoint,
|
||||
@@ -1581,7 +1384,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
}
|
||||
}
|
||||
|
||||
this.#emit("stopped", {
|
||||
this.emitAdapterEvent("stopped", {
|
||||
threadId: this.#threadId,
|
||||
reason: this.#stopped,
|
||||
hitBreakpointIds,
|
||||
@@ -1598,20 +1401,20 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
}
|
||||
}
|
||||
|
||||
this.#emit("continued", {
|
||||
this.emitAdapterEvent("continued", {
|
||||
threadId: this.#threadId,
|
||||
});
|
||||
}
|
||||
|
||||
["Process.stdout"](output: string): void {
|
||||
this.#emit("output", {
|
||||
this.emitAdapterEvent("output", {
|
||||
category: "debug console",
|
||||
output,
|
||||
});
|
||||
}
|
||||
|
||||
["Process.stderr"](output: string): void {
|
||||
this.#emit("output", {
|
||||
this.emitAdapterEvent("output", {
|
||||
category: "debug console",
|
||||
output,
|
||||
});
|
||||
@@ -1695,8 +1498,8 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
|
||||
// If the path changed or the source has a source reference,
|
||||
// the old source should be marked as removed.
|
||||
if (path !== oldPath || sourceReference) {
|
||||
this.#emit("loadedSource", {
|
||||
if (path !== oldPath /*|| sourceReference*/) {
|
||||
this.emitAdapterEvent("loadedSource", {
|
||||
reason: "removed",
|
||||
source: oldSource,
|
||||
});
|
||||
@@ -1706,7 +1509,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
this.#sources.set(sourceId, source);
|
||||
this.#sources.set(scriptId, source);
|
||||
|
||||
this.#emit("loadedSource", {
|
||||
this.emitAdapterEvent("loadedSource", {
|
||||
// If the reason is "changed", the source will be retrieved using
|
||||
// the `source` command, which is why it cannot be set when `path` is present.
|
||||
reason: oldSource && !path ? "changed" : "new",
|
||||
@@ -1762,9 +1565,9 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
}
|
||||
|
||||
// If the source is not present, it may not have been loaded yet.
|
||||
let resolves = this.#pendingSources.get(sourceId);
|
||||
let resolves = this.#pendingSources.get(sourceId.toString());
|
||||
if (!resolves) {
|
||||
this.#pendingSources.set(sourceId, (resolves = []));
|
||||
this.#pendingSources.set(sourceId.toString(), (resolves = []));
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
@@ -2016,7 +1819,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
const callFrameId = this.#getCallFrameId(frameId);
|
||||
const objectGroup = callFrameId ? "debugger" : "repl";
|
||||
|
||||
const { result, wasThrown } = await this.#evaluate({
|
||||
const { result, wasThrown } = await this.evaluateInternal({
|
||||
expression: `${expression} = (${value});`,
|
||||
objectGroup: "repl",
|
||||
callFrameId,
|
||||
@@ -2216,12 +2019,11 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.#process?.kill();
|
||||
this.#inspector.close();
|
||||
this.#reset();
|
||||
this.inspector.close();
|
||||
this.resetInternal();
|
||||
}
|
||||
|
||||
#reset(): void {
|
||||
protected resetInternal(): void {
|
||||
this.#pendingSources.clear();
|
||||
this.#sources.clear();
|
||||
this.#stackFrames.length = 0;
|
||||
@@ -2232,10 +2034,309 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
this.#functionBreakpoints.clear();
|
||||
this.#targets.clear();
|
||||
this.#variables.clear();
|
||||
this.#options = undefined;
|
||||
this.options = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a debug adapter that connects over a unix/tcp socket. Usually
|
||||
* in the case of a reverse connection. This is used by the vscode extension.
|
||||
*
|
||||
* @warning This will gracefully handle socket closure, you don't need to add extra handling.
|
||||
*/
|
||||
export class NodeSocketDebugAdapter extends BaseDebugAdapter<NodeSocketInspector> {
|
||||
public constructor(socket: Socket, untitledDocPath?: string, bunEvalPath?: string) {
|
||||
super(new NodeSocketInspector(socket), untitledDocPath, bunEvalPath);
|
||||
|
||||
socket.once("close", () => {
|
||||
this.resetInternal();
|
||||
});
|
||||
}
|
||||
|
||||
protected exitJSProcess(): void {
|
||||
this.evaluateInternal({
|
||||
expression: "process.exit(0)",
|
||||
});
|
||||
}
|
||||
|
||||
public async start() {
|
||||
const ok = await this.inspector.start();
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default debug adapter. Connects via WebSocket
|
||||
*/
|
||||
export class WebSocketDebugAdapter extends BaseDebugAdapter<WebSocketInspector> {
|
||||
#process?: ChildProcess;
|
||||
|
||||
public constructor(url?: string | URL, untitledDocPath?: string, bunEvalPath?: string) {
|
||||
super(new WebSocketInspector(url), untitledDocPath, bunEvalPath);
|
||||
}
|
||||
|
||||
async ["Inspector.disconnected"](error?: Error): Promise<void> {
|
||||
await super["Inspector.disconnected"](error);
|
||||
|
||||
if (this.#process?.exitCode !== null) {
|
||||
this.emitAdapterEvent("terminated");
|
||||
}
|
||||
}
|
||||
|
||||
protected exitJSProcess() {
|
||||
if (!this.#process?.kill()) {
|
||||
this.evaluateInternal({
|
||||
expression: "process.exit(0)",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the inspector.
|
||||
* @param url the inspector url, will default to the one provided in the constructor (if any). If none
|
||||
* @returns if the inspector was able to connect
|
||||
*/
|
||||
start(url?: string): Promise<boolean> {
|
||||
return this.#attach({ url });
|
||||
}
|
||||
|
||||
close() {
|
||||
this.#process?.kill();
|
||||
super.close();
|
||||
}
|
||||
|
||||
async launch(request: DAP.LaunchRequest): Promise<void> {
|
||||
this.options = { ...request, type: "launch" };
|
||||
|
||||
try {
|
||||
await this.#launch(request);
|
||||
} catch (error) {
|
||||
// Some clients, like VSCode, will show a system-level popup when a `launch` request fails.
|
||||
// Instead, we want to show the error as a sidebar notification.
|
||||
const { message } = unknownToError(error);
|
||||
|
||||
this.emitAdapterEvent("output", {
|
||||
category: "stderr",
|
||||
output: `Failed to start debugger.\n${message}`,
|
||||
});
|
||||
|
||||
this.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
async #launch(request: LaunchRequest): Promise<void> {
|
||||
const {
|
||||
runtime = "bun",
|
||||
runtimeArgs = [],
|
||||
program,
|
||||
args = [],
|
||||
cwd,
|
||||
env = {},
|
||||
strictEnv = false,
|
||||
watchMode = false,
|
||||
stopOnEntry = false,
|
||||
__skipValidation = false,
|
||||
stdin,
|
||||
} = request;
|
||||
|
||||
if (!__skipValidation && !program) {
|
||||
throw new Error("No program specified");
|
||||
}
|
||||
|
||||
const processArgs = [...runtimeArgs];
|
||||
|
||||
if (program === "-" && stdin) {
|
||||
processArgs.push("--eval", stdin);
|
||||
} else if (program) {
|
||||
processArgs.push(program);
|
||||
}
|
||||
|
||||
processArgs.push(...args);
|
||||
|
||||
if (program && isTestJavaScript(program) && !runtimeArgs.includes("test")) {
|
||||
processArgs.unshift("test");
|
||||
}
|
||||
|
||||
if (watchMode && !runtimeArgs.includes("--watch") && !runtimeArgs.includes("--hot")) {
|
||||
processArgs.unshift(watchMode === "hot" ? "--hot" : "--watch");
|
||||
}
|
||||
|
||||
const processEnv = strictEnv
|
||||
? {
|
||||
...env,
|
||||
}
|
||||
: {
|
||||
...process.env,
|
||||
...env,
|
||||
};
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
// we're on unix
|
||||
const url = `ws+unix://${randomUnixPath()}`;
|
||||
const signal = new UnixSignal();
|
||||
|
||||
signal.on("Signal.received", () => {
|
||||
this.#attach({ url });
|
||||
});
|
||||
|
||||
this.once("Adapter.terminated", () => {
|
||||
signal.close();
|
||||
});
|
||||
|
||||
const query = stopOnEntry ? "break=1" : "wait=1";
|
||||
processEnv["BUN_INSPECT"] = `${url}?${query}`;
|
||||
processEnv["BUN_INSPECT_NOTIFY"] = signal.url;
|
||||
|
||||
// This is probably not correct, but it's the best we can do for now.
|
||||
processEnv["FORCE_COLOR"] = "1";
|
||||
processEnv["BUN_QUIET_DEBUG_LOGS"] = "1";
|
||||
processEnv["BUN_DEBUG_QUIET_LOGS"] = "1";
|
||||
|
||||
const started = await this.#spawn({
|
||||
command: runtime,
|
||||
args: processArgs,
|
||||
env: processEnv,
|
||||
cwd,
|
||||
isDebugee: true,
|
||||
});
|
||||
|
||||
if (!started) {
|
||||
throw new Error("Program could not be started.");
|
||||
}
|
||||
} else {
|
||||
// we're on windows
|
||||
// Create TCPSocketSignal
|
||||
const url = `ws://127.0.0.1:${await getAvailablePort()}/${getRandomId()}`; // 127.0.0.1 so it resolves correctly on windows
|
||||
const signal = new TCPSocketSignal(await getAvailablePort());
|
||||
|
||||
signal.on("Signal.received", async () => {
|
||||
this.#attach({ url });
|
||||
});
|
||||
|
||||
this.once("Adapter.terminated", () => {
|
||||
signal.close();
|
||||
});
|
||||
|
||||
const query = stopOnEntry ? "break=1" : "wait=1";
|
||||
processEnv["BUN_INSPECT"] = `${url}?${query}`;
|
||||
processEnv["BUN_INSPECT_NOTIFY"] = signal.url; // 127.0.0.1 so it resolves correctly on windows
|
||||
|
||||
// This is probably not correct, but it's the best we can do for now.
|
||||
processEnv["FORCE_COLOR"] = "1";
|
||||
processEnv["BUN_QUIET_DEBUG_LOGS"] = "1";
|
||||
processEnv["BUN_DEBUG_QUIET_LOGS"] = "1";
|
||||
|
||||
const started = await this.#spawn({
|
||||
command: runtime,
|
||||
args: processArgs,
|
||||
env: processEnv,
|
||||
cwd,
|
||||
isDebugee: true,
|
||||
});
|
||||
|
||||
if (!started) {
|
||||
throw new Error("Program could not be started.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #spawn(options: {
|
||||
command: string;
|
||||
args?: string[];
|
||||
cwd?: string;
|
||||
env?: Record<string, string | undefined>;
|
||||
isDebugee?: boolean;
|
||||
}): Promise<boolean> {
|
||||
const { command, args = [], cwd, env, isDebugee } = options;
|
||||
const request = { command, args, cwd, env };
|
||||
this.emit("Process.requested", request);
|
||||
|
||||
let subprocess: ChildProcess;
|
||||
try {
|
||||
subprocess = spawn(command, args, {
|
||||
...request,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
} catch (cause) {
|
||||
this.emit("Process.exited", new Error("Failed to spawn process", { cause }), null);
|
||||
return false;
|
||||
}
|
||||
|
||||
subprocess.on("spawn", () => {
|
||||
this.emit("Process.spawned", subprocess);
|
||||
|
||||
if (isDebugee) {
|
||||
this.#process = subprocess;
|
||||
this.emitAdapterEvent("process", {
|
||||
name: `${command} ${args.join(" ")}`,
|
||||
systemProcessId: subprocess.pid,
|
||||
isLocalProcess: true,
|
||||
startMethod: "launch",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
subprocess.on("exit", (code, signal) => {
|
||||
this.emit("Process.exited", code, signal);
|
||||
|
||||
if (isDebugee) {
|
||||
this.#process = undefined;
|
||||
this.emitAdapterEvent("exited", {
|
||||
exitCode: code ?? -1,
|
||||
});
|
||||
this.emitAdapterEvent("terminated");
|
||||
}
|
||||
});
|
||||
|
||||
subprocess.stdout?.on("data", data => {
|
||||
this.emit("Process.stdout", data.toString());
|
||||
});
|
||||
|
||||
subprocess.stderr?.on("data", data => {
|
||||
this.emit("Process.stderr", data.toString());
|
||||
});
|
||||
|
||||
return new Promise(resolve => {
|
||||
subprocess.on("spawn", () => resolve(true));
|
||||
subprocess.on("exit", () => resolve(false));
|
||||
subprocess.on("error", () => resolve(false));
|
||||
});
|
||||
}
|
||||
|
||||
async attach(request: AttachRequest): Promise<void> {
|
||||
this.options = { ...request, type: "attach" };
|
||||
|
||||
try {
|
||||
await this.#attach(request);
|
||||
} catch (error) {
|
||||
// Some clients, like VSCode, will show a system-level popup when a `launch` request fails.
|
||||
// Instead, we want to show the error as a sidebar notification.
|
||||
const { message } = unknownToError(error);
|
||||
this.emitAdapterEvent("output", {
|
||||
category: "stderr",
|
||||
output: `Failed to start debugger.\n${message}`,
|
||||
});
|
||||
this.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
async #attach(request: AttachRequest): Promise<boolean> {
|
||||
const { url } = request;
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const ok = await this.inspector.start(url);
|
||||
if (ok) {
|
||||
return true;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100 * i));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const DebugAdapter = WebSocketDebugAdapter;
|
||||
|
||||
function stoppedReason(reason: JSC.Debugger.PausedEvent["reason"]): DAP.StoppedEvent["reason"] {
|
||||
switch (reason) {
|
||||
case "Breakpoint":
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { Socket } from "node:net";
|
||||
const enum FramerState {
|
||||
WaitingForLength,
|
||||
WaitingForMessage,
|
||||
}
|
||||
|
||||
let socketFramerMessageLengthBuffer: Buffer;
|
||||
export class SocketFramer {
|
||||
state: FramerState = FramerState.WaitingForLength;
|
||||
pendingLength: number = 0;
|
||||
sizeBuffer: Buffer = Buffer.alloc(4);
|
||||
sizeBufferIndex: number = 0;
|
||||
bufferedData: Buffer = Buffer.alloc(0);
|
||||
socket: Socket;
|
||||
private onMessage: (message: string | string[]) => void;
|
||||
|
||||
constructor(socket: Socket, onMessage: (message: string | string[]) => void) {
|
||||
this.socket = socket;
|
||||
this.onMessage = onMessage;
|
||||
|
||||
if (!socketFramerMessageLengthBuffer) {
|
||||
socketFramerMessageLengthBuffer = Buffer.alloc(4);
|
||||
}
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.state = FramerState.WaitingForLength;
|
||||
this.bufferedData = Buffer.alloc(0);
|
||||
this.sizeBufferIndex = 0;
|
||||
this.sizeBuffer = Buffer.alloc(4);
|
||||
}
|
||||
|
||||
send(data: string): void {
|
||||
socketFramerMessageLengthBuffer.writeUInt32BE(data.length, 0);
|
||||
this.socket.write(socketFramerMessageLengthBuffer);
|
||||
this.socket.write(data);
|
||||
}
|
||||
|
||||
onData(data: Buffer): void {
|
||||
this.bufferedData = this.bufferedData.length > 0 ? Buffer.concat([this.bufferedData, data]) : data;
|
||||
|
||||
let messagesToDeliver: string[] = [];
|
||||
let position = 0;
|
||||
|
||||
while (position < this.bufferedData.length) {
|
||||
// Need 4 bytes for the length
|
||||
if (this.bufferedData.length - position < 4) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Read the length prefix
|
||||
const messageLength = this.bufferedData.readUInt32BE(position);
|
||||
|
||||
// Validate message length
|
||||
if (messageLength <= 0 || messageLength > 1024 * 1024) {
|
||||
// 1MB max
|
||||
// Try to resync by looking for the next valid message
|
||||
let newPosition = position + 1;
|
||||
let found = false;
|
||||
|
||||
while (newPosition < this.bufferedData.length - 4) {
|
||||
const testLength = this.bufferedData.readUInt32BE(newPosition);
|
||||
|
||||
if (testLength > 0 && testLength <= 1024 * 1024) {
|
||||
// Verify we can read the full message
|
||||
if (this.bufferedData.length - newPosition - 4 >= testLength) {
|
||||
const testMessage = this.bufferedData.toString("utf-8", newPosition + 4, newPosition + 4 + testLength);
|
||||
|
||||
if (testMessage.startsWith('{"')) {
|
||||
position = newPosition;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newPosition++;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
// Couldn't find a valid message, discard buffer up to this point
|
||||
this.bufferedData = this.bufferedData.slice(position + 4);
|
||||
return;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we have the complete message
|
||||
if (this.bufferedData.length - position - 4 < messageLength) {
|
||||
break;
|
||||
}
|
||||
|
||||
const message = this.bufferedData.toString("utf-8", position + 4, position + 4 + messageLength);
|
||||
if (message.startsWith('{"')) {
|
||||
messagesToDeliver.push(message);
|
||||
}
|
||||
|
||||
position += 4 + messageLength;
|
||||
}
|
||||
|
||||
if (position > 0) {
|
||||
this.bufferedData =
|
||||
position < this.bufferedData.length ? this.bufferedData.slice(position) : SocketFramer.emptyBuffer;
|
||||
}
|
||||
|
||||
if (messagesToDeliver.length === 1) {
|
||||
this.onMessage(messagesToDeliver[0]);
|
||||
} else if (messagesToDeliver.length > 1) {
|
||||
this.onMessage(messagesToDeliver);
|
||||
}
|
||||
}
|
||||
|
||||
private static emptyBuffer = Buffer.from([]);
|
||||
}
|
||||
@@ -11,6 +11,8 @@ export type UnixSignalEventMap = {
|
||||
"Signal.error": [Error];
|
||||
"Signal.received": [string];
|
||||
"Signal.closed": [];
|
||||
"Signal.Socket.closed": [socket: Socket];
|
||||
"Signal.Socket.connect": [socket: Socket];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -21,7 +23,7 @@ export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
|
||||
#server: Server;
|
||||
#ready: Promise<void>;
|
||||
|
||||
constructor(path?: string | URL) {
|
||||
constructor(path?: string | URL | undefined) {
|
||||
super();
|
||||
this.#path = path ? parseUnixPath(path) : randomUnixPath();
|
||||
this.#server = createServer();
|
||||
@@ -29,9 +31,13 @@ export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
|
||||
this.#server.on("error", error => this.emit("Signal.error", error));
|
||||
this.#server.on("close", () => this.emit("Signal.closed"));
|
||||
this.#server.on("connection", socket => {
|
||||
this.emit("Signal.Socket.connect", socket);
|
||||
socket.on("data", data => {
|
||||
this.emit("Signal.received", data.toString());
|
||||
});
|
||||
socket.on("close", () => {
|
||||
this.emit("Signal.Socket.closed", socket);
|
||||
});
|
||||
});
|
||||
this.#ready = new Promise((resolve, reject) => {
|
||||
this.#server.on("listening", resolve);
|
||||
@@ -45,7 +51,7 @@ export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
|
||||
console.log(event, ...args);
|
||||
}
|
||||
|
||||
return super.emit(event, ...args);
|
||||
return super.emit(event, ...(args as never));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,6 +97,8 @@ export type TCPSocketSignalEventMap = {
|
||||
"Signal.error": [Error];
|
||||
"Signal.closed": [];
|
||||
"Signal.received": [string];
|
||||
"Signal.Socket.closed": [socket: Socket];
|
||||
"Signal.Socket.connect": [socket: Socket];
|
||||
};
|
||||
|
||||
export class TCPSocketSignal extends EventEmitter {
|
||||
@@ -103,6 +111,8 @@ export class TCPSocketSignal extends EventEmitter {
|
||||
this.#port = port;
|
||||
|
||||
this.#server = createServer((socket: Socket) => {
|
||||
this.emit("Signal.Socket.connect", socket);
|
||||
|
||||
socket.on("data", data => {
|
||||
this.emit("Signal.received", data.toString());
|
||||
});
|
||||
@@ -112,10 +122,14 @@ export class TCPSocketSignal extends EventEmitter {
|
||||
});
|
||||
|
||||
socket.on("close", () => {
|
||||
this.emit("Signal.closed");
|
||||
this.emit("Signal.Socket.closed", socket);
|
||||
});
|
||||
});
|
||||
|
||||
this.#server.on("close", () => {
|
||||
this.emit("Signal.closed");
|
||||
});
|
||||
|
||||
this.#ready = new Promise((resolve, reject) => {
|
||||
this.#server.listen(this.#port, () => {
|
||||
this.emit("Signal.listening");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { SourceMap } from "./sourcemap";
|
||||
import { SourceMap } from "./sourcemap.js";
|
||||
|
||||
test("works without source map", () => {
|
||||
const sourceMap = getSourceMap("without-sourcemap.js");
|
||||
|
||||
@@ -21,7 +21,15 @@ export type Location = {
|
||||
);
|
||||
|
||||
export interface SourceMap {
|
||||
/**
|
||||
* Converts a location in the original source to a location in the generated source.
|
||||
* @param request A request
|
||||
*/
|
||||
generatedLocation(request: LocationRequest): Location;
|
||||
/**
|
||||
* Converts a location in the generated source to a location in the original source.
|
||||
* @param request A request
|
||||
*/
|
||||
originalLocation(request: LocationRequest): Location;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"module": "NodeNext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "nodenext",
|
||||
"moduleDetection": "force",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"composite": true,
|
||||
// "composite": true,
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -15,7 +15,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSourceMap": true,
|
||||
"allowJs": true,
|
||||
"outDir": "dist",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "scripts", "../bun-types/index.d.ts", "../bun-inspector-protocol/src"]
|
||||
"include": ["src", "scripts", "../bun-types/index.d.ts", "../bun-inspector-protocol/**/*.ts"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type * from "./src/inspector";
|
||||
export * from "./src/inspector/websocket";
|
||||
export type * from "./src/protocol";
|
||||
export * from "./src/util/preview";
|
||||
export type * from "./src/inspector/index.js";
|
||||
export * from "./src/inspector/websocket.js";
|
||||
export type * from "./src/protocol/index.js";
|
||||
export * from "./src/util/preview.js";
|
||||
|
||||
@@ -1,26 +1,7 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { readFileSync, writeFileSync, realpathSync } from "node:fs";
|
||||
import type { Domain, Property, Protocol } from "../src/protocol/schema";
|
||||
|
||||
run().catch(console.error);
|
||||
|
||||
async function run() {
|
||||
const cwd = new URL("../src/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));
|
||||
}
|
||||
import path from "node:path";
|
||||
|
||||
function formatProtocol(protocol: Protocol, extraTs?: string): string {
|
||||
const { name, domains } = protocol;
|
||||
@@ -29,6 +10,7 @@ function formatProtocol(protocol: Protocol, extraTs?: string): string {
|
||||
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);
|
||||
}
|
||||
@@ -153,32 +135,12 @@ async function downloadV8(): Promise<Protocol> {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 getJSC(): Promise<Protocol> {
|
||||
let bunExecutable = Bun.which("bun-debug") || process.execPath;
|
||||
if (!bunExecutable) {
|
||||
throw new Error("bun-debug not found");
|
||||
}
|
||||
bunExecutable = realpathSync(bunExecutable);
|
||||
}
|
||||
|
||||
async function download<V>(url: string): Promise<V> {
|
||||
@@ -200,3 +162,39 @@ function toComment(description?: string): string {
|
||||
const lines = ["/**", ...description.split("\n").map(line => ` * ${line.trim()}`), "*/"];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
const cwd = new URL("../src/protocol/", import.meta.url);
|
||||
const runner = "Bun" in globalThis ? "bunx" : "npx";
|
||||
const write = (name: string, data: string) => {
|
||||
const filePath = path.resolve(__dirname, "..", "src", "protocol", name);
|
||||
writeFileSync(filePath, data);
|
||||
spawnSync(runner, ["prettier", "--write", filePath], { cwd, stdio: "ignore" });
|
||||
};
|
||||
const base = readFileSync(new URL("protocol.d.ts", cwd), "utf-8");
|
||||
const baseNoComments = base.replace(/\/\/.*/g, "");
|
||||
|
||||
const jscJsonFile = path.resolve(__dirname, process.argv.at(-1) ?? "");
|
||||
let jscJSONFile;
|
||||
try {
|
||||
jscJSONFile = await Bun.file(jscJsonFile).json();
|
||||
} catch (error) {
|
||||
console.warn("Failed to read CombinedDomains.json from WebKit build. Is this a WebKit build from Bun?");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const jsc = {
|
||||
name: "JSC",
|
||||
version: {
|
||||
major: 1,
|
||||
minor: 4,
|
||||
},
|
||||
domains: jscJSONFile.domains
|
||||
.filter(a => a.debuggableTypes?.includes?.("javascript"))
|
||||
.sort((a, b) => a.domain.localeCompare(b.domain)),
|
||||
};
|
||||
write("jsc/protocol.json", JSON.stringify(jsc, null, 2));
|
||||
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));
|
||||
|
||||
235
packages/bun-inspector-protocol/src/inspector/node-socket.ts
Normal file
235
packages/bun-inspector-protocol/src/inspector/node-socket.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { Socket } from "node:net";
|
||||
import { SocketFramer } from "../../../bun-debug-adapter-protocol/src/debugger/node-socket-framer.js";
|
||||
import type { JSC } from "../protocol";
|
||||
import type { Inspector, InspectorEventMap } from "./index";
|
||||
|
||||
/**
|
||||
* An inspector that communicates with a debugger over a (unix) socket.
|
||||
* This is used in the extension as follows:
|
||||
*
|
||||
* 1. Extension sets environment variable `BUN_INSPECT_NOTIFY` inside of all vscode terminals.
|
||||
* This is a path to a unix socket that the extension will listen on.
|
||||
* 2. Bun reads it and connects to the socket, setting up a reverse connection for sending DAP
|
||||
* messages.
|
||||
*/
|
||||
export class NodeSocketInspector extends EventEmitter<InspectorEventMap> implements Inspector {
|
||||
#ready: Promise<boolean> | undefined;
|
||||
#socket: Socket;
|
||||
#requestId: number;
|
||||
#pendingRequests: JSC.Request[];
|
||||
#pendingResponses: Map<
|
||||
number,
|
||||
{
|
||||
request: JSC.Request;
|
||||
done: (result: unknown) => void;
|
||||
}
|
||||
>;
|
||||
#framer: SocketFramer;
|
||||
|
||||
constructor(socket: Socket) {
|
||||
super();
|
||||
this.#socket = socket;
|
||||
this.#requestId = 1;
|
||||
this.#pendingRequests = [];
|
||||
this.#pendingResponses = new Map();
|
||||
|
||||
this.#framer = new SocketFramer(socket, message => {
|
||||
if (Array.isArray(message)) {
|
||||
for (const m of message) {
|
||||
this.#accept(m);
|
||||
}
|
||||
} else {
|
||||
this.#accept(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onConnectOrImmediately(cb: () => void) {
|
||||
const isAlreadyConnected = this.#socket.connecting === false;
|
||||
|
||||
if (isAlreadyConnected) {
|
||||
cb();
|
||||
} else {
|
||||
this.#socket.once("connect", cb);
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<boolean> {
|
||||
if (this.#ready) {
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
if (this.closed) {
|
||||
this.close();
|
||||
const addressWithPort = this.#socket.remoteAddress + ":" + this.#socket.remotePort;
|
||||
this.emit("Inspector.connecting", addressWithPort);
|
||||
}
|
||||
|
||||
const socket = this.#socket;
|
||||
|
||||
this.onConnectOrImmediately(() => {
|
||||
this.emit("Inspector.connected");
|
||||
|
||||
for (let i = 0; i < this.#pendingRequests.length; i++) {
|
||||
const request = this.#pendingRequests[i];
|
||||
|
||||
if (this.#send(request)) {
|
||||
this.emit("Inspector.request", request);
|
||||
} else {
|
||||
this.#pendingRequests = this.#pendingRequests.slice(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("data", data => this.#framer.onData(data));
|
||||
|
||||
socket.on("error", error => {
|
||||
this.#close(unknownToError(error));
|
||||
});
|
||||
|
||||
socket.on("close", hadError => {
|
||||
if (hadError) {
|
||||
this.#close(new Error("Socket closed due to a transmission error"));
|
||||
} else {
|
||||
this.#close();
|
||||
}
|
||||
});
|
||||
|
||||
const ready = new Promise<boolean>(resolve => {
|
||||
if (socket.connecting) {
|
||||
socket.on("connect", () => resolve(true));
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
socket.on("close", () => resolve(false));
|
||||
socket.on("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) => {
|
||||
let timerId: number | undefined;
|
||||
const done = (result: any) => {
|
||||
this.#pendingResponses.delete(id);
|
||||
if (timerId) {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
if (result instanceof Error) {
|
||||
reject(result);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
|
||||
this.#pendingResponses.set(id, {
|
||||
request: request,
|
||||
done: done,
|
||||
});
|
||||
|
||||
if (this.#send(request)) {
|
||||
timerId = +setTimeout(() => done(new Error(`Timed out: ${method}`)), 10_000);
|
||||
this.emit("Inspector.request", request);
|
||||
} else {
|
||||
this.emit("Inspector.pendingRequest", request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#send(request: JSC.Request): boolean {
|
||||
this.#framer.send(JSON.stringify(request));
|
||||
|
||||
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 handle = this.#pendingResponses.get(id);
|
||||
if (!handle) {
|
||||
this.emit("Inspector.error", new Error(`Failed to find matching request for ID: ${id}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if ("error" in data) {
|
||||
const { error } = data;
|
||||
const { message } = error;
|
||||
handle.done(new Error(message));
|
||||
} else {
|
||||
const { result } = data;
|
||||
handle.done(result);
|
||||
}
|
||||
}
|
||||
|
||||
get closed(): boolean {
|
||||
return !this.#socket.writable;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.#socket?.end();
|
||||
}
|
||||
|
||||
#close(error?: Error): void {
|
||||
for (const handle of this.#pendingResponses.values()) {
|
||||
handle.done(error ?? new Error("Socket closed while waiting for: " + handle.request.method));
|
||||
}
|
||||
|
||||
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,7 +1,7 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { WebSocket } from "ws";
|
||||
import type { Inspector, InspectorEventMap } from ".";
|
||||
import type { JSC } from "../protocol";
|
||||
import type { Inspector, InspectorEventMap } from "./index";
|
||||
|
||||
/**
|
||||
* An inspector that communicates with a debugger over a WebSocket.
|
||||
@@ -170,6 +170,7 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
|
||||
|
||||
#accept(message: string): void {
|
||||
let data: JSC.Event | JSC.Response;
|
||||
|
||||
try {
|
||||
data = JSON.parse(message);
|
||||
} catch (cause) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
"lib": ["ESNext"],
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"moduleDetection": "force",
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
@@ -12,7 +12,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSourceMap": true,
|
||||
"allowJs": true,
|
||||
"outDir": "dist",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [".", "../bun-types/index.d.ts"]
|
||||
}
|
||||
|
||||
4
packages/bun-types/.gitignore
vendored
4
packages/bun-types/.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
dist/
|
||||
docs/
|
||||
*.tgz
|
||||
10
packages/bun-types/bun.d.ts
vendored
10
packages/bun-types/bun.d.ts
vendored
@@ -3873,7 +3873,6 @@ declare module "bun" {
|
||||
* The default loader for this file extension
|
||||
*/
|
||||
loader: Loader;
|
||||
|
||||
/**
|
||||
* Defer the execution of this callback until all other modules have been parsed.
|
||||
*
|
||||
@@ -3899,6 +3898,10 @@ declare module "bun" {
|
||||
* The namespace of the importer.
|
||||
*/
|
||||
namespace: string;
|
||||
/**
|
||||
* The directory to perform file-based resolutions in.
|
||||
*/
|
||||
resolveDir: string;
|
||||
/**
|
||||
* The kind of import this resolve is for.
|
||||
*/
|
||||
@@ -4534,6 +4537,11 @@ declare module "bun" {
|
||||
unix: string;
|
||||
}
|
||||
|
||||
interface FdSocketOptions<Data = undefined> extends SocketOptions<Data> {
|
||||
tls?: TLSOptions;
|
||||
fd: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a TCP client that connects to a server
|
||||
*
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
"license": "MIT",
|
||||
"main": "",
|
||||
"types": "index.d.ts",
|
||||
"description": "Type definitions for Bun, an incredibly fast JavaScript runtime",
|
||||
"description": "Type definitions and documentation for Bun, an incredibly fast JavaScript runtime",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/oven-sh/bun",
|
||||
"directory": "packages/bun-types"
|
||||
},
|
||||
"files": [
|
||||
"*.d.ts"
|
||||
"*.d.ts",
|
||||
"docs/**/*.md",
|
||||
"docs/*.md"
|
||||
],
|
||||
"homepage": "https://bun.sh",
|
||||
"dependencies": {
|
||||
@@ -25,7 +27,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "echo $(pwd)",
|
||||
"build": "bun scripts/build.ts && bun run fmt",
|
||||
"copy-docs": "rm -rf docs && cp -r ../../docs/ ./docs",
|
||||
"build": "bun run copy-docs && bun scripts/build.ts && bun run fmt",
|
||||
"test": "tsc",
|
||||
"fmt": "echo $(which biome) && biome format --write ."
|
||||
},
|
||||
|
||||
@@ -18,8 +18,7 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const now = new Date();
|
||||
|
||||
const formatDate = d => {
|
||||
const iso = d.toISOString();
|
||||
return iso.substring(0, iso.indexOf("T"));
|
||||
return d;
|
||||
};
|
||||
|
||||
const getCertdataURL = version => {
|
||||
@@ -146,26 +145,35 @@ if (values.help) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const scheduleURL = "https://wiki.mozilla.org/NSS:Release_Versions";
|
||||
if (values.verbose) {
|
||||
console.log(`Fetching NSS release schedule from ${scheduleURL}`);
|
||||
}
|
||||
const schedule = await fetch(scheduleURL);
|
||||
if (!schedule.ok) {
|
||||
console.error(`Failed to fetch ${scheduleURL}: ${schedule.status}: ${schedule.statusText}`);
|
||||
process.exit(-1);
|
||||
}
|
||||
const scheduleText = await schedule.text();
|
||||
const nssReleases = getReleases(scheduleText);
|
||||
const versions = await fetch("https://nucleus.mozilla.org/rna/all-releases.json").then(res => res.json());
|
||||
|
||||
// Retrieve metadata for the NSS release being updated to.
|
||||
const version = positionals[0] ?? (await getLatestVersion(nssReleases));
|
||||
const release = nssReleases.find(r => {
|
||||
return new RegExp(`^${version.replace(".", "\\.")}\\b`).test(r[kNSSVersion]);
|
||||
});
|
||||
if (!pastRelease(release)) {
|
||||
console.warn(`Warning: NSS ${version} is not due to be released until ${formatDate(release[kNSSDate])}`);
|
||||
const today = new Date().toISOString().split("T")[0].trim();
|
||||
const releases = versions
|
||||
.filter(
|
||||
version =>
|
||||
version.channel == "Release" &&
|
||||
version.product === "Firefox" &&
|
||||
version.is_public &&
|
||||
version.release_date <= today,
|
||||
)
|
||||
.sort((a, b) => (a > b ? (a == b ? 0 : -1) : 1));
|
||||
const latest = releases[0];
|
||||
const release_tag = `FIREFOX_${latest.version.replaceAll(".", "_")}_RELEASE`;
|
||||
if (values.verbose) {
|
||||
console.log(`Fetching NSS release from ${release_tag}`);
|
||||
}
|
||||
const version = await fetch(
|
||||
`https://hg.mozilla.org/releases/mozilla-release/raw-file/${release_tag}/security/nss/TAG-INFO`,
|
||||
)
|
||||
.then(res => res.text())
|
||||
.then(txt => txt.trim().split("NSS_")[1].split("_RTM").join("").split("_").join(".").trim());
|
||||
|
||||
const release = {
|
||||
version: version,
|
||||
firefoxVersion: latest.version,
|
||||
firefoxDate: latest.release_date,
|
||||
date: latest.release_date,
|
||||
};
|
||||
if (values.verbose) {
|
||||
console.log("Found NSS version:");
|
||||
console.log(release);
|
||||
|
||||
@@ -623,18 +623,34 @@ inline __attribute__((always_inline)) LIBUS_SOCKET_DESCRIPTOR bsd_bind_listen_fd
|
||||
setsockopt(listenFd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (void *) &optval2, sizeof(optval2));
|
||||
#endif
|
||||
} else {
|
||||
#if defined(SO_REUSEPORT)
|
||||
int optval2 = 1;
|
||||
setsockopt(listenFd, SOL_SOCKET, SO_REUSEPORT, (void *) &optval2, sizeof(optval2));
|
||||
#endif
|
||||
#if defined(SO_REUSEPORT)
|
||||
if((options & LIBUS_LISTEN_REUSE_PORT)) {
|
||||
int optval2 = 1;
|
||||
setsockopt(listenFd, SOL_SOCKET, SO_REUSEPORT, (void *) &optval2, sizeof(optval2));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(SO_REUSEADDR)
|
||||
#ifndef _WIN32
|
||||
|
||||
// Unlike on Unix, here we don't set SO_REUSEADDR, because it doesn't just
|
||||
// allow binding to addresses that are in use by sockets in TIME_WAIT, it
|
||||
// effectively allows 'stealing' a port which is in use by another application.
|
||||
// See libuv issue #1360.
|
||||
|
||||
|
||||
int optval3 = 1;
|
||||
setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, (void *) &optval3, sizeof(optval3));
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef IPV6_V6ONLY
|
||||
// TODO: revise support to match node.js
|
||||
// if (listenAddr->ai_family == AF_INET6) {
|
||||
// int disabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0;
|
||||
// setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, (void *) &disabled, sizeof(disabled));
|
||||
// }
|
||||
int disabled = 0;
|
||||
setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, (void *) &disabled, sizeof(disabled));
|
||||
#endif
|
||||
|
||||
@@ -1858,9 +1858,6 @@ ssl_wrapped_context_on_close(struct us_internal_ssl_socket_t *s, int code,
|
||||
(struct us_wrapped_socket_context_t *)us_internal_ssl_socket_context_ext(
|
||||
context);
|
||||
|
||||
if (wrapped_context->events.on_close) {
|
||||
wrapped_context->events.on_close((struct us_socket_t *)s, code, reason);
|
||||
}
|
||||
|
||||
// writting here can cause the context to not be writable anymore but its the
|
||||
// user responsability to check for that
|
||||
@@ -1868,6 +1865,10 @@ ssl_wrapped_context_on_close(struct us_internal_ssl_socket_t *s, int code,
|
||||
wrapped_context->old_events.on_close((struct us_socket_t *)s, code, reason);
|
||||
}
|
||||
|
||||
if (wrapped_context->events.on_close) {
|
||||
wrapped_context->events.on_close((struct us_socket_t *)s, code, reason);
|
||||
}
|
||||
|
||||
us_socket_context_unref(0, wrapped_context->tcp_context);
|
||||
return s;
|
||||
}
|
||||
@@ -1880,9 +1881,6 @@ ssl_wrapped_context_on_writable(struct us_internal_ssl_socket_t *s) {
|
||||
(struct us_wrapped_socket_context_t *)us_internal_ssl_socket_context_ext(
|
||||
context);
|
||||
|
||||
if (wrapped_context->events.on_writable) {
|
||||
wrapped_context->events.on_writable((struct us_socket_t *)s);
|
||||
}
|
||||
|
||||
// writting here can cause the context to not be writable anymore but its the
|
||||
// user responsability to check for that
|
||||
@@ -1890,6 +1888,10 @@ ssl_wrapped_context_on_writable(struct us_internal_ssl_socket_t *s) {
|
||||
wrapped_context->old_events.on_writable((struct us_socket_t *)s);
|
||||
}
|
||||
|
||||
if (wrapped_context->events.on_writable) {
|
||||
wrapped_context->events.on_writable((struct us_socket_t *)s);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -1916,14 +1918,14 @@ ssl_wrapped_context_on_timeout(struct us_internal_ssl_socket_t *s) {
|
||||
struct us_wrapped_socket_context_t *wrapped_context =
|
||||
(struct us_wrapped_socket_context_t *)us_internal_ssl_socket_context_ext(
|
||||
context);
|
||||
if (wrapped_context->old_events.on_timeout) {
|
||||
wrapped_context->old_events.on_timeout((struct us_socket_t *)s);
|
||||
}
|
||||
|
||||
if (wrapped_context->events.on_timeout) {
|
||||
wrapped_context->events.on_timeout((struct us_socket_t *)s);
|
||||
}
|
||||
|
||||
if (wrapped_context->old_events.on_timeout) {
|
||||
wrapped_context->old_events.on_timeout((struct us_socket_t *)s);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
@@ -1935,15 +1937,14 @@ ssl_wrapped_context_on_long_timeout(struct us_internal_ssl_socket_t *s) {
|
||||
struct us_wrapped_socket_context_t *wrapped_context =
|
||||
(struct us_wrapped_socket_context_t *)us_internal_ssl_socket_context_ext(
|
||||
context);
|
||||
if (wrapped_context->old_events.on_long_timeout) {
|
||||
wrapped_context->old_events.on_long_timeout((struct us_socket_t *)s);
|
||||
}
|
||||
|
||||
if (wrapped_context->events.on_long_timeout) {
|
||||
wrapped_context->events.on_long_timeout((struct us_socket_t *)s);
|
||||
}
|
||||
|
||||
if (wrapped_context->old_events.on_long_timeout) {
|
||||
wrapped_context->old_events.on_long_timeout((struct us_socket_t *)s);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -1954,14 +1955,13 @@ ssl_wrapped_context_on_end(struct us_internal_ssl_socket_t *s) {
|
||||
struct us_wrapped_socket_context_t *wrapped_context =
|
||||
(struct us_wrapped_socket_context_t *)us_internal_ssl_socket_context_ext(
|
||||
context);
|
||||
|
||||
if (wrapped_context->events.on_end) {
|
||||
wrapped_context->events.on_end((struct us_socket_t *)s);
|
||||
}
|
||||
|
||||
if (wrapped_context->old_events.on_end) {
|
||||
wrapped_context->old_events.on_end((struct us_socket_t *)s);
|
||||
}
|
||||
if (wrapped_context->events.on_end) {
|
||||
wrapped_context->events.on_end((struct us_socket_t *)s);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -1973,13 +1973,13 @@ ssl_wrapped_on_connect_error(struct us_internal_ssl_socket_t *s, int code) {
|
||||
(struct us_wrapped_socket_context_t *)us_internal_ssl_socket_context_ext(
|
||||
context);
|
||||
|
||||
if (wrapped_context->old_events.on_connect_error) {
|
||||
wrapped_context->old_events.on_connect_error((struct us_connecting_socket_t *)s, code);
|
||||
}
|
||||
if (wrapped_context->events.on_connect_error) {
|
||||
wrapped_context->events.on_connect_error((struct us_connecting_socket_t *)s, code);
|
||||
}
|
||||
|
||||
if (wrapped_context->old_events.on_connect_error) {
|
||||
wrapped_context->old_events.on_connect_error((struct us_connecting_socket_t *)s, code);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -1990,14 +1990,14 @@ ssl_wrapped_on_socket_connect_error(struct us_internal_ssl_socket_t *s, int code
|
||||
struct us_wrapped_socket_context_t *wrapped_context =
|
||||
(struct us_wrapped_socket_context_t *)us_internal_ssl_socket_context_ext(
|
||||
context);
|
||||
|
||||
if (wrapped_context->old_events.on_connecting_socket_error) {
|
||||
wrapped_context->old_events.on_connecting_socket_error((struct us_socket_t *)s, code);
|
||||
}
|
||||
if (wrapped_context->events.on_connecting_socket_error) {
|
||||
wrapped_context->events.on_connecting_socket_error((struct us_socket_t *)s, code);
|
||||
}
|
||||
|
||||
if (wrapped_context->old_events.on_connecting_socket_error) {
|
||||
wrapped_context->old_events.on_connecting_socket_error((struct us_socket_t *)s, code);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,10 @@ enum {
|
||||
LIBUS_LISTEN_EXCLUSIVE_PORT = 1,
|
||||
/* Allow socket to keep writing after readable side closes */
|
||||
LIBUS_SOCKET_ALLOW_HALF_OPEN = 2,
|
||||
/* Setting reusePort allows multiple sockets on the same host to bind to the same port. Incoming connections are distributed by the operating system to listening sockets. This option is available only on some platforms, such as Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+*/
|
||||
LIBUS_LISTEN_REUSE_PORT = 4,
|
||||
/* etting ipv6Only will disable dual-stack support, i.e., binding to host :: won't make 0.0.0.0 be bound.*/
|
||||
LIBUS_SOCKET_IPV6_ONLY = 8,
|
||||
};
|
||||
|
||||
/* Library types publicly available */
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <sys/ioctl.h>
|
||||
#endif
|
||||
|
||||
|
||||
/* The loop has 2 fallthrough polls */
|
||||
void us_internal_loop_data_init(struct us_loop_t *loop, void (*wakeup_cb)(struct us_loop_t *loop),
|
||||
void (*pre_cb)(struct us_loop_t *loop), void (*post_cb)(struct us_loop_t *loop)) {
|
||||
|
||||
@@ -19,6 +19,26 @@ At its core is the _Bun runtime_, a fast JavaScript runtime designed as a drop-i
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
## Features:
|
||||
|
||||
- Live in-editor error messages (gif below)
|
||||
- Test runner codelens
|
||||
- Debugger support
|
||||
- Run scripts from package.json
|
||||
- Visual lockfile viewer (`bun.lockb`)
|
||||
|
||||
## In-editor error messages
|
||||
|
||||
When running programs with Bun from a Visual Studio Code terminal, Bun will connect to the extension and report errors as they happen, at the exact location they happened. We recommend using this feature with `bun --watch` so you can see errors on every save.
|
||||
|
||||

|
||||
|
||||
<div align="center">
|
||||
<sup>In the example above VSCode is saving on every keypress. Under normal configuration you'd only see errors on every save.</sup>
|
||||
</div>
|
||||
|
||||
Errors are cleared whenever you start typing, or whenever the extension detects that Bun just started running (or reloading) a new program.
|
||||
|
||||
## Configuration
|
||||
|
||||
### `.vscode/launch.json`
|
||||
@@ -75,8 +95,8 @@ You can use the following configurations to debug JavaScript and TypeScript file
|
||||
// The URL of the WebSocket inspector to attach to.
|
||||
// This value can be retrieved by using `bun --inspect`.
|
||||
"url": "ws://localhost:6499/",
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
@@ -91,8 +111,11 @@ You can use the following configurations to customize the behavior of the Bun ex
|
||||
|
||||
// If support for Bun should be added to the default "JavaScript Debug Terminal".
|
||||
"bun.debugTerminal.enabled": true,
|
||||
|
||||
|
||||
// If the debugger should stop on the first line of the program.
|
||||
"bun.debugTerminal.stopOnEntry": false,
|
||||
|
||||
// Glob pattern to find test files. Defaults to the value shown below.
|
||||
"bun.test.filePattern": "**/*{.test.,.spec.,_test_,_spec_}{js,ts,tsx,jsx,mts,cts}",
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
BIN
packages/bun-vscode/error-messages.gif
Normal file
BIN
packages/bun-vscode/error-messages.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 462 KiB |
3
packages/bun-vscode/example/.gitignore
vendored
Normal file
3
packages/bun-vscode/example/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.bake-debug
|
||||
dist
|
||||
node_modules
|
||||
6
packages/bun-vscode/example/bake-test/bun.app.ts
Normal file
6
packages/bun-vscode/example/bake-test/bun.app.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
port: 3000,
|
||||
app: {
|
||||
framework: "react",
|
||||
},
|
||||
};
|
||||
10
packages/bun-vscode/example/bake-test/pages/_layout.tsx
Normal file
10
packages/bun-vscode/example/bake-test/pages/_layout.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export default function Layout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
<footer>some rights reserved - {new Date().toString()}</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
packages/bun-vscode/example/bake-test/pages/index.tsx
Normal file
17
packages/bun-vscode/example/bake-test/pages/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* @ts-expect-error */}
|
||||
<button onClick={() => setCount(count => count.charAt(0))}>count is {count}</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
3
packages/bun-vscode/example/bake-test/pages/two.tsx
Normal file
3
packages/bun-vscode/example/bake-test/pages/two.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Two() {
|
||||
return <p>Wow a second page! Bake is groundbreaking</p>;
|
||||
}
|
||||
1
packages/bun-vscode/example/bug-preload.js
Normal file
1
packages/bun-vscode/example/bug-preload.js
Normal file
@@ -0,0 +1 @@
|
||||
Math.max = undefined;
|
||||
Binary file not shown.
@@ -3,10 +3,13 @@ import { describe, expect, test } from "bun:test";
|
||||
describe("example", () => {
|
||||
test("it works", () => {
|
||||
expect(1).toBe(1);
|
||||
expect(1).not.toBe(2);
|
||||
|
||||
expect(10).toBe(10);
|
||||
|
||||
expect(() => {
|
||||
throw new TypeError("Oops! I did it again.");
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
throw new Error("Parent error.", {
|
||||
cause: new TypeError("Child error."),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
type OS = "Windows";
|
||||
import * as os from "node:os";
|
||||
|
||||
Bun.serve({
|
||||
fetch(req: Request) {
|
||||
return new Response(`Hello, ${"Windows" as OS}!`);
|
||||
return new Response(`Hello from ${os.arch()}!`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
"private": true,
|
||||
"name": "example",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.7",
|
||||
"elysia": "^0.6.3",
|
||||
"express": "^4.18.2",
|
||||
"mime": "^3.0.0",
|
||||
"mime-db": "^1.52.0"
|
||||
"mime-db": "^1.52.0",
|
||||
"react": "^0.0.0-experimental-380f5d67-20241113",
|
||||
"react-dom": "^0.0.0-experimental-380f5d67-20241113",
|
||||
"react-refresh": "^0.0.0-experimental-380f5d67-20241113",
|
||||
"react-server-dom-bun": "^0.0.0-experimental-603e6108-20241029",
|
||||
"react-server-dom-webpack": "^0.0.0-experimental-380f5d67-20241113"
|
||||
},
|
||||
"scripts": {
|
||||
"run": "node hello.js",
|
||||
|
||||
7
packages/bun-vscode/example/print.ts
Normal file
7
packages/bun-vscode/example/print.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
function getOldestPersonInBooking(ages: number[]): number {
|
||||
console.log("ok");
|
||||
throw new Error("TODO! Perhaps we can use Math.max() for this?");
|
||||
}
|
||||
|
||||
const ticketAges = [5, 25, 30];
|
||||
console.log(getOldestPersonInBooking(ticketAges));
|
||||
9
packages/bun-vscode/example/test.ts
Normal file
9
packages/bun-vscode/example/test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import axios from "axios";
|
||||
|
||||
async function foo() {
|
||||
const res = await axios.get("http://example.com");
|
||||
|
||||
throw new Error("potato");
|
||||
}
|
||||
|
||||
console.log(await foo());
|
||||
@@ -14,9 +14,6 @@
|
||||
"jsx": "preserve",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
"allowJs": true
|
||||
}
|
||||
}
|
||||
|
||||
13
packages/bun-vscode/example/user.ts
Normal file
13
packages/bun-vscode/example/user.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// await Bun.sleep(100);
|
||||
|
||||
interface User {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const user = {
|
||||
name: "Alistair",
|
||||
} as User;
|
||||
|
||||
console.log(`First letter us '${user.name.charAt(0)}'`);
|
||||
|
||||
await Bun.sleep(100);
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bun-vscode",
|
||||
"version": "0.0.15",
|
||||
"version": "0.0.19",
|
||||
"author": "oven",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -18,44 +18,6 @@
|
||||
"esbuild": "^0.19.2",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"description": "The Visual Studio Code extension for Bun.",
|
||||
"displayName": "Bun for Visual Studio Code",
|
||||
"engines": {
|
||||
"vscode": "^1.60.0"
|
||||
},
|
||||
"extensionKind": [
|
||||
"workspace"
|
||||
],
|
||||
"galleryBanner": {
|
||||
"color": "#3B3738",
|
||||
"theme": "dark"
|
||||
},
|
||||
"homepage": "https://bun.sh/",
|
||||
"icon": "assets/icon.png",
|
||||
"keywords": [
|
||||
"bun",
|
||||
"node.js",
|
||||
"javascript",
|
||||
"typescript",
|
||||
"vscode"
|
||||
],
|
||||
"license": "MIT",
|
||||
"publisher": "oven",
|
||||
"scripts": {
|
||||
"build": "node scripts/build.mjs",
|
||||
"pretest": "bun run build",
|
||||
"test": "node scripts/test.mjs",
|
||||
"dev": "vscode-test --config scripts/dev.mjs",
|
||||
"prepublish": "npm version patch && bun run build",
|
||||
"publish": "cd extension && bunx vsce publish"
|
||||
},
|
||||
"workspaceTrust": {
|
||||
"request": "never"
|
||||
},
|
||||
"workspaces": [
|
||||
"../bun-debug-adapter-protocol",
|
||||
"../bun-inspector-protocol"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onStartupFinished"
|
||||
],
|
||||
@@ -95,6 +57,21 @@
|
||||
"description": "If the debugger should stop on the first line when used in the JavaScript Debug Terminal.",
|
||||
"scope": "window",
|
||||
"default": false
|
||||
},
|
||||
"bun.test.filePattern": {
|
||||
"type": "string",
|
||||
"default": "**/*{.test.,.spec.,_test_,_spec_}{js,ts,tsx,jsx,mts,cts}",
|
||||
"description": "Glob pattern to find test files"
|
||||
},
|
||||
"bun.test.customFlag": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Custom flag added to the end of test command"
|
||||
},
|
||||
"bun.test.customScript": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Custom script to use instead of `bun test`, for example script from `package.json`"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -122,6 +99,20 @@
|
||||
"category": "Bun",
|
||||
"enablement": "!inDebugMode && resourceLangId =~ /^(javascript|typescript|javascriptreact|typescriptreact)$/ && !isInDiffEditor && resourceScheme == 'untitled'",
|
||||
"icon": "$(play-circle)"
|
||||
},
|
||||
{
|
||||
"command": "extension.bun.runTest",
|
||||
"title": "Run all tests",
|
||||
"shortTitle": "Run Test",
|
||||
"category": "Bun",
|
||||
"icon": "$(play)"
|
||||
},
|
||||
{
|
||||
"command": "extension.bun.watchTest",
|
||||
"title": "Run all tests in watch mode",
|
||||
"shortTitle": "Run Test Watch",
|
||||
"category": "Bun",
|
||||
"icon": "$(sync)"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -328,5 +319,43 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"description": "The Visual Studio Code extension for Bun.",
|
||||
"displayName": "Bun for Visual Studio Code",
|
||||
"engines": {
|
||||
"vscode": "^1.60.0"
|
||||
},
|
||||
"extensionKind": [
|
||||
"workspace"
|
||||
],
|
||||
"galleryBanner": {
|
||||
"color": "#3B3738",
|
||||
"theme": "dark"
|
||||
},
|
||||
"homepage": "https://bun.sh/",
|
||||
"icon": "assets/icon.png",
|
||||
"keywords": [
|
||||
"bun",
|
||||
"node.js",
|
||||
"javascript",
|
||||
"typescript",
|
||||
"vscode"
|
||||
],
|
||||
"license": "MIT",
|
||||
"publisher": "oven",
|
||||
"scripts": {
|
||||
"build": "node scripts/build.mjs",
|
||||
"pretest": "bun run build",
|
||||
"test": "node scripts/test.mjs",
|
||||
"dev": "vscode-test --config scripts/dev.mjs",
|
||||
"prepublish": "npm version patch && bun run build",
|
||||
"publish": "cd extension && bunx vsce publish"
|
||||
},
|
||||
"workspaceTrust": {
|
||||
"request": "never"
|
||||
},
|
||||
"workspaces": [
|
||||
"../bun-debug-adapter-protocol",
|
||||
"../bun-inspector-protocol"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as vscode from "vscode";
|
||||
import { registerDebugger, debugCommand } from "./features/debug";
|
||||
import { registerDebugger } from "./features/debug";
|
||||
import { registerDiagnosticsSocket } from "./features/diagnostics/diagnostics";
|
||||
import { registerBunlockEditor } from "./features/lockfile";
|
||||
import { registerPackageJsonProviders } from "./features/tasks/package.json";
|
||||
import { registerTaskProvider } from "./features/tasks/tasks";
|
||||
import { registerTestCodeLens, registerTestRunner } from "./features/tests";
|
||||
|
||||
async function runUnsavedCode() {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
@@ -44,9 +46,10 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
registerDebugger(context);
|
||||
registerTaskProvider(context);
|
||||
registerPackageJsonProviders(context);
|
||||
registerDiagnosticsSocket(context);
|
||||
registerTestRunner(context);
|
||||
registerTestCodeLens(context);
|
||||
|
||||
// Only register for text editors
|
||||
context.subscriptions.push(vscode.commands.registerTextEditorCommand("extension.bun.runUnsavedCode", runUnsavedCode));
|
||||
}
|
||||
|
||||
export function deactivate() {}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { join } from "node:path";
|
||||
import * as vscode from "vscode";
|
||||
import {
|
||||
type DAP,
|
||||
DebugAdapter,
|
||||
getAvailablePort,
|
||||
getRandomId,
|
||||
TCPSocketSignal,
|
||||
UnixSignal,
|
||||
WebSocketDebugAdapter,
|
||||
} from "../../../bun-debug-adapter-protocol";
|
||||
|
||||
export const DEBUG_CONFIGURATION: vscode.DebugConfiguration = {
|
||||
@@ -101,16 +101,18 @@ async function injectDebugTerminal(terminal: vscode.Terminal): Promise<void> {
|
||||
}
|
||||
|
||||
const { env } = creationOptions as vscode.TerminalOptions;
|
||||
if (env["BUN_INSPECT"]) {
|
||||
if (env && env["BUN_INSPECT"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const session = new TerminalDebugSession();
|
||||
await session.initialize();
|
||||
|
||||
const { adapter, signal } = session;
|
||||
|
||||
const stopOnEntry = getConfig("debugTerminal.stopOnEntry") === true;
|
||||
const query = stopOnEntry ? "break=1" : "wait=1";
|
||||
|
||||
const debugSession = new TerminalDebugSession();
|
||||
await debugSession.initialize();
|
||||
const { adapter, signal } = debugSession;
|
||||
const debug = vscode.window.createTerminal({
|
||||
...creationOptions,
|
||||
name: "JavaScript Debug Terminal",
|
||||
@@ -118,6 +120,7 @@ async function injectDebugTerminal(terminal: vscode.Terminal): Promise<void> {
|
||||
...env,
|
||||
"BUN_INSPECT": `${adapter.url}?${query}`,
|
||||
"BUN_INSPECT_NOTIFY": signal.url,
|
||||
BUN_INSPECT_CONNECT_TO: "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -234,7 +237,10 @@ interface RuntimeExceptionThrownEvent {
|
||||
}
|
||||
|
||||
class FileDebugSession extends DebugSession {
|
||||
adapter: DebugAdapter;
|
||||
// If these classes are moved/published, we should make sure
|
||||
// we remove these non-null assertions so consumers of
|
||||
// this lib are not running into these hard
|
||||
adapter!: WebSocketDebugAdapter;
|
||||
sessionId?: string;
|
||||
untitledDocPath?: string;
|
||||
bunEvalPath?: string;
|
||||
@@ -258,7 +264,7 @@ class FileDebugSession extends DebugSession {
|
||||
: `ws+unix://${tmpdir()}/${uniqueId}.sock`;
|
||||
|
||||
const { untitledDocPath, bunEvalPath } = this;
|
||||
this.adapter = new DebugAdapter(url, untitledDocPath, bunEvalPath);
|
||||
this.adapter = new WebSocketDebugAdapter(url, untitledDocPath, bunEvalPath);
|
||||
|
||||
if (untitledDocPath) {
|
||||
this.adapter.on("Adapter.response", (response: DebugProtocolResponse) => {
|
||||
@@ -319,7 +325,7 @@ class FileDebugSession extends DebugSession {
|
||||
}
|
||||
|
||||
class TerminalDebugSession extends FileDebugSession {
|
||||
signal: TCPSocketSignal | UnixSignal;
|
||||
signal!: TCPSocketSignal | UnixSignal;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -346,6 +352,7 @@ class TerminalDebugSession extends FileDebugSession {
|
||||
env: {
|
||||
"BUN_INSPECT": `${this.adapter.url}?wait=1`,
|
||||
"BUN_INSPECT_NOTIFY": this.signal.url,
|
||||
BUN_INSPECT_CONNECT_TO: "",
|
||||
},
|
||||
isTransient: true,
|
||||
iconPath: new vscode.ThemeIcon("debug-console"),
|
||||
|
||||
276
packages/bun-vscode/src/features/diagnostics/diagnostics.ts
Normal file
276
packages/bun-vscode/src/features/diagnostics/diagnostics.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import { Socket } from "node:net";
|
||||
import * as os from "node:os";
|
||||
import { inspect } from "node:util";
|
||||
import * as vscode from "vscode";
|
||||
import {
|
||||
getAvailablePort,
|
||||
NodeSocketDebugAdapter,
|
||||
TCPSocketSignal,
|
||||
UnixSignal,
|
||||
} from "../../../../bun-debug-adapter-protocol";
|
||||
import type { JSC } from "../../../../bun-inspector-protocol";
|
||||
import { typedGlobalState } from "../../global-state";
|
||||
|
||||
const output = vscode.window.createOutputChannel("Bun - Diagnostics");
|
||||
|
||||
const ansiRegex = (() => {
|
||||
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
|
||||
const pattern = [
|
||||
`[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?${ST})`,
|
||||
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))",
|
||||
].join("|");
|
||||
|
||||
return new RegExp(pattern, "g");
|
||||
})();
|
||||
|
||||
function stripAnsi(str: string) {
|
||||
return str.replace(ansiRegex, "");
|
||||
}
|
||||
|
||||
class EditorStateManager {
|
||||
private diagnosticCollection: vscode.DiagnosticCollection;
|
||||
private disposables: vscode.Disposable[] = [];
|
||||
|
||||
public constructor() {
|
||||
this.diagnosticCollection = vscode.languages.createDiagnosticCollection("BunDiagnostics");
|
||||
}
|
||||
|
||||
getVisibleEditorsWithErrors() {
|
||||
return vscode.window.visibleTextEditors.filter(editor => {
|
||||
const diagnostics = this.diagnosticCollection.get(editor.document.uri);
|
||||
|
||||
return diagnostics && diagnostics.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
clearInFile(uri: vscode.Uri) {
|
||||
if (this.diagnosticCollection.has(uri)) {
|
||||
output.appendLine(`Clearing diagnostics for ${uri.toString()}`);
|
||||
this.diagnosticCollection.delete(uri);
|
||||
}
|
||||
}
|
||||
|
||||
clearAll(reason: string) {
|
||||
output.appendLine("Clearing all because: " + reason);
|
||||
this.diagnosticCollection.clear();
|
||||
}
|
||||
|
||||
set(uri: vscode.Uri, diagnostic: vscode.Diagnostic) {
|
||||
this.diagnosticCollection.set(uri, [diagnostic]);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.clearAll("Editor state was disposed");
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
class BunDiagnosticsManager {
|
||||
private readonly editorState: EditorStateManager;
|
||||
private readonly signal: UnixSignal | TCPSocketSignal;
|
||||
private readonly context: vscode.ExtensionContext;
|
||||
|
||||
public get signalUrl() {
|
||||
return this.signal.url;
|
||||
}
|
||||
|
||||
private static async getOrRecreateSignal(context: vscode.ExtensionContext) {
|
||||
const globalState = typedGlobalState(context.globalState);
|
||||
const existing = globalState.get("BUN_INSPECT_CONNECT_TO");
|
||||
|
||||
const isWin = os.platform() === "win32";
|
||||
|
||||
if (existing) {
|
||||
if (existing.type === "unix") {
|
||||
output.appendLine(`Reusing existing unix socket: ${existing.url}`);
|
||||
|
||||
if ("url" in existing) {
|
||||
await fs.unlink(existing.url).catch(() => {
|
||||
// ? lol
|
||||
});
|
||||
}
|
||||
|
||||
return new UnixSignal(existing.url);
|
||||
} else {
|
||||
output.appendLine(`Reusing existing tcp socket on: ${existing.port}`);
|
||||
return new TCPSocketSignal(existing.port);
|
||||
}
|
||||
}
|
||||
|
||||
if (isWin) {
|
||||
const port = await getAvailablePort();
|
||||
|
||||
await globalState.update("BUN_INSPECT_CONNECT_TO", {
|
||||
type: "tcp",
|
||||
port,
|
||||
});
|
||||
|
||||
output.appendLine(`Created new tcp socket on: ${port}`);
|
||||
|
||||
return new TCPSocketSignal(port);
|
||||
} else {
|
||||
const signal = new UnixSignal();
|
||||
|
||||
await globalState.update("BUN_INSPECT_CONNECT_TO", {
|
||||
type: "unix",
|
||||
url: signal.url,
|
||||
});
|
||||
|
||||
output.appendLine(`Created new unix socket: ${signal.url}`);
|
||||
|
||||
return signal;
|
||||
}
|
||||
}
|
||||
|
||||
// private static getOrCreateOldVersionInspectURL = createGlobalStateGenerationFn(
|
||||
// "DIAGNOSTICS_BUN_INSPECT",
|
||||
// async () => {
|
||||
// const url =
|
||||
// process.platform === "win32"
|
||||
// ? `ws://127.0.0.1:${await getAvailablePort()}/${getRandomId()}`
|
||||
// : `ws+unix://${os.tmpdir()}/${getRandomId()}.sock`;
|
||||
|
||||
// return url;
|
||||
// },
|
||||
// );
|
||||
|
||||
public static async initialize(context: vscode.ExtensionContext) {
|
||||
const signal = await BunDiagnosticsManager.getOrRecreateSignal(context);
|
||||
|
||||
return new BunDiagnosticsManager(context, signal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when Bun pings BUN_INSPECT_NOTIFY (indicating a program has started).
|
||||
*/
|
||||
private async handleSocketConnection(socket: Socket) {
|
||||
const debugAdapter = new NodeSocketDebugAdapter(socket);
|
||||
|
||||
this.editorState.clearAll("A new socket connected");
|
||||
|
||||
debugAdapter.on("LifecycleReporter.reload", async () => {
|
||||
this.editorState.clearAll("LifecycleReporter reported a reload event");
|
||||
});
|
||||
|
||||
debugAdapter.on("Inspector.event", e => {
|
||||
output.appendLine(`Received inspector event: ${e.method}`);
|
||||
});
|
||||
|
||||
debugAdapter.on("Inspector.error", e => {
|
||||
output.appendLine(inspect(e, true, null));
|
||||
});
|
||||
|
||||
debugAdapter.on("LifecycleReporter.error", event => this.handleLifecycleError(event));
|
||||
|
||||
const ok = await debugAdapter.start();
|
||||
|
||||
if (!ok) {
|
||||
await vscode.window.showErrorMessage("Failed to start debug adapter");
|
||||
debugAdapter.removeAllListeners();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
debugAdapter.initialize({
|
||||
adapterID: "bun-vsc-terminal-debug-adapter",
|
||||
enableControlFlowProfiler: false,
|
||||
enableLifecycleAgentReporter: true,
|
||||
sendImmediatePreventExit: false,
|
||||
enableDebugger: false, // Performance overhead when debugger is enabled
|
||||
});
|
||||
}
|
||||
|
||||
private handleLifecycleError(event: JSC.LifecycleReporter.ErrorEvent) {
|
||||
const message = stripAnsi(event.message).trim() || event.name || "Error";
|
||||
|
||||
output.appendLine(
|
||||
`Received error event: '{name:${event.name}} ${message.split("\n")[0].trim().substring(0, 100)}'`,
|
||||
);
|
||||
|
||||
const [url = null] = event.urls;
|
||||
const [line = null, col = null] = event.lineColumns;
|
||||
|
||||
if (url === null || url.length === 0 || line === null || col === null) {
|
||||
output.appendLine("No valid url or line/column found in error event");
|
||||
output.appendLine(JSON.stringify(event));
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = vscode.Uri.file(url);
|
||||
|
||||
// range is really just 1 character here..
|
||||
const range = new vscode.Range(new vscode.Position(line - 1, col - 1), new vscode.Position(line - 1, col));
|
||||
|
||||
const document = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString());
|
||||
|
||||
// ...but we want to highlight the entire word after(inclusive) the character
|
||||
const rangeOfWord = document?.getWordRangeAtPosition(range.start) ?? range; // Fallback to just the character if no editor or no word range is found
|
||||
|
||||
const diagnostic = new vscode.Diagnostic(rangeOfWord, message, vscode.DiagnosticSeverity.Error);
|
||||
|
||||
diagnostic.source = "Bun";
|
||||
|
||||
const relatedInformation = event.urls.flatMap((url, i) => {
|
||||
if (i === 0 || url === "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [line = null, col = null] = event.lineColumns.slice(i * 2, i * 2 + 2);
|
||||
|
||||
if (line === null || col === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new vscode.DiagnosticRelatedInformation(
|
||||
new vscode.Location(vscode.Uri.file(url), new vscode.Position(line - 1, col - 1)),
|
||||
message,
|
||||
),
|
||||
];
|
||||
});
|
||||
|
||||
diagnostic.relatedInformation = relatedInformation;
|
||||
|
||||
this.editorState.set(uri, diagnostic);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
return vscode.Disposable.from(this.editorState, {
|
||||
dispose: () => {
|
||||
this.signal.close();
|
||||
this.signal.removeAllListeners();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private constructor(context: vscode.ExtensionContext, signal: UnixSignal | TCPSocketSignal) {
|
||||
this.editorState = new EditorStateManager();
|
||||
this.signal = signal;
|
||||
this.context = context;
|
||||
|
||||
this.context.subscriptions.push(
|
||||
// on did type
|
||||
vscode.workspace.onDidChangeTextDocument(e => {
|
||||
this.editorState.clearInFile(e.document.uri);
|
||||
}),
|
||||
);
|
||||
|
||||
this.signal.on("Signal.Socket.connect", this.handleSocketConnection.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
const description = new vscode.MarkdownString(
|
||||
"Bun's VSCode extension communicates with Bun over a socket. We set the url in your terminal with the `BUN_INSPECT_NOTIFY` environment variable",
|
||||
);
|
||||
|
||||
export async function registerDiagnosticsSocket(context: vscode.ExtensionContext) {
|
||||
context.environmentVariableCollection.clear();
|
||||
context.environmentVariableCollection.description = description;
|
||||
|
||||
const manager = await BunDiagnosticsManager.initialize(context);
|
||||
|
||||
context.environmentVariableCollection.replace("BUN_INSPECT_CONNECT_TO", manager.signalUrl);
|
||||
|
||||
context.subscriptions.push(manager);
|
||||
}
|
||||
@@ -49,7 +49,7 @@ function renderLockfile({ webview }: vscode.WebviewPanel, preview: string, exten
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource};">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource};">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="${styleVSCodeUri}" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
204
packages/bun-vscode/src/features/tests/index.ts
Normal file
204
packages/bun-vscode/src/features/tests/index.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import ts from "typescript";
|
||||
import * as vscode from "vscode";
|
||||
|
||||
/**
|
||||
* Find all matching test via ts AST
|
||||
*/
|
||||
function findTests(document: vscode.TextDocument): Array<{ name: string; range: vscode.Range }> {
|
||||
const sourceFile = ts.createSourceFile(document.fileName, document.getText(), ts.ScriptTarget.Latest, true);
|
||||
const tests: Array<{ name: string; range: vscode.Range }> = [];
|
||||
|
||||
// Visit all nodes in the AST
|
||||
function visit(node: ts.Node) {
|
||||
if (ts.isCallExpression(node)) {
|
||||
const expressionText = node.expression.getText(sourceFile);
|
||||
|
||||
// Check if the expression is a test function
|
||||
const isTest = expressionText === "test" || expressionText === "describe" || expressionText === "it";
|
||||
|
||||
if (!isTest) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the test name from the first argument
|
||||
const testName = node.arguments[0] && ts.isStringLiteral(node.arguments[0]) ? node.arguments[0].text : null;
|
||||
if (!testName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the range of the test function for the CodeLens
|
||||
const start = document.positionAt(node.getStart());
|
||||
const end = document.positionAt(node.getEnd());
|
||||
const range = new vscode.Range(start, end);
|
||||
tests.push({ name: testName, range });
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
return tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class provides CodeLens for test functions in the editor - find all tests in current document and provide CodeLens for them.
|
||||
* It finds all test functions in the current document and provides CodeLens for them (Run Test, Watch Test buttons).
|
||||
*/
|
||||
class TestCodeLensProvider implements vscode.CodeLensProvider {
|
||||
public provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] {
|
||||
const codeLenses: vscode.CodeLens[] = [];
|
||||
const tests = findTests(document);
|
||||
|
||||
for (const test of tests) {
|
||||
const runTestCommand = {
|
||||
title: "Run Test",
|
||||
command: "extension.bun.runTest",
|
||||
arguments: [document.fileName, test.name],
|
||||
};
|
||||
|
||||
const watchTestCommand = {
|
||||
title: "Watch Test",
|
||||
command: "extension.bun.watchTest",
|
||||
arguments: [document.fileName, test.name],
|
||||
};
|
||||
|
||||
codeLenses.push(new vscode.CodeLens(test.range, runTestCommand));
|
||||
codeLenses.push(new vscode.CodeLens(test.range, watchTestCommand));
|
||||
}
|
||||
|
||||
return codeLenses;
|
||||
}
|
||||
}
|
||||
|
||||
// default file pattern to search for tests
|
||||
const DEFAULT_FILE_PATTERN = "**/*{.test.,.spec.,_test_,_spec_}{js,ts,tsx,jsx,mts,cts}";
|
||||
|
||||
/**
|
||||
* This function registers a CodeLens provider for test files. It is used to display the "Run" and "Watch" buttons.
|
||||
*/
|
||||
export function registerTestCodeLens(context: vscode.ExtensionContext) {
|
||||
const codeLensProvider = new TestCodeLensProvider();
|
||||
|
||||
// Get the user-defined file pattern from the settings, or use the default
|
||||
// Setting is:
|
||||
// bun.test.filePattern
|
||||
const pattern = vscode.workspace.getConfiguration("bun.test").get("filePattern", DEFAULT_FILE_PATTERN);
|
||||
const options = { scheme: "file", pattern };
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeLensProvider({ ...options, language: "javascript" }, codeLensProvider),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeLensProvider({ ...options, language: "typescript" }, codeLensProvider),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeLensProvider({ ...options, language: "javascriptreact" }, codeLensProvider),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeLensProvider({ ...options, language: "typescriptreact" }, codeLensProvider),
|
||||
);
|
||||
}
|
||||
|
||||
// Tracking only one active terminal, so there will be only one terminal running at a time.
|
||||
// Example: when user clicks "Run Test" button, the previous terminal will be disposed.
|
||||
let activeTerminal: vscode.Terminal | null = null;
|
||||
|
||||
/**
|
||||
* This function registers the test runner commands.
|
||||
*/
|
||||
export function registerTestRunner(context: vscode.ExtensionContext) {
|
||||
// Register the "Run Test" command
|
||||
const runTestCommand = vscode.commands.registerCommand(
|
||||
"extension.bun.runTest",
|
||||
async (filePath?: string, testName?: string, isWatchMode: boolean = false) => {
|
||||
// Get custom flag
|
||||
const customFlag = vscode.workspace.getConfiguration("bun.test").get("customFlag", "").trim();
|
||||
const customScriptSetting = vscode.workspace.getConfiguration("bun.test").get("customScript", "bun test").trim();
|
||||
|
||||
const customScript = customScriptSetting.length ? customScriptSetting : "bun test";
|
||||
|
||||
// When this command is called from the command palette, the fileName and testName arguments are not passed (commands in package.json)
|
||||
// so then fileName is taken from the active text editor and it run for the whole file.
|
||||
if (!filePath) {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
|
||||
if (!editor) {
|
||||
await vscode.window.showErrorMessage("No active editor to run tests in");
|
||||
return;
|
||||
}
|
||||
|
||||
filePath = editor.document.fileName;
|
||||
}
|
||||
|
||||
// Detect if along file path there is package.json, like in mono-repo, if so, then switch to that directory
|
||||
const packageJsonPaths = await vscode.workspace.findFiles("**/package.json");
|
||||
|
||||
// Sort by length, so the longest path is first, so we can switch to the deepest directory
|
||||
const packagesRootPaths = packageJsonPaths
|
||||
.map(uri => uri.fsPath.replace("/package.json", ""))
|
||||
.sort((a, b) => b.length - a.length);
|
||||
|
||||
const packageJsonPath: string | undefined = packagesRootPaths.find(path => filePath.includes(path));
|
||||
|
||||
if (activeTerminal) {
|
||||
activeTerminal.dispose();
|
||||
activeTerminal = null;
|
||||
}
|
||||
|
||||
const cwd = packageJsonPath ?? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? process.cwd();
|
||||
|
||||
const message = isWatchMode
|
||||
? `Watching \x1b[1m\x1b[32m${testName ?? filePath}\x1b[0m test`
|
||||
: `Running \x1b[1m\x1b[32m${testName ?? filePath}\x1b[0m test`;
|
||||
|
||||
const terminalOptions: vscode.TerminalOptions = {
|
||||
cwd,
|
||||
name: "Bun Test Runner",
|
||||
location: vscode.TerminalLocation.Panel,
|
||||
message,
|
||||
hideFromUser: true,
|
||||
};
|
||||
|
||||
activeTerminal = vscode.window.createTerminal(terminalOptions);
|
||||
activeTerminal.show();
|
||||
|
||||
let command = customScript;
|
||||
|
||||
if (filePath.length !== 0) {
|
||||
command += ` ${filePath}`;
|
||||
}
|
||||
|
||||
if (testName && testName.length) {
|
||||
if (customScriptSetting.length) {
|
||||
// escape the quotes in the test name
|
||||
command += ` -t "${testName}"`;
|
||||
} else {
|
||||
command += ` -t "${testName}"`;
|
||||
}
|
||||
}
|
||||
|
||||
if (isWatchMode) {
|
||||
command += ` --watch`;
|
||||
}
|
||||
|
||||
if (customFlag.length) {
|
||||
command += ` ${customFlag}`;
|
||||
}
|
||||
|
||||
activeTerminal.sendText(command);
|
||||
},
|
||||
);
|
||||
|
||||
// Register the "Watch Test" command, which just calls the "Run Test" command with the watch flag
|
||||
const watchTestCommand = vscode.commands.registerCommand(
|
||||
"extension.bun.watchTest",
|
||||
async (fileName?: string, testName?: string) => {
|
||||
vscode.commands.executeCommand("extension.bun.runTest", fileName, testName, true);
|
||||
},
|
||||
);
|
||||
|
||||
context.subscriptions.push(runTestCommand);
|
||||
context.subscriptions.push(watchTestCommand);
|
||||
}
|
||||
65
packages/bun-vscode/src/global-state.ts
Normal file
65
packages/bun-vscode/src/global-state.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ExtensionContext } from "vscode";
|
||||
|
||||
export const GLOBAL_STATE_VERSION = 1;
|
||||
|
||||
export type GlobalStateTypes = {
|
||||
BUN_INSPECT_CONNECT_TO:
|
||||
| {
|
||||
type: "tcp";
|
||||
port: number;
|
||||
}
|
||||
| {
|
||||
type: "unix";
|
||||
url: string;
|
||||
};
|
||||
|
||||
DIAGNOSTICS_BUN_INSPECT: string;
|
||||
};
|
||||
|
||||
export async function clearGlobalState(gs: ExtensionContext["globalState"]) {
|
||||
const tgs = typedGlobalState(gs);
|
||||
|
||||
await Promise.all(tgs.keys().map(key => tgs.update(key, undefined as never)));
|
||||
}
|
||||
|
||||
export function typedGlobalState(state: ExtensionContext["globalState"]) {
|
||||
return state as {
|
||||
get<K extends keyof GlobalStateTypes>(key: K): GlobalStateTypes[K] | undefined;
|
||||
|
||||
keys(): readonly (keyof GlobalStateTypes)[];
|
||||
|
||||
update<K extends keyof GlobalStateTypes>(key: K, value: GlobalStateTypes[K]): Thenable<void>;
|
||||
|
||||
/**
|
||||
* Set the keys whose values should be synchronized across devices when synchronizing user-data
|
||||
* like configuration, extensions, and mementos.
|
||||
*
|
||||
* Note that this function defines the whole set of keys whose values are synchronized:
|
||||
* - calling it with an empty array stops synchronization for this memento
|
||||
* - calling it with a non-empty array replaces all keys whose values are synchronized
|
||||
*
|
||||
* For any given set of keys this function needs to be called only once but there is no harm in
|
||||
* repeatedly calling it.
|
||||
*
|
||||
* @param keys The set of keys whose values are synced.
|
||||
*/
|
||||
setKeysForSync(keys: readonly (keyof GlobalStateTypes)[]): void;
|
||||
};
|
||||
}
|
||||
|
||||
export function createGlobalStateGenerationFn<T extends keyof GlobalStateTypes>(
|
||||
key: T,
|
||||
resolve: () => Promise<GlobalStateTypes[T]>,
|
||||
) {
|
||||
return async (gs: ExtensionContext["globalState"]) => {
|
||||
const value = (gs as TypedGlobalState).get(key);
|
||||
if (value) return value;
|
||||
|
||||
const next = await resolve();
|
||||
await (gs as TypedGlobalState).update(key, next);
|
||||
|
||||
return next;
|
||||
};
|
||||
}
|
||||
|
||||
export type TypedGlobalState = ReturnType<typeof typedGlobalState>;
|
||||
@@ -7,7 +7,7 @@
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": false,
|
||||
"skipLibCheck": true,
|
||||
"types": ["bun-types"]
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src", "test", "types", "../../bun-devtools", "../../bun-debug-adapter-protocol"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -32,7 +32,10 @@ async function doBuildkiteAgent(action) {
|
||||
|
||||
let homePath, cachePath, logsPath, agentLogPath, pidPath;
|
||||
if (isWindows) {
|
||||
throw new Error("TODO: Windows");
|
||||
homePath = "C:\\buildkite-agent";
|
||||
cachePath = join(homePath, "cache");
|
||||
logsPath = join(homePath, "logs");
|
||||
agentLogPath = join(logsPath, "buildkite-agent.log");
|
||||
} else {
|
||||
homePath = "/var/lib/buildkite-agent";
|
||||
cachePath = "/var/cache/buildkite-agent";
|
||||
@@ -45,6 +48,19 @@ async function doBuildkiteAgent(action) {
|
||||
const command = process.execPath;
|
||||
const args = [realpathSync(process.argv[1]), "start"];
|
||||
|
||||
if (isWindows) {
|
||||
const serviceCommand = [
|
||||
"New-Service",
|
||||
"-Name",
|
||||
"buildkite-agent",
|
||||
"-StartupType",
|
||||
"Automatic",
|
||||
"-BinaryPathName",
|
||||
`${escape(command)} ${escape(args.map(escape).join(" "))}`,
|
||||
];
|
||||
await spawnSafe(["powershell", "-Command", serviceCommand.join(" ")], { stdio: "inherit" });
|
||||
}
|
||||
|
||||
if (isOpenRc()) {
|
||||
const servicePath = "/etc/init.d/buildkite-agent";
|
||||
const service = `#!/sbin/openrc-run
|
||||
@@ -67,6 +83,7 @@ async function doBuildkiteAgent(action) {
|
||||
}
|
||||
`;
|
||||
writeFile(servicePath, service, { mode: 0o755 });
|
||||
writeFile(`/etc/conf.d/buildkite-agent`, `rc_ulimit="-n 262144"`);
|
||||
await spawnSafe(["rc-update", "add", "buildkite-agent", "default"], { stdio: "inherit", privileged: true });
|
||||
}
|
||||
|
||||
@@ -77,11 +94,11 @@ async function doBuildkiteAgent(action) {
|
||||
Description=Buildkite Agent
|
||||
After=syslog.target
|
||||
After=network-online.target
|
||||
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=${username}
|
||||
ExecStart=${escape(command)} ${escape(args.map(escape).join(" "))}
|
||||
ExecStart=${escape(command)} ${args.map(escape).join(" ")}
|
||||
RestartSec=5
|
||||
Restart=on-failure
|
||||
KillMode=process
|
||||
@@ -89,7 +106,7 @@ async function doBuildkiteAgent(action) {
|
||||
[Journal]
|
||||
Storage=persistent
|
||||
StateDirectory=${escape(agentLogPath)}
|
||||
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`;
|
||||
|
||||
339
scripts/bootstrap.ps1
Executable file
339
scripts/bootstrap.ps1
Executable file
@@ -0,0 +1,339 @@
|
||||
# Version: 4
|
||||
# A powershell script that installs the dependencies needed to build and test Bun.
|
||||
# This should work on Windows 10 or newer.
|
||||
|
||||
# If this script does not work on your machine, please open an issue:
|
||||
# https://github.com/oven-sh/bun/issues
|
||||
|
||||
# If you need to make a change to this script, such as upgrading a dependency,
|
||||
# increment the version comment to indicate that a new image should be built.
|
||||
# Otherwise, the existing image will be retroactively updated.
|
||||
|
||||
param (
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$CI = $false,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$Optimize = $CI
|
||||
)
|
||||
|
||||
function Execute-Command {
|
||||
$command = $args -join ' '
|
||||
Write-Output "$ $command"
|
||||
|
||||
& $args[0] $args[1..$args.Length]
|
||||
|
||||
if ((-not $?) -or ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE)) {
|
||||
throw "Command failed: $command"
|
||||
}
|
||||
}
|
||||
|
||||
function Which {
|
||||
param ([switch]$Required = $false)
|
||||
|
||||
foreach ($command in $args) {
|
||||
$result = Get-Command $command -ErrorAction SilentlyContinue
|
||||
if ($result -and $result.Path) {
|
||||
return $result.Path
|
||||
}
|
||||
}
|
||||
|
||||
if ($Required) {
|
||||
$commands = $args -join ', '
|
||||
throw "Command not found: $commands"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Chocolatey {
|
||||
if (Which choco) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Output "Installing Chocolatey..."
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
iex -Command ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
Refresh-Path
|
||||
}
|
||||
|
||||
function Refresh-Path {
|
||||
$paths = @(
|
||||
[System.Environment]::GetEnvironmentVariable("Path", "Machine"),
|
||||
[System.Environment]::GetEnvironmentVariable("Path", "User"),
|
||||
[System.Environment]::GetEnvironmentVariable("Path", "Process")
|
||||
)
|
||||
$uniquePaths = $paths |
|
||||
Where-Object { $_ } |
|
||||
ForEach-Object { $_.Split(';', [StringSplitOptions]::RemoveEmptyEntries) } |
|
||||
Where-Object { $_ -and (Test-Path $_) } |
|
||||
Select-Object -Unique
|
||||
$env:Path = ($uniquePaths -join ';').TrimEnd(';')
|
||||
|
||||
if ($env:ChocolateyInstall) {
|
||||
Import-Module $env:ChocolateyInstall\helpers\chocolateyProfile.psm1 -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Add-To-Path {
|
||||
$absolutePath = Resolve-Path $args[0]
|
||||
$currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine")
|
||||
if ($currentPath -like "*$absolutePath*") {
|
||||
return
|
||||
}
|
||||
|
||||
$newPath = $currentPath.TrimEnd(";") + ";" + $absolutePath
|
||||
if ($newPath.Length -ge 2048) {
|
||||
Write-Warning "PATH is too long, removing duplicate and old entries..."
|
||||
|
||||
$paths = $currentPath.Split(';', [StringSplitOptions]::RemoveEmptyEntries) |
|
||||
Where-Object { $_ -and (Test-Path $_) } |
|
||||
Select-Object -Unique
|
||||
|
||||
$paths += $absolutePath
|
||||
$newPath = $paths -join ';'
|
||||
while ($newPath.Length -ge 2048 -and $paths.Count -gt 1) {
|
||||
$paths = $paths[1..$paths.Count]
|
||||
$newPath = $paths -join ';'
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output "Adding $absolutePath to PATH..."
|
||||
[Environment]::SetEnvironmentVariable("Path", $newPath, "Machine")
|
||||
Refresh-Path
|
||||
}
|
||||
|
||||
function Install-Package {
|
||||
param (
|
||||
[Parameter(Mandatory = $true, Position = 0)]
|
||||
[string]$Name,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Command = $Name,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Version,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$Force = $false,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string[]]$ExtraArgs = @()
|
||||
)
|
||||
|
||||
if (-not $Force `
|
||||
-and (Which $Command) `
|
||||
-and (-not $Version -or (& $Command --version) -like "*$Version*")) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Output "Installing $Name..."
|
||||
$flags = @(
|
||||
"--yes",
|
||||
"--accept-license",
|
||||
"--no-progress",
|
||||
"--force"
|
||||
)
|
||||
if ($Version) {
|
||||
$flags += "--version=$Version"
|
||||
}
|
||||
|
||||
Execute-Command choco install $Name @flags @ExtraArgs
|
||||
Refresh-Path
|
||||
}
|
||||
|
||||
function Install-Packages {
|
||||
foreach ($package in $args) {
|
||||
Install-Package -Name $package
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Common-Software {
|
||||
Install-Chocolatey
|
||||
Install-Pwsh
|
||||
Install-Git
|
||||
Install-Packages curl 7zip
|
||||
Install-NodeJs
|
||||
Install-Bun
|
||||
Install-Cygwin
|
||||
if ($CI) {
|
||||
Install-Tailscale
|
||||
Install-Buildkite
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Pwsh {
|
||||
Install-Package powershell-core -Command pwsh
|
||||
|
||||
if ($CI) {
|
||||
$shellPath = (Which pwsh -Required)
|
||||
New-ItemProperty `
|
||||
-Path "HKLM:\\SOFTWARE\\OpenSSH" `
|
||||
-Name DefaultShell `
|
||||
-Value $shellPath `
|
||||
-PropertyType String `
|
||||
-Force
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Git {
|
||||
Install-Packages git
|
||||
|
||||
if ($CI) {
|
||||
Execute-Command git config --system --add safe.directory "*"
|
||||
Execute-Command git config --system core.autocrlf false
|
||||
Execute-Command git config --system core.eol lf
|
||||
Execute-Command git config --system core.longpaths true
|
||||
}
|
||||
}
|
||||
|
||||
function Install-NodeJs {
|
||||
Install-Package nodejs -Command node -Version "22.9.0"
|
||||
}
|
||||
|
||||
function Install-Bun {
|
||||
Install-Package bun -Version "1.1.30"
|
||||
}
|
||||
|
||||
function Install-Cygwin {
|
||||
Install-Package cygwin
|
||||
Add-To-Path "C:\tools\cygwin\bin"
|
||||
}
|
||||
|
||||
function Install-Tailscale {
|
||||
Install-Package tailscale
|
||||
}
|
||||
|
||||
function Install-Buildkite {
|
||||
if (Which buildkite-agent) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Output "Installing Buildkite agent..."
|
||||
$env:buildkiteAgentToken = "xxx"
|
||||
iex ((New-Object System.Net.WebClient).DownloadString("https://raw.githubusercontent.com/buildkite/agent/main/install.ps1"))
|
||||
Refresh-Path
|
||||
}
|
||||
|
||||
function Install-Build-Essentials {
|
||||
# Install-Visual-Studio
|
||||
Install-Packages `
|
||||
cmake `
|
||||
make `
|
||||
ninja `
|
||||
ccache `
|
||||
python `
|
||||
golang `
|
||||
nasm `
|
||||
ruby `
|
||||
mingw
|
||||
Install-Rust
|
||||
Install-Llvm
|
||||
}
|
||||
|
||||
function Install-Visual-Studio {
|
||||
$components = @(
|
||||
"Microsoft.VisualStudio.Workload.NativeDesktop",
|
||||
"Microsoft.VisualStudio.Component.Windows10SDK.18362",
|
||||
"Microsoft.VisualStudio.Component.Windows11SDK.22000",
|
||||
"Microsoft.VisualStudio.Component.Windows11Sdk.WindowsPerformanceToolkit",
|
||||
"Microsoft.VisualStudio.Component.VC.ASAN", # C++ AddressSanitizer
|
||||
"Microsoft.VisualStudio.Component.VC.ATL", # C++ ATL for latest v143 build tools (x86 & x64)
|
||||
"Microsoft.VisualStudio.Component.VC.DiagnosticTools", # C++ Diagnostic Tools
|
||||
"Microsoft.VisualStudio.Component.VC.CLI.Support", # C++/CLI support for v143 build tools (Latest)
|
||||
"Microsoft.VisualStudio.Component.VC.CoreIde", # C++ core features
|
||||
"Microsoft.VisualStudio.Component.VC.Redist.14.Latest" # C++ 2022 Redistributable Update
|
||||
)
|
||||
|
||||
$arch = (Get-WmiObject Win32_Processor).Architecture
|
||||
if ($arch -eq 9) {
|
||||
$components += @(
|
||||
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64", # MSVC v143 build tools (x86 & x64)
|
||||
"Microsoft.VisualStudio.Component.VC.Modules.x86.x64" # MSVC v143 C++ Modules for latest v143 build tools (x86 & x64)
|
||||
)
|
||||
} elseif ($arch -eq 5) {
|
||||
$components += @(
|
||||
"Microsoft.VisualStudio.Component.VC.Tools.ARM64", # MSVC v143 build tools (ARM64)
|
||||
"Microsoft.VisualStudio.Component.UWP.VC.ARM64" # C++ Universal Windows Platform support for v143 build tools (ARM64/ARM64EC)
|
||||
)
|
||||
}
|
||||
|
||||
$packageParameters = $components | ForEach-Object { "--add $_" }
|
||||
Install-Package visualstudio2022community `
|
||||
-ExtraArgs "--package-parameters '--add Microsoft.VisualStudio.Workload.NativeDesktop --includeRecommended --includeOptional'"
|
||||
}
|
||||
|
||||
function Install-Rust {
|
||||
if (Which rustc) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Output "Installing Rust..."
|
||||
$rustupInit = "$env:TEMP\rustup-init.exe"
|
||||
(New-Object System.Net.WebClient).DownloadFile("https://win.rustup.rs/", $rustupInit)
|
||||
Execute-Command $rustupInit -y
|
||||
Add-To-Path "$env:USERPROFILE\.cargo\bin"
|
||||
}
|
||||
|
||||
function Install-Llvm {
|
||||
Install-Package llvm `
|
||||
-Command clang-cl `
|
||||
-Version "18.1.8"
|
||||
Add-To-Path "C:\Program Files\LLVM\bin"
|
||||
}
|
||||
|
||||
function Optimize-System {
|
||||
Disable-Windows-Defender
|
||||
Disable-Windows-Threat-Protection
|
||||
Disable-Windows-Services
|
||||
Disable-Power-Management
|
||||
Uninstall-Windows-Defender
|
||||
}
|
||||
|
||||
function Disable-Windows-Defender {
|
||||
Write-Output "Disabling Windows Defender..."
|
||||
Set-MpPreference -DisableRealtimeMonitoring $true
|
||||
Add-MpPreference -ExclusionPath "C:\", "D:\"
|
||||
}
|
||||
|
||||
function Disable-Windows-Threat-Protection {
|
||||
$itemPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection"
|
||||
if (Test-Path $itemPath) {
|
||||
Write-Output "Disabling Windows Threat Protection..."
|
||||
Set-ItemProperty -Path $itemPath -Name "ForceDefenderPassiveMode" -Value 1 -Type DWORD
|
||||
}
|
||||
}
|
||||
|
||||
function Uninstall-Windows-Defender {
|
||||
Write-Output "Uninstalling Windows Defender..."
|
||||
Uninstall-WindowsFeature -Name Windows-Defender
|
||||
}
|
||||
|
||||
function Disable-Windows-Services {
|
||||
$services = @(
|
||||
"WSearch", # Windows Search
|
||||
"wuauserv", # Windows Update
|
||||
"DiagTrack", # Connected User Experiences and Telemetry
|
||||
"dmwappushservice", # WAP Push Message Routing Service
|
||||
"PcaSvc", # Program Compatibility Assistant
|
||||
"SysMain" # Superfetch
|
||||
)
|
||||
|
||||
foreach ($service in $services) {
|
||||
Stop-Service $service -Force
|
||||
Set-Service $service -StartupType Disabled
|
||||
}
|
||||
}
|
||||
|
||||
function Disable-Power-Management {
|
||||
Write-Output "Disabling power management features..."
|
||||
powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c # High performance
|
||||
powercfg /change monitor-timeout-ac 0
|
||||
powercfg /change monitor-timeout-dc 0
|
||||
powercfg /change standby-timeout-ac 0
|
||||
powercfg /change standby-timeout-dc 0
|
||||
powercfg /change hibernate-timeout-ac 0
|
||||
powercfg /change hibernate-timeout-dc 0
|
||||
}
|
||||
|
||||
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
|
||||
if ($Optimize) {
|
||||
Optimize-System
|
||||
}
|
||||
|
||||
Install-Common-Software
|
||||
Install-Build-Essentials
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
# Version: 4
|
||||
# Version: 5
|
||||
|
||||
# A script that installs the dependencies needed to build and test Bun.
|
||||
# This should work on macOS and Linux with a POSIX shell.
|
||||
@@ -124,6 +124,22 @@ append_to_file() {
|
||||
done
|
||||
}
|
||||
|
||||
append_to_file_sudo() {
|
||||
file="$1"
|
||||
content="$2"
|
||||
|
||||
if ! [ -f "$file" ]; then
|
||||
execute_sudo mkdir -p "$(dirname "$file")"
|
||||
execute_sudo touch "$file"
|
||||
fi
|
||||
|
||||
echo "$content" | while read -r line; do
|
||||
if ! grep -q "$line" "$file"; then
|
||||
echo "$line" | execute_sudo tee "$file" > /dev/null
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
append_to_profile() {
|
||||
content="$1"
|
||||
profiles=".profile .zprofile .bash_profile .bashrc .zshrc"
|
||||
@@ -309,7 +325,7 @@ check_package_manager() {
|
||||
pm="brew"
|
||||
;;
|
||||
linux)
|
||||
if [ -f "$(which apt-get)" ]; then
|
||||
if [ -f "$(which apt)" ]; then
|
||||
pm="apt"
|
||||
elif [ -f "$(which dnf)" ]; then
|
||||
pm="dnf"
|
||||
@@ -327,7 +343,8 @@ check_package_manager() {
|
||||
print "Updating package manager..."
|
||||
case "$pm" in
|
||||
apt)
|
||||
DEBIAN_FRONTEND=noninteractive package_manager update -y
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
package_manager update -y
|
||||
;;
|
||||
apk)
|
||||
package_manager update
|
||||
@@ -370,10 +387,7 @@ check_user() {
|
||||
package_manager() {
|
||||
case "$pm" in
|
||||
apt)
|
||||
while ! sudo -n apt-get update -y; do
|
||||
sleep 1
|
||||
done
|
||||
DEBIAN_FRONTEND=noninteractive execute_sudo apt-get "$@"
|
||||
execute_sudo apt "$@"
|
||||
;;
|
||||
dnf)
|
||||
case "$distro" in
|
||||
@@ -569,28 +583,25 @@ install_nodejs() {
|
||||
install_packages nodejs
|
||||
;;
|
||||
esac
|
||||
|
||||
# Some distros do not install the node headers by default.
|
||||
# These are needed for certain FFI tests, such as: `cc.test.ts`
|
||||
case "$distro" in
|
||||
alpine | amzn)
|
||||
install_nodejs_headers
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_nodejs_headers() {
|
||||
headers_tar="$(download_file "https://nodejs.org/download/release/v$(nodejs_version_exact)/node-v$(nodejs_version_exact)-headers.tar.gz")"
|
||||
headers_dir="$(dirname "$headers_tar")"
|
||||
execute tar -xzf "$headers_tar" -C "$headers_dir"
|
||||
headers_include="$headers_dir/node-v$(nodejs_version_exact)/include"
|
||||
execute_sudo cp -R "$headers_include/" "/usr"
|
||||
}
|
||||
|
||||
install_bun() {
|
||||
case "$os-$abi" in
|
||||
linux-musl)
|
||||
case "$arch" in
|
||||
x64)
|
||||
exe="$(download_file https://pub-61e0d0e2da4146a099e4545a59a9f0f7.r2.dev/bun-musl-x64)"
|
||||
;;
|
||||
aarch64)
|
||||
exe="$(download_file https://pub-61e0d0e2da4146a099e4545a59a9f0f7.r2.dev/bun-musl-arm64)"
|
||||
;;
|
||||
esac
|
||||
execute chmod +x "$exe"
|
||||
execute mkdir -p "$home/.bun/bin"
|
||||
execute mv "$exe" "$home/.bun/bin/bun"
|
||||
execute ln -fs "$home/.bun/bin/bun" "$home/.bun/bin/bunx"
|
||||
link_to_bin "$home/.bun/bin"
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
bash="$(require bash)"
|
||||
script=$(download_file "https://bun.sh/install")
|
||||
|
||||
@@ -841,7 +852,6 @@ create_buildkite_user() {
|
||||
user="buildkite-agent"
|
||||
group="$user"
|
||||
home="/var/lib/buildkite-agent"
|
||||
sh="$(require sh)"
|
||||
|
||||
case "$distro" in
|
||||
amzn)
|
||||
@@ -855,7 +865,6 @@ create_buildkite_user() {
|
||||
execute_sudo useradd "$user" \
|
||||
--system \
|
||||
--no-create-home \
|
||||
--shell "$sh" \
|
||||
--home-dir "$home"
|
||||
fi
|
||||
|
||||
@@ -868,7 +877,7 @@ create_buildkite_user() {
|
||||
execute_sudo mkdir -p "$path"
|
||||
execute_sudo chown -R "$user:$group" "$path"
|
||||
done
|
||||
|
||||
|
||||
files="/var/run/buildkite-agent/buildkite-agent.pid"
|
||||
for file in $files; do
|
||||
execute_sudo touch "$file"
|
||||
@@ -961,6 +970,18 @@ install_chrome_dependencies() {
|
||||
xorg-x11-utils
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$distro" in
|
||||
amzn)
|
||||
install_packages \
|
||||
mesa-libgbm
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
raise_file_descriptor_limit() {
|
||||
append_to_file_sudo /etc/security/limits.conf '* soft nofile 262144'
|
||||
append_to_file_sudo /etc/security/limits.conf '* hard nofile 262144'
|
||||
}
|
||||
|
||||
main() {
|
||||
@@ -973,6 +994,7 @@ main() {
|
||||
install_common_software
|
||||
install_build_essentials
|
||||
install_chrome_dependencies
|
||||
raise_file_descriptor_limit # XXX: temporary
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
tmpdir,
|
||||
waitForPort,
|
||||
which,
|
||||
escapePowershell,
|
||||
} from "./utils.mjs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
@@ -119,7 +120,7 @@ export const aws = {
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @returns {Promise<any>}
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
async spawn(args) {
|
||||
const aws = which("aws");
|
||||
@@ -136,7 +137,14 @@ export const aws = {
|
||||
};
|
||||
}
|
||||
|
||||
const { stdout } = await spawnSafe($`${aws} ${args} --output json`, { env });
|
||||
const { error, stdout } = await spawn($`${aws} ${args} --output json`, { env });
|
||||
if (error) {
|
||||
if (/max attempts exceeded/i.test(inspect(error))) {
|
||||
return this.spawn(args);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(stdout);
|
||||
} catch {
|
||||
@@ -286,16 +294,7 @@ export const aws = {
|
||||
* @link https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/wait/image-available.html
|
||||
*/
|
||||
async waitImage(action, ...imageIds) {
|
||||
while (true) {
|
||||
try {
|
||||
await aws.spawn($`ec2 wait ${action} --image-ids ${imageIds}`);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (!/max attempts exceeded/i.test(inspect(error))) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
await aws.spawn($`ec2 wait ${action} --image-ids ${imageIds}`);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -386,7 +385,7 @@ export const aws = {
|
||||
* @returns {Promise<Machine>}
|
||||
*/
|
||||
async createMachine(options) {
|
||||
const { arch, imageId, instanceType, metadata } = options;
|
||||
const { os, arch, imageId, instanceType, tags } = options;
|
||||
|
||||
/** @type {AwsImage} */
|
||||
let image;
|
||||
@@ -411,14 +410,18 @@ export const aws = {
|
||||
});
|
||||
|
||||
const username = getUsername(Name);
|
||||
const userData = getCloudInit({ ...options, username });
|
||||
|
||||
let userData = getUserData({ ...options, username });
|
||||
if (os === "windows") {
|
||||
userData = `<powershell>${userData}</powershell><powershellArguments>-ExecutionPolicy Unrestricted -NoProfile -NonInteractive</powershellArguments><persist>false</persist>`;
|
||||
}
|
||||
|
||||
let tagSpecification = [];
|
||||
if (metadata) {
|
||||
if (tags) {
|
||||
tagSpecification = ["instance", "volume"].map(resourceType => {
|
||||
return {
|
||||
ResourceType: resourceType,
|
||||
Tags: Object.entries(metadata).map(([Key, Value]) => ({ Key, Value: String(Value) })),
|
||||
Tags: Object.entries(tags).map(([Key, Value]) => ({ Key, Value: String(Value) })),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -435,6 +438,7 @@ export const aws = {
|
||||
"InstanceMetadataTags": "enabled",
|
||||
}),
|
||||
["tag-specifications"]: JSON.stringify(tagSpecification),
|
||||
["key-name"]: "ashcon-bun",
|
||||
});
|
||||
|
||||
return aws.toMachine(instance, { ...options, username });
|
||||
@@ -672,6 +676,14 @@ const google = {
|
||||
* @property {string} [password]
|
||||
*/
|
||||
|
||||
function getUserData(cloudInit) {
|
||||
const { os } = cloudInit;
|
||||
if (os === "windows") {
|
||||
return getWindowsStartupScript(cloudInit);
|
||||
}
|
||||
return getCloudInit(cloudInit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CloudInit} cloudInit
|
||||
* @returns {string}
|
||||
@@ -696,12 +708,6 @@ function getCloudInit(cloudInit) {
|
||||
// https://cloudinit.readthedocs.io/en/stable/
|
||||
return `#cloud-config
|
||||
|
||||
package_update: true
|
||||
packages:
|
||||
- curl
|
||||
- ca-certificates
|
||||
- openssh-server
|
||||
|
||||
write_files:
|
||||
- path: /etc/ssh/sshd_config
|
||||
content: |
|
||||
@@ -722,6 +728,80 @@ function getCloudInit(cloudInit) {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CloudInit} cloudInit
|
||||
* @returns {string}
|
||||
*/
|
||||
function getWindowsStartupScript(cloudInit) {
|
||||
const { sshKeys } = cloudInit;
|
||||
const authorizedKeys = sshKeys.filter(({ publicKey }) => publicKey).map(({ publicKey }) => publicKey);
|
||||
|
||||
return `
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
|
||||
|
||||
function Install-Ssh {
|
||||
$sshService = Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*'
|
||||
if ($sshService.State -ne "Installed") {
|
||||
Write-Output "Installing OpenSSH server..."
|
||||
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
|
||||
}
|
||||
|
||||
$pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path
|
||||
if (-not $pwshPath) {
|
||||
$pwshPath = Get-Command powershell -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path
|
||||
}
|
||||
|
||||
if (-not (Get-Service -Name sshd -ErrorAction SilentlyContinue)) {
|
||||
Write-Output "Enabling OpenSSH server..."
|
||||
Set-Service -Name sshd -StartupType Automatic
|
||||
Start-Service sshd
|
||||
}
|
||||
|
||||
if ($pwshPath) {
|
||||
Write-Output "Setting default shell to $pwshPath..."
|
||||
New-ItemProperty -Path "HKLM:\\SOFTWARE\\OpenSSH" -Name DefaultShell -Value $pwshPath -PropertyType String -Force
|
||||
}
|
||||
|
||||
$firewallRule = Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue
|
||||
if (-not $firewallRule) {
|
||||
Write-Output "Configuring firewall..."
|
||||
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
|
||||
}
|
||||
|
||||
$sshPath = "C:\\ProgramData\\ssh"
|
||||
if (-not (Test-Path $sshPath)) {
|
||||
Write-Output "Creating SSH directory..."
|
||||
New-Item -Path $sshPath -ItemType Directory
|
||||
}
|
||||
|
||||
$authorizedKeysPath = Join-Path $sshPath "administrators_authorized_keys"
|
||||
$authorizedKeys = @(${authorizedKeys.map(key => `"${escapePowershell(key)}"`).join("\n")})
|
||||
if (-not (Test-Path $authorizedKeysPath) -or (Get-Content $authorizedKeysPath) -ne $authorizedKeys) {
|
||||
Write-Output "Adding SSH keys..."
|
||||
Set-Content -Path $authorizedKeysPath -Value $authorizedKeys
|
||||
}
|
||||
|
||||
$sshdConfigPath = Join-Path $sshPath "sshd_config"
|
||||
$sshdConfig = @"
|
||||
PasswordAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
AuthorizedKeysFile $authorizedKeysPath
|
||||
Subsystem sftp sftp-server.exe
|
||||
"@
|
||||
if (-not (Test-Path $sshdConfigPath) -or (Get-Content $sshdConfigPath) -ne $sshdConfig) {
|
||||
Write-Output "Writing SSH configuration..."
|
||||
Set-Content -Path $sshdConfigPath -Value $sshdConfig
|
||||
}
|
||||
|
||||
Write-Output "Restarting SSH server..."
|
||||
Restart-Service sshd
|
||||
}
|
||||
|
||||
Install-Ssh
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} distro
|
||||
* @returns {string}
|
||||
@@ -824,7 +904,7 @@ function getSshKeys() {
|
||||
publicPath,
|
||||
privatePath: publicPath.replace(/\.pub$/, ""),
|
||||
get publicKey() {
|
||||
return readFile(publicPath, { cache: true });
|
||||
return readFile(publicPath, { cache: true }).trim();
|
||||
},
|
||||
})),
|
||||
);
|
||||
@@ -1030,7 +1110,7 @@ async function main() {
|
||||
"cloud": { type: "string", default: "aws" },
|
||||
"os": { type: "string", default: "linux" },
|
||||
"arch": { type: "string", default: "x64" },
|
||||
"distro": { type: "string", default: "debian" },
|
||||
"distro": { type: "string" },
|
||||
"distro-version": { type: "string" },
|
||||
"instance-type": { type: "string" },
|
||||
"image-id": { type: "string" },
|
||||
@@ -1080,7 +1160,7 @@ async function main() {
|
||||
|
||||
let bootstrapPath, agentPath;
|
||||
if (bootstrap) {
|
||||
bootstrapPath = resolve(import.meta.dirname, "bootstrap.sh");
|
||||
bootstrapPath = resolve(import.meta.dirname, os === "windows" ? "bootstrap.ps1" : "bootstrap.sh");
|
||||
if (!existsSync(bootstrapPath)) {
|
||||
throw new Error(`Script not found: ${bootstrapPath}`);
|
||||
}
|
||||
@@ -1127,38 +1207,56 @@ async function main() {
|
||||
});
|
||||
|
||||
if (bootstrapPath) {
|
||||
const remotePath = "/tmp/bootstrap.sh";
|
||||
const args = ci ? ["--ci"] : [];
|
||||
await startGroup("Running bootstrap...", async () => {
|
||||
await machine.upload(bootstrapPath, remotePath);
|
||||
await machine.spawnSafe(["sh", remotePath, ...args], { stdio: "inherit" });
|
||||
});
|
||||
if (os === "windows") {
|
||||
const remotePath = "C:\\Windows\\Temp\\bootstrap.ps1";
|
||||
const args = ci ? ["-CI"] : [];
|
||||
await startGroup("Running bootstrap...", async () => {
|
||||
await machine.upload(bootstrapPath, remotePath);
|
||||
await machine.spawnSafe(["powershell", remotePath, ...args], { stdio: "inherit" });
|
||||
});
|
||||
} else {
|
||||
const remotePath = "/tmp/bootstrap.sh";
|
||||
const args = ci ? ["--ci"] : [];
|
||||
await startGroup("Running bootstrap...", async () => {
|
||||
await machine.upload(bootstrapPath, remotePath);
|
||||
await machine.spawnSafe(["sh", remotePath, ...args], { stdio: "inherit" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (agentPath) {
|
||||
const tmpPath = "/tmp/agent.mjs";
|
||||
const remotePath = "/var/lib/buildkite-agent/agent.mjs";
|
||||
await startGroup("Installing agent...", async () => {
|
||||
await machine.upload(agentPath, tmpPath);
|
||||
const command = [];
|
||||
{
|
||||
const { exitCode } = await machine.spawn(["sudo", "echo", "1"], { stdio: "ignore" });
|
||||
if (exitCode === 0) {
|
||||
command.unshift("sudo");
|
||||
if (os === "windows") {
|
||||
// TODO
|
||||
// const remotePath = "C:\\Windows\\Temp\\agent.mjs";
|
||||
// await startGroup("Installing agent...", async () => {
|
||||
// await machine.upload(agentPath, remotePath);
|
||||
// await machine.spawnSafe(["node", remotePath, "install"], { stdio: "inherit" });
|
||||
// });
|
||||
} else {
|
||||
const tmpPath = "/tmp/agent.mjs";
|
||||
const remotePath = "/var/lib/buildkite-agent/agent.mjs";
|
||||
await startGroup("Installing agent...", async () => {
|
||||
await machine.upload(agentPath, tmpPath);
|
||||
const command = [];
|
||||
{
|
||||
const { exitCode } = await machine.spawn(["sudo", "echo", "1"], { stdio: "ignore" });
|
||||
if (exitCode === 0) {
|
||||
command.unshift("sudo");
|
||||
}
|
||||
}
|
||||
}
|
||||
await machine.spawnSafe([...command, "cp", tmpPath, remotePath]);
|
||||
{
|
||||
const { stdout } = await machine.spawn(["node", "-v"]);
|
||||
const version = parseInt(stdout.trim().replace(/^v/, ""));
|
||||
if (isNaN(version) || version < 20) {
|
||||
command.push("bun");
|
||||
} else {
|
||||
command.push("node");
|
||||
await machine.spawnSafe([...command, "cp", tmpPath, remotePath]);
|
||||
{
|
||||
const { stdout } = await machine.spawn(["node", "-v"]);
|
||||
const version = parseInt(stdout.trim().replace(/^v/, ""));
|
||||
if (isNaN(version) || version < 20) {
|
||||
command.push("bun");
|
||||
} else {
|
||||
command.push("node");
|
||||
}
|
||||
}
|
||||
}
|
||||
await machine.spawnSafe([...command, remotePath, "install"], { stdio: "inherit" });
|
||||
});
|
||||
await machine.spawnSafe([...command, remotePath, "install"], { stdio: "inherit" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (command === "create-image" || command === "publish-image") {
|
||||
|
||||
@@ -495,11 +495,11 @@ async function spawnBun(execPath, { args, cwd, timeout, env, stdout, stderr }) {
|
||||
stderr,
|
||||
});
|
||||
} finally {
|
||||
try {
|
||||
rmSync(tmpdirPath, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
// try {
|
||||
// rmSync(tmpdirPath, { recursive: true, force: true });
|
||||
// } catch (error) {
|
||||
// console.warn(error);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -872,14 +872,16 @@ export function writeFile(filename, content, options = {}) {
|
||||
*/
|
||||
export function which(command, options = {}) {
|
||||
const commands = Array.isArray(command) ? command : [command];
|
||||
const executables = isWindows ? commands.flatMap(name => [name, `${name}.exe`, `${name}.cmd`]) : commands;
|
||||
|
||||
const path = getEnv("PATH", false) || "";
|
||||
const binPaths = path.split(isWindows ? ";" : ":");
|
||||
|
||||
for (const binPath of binPaths) {
|
||||
for (const command of commands) {
|
||||
const commandPath = join(binPath, command);
|
||||
if (existsSync(commandPath)) {
|
||||
return commandPath;
|
||||
for (const executable of executables) {
|
||||
const executablePath = join(binPath, executable);
|
||||
if (existsSync(executablePath)) {
|
||||
return executablePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1249,6 +1251,14 @@ export function escapeCodeBlock(string) {
|
||||
return string.replace(/`/g, "\\`");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} string
|
||||
* @returns {string}
|
||||
*/
|
||||
export function escapePowershell(string) {
|
||||
return string.replace(/'/g, "''").replace(/`/g, "``");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
@@ -1280,14 +1290,6 @@ export function tmpdir() {
|
||||
return nodeTmpdir();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} string
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapePowershell(string) {
|
||||
return string.replace(/'/g, "''").replace(/`/g, "``");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
* @param {string} [output]
|
||||
@@ -1697,7 +1699,7 @@ export function getDistro() {
|
||||
const releasePath = "/etc/os-release";
|
||||
if (existsSync(releasePath)) {
|
||||
const releaseFile = readFile(releasePath, { cache: true });
|
||||
const match = releaseFile.match(/ID=\"(.*)\"/);
|
||||
const match = releaseFile.match(/^ID=\"?(.*)\"?/m);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
@@ -1742,7 +1744,7 @@ export function getDistroVersion() {
|
||||
const releasePath = "/etc/os-release";
|
||||
if (existsSync(releasePath)) {
|
||||
const releaseFile = readFile(releasePath, { cache: true });
|
||||
const match = releaseFile.match(/VERSION_ID=\"(.*)\"/);
|
||||
const match = releaseFile.match(/^VERSION_ID=\"?(.*)\"?/m);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
@@ -2133,6 +2135,11 @@ export function printEnvironment() {
|
||||
console.log("Working Directory:", process.cwd());
|
||||
console.log("Temporary Directory:", tmpdir());
|
||||
});
|
||||
if (isPosix) {
|
||||
startGroup("ulimit -a", () => {
|
||||
spawnSync(["ulimit", "-a"], { stdio: ["ignore", "inherit", "inherit"] });
|
||||
});
|
||||
}
|
||||
|
||||
if (isCI) {
|
||||
startGroup("Environment", () => {
|
||||
|
||||
@@ -498,7 +498,7 @@ pub fn BSSMap(comptime ValueType: type, comptime count: anytype, comptime store_
|
||||
}
|
||||
|
||||
pub fn getOrPut(self: *Self, denormalized_key: []const u8) !Result {
|
||||
const key = if (comptime remove_trailing_slashes) std.mem.trimRight(u8, denormalized_key, "/") else denormalized_key;
|
||||
const key = if (comptime remove_trailing_slashes) std.mem.trimRight(u8, denormalized_key, std.fs.path.sep_str) else denormalized_key;
|
||||
const _key = bun.hash(key);
|
||||
|
||||
self.mutex.lock();
|
||||
@@ -526,7 +526,7 @@ pub fn BSSMap(comptime ValueType: type, comptime count: anytype, comptime store_
|
||||
}
|
||||
|
||||
pub fn get(self: *Self, denormalized_key: []const u8) ?*ValueType {
|
||||
const key = if (comptime remove_trailing_slashes) std.mem.trimRight(u8, denormalized_key, "/") else denormalized_key;
|
||||
const key = if (comptime remove_trailing_slashes) std.mem.trimRight(u8, denormalized_key, std.fs.path.sep_str) else denormalized_key;
|
||||
const _key = bun.hash(key);
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
@@ -588,7 +588,7 @@ pub fn BSSMap(comptime ValueType: type, comptime count: anytype, comptime store_
|
||||
defer self.mutex.unlock();
|
||||
|
||||
const key = if (comptime remove_trailing_slashes)
|
||||
std.mem.trimRight(u8, denormalized_key, "/")
|
||||
std.mem.trimRight(u8, denormalized_key, std.fs.path.sep_str)
|
||||
else
|
||||
denormalized_key;
|
||||
|
||||
|
||||
@@ -99,8 +99,8 @@ pub const Features = struct {
|
||||
pub var https_server: usize = 0;
|
||||
/// Set right before JSC::initialize is called
|
||||
pub var jsc: usize = 0;
|
||||
/// Set when kit.DevServer is initialized
|
||||
pub var kit_dev: usize = 0;
|
||||
/// Set when bake.DevServer is initialized
|
||||
pub var dev_server: usize = 0;
|
||||
pub var lifecycle_scripts: usize = 0;
|
||||
pub var loaders: usize = 0;
|
||||
pub var lockfile_migration_from_package_lock: usize = 0;
|
||||
|
||||
@@ -11,6 +11,7 @@ extern "C" JSC::JSPromise* BakeRenderRoutesForProdStatic(
|
||||
BunString outBase,
|
||||
JSC::JSValue allServerFiles,
|
||||
JSC::JSValue renderStatic,
|
||||
JSC::JSValue getParams,
|
||||
JSC::JSValue clientEntryUrl,
|
||||
JSC::JSValue pattern,
|
||||
JSC::JSValue files,
|
||||
@@ -27,6 +28,7 @@ extern "C" JSC::JSPromise* BakeRenderRoutesForProdStatic(
|
||||
args.append(JSC::jsString(vm, outBase.toWTFString()));
|
||||
args.append(allServerFiles);
|
||||
args.append(renderStatic);
|
||||
args.append(getParams);
|
||||
args.append(clientEntryUrl);
|
||||
args.append(pattern);
|
||||
args.append(files);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,8 @@ const FrameworkRouter = @This();
|
||||
/// where it is an entrypoint index.
|
||||
pub const OpaqueFileId = bun.GenericIndex(u32, opaque {});
|
||||
|
||||
/// Absolute path to root directory of the router.
|
||||
root: []const u8,
|
||||
types: []Type,
|
||||
routes: std.ArrayListUnmanaged(Route),
|
||||
/// Keys are full URL, with leading /, no trailing /
|
||||
@@ -83,6 +85,7 @@ pub const Type = struct {
|
||||
ignore_dirs: []const []const u8 = &.{ ".git", "node_modules" },
|
||||
extensions: []const []const u8,
|
||||
style: Style,
|
||||
allow_layouts: bool,
|
||||
/// `FrameworkRouter` itself does not use this value.
|
||||
client_file: OpaqueFileId.Optional,
|
||||
/// `FrameworkRouter` itself does not use this value.
|
||||
@@ -97,11 +100,16 @@ pub const Type = struct {
|
||||
pub const Index = bun.GenericIndex(u8, Type);
|
||||
};
|
||||
|
||||
pub fn initEmpty(types: []Type, allocator: Allocator) !FrameworkRouter {
|
||||
pub fn initEmpty(root: []const u8, types: []Type, allocator: Allocator) !FrameworkRouter {
|
||||
bun.assert(std.fs.path.isAbsolute(root));
|
||||
|
||||
var routes = try std.ArrayListUnmanaged(Route).initCapacity(allocator, types.len);
|
||||
errdefer routes.deinit(allocator);
|
||||
|
||||
for (0..types.len) |type_index| {
|
||||
for (types, 0..) |*ty, type_index| {
|
||||
ty.abs_root = bun.strings.withoutTrailingSlashWindowsPath(ty.abs_root);
|
||||
bun.assert(bun.strings.hasPrefix(ty.abs_root, root));
|
||||
|
||||
routes.appendAssumeCapacity(.{
|
||||
.part = .{ .text = "" },
|
||||
.type = Type.Index.init(@intCast(type_index)),
|
||||
@@ -115,6 +123,7 @@ pub fn initEmpty(types: []Type, allocator: Allocator) !FrameworkRouter {
|
||||
});
|
||||
}
|
||||
return .{
|
||||
.root = bun.strings.withoutTrailingSlashWindowsPath(root),
|
||||
.types = types,
|
||||
.routes = routes,
|
||||
.dynamic_routes = .{},
|
||||
@@ -389,36 +398,68 @@ pub const ParsedPattern = struct {
|
||||
};
|
||||
};
|
||||
|
||||
pub const Style = enum {
|
||||
@"nextjs-pages-ui",
|
||||
@"nextjs-pages-routes",
|
||||
@"nextjs-app-ui",
|
||||
@"nextjs-app-routes",
|
||||
javascript_defined,
|
||||
pub const Style = union(enum) {
|
||||
nextjs_pages,
|
||||
nextjs_app_ui,
|
||||
nextjs_app_routes,
|
||||
javascript_defined: JSC.Strong,
|
||||
|
||||
pub const map = bun.ComptimeStringMap(Style, .{
|
||||
.{ "nextjs-pages", .nextjs_pages },
|
||||
.{ "nextjs-app-ui", .nextjs_app_ui },
|
||||
.{ "nextjs-app-routes", .nextjs_app_routes },
|
||||
});
|
||||
pub const error_message = "'style' must be either \"nextjs-pages\", \"nextjs-app-ui\", \"nextjs-app-routes\", or a function.";
|
||||
|
||||
pub fn fromJS(value: JSValue, global: *JSC.JSGlobalObject) !Style {
|
||||
if (value.isString()) {
|
||||
const bun_string = try value.toBunString2(global);
|
||||
var sfa = std.heap.stackFallback(4096, bun.default_allocator);
|
||||
const utf8 = bun_string.toUTF8(sfa.get());
|
||||
defer utf8.deinit();
|
||||
if (map.get(utf8.slice())) |style| {
|
||||
return style;
|
||||
}
|
||||
} else if (value.isCallable(global.vm())) {
|
||||
return .{ .javascript_defined = JSC.Strong.create(value, global) };
|
||||
}
|
||||
|
||||
return global.throwInvalidArguments(error_message, .{});
|
||||
}
|
||||
|
||||
pub fn deinit(style: *Style) void {
|
||||
switch (style.*) {
|
||||
.javascript_defined => |*strong| strong.deinit(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub const UiOrRoutes = enum { ui, routes };
|
||||
const NextRoutingConvention = enum { app, pages };
|
||||
|
||||
pub fn parse(style: Style, file_path: []const u8, ext: []const u8, log: *TinyLog, arena: Allocator) !?ParsedPattern {
|
||||
pub fn parse(style: Style, file_path: []const u8, ext: []const u8, log: *TinyLog, allow_layouts: bool, arena: Allocator) !?ParsedPattern {
|
||||
bun.assert(file_path[0] == '/');
|
||||
|
||||
return switch (style) {
|
||||
.@"nextjs-pages-ui" => parseNextJsPages(file_path, ext, log, arena, .ui),
|
||||
.@"nextjs-pages-routes" => parseNextJsPages(file_path, ext, log, arena, .routes),
|
||||
.@"nextjs-app-ui" => parseNextJsApp(file_path, ext, log, arena, .ui),
|
||||
.@"nextjs-app-routes" => parseNextJsApp(file_path, ext, log, arena, .routes),
|
||||
.nextjs_pages => parseNextJsPages(file_path, ext, log, allow_layouts, arena),
|
||||
.nextjs_app_ui => parseNextJsApp(file_path, ext, log, allow_layouts, arena, .ui),
|
||||
.nextjs_app_routes => parseNextJsApp(file_path, ext, log, allow_layouts, arena, .routes),
|
||||
|
||||
// The strategy for this should be to collect a list of candidates,
|
||||
// then batch-call the javascript handler and collect all results.
|
||||
// This will avoid most of the back-and-forth native<->js overhead.
|
||||
.javascript_defined => @panic("TODO: customizable Style"),
|
||||
};
|
||||
}
|
||||
|
||||
/// Implements the pages router parser from Next.js:
|
||||
/// https://nextjs.org/docs/getting-started/project-structure#pages-routing-conventions
|
||||
pub fn parseNextJsPages(file_path_raw: []const u8, ext: []const u8, log: *TinyLog, arena: Allocator, extract: UiOrRoutes) !?ParsedPattern {
|
||||
pub fn parseNextJsPages(file_path_raw: []const u8, ext: []const u8, log: *TinyLog, allow_layouts: bool, arena: Allocator) !?ParsedPattern {
|
||||
var file_path = file_path_raw[0 .. file_path_raw.len - ext.len];
|
||||
var kind: ParsedPattern.Kind = .page;
|
||||
if (strings.hasSuffixComptime(file_path, "/index")) {
|
||||
file_path.len -= "/index".len;
|
||||
} else if (extract == .ui and strings.hasSuffixComptime(file_path, "/_layout")) {
|
||||
} else if (allow_layouts and strings.hasSuffixComptime(file_path, "/_layout")) {
|
||||
file_path.len -= "/_layout".len;
|
||||
kind = .layout;
|
||||
}
|
||||
@@ -439,6 +480,7 @@ pub const Style = enum {
|
||||
file_path_raw: []const u8,
|
||||
ext: []const u8,
|
||||
log: *TinyLog,
|
||||
allow_layouts: bool,
|
||||
arena: Allocator,
|
||||
comptime extract: UiOrRoutes,
|
||||
) !?ParsedPattern {
|
||||
@@ -468,6 +510,8 @@ pub const Style = enum {
|
||||
}).get(basename) orelse
|
||||
return null;
|
||||
|
||||
if (kind == .layout and !allow_layouts) return null;
|
||||
|
||||
const dirname = bun.path.dirname(without_ext, .posix);
|
||||
if (dirname.len <= 1) return .{
|
||||
.kind = kind,
|
||||
@@ -769,6 +813,7 @@ fn newEdge(fr: *FrameworkRouter, alloc: Allocator, edge_data: Route.Edge) !Route
|
||||
const PatternParseError = error{InvalidRoutePattern};
|
||||
|
||||
/// Non-allocating single message log, specialized for the messages from the route pattern parsers.
|
||||
/// DevServer uses this to special-case the printing of these messages to highlight the offending part of the filename
|
||||
pub const TinyLog = struct {
|
||||
msg: std.BoundedArray(u8, 512 + std.fs.max_path_bytes),
|
||||
cursor_at: u32,
|
||||
@@ -777,14 +822,47 @@ pub const TinyLog = struct {
|
||||
pub const empty: TinyLog = .{ .cursor_at = std.math.maxInt(u32), .cursor_len = 0, .msg = .{} };
|
||||
|
||||
pub fn fail(log: *TinyLog, comptime fmt: []const u8, args: anytype, cursor_at: usize, cursor_len: usize) PatternParseError {
|
||||
log.write(fmt, args);
|
||||
log.cursor_at = @intCast(cursor_at);
|
||||
log.cursor_len = @intCast(cursor_len);
|
||||
return PatternParseError.InvalidRoutePattern;
|
||||
}
|
||||
|
||||
pub fn write(log: *TinyLog, comptime fmt: []const u8, args: anytype) void {
|
||||
log.msg.len = @intCast(if (std.fmt.bufPrint(&log.msg.buffer, fmt, args)) |slice| slice.len else |_| brk: {
|
||||
// truncation should never happen because the buffer is HUGE. handle it anyways
|
||||
@memcpy(log.msg.buffer[log.msg.buffer.len - 3 ..], "...");
|
||||
break :brk log.msg.buffer.len;
|
||||
});
|
||||
log.cursor_at = @intCast(cursor_at);
|
||||
log.cursor_len = @intCast(cursor_len);
|
||||
return PatternParseError.InvalidRoutePattern;
|
||||
}
|
||||
|
||||
pub fn print(log: *const TinyLog, rel_path: []const u8) void {
|
||||
const after = rel_path[@max(0, log.cursor_at)..];
|
||||
bun.Output.errGeneric("\"{s}<blue>{s}<r>{s}\" is not a valid route", .{
|
||||
rel_path[0..@max(0, log.cursor_at)],
|
||||
after[0..@min(log.cursor_len, after.len)],
|
||||
after[@min(log.cursor_len, after.len)..],
|
||||
});
|
||||
const w = bun.Output.errorWriterBuffered();
|
||||
w.writeByteNTimes(' ', "error: \"".len + log.cursor_at) catch return;
|
||||
if (bun.Output.enable_ansi_colors_stderr) {
|
||||
const symbols = bun.fmt.TableSymbols.unicode;
|
||||
bun.Output.prettyError("<blue>" ++ symbols.topColumnSep(), .{});
|
||||
if (log.cursor_len > 1) {
|
||||
w.writeBytesNTimes(symbols.horizontalEdge(), log.cursor_len - 1) catch return;
|
||||
}
|
||||
} else {
|
||||
if (log.cursor_len <= 1) {
|
||||
w.writeAll("|") catch return;
|
||||
} else {
|
||||
w.writeByteNTimes('-', log.cursor_len - 1) catch return;
|
||||
}
|
||||
}
|
||||
w.writeByte('\n') catch return;
|
||||
w.writeByteNTimes(' ', "error: \"".len + log.cursor_at) catch return;
|
||||
w.writeAll(log.msg.slice()) catch return;
|
||||
bun.Output.prettyError("<r>\n", .{});
|
||||
bun.Output.flush();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -794,6 +872,8 @@ pub const InsertionContext = struct {
|
||||
vtable: *const VTable,
|
||||
const VTable = struct {
|
||||
getFileIdForRouter: *const fn (*anyopaque, abs_path: []const u8, associated_route: Route.Index, kind: Route.FileKind) bun.OOM!OpaqueFileId,
|
||||
onRouterSyntaxError: *const fn (*anyopaque, rel_path: []const u8, fail: TinyLog) bun.OOM!void,
|
||||
onRouterCollisionError: *const fn (*anyopaque, rel_path: []const u8, other_id: OpaqueFileId, file_kind: Route.FileKind) bun.OOM!void,
|
||||
};
|
||||
pub fn wrap(comptime T: type, ctx: *T) InsertionContext {
|
||||
const wrapper = struct {
|
||||
@@ -801,11 +881,23 @@ pub const InsertionContext = struct {
|
||||
const cast_ctx: *T = @alignCast(@ptrCast(opaque_ctx));
|
||||
return try cast_ctx.getFileIdForRouter(abs_path, associated_route, kind);
|
||||
}
|
||||
fn onRouterSyntaxError(opaque_ctx: *anyopaque, rel_path: []const u8, log: TinyLog) bun.OOM!void {
|
||||
const cast_ctx: *T = @alignCast(@ptrCast(opaque_ctx));
|
||||
if (!@hasDecl(T, "onRouterSyntaxError")) @panic("TODO: onRouterSyntaxError for " ++ @typeName(T));
|
||||
return try cast_ctx.onRouterSyntaxError(rel_path, log);
|
||||
}
|
||||
fn onRouterCollisionError(opaque_ctx: *anyopaque, rel_path: []const u8, other_id: OpaqueFileId, file_kind: Route.FileKind) bun.OOM!void {
|
||||
const cast_ctx: *T = @alignCast(@ptrCast(opaque_ctx));
|
||||
if (!@hasDecl(T, "onRouterCollisionError")) @panic("TODO: onRouterCollisionError for " ++ @typeName(T));
|
||||
return try cast_ctx.onRouterCollisionError(rel_path, other_id, file_kind);
|
||||
}
|
||||
};
|
||||
return .{
|
||||
.opaque_ctx = ctx,
|
||||
.vtable = comptime &.{
|
||||
.getFileIdForRouter = &wrapper.getFileIdForRouter,
|
||||
.onRouterSyntaxError = &wrapper.onRouterSyntaxError,
|
||||
.onRouterCollisionError = &wrapper.onRouterCollisionError,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -817,12 +909,12 @@ pub fn scan(
|
||||
ty: Type.Index,
|
||||
r: *Resolver,
|
||||
ctx: InsertionContext,
|
||||
) !void {
|
||||
) bun.OOM!void {
|
||||
const t = &fw.types[ty.get()];
|
||||
bun.assert(!strings.hasSuffixComptime(t.abs_root, "/"));
|
||||
bun.assert(std.fs.path.isAbsolute(t.abs_root));
|
||||
const root_info = try r.readDirInfo(t.abs_root) orelse
|
||||
return error.RootDirMissing;
|
||||
const root_info = r.readDirInfoIgnoreError(t.abs_root) orelse
|
||||
return;
|
||||
var arena_state = std.heap.ArenaAllocator.init(alloc);
|
||||
defer arena_state.deinit();
|
||||
try fw.scanInner(alloc, t, ty, r, root_info, &arena_state, ctx);
|
||||
@@ -837,7 +929,7 @@ fn scanInner(
|
||||
dir_info: *const DirInfo,
|
||||
arena_state: *std.heap.ArenaAllocator,
|
||||
ctx: InsertionContext,
|
||||
) !void {
|
||||
) bun.OOM!void {
|
||||
const fs = r.fs;
|
||||
const fs_impl = &fs.fs;
|
||||
|
||||
@@ -871,19 +963,29 @@ fn scanInner(
|
||||
}
|
||||
|
||||
var rel_path_buf: bun.PathBuffer = undefined;
|
||||
var rel_path = bun.path.relativeNormalizedBuf(
|
||||
var full_rel_path = bun.path.relativeNormalizedBuf(
|
||||
rel_path_buf[1..],
|
||||
t.abs_root,
|
||||
fr.root,
|
||||
fs.abs(&.{ file.dir, file.base() }),
|
||||
.posix,
|
||||
.auto,
|
||||
true,
|
||||
);
|
||||
rel_path_buf[0] = '/';
|
||||
rel_path = rel_path_buf[0 .. rel_path.len + 1];
|
||||
bun.path.platformToPosixInPlace(u8, rel_path_buf[0..full_rel_path.len]);
|
||||
const rel_path = if (t.abs_root.len == fr.root.len)
|
||||
rel_path_buf[0 .. full_rel_path.len + 1]
|
||||
else
|
||||
full_rel_path[t.abs_root.len - fr.root.len - 1 ..];
|
||||
var log = TinyLog.empty;
|
||||
defer _ = arena_state.reset(.retain_capacity);
|
||||
const parsed = (t.style.parse(rel_path, ext, &log, arena_state.allocator()) catch
|
||||
@panic("TODO: propagate error message")) orelse continue :outer;
|
||||
const parsed = (t.style.parse(rel_path, ext, &log, t.allow_layouts, arena_state.allocator()) catch {
|
||||
log.cursor_at += @intCast(t.abs_root.len - fr.root.len);
|
||||
try ctx.vtable.onRouterSyntaxError(ctx.opaque_ctx, full_rel_path, log);
|
||||
continue :outer;
|
||||
}) orelse continue :outer;
|
||||
|
||||
if (parsed.kind == .page and t.ignore_underscores and bun.strings.hasPrefixComptime(base, "_"))
|
||||
continue :outer;
|
||||
|
||||
var static_total_len: usize = 0;
|
||||
var param_count: usize = 0;
|
||||
@@ -901,11 +1003,18 @@ fn scanInner(
|
||||
}
|
||||
|
||||
if (param_count > 64) {
|
||||
@panic("TODO: propagate error for more than 64 params");
|
||||
log.write("Pattern cannot have more than 64 param", .{});
|
||||
try ctx.vtable.onRouterSyntaxError(ctx.opaque_ctx, full_rel_path, log);
|
||||
continue :outer;
|
||||
}
|
||||
|
||||
if (parsed.kind == .page and t.ignore_underscores and bun.strings.hasPrefixComptime(base, "_"))
|
||||
continue :outer;
|
||||
var out_colliding_file_id: OpaqueFileId = undefined;
|
||||
|
||||
const file_kind: Route.FileKind = switch (parsed.kind) {
|
||||
.page => .page,
|
||||
.layout => .layout,
|
||||
.extra => @panic("TODO: associate extra files with route"),
|
||||
};
|
||||
|
||||
const result = switch (param_count > 0) {
|
||||
inline else => |has_dynamic_comptime| result: {
|
||||
@@ -926,18 +1035,13 @@ fn scanInner(
|
||||
bun.assert(s.getWritten().len == allocation.len);
|
||||
break :static_route StaticPattern{ .route_path = allocation };
|
||||
};
|
||||
var out_colliding_file_id: OpaqueFileId = undefined;
|
||||
|
||||
break :result fr.insert(
|
||||
alloc,
|
||||
t_index,
|
||||
if (has_dynamic_comptime) .dynamic else .static,
|
||||
pattern,
|
||||
switch (parsed.kind) {
|
||||
.page => .page,
|
||||
.layout => .layout,
|
||||
.extra => @panic("TODO: extra files"),
|
||||
},
|
||||
file_kind,
|
||||
fs.abs(&.{ file.dir, file.base() }),
|
||||
ctx,
|
||||
&out_colliding_file_id,
|
||||
@@ -945,12 +1049,20 @@ fn scanInner(
|
||||
},
|
||||
};
|
||||
|
||||
result catch @panic("TODO: propagate error message");
|
||||
result catch |err| switch (err) {
|
||||
error.OutOfMemory => |e| return e,
|
||||
error.RouteCollision => {
|
||||
try ctx.vtable.onRouterCollisionError(
|
||||
ctx.opaque_ctx,
|
||||
full_rel_path,
|
||||
out_colliding_file_id,
|
||||
file_kind,
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -963,6 +1075,11 @@ pub const JSFrameworkRouter = struct {
|
||||
|
||||
files: std.ArrayListUnmanaged(bun.String),
|
||||
router: FrameworkRouter,
|
||||
stored_parse_errors: std.ArrayListUnmanaged(struct {
|
||||
// Owned by bun.default_allocator
|
||||
rel_path: []const u8,
|
||||
log: TinyLog,
|
||||
}),
|
||||
|
||||
const validators = bun.JSC.Node.validators;
|
||||
|
||||
@@ -976,19 +1093,14 @@ pub const JSFrameworkRouter = struct {
|
||||
pub fn constructor(global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) !*JSFrameworkRouter {
|
||||
const opts = callframe.argumentsAsArray(1)[0];
|
||||
if (!opts.isObject())
|
||||
return global.throwInvalidArguments2("FrameworkRouter needs an object as it's first argument", .{});
|
||||
return global.throwInvalidArguments("FrameworkRouter needs an object as it's first argument", .{});
|
||||
|
||||
const root = try opts.getOptional(global, "root", bun.String.Slice) orelse
|
||||
return global.throwInvalidArguments2("Missing options.root", .{});
|
||||
return global.throwInvalidArguments("Missing options.root", .{});
|
||||
defer root.deinit();
|
||||
|
||||
const style = try validators.validateStringEnum(
|
||||
Style,
|
||||
global,
|
||||
try opts.getOptional(global, "style", JSValue) orelse .undefined,
|
||||
"style",
|
||||
.{},
|
||||
);
|
||||
var style = try Style.fromJS(try opts.getOptional(global, "style", JSValue) orelse .undefined, global);
|
||||
errdefer style.deinit();
|
||||
|
||||
const abs_root = try bun.default_allocator.dupe(u8, bun.strings.withoutTrailingSlash(
|
||||
bun.path.joinAbs(bun.fs.FileSystem.instance.top_level_dir, .auto, root.slice()),
|
||||
@@ -1000,6 +1112,7 @@ pub const JSFrameworkRouter = struct {
|
||||
.ignore_underscores = false,
|
||||
.extensions = &.{ ".tsx", ".ts", ".jsx", ".js" },
|
||||
.style = style,
|
||||
.allow_layouts = true,
|
||||
// Unused by JSFrameworkRouter
|
||||
.client_file = undefined,
|
||||
.server_file = undefined,
|
||||
@@ -1008,19 +1121,34 @@ pub const JSFrameworkRouter = struct {
|
||||
errdefer bun.default_allocator.free(types);
|
||||
|
||||
const jsfr = bun.new(JSFrameworkRouter, .{
|
||||
.router = try FrameworkRouter.initEmpty(types, bun.default_allocator),
|
||||
.router = try FrameworkRouter.initEmpty(abs_root, types, bun.default_allocator),
|
||||
.files = .{},
|
||||
.stored_parse_errors = .{},
|
||||
});
|
||||
|
||||
jsfr.router.scan(
|
||||
try jsfr.router.scan(
|
||||
bun.default_allocator,
|
||||
Type.Index.init(0),
|
||||
&global.bunVM().bundler.resolver,
|
||||
InsertionContext.wrap(JSFrameworkRouter, jsfr),
|
||||
) catch |err| {
|
||||
global.throwError(err, "while scanning route list");
|
||||
return error.JSError;
|
||||
};
|
||||
);
|
||||
if (jsfr.stored_parse_errors.items.len > 0) {
|
||||
const arr = JSValue.createEmptyArray(global, jsfr.stored_parse_errors.items.len);
|
||||
for (jsfr.stored_parse_errors.items, 0..) |*item, i| {
|
||||
arr.putIndex(
|
||||
global,
|
||||
@intCast(i),
|
||||
global.createErrorInstance("Invalid route {}: {s}", .{
|
||||
bun.fmt.quote(item.rel_path),
|
||||
item.log.msg.slice(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return global.throwValue2(global.createAggregateErrorWithArray(
|
||||
bun.String.static("Errors scanning routes"),
|
||||
arr,
|
||||
));
|
||||
}
|
||||
|
||||
return jsfr;
|
||||
}
|
||||
@@ -1105,6 +1233,8 @@ pub const JSFrameworkRouter = struct {
|
||||
this.files.deinit(bun.default_allocator);
|
||||
this.router.deinit(bun.default_allocator);
|
||||
bun.default_allocator.free(this.router.types);
|
||||
for (this.stored_parse_errors.items) |i| bun.default_allocator.free(i.rel_path);
|
||||
this.stored_parse_errors.deinit(bun.default_allocator);
|
||||
bun.destroy(this);
|
||||
}
|
||||
|
||||
@@ -1114,19 +1244,16 @@ pub const JSFrameworkRouter = struct {
|
||||
const alloc = arena.allocator();
|
||||
|
||||
if (frame.argumentsCount() < 2)
|
||||
return global.throwInvalidArguments2("parseRoutePattern takes two arguments", .{});
|
||||
return global.throwInvalidArguments("parseRoutePattern takes two arguments", .{});
|
||||
|
||||
const style_js, const filepath_js = frame.argumentsAsArray(2);
|
||||
const filepath = try filepath_js.toSlice2(global, alloc);
|
||||
defer filepath.deinit();
|
||||
const style_string = try style_js.toSlice2(global, alloc);
|
||||
defer style_string.deinit();
|
||||
|
||||
const style = std.meta.stringToEnum(Style, style_string.slice()) orelse
|
||||
return global.throwInvalidArguments2("unknown router style {}", .{bun.fmt.quote(style_string.slice())});
|
||||
var style = try Style.fromJS(style_js, global);
|
||||
errdefer style.deinit();
|
||||
|
||||
var log = TinyLog.empty;
|
||||
const parsed = style.parse(filepath.slice(), std.fs.path.extension(filepath.slice()), &log, alloc) catch |err| switch (err) {
|
||||
const parsed = style.parse(filepath.slice(), std.fs.path.extension(filepath.slice()), &log, true, alloc) catch |err| switch (err) {
|
||||
error.InvalidRoutePattern => {
|
||||
global.throw("{s} ({d}:{d})", .{ log.msg.slice(), log.cursor_at, log.cursor_len });
|
||||
return error.JSError;
|
||||
@@ -1167,6 +1294,15 @@ pub const JSFrameworkRouter = struct {
|
||||
return OpaqueFileId.init(@intCast(jsfr.files.items.len - 1));
|
||||
}
|
||||
|
||||
pub fn onRouterSyntaxError(jsfr: *JSFrameworkRouter, rel_path: []const u8, log: TinyLog) !void {
|
||||
const rel_path_dupe = try bun.default_allocator.dupe(u8, rel_path);
|
||||
errdefer bun.default_allocator.free(rel_path_dupe);
|
||||
try jsfr.stored_parse_errors.append(bun.default_allocator, .{
|
||||
.rel_path = rel_path_dupe,
|
||||
.log = log,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn fileIdToJS(jsfr: *JSFrameworkRouter, global: *JSGlobalObject, id: OpaqueFileId.Optional) JSValue {
|
||||
return jsfr.files.items[(id.unwrap() orelse return .null).get()].toJS(global);
|
||||
}
|
||||
|
||||
164
src/bake/bake.d.ts
vendored
164
src/bake/bake.d.ts
vendored
@@ -9,9 +9,6 @@ declare module "bun" {
|
||||
|
||||
declare namespace Bake {
|
||||
interface Options {
|
||||
/** Will be replaced by fileSystemRouters */
|
||||
routes: {}[];
|
||||
|
||||
/**
|
||||
* Bun provides built-in support for using React as a framework by passing
|
||||
* 'react' as the framework name. Otherwise, frameworks are config objects.
|
||||
@@ -24,29 +21,33 @@ declare module "bun" {
|
||||
framework: Framework | "react";
|
||||
// Note: To contribute to 'bun-framework-react', it can be run from this file:
|
||||
// https://github.com/oven-sh/bun/blob/main/src/bake/bun-framework-react/index.ts
|
||||
|
||||
/**
|
||||
* A subset of the options from Bun.build can be configured. Keep in mind,
|
||||
* your framework may set different defaults.
|
||||
* A subset of the options from Bun.build can be configured. While the framework
|
||||
* can also set these options, this property overrides and merges with them.
|
||||
*
|
||||
* @default {}
|
||||
*/
|
||||
bundlerOptions?: BundlerOptions | undefined;
|
||||
/**
|
||||
* These plugins are applied after `framework.plugins`
|
||||
*/
|
||||
plugins?: BunPlugin[] | undefined;
|
||||
}
|
||||
|
||||
/** Bake only allows a subset of options from `Bun.build` */
|
||||
type BuildConfigSubset = Pick<
|
||||
BuildConfig,
|
||||
"conditions" | "plugins" | "define" | "loader" | "ignoreDCEAnnotations" | "drop"
|
||||
"conditions" | "define" | "loader" | "ignoreDCEAnnotations" | "drop"
|
||||
// - format is not allowed because it is set to an internal "hmr" format
|
||||
// - entrypoints/outfile/outdir doesnt make sense to set
|
||||
// - disabling sourcemap is not allowed because it makes code impossible to debug
|
||||
// - enabling minifyIdentifiers in dev is not allowed because some generated code does not support it
|
||||
// - publicPath is set elsewhere (TODO:)
|
||||
// - publicPath is set by the user (TODO: add options.publicPath)
|
||||
// - emitDCEAnnotations is not useful
|
||||
// - banner and footer do not make sense in these multi-file builds
|
||||
// - experimentalCss cannot be disabled
|
||||
// - disabling external would make it exclude imported files.
|
||||
// - plugins is specified in the framework object, and currently merge between client and server.
|
||||
|
||||
// TODO: jsx customization
|
||||
// TODO: chunk naming
|
||||
@@ -114,6 +115,8 @@ declare module "bun" {
|
||||
* @default false
|
||||
*/
|
||||
reactFastRefresh?: boolean | ReactFastRefreshOptions | undefined;
|
||||
/** Framework bundler plugins load before the user-provided ones. */
|
||||
plugins?: BunPlugin[];
|
||||
|
||||
// /**
|
||||
// * Called after the list of routes is updated. This can be used to
|
||||
@@ -123,6 +126,7 @@ declare module "bun" {
|
||||
// onRouteListUpdate?: (routes: OnRouteListUpdateItem) => void;
|
||||
}
|
||||
|
||||
/** Using `code` here will cause import resolution to happen from the root. */
|
||||
type BuiltInModule = { import: string; code: string } | { import: string; path: string };
|
||||
|
||||
/**
|
||||
@@ -167,7 +171,7 @@ declare module "bun" {
|
||||
* where every export calls this export from `serverRuntimeImportSource`.
|
||||
* This is used to implement client components on the server.
|
||||
*
|
||||
* The call is given three arguments:
|
||||
* When separateSSRGraph is enabled, the call looks like:
|
||||
*
|
||||
* export const ClientComp = registerClientReference(
|
||||
* // A function which may be passed through, it throws an error
|
||||
@@ -181,6 +185,24 @@ declare module "bun" {
|
||||
* // name the user has given.
|
||||
* "ClientComp",
|
||||
* );
|
||||
*
|
||||
* When separateSSRGraph is disabled, the call looks like:
|
||||
*
|
||||
* export const ClientComp = registerClientReference(
|
||||
* function () { ... original user implementation here ... },
|
||||
*
|
||||
* // The file path of the client-side file to import in the browser.
|
||||
* "/_bun/d41d8cd0.js",
|
||||
*
|
||||
* // The export within the client-side file to load. This is
|
||||
* // not guaranteed to match the export name the user has given.
|
||||
* "ClientComp",
|
||||
* );
|
||||
*
|
||||
* While subtle, the parameters in `separateSSRGraph` mode are opaque
|
||||
* strings that have to be looked up in the server manifest. While when
|
||||
* there isn't a separate SSR graph, the two parameters are the actual
|
||||
* URLs to load on the client; The manifest is not required for anything.
|
||||
*
|
||||
* Additionally, the bundler will assemble a component manifest to be used
|
||||
* during rendering.
|
||||
@@ -255,6 +277,7 @@ declare module "bun" {
|
||||
* Do not traverse into directories and files that start with an `_`. Do
|
||||
* not index pages that start with an `_`. Does not prevent stuff like
|
||||
* `_layout.tsx` from being recognized.
|
||||
* @default false
|
||||
*/
|
||||
ignoreUnderscores?: boolean;
|
||||
/**
|
||||
@@ -264,8 +287,9 @@ declare module "bun" {
|
||||
/**
|
||||
* Extensions to match on.
|
||||
* '*' - any extension
|
||||
* @default (set of all valid JavaScript/TypeScript extensions)
|
||||
*/
|
||||
extensions: string[] | "*";
|
||||
extensions?: string[] | "*";
|
||||
/**
|
||||
* 'nextjs-app' builds routes out of directories with `page.tsx` and `layout.tsx`
|
||||
* 'nextjs-pages' builds routes out of any `.tsx` file and layouts with `_layout.tsx`.
|
||||
@@ -386,7 +410,7 @@ declare module "bun" {
|
||||
* return { exhaustive: false };
|
||||
* }
|
||||
*/
|
||||
getParams?: (paramsMetadata: ParamsMetadata) => Awaitable<ParamsResult>;
|
||||
getParams?: (paramsMetadata: ParamsMetadata) => Awaitable<GetParamIterator>;
|
||||
/**
|
||||
* When a dynamic build uses static assets, Bun can map content types in the
|
||||
* user's `Accept` header to the different static files.
|
||||
@@ -421,15 +445,15 @@ declare module "bun" {
|
||||
}
|
||||
|
||||
interface ClientEntryPoint {
|
||||
/**
|
||||
* Called when server-side code is changed. This can be used to fetch a
|
||||
* non-html version of the updated page to perform a faster reload. If
|
||||
* this function does not exist or throws, the client will perform a
|
||||
* hard reload.
|
||||
*
|
||||
* Tree-shaken away in production builds.
|
||||
*/
|
||||
onServerSideReload?: () => Promise<void> | void;
|
||||
// No exports
|
||||
}
|
||||
|
||||
interface DevServerHookEntryPoint {
|
||||
default: (dev: DevServerHookAPI) => Awaitable<void>;
|
||||
}
|
||||
|
||||
interface DevServerHookAPI {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -459,7 +483,7 @@ declare module "bun" {
|
||||
/**
|
||||
* A list of js files that the route will need to be interactive.
|
||||
*/
|
||||
readonly scripts: ReadonlyArray<string>;
|
||||
readonly modules: ReadonlyArray<string>;
|
||||
/**
|
||||
* A list of js files that should be preloaded.
|
||||
*
|
||||
@@ -482,12 +506,31 @@ declare module "bun" {
|
||||
}
|
||||
}
|
||||
|
||||
interface GenericServeOptions {
|
||||
declare interface GenericServeOptions {
|
||||
/** Add a fullstack web app to this server using Bun Bake */
|
||||
app?: Bake.Options | undefined;
|
||||
}
|
||||
|
||||
declare interface PluginBuilder {
|
||||
/**
|
||||
* Inject a module into the development server's runtime, to be loaded
|
||||
* before all other user code.
|
||||
*/
|
||||
addPreload(module: string, side: 'client' | 'server'): void;
|
||||
}
|
||||
|
||||
declare interface OnLoadArgs {
|
||||
/**
|
||||
* When using server-components, the same bundle has both client and server
|
||||
* files; A single plugin can operate on files from both module graphs.
|
||||
* Outside of server-components, this will be "client" when the target is
|
||||
* set to "browser" and "server" otherwise.
|
||||
*/
|
||||
side: 'server' | 'client';
|
||||
}
|
||||
}
|
||||
|
||||
/** Available in server-side files only. */
|
||||
declare module "bun:bake/server" {
|
||||
// NOTE: The format of these manifests will likely be customizable in the future.
|
||||
|
||||
@@ -496,60 +539,73 @@ declare module "bun:bake/server" {
|
||||
* is a mapping of component IDs to the client-side file it is exported in.
|
||||
* The specifiers from here are to be imported in the client.
|
||||
*
|
||||
* To perform SSR with client components, see `clientManifest`
|
||||
* To perform SSR with client components, see `ssrManifest`
|
||||
*/
|
||||
declare const serverManifest: ReactServerManifest;
|
||||
declare const serverManifest: ServerManifest;
|
||||
/**
|
||||
* Entries in this manifest map from client-side files to their respective SSR
|
||||
* bundles. They can be loaded by `await import()` or `require()`.
|
||||
*/
|
||||
declare const clientManifest: ReactClientManifest;
|
||||
declare const ssrManifest: SSRManifest;
|
||||
|
||||
/** (insert teaser trailer) */
|
||||
declare const actionManifest: never;
|
||||
|
||||
declare interface ReactServerManifest {
|
||||
declare interface ServerManifest {
|
||||
/**
|
||||
* Concatenation of the component file ID and the instance id with '#'
|
||||
* Example: 'components/Navbar.tsx#default' (dev) or 'l2#a' (prod/minified)
|
||||
*
|
||||
* The component file ID and the instance id are both passed to `registerClientReference`
|
||||
*/
|
||||
[combinedComponentId: string]: {
|
||||
/**
|
||||
* The `id` in ReactClientManifest.
|
||||
* Correlates but is not required to be the filename
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The `name` in ReactServerManifest
|
||||
* Correlates but is not required to be the export name
|
||||
*/
|
||||
name: string;
|
||||
/** Currently not implemented; always an empty array */
|
||||
chunks: [];
|
||||
[combinedComponentId: string]: ServerManifestEntry;
|
||||
}
|
||||
|
||||
declare interface ServerManifestEntry {
|
||||
/**
|
||||
* The `id` in ReactClientManifest.
|
||||
* Correlates but is not required to be the filename
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The `name` in ReactServerManifest
|
||||
* Correlates but is not required to be the export name
|
||||
*/
|
||||
name: string;
|
||||
/** Currently not implemented; always an empty array */
|
||||
chunks: [];
|
||||
}
|
||||
|
||||
declare interface SSRManifest {
|
||||
/** ServerManifest[...].id */
|
||||
[id: string]: {
|
||||
/** ServerManifest[...].name */
|
||||
[name: string]: SSRManifestEntry;
|
||||
};
|
||||
}
|
||||
|
||||
declare interface ReactClientManifest {
|
||||
/** ReactServerManifest[...].id */
|
||||
[id: string]: {
|
||||
/** ReactServerManifest[...].name */
|
||||
[name: string]: {
|
||||
/** Valid specifier to import */
|
||||
specifier: string;
|
||||
/** Export name */
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
declare interface SSRManifestEntry {
|
||||
/** Valid specifier to import */
|
||||
specifier: string;
|
||||
/** Export name */
|
||||
name: string;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Available in client-side files. */
|
||||
declare module "bun:bake/client" {
|
||||
/**
|
||||
* Due to the current implementation of the Dev Server, it must be informed of
|
||||
* client-side routing so it can load client components. This is not necessary
|
||||
* in production, and calling this in that situation will fail to compile.
|
||||
* Callback is invoked when server-side code is changed. This can be used to
|
||||
* fetch a non-html version of the updated page to perform a faster reload. If
|
||||
* not provided, the client will perform a hard reload.
|
||||
*
|
||||
* Only one callback can be set. This function overwrites the previous one.
|
||||
*/
|
||||
declare function bundleRouteForDevelopment(href: string, options?: { signal?: AbortSignal }): Promise<void>;
|
||||
export function onServerSideReload(cb: () => void | Promise<void>): Promise<void>;
|
||||
}
|
||||
|
||||
/** Available during development */
|
||||
declare module "bun:bake/dev" {
|
||||
|
||||
};
|
||||
|
||||
8
src/bake/bake.private.d.ts
vendored
8
src/bake/bake.private.d.ts
vendored
@@ -47,11 +47,11 @@ declare var __bun_f: any;
|
||||
|
||||
// The following interfaces have been transcribed manually.
|
||||
|
||||
declare module "react-server-dom-webpack/client.browser" {
|
||||
declare module "react-server-dom-bun/client.browser" {
|
||||
export function createFromReadableStream<T = any>(readable: ReadableStream<Uint8Array>): Promise<T>;
|
||||
}
|
||||
|
||||
declare module "react-server-dom-webpack/client.node.unbundled.js" {
|
||||
declare module "react-server-dom-bun/client.node.unbundled.js" {
|
||||
import type { ReactClientManifest } from "bun:bake/server";
|
||||
import type { Readable } from "node:stream";
|
||||
export interface Manifest {
|
||||
@@ -70,7 +70,7 @@ declare module "react-server-dom-webpack/client.node.unbundled.js" {
|
||||
export function createFromNodeStream<T = any>(readable: Readable, manifest?: Manifest): Promise<T>;
|
||||
}
|
||||
|
||||
declare module "react-server-dom-webpack/server.node.unbundled.js" {
|
||||
declare module "react-server-dom-bun/server.node.unbundled.js" {
|
||||
import type { ReactServerManifest } from "bun:bake/server";
|
||||
import type { ReactElement, ReactElement } from "react";
|
||||
import type { Writable } from "node:stream";
|
||||
@@ -98,7 +98,7 @@ declare module "react-server-dom-webpack/server.node.unbundled.js" {
|
||||
}
|
||||
|
||||
declare module "react-dom/server.node" {
|
||||
import type { PipeableStream } from "react-server-dom-webpack/server.node.unbundled.js";
|
||||
import type { PipeableStream } from "react-server-dom-bun/server.node.unbundled.js";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
export type RenderToPipeableStreamOptions = any;
|
||||
|
||||
@@ -12,7 +12,7 @@ pub const api_name = "app";
|
||||
|
||||
/// Zig version of the TS definition 'Bake.Options' in 'bake.d.ts'
|
||||
pub const UserOptions = struct {
|
||||
arena: std.heap.ArenaAllocator.State,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
allocations: StringRefList,
|
||||
|
||||
root: []const u8,
|
||||
@@ -20,13 +20,15 @@ pub const UserOptions = struct {
|
||||
bundler_options: SplitBundlerOptions,
|
||||
|
||||
pub fn deinit(options: *UserOptions) void {
|
||||
options.arena.promote(bun.default_allocator).deinit();
|
||||
options.arena.deinit();
|
||||
options.allocations.free();
|
||||
if (options.bundler_options.plugin) |p| p.deinit();
|
||||
}
|
||||
|
||||
/// Currently, this function must run at the top of the event loop.
|
||||
pub fn fromJS(config: JSValue, global: *JSC.JSGlobalObject) !UserOptions {
|
||||
if (!config.isObject()) {
|
||||
return global.throwInvalidArguments2("'" ++ api_name ++ "' is not an object", .{});
|
||||
return global.throwInvalidArguments("'" ++ api_name ++ "' is not an object", .{});
|
||||
}
|
||||
var arena = std.heap.ArenaAllocator.init(bun.default_allocator);
|
||||
errdefer arena.deinit();
|
||||
@@ -34,11 +36,11 @@ pub const UserOptions = struct {
|
||||
|
||||
var allocations = StringRefList.empty;
|
||||
errdefer allocations.free();
|
||||
var bundler_options: SplitBundlerOptions = .{};
|
||||
var bundler_options = SplitBundlerOptions.empty;
|
||||
|
||||
const framework = try Framework.fromJS(
|
||||
try config.get2(global, "framework") orelse {
|
||||
return global.throwInvalidArguments2("'" ++ api_name ++ "' is missing 'framework'", .{});
|
||||
try config.get(global, "framework") orelse {
|
||||
return global.throwInvalidArguments("'" ++ api_name ++ "' is missing 'framework'", .{});
|
||||
},
|
||||
global,
|
||||
&allocations,
|
||||
@@ -51,17 +53,19 @@ pub const UserOptions = struct {
|
||||
else
|
||||
bun.getcwdAlloc(alloc) catch |err| switch (err) {
|
||||
error.OutOfMemory => {
|
||||
global.throwOutOfMemory();
|
||||
return error.JSError;
|
||||
return global.throwOutOfMemory();
|
||||
},
|
||||
else => {
|
||||
global.throwError(err, "while querying current working directory");
|
||||
return error.JSError;
|
||||
return global.throwError(err, "while querying current working directory");
|
||||
},
|
||||
};
|
||||
|
||||
if (try config.get(global, "plugins")) |plugin_array| {
|
||||
try bundler_options.parsePluginArray(plugin_array, global);
|
||||
}
|
||||
|
||||
return .{
|
||||
.arena = arena.state,
|
||||
.arena = arena,
|
||||
.allocations = allocations,
|
||||
.root = root,
|
||||
.framework = framework,
|
||||
@@ -87,11 +91,62 @@ const StringRefList = struct {
|
||||
pub const empty: StringRefList = .{ .strings = .{} };
|
||||
};
|
||||
|
||||
const SplitBundlerOptions = struct {
|
||||
pub const SplitBundlerOptions = struct {
|
||||
plugin: ?*Plugin = null,
|
||||
all: BuildConfigSubset = .{},
|
||||
client: BuildConfigSubset = .{},
|
||||
server: BuildConfigSubset = .{},
|
||||
ssr: BuildConfigSubset = .{},
|
||||
|
||||
pub const empty: SplitBundlerOptions = .{
|
||||
.plugin = null,
|
||||
.all = .{},
|
||||
.client = .{},
|
||||
.server = .{},
|
||||
.ssr = .{},
|
||||
};
|
||||
|
||||
pub fn parsePluginArray(opts: *SplitBundlerOptions, plugin_array: JSValue, global: *JSC.JSGlobalObject) !void {
|
||||
const plugin = opts.plugin orelse Plugin.create(global, .bun);
|
||||
opts.plugin = plugin;
|
||||
const empty_object = JSValue.createEmptyObject(global, 0);
|
||||
|
||||
var iter = plugin_array.arrayIterator(global);
|
||||
while (iter.next()) |plugin_config| {
|
||||
if (!plugin_config.isObject()) {
|
||||
return global.throwInvalidArguments("Expected plugin to be an object", .{});
|
||||
}
|
||||
|
||||
if (try plugin_config.getOptional(global, "name", ZigString.Slice)) |slice| {
|
||||
defer slice.deinit();
|
||||
if (slice.len == 0) {
|
||||
return global.throwInvalidArguments("Expected plugin to have a non-empty name", .{});
|
||||
}
|
||||
} else {
|
||||
return global.throwInvalidArguments("Expected plugin to have a name", .{});
|
||||
}
|
||||
|
||||
const function = try plugin_config.getFunction(global, "setup") orelse {
|
||||
return global.throwInvalidArguments("Expected plugin to have a setup() function", .{});
|
||||
};
|
||||
const plugin_result = try plugin.addPlugin(function, empty_object, .null, false, true);
|
||||
if (plugin_result.asAnyPromise()) |promise| {
|
||||
promise.setHandled(global.vm());
|
||||
// TODO: remove this call, replace with a promise list that must
|
||||
// be resolved before the first bundle task can begin.
|
||||
global.bunVM().waitForPromise(promise);
|
||||
switch (promise.unwrap(global.vm(), .mark_handled)) {
|
||||
.pending => unreachable,
|
||||
.fulfilled => |val| {
|
||||
_ = val;
|
||||
},
|
||||
.rejected => |err| {
|
||||
return global.throwValue2(err);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const BuildConfigSubset = struct {
|
||||
@@ -100,32 +155,14 @@ const BuildConfigSubset = struct {
|
||||
conditions: bun.StringArrayHashMapUnmanaged(void) = .{},
|
||||
drop: bun.StringArrayHashMapUnmanaged(void) = .{},
|
||||
// TODO: plugins
|
||||
|
||||
pub fn loadFromJs(config: *BuildConfigSubset, value: JSValue, arena: Allocator) !void {
|
||||
_ = config; // autofix
|
||||
_ = value; // autofix
|
||||
_ = arena; // autofix
|
||||
}
|
||||
};
|
||||
|
||||
/// Temporary function to invoke dev server via JavaScript. Will be
|
||||
/// replaced with a user-facing API. Refs the event loop forever.
|
||||
pub fn jsWipDevServer(global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue {
|
||||
_ = global;
|
||||
_ = callframe;
|
||||
|
||||
if (!bun.FeatureFlags.bake) return .undefined;
|
||||
|
||||
bun.Output.errGeneric(
|
||||
\\This api has moved to the `app` property of the default export.
|
||||
\\
|
||||
\\ export default {{
|
||||
\\ port: 3000,
|
||||
\\ app: {{
|
||||
\\ framework: 'react'
|
||||
\\ }},
|
||||
\\ }};
|
||||
\\
|
||||
,
|
||||
.{},
|
||||
);
|
||||
return .undefined;
|
||||
}
|
||||
|
||||
/// A "Framework" in our eyes is simply set of bundler options that a framework
|
||||
/// author would set in order to integrate the framework with the application.
|
||||
/// Since many fields have default values which may point to static memory, this
|
||||
@@ -133,6 +170,7 @@ pub fn jsWipDevServer(global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bu
|
||||
///
|
||||
/// Full documentation on these fields is located in the TypeScript definitions.
|
||||
pub const Framework = struct {
|
||||
is_built_in_react: bool,
|
||||
file_system_router_types: []FileSystemRouterType,
|
||||
// static_routers: [][]const u8,
|
||||
server_components: ?ServerComponents = null,
|
||||
@@ -142,9 +180,10 @@ pub const Framework = struct {
|
||||
/// Bun provides built-in support for using React as a framework.
|
||||
/// Depends on externally provided React
|
||||
///
|
||||
/// $ bun i react@experimental react-dom@experimental react-server-dom-webpack@experimental react-refresh@experimental
|
||||
/// $ bun i react@experimental react-dom@experimental react-refresh@experimental react-server-dom-bun
|
||||
pub fn react(arena: std.mem.Allocator) !Framework {
|
||||
return .{
|
||||
.is_built_in_react = true,
|
||||
.server_components = .{
|
||||
.separate_ssr_graph = true,
|
||||
.server_runtime_import = "react-server-dom-bun/server",
|
||||
@@ -159,7 +198,8 @@ pub const Framework = struct {
|
||||
.ignore_underscores = true,
|
||||
.ignore_dirs = &.{ "node_modules", ".git" },
|
||||
.extensions = &.{ ".tsx", ".jsx" },
|
||||
.style = .@"nextjs-pages-ui",
|
||||
.style = .nextjs_pages,
|
||||
.allow_layouts = true,
|
||||
},
|
||||
}),
|
||||
// .static_routers = try arena.dupe([]const u8, &.{"public"}),
|
||||
@@ -180,7 +220,7 @@ pub const Framework = struct {
|
||||
};
|
||||
}
|
||||
|
||||
const FileSystemRouterType = struct {
|
||||
pub const FileSystemRouterType = struct {
|
||||
root: []const u8,
|
||||
prefix: []const u8,
|
||||
entry_server: []const u8,
|
||||
@@ -189,14 +229,15 @@ pub const Framework = struct {
|
||||
ignore_dirs: []const []const u8,
|
||||
extensions: []const []const u8,
|
||||
style: FrameworkRouter.Style,
|
||||
allow_layouts: bool,
|
||||
};
|
||||
|
||||
const BuiltInModule = union(enum) {
|
||||
pub const BuiltInModule = union(enum) {
|
||||
import: []const u8,
|
||||
code: []const u8,
|
||||
};
|
||||
|
||||
const ServerComponents = struct {
|
||||
pub const ServerComponents = struct {
|
||||
separate_ssr_graph: bool = false,
|
||||
server_runtime_import: []const u8,
|
||||
// client_runtime_import: []const u8,
|
||||
@@ -209,11 +250,22 @@ pub const Framework = struct {
|
||||
import_source: []const u8 = "react-refresh/runtime",
|
||||
};
|
||||
|
||||
/// Given a Framework configuration, this returns another one with all modules resolved.
|
||||
pub const react_install_command = "bun i react@experimental react-dom@experimental react-refresh@experimental react-server-dom-bun";
|
||||
|
||||
pub fn addReactInstallCommandNote(log: *bun.logger.Log) !void {
|
||||
try log.addMsg(.{
|
||||
.kind = .note,
|
||||
.data = try bun.logger.rangeData(null, bun.logger.Range.none, "Install the built in react integration with \"" ++ react_install_command ++ "\"")
|
||||
.cloneLineText(log.clone_line_text, log.msgs.allocator),
|
||||
});
|
||||
}
|
||||
|
||||
/// Given a Framework configuration, this returns another one with all paths resolved.
|
||||
/// New memory allocated into provided arena.
|
||||
///
|
||||
/// All resolution errors will happen before returning error.ModuleNotFound
|
||||
/// Details written into `r.log`
|
||||
pub fn resolve(f: Framework, server: *bun.resolver.Resolver, client: *bun.resolver.Resolver) !Framework {
|
||||
/// Errors written into `r.log`
|
||||
pub fn resolve(f: Framework, server: *bun.resolver.Resolver, client: *bun.resolver.Resolver, arena: Allocator) !Framework {
|
||||
var clone = f;
|
||||
var had_errors: bool = false;
|
||||
|
||||
@@ -227,8 +279,7 @@ pub const Framework = struct {
|
||||
}
|
||||
|
||||
for (clone.file_system_router_types) |*fsr| {
|
||||
// TODO: unonwned memory
|
||||
fsr.root = bun.path.joinAbs(server.fs.top_level_dir, .auto, fsr.root);
|
||||
fsr.root = try arena.dupe(u8, bun.path.joinAbs(server.fs.top_level_dir, .auto, fsr.root));
|
||||
if (fsr.entry_client) |*entry_client| f.resolveHelper(client, entry_client, &had_errors, "client side entrypoint");
|
||||
f.resolveHelper(client, &fsr.entry_server, &had_errors, "server side entrypoint");
|
||||
}
|
||||
@@ -262,7 +313,6 @@ pub const Framework = struct {
|
||||
bundler_options: *SplitBundlerOptions,
|
||||
arena: Allocator,
|
||||
) !Framework {
|
||||
_ = bundler_options; // autofix
|
||||
if (opts.isString()) {
|
||||
const str = try opts.toBunString2(global);
|
||||
defer str.deref();
|
||||
@@ -279,29 +329,29 @@ pub const Framework = struct {
|
||||
}
|
||||
|
||||
if (!opts.isObject()) {
|
||||
return global.throwInvalidArguments2("Framework must be an object", .{});
|
||||
return global.throwInvalidArguments("Framework must be an object", .{});
|
||||
}
|
||||
|
||||
if (try opts.get2(global, "serverEntryPoint") != null) {
|
||||
if (try opts.get(global, "serverEntryPoint") != null) {
|
||||
bun.Output.warn("deprecation notice: 'framework.serverEntryPoint' has been replaced with 'fileSystemRouterTypes[n].serverEntryPoint'", .{});
|
||||
}
|
||||
if (try opts.get2(global, "clientEntryPoint") != null) {
|
||||
if (try opts.get(global, "clientEntryPoint") != null) {
|
||||
bun.Output.warn("deprecation notice: 'framework.clientEntryPoint' has been replaced with 'fileSystemRouterTypes[n].clientEntryPoint'", .{});
|
||||
}
|
||||
|
||||
const react_fast_refresh: ?ReactFastRefresh = brk: {
|
||||
const rfr: JSValue = try opts.get2(global, "reactFastRefresh") orelse
|
||||
const rfr: JSValue = try opts.get(global, "reactFastRefresh") orelse
|
||||
break :brk null;
|
||||
|
||||
if (rfr == .true) break :brk .{};
|
||||
if (rfr == .false or rfr == .null or rfr == .undefined) break :brk null;
|
||||
|
||||
if (!rfr.isObject()) {
|
||||
return global.throwInvalidArguments2("'framework.reactFastRefresh' must be an object or 'true'", .{});
|
||||
return global.throwInvalidArguments("'framework.reactFastRefresh' must be an object or 'true'", .{});
|
||||
}
|
||||
|
||||
const prop = rfr.get(global, "importSource") orelse {
|
||||
return global.throwInvalidArguments2("'framework.reactFastRefresh' is missing 'importSource'", .{});
|
||||
const prop = try rfr.get(global, "importSource") orelse {
|
||||
return global.throwInvalidArguments("'framework.reactFastRefresh' is missing 'importSource'", .{});
|
||||
};
|
||||
|
||||
const str = try prop.toBunString2(global);
|
||||
@@ -312,34 +362,37 @@ pub const Framework = struct {
|
||||
};
|
||||
};
|
||||
const server_components: ?ServerComponents = sc: {
|
||||
const sc: JSValue = try opts.get2(global, "serverComponents") orelse
|
||||
const sc: JSValue = try opts.get(global, "serverComponents") orelse
|
||||
break :sc null;
|
||||
if (sc == .false or sc == .null or sc == .undefined) break :sc null;
|
||||
|
||||
if (!sc.isObject()) {
|
||||
return global.throwInvalidArguments2("'framework.serverComponents' must be an object or 'undefined'", .{});
|
||||
return global.throwInvalidArguments("'framework.serverComponents' must be an object or 'undefined'", .{});
|
||||
}
|
||||
|
||||
break :sc .{
|
||||
.separate_ssr_graph = brk: {
|
||||
// Intentionally not using a truthiness check
|
||||
const prop = try sc.getOptional(global, "separateSSRGraph", JSValue) orelse {
|
||||
return global.throwInvalidArguments2("Missing 'framework.serverComponents.separateSSRGraph'", .{});
|
||||
return global.throwInvalidArguments("Missing 'framework.serverComponents.separateSSRGraph'", .{});
|
||||
};
|
||||
if (prop == .true) break :brk true;
|
||||
if (prop == .false) break :brk false;
|
||||
return global.throwInvalidArguments2("'framework.serverComponents.separateSSRGraph' must be a boolean", .{});
|
||||
return global.throwInvalidArguments("'framework.serverComponents.separateSSRGraph' must be a boolean", .{});
|
||||
},
|
||||
.server_runtime_import = refs.track(
|
||||
try sc.getOptional(global, "serverRuntimeImportSource", ZigString.Slice) orelse {
|
||||
return global.throwInvalidArguments2("Missing 'framework.serverComponents.serverRuntimeImportSource'", .{});
|
||||
},
|
||||
),
|
||||
.server_register_client_reference = refs.track(
|
||||
try sc.getOptional(global, "serverRegisterClientReferenceExport", ZigString.Slice) orelse {
|
||||
return global.throwInvalidArguments2("Missing 'framework.serverComponents.serverRegisterClientReferenceExport'", .{});
|
||||
return global.throwInvalidArguments("Missing 'framework.serverComponents.serverRuntimeImportSource'", .{});
|
||||
},
|
||||
),
|
||||
.server_register_client_reference = if (try sc.getOptional(
|
||||
global,
|
||||
"serverRegisterClientReferenceExport",
|
||||
ZigString.Slice,
|
||||
)) |slice|
|
||||
refs.track(slice)
|
||||
else
|
||||
"registerClientReference",
|
||||
};
|
||||
};
|
||||
const built_in_modules: bun.StringArrayHashMapUnmanaged(BuiltInModule) = built_in_modules: {
|
||||
@@ -354,11 +407,11 @@ pub const Framework = struct {
|
||||
var i: usize = 0;
|
||||
while (it.next()) |file| : (i += 1) {
|
||||
if (!file.isObject()) {
|
||||
return global.throwInvalidArguments2("'builtInModules[{d}]' is not an object", .{i});
|
||||
return global.throwInvalidArguments("'builtInModules[{d}]' is not an object", .{i});
|
||||
}
|
||||
|
||||
const path = try getOptionalString(file, global, "import", refs, arena) orelse {
|
||||
return global.throwInvalidArguments2("'builtInModules[{d}]' is missing 'import'", .{i});
|
||||
return global.throwInvalidArguments("'builtInModules[{d}]' is missing 'import'", .{i});
|
||||
};
|
||||
|
||||
const value: BuiltInModule = if (try getOptionalString(file, global, "path", refs, arena)) |str|
|
||||
@@ -366,7 +419,7 @@ pub const Framework = struct {
|
||||
else if (try getOptionalString(file, global, "code", refs, arena)) |str|
|
||||
.{ .code = str }
|
||||
else
|
||||
return global.throwInvalidArguments2("'builtInModules[{d}]' needs either 'path' or 'code'", .{i});
|
||||
return global.throwInvalidArguments("'builtInModules[{d}]' needs either 'path' or 'code'", .{i});
|
||||
|
||||
files.putAssumeCapacity(path, value);
|
||||
}
|
||||
@@ -375,36 +428,35 @@ pub const Framework = struct {
|
||||
};
|
||||
const file_system_router_types: []FileSystemRouterType = brk: {
|
||||
const array: JSValue = try opts.getArray(global, "fileSystemRouterTypes") orelse {
|
||||
return global.throwInvalidArguments2("Missing 'framework.fileSystemRouterTypes'", .{});
|
||||
return global.throwInvalidArguments("Missing 'framework.fileSystemRouterTypes'", .{});
|
||||
};
|
||||
const len = array.getLength(global);
|
||||
if (len > 256) {
|
||||
return global.throwInvalidArguments2("Framework can only define up to 256 file-system router types", .{});
|
||||
return global.throwInvalidArguments("Framework can only define up to 256 file-system router types", .{});
|
||||
}
|
||||
const file_system_router_types = try arena.alloc(FileSystemRouterType, len);
|
||||
|
||||
var it = array.arrayIterator(global);
|
||||
var i: usize = 0;
|
||||
errdefer for (file_system_router_types[0..i]) |*fsr| fsr.style.deinit();
|
||||
while (it.next()) |fsr_opts| : (i += 1) {
|
||||
const root = try getOptionalString(fsr_opts, global, "root", refs, arena) orelse {
|
||||
return global.throwInvalidArguments2("'fileSystemRouterTypes[{d}]' is missing 'root'", .{i});
|
||||
return global.throwInvalidArguments("'fileSystemRouterTypes[{d}]' is missing 'root'", .{i});
|
||||
};
|
||||
const server_entry_point = try getOptionalString(fsr_opts, global, "serverEntryPoint", refs, arena) orelse {
|
||||
return global.throwInvalidArguments2("'fileSystemRouterTypes[{d}]' is missing 'serverEntryPoint'", .{i});
|
||||
return global.throwInvalidArguments("'fileSystemRouterTypes[{d}]' is missing 'serverEntryPoint'", .{i});
|
||||
};
|
||||
const client_entry_point = try getOptionalString(fsr_opts, global, "clientEntryPoint", refs, arena);
|
||||
const prefix = try getOptionalString(fsr_opts, global, "prefix", refs, arena) orelse "/";
|
||||
const ignore_underscores = try fsr_opts.getBooleanStrict(global, "ignoreUnderscores") orelse false;
|
||||
const layouts = try fsr_opts.getBooleanStrict(global, "layouts") orelse false;
|
||||
|
||||
const style = try validators.validateStringEnum(
|
||||
FrameworkRouter.Style,
|
||||
global,
|
||||
try opts.getOptional(global, "style", JSValue) orelse .undefined,
|
||||
"style",
|
||||
.{},
|
||||
);
|
||||
var style = try FrameworkRouter.Style.fromJS(try fsr_opts.get(global, "style") orelse {
|
||||
return global.throwInvalidArguments("'fileSystemRouterTypes[{d}]' is missing 'style'", .{i});
|
||||
}, global);
|
||||
errdefer style.deinit();
|
||||
|
||||
const extensions: []const []const u8 = if (try fsr_opts.get2(global, "extensions")) |exts_js| exts: {
|
||||
const extensions: []const []const u8 = if (try fsr_opts.get(global, "extensions")) |exts_js| exts: {
|
||||
if (exts_js.isString()) {
|
||||
const str = try exts_js.toSlice2(global, arena);
|
||||
defer str.deinit();
|
||||
@@ -412,20 +464,30 @@ pub const Framework = struct {
|
||||
break :exts &.{};
|
||||
}
|
||||
} else if (exts_js.isArray()) {
|
||||
var it_2 = array.arrayIterator(global);
|
||||
var it_2 = exts_js.arrayIterator(global);
|
||||
var i_2: usize = 0;
|
||||
const extensions = try arena.alloc([]const u8, len);
|
||||
const extensions = try arena.alloc([]const u8, array.getLength(global));
|
||||
while (it_2.next()) |array_item| : (i_2 += 1) {
|
||||
// TODO: remove/add the prefix `.`, throw error if specifying '*' as an array item instead of as root
|
||||
extensions[i_2] = refs.track(try array_item.toSlice2(global, arena));
|
||||
const slice = refs.track(try array_item.toSlice2(global, arena));
|
||||
if (bun.strings.eqlComptime(slice, "*"))
|
||||
return global.throwInvalidArguments("'extensions' cannot include \"*\" as an extension. Pass \"*\" instead of the array.", .{});
|
||||
|
||||
if (slice.len == 0) {
|
||||
return global.throwInvalidArguments("'extensions' cannot include \"\" as an extension.", .{});
|
||||
}
|
||||
|
||||
extensions[i_2] = if (slice[0] == '.')
|
||||
slice
|
||||
else
|
||||
try std.mem.concat(arena, u8, &.{ ".", slice });
|
||||
}
|
||||
break :exts extensions;
|
||||
}
|
||||
|
||||
return global.throwInvalidArguments2("'extensions' must be an array of strings or \"*\" for all extensions", .{});
|
||||
return global.throwInvalidArguments("'extensions' must be an array of strings or \"*\" for all extensions", .{});
|
||||
} else &.{ ".jsx", ".tsx", ".js", ".ts", ".cjs", ".cts", ".mjs", ".mts" };
|
||||
|
||||
const ignore_dirs: []const []const u8 = if (try fsr_opts.get2(global, "ignoreDirs")) |exts_js| exts: {
|
||||
const ignore_dirs: []const []const u8 = if (try fsr_opts.get(global, "ignoreDirs")) |exts_js| exts: {
|
||||
if (exts_js.isArray()) {
|
||||
var it_2 = array.arrayIterator(global);
|
||||
var i_2: usize = 0;
|
||||
@@ -436,7 +498,7 @@ pub const Framework = struct {
|
||||
break :exts dirs;
|
||||
}
|
||||
|
||||
return global.throwInvalidArguments2("'ignoreDirs' must be an array of strings or \"*\" for all extensions", .{});
|
||||
return global.throwInvalidArguments("'ignoreDirs' must be an array of strings or \"*\" for all extensions", .{});
|
||||
} else &.{ ".git", "node_modules" };
|
||||
|
||||
file_system_router_types[i] = .{
|
||||
@@ -448,22 +510,29 @@ pub const Framework = struct {
|
||||
.ignore_underscores = ignore_underscores,
|
||||
.extensions = extensions,
|
||||
.ignore_dirs = ignore_dirs,
|
||||
.allow_layouts = layouts,
|
||||
};
|
||||
}
|
||||
|
||||
break :brk file_system_router_types;
|
||||
};
|
||||
errdefer for (file_system_router_types) |*fsr| fsr.style.deinit();
|
||||
|
||||
const framework: Framework = .{
|
||||
.is_built_in_react = false,
|
||||
.file_system_router_types = file_system_router_types,
|
||||
.react_fast_refresh = react_fast_refresh,
|
||||
.server_components = server_components,
|
||||
.built_in_modules = built_in_modules,
|
||||
};
|
||||
|
||||
if (try opts.getOptional(global, "plugins", JSValue)) |plugin_array| {
|
||||
try bundler_options.parsePluginArray(plugin_array, global);
|
||||
}
|
||||
|
||||
if (try opts.getOptional(global, "bundlerOptions", JSValue)) |js_options| {
|
||||
_ = js_options; // autofix
|
||||
// try SplitBundlerOptions.parseInto(global, js_options, bundler_options, .root);
|
||||
_ = js_options; // TODO:
|
||||
// try bundler_options.parseInto(global, js_options, .root);
|
||||
}
|
||||
|
||||
return framework;
|
||||
@@ -514,19 +583,27 @@ pub const Framework = struct {
|
||||
if (renderer == .server and framework.server_components != null) {
|
||||
try out.options.conditions.appendSlice(&.{"react-server"});
|
||||
}
|
||||
if (mode == .development) {
|
||||
// Support `esm-env` package using this condition.
|
||||
try out.options.conditions.appendSlice(&.{"development"});
|
||||
}
|
||||
|
||||
out.options.production = mode != .development;
|
||||
|
||||
out.options.tree_shaking = mode != .development;
|
||||
out.options.minify_syntax = true; // required for DCE
|
||||
// out.options.minify_identifiers = mode != .development;
|
||||
// out.options.minify_whitespace = mode != .development;
|
||||
out.options.minify_syntax = mode != .development;
|
||||
out.options.minify_identifiers = mode != .development;
|
||||
out.options.minify_whitespace = mode != .development;
|
||||
|
||||
out.options.experimental_css = true;
|
||||
out.options.css_chunking = true;
|
||||
|
||||
out.options.framework = framework;
|
||||
|
||||
// In development mode, source maps must always be `linked`
|
||||
// In production, TODO: follow user configuration
|
||||
out.options.source_map = .linked;
|
||||
|
||||
out.configureLinker();
|
||||
try out.configureDefines();
|
||||
|
||||
@@ -553,7 +630,7 @@ fn getOptionalString(
|
||||
allocations: *StringRefList,
|
||||
arena: Allocator,
|
||||
) !?[]const u8 {
|
||||
const value = try target.get2(global, property) orelse
|
||||
const value = try target.get(global, property) orelse
|
||||
return null;
|
||||
if (value == .undefined or value == .null)
|
||||
return null;
|
||||
@@ -561,11 +638,6 @@ fn getOptionalString(
|
||||
return allocations.track(str.toUTF8(arena));
|
||||
}
|
||||
|
||||
export fn Bun__getTemporaryDevServer(global: *JSC.JSGlobalObject) JSValue {
|
||||
if (!bun.FeatureFlags.bake) return .undefined;
|
||||
return JSC.JSFunction.create(global, "wipDevServer", jsWipDevServer, 0, .{});
|
||||
}
|
||||
|
||||
pub inline fn getHmrRuntime(side: Side) [:0]const u8 {
|
||||
return if (Environment.codegen_embed)
|
||||
switch (side) {
|
||||
@@ -587,6 +659,13 @@ pub const Mode = enum {
|
||||
pub const Side = enum(u1) {
|
||||
client,
|
||||
server,
|
||||
|
||||
pub fn graph(s: Side) Graph {
|
||||
return switch (s) {
|
||||
.client => .client,
|
||||
.server => .server,
|
||||
};
|
||||
}
|
||||
};
|
||||
pub const Graph = enum(u2) {
|
||||
client,
|
||||
@@ -637,6 +716,14 @@ pub fn addImportMetaDefines(
|
||||
"import.meta.env.STATIC",
|
||||
Define.Data.initBoolean(mode == .production_static),
|
||||
);
|
||||
|
||||
if (mode != .development) {
|
||||
try define.insert(
|
||||
allocator,
|
||||
"import.meta.hot",
|
||||
Define.Data.initBoolean(false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub const server_virtual_source: bun.logger.Source = .{
|
||||
@@ -671,6 +758,7 @@ pub const PatternBuffer = struct {
|
||||
pub fn prependPart(pb: *PatternBuffer, part: FrameworkRouter.Part) void {
|
||||
switch (part) {
|
||||
.text => |text| {
|
||||
bun.assert(text.len == 0 or text[0] != '/');
|
||||
pb.prepend(text);
|
||||
pb.prepend("/");
|
||||
},
|
||||
@@ -687,13 +775,28 @@ pub const PatternBuffer = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub fn printWarning() void {
|
||||
// Silence this for the test suite
|
||||
if (bun.getenvZ("BUN_DEV_SERVER_TEST_RUNNER") == null) {
|
||||
bun.Output.warn(
|
||||
\\Be advised that Bun Bake is highly experimental, and its API
|
||||
\\will have breaking changes. Join the <magenta>#bake<r> Discord
|
||||
\\channel to help us find bugs: <blue>https://bun.sh/discord<r>
|
||||
\\
|
||||
\\
|
||||
, .{});
|
||||
bun.Output.flush();
|
||||
}
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const bun = @import("root").bun;
|
||||
const Environment = bun.Environment;
|
||||
const ZigString = bun.JSC.ZigString;
|
||||
|
||||
const JSC = bun.JSC;
|
||||
const JSValue = JSC.JSValue;
|
||||
const validators = bun.JSC.Node.validators;
|
||||
const ZigString = JSC.ZigString;
|
||||
const Plugin = JSC.API.JSBundler.Plugin;
|
||||
|
||||
@@ -5,15 +5,30 @@
|
||||
import * as React from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { createFromReadableStream } from "react-server-dom-bun/client.browser";
|
||||
import { bundleRouteForDevelopment } from "bun:bake/client";
|
||||
import { onServerSideReload } from 'bun:bake/client';
|
||||
import { flushSync } from 'react-dom';
|
||||
|
||||
let encoder = new TextEncoder();
|
||||
let promise = createFromReadableStream(
|
||||
const te = new TextEncoder();
|
||||
const td = new TextDecoder();
|
||||
|
||||
// It is the framework's responsibility to ensure that client-side navigation
|
||||
// loads CSS files. The implementation here loads all CSS files as <link> tags,
|
||||
// and uses the ".disabled" property to enable/disable them.
|
||||
const cssFiles = new Map<string, { promise: Promise<void> | null; link: HTMLLinkElement }>();
|
||||
let currentCssList: string[] | undefined = undefined;
|
||||
|
||||
// The initial RSC payload is put into inline <script> tags that follow the pattern
|
||||
// `(self.__bun_f ??= []).push(chunk)`, which is converted into a ReadableStream
|
||||
// here for React hydration. Since inline scripts are executed immediately, and
|
||||
// this file is loaded asynchronously, the `__bun_f` becomes a clever way to
|
||||
// stream the arbitrary data while HTML is loading. In a static build, this is
|
||||
// setup as an array with one string.
|
||||
let rscPayload: any = createFromReadableStream(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
let handleChunk = chunk =>
|
||||
typeof chunk === "string" //
|
||||
? controller.enqueue(encoder.encode(chunk))
|
||||
? controller.enqueue(te.encode(chunk))
|
||||
: controller.enqueue(chunk);
|
||||
|
||||
(self.__bun_f ||= []).forEach((__bun_f.push = handleChunk));
|
||||
@@ -32,37 +47,109 @@ let promise = createFromReadableStream(
|
||||
// This is a function component that uses the `use` hook, which unwraps a
|
||||
// promise. The promise results in a component containing suspense boundaries.
|
||||
// This is the same logic that happens on the server, except there is also a
|
||||
// hook to update the promise when the client navigates.
|
||||
// hook to update the promise when the client navigates. The `Root` component
|
||||
// also updates CSS files when navigating between routes.
|
||||
let setPage;
|
||||
let abortOnRender: AbortController | undefined;
|
||||
const Root = () => {
|
||||
setPage = React.useState(promise)[1];
|
||||
return React.use(promise);
|
||||
setPage = React.useState(rscPayload)[1];
|
||||
|
||||
// Layout effects are executed right before the browser paints,
|
||||
// which is the perfect time to make CSS visible.
|
||||
React.useLayoutEffect(() => {
|
||||
if (abortOnRender) {
|
||||
try {
|
||||
abortOnRender.abort();
|
||||
abortOnRender = undefined;
|
||||
} catch {}
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
if (currentCssList) disableUnusedCssFiles();
|
||||
});
|
||||
});
|
||||
|
||||
// Unwrap the promise if it is one
|
||||
return rscPayload.then ? React.use(rscPayload) : rscPayload;
|
||||
};
|
||||
const root = hydrateRoot(document, <Root />, {
|
||||
// handle `onUncaughtError` here
|
||||
onUncaughtError(e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Client side navigation is implemented by updating the app's `useState` with a
|
||||
// new RSC payload promise. An abort controller is used to cancel a previous
|
||||
// navigation. Callers of `goto` are expected to manage history state.
|
||||
let currentReloadCtrl: AbortController | null = null;
|
||||
async function goto(href: string) {
|
||||
// TODO: this abort signal stuff doesnt work
|
||||
// if (currentReloadCtrl) {
|
||||
// currentReloadCtrl.abort();
|
||||
// }
|
||||
// const signal = (currentReloadCtrl = new AbortController()).signal;
|
||||
// Keep a cache of page objects to avoid re-fetching a page when pressing the
|
||||
// back button. The cache is indexed by the date it was created.
|
||||
const cachedPages = new Map<number, Page>();
|
||||
// const defaultPageExpiryTime = 1000 * 60 * 5; // 5 minutes
|
||||
interface Page {
|
||||
css: string[];
|
||||
element: unknown;
|
||||
}
|
||||
|
||||
// Due to the current implementation of the Dev Server, it must be informed of
|
||||
// client-side routing so it can load client components. This is not necessary
|
||||
// in production, and calling this in that situation will fail to compile.
|
||||
if (import.meta.env.DEV) {
|
||||
await bundleRouteForDevelopment(href, {
|
||||
// signal
|
||||
const firstPageId = Date.now();
|
||||
{
|
||||
history.replaceState(firstPageId, "", location.href);
|
||||
rscPayload.then(result => {
|
||||
if (lastNavigationId > 0) return;
|
||||
|
||||
// Collect the list of CSS files that were added from SSR
|
||||
const links = document.querySelectorAll<HTMLLinkElement>("link[data-bake-ssr]");
|
||||
currentCssList = [];
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
const link = links[i];
|
||||
const href = new URL(link.href).pathname;
|
||||
currentCssList.push(href);
|
||||
|
||||
// Hack: cannot add this to `cssFiles` because React owns the element, and
|
||||
// it will be removed when any navigation is performed.
|
||||
}
|
||||
|
||||
cachedPages.set(firstPageId, {
|
||||
css: currentCssList!,
|
||||
element: result,
|
||||
});
|
||||
});
|
||||
|
||||
if (document.startViewTransition as unknown) {
|
||||
// View transitions are used by navigations to ensure that the page rerender
|
||||
// all happens in one operation. Additionally, developers may animate
|
||||
// different elements. The default fade animation is disabled so that the
|
||||
// out-of-the-box experience feels like there are no view transitions.
|
||||
// This is done client-side because a React error will unmount all elements.
|
||||
const sheet = new CSSStyleSheet();
|
||||
document.adoptedStyleSheets.push(sheet);
|
||||
sheet.replaceSync(':where(*)::view-transition-group(root){animation:none}');
|
||||
}
|
||||
}
|
||||
|
||||
let lastNavigationId = 0;
|
||||
let lastNavigationController: AbortController;
|
||||
|
||||
// Client side navigation is implemented by updating the app's `useState` with a
|
||||
// new RSC payload promise. Callers of `goto` are expected to manage history state.
|
||||
// A navigation id is used
|
||||
async function goto(href: string, cacheId?: number) {
|
||||
const thisNavigationId = ++lastNavigationId;
|
||||
const olderController = lastNavigationController;
|
||||
lastNavigationController = new AbortController();
|
||||
const signal = lastNavigationController.signal;
|
||||
signal.addEventListener("abort", () => {
|
||||
olderController?.abort();
|
||||
});
|
||||
|
||||
// If the page is cached, use the cached promise instead of fetching it again.
|
||||
const cached = cacheId && cachedPages.get(cacheId);
|
||||
if (cached) {
|
||||
currentCssList = cached.css;
|
||||
await ensureCssIsReady(currentCssList);
|
||||
setPage?.(rscPayload = cached.element);
|
||||
console.log("cached", cached);
|
||||
if (olderController?.signal.aborted === false)
|
||||
abortOnRender = olderController;
|
||||
return;
|
||||
}
|
||||
|
||||
let response;
|
||||
let response: Response;
|
||||
try {
|
||||
// When using static builds, it isn't possible for the server to reliably
|
||||
// branch on the `Accept` header. Instead, a static build creates a `.rsc`
|
||||
@@ -75,35 +162,110 @@ async function goto(href: string) {
|
||||
headers: {
|
||||
Accept: "text/x-component",
|
||||
},
|
||||
// signal: signal,
|
||||
signal,
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${href}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
} catch (err) {
|
||||
// Bail out to browser navigation if this fetch fails.
|
||||
console.error(err);
|
||||
location.href = href;
|
||||
if (thisNavigationId === lastNavigationId) {
|
||||
// Bail out to browser navigation if this fetch fails.
|
||||
console.error(err);
|
||||
location.href = href;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// if (signal.aborted) return;
|
||||
// If the navigation id has changed, this fetch is no longer relevant.
|
||||
if (thisNavigationId !== lastNavigationId) return;
|
||||
let stream = response.body!;
|
||||
|
||||
// TODO: error handling? abort handling?
|
||||
const p = createFromReadableStream(response.body!);
|
||||
// Read the css metadata at the start before handing it to react.
|
||||
stream = await readCssMetadata(stream);
|
||||
if (thisNavigationId !== lastNavigationId) return;
|
||||
|
||||
// TODO: ensure CSS is ready
|
||||
// Right now you can see a flash of unstyled content, since react does not
|
||||
// wait for new link tags to load before they are injected.
|
||||
const cssWaitPromise = ensureCssIsReady(currentCssList!);
|
||||
|
||||
// Use a react transition to update the page promise.
|
||||
// TODO: How to get this show after 100ms, it hangs until all suspenses resolve
|
||||
// React.startTransition(() => {
|
||||
// if (signal.aborted) return;
|
||||
// setPage((promise = p));
|
||||
// });
|
||||
setPage?.((promise = p));
|
||||
const p = await createFromReadableStream(stream);
|
||||
if (thisNavigationId !== lastNavigationId) return;
|
||||
|
||||
if (cssWaitPromise) {
|
||||
await cssWaitPromise;
|
||||
if (thisNavigationId !== lastNavigationId) return;
|
||||
}
|
||||
|
||||
// Save this promise so that pressing the back button in the browser navigates
|
||||
// to the same instance of the old page, instead of re-fetching it.
|
||||
if (cacheId) {
|
||||
cachedPages.set(cacheId, { css: currentCssList, element: p });
|
||||
}
|
||||
|
||||
// Defer aborting a previous request until VERY late. If a previous stream is
|
||||
// aborted while rendering, it will cancel the render, resulting in a flash of
|
||||
// a blank page.
|
||||
if (olderController?.signal.aborted === false) {
|
||||
abortOnRender = olderController;
|
||||
}
|
||||
|
||||
// Tell react about the new page promise
|
||||
if (setPage) {
|
||||
if (document.startViewTransition as unknown) {
|
||||
document.startViewTransition(() => {
|
||||
flushSync(() => {
|
||||
if (thisNavigationId === lastNavigationId)
|
||||
setPage(rscPayload = p);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setPage((rscPayload = p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function blocks until all CSS files are loaded.
|
||||
function ensureCssIsReady(cssList: string[]) {
|
||||
const wait: Promise<void>[] = [];
|
||||
for (const href of cssList) {
|
||||
console.log("check", href);
|
||||
const existing = cssFiles.get(href);
|
||||
console.log("get", existing);
|
||||
if (existing) {
|
||||
const { promise, link } = existing;
|
||||
if (promise) {
|
||||
wait.push(promise);
|
||||
}
|
||||
link.disabled = false;
|
||||
} else {
|
||||
const link = document.createElement("link");
|
||||
let entry;
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
link.rel = "stylesheet";
|
||||
link.onload = resolve as any;
|
||||
link.onerror = reject;
|
||||
link.href = href;
|
||||
document.head.appendChild(link);
|
||||
}).then(() => {
|
||||
console.log("loaded", href);
|
||||
entry.promise = null;
|
||||
});
|
||||
entry = { promise, link };
|
||||
cssFiles.set(href, entry);
|
||||
wait.push(promise);
|
||||
}
|
||||
}
|
||||
if (wait.length === 0) return;
|
||||
return Promise.all(wait);
|
||||
}
|
||||
|
||||
function disableUnusedCssFiles() {
|
||||
// TODO: create a list of files that should be updated instead of a full loop
|
||||
for (const [href, { link }] of cssFiles) {
|
||||
if (!currentCssList!.includes(href)) {
|
||||
link.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instead of relying on a "<Link />" component, a global event listener on all
|
||||
@@ -156,8 +318,9 @@ document.addEventListener("click", async (event, element = event.target as HTMLA
|
||||
}
|
||||
|
||||
const href = url.href;
|
||||
history.pushState({}, "", href);
|
||||
goto(href);
|
||||
const newId = Date.now();
|
||||
history.pushState(newId, "", href);
|
||||
goto(href, newId);
|
||||
|
||||
return event.preventDefault();
|
||||
}
|
||||
@@ -168,10 +331,149 @@ document.addEventListener("click", async (event, element = event.target as HTMLA
|
||||
});
|
||||
|
||||
// Handle browser navigation events
|
||||
window.addEventListener("popstate", () => goto(location.href));
|
||||
window.addEventListener("popstate", event => {
|
||||
console.log("popstate", event);
|
||||
let state = event.state;
|
||||
if (typeof state !== "number") {
|
||||
state = undefined;
|
||||
}
|
||||
goto(location.href, state);
|
||||
});
|
||||
|
||||
// Frameworks can export a `onServerSideReload` function to hook into server-side
|
||||
// hot module reloading. This export is not used in production and tree-shaken.
|
||||
export async function onServerSideReload() {
|
||||
await goto(location.href);
|
||||
if (import.meta.env.DEV) {
|
||||
// Frameworks can call `onServerSideReload` to hook into server-side hot
|
||||
// module reloading.
|
||||
onServerSideReload(async() => {
|
||||
const newId = Date.now();
|
||||
history.replaceState(newId, "", location.href);
|
||||
await goto(location.href, newId);
|
||||
});
|
||||
|
||||
// Expose a global in Development mode
|
||||
(window as any).$bake = {
|
||||
goto,
|
||||
onServerSideReload,
|
||||
get currentCssList() {
|
||||
return currentCssList;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function readCssMetadata(stream: ReadableStream<Uint8Array>) {
|
||||
let reader;
|
||||
try {
|
||||
// Using BYOB reader allows reading an exact amount of bytes, which allows
|
||||
// passing the stream to react without creating a wrapped stream.
|
||||
reader = stream.getReader({ mode: "byob" });
|
||||
} catch (e) {
|
||||
return readCssMetadataFallback(stream);
|
||||
}
|
||||
|
||||
const header = (await reader.read(new Uint32Array(1))).value;
|
||||
if (!header) {
|
||||
if (import.meta.env.DEV) {
|
||||
throw new Error("Did not read all bytes! This is a bug in bun-framework-react");
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
if (header[0] > 0) {
|
||||
const cssRaw = (await reader.read(new Uint8Array(header[0]))).value;
|
||||
if (!cssRaw) {
|
||||
if (import.meta.env.DEV) {
|
||||
throw new Error("Did not read all bytes! This is a bug in bun-framework-react");
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
currentCssList = td.decode(cssRaw).split("\n");
|
||||
} else {
|
||||
currentCssList = [];
|
||||
}
|
||||
reader.releaseLock();
|
||||
return stream;
|
||||
}
|
||||
|
||||
// Safari does not support BYOB reader. When this is resolved, this fallback
|
||||
// should be kept for a few years since Safari on iOS is versioned to the OS.
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=283065
|
||||
async function readCssMetadataFallback(stream: ReadableStream<Uint8Array>) {
|
||||
const reader = stream.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
let totalBytes = 0;
|
||||
const readChunk = async size => {
|
||||
while (totalBytes < size) {
|
||||
const { value, done } = await reader.read();
|
||||
if (!done) {
|
||||
chunks.push(value);
|
||||
totalBytes += value.byteLength;
|
||||
} else if (totalBytes < size) {
|
||||
if (import.meta.env.DEV) {
|
||||
throw new Error("Not enough bytes, expected " + size + " but got " + totalBytes);
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chunks.length === 1) {
|
||||
const first = chunks[0];
|
||||
if(first.byteLength >= size) {
|
||||
chunks[0] = first.subarray(size);
|
||||
totalBytes -= size;
|
||||
return first.subarray(0, size);
|
||||
} else {
|
||||
chunks.length = 0;
|
||||
totalBytes = 0;
|
||||
return first;
|
||||
}
|
||||
} else {
|
||||
const buffer = new Uint8Array(size);
|
||||
let i = 0;
|
||||
let chunk;
|
||||
let len;
|
||||
while (size > 0) {
|
||||
chunk = chunks.shift();
|
||||
const { byteLength } = chunk;
|
||||
len = Math.min(byteLength, size);
|
||||
buffer.set(len === byteLength ? chunk : chunk.subarray(0, len), i);
|
||||
i += len;
|
||||
size -= len;
|
||||
}
|
||||
if (chunk.byteLength > len) {
|
||||
chunks.unshift(chunk.subarray(len));
|
||||
}
|
||||
totalBytes -= size;
|
||||
return buffer;
|
||||
}
|
||||
};
|
||||
const header = new Uint32Array(await readChunk(4))[0];
|
||||
console.log('h', header);
|
||||
if (header === 0) {
|
||||
currentCssList = [];
|
||||
} else {
|
||||
currentCssList = td.decode(await readChunk(header)).split("\n");
|
||||
}
|
||||
console.log('cc', currentCssList);
|
||||
if (chunks.length === 0) {
|
||||
return stream;
|
||||
}
|
||||
// New readable stream that includes the remaining data
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
for (const chunk of chunks) {
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(value);
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
reader.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ export function react(): Bake.Framework {
|
||||
clientEntryPoint: "bun-framework-react/client.tsx",
|
||||
serverEntryPoint: "bun-framework-react/server.tsx",
|
||||
extensions: ["jsx", "tsx"],
|
||||
style: "nextjs-pages-ui",
|
||||
style: "nextjs-pages",
|
||||
layouts: true,
|
||||
ignoreUnderscores: true,
|
||||
},
|
||||
],
|
||||
staticRouters: ["public"],
|
||||
|
||||
@@ -12,16 +12,12 @@ function assertReactComponent(Component: any) {
|
||||
}
|
||||
|
||||
// This function converts the route information into a React component tree.
|
||||
function getPage(meta: Bake.RouteMetadata) {
|
||||
const { styles } = meta;
|
||||
|
||||
const Page = meta.pageModule.default;
|
||||
if (import.meta.env.DEV) assertReactComponent(Page);
|
||||
let route = <Page />;
|
||||
function getPage(meta: Bake.RouteMetadata, styles: readonly string[]) {
|
||||
let route = component(meta.pageModule, meta.params);
|
||||
for (const layout of meta.layouts) {
|
||||
const Layout = layout.default;
|
||||
if (import.meta.env.DEV) assertReactComponent(Layout);
|
||||
route = <Layout>{route}</Layout>;
|
||||
route = <Layout params={meta.params}>{route}</Layout>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -30,7 +26,8 @@ function getPage(meta: Bake.RouteMetadata) {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Bun + React Server Components</title>
|
||||
{styles.map(url => (
|
||||
<link key={url} rel="stylesheet" href={url} />
|
||||
// `data-bake-ssr` is used on the client-side to construct the styles array.
|
||||
<link key={url} rel="stylesheet" href={url} data-bake-ssr />
|
||||
))}
|
||||
</head>
|
||||
<body>{route}</body>
|
||||
@@ -38,6 +35,23 @@ function getPage(meta: Bake.RouteMetadata) {
|
||||
);
|
||||
}
|
||||
|
||||
function component(mod: any, params: Record<string, string> | null) {
|
||||
const Page = mod.default;
|
||||
let props = {};
|
||||
if (import.meta.env.DEV) assertReactComponent(Page);
|
||||
|
||||
let method;
|
||||
if ((import.meta.env.DEV || import.meta.env.STATIC) && (method = mod.getStaticProps)) {
|
||||
if (mod.getServerSideProps) {
|
||||
throw new Error("Cannot have both getStaticProps and getServerSideProps");
|
||||
}
|
||||
|
||||
props = method();
|
||||
}
|
||||
|
||||
return <Page params={params} {...props} />;
|
||||
}
|
||||
|
||||
// `server.tsx` exports a function to be used for handling user routes. It takes
|
||||
// in the Request object, the route's module, and extra route metadata.
|
||||
export async function render(request: Request, meta: Bake.RouteMetadata): Promise<Response> {
|
||||
@@ -45,17 +59,45 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis
|
||||
// - Standard browser navigation
|
||||
// - Client-side navigation
|
||||
//
|
||||
// For React, this means we will always perform `renderToReadableStream` to
|
||||
// generate the RSC payload, but only generate HTML for the former of these
|
||||
// rendering modes. This is signaled by `client.tsx` via the `Accept` header.
|
||||
// For React, this means calling `renderToReadableStream` to generate the RSC
|
||||
// payload, but only generate HTML for the former of these rendering modes.
|
||||
// This is signaled by `client.tsx` via the `Accept` header.
|
||||
const skipSSR = request.headers.get("Accept")?.includes("text/x-component");
|
||||
|
||||
const page = getPage(meta);
|
||||
// Do not render <link> tags if the request is skipping SSR.
|
||||
const page = getPage(meta, skipSSR ? [] : meta.styles);
|
||||
|
||||
// TODO: write a lightweight version of PassThrough
|
||||
const rscPayload = new PassThrough();
|
||||
|
||||
if (skipSSR) {
|
||||
// "client.tsx" reads the start of the response to determine the
|
||||
// CSS files to load. The styles are loaded before the new page
|
||||
// is presented, to avoid a flash of unstyled content.
|
||||
const int = Buffer.allocUnsafe(4);
|
||||
const str = meta.styles.join("\n");
|
||||
int.writeUInt32LE(str.length, 0);
|
||||
rscPayload.write(int);
|
||||
rscPayload.write(str);
|
||||
}
|
||||
|
||||
// This renders Server Components to a ReadableStream "RSC Payload"
|
||||
const rscPayload = renderToPipeableStream(page, serverManifest)
|
||||
// TODO: write a lightweight version of PassThrough
|
||||
.pipe(new PassThrough());
|
||||
let pipe;
|
||||
const signal: MiniAbortSignal = { aborted: false, abort: null! };
|
||||
({ pipe, abort: signal.abort } = renderToPipeableStream(page, serverManifest, {
|
||||
onError: err => {
|
||||
if (signal.aborted) return;
|
||||
console.error(err);
|
||||
},
|
||||
filterStackFrame: () => false,
|
||||
}));
|
||||
pipe(rscPayload);
|
||||
|
||||
rscPayload.on("error", err => {
|
||||
if (signal.aborted) return;
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
if (skipSSR) {
|
||||
return new Response(rscPayload as any, {
|
||||
status: 200,
|
||||
@@ -63,8 +105,8 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis
|
||||
});
|
||||
}
|
||||
|
||||
// Then the RSC payload is rendered into HTML
|
||||
return new Response(await renderToHtml(rscPayload, meta.scripts), {
|
||||
// The RSC payload is rendered into HTML
|
||||
return new Response(await renderToHtml(rscPayload, meta.modules, signal), {
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf8",
|
||||
},
|
||||
@@ -75,16 +117,18 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis
|
||||
// function returns no files, the route is always dynamic. When building an app
|
||||
// to static files, all routes get pre-rendered (build failure if not possible).
|
||||
export async function prerender(meta: Bake.RouteMetadata) {
|
||||
const page = getPage(meta);
|
||||
const page = getPage(meta, meta.styles);
|
||||
|
||||
const rscPayload = renderToPipeableStream(page, serverManifest)
|
||||
// TODO: write a lightweight version of PassThrough
|
||||
.pipe(new PassThrough());
|
||||
|
||||
let rscChunks: Uint8Array[] = [];
|
||||
const int = new Uint32Array(1);
|
||||
int[0] = meta.styles.length;
|
||||
let rscChunks: Array<BlobPart> = [int.buffer as ArrayBuffer, meta.styles.join("\n")];
|
||||
rscPayload.on("data", chunk => rscChunks.push(chunk));
|
||||
|
||||
const html = await renderToStaticHtml(rscPayload, meta.scripts);
|
||||
const html = await renderToStaticHtml(rscPayload, meta.modules);
|
||||
const rsc = new Blob(rscChunks, { type: "text/x-component" });
|
||||
|
||||
return {
|
||||
@@ -109,9 +153,38 @@ export async function prerender(meta: Bake.RouteMetadata) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getParams(meta: Bake.ParamsMetadata): Promise<Bake.GetParamIterator> {
|
||||
const getStaticPaths = meta.pageModule.getStaticPaths;
|
||||
if (getStaticPaths == null) {
|
||||
if (import.meta.env.STATIC) {
|
||||
throw new Error(
|
||||
"In files with dynamic params, a `getStaticPaths` function must be exported to tell Bun what files to render.",
|
||||
);
|
||||
} else {
|
||||
return { pages: [], exhaustive: false };
|
||||
}
|
||||
}
|
||||
const result = await meta.pageModule.getStaticPaths();
|
||||
// Remap the Next.js pagess paradigm to Bun's format
|
||||
if (result.paths) {
|
||||
return {
|
||||
pages: result.paths.map(path => path.params),
|
||||
};
|
||||
}
|
||||
// Allow returning the array directly
|
||||
return result;
|
||||
}
|
||||
|
||||
// When a dynamic build uses static assets, Bun can map content types in the
|
||||
// user's `Accept` header to the different static files.
|
||||
export const contentTypeToStaticFile = {
|
||||
"text/html": "index.html",
|
||||
"text/x-component": "index.rsc",
|
||||
};
|
||||
|
||||
/** Instead of using AbortController, this is used */
|
||||
export interface MiniAbortSignal {
|
||||
aborted: boolean;
|
||||
/** Caller must set `aborted` to true before calling. */
|
||||
abort: () => void;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
// no longer set. This means we can import client components, using `react-dom`
|
||||
// to perform Server-side rendering (creating HTML) out of the RSC payload.
|
||||
import * as React from "react";
|
||||
import { clientManifest } from "bun:bake/server";
|
||||
import { ssrManifest } from "bun:bake/server";
|
||||
import type { Readable } from "node:stream";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { createFromNodeStream, type Manifest } from "react-server-dom-bun/client.node.unbundled.js";
|
||||
import { renderToPipeableStream } from "react-dom/server.node";
|
||||
import { MiniAbortSignal } from "./server";
|
||||
|
||||
// Verify that React 19 is being used.
|
||||
if (!React.use) {
|
||||
@@ -14,7 +15,7 @@ if (!React.use) {
|
||||
}
|
||||
|
||||
const createFromNodeStreamOptions: Manifest = {
|
||||
moduleMap: clientManifest,
|
||||
moduleMap: ssrManifest,
|
||||
moduleLoading: { prefix: "/" },
|
||||
};
|
||||
|
||||
@@ -31,7 +32,11 @@ const createFromNodeStreamOptions: Manifest = {
|
||||
// References:
|
||||
// - https://github.com/vercel/next.js/blob/15.0.2/packages/next/src/server/app-render/use-flight-response.tsx
|
||||
// - https://github.com/devongovett/rsc-html-stream
|
||||
export function renderToHtml(rscPayload: Readable, bootstrapModules: readonly string[]): ReadableStream {
|
||||
export function renderToHtml(
|
||||
rscPayload: Readable,
|
||||
bootstrapModules: readonly string[],
|
||||
signal: MiniAbortSignal,
|
||||
): ReadableStream {
|
||||
// Bun supports a special type of readable stream type called "direct",
|
||||
// which provides a raw handle to the controller. We can bypass all of
|
||||
// the Web Streams API (slow) and use the controller directly.
|
||||
@@ -40,14 +45,11 @@ export function renderToHtml(rscPayload: Readable, bootstrapModules: readonly st
|
||||
return new ReadableStream({
|
||||
type: "direct",
|
||||
pull(controller) {
|
||||
// Initialize the injection stream so it gets the first "data" listener.
|
||||
stream = new RscInjectionStream(rscPayload, controller);
|
||||
|
||||
// `createFromNodeStream` turns the RSC payload into a React component.
|
||||
const promise = createFromNodeStream(rscPayload, {
|
||||
// React takes in a manifest mapping client-side assets
|
||||
// to the imports needed for server-side rendering.
|
||||
moduleMap: clientManifest,
|
||||
moduleMap: ssrManifest,
|
||||
moduleLoading: { prefix: "/" },
|
||||
});
|
||||
// The root is this "Root" component that unwraps the streamed promise
|
||||
@@ -59,14 +61,22 @@ export function renderToHtml(rscPayload: Readable, bootstrapModules: readonly st
|
||||
let pipe: (stream: any) => void;
|
||||
({ pipe, abort } = renderToPipeableStream(<Root />, {
|
||||
bootstrapModules,
|
||||
onError(error) {
|
||||
if (!signal.aborted) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
stream = new RscInjectionStream(rscPayload, controller);
|
||||
pipe(stream);
|
||||
|
||||
// Promise resolved after all data is combined.
|
||||
return stream.finished;
|
||||
},
|
||||
cancel() {
|
||||
stream?.destroy();
|
||||
signal.aborted = true;
|
||||
signal.abort();
|
||||
abort?.();
|
||||
},
|
||||
} as Bun.DirectUnderlyingSource as any);
|
||||
@@ -139,6 +149,17 @@ class RscInjectionStream extends EventEmitter {
|
||||
}
|
||||
|
||||
write(data: Uint8Array) {
|
||||
if (import.meta.env.DEV && process.env.VERBOSE_SSR)
|
||||
console.write(
|
||||
"write" +
|
||||
Bun.inspect(
|
||||
{
|
||||
data: new TextDecoder().decode(data),
|
||||
},
|
||||
{ colors: true },
|
||||
) +
|
||||
"\n",
|
||||
);
|
||||
if (endsWithClosingScript(data)) {
|
||||
// The HTML is not done yet, but it's a suitible time to inject RSC data.
|
||||
const { controller } = this;
|
||||
@@ -152,7 +173,7 @@ class RscInjectionStream extends EventEmitter {
|
||||
controller.write(data.subarray(0, data.length - closingBodyTag.length));
|
||||
this.drainRscChunks();
|
||||
controller.write(closingBodyTag);
|
||||
controller.end();
|
||||
controller.flush();
|
||||
this.finalize();
|
||||
} else {
|
||||
this.controller.write(data);
|
||||
@@ -181,6 +202,18 @@ class RscInjectionStream extends EventEmitter {
|
||||
}
|
||||
|
||||
writeRscData(chunk: Uint8Array) {
|
||||
if (import.meta.env.DEV && process.env.VERBOSE_SSR)
|
||||
console.write(
|
||||
"writeRscData " +
|
||||
Bun.inspect(
|
||||
{
|
||||
data: new TextDecoder().decode(chunk),
|
||||
},
|
||||
{ colors: true },
|
||||
) +
|
||||
"\n",
|
||||
);
|
||||
|
||||
if (this.html === HtmlState.Boundary) {
|
||||
const { controller, decoder } = this;
|
||||
if (this.rsc === RscState.Waiting) {
|
||||
@@ -199,13 +232,9 @@ class RscInjectionStream extends EventEmitter {
|
||||
// Ignore flush requests from React. Bun will automatically flush when reasonable.
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// TODO:
|
||||
}
|
||||
destroy() {}
|
||||
|
||||
end() {
|
||||
this.finalize();
|
||||
}
|
||||
end() {}
|
||||
}
|
||||
|
||||
class StaticRscInjectionStream extends EventEmitter {
|
||||
|
||||
186
src/bake/client/css-reloader.ts
Normal file
186
src/bake/client/css-reloader.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
// CSS hot reloading is implemented a bit weirdly. A lot of opinions on how CSS
|
||||
// is managed is put in the hands of the framework implementation, but it is
|
||||
// assumed that some basic things always hold true:
|
||||
//
|
||||
// - SSR injects <link> elements with the URLs that Bun provided
|
||||
// - CSR will remove or append new <link> elements
|
||||
// - These link elements are direct children of <head>
|
||||
// - The URL bar contains the current route reflected by the UI
|
||||
//
|
||||
// With this, production mode is fully implemented in the framework, and
|
||||
// DevServer can hot-reload these files with some clever observation.
|
||||
//
|
||||
// The approach is to attach CSSStyleSheet objects to the page, which
|
||||
// the runtime can update at will. Then, a MutationObserver is used to
|
||||
// allow the framework to change the <link> tags as a part of its own
|
||||
// client-side navigation.
|
||||
|
||||
const cssStore = new Map<string, CSS>();
|
||||
const registeredLinkTags = new Map<HTMLLinkElement, string>();
|
||||
|
||||
interface CSS {
|
||||
sheet: CSSStyleSheet | null;
|
||||
link: HTMLLinkElement | null;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
function validateCssId(id: string) {
|
||||
if (!/^[a-f0-9]{16}$/.test(id)) {
|
||||
throw new Error(`Invalid CSS id: ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
function deactivateCss(css: CSS) {
|
||||
if (css.active) {
|
||||
const { sheet, link } = css;
|
||||
css.active = false;
|
||||
if (sheet) {
|
||||
sheet.disabled = true;
|
||||
} else if (link) {
|
||||
const linkSheet = link.sheet;
|
||||
if (linkSheet) linkSheet.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function activateCss(css: CSS) {
|
||||
if (!css.active) {
|
||||
css.active = true;
|
||||
if (css.sheet) {
|
||||
css.sheet.disabled = false;
|
||||
} else if (css.link) {
|
||||
const linkSheet = css.link.sheet;
|
||||
if (linkSheet) linkSheet.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A mutation observer detects when the framework does client-side routing.
|
||||
const headObserver = new MutationObserver(list => {
|
||||
for (const mutation of list) {
|
||||
if (mutation.type === "childList") {
|
||||
// This allows frameworks to add and remove link tags. Removing a link tag
|
||||
// that Bun had reloaded needs to disable the wrapped sheet. The wrapper
|
||||
// is kept around in case the framework re-adds the link tag.
|
||||
let i = 0;
|
||||
let len = mutation.removedNodes.length;
|
||||
while (i < len) {
|
||||
const node = mutation.removedNodes[i];
|
||||
const id = registeredLinkTags.get(node as HTMLLinkElement);
|
||||
if (id) {
|
||||
const existingSheet = cssStore.get(id);
|
||||
if (existingSheet) {
|
||||
deactivateCss(existingSheet);
|
||||
}
|
||||
registeredLinkTags.delete(node as HTMLLinkElement);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
i = 0;
|
||||
len = mutation.addedNodes.length;
|
||||
while (i < len) {
|
||||
const node = mutation.addedNodes[i];
|
||||
if (node instanceof HTMLLinkElement) {
|
||||
maybeAddCssLink(node);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
} else if (mutation.type === "attributes") {
|
||||
// This allows frameworks to set the `disabled` attribute on the link tag
|
||||
const target = mutation.target as HTMLLinkElement;
|
||||
if (target.tagName === "LINK" && target.rel === "stylesheet") {
|
||||
const id = registeredLinkTags.get(target);
|
||||
if (id) {
|
||||
const existing = cssStore.get(id);
|
||||
if (existing) {
|
||||
const disabled = target.disabled;
|
||||
if (disabled) {
|
||||
deactivateCss(existing);
|
||||
} else {
|
||||
activateCss(existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function maybeAddCssLink(link: HTMLLinkElement) {
|
||||
const pathname = new URL(link.href).pathname;
|
||||
if (pathname.startsWith("/_bun/css/")) {
|
||||
const id = pathname.slice("/_bun/css/".length).slice(0, 16);
|
||||
if ( !/^[a-f0-9]{16}$/.test(id)) {
|
||||
return;
|
||||
}
|
||||
const existing = cssStore.get(id);
|
||||
if (existing) {
|
||||
const { sheet } = existing;
|
||||
if (sheet) {
|
||||
// The HMR runtime has a managed sheet already.
|
||||
sheet.disabled = false;
|
||||
const linkSheet = link.sheet;
|
||||
if (linkSheet) linkSheet.disabled = true;
|
||||
}
|
||||
existing.link = link;
|
||||
} else {
|
||||
cssStore.set(id, {
|
||||
sheet: null,
|
||||
link,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
registeredLinkTags.set(link, id);
|
||||
}
|
||||
}
|
||||
|
||||
headObserver.observe(document.head, {
|
||||
childList: true,
|
||||
// TODO: consider using a separate observer for attributes, this can avoid subtree
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["disabled"],
|
||||
});
|
||||
document.querySelectorAll<HTMLLinkElement>("head>link[rel=stylesheet]").forEach(maybeAddCssLink);
|
||||
|
||||
export function editCssArray(array: string[]) {
|
||||
const removedCssKeys = new Set(cssStore.keys());
|
||||
for (const css of array) {
|
||||
if (IS_BUN_DEVELOPMENT) validateCssId(css);
|
||||
const existing = cssStore.get(css);
|
||||
removedCssKeys.delete(css);
|
||||
if (existing) {
|
||||
activateCss(existing);
|
||||
} else {
|
||||
// This will be populated shortly by a call to `editCssContent`
|
||||
cssStore.set(css, {
|
||||
sheet: null,
|
||||
link: null,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const css of removedCssKeys) {
|
||||
const entry = cssStore.get(css);
|
||||
if (entry) {
|
||||
deactivateCss(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function editCssContent(id: string, newContent: string) {
|
||||
let entry = cssStore.get(id);
|
||||
if (!entry) return;
|
||||
let sheet = entry.sheet;
|
||||
if (!entry.sheet) {
|
||||
sheet = entry.sheet = new CSSStyleSheet();
|
||||
sheet.replace(newContent);
|
||||
document.adoptedStyleSheets.push(sheet);
|
||||
|
||||
// Disable the link tag if it exists
|
||||
const linkSheet = entry.link?.sheet;
|
||||
if (linkSheet) linkSheet.disabled = true;
|
||||
return;
|
||||
}
|
||||
sheet!.replace(newContent);
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import { DataViewReader } from "./reader";
|
||||
|
||||
if (side !== "client") throw new Error("Not client side!");
|
||||
|
||||
export let hasFatalError = false;
|
||||
|
||||
// I would have used JSX, but TypeScript types interfere in odd ways.
|
||||
function elem(tagName: string, props?: null | Record<string, string>, children?: (HTMLElement | Text)[]) {
|
||||
const node = document.createElement(tagName);
|
||||
@@ -128,10 +130,18 @@ export function onErrorMessage(view: DataView) {
|
||||
updateErrorOverlay();
|
||||
}
|
||||
|
||||
export function onErrorClearedMessage() {
|
||||
errors.keys().forEach(key => updatedErrorOwners.add(key));
|
||||
errors.clear();
|
||||
updateErrorOverlay();
|
||||
export const enum RuntimeErrorType {
|
||||
recoverable,
|
||||
/** Requires that clearances perform a full page reload */
|
||||
fatal,
|
||||
}
|
||||
|
||||
export function onRuntimeError(err: any, type: RuntimeErrorType) {
|
||||
if (type === RuntimeErrorType.fatal) {
|
||||
hasFatalError = true;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,12 @@ export class DataViewReader {
|
||||
return value;
|
||||
}
|
||||
|
||||
i32() {
|
||||
const value = this.view.getInt32(this.cursor, true);
|
||||
this.cursor += 4;
|
||||
return value;
|
||||
}
|
||||
|
||||
u16() {
|
||||
const value = this.view.getUint16(this.cursor, true);
|
||||
this.cursor += 2;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const isLocal = location.host === "localhost" || location.host === "127.0.0.1";
|
||||
|
||||
function wait() {
|
||||
return new Promise<void>(done => {
|
||||
let wait = typeof document !== 'undefined'
|
||||
? () => new Promise<void>(done => {
|
||||
let timer: Timer | null = null;
|
||||
|
||||
const onBlur = () => {
|
||||
@@ -30,12 +30,37 @@ function wait() {
|
||||
);
|
||||
|
||||
window.addEventListener("blur", onBlur);
|
||||
}
|
||||
});
|
||||
}})
|
||||
: () => new Promise<void>(done => setTimeout(done, 2_500));
|
||||
|
||||
interface WebSocketWrapper {
|
||||
/** When re-connected, this is re-assigned */
|
||||
wrapped: WebSocket | null;
|
||||
send(data: string | ArrayBuffer): void;
|
||||
close(): void;
|
||||
[Symbol.dispose](): void;
|
||||
}
|
||||
|
||||
export function initWebSocket(handlers: Record<number, (dv: DataView) => void>) {
|
||||
export function initWebSocket(handlers: Record<number, (dv: DataView, ws: WebSocket) => void>, url: string = "/_bun/hmr") :WebSocketWrapper {
|
||||
let firstConnection = true;
|
||||
let closed = false;
|
||||
|
||||
const wsProxy: WebSocketWrapper = {
|
||||
wrapped: null,
|
||||
send(data) {
|
||||
const wrapped = this.wrapped;
|
||||
if (wrapped && wrapped.readyState === 1) {
|
||||
wrapped.send(data);
|
||||
}
|
||||
},
|
||||
close() {
|
||||
closed = true;
|
||||
this.wrapped?.close();
|
||||
},
|
||||
[Symbol.dispose]() {
|
||||
this.close();
|
||||
},
|
||||
};
|
||||
|
||||
function onOpen() {
|
||||
if (firstConnection) {
|
||||
@@ -51,7 +76,7 @@ export function initWebSocket(handlers: Record<number, (dv: DataView) => void>)
|
||||
if (IS_BUN_DEVELOPMENT) {
|
||||
console.info("[WS] " + String.fromCharCode(view.getUint8(0)));
|
||||
}
|
||||
handlers[view.getUint8(0)]?.(view);
|
||||
handlers[view.getUint8(0)]?.(view, ws);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,13 +88,14 @@ export function initWebSocket(handlers: Record<number, (dv: DataView) => void>)
|
||||
console.warn("[Bun] Hot-module-reloading socket disconnected, reconnecting...");
|
||||
|
||||
while (true) {
|
||||
if (closed) return;
|
||||
await wait();
|
||||
|
||||
// Note: Cannot use Promise.withResolvers due to lacking support on iOS
|
||||
let done;
|
||||
const promise = new Promise<boolean>(cb => (done = cb));
|
||||
|
||||
ws = new WebSocket("/_bun/hmr");
|
||||
ws = wsProxy.wrapped = new WebSocket(url);
|
||||
ws.binaryType = "arraybuffer";
|
||||
ws.onopen = () => {
|
||||
console.info("[Bun] Reconnected");
|
||||
@@ -89,10 +115,12 @@ export function initWebSocket(handlers: Record<number, (dv: DataView) => void>)
|
||||
}
|
||||
}
|
||||
|
||||
let ws = new WebSocket("/_bun/hmr");
|
||||
let ws = wsProxy.wrapped = new WebSocket(url);
|
||||
ws.binaryType = "arraybuffer";
|
||||
ws.onopen = onOpen;
|
||||
ws.onmessage = onMessage;
|
||||
ws.onclose = onClose;
|
||||
ws.onerror = onError;
|
||||
|
||||
return wsProxy;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export type ExportsCallbackFunction = (new_exports: any) => void;
|
||||
|
||||
export const enum State {
|
||||
Loading,
|
||||
Boundary,
|
||||
Ready,
|
||||
Error,
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ export const enum LoadModuleType {
|
||||
UserDynamic,
|
||||
}
|
||||
|
||||
interface DepEntry {
|
||||
_callback: ExportsCallbackFunction;
|
||||
_expectedImports: string[] | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This object is passed as the CommonJS "module", but has a bunch of
|
||||
* non-standard properties that are used for implementing hot-module reloading.
|
||||
@@ -33,7 +38,8 @@ export class HotModule<E = any> {
|
||||
_import_meta: ImportMeta | undefined = undefined;
|
||||
_cached_failure: any = undefined;
|
||||
// modules that import THIS module
|
||||
_deps: Map<HotModule, ExportsCallbackFunction | undefined> = new Map();
|
||||
_deps: Map<HotModule, DepEntry | undefined> = new Map();
|
||||
_onDispose: HotDisposeFunction[] | undefined = undefined;
|
||||
|
||||
constructor(id: Id) {
|
||||
this.id = id;
|
||||
@@ -41,18 +47,27 @@ export class HotModule<E = any> {
|
||||
|
||||
require(id: Id, onReload?: ExportsCallbackFunction) {
|
||||
const mod = loadModule(id, LoadModuleType.UserDynamic);
|
||||
mod._deps.set(this, onReload);
|
||||
mod._deps.set(this, onReload ? { _callback: onReload, _expectedImports: undefined } : undefined);
|
||||
return mod.exports;
|
||||
}
|
||||
|
||||
importSync(id: Id, onReload?: ExportsCallbackFunction) {
|
||||
importSync(id: Id, onReload?: ExportsCallbackFunction, expectedImports?: string[]) {
|
||||
const mod = loadModule(id, LoadModuleType.AssertPresent);
|
||||
// insert into the map if not present
|
||||
mod._deps.set(this, onReload);
|
||||
mod._deps.set(this, onReload ? { _callback: onReload, _expectedImports: expectedImports } : undefined);
|
||||
const { exports, __esModule } = mod;
|
||||
return __esModule ? exports : (mod._ext_exports ??= { ...exports, default: exports });
|
||||
const object = __esModule ? exports : (mod._ext_exports ??= { ...exports, default: exports });
|
||||
|
||||
if (expectedImports && mod._state === State.Ready) {
|
||||
for (const key of expectedImports) {
|
||||
if (!(key in object)) {
|
||||
throw new SyntaxError(`The requested module '${id}' does not provide an export named '${key}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
/// Equivalent to `import()` in ES modules
|
||||
async dynamicImport(specifier: string, opts?: ImportCallOptions) {
|
||||
const mod = loadModule(specifier, LoadModuleType.UserDynamic);
|
||||
// insert into the map if not present
|
||||
@@ -76,7 +91,79 @@ if (side === "server") {
|
||||
}
|
||||
|
||||
function initImportMeta(m: HotModule): ImportMeta {
|
||||
throw new Error("TODO: import meta object");
|
||||
return {
|
||||
url: `bun://${m.id}`,
|
||||
main: false,
|
||||
// @ts-ignore
|
||||
get hot() {
|
||||
const hot = new Hot(m);
|
||||
Object.defineProperty(this, "hot", { value: hot });
|
||||
return hot;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type HotAcceptFunction = (esmExports: any | void) => void;
|
||||
type HotArrayAcceptFunction = (esmExports: (any | void)[]) => void;
|
||||
type HotDisposeFunction = (data: any) => void;
|
||||
type HotEventHandler = (data: any) => void;
|
||||
|
||||
class Hot {
|
||||
private _module: HotModule;
|
||||
|
||||
data = {};
|
||||
|
||||
constructor(module: HotModule) {
|
||||
this._module = module;
|
||||
}
|
||||
|
||||
accept(
|
||||
arg1: string | readonly string[] | HotAcceptFunction,
|
||||
arg2: HotAcceptFunction | HotArrayAcceptFunction | undefined,
|
||||
) {
|
||||
console.warn("TODO: implement ImportMetaHot.accept (called from " + JSON.stringify(this._module.id) + ")");
|
||||
}
|
||||
|
||||
decline() {} // Vite: "This is currently a noop and is there for backward compatibility"
|
||||
|
||||
dispose(cb: HotDisposeFunction) {
|
||||
(this._module._onDispose ??= []).push(cb);
|
||||
}
|
||||
|
||||
prune(cb: HotDisposeFunction) {
|
||||
throw new Error("TODO: implement ImportMetaHot.prune");
|
||||
}
|
||||
|
||||
invalidate() {
|
||||
throw new Error("TODO: implement ImportMetaHot.invalidate");
|
||||
}
|
||||
|
||||
on(event: string, cb: HotEventHandler) {
|
||||
if (isUnsupportedViteEventName(event)) {
|
||||
throw new Error(`Unsupported event name: ${event}`);
|
||||
}
|
||||
|
||||
throw new Error("TODO: implement ImportMetaHot.on");
|
||||
}
|
||||
|
||||
off(event: string, cb: HotEventHandler) {
|
||||
throw new Error("TODO: implement ImportMetaHot.off");
|
||||
}
|
||||
|
||||
send(event: string, cb: HotEventHandler) {
|
||||
throw new Error("TODO: implement ImportMetaHot.send");
|
||||
}
|
||||
}
|
||||
|
||||
function isUnsupportedViteEventName(str: string) {
|
||||
return str === 'vite:beforeUpdate'
|
||||
|| str === 'vite:afterUpdate'
|
||||
|| str === 'vite:beforeFullReload'
|
||||
|| str === 'vite:beforePrune'
|
||||
|| str === 'vite:invalidate'
|
||||
|| str === 'vite:error'
|
||||
|| str === 'vite:ws:disconnect'
|
||||
|| str === 'vite:ws:connect';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,6 +194,10 @@ export function loadModule<T = any>(key: Id, type: LoadModuleType): HotModule<T>
|
||||
try {
|
||||
registry.set(key, mod);
|
||||
load(mod);
|
||||
mod._state = State.Ready;
|
||||
mod._deps.forEach((entry, dep) => {
|
||||
entry._callback?.(mod.exports);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
mod._cached_failure = err;
|
||||
@@ -121,11 +212,12 @@ export const getModule = registry.get.bind(registry);
|
||||
export function replaceModule(key: Id, load: ModuleLoadFunction) {
|
||||
const module = registry.get(key);
|
||||
if (module) {
|
||||
module._onDispose?.forEach((cb) => cb(null));
|
||||
module.exports = {};
|
||||
load(module);
|
||||
const { exports } = module;
|
||||
for (const updater of module._deps.values()) {
|
||||
updater?.(exports);
|
||||
updater?._callback?.(exports);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,12 +247,14 @@ export function replaceModules(modules: any) {
|
||||
}
|
||||
|
||||
export const serverManifest = {};
|
||||
export const clientManifest = {};
|
||||
export const ssrManifest = {};
|
||||
|
||||
export let onServerSideReload: (() => Promise<void>) | null = null;
|
||||
|
||||
if (side === "server") {
|
||||
const server_module = new HotModule("bun:bake/server");
|
||||
server_module.__esModule = true;
|
||||
server_module.exports = { serverManifest, clientManifest };
|
||||
server_module.exports = { serverManifest, ssrManifest, actionManifest: null };
|
||||
registry.set(server_module.id, server_module);
|
||||
}
|
||||
|
||||
@@ -174,7 +268,9 @@ if (side === "client") {
|
||||
const server_module = new HotModule("bun:bake/client");
|
||||
server_module.__esModule = true;
|
||||
server_module.exports = {
|
||||
bundleRouteForDevelopment: async () => {},
|
||||
onServerSideReload: async (cb) => {
|
||||
onServerSideReload = cb;
|
||||
},
|
||||
};
|
||||
registry.set(server_module.id, server_module);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user