mirror of
https://github.com/oven-sh/bun
synced 2026-02-26 03:27:23 +01:00
Compare commits
7 Commits
claude/fix
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64836a9a85 | ||
|
|
ddefa11070 | ||
|
|
35f8154319 | ||
|
|
9d68ec882a | ||
|
|
1337f5dba4 | ||
|
|
56b5be4ba4 | ||
|
|
6c119d608e |
@@ -259,18 +259,13 @@ $ git clone https://github.com/oven-sh/WebKit vendor/WebKit
|
||||
# Check out the commit hash specified in `set(WEBKIT_VERSION <commit_hash>)` in cmake/tools/SetupWebKit.cmake
|
||||
$ git -C vendor/WebKit checkout <commit_hash>
|
||||
|
||||
# Make a debug build of JSC. This will output build artifacts in ./vendor/WebKit/WebKitBuild/Debug
|
||||
# Optionally, you can use `bun run jsc:build` for a release build
|
||||
$ bun run jsc:build:debug && rm vendor/WebKit/WebKitBuild/Debug/JavaScriptCore/DerivedSources/inspector/InspectorProtocolObjects.h
|
||||
|
||||
# After an initial run of `make jsc-debug`, you can rebuild JSC with:
|
||||
$ cmake --build vendor/WebKit/WebKitBuild/Debug --target jsc && rm vendor/WebKit/WebKitBuild/Debug/JavaScriptCore/DerivedSources/inspector/InspectorProtocolObjects.h
|
||||
|
||||
# Build bun with the local JSC build
|
||||
# Build bun with the local JSC build — this automatically configures and builds JSC
|
||||
$ bun run build:local
|
||||
```
|
||||
|
||||
Using `bun run build:local` will build Bun in the `./build/debug-local` directory (instead of `./build/debug`), you'll have to change a couple of places to use this new directory:
|
||||
`bun run build:local` handles everything: configuring JSC, building JSC, and building Bun. On subsequent runs, JSC will incrementally rebuild if any WebKit sources changed. `ninja -Cbuild/debug-local` also works after the first build, and will build Bun+JSC.
|
||||
|
||||
The build output goes to `./build/debug-local` (instead of `./build/debug`), so you'll need to update a couple of places:
|
||||
|
||||
- The first line in [`src/js/builtins.d.ts`](/src/js/builtins.d.ts)
|
||||
- The `CompilationDatabase` line in [`.clangd` config](/.clangd) should be `CompilationDatabase: build/debug-local`
|
||||
@@ -281,7 +276,7 @@ Note that the WebKit folder, including build artifacts, is 8GB+ in size.
|
||||
|
||||
If you are using a JSC debug build and using VScode, make sure to run the `C/C++: Select a Configuration` command to configure intellisense to find the debug headers.
|
||||
|
||||
Note that if you change make changes to our [WebKit fork](https://github.com/oven-sh/WebKit), you will also have to change [`SetupWebKit.cmake`](/cmake/tools/SetupWebKit.cmake) to point to the commit hash.
|
||||
Note that if you make changes to our [WebKit fork](https://github.com/oven-sh/WebKit), you will also have to change [`SetupWebKit.cmake`](/cmake/tools/SetupWebKit.cmake) to point to the commit hash.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -1273,13 +1273,18 @@ else()
|
||||
${WEBKIT_LIB_PATH}/libWTF.a
|
||||
${WEBKIT_LIB_PATH}/libJavaScriptCore.a
|
||||
)
|
||||
if(NOT APPLE OR EXISTS ${WEBKIT_LIB_PATH}/libbmalloc.a)
|
||||
if(WEBKIT_LOCAL OR NOT APPLE OR EXISTS ${WEBKIT_LIB_PATH}/libbmalloc.a)
|
||||
target_link_libraries(${bun} PRIVATE ${WEBKIT_LIB_PATH}/libbmalloc.a)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
include_directories(${WEBKIT_INCLUDE_PATH})
|
||||
|
||||
# When building with a local WebKit, ensure JSC is built before compiling Bun's C++ sources.
|
||||
if(WEBKIT_LOCAL AND TARGET jsc)
|
||||
add_dependencies(${bun} jsc)
|
||||
endif()
|
||||
|
||||
# Include the generated dependency versions header
|
||||
include_directories(${CMAKE_BINARY_DIR})
|
||||
|
||||
@@ -1324,9 +1329,14 @@ if(LINUX)
|
||||
target_link_libraries(${bun} PUBLIC libatomic.so)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${bun} PRIVATE ${WEBKIT_LIB_PATH}/libicudata.a)
|
||||
target_link_libraries(${bun} PRIVATE ${WEBKIT_LIB_PATH}/libicui18n.a)
|
||||
target_link_libraries(${bun} PRIVATE ${WEBKIT_LIB_PATH}/libicuuc.a)
|
||||
if(WEBKIT_LOCAL)
|
||||
find_package(ICU REQUIRED COMPONENTS data i18n uc)
|
||||
target_link_libraries(${bun} PRIVATE ICU::data ICU::i18n ICU::uc)
|
||||
else()
|
||||
target_link_libraries(${bun} PRIVATE ${WEBKIT_LIB_PATH}/libicudata.a)
|
||||
target_link_libraries(${bun} PRIVATE ${WEBKIT_LIB_PATH}/libicui18n.a)
|
||||
target_link_libraries(${bun} PRIVATE ${WEBKIT_LIB_PATH}/libicuuc.a)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
|
||||
@@ -3,6 +3,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")
|
||||
option(WEBKIT_BUILD_TYPE "The build type for local WebKit (defaults to CMAKE_BUILD_TYPE)")
|
||||
|
||||
if(NOT WEBKIT_VERSION)
|
||||
set(WEBKIT_VERSION 515344bc5d65aa2d4f9ff277b5fb944f0e051dcd)
|
||||
@@ -15,7 +16,10 @@ string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX)
|
||||
string(SUBSTRING ${WEBKIT_VERSION} 0 8 WEBKIT_VERSION_SHORT)
|
||||
|
||||
if(WEBKIT_LOCAL)
|
||||
set(DEFAULT_WEBKIT_PATH ${VENDOR_PATH}/WebKit/WebKitBuild/${CMAKE_BUILD_TYPE})
|
||||
if(NOT WEBKIT_BUILD_TYPE)
|
||||
set(WEBKIT_BUILD_TYPE ${CMAKE_BUILD_TYPE})
|
||||
endif()
|
||||
set(DEFAULT_WEBKIT_PATH ${VENDOR_PATH}/WebKit/WebKitBuild/${WEBKIT_BUILD_TYPE})
|
||||
else()
|
||||
set(DEFAULT_WEBKIT_PATH ${CACHE_PATH}/webkit-${WEBKIT_VERSION_PREFIX})
|
||||
endif()
|
||||
@@ -30,35 +34,153 @@ set(WEBKIT_INCLUDE_PATH ${WEBKIT_PATH}/include)
|
||||
set(WEBKIT_LIB_PATH ${WEBKIT_PATH}/lib)
|
||||
|
||||
if(WEBKIT_LOCAL)
|
||||
if(EXISTS ${WEBKIT_PATH}/cmakeconfig.h)
|
||||
# You may need to run:
|
||||
# make jsc-compile-debug jsc-copy-headers
|
||||
include_directories(
|
||||
${WEBKIT_PATH}
|
||||
${WEBKIT_PATH}/JavaScriptCore/Headers
|
||||
${WEBKIT_PATH}/JavaScriptCore/Headers/JavaScriptCore
|
||||
${WEBKIT_PATH}/JavaScriptCore/PrivateHeaders
|
||||
${WEBKIT_PATH}/bmalloc/Headers
|
||||
${WEBKIT_PATH}/WTF/Headers
|
||||
${WEBKIT_PATH}/JavaScriptCore/PrivateHeaders/JavaScriptCore
|
||||
${WEBKIT_PATH}/JavaScriptCore/DerivedSources/inspector
|
||||
)
|
||||
set(WEBKIT_SOURCE_DIR ${VENDOR_PATH}/WebKit)
|
||||
|
||||
# On Windows, add ICU include path from vcpkg
|
||||
if(WIN32)
|
||||
# Auto-detect vcpkg triplet
|
||||
set(VCPKG_ARM64_PATH ${VENDOR_PATH}/WebKit/vcpkg_installed/arm64-windows-static)
|
||||
set(VCPKG_X64_PATH ${VENDOR_PATH}/WebKit/vcpkg_installed/x64-windows-static)
|
||||
if(EXISTS ${VCPKG_ARM64_PATH})
|
||||
set(VCPKG_ICU_PATH ${VCPKG_ARM64_PATH})
|
||||
if(WIN32)
|
||||
# --- Build ICU from source (Windows only) ---
|
||||
# On macOS, ICU is found automatically (Homebrew icu4c for headers, system for libs).
|
||||
# On Linux, ICU is found automatically from system packages (e.g. libicu-dev).
|
||||
# On Windows, there is no system ICU, so we build it from source.
|
||||
set(ICU_LOCAL_ROOT ${VENDOR_PATH}/WebKit/WebKitBuild/icu)
|
||||
if(NOT EXISTS ${ICU_LOCAL_ROOT}/lib/sicudt.lib)
|
||||
message(STATUS "Building ICU from source...")
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|ARM64|aarch64|AARCH64")
|
||||
set(ICU_PLATFORM "ARM64")
|
||||
else()
|
||||
set(VCPKG_ICU_PATH ${VCPKG_X64_PATH})
|
||||
set(ICU_PLATFORM "x64")
|
||||
endif()
|
||||
if(EXISTS ${VCPKG_ICU_PATH}/include)
|
||||
include_directories(${VCPKG_ICU_PATH}/include)
|
||||
message(STATUS "Using ICU from vcpkg: ${VCPKG_ICU_PATH}/include")
|
||||
execute_process(
|
||||
COMMAND powershell -ExecutionPolicy Bypass -File
|
||||
${WEBKIT_SOURCE_DIR}/build-icu.ps1
|
||||
-Platform ${ICU_PLATFORM}
|
||||
-BuildType ${WEBKIT_BUILD_TYPE}
|
||||
-OutputDir ${ICU_LOCAL_ROOT}
|
||||
RESULT_VARIABLE ICU_BUILD_RESULT
|
||||
)
|
||||
if(NOT ICU_BUILD_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to build ICU (exit code: ${ICU_BUILD_RESULT}).")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Copy ICU libs to WEBKIT_LIB_PATH with the names BuildBun.cmake expects.
|
||||
# Prebuilt WebKit uses 's' prefix (static) and 'd' suffix (debug).
|
||||
file(MAKE_DIRECTORY ${WEBKIT_LIB_PATH})
|
||||
if(WEBKIT_BUILD_TYPE STREQUAL "Debug")
|
||||
set(ICU_SUFFIX "d")
|
||||
else()
|
||||
set(ICU_SUFFIX "")
|
||||
endif()
|
||||
file(COPY_FILE ${ICU_LOCAL_ROOT}/lib/sicudt.lib ${WEBKIT_LIB_PATH}/sicudt${ICU_SUFFIX}.lib ONLY_IF_DIFFERENT)
|
||||
file(COPY_FILE ${ICU_LOCAL_ROOT}/lib/icuin.lib ${WEBKIT_LIB_PATH}/sicuin${ICU_SUFFIX}.lib ONLY_IF_DIFFERENT)
|
||||
file(COPY_FILE ${ICU_LOCAL_ROOT}/lib/icuuc.lib ${WEBKIT_LIB_PATH}/sicuuc${ICU_SUFFIX}.lib ONLY_IF_DIFFERENT)
|
||||
endif()
|
||||
|
||||
# --- Configure JSC ---
|
||||
message(STATUS "Configuring JSC from local WebKit source at ${WEBKIT_SOURCE_DIR}...")
|
||||
|
||||
set(JSC_CMAKE_ARGS
|
||||
-S ${WEBKIT_SOURCE_DIR}
|
||||
-B ${WEBKIT_PATH}
|
||||
-G ${CMAKE_GENERATOR}
|
||||
-DPORT=JSCOnly
|
||||
-DENABLE_STATIC_JSC=ON
|
||||
-DUSE_THIN_ARCHIVES=OFF
|
||||
-DENABLE_FTL_JIT=ON
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||
-DUSE_BUN_JSC_ADDITIONS=ON
|
||||
-DUSE_BUN_EVENT_LOOP=ON
|
||||
-DENABLE_BUN_SKIP_FAILING_ASSERTIONS=ON
|
||||
-DALLOW_LINE_AND_COLUMN_NUMBER_IN_BUILTINS=ON
|
||||
-DCMAKE_BUILD_TYPE=${WEBKIT_BUILD_TYPE}
|
||||
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
|
||||
-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
|
||||
-DENABLE_REMOTE_INSPECTOR=ON
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
# ICU paths and Windows-specific compiler/linker settings
|
||||
list(APPEND JSC_CMAKE_ARGS
|
||||
-DICU_ROOT=${ICU_LOCAL_ROOT}
|
||||
-DICU_LIBRARY=${ICU_LOCAL_ROOT}/lib
|
||||
-DICU_INCLUDE_DIR=${ICU_LOCAL_ROOT}/include
|
||||
-DCMAKE_LINKER=lld-link
|
||||
)
|
||||
# Static CRT and U_STATIC_IMPLEMENTATION
|
||||
if(WEBKIT_BUILD_TYPE STREQUAL "Debug")
|
||||
set(JSC_MSVC_RUNTIME "MultiThreadedDebug")
|
||||
else()
|
||||
set(JSC_MSVC_RUNTIME "MultiThreaded")
|
||||
endif()
|
||||
list(APPEND JSC_CMAKE_ARGS
|
||||
-DCMAKE_MSVC_RUNTIME_LIBRARY=${JSC_MSVC_RUNTIME}
|
||||
"-DCMAKE_C_FLAGS=/DU_STATIC_IMPLEMENTATION"
|
||||
"-DCMAKE_CXX_FLAGS=/DU_STATIC_IMPLEMENTATION /clang:-fno-c++-static-destructors"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(ENABLE_ASAN)
|
||||
list(APPEND JSC_CMAKE_ARGS -DENABLE_SANITIZERS=address)
|
||||
endif()
|
||||
|
||||
# Pass through ccache if available
|
||||
if(CMAKE_C_COMPILER_LAUNCHER)
|
||||
list(APPEND JSC_CMAKE_ARGS -DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER})
|
||||
endif()
|
||||
if(CMAKE_CXX_COMPILER_LAUNCHER)
|
||||
list(APPEND JSC_CMAKE_ARGS -DCMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER})
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} ${JSC_CMAKE_ARGS}
|
||||
RESULT_VARIABLE JSC_CONFIGURE_RESULT
|
||||
)
|
||||
if(NOT JSC_CONFIGURE_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to configure JSC (exit code: ${JSC_CONFIGURE_RESULT}). "
|
||||
"Check the output above for errors.")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
set(JSC_BYPRODUCTS
|
||||
${WEBKIT_LIB_PATH}/JavaScriptCore.lib
|
||||
${WEBKIT_LIB_PATH}/WTF.lib
|
||||
${WEBKIT_LIB_PATH}/bmalloc.lib
|
||||
)
|
||||
else()
|
||||
set(JSC_BYPRODUCTS
|
||||
${WEBKIT_LIB_PATH}/libJavaScriptCore.a
|
||||
${WEBKIT_LIB_PATH}/libWTF.a
|
||||
${WEBKIT_LIB_PATH}/libbmalloc.a
|
||||
)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
add_custom_target(jsc ALL
|
||||
COMMAND ${CMAKE_COMMAND} --build ${WEBKIT_PATH} --config ${WEBKIT_BUILD_TYPE} --target jsc
|
||||
BYPRODUCTS ${JSC_BYPRODUCTS}
|
||||
COMMENT "Building JSC (${WEBKIT_PATH})"
|
||||
)
|
||||
else()
|
||||
add_custom_target(jsc ALL
|
||||
COMMAND ${CMAKE_COMMAND} --build ${WEBKIT_PATH} --config ${WEBKIT_BUILD_TYPE} --target jsc
|
||||
BYPRODUCTS ${JSC_BYPRODUCTS}
|
||||
COMMENT "Building JSC (${WEBKIT_PATH})"
|
||||
USES_TERMINAL
|
||||
)
|
||||
endif()
|
||||
|
||||
include_directories(
|
||||
${WEBKIT_PATH}
|
||||
${WEBKIT_PATH}/JavaScriptCore/Headers
|
||||
${WEBKIT_PATH}/JavaScriptCore/Headers/JavaScriptCore
|
||||
${WEBKIT_PATH}/JavaScriptCore/PrivateHeaders
|
||||
${WEBKIT_PATH}/bmalloc/Headers
|
||||
${WEBKIT_PATH}/WTF/Headers
|
||||
${WEBKIT_PATH}/JavaScriptCore/PrivateHeaders/JavaScriptCore
|
||||
)
|
||||
|
||||
# On Windows, add ICU headers from the local ICU build
|
||||
if(WIN32)
|
||||
include_directories(${ICU_LOCAL_ROOT}/include)
|
||||
endif()
|
||||
|
||||
# After this point, only prebuilt WebKit is supported
|
||||
|
||||
@@ -7,9 +7,9 @@ Bytecode caching is a build-time optimization that dramatically improves applica
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic usage
|
||||
### Basic usage (CommonJS)
|
||||
|
||||
Enable bytecode caching with the `--bytecode` flag:
|
||||
Enable bytecode caching with the `--bytecode` flag. Without `--format`, this defaults to CommonJS:
|
||||
|
||||
```bash terminal icon="terminal"
|
||||
bun build ./index.ts --target=bun --bytecode --outdir=./dist
|
||||
@@ -17,7 +17,7 @@ bun build ./index.ts --target=bun --bytecode --outdir=./dist
|
||||
|
||||
This generates two files:
|
||||
|
||||
- `dist/index.js` - Your bundled JavaScript
|
||||
- `dist/index.js` - Your bundled JavaScript (CommonJS)
|
||||
- `dist/index.jsc` - The bytecode cache file
|
||||
|
||||
At runtime, Bun automatically detects and uses the `.jsc` file:
|
||||
@@ -28,14 +28,24 @@ bun ./dist/index.js # Automatically uses index.jsc
|
||||
|
||||
### With standalone executables
|
||||
|
||||
When creating executables with `--compile`, bytecode is embedded into the binary:
|
||||
When creating executables with `--compile`, bytecode is embedded into the binary. Both ESM and CommonJS formats are supported:
|
||||
|
||||
```bash terminal icon="terminal"
|
||||
# ESM (requires --compile)
|
||||
bun build ./cli.ts --compile --bytecode --format=esm --outfile=mycli
|
||||
|
||||
# CommonJS (works with or without --compile)
|
||||
bun build ./cli.ts --compile --bytecode --outfile=mycli
|
||||
```
|
||||
|
||||
The resulting executable contains both the code and bytecode, giving you maximum performance in a single file.
|
||||
|
||||
### ESM bytecode
|
||||
|
||||
ESM bytecode requires `--compile` because Bun embeds module metadata (import/export information) in the compiled binary. This metadata allows the JavaScript engine to skip parsing entirely at runtime.
|
||||
|
||||
Without `--compile`, ESM bytecode would still require parsing the source to analyze module dependencies—defeating the purpose of bytecode caching.
|
||||
|
||||
### Combining with other optimizations
|
||||
|
||||
Bytecode works great with minification and source maps:
|
||||
@@ -90,35 +100,9 @@ Larger applications benefit more because they have more code to parse.
|
||||
- ❌ **Code that runs once**
|
||||
- ❌ **Development builds**
|
||||
- ❌ **Size-constrained environments**
|
||||
- ❌ **Code with top-level await** (not supported)
|
||||
|
||||
## Limitations
|
||||
|
||||
### CommonJS only
|
||||
|
||||
Bytecode caching currently works with CommonJS output format. Bun's bundler automatically converts most ESM code to CommonJS, but **top-level await** is the exception:
|
||||
|
||||
```js
|
||||
// This prevents bytecode caching
|
||||
const data = await fetch("https://api.example.com");
|
||||
export default data;
|
||||
```
|
||||
|
||||
**Why**: Top-level await requires async module evaluation, which can't be represented in CommonJS. The module graph becomes asynchronous, and the CommonJS wrapper function model breaks down.
|
||||
|
||||
**Workaround**: Move async initialization into a function:
|
||||
|
||||
```js
|
||||
async function init() {
|
||||
const data = await fetch("https://api.example.com");
|
||||
return data;
|
||||
}
|
||||
|
||||
export default init;
|
||||
```
|
||||
|
||||
Now the module exports a function that the consumer can await when needed.
|
||||
|
||||
### Version compatibility
|
||||
|
||||
Bytecode is **not portable across Bun versions**. The bytecode format is tied to JavaScriptCore's internal representation, which changes between versions.
|
||||
@@ -236,8 +220,6 @@ It's normal for it it to log a cache miss multiple times since Bun doesn't curre
|
||||
- Compressing `.jsc` files for network transfer (gzip/brotli)
|
||||
- Evaluating if the startup performance gain is worth the size increase
|
||||
|
||||
**Top-level await**: Not supported. Refactor to use async initialization functions.
|
||||
|
||||
## What is bytecode?
|
||||
|
||||
When you run JavaScript, the JavaScript engine doesn't execute your source code directly. Instead, it goes through several steps:
|
||||
|
||||
@@ -322,10 +322,7 @@ Using bytecode compilation, `tsc` starts 2x faster:
|
||||
|
||||
Bytecode compilation moves parsing overhead for large input files from runtime to bundle time. Your app starts faster, in exchange for making the `bun build` command a little slower. It doesn't obscure source code.
|
||||
|
||||
<Warning>
|
||||
**Experimental:** Bytecode compilation is an experimental feature. Only `cjs` format is supported (which means no
|
||||
top-level-await). Let us know if you run into any issues!
|
||||
</Warning>
|
||||
<Note>Bytecode compilation supports both `cjs` and `esm` formats when used with `--compile`.</Note>
|
||||
|
||||
### What do these flags do?
|
||||
|
||||
|
||||
@@ -1508,22 +1508,43 @@ BuildArtifact (entry-point) {
|
||||
|
||||
## Bytecode
|
||||
|
||||
The `bytecode: boolean` option can be used to generate bytecode for any JavaScript/TypeScript entrypoints. This can greatly improve startup times for large applications. Only supported for `"cjs"` format, only supports `"target": "bun"` and dependent on a matching version of Bun. This adds a corresponding `.jsc` file for each entrypoint.
|
||||
The `bytecode: boolean` option can be used to generate bytecode for any JavaScript/TypeScript entrypoints. This can greatly improve startup times for large applications. Requires `"target": "bun"` and is dependent on a matching version of Bun.
|
||||
|
||||
- **CommonJS**: Works with or without `compile: true`. Generates a `.jsc` file alongside each entrypoint.
|
||||
- **ESM**: Requires `compile: true`. Bytecode and module metadata are embedded in the standalone executable.
|
||||
|
||||
Without an explicit `format`, bytecode defaults to CommonJS.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="JavaScript">
|
||||
```ts title="build.ts" icon="/icons/typescript.svg"
|
||||
// CommonJS bytecode (generates .jsc files)
|
||||
await Bun.build({
|
||||
entrypoints: ["./index.tsx"],
|
||||
outdir: "./out",
|
||||
bytecode: true,
|
||||
})
|
||||
|
||||
// ESM bytecode (requires compile)
|
||||
await Bun.build({
|
||||
entrypoints: ["./index.tsx"],
|
||||
outfile: "./mycli",
|
||||
bytecode: true,
|
||||
format: "esm",
|
||||
compile: true,
|
||||
})
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="CLI">
|
||||
```bash terminal icon="terminal"
|
||||
# CommonJS bytecode
|
||||
bun build ./index.tsx --outdir ./out --bytecode
|
||||
|
||||
# ESM bytecode (requires --compile)
|
||||
bun build ./index.tsx --outfile ./mycli --bytecode --format=esm --compile
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -1690,7 +1711,10 @@ interface BuildConfig {
|
||||
* start times, but will make the final output larger and slightly increase
|
||||
* memory usage.
|
||||
*
|
||||
* Bytecode is currently only supported for CommonJS (`format: "cjs"`).
|
||||
* - CommonJS: works with or without `compile: true`
|
||||
* - ESM: requires `compile: true`
|
||||
*
|
||||
* Without an explicit `format`, defaults to CommonJS.
|
||||
*
|
||||
* Must be `target: "bun"`
|
||||
* @default false
|
||||
|
||||
@@ -266,18 +266,13 @@ git clone https://github.com/oven-sh/WebKit vendor/WebKit
|
||||
# Check out the commit hash specified in `set(WEBKIT_VERSION <commit_hash>)` in cmake/tools/SetupWebKit.cmake
|
||||
git -C vendor/WebKit checkout <commit_hash>
|
||||
|
||||
# Make a debug build of JSC. This will output build artifacts in ./vendor/WebKit/WebKitBuild/Debug
|
||||
# Optionally, you can use `bun run jsc:build` for a release build
|
||||
bun run jsc:build:debug && rm vendor/WebKit/WebKitBuild/Debug/JavaScriptCore/DerivedSources/inspector/InspectorProtocolObjects.h
|
||||
|
||||
# After an initial run of `make jsc-debug`, you can rebuild JSC with:
|
||||
cmake --build vendor/WebKit/WebKitBuild/Debug --target jsc && rm vendor/WebKit/WebKitBuild/Debug/JavaScriptCore/DerivedSources/inspector/InspectorProtocolObjects.h
|
||||
|
||||
# Build bun with the local JSC build
|
||||
# Build bun with the local JSC build — this automatically configures and builds JSC
|
||||
bun run build:local
|
||||
```
|
||||
|
||||
Using `bun run build:local` will build Bun in the `./build/debug-local` directory (instead of `./build/debug`), you'll have to change a couple of places to use this new directory:
|
||||
`bun run build:local` handles everything: configuring JSC, building JSC, and building Bun. On subsequent runs, JSC will incrementally rebuild if any WebKit sources changed. `ninja -Cbuild/debug-local` also works after the first build, and will build Bun+JSC.
|
||||
|
||||
The build output goes to `./build/debug-local` (instead of `./build/debug`), so you'll need to update a couple of places:
|
||||
|
||||
- The first line in `src/js/builtins.d.ts`
|
||||
- The `CompilationDatabase` line in `.clangd` config should be `CompilationDatabase: build/debug-local`
|
||||
@@ -288,7 +283,7 @@ Note that the WebKit folder, including build artifacts, is 8GB+ in size.
|
||||
|
||||
If you are using a JSC debug build and using VScode, make sure to run the `C/C++: Select a Configuration` command to configure intellisense to find the debug headers.
|
||||
|
||||
Note that if you change make changes to our [WebKit fork](https://github.com/oven-sh/WebKit), you will also have to change `SetupWebKit.cmake` to point to the commit hash.
|
||||
Note that if you make changes to our [WebKit fork](https://github.com/oven-sh/WebKit), you will also have to change `SetupWebKit.cmake` to point to the commit hash.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ bun build <entry points>
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="--format" type="string" default="esm">
|
||||
Module format of the output bundle. One of <code>esm</code>, <code>cjs</code>, or <code>iife</code>
|
||||
Module format of the output bundle. One of <code>esm</code>, <code>cjs</code>, or <code>iife</code>. Defaults to{" "}
|
||||
<code>cjs</code> when <code>--bytecode</code> is used.
|
||||
</ParamField>
|
||||
|
||||
### File Naming
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "bun",
|
||||
"version": "1.3.8",
|
||||
"version": "1.3.9",
|
||||
"workspaces": [
|
||||
"./packages/bun-types",
|
||||
"./packages/@types/bun"
|
||||
|
||||
5
packages/bun-types/bun.d.ts
vendored
5
packages/bun-types/bun.d.ts
vendored
@@ -2594,7 +2594,10 @@ declare module "bun" {
|
||||
* start times, but will make the final output larger and slightly increase
|
||||
* memory usage.
|
||||
*
|
||||
* Bytecode is currently only supported for CommonJS (`format: "cjs"`).
|
||||
* - CommonJS: works with or without `compile: true`
|
||||
* - ESM: requires `compile: true`
|
||||
*
|
||||
* Without an explicit `format`, defaults to CommonJS.
|
||||
*
|
||||
* Must be `target: "bun"`
|
||||
* @default false
|
||||
|
||||
@@ -285,7 +285,9 @@ pub const Run = struct {
|
||||
.dir = cpu_prof_opts.dir,
|
||||
.md_format = cpu_prof_opts.md_format,
|
||||
.json_format = cpu_prof_opts.json_format,
|
||||
.interval = cpu_prof_opts.interval,
|
||||
};
|
||||
CPUProfiler.setSamplingInterval(cpu_prof_opts.interval);
|
||||
CPUProfiler.startCPUProfiler(vm.jsc_vm);
|
||||
bun.analytics.Features.cpu_profile += 1;
|
||||
}
|
||||
|
||||
@@ -1019,6 +1019,13 @@ pub const JSBundler = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// ESM bytecode requires compile because module_info (import/export metadata)
|
||||
// is only available in compiled binaries. Without it, JSC must parse the file
|
||||
// twice (once for module analysis, once for bytecode), which is a deopt.
|
||||
if (this.bytecode and this.format == .esm and this.compile == null) {
|
||||
return globalThis.throwInvalidArguments("ESM bytecode requires compile: true. Use format: 'cjs' for bytecode without compile.", .{});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
|
||||
extern "C" void Bun__startCPUProfiler(JSC::VM* vm);
|
||||
extern "C" void Bun__stopCPUProfiler(JSC::VM* vm, BunString* outJSON, BunString* outText);
|
||||
extern "C" void Bun__setSamplingInterval(int intervalMicroseconds);
|
||||
|
||||
void Bun__setSamplingInterval(int intervalMicroseconds)
|
||||
{
|
||||
Bun::setSamplingInterval(intervalMicroseconds);
|
||||
}
|
||||
|
||||
namespace Bun {
|
||||
|
||||
|
||||
@@ -3,11 +3,17 @@ pub const CPUProfilerConfig = struct {
|
||||
dir: []const u8,
|
||||
md_format: bool = false,
|
||||
json_format: bool = false,
|
||||
interval: u32 = 1000,
|
||||
};
|
||||
|
||||
// C++ function declarations
|
||||
extern fn Bun__startCPUProfiler(vm: *jsc.VM) void;
|
||||
extern fn Bun__stopCPUProfiler(vm: *jsc.VM, outJSON: ?*bun.String, outText: ?*bun.String) void;
|
||||
extern fn Bun__setSamplingInterval(intervalMicroseconds: c_int) void;
|
||||
|
||||
pub fn setSamplingInterval(interval: u32) void {
|
||||
Bun__setSamplingInterval(@intCast(interval));
|
||||
}
|
||||
|
||||
pub fn startCPUProfiler(vm: *jsc.VM) void {
|
||||
Bun__startCPUProfiler(vm);
|
||||
|
||||
@@ -649,6 +649,10 @@ pub const PathLike = union(enum) {
|
||||
const normal = path_handler.normalizeBuf(resolve, b, .windows);
|
||||
return strings.toKernel32Path(@alignCast(std.mem.bytesAsSlice(u16, buf)), normal);
|
||||
}
|
||||
// Handle "." specially since normalizeStringBuf strips it to an empty string
|
||||
if (s.len == 1 and s[0] == '.') {
|
||||
return strings.toKernel32Path(@alignCast(std.mem.bytesAsSlice(u16, buf)), ".");
|
||||
}
|
||||
const normal = path_handler.normalizeStringBuf(s, b, true, .windows, false);
|
||||
return strings.toKernel32Path(@alignCast(std.mem.bytesAsSlice(u16, buf)), normal);
|
||||
}
|
||||
|
||||
@@ -3134,7 +3134,7 @@ pub fn getStat(this: *Blob, globalThis: *jsc.JSGlobalObject, callback: *jsc.Call
|
||||
.encoded_slice = switch (path_like) {
|
||||
// it's already converted to utf8
|
||||
.encoded_slice => |slice| try slice.toOwned(bun.default_allocator),
|
||||
else => try ZigString.initUTF8(path_like.slice()).toSliceClone(bun.default_allocator),
|
||||
else => try ZigString.init(path_like.slice()).toSliceClone(bun.default_allocator),
|
||||
},
|
||||
},
|
||||
}, globalThis.bunVM());
|
||||
|
||||
@@ -263,7 +263,7 @@ pub const File = struct {
|
||||
.path = .{
|
||||
.encoded_slice = switch (path_like) {
|
||||
.encoded_slice => |slice| try slice.toOwned(bun.default_allocator),
|
||||
else => try jsc.ZigString.initUTF8(path_like.slice()).toSliceClone(bun.default_allocator),
|
||||
else => try jsc.ZigString.init(path_like.slice()).toSliceClone(bun.default_allocator),
|
||||
},
|
||||
},
|
||||
}, globalThis.bunVM()),
|
||||
|
||||
@@ -392,6 +392,7 @@ pub const Command = struct {
|
||||
enabled: bool = false,
|
||||
name: []const u8 = "",
|
||||
dir: []const u8 = "",
|
||||
interval: u32 = 1000,
|
||||
md_format: bool = false,
|
||||
json_format: bool = false,
|
||||
} = .{},
|
||||
|
||||
@@ -91,6 +91,7 @@ pub const runtime_params_ = [_]ParamType{
|
||||
clap.parseParam("--cpu-prof-name <STR> Specify the name of the CPU profile file") catch unreachable,
|
||||
clap.parseParam("--cpu-prof-dir <STR> Specify the directory where the CPU profile will be saved") catch unreachable,
|
||||
clap.parseParam("--cpu-prof-md Output CPU profile in markdown format (grep-friendly, designed for LLM analysis)") catch unreachable,
|
||||
clap.parseParam("--cpu-prof-interval <STR> Specify the sampling interval in microseconds for CPU profiling (default: 1000)") catch unreachable,
|
||||
clap.parseParam("--heap-prof Generate V8 heap snapshot on exit (.heapsnapshot)") catch unreachable,
|
||||
clap.parseParam("--heap-prof-name <STR> Specify the name of the heap profile file") catch unreachable,
|
||||
clap.parseParam("--heap-prof-dir <STR> Specify the directory where the heap profile will be saved") catch unreachable,
|
||||
@@ -178,7 +179,7 @@ pub const build_only_params = [_]ParamType{
|
||||
clap.parseParam("--sourcemap <STR>? Build with sourcemaps - 'linked', 'inline', 'external', or 'none'") catch unreachable,
|
||||
clap.parseParam("--banner <STR> Add a banner to the bundled output such as \"use client\"; for a bundle being used with RSCs") catch unreachable,
|
||||
clap.parseParam("--footer <STR> Add a footer to the bundled output such as // built with bun!") catch unreachable,
|
||||
clap.parseParam("--format <STR> Specifies the module format to build to. \"esm\", \"cjs\" and \"iife\" are supported. Defaults to \"esm\".") catch unreachable,
|
||||
clap.parseParam("--format <STR> Specifies the module format to build to. \"esm\", \"cjs\" and \"iife\" are supported. Defaults to \"esm\", or \"cjs\" with --bytecode.") catch unreachable,
|
||||
clap.parseParam("--root <STR> Root directory used for multiple entry points") catch unreachable,
|
||||
clap.parseParam("--splitting Enable code splitting") catch unreachable,
|
||||
clap.parseParam("--public-path <STR> A prefix to be appended to any import paths in bundled code") catch unreachable,
|
||||
@@ -864,6 +865,9 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
ctx.runtime_options.cpu_prof.md_format = cpu_prof_md_flag;
|
||||
// json_format is true if --cpu-prof is passed (regardless of --cpu-prof-md)
|
||||
ctx.runtime_options.cpu_prof.json_format = cpu_prof_flag;
|
||||
if (args.option("--cpu-prof-interval")) |interval_str| {
|
||||
ctx.runtime_options.cpu_prof.interval = std.fmt.parseInt(u32, interval_str, 10) catch 1000;
|
||||
}
|
||||
} else {
|
||||
// Warn if --cpu-prof-name or --cpu-prof-dir is used without a profiler flag
|
||||
if (args.option("--cpu-prof-name")) |_| {
|
||||
@@ -872,6 +876,9 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
if (args.option("--cpu-prof-dir")) |_| {
|
||||
Output.warn("--cpu-prof-dir requires --cpu-prof or --cpu-prof-md to be enabled", .{});
|
||||
}
|
||||
if (args.option("--cpu-prof-interval")) |_| {
|
||||
Output.warn("--cpu-prof-interval requires --cpu-prof or --cpu-prof-md to be enabled", .{});
|
||||
}
|
||||
}
|
||||
|
||||
const heap_prof_v8 = args.flag("--heap-prof");
|
||||
@@ -1339,10 +1346,18 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
}
|
||||
|
||||
ctx.bundler_options.output_format = format;
|
||||
// ESM bytecode is supported for --compile builds (module_info is embedded in binary)
|
||||
if (format != .cjs and format != .esm and ctx.bundler_options.bytecode) {
|
||||
Output.errGeneric("format must be 'cjs' or 'esm' when bytecode is true.", .{});
|
||||
Global.exit(1);
|
||||
if (ctx.bundler_options.bytecode) {
|
||||
if (format != .cjs and format != .esm) {
|
||||
Output.errGeneric("format must be 'cjs' or 'esm' when bytecode is true.", .{});
|
||||
Global.exit(1);
|
||||
}
|
||||
// ESM bytecode requires --compile because module_info (import/export metadata)
|
||||
// is only available in compiled binaries. Without it, JSC must parse the file
|
||||
// twice (once for module analysis, once for bytecode), which is a deopt.
|
||||
if (format == .esm and !ctx.bundler_options.compile) {
|
||||
Output.errGeneric("ESM bytecode requires --compile. Use --format=cjs for bytecode without --compile.", .{});
|
||||
Global.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -525,6 +525,40 @@ pub const UpdateInteractiveCommand = struct {
|
||||
try updatePackageJsonFilesFromUpdates(manager, package_updates.items);
|
||||
}
|
||||
|
||||
// Populate updating_packages with the selected packages so that
|
||||
// PackageManagerEnqueue.zig only updates these packages.
|
||||
// This is critical - without this, all packages would be updated.
|
||||
for (package_updates.items) |update| {
|
||||
const entry = bun.handleOom(manager.updating_packages.getOrPut(manager.allocator, update.name));
|
||||
if (!entry.found_existing) {
|
||||
entry.value_ptr.* = .{
|
||||
.original_version_literal = update.original_version,
|
||||
.is_alias = false,
|
||||
.original_version = null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Also populate for catalog updates
|
||||
if (has_catalog_updates) {
|
||||
var catalog_it = catalog_updates.iterator();
|
||||
while (catalog_it.next()) |entry| {
|
||||
const catalog_key = entry.key_ptr.*;
|
||||
// Parse catalog_key (format: "package_name" or "package_name:catalog_name")
|
||||
const colon_index = std.mem.indexOf(u8, catalog_key, ":");
|
||||
const package_name = if (colon_index) |idx| catalog_key[0..idx] else catalog_key;
|
||||
|
||||
const pkg_entry = bun.handleOom(manager.updating_packages.getOrPut(manager.allocator, package_name));
|
||||
if (!pkg_entry.found_existing) {
|
||||
pkg_entry.value_ptr.* = .{
|
||||
.original_version_literal = "",
|
||||
.is_alias = false,
|
||||
.original_version = null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manager.to_update = true;
|
||||
|
||||
// Reset the timer to show actual install time instead of total command time
|
||||
|
||||
@@ -1375,8 +1375,9 @@ fn getOrPutResolvedPackageWithFindResult(
|
||||
const should_update = this.to_update and
|
||||
// If updating, only update packages in the current workspace
|
||||
this.lockfile.isRootDependency(this, dependency_id) and
|
||||
// no need to do a look up if update requests are empty (`bun update` with no args)
|
||||
(this.update_requests.len == 0 or
|
||||
// Update all packages if both update_requests and updating_packages are empty (`bun update` with no args).
|
||||
// Otherwise, only update packages explicitly listed in updating_packages.
|
||||
((this.update_requests.len == 0 and this.updating_packages.count() == 0) or
|
||||
this.updating_packages.contains(dependency.name.slice(this.lockfile.buffers.string_bytes.items)));
|
||||
|
||||
// Was this package already allocated? Let's reuse the existing one.
|
||||
|
||||
@@ -280,6 +280,17 @@ pub const Interpreter = struct {
|
||||
exit_code: ?ExitCode = 0,
|
||||
this_jsvalue: JSValue = .zero,
|
||||
|
||||
/// Tracks which resources have been cleaned up to avoid double-free.
|
||||
/// When the interpreter finishes normally via finish(), it cleans up
|
||||
/// the runtime resources (IO, shell env) and sets this to .runtime_cleaned.
|
||||
/// The GC finalizer then only cleans up what remains (args, interpreter itself).
|
||||
cleanup_state: enum(u8) {
|
||||
/// Nothing has been cleaned up yet - need full cleanup
|
||||
needs_full_cleanup,
|
||||
/// Runtime resources (IO, shell env) have been cleaned up via finish()
|
||||
runtime_cleaned,
|
||||
} = .needs_full_cleanup,
|
||||
|
||||
__alloc_scope: if (bun.Environment.enableAllocScopes) bun.AllocationScope else void,
|
||||
estimated_size_for_gc: usize = 0,
|
||||
|
||||
@@ -1222,6 +1233,11 @@ pub const Interpreter = struct {
|
||||
}
|
||||
|
||||
fn #derefRootShellAndIOIfNeeded(this: *ThisInterpreter, free_buffered_io: bool) void {
|
||||
// Check if already cleaned up to prevent double-free
|
||||
if (this.cleanup_state == .runtime_cleaned) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (free_buffered_io) {
|
||||
// Can safely be called multiple times.
|
||||
if (this.root_shell._buffered_stderr == .owned) {
|
||||
@@ -1240,10 +1256,26 @@ pub const Interpreter = struct {
|
||||
}
|
||||
|
||||
this.this_jsvalue = .zero;
|
||||
// Mark that runtime resources have been cleaned up
|
||||
this.cleanup_state = .runtime_cleaned;
|
||||
}
|
||||
|
||||
fn deinitFromFinalizer(this: *ThisInterpreter) void {
|
||||
this.#derefRootShellAndIOIfNeeded(true);
|
||||
log("Interpreter(0x{x}) deinitFromFinalizer (cleanup_state={s})", .{ @intFromPtr(this), @tagName(this.cleanup_state) });
|
||||
|
||||
switch (this.cleanup_state) {
|
||||
.needs_full_cleanup => {
|
||||
// The interpreter never finished normally (e.g., early error or never started),
|
||||
// so we need to clean up IO and shell env here
|
||||
this.root_io.deref();
|
||||
this.root_shell.deinitImpl(false, true);
|
||||
},
|
||||
.runtime_cleaned => {
|
||||
// finish() already cleaned up IO and shell env via #derefRootShellAndIOIfNeeded,
|
||||
// nothing more to do for those resources
|
||||
},
|
||||
}
|
||||
|
||||
this.keep_alive.disable();
|
||||
this.args.deinit();
|
||||
this.allocator.destroy(this);
|
||||
|
||||
42
test/regression/issue/26631.test.ts
Normal file
42
test/regression/issue/26631.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import { exists, stat } from "node:fs/promises";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/26631
|
||||
// Path resolution fails for current directory '.' on Windows
|
||||
|
||||
test("existsSync('.') should return true", () => {
|
||||
expect(existsSync(".")).toBe(true);
|
||||
});
|
||||
|
||||
test("exists('.') should return true", async () => {
|
||||
expect(await exists(".")).toBe(true);
|
||||
});
|
||||
|
||||
test("statSync('.') should return directory stats", () => {
|
||||
const stats = statSync(".");
|
||||
expect(stats.isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
test("stat('.') should return directory stats", async () => {
|
||||
const stats = await stat(".");
|
||||
expect(stats.isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
test("existsSync('..') should return true", () => {
|
||||
expect(existsSync("..")).toBe(true);
|
||||
});
|
||||
|
||||
test("exists('..') should return true", async () => {
|
||||
expect(await exists("..")).toBe(true);
|
||||
});
|
||||
|
||||
test("statSync('..') should return directory stats", () => {
|
||||
const stats = statSync("..");
|
||||
expect(stats.isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
test("stat('..') should return directory stats", async () => {
|
||||
const stats = await stat("..");
|
||||
expect(stats.isDirectory()).toBe(true);
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { tempDir } from "harness";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/26647
|
||||
// Bun.file().stat() and Bun.file().delete() corrupt UTF-8 paths with non-ASCII
|
||||
// characters when the path is passed as a Buffer.
|
||||
|
||||
test("Bun.file() with Buffer path handles UTF-8 correctly for stat()", async () => {
|
||||
using dir = tempDir("test-26647", {
|
||||
"über.txt": "content",
|
||||
});
|
||||
|
||||
const filepath = `${dir}/über.txt`;
|
||||
|
||||
// Verify the file exists first using string path
|
||||
const bunFile1 = Bun.file(filepath);
|
||||
const stat1 = await bunFile1.stat();
|
||||
expect(stat1.size).toBe(7); // "content" is 7 bytes
|
||||
|
||||
// Now test with Buffer path - this was failing before the fix
|
||||
const bufPath = Buffer.from(filepath, "utf8");
|
||||
const bunFile2 = Bun.file(bufPath);
|
||||
const stat2 = await bunFile2.stat();
|
||||
expect(stat2.size).toBe(7);
|
||||
});
|
||||
|
||||
test("Bun.file() with Buffer path handles UTF-8 correctly for delete()", async () => {
|
||||
using dir = tempDir("test-26647", {
|
||||
"über.txt": "content",
|
||||
});
|
||||
|
||||
const filepath = `${dir}/über.txt`;
|
||||
|
||||
// Test delete() with Buffer path - this was failing before the fix
|
||||
const bufPath = Buffer.from(filepath, "utf8");
|
||||
const bunFile = Bun.file(bufPath);
|
||||
|
||||
// Verify file exists before delete
|
||||
const stat = await bunFile.stat();
|
||||
expect(stat.size).toBe(7);
|
||||
|
||||
// Delete should succeed
|
||||
await bunFile.delete();
|
||||
|
||||
// Verify file no longer exists
|
||||
const exists = await Bun.file(filepath).exists();
|
||||
expect(exists).toBe(false);
|
||||
});
|
||||
|
||||
test("Bun.file() with Buffer path handles various UTF-8 characters", async () => {
|
||||
using dir = tempDir("test-26647", {
|
||||
"日本語.txt": "japanese",
|
||||
"émoji🎉.txt": "emoji",
|
||||
"中文测试.txt": "chinese",
|
||||
});
|
||||
|
||||
// Test Japanese filename
|
||||
const jpPath = Buffer.from(`${dir}/日本語.txt`, "utf8");
|
||||
const jpStat = await Bun.file(jpPath).stat();
|
||||
expect(jpStat.size).toBe(8); // "japanese" is 8 bytes
|
||||
|
||||
// Test emoji filename
|
||||
const emojiPath = Buffer.from(`${dir}/émoji🎉.txt`, "utf8");
|
||||
const emojiStat = await Bun.file(emojiPath).stat();
|
||||
expect(emojiStat.size).toBe(5); // "emoji" is 5 bytes
|
||||
|
||||
// Test Chinese filename
|
||||
const cnPath = Buffer.from(`${dir}/中文测试.txt`, "utf8");
|
||||
const cnStat = await Bun.file(cnPath).stat();
|
||||
expect(cnStat.size).toBe(7); // "chinese" is 7 bytes
|
||||
});
|
||||
201
test/regression/issue/26758.test.ts
Normal file
201
test/regression/issue/26758.test.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
// Regression test for https://github.com/oven-sh/bun/issues/26758
|
||||
// bun update --interactive does not respect user selections - updates ALL packages
|
||||
// instead of only the selected ones.
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "fs";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
describe("issue #26758 - bun update --interactive respects selections", () => {
|
||||
test("should only update selected package to latest, not all packages", async () => {
|
||||
// Create a project with multiple outdated dependencies using exact versions.
|
||||
// The bug manifests when using --latest or 'l' to toggle latest in interactive mode.
|
||||
using dir = tempDir("update-interactive-26758", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-project",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
// Use exact versions - these have newer versions available
|
||||
lodash: "4.17.0", // 4.17.0 -> 4.17.23
|
||||
debug: "4.0.0", // 4.0.0 -> 4.4.3
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// First, run bun install to create initial node_modules and lockfile
|
||||
await using installProc = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const installExitCode = await installProc.exited;
|
||||
expect(installExitCode).toBe(0);
|
||||
|
||||
// Verify initial installation
|
||||
const initialLodashPkg = JSON.parse(
|
||||
readFileSync(join(String(dir), "node_modules", "lodash", "package.json"), "utf8"),
|
||||
);
|
||||
const initialDebugPkg = JSON.parse(
|
||||
readFileSync(join(String(dir), "node_modules", "debug", "package.json"), "utf8"),
|
||||
);
|
||||
expect(initialLodashPkg.version).toBe("4.17.0");
|
||||
expect(initialDebugPkg.version).toBe("4.0.0");
|
||||
|
||||
// Now run update --interactive
|
||||
// Select only lodash using 'l' (which toggles latest AND selects)
|
||||
// debug comes before lodash alphabetically
|
||||
await using updateProc = Bun.spawn({
|
||||
cmd: [bunExe(), "update", "--interactive"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
try {
|
||||
// Move down to lodash (second item) and use 'l' to toggle latest + select
|
||||
updateProc.stdin.write("j"); // Move down to lodash
|
||||
updateProc.stdin.write("l"); // Toggle latest (also selects)
|
||||
updateProc.stdin.write("\r"); // Enter to confirm
|
||||
updateProc.stdin.end();
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
updateProc.stdout.text(),
|
||||
updateProc.stderr.text(),
|
||||
updateProc.exited,
|
||||
]);
|
||||
|
||||
// Debug output if test fails
|
||||
if (exitCode !== 0) {
|
||||
console.log("STDOUT:", stdout);
|
||||
console.log("STDERR:", stderr);
|
||||
}
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check the installed versions
|
||||
const updatedLodashPkg = JSON.parse(
|
||||
readFileSync(join(String(dir), "node_modules", "lodash", "package.json"), "utf8"),
|
||||
);
|
||||
const updatedDebugPkg = JSON.parse(
|
||||
readFileSync(join(String(dir), "node_modules", "debug", "package.json"), "utf8"),
|
||||
);
|
||||
|
||||
// The SELECTED package (lodash) should be updated to latest
|
||||
expect(updatedLodashPkg.version).not.toBe("4.17.0");
|
||||
expect(Bun.semver.satisfies(updatedLodashPkg.version, ">4.17.0")).toBe(true);
|
||||
|
||||
// The UNSELECTED package (debug) should NOT be updated - THIS IS THE BUG FIX
|
||||
// Before the fix, debug would also be updated even though it wasn't selected
|
||||
expect(updatedDebugPkg.version).toBe("4.0.0");
|
||||
|
||||
// Verify package.json was only updated for the selected package
|
||||
const updatedPackageJson = JSON.parse(readFileSync(join(String(dir), "package.json"), "utf8"));
|
||||
expect(updatedPackageJson.dependencies["debug"]).toBe("4.0.0");
|
||||
expect(updatedPackageJson.dependencies["lodash"]).not.toBe("4.17.0");
|
||||
} catch (err) {
|
||||
updateProc.stdin.end();
|
||||
updateProc.kill();
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
test("should update multiple selected packages but not unselected ones", async () => {
|
||||
// Create a project with three outdated dependencies using exact versions
|
||||
using dir = tempDir("update-interactive-26758-multi", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-project",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
// Using exact versions
|
||||
chalk: "4.0.0", // 4.0.0 -> 5.x
|
||||
debug: "4.0.0", // 4.0.0 -> 4.4.3
|
||||
lodash: "4.17.0", // 4.17.0 -> 4.17.23
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// First install
|
||||
await using installProc = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const installExitCode = await installProc.exited;
|
||||
expect(installExitCode).toBe(0);
|
||||
|
||||
// Verify initial versions
|
||||
expect(JSON.parse(readFileSync(join(String(dir), "node_modules", "chalk", "package.json"), "utf8")).version).toBe(
|
||||
"4.0.0",
|
||||
);
|
||||
expect(JSON.parse(readFileSync(join(String(dir), "node_modules", "debug", "package.json"), "utf8")).version).toBe(
|
||||
"4.0.0",
|
||||
);
|
||||
expect(JSON.parse(readFileSync(join(String(dir), "node_modules", "lodash", "package.json"), "utf8")).version).toBe(
|
||||
"4.17.0",
|
||||
);
|
||||
|
||||
// Select chalk (first) and lodash (third), skip debug (second)
|
||||
// Packages sorted: chalk, debug, lodash
|
||||
await using updateProc = Bun.spawn({
|
||||
cmd: [bunExe(), "update", "--interactive"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
try {
|
||||
updateProc.stdin.write("l"); // Toggle latest + select chalk (cursor at first item)
|
||||
updateProc.stdin.write("j"); // Move down to debug
|
||||
// Don't select debug
|
||||
updateProc.stdin.write("j"); // Move down to lodash
|
||||
updateProc.stdin.write("l"); // Toggle latest + select lodash
|
||||
updateProc.stdin.write("\r"); // Confirm
|
||||
updateProc.stdin.end();
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
updateProc.stdout.text(),
|
||||
updateProc.stderr.text(),
|
||||
updateProc.exited,
|
||||
]);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
console.log("STDOUT:", stdout);
|
||||
console.log("STDERR:", stderr);
|
||||
}
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check updated versions
|
||||
const updatedChalkPkg = JSON.parse(
|
||||
readFileSync(join(String(dir), "node_modules", "chalk", "package.json"), "utf8"),
|
||||
);
|
||||
const updatedDebugPkg = JSON.parse(
|
||||
readFileSync(join(String(dir), "node_modules", "debug", "package.json"), "utf8"),
|
||||
);
|
||||
const updatedLodashPkg = JSON.parse(
|
||||
readFileSync(join(String(dir), "node_modules", "lodash", "package.json"), "utf8"),
|
||||
);
|
||||
|
||||
// Selected packages (chalk and lodash) should be updated
|
||||
expect(updatedChalkPkg.version).not.toBe("4.0.0");
|
||||
expect(updatedLodashPkg.version).not.toBe("4.17.0");
|
||||
|
||||
// Unselected package (debug) should NOT be updated - THIS IS THE BUG FIX
|
||||
expect(updatedDebugPkg.version).toBe("4.0.0");
|
||||
} catch (err) {
|
||||
updateProc.stdin.end();
|
||||
updateProc.kill();
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user