Compare commits

...

9 Commits

Author SHA1 Message Date
Claude Bot
9e71b8cd33 fix(require): prevent Object.prototype setters from triggering during module loading (#24336)
When `Object.prototype[0]` has a setter defined, `require('http')` and
`require('url')` would trigger it multiple times during module
initialization. Node.js does not exhibit this behavior.

Two root causes:

1. `createNodeURLBinding` in NodeURL.cpp used `putByIndexInline` to
   populate an array, which walks the prototype chain and triggers
   setters. Changed to `putDirectIndex` which sets properties directly.

2. TypeScript `const enum` declarations in `internal/http.ts` were being
   compiled to the standard enum IIFE pattern with reverse mappings
   (e.g. `Enum[Enum["x"] = 0] = "x"`), which writes to numeric indices
   on plain objects inheriting from `Object.prototype`. Replaced with
   plain `as const` object literals that don't generate reverse mappings.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 11:06:47 +00:00
Jarred Sumner
9fbe6a5826 Update standalone-html.mdx 2026-02-17 23:30:04 -08:00
Jarred Sumner
c0d97ebd88 Add docs for standalone HTML 2026-02-17 23:22:31 -08:00
robobun
0b580054a7 fix(stripANSI): validate SIMD candidates to prevent infinite loop on non-escape control chars (#27015)
## Summary
- Fix infinite loop in `Bun.stripANSI()` when input contains control
characters in the `0x10-0x1F` range that are not ANSI escape introducers
(e.g. `0x16` SYN, `0x19` EM)
- The SIMD fast-path in `findEscapeCharacter` matched the broad range
`0x10-0x1F` / `0x90-0x9F`, but `consumeANSI` only handles a subset of
those characters. When `consumeANSI` returned the same pointer for an
unrecognized byte, the main loop in `stripANSI` never advanced, causing
a hang in release builds and an assertion failure in debug builds.
- Fix verifies SIMD candidates through `isEscapeCharacter()` before
returning, matching the behavior the scalar fallback path already had

## Test plan
- [x] Added regression test in `test/regression/issue/27014.test.ts`
with 4 test cases
- [x] Verified test hangs with system bun (v1.3.9) confirming the bug
- [x] All 4 new tests pass with debug build
- [x] All 265 existing `stripANSI.test.ts` tests pass with debug build

Closes #27014

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 15:28:27 -08:00
robobun
b817abe55e feat(bundler): add --compile --target=browser for self-contained HTML output (#27056)
## Summary
- Adds self-contained HTML output mode: `--compile --target=browser`
(CLI) or `compile: true, target: "browser"` (`Bun.build()` API)
- Produces HTML files with all JS, CSS, and assets inlined directly:
`<script src="...">` → inline `<script>`, `<link rel="stylesheet">` →
inline `<style>`, asset references → `data:` URIs
- All entrypoints must be `.html` files when using `--compile
--target=browser`
- Validates: errors if any entrypoints aren't HTML, or if `--splitting`
is used
- Useful for distributing `.html` files that work via `file://` URLs
without needing a web server or worrying about CORS restrictions

## Test plan
- [x] Added `test/bundler/standalone.test.ts` covering:
  - Basic JS inlining into HTML
  - CSS inlining into HTML  
  - Combined JS + CSS inlining
  - Asset inlining as data URIs
  - CSS `url()` references inlined as data URIs
  - Validation: non-HTML entrypoints rejected
  - Validation: mixed HTML/non-HTML entrypoints rejected
  - Validation: splitting rejected
  - `Bun.build()` API with `compile: true, target: "browser"`
  - CLI `--compile --target=browser`
  - Minification works with compile+browser

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-17 15:27:36 -08:00
Jarred Sumner
9256b3d777 fix(Error): captureStackTrace with non-stack function returns proper stack string (#27017)
### What does this PR do?

When Error.captureStackTrace(e, fn) is called with a function that isn't
in the call stack, all frames are filtered out and e.stack should return
just the error name and message (e.g. "Error: test"), matching Node.js
behavior. Previously Bun returned undefined because:

1. The empty frame vector replaced the original stack frames via
setStackFrames(), but the lazy stack accessor was only installed when
hasMaterializedErrorInfo() was true (i.e. stack was previously
accessed). When it wasn't, JSC's internal materialization saw the
empty/null frames and produced no stack property at all.

2. The custom lazy getter returned undefined when stackTrace was
nullptr, instead of computing the error name+message string with zero
frames.

Fix: always force materialization before replacing frames, always
install the custom lazy accessor, and handle nullptr stackTrace in the
getter by computing the error string with an empty frame list.


### How did you verify your code works?

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-17 12:34:19 -08:00
SUZUKI Sosuke
6763fe5a8a fix: remove unsafe ObjectInitializationScope from fromEntries (#27088)
## Summary

Speculative fix for
[BUN-1K54](https://bun-p9.sentry.io/issues/7260165386/) — a segfault in
`JSC__JSValue__fromEntries` with 238 occurrences on Windows x86_64 (Bun
1.3.9).

## Problem

`JSC__JSValue__fromEntries` wraps its property insertion loop inside a
`JSC::ObjectInitializationScope`. This scope is designed for fast,
allocation-free object initialization using `putDirectOffset`. However,
the code uses `putDirect` (which can trigger structure transitions)
along with `toJSStringGC` and `toIdentifier` (which allocate on the GC
heap).

In **debug builds**, `ObjectInitializationScope` includes `AssertNoGC`
and `DisallowVMEntry` guards that would catch this misuse immediately.
In **release builds**, the scope is essentially a no-op (only emits a
`mutatorFence` on destruction), so GC can silently trigger during the
loop. When this happens, the GC may encounter partially-initialized
object slots containing garbage values, leading to a segfault.

## Fix

- Remove the `ObjectInitializationScope` block, since `putDirect` with
allocating helpers is incompatible with its contract.
- Add `RETURN_IF_EXCEPTION` checks inside each loop iteration to
properly propagate exceptions (e.g., OOM during string allocation).

## Test

Added a regression test that creates a `FileSystemRouter` with 128
routes and accesses `router.routes` under GC pressure. Verified via
temporary `fprintf` logging that the test exercises the modified
`JSC__JSValue__fromEntries` code path with `initialCapacity=128,
clone=true`.

Note: The original crash is a GC timing issue that cannot be
deterministically reproduced in a test. This test validates correctness
of the code path rather than reproducing the specific crash.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:30:09 -08:00
Alan Stott
7848648e09 fix: clean up raw TCP socket on TLS upgrade close (#26766)
## Summary

Fixes #12117, #24118, #25948

When a TCP socket is upgraded to TLS via `tls.connect({ socket })`,
`upgradeTLS()` creates **two** `TLSSocket` structs — a TLS wrapper and a
raw TCP wrapper. Both are `markActive()`'d and `ref()`'d. On close, uws
fires `onClose` through the **TLS context only**, so the TLS wrapper is
properly cleaned up, but the raw TCP wrapper's `onClose` never fires.
Its `has_pending_activity` stays `true` forever and its `ref_count` is
never decremented, **leaking one raw `TLSSocket` per upgrade cycle**.

This affects any code using the `tls.connect({ socket })` "starttls"
pattern:
- **MongoDB Node.js driver** — SDAM heartbeat connections cycle TLS
upgrades every ~10s, causing unbounded memory growth in production
- **mysql2** TLS upgrade path
- Any custom starttls implementation

### The fix

Adds a `defer` block in `NewWrappedHandler(true).onClose` that cleans up
the raw TCP socket when the TLS socket closes:

```zig
defer {
    if (!this.tcp.socket.isDetached()) {
        this.tcp.socket.detach();
        this.tcp.has_pending_activity.store(false, .release);
        this.tcp.deref();
    }
}
```

- **`isDetached()` guard** — skips cleanup if the raw socket was already
closed through another code path (e.g., JS-side `handle.close()`)
- **`socket.detach()`** — marks `InternalSocket` as `.detached` so
`isClosed()` returns `true` safely (the underlying `us_socket_t` is
freed when uws closes the TLS context)
- **`has_pending_activity.store(false)`** — allows JSC GC to collect the
raw socket's JS wrapper
- **`deref()`** — balances the `ref()` from `upgradeTLS`; the remaining
ref is the implicit one from JSC (`ref_count.init() == 1`). When GC
later calls `finalize()` → `deref()`, ref_count hits 0 and `deinit()`
runs the full cleanup chain (markInactive, handlers, poll_ref,
socket_context)

`markInactive()` is intentionally **not** called in the defer — it must
run inside `deinit()` to avoid double-freeing the handlers struct.

### Why Node.js doesn't have this bug

Node.js implements TLS upgrades purely in JavaScript/C++ with OpenSSL,
where the TLS wrapper takes ownership of the underlying socket. There is
no separate "raw socket wrapper" that needs independent cleanup.

## Test Results

### Regression test
```
$ bun test test/js/node/tls/node-tls-upgrade-leak.test.ts
 1 pass, 0 fail
```
Creates 20 TCP→TLS upgrade cycles, closes all connections, runs GC,
asserts `TLSSocket` count stays below 10.

### Existing TLS test suite (all passing)
```
node-tls-upgrade.test.ts      1 pass
node-tls-connect.test.ts     24 pass, 1 skip
node-tls-server.test.ts      21 pass
node-tls-cert.test.ts        25 pass, 3 todo
renegotiation.test.ts          6 pass
```

### MongoDB TLS scenario (patched Bun, 4 minutes)
```
Baseline: RSS=282.4 MB | Heap Used=26.4 MB
Check #4:  RSS=166.7 MB | Heap Used=24.2 MB — No TLSSocket growth. RSS DECREASED.
```

## Test plan

- [x] New regression test passes (`node-tls-upgrade-leak.test.ts`)
- [x] All existing TLS tests pass (upgrade, connect, server, cert,
renegotiation)
- [x] MongoDB TLS scenario shows zero `TLSSocket` accumulation
- [x] Node.js control confirms leak is Bun-specific
- [ ] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-16 23:42:57 -08:00
Jarred Sumner
379daff22d Fix test failure (#27073)
### What does this PR do?

### How did you verify your code works?

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-16 22:52:43 -08:00
32 changed files with 1777 additions and 202 deletions

View File

@@ -1184,7 +1184,8 @@ Currently, the `--compile` flag can only accept a single entrypoint at a time an
- `--outdir` — use `outfile` instead (except when using with `--splitting`).
- `--public-path`
- `--target=node` or `--target=browser`
- `--target=node`
- `--target=browser` (without HTML entrypoints — see [Standalone HTML](/bundler/standalone-html) for `--compile --target=browser` with `.html` files)
- `--no-bundle` - we always bundle everything into the executable.
---

View File

@@ -481,6 +481,16 @@ All paths are resolved relative to your HTML file, making it easy to organize yo
This is a small wrapper around Bun's support for HTML imports in JavaScript.
## Standalone HTML
You can bundle your entire frontend into a **single self-contained `.html` file** with no external dependencies using `--compile --target=browser`. All JavaScript, CSS, and images are inlined directly into the HTML.
```bash terminal icon="terminal"
bun build --compile --target=browser ./index.html --outdir=dist
```
Learn more in the [Standalone HTML docs](/bundler/standalone-html).
## Adding a backend to your frontend
To add a backend to your frontend, you can use the "routes" option in `Bun.serve`.

View File

@@ -0,0 +1,314 @@
---
title: Standalone HTML
description: Bundle a single-page app into a single self-contained .html file with no external dependencies
---
Bun can bundle your entire frontend into a **single `.html` file** with zero external dependencies. JavaScript, TypeScript, JSX, CSS, images, fonts, videos, WASM — everything gets inlined into one file.
```bash terminal icon="terminal"
bun build --compile --target=browser ./index.html --outdir=dist
```
The output is a completely self-contained HTML document. No relative paths. No external files. No server required. Just one `.html` file that works anywhere a browser can open it.
## One file. Upload anywhere. It just works.
The output is a single `.html` file you can put anywhere:
- **Upload it to S3** or any static file host — no directory structure to maintain, just one file
- **Double-click it from your desktop** — it opens in the browser and works offline, no localhost server needed
- **Embed it in your webview** — No need to deal with relative files
- **Insert it in an `<iframe>`** — embed interactive content in another page with a single file URL
- **Serve it from anywhere** — any HTTP server, CDN, or file share. One file, zero configuration.
There's nothing to install, no `node_modules` to deploy, no build artifacts to coordinate, no relative paths to think about. The entire app — framework code, stylesheets, images, everything — lives in that one file.
## Truly one file
Normally, distributing a web page means managing a folder of assets — the HTML, the JavaScript bundles, the CSS files, the images. Move the HTML without the rest and everything breaks. Browsers have tried to solve this before: Safari's `.webarchive` and `.mhtml` are supposed to save a page as a single file, but in practice they unpack into a folder of loose files on your computer — defeating the purpose.
Standalone HTML is different. The output is a plain `.html` file. Not an archive. Not a folder. One file, with everything inside it. Every image, every font, every line of CSS and JavaScript is embedded directly in the HTML using standard `<style>` tags, `<script>` tags, and `data:` URIs. Any browser can open it. Any server can host it. Any file host can store it.
This makes it practical to distribute web pages the same way you'd distribute a PDF — as a single file you can move, copy, upload, or share without worrying about broken paths or missing assets.
## Quick start
<CodeGroup>
```html index.html icon="file-code"
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div id="root"></div>
<script src="./app.tsx"></script>
</body>
</html>
```
```tsx app.tsx icon="/icons/typescript.svg"
import React from "react";
import { createRoot } from "react-dom/client";
function App() {
return <h1>Hello from a single HTML file!</h1>;
}
createRoot(document.getElementById("root")!).render(<App />);
```
```css styles.css icon="file-code"
body {
margin: 0;
font-family: system-ui, sans-serif;
background: #f5f5f5;
}
```
</CodeGroup>
```bash terminal icon="terminal"
bun build --compile --target=browser ./index.html --outdir=dist
```
Open `dist/index.html` — the React app works with no server.
## Everything gets inlined
Bun inlines every local asset it finds in your HTML. If it has a relative path, it gets embedded into the output file. This isn't limited to images and stylesheets — it works with any file type.
### What gets inlined
| In your source | In the output |
| ------------------------------------------------ | ------------------------------------------------------------------------ |
| `<script src="./app.tsx">` | `<script type="module">...bundled code...</script>` |
| `<link rel="stylesheet" href="./styles.css">` | `<style>...bundled CSS...</style>` |
| `<img src="./logo.png">` | `<img src="data:image/png;base64,...">` |
| `<img src="./icon.svg">` | `<img src="data:image/svg+xml;base64,...">` |
| `<video src="./demo.mp4">` | `<video src="data:video/mp4;base64,...">` |
| `<audio src="./click.wav">` | `<audio src="data:audio/wav;base64,...">` |
| `<source src="./clip.webm">` | `<source src="data:video/webm;base64,...">` |
| `<video poster="./thumb.jpg">` | `<video poster="data:image/jpeg;base64,...">` |
| `<link rel="icon" href="./favicon.ico">` | `<link rel="icon" href="data:image/x-icon;base64,...">` |
| `<link rel="manifest" href="./app.webmanifest">` | `<link rel="manifest" href="data:application/manifest+json;base64,...">` |
| CSS `url("./bg.png")` | CSS `url(data:image/png;base64,...)` |
| CSS `@import "./reset.css"` | Flattened into the `<style>` tag |
| CSS `url("./font.woff2")` | CSS `url(data:font/woff2;base64,...)` |
| JS `import "./styles.css"` | Merged into the `<style>` tag |
Images, fonts, WASM binaries, videos, audio files, SVGs — any file referenced by a relative path gets base64-encoded into a `data:` URI and embedded directly in the HTML. The MIME type is automatically detected from the file extension.
External URLs (like CDN links or absolute URLs) are left untouched.
## Using with React
React apps work out of the box. Bun handles JSX transpilation and npm package resolution automatically.
```bash terminal icon="terminal"
bun install react react-dom
```
<CodeGroup>
```html index.html icon="file-code"
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My App</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div id="root"></div>
<script src="./app.tsx"></script>
</body>
</html>
```
```tsx app.tsx icon="/icons/typescript.svg"
import React, { useState } from "react";
import { createRoot } from "react-dom/client";
import { Counter } from "./components/Counter.tsx";
function App() {
return (
<main>
<h1>Single-file React App</h1>
<Counter />
</main>
);
}
createRoot(document.getElementById("root")!).render(<App />);
```
```tsx components/Counter.tsx icon="/icons/typescript.svg"
import React, { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
```
</CodeGroup>
```bash terminal icon="terminal"
bun build --compile --target=browser ./index.html --outdir=dist
```
All of React, your components, and your CSS are bundled into `dist/index.html`. Upload that one file anywhere and it works.
## Using with Tailwind CSS
Install the plugin and reference Tailwind in your HTML or CSS:
```bash terminal icon="terminal"
bun install --dev bun-plugin-tailwind
```
<CodeGroup>
```html index.html icon="file-code"
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="tailwindcss" />
</head>
<body class="bg-gray-100 flex items-center justify-center min-h-screen">
<div id="root"></div>
<script src="./app.tsx"></script>
</body>
</html>
```
```tsx app.tsx icon="/icons/typescript.svg"
import React from "react";
import { createRoot } from "react-dom/client";
function App() {
return (
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md">
<h1 className="text-2xl font-bold text-gray-800">Hello Tailwind</h1>
<p className="text-gray-600 mt-2">This is a single HTML file.</p>
</div>
);
}
createRoot(document.getElementById("root")!).render(<App />);
```
</CodeGroup>
Build with the plugin using the JavaScript API:
```ts build.ts icon="/icons/typescript.svg"
await Bun.build({
entrypoints: ["./index.html"],
compile: true,
target: "browser",
outdir: "./dist",
plugins: [require("bun-plugin-tailwind")],
});
```
```bash terminal icon="terminal"
bun run build.ts
```
The generated Tailwind CSS is inlined directly into the HTML file as a `<style>` tag.
## How it works
When you pass `--compile --target=browser` with an HTML entrypoint, Bun:
1. Parses the HTML and discovers all `<script>`, `<link>`, `<img>`, `<video>`, `<audio>`, `<source>`, and other asset references
2. Bundles all JavaScript/TypeScript/JSX into a single module
3. Bundles all CSS (including `@import` chains and CSS imported from JS) into a single stylesheet
4. Converts every relative asset reference into a base64 `data:` URI
5. Inlines the bundled JS as `<script type="module">` before `</body>`
6. Inlines the bundled CSS as `<style>` in `<head>`
7. Outputs a single `.html` file with no external dependencies
## Minification
Add `--minify` to minify the JavaScript and CSS:
```bash terminal icon="terminal"
bun build --compile --target=browser --minify ./index.html --outdir=dist
```
Or via the API:
```ts build.ts icon="/icons/typescript.svg"
await Bun.build({
entrypoints: ["./index.html"],
compile: true,
target: "browser",
outdir: "./dist",
minify: true,
});
```
## JavaScript API
You can use `Bun.build()` to produce standalone HTML programmatically:
```ts build.ts icon="/icons/typescript.svg"
const result = await Bun.build({
entrypoints: ["./index.html"],
compile: true,
target: "browser",
outdir: "./dist", // optional — omit to get output as BuildArtifact
minify: true,
});
if (!result.success) {
console.error("Build failed:");
for (const log of result.logs) {
console.error(log);
}
} else {
console.log("Built:", result.outputs[0].path);
}
```
When `outdir` is omitted, the output is available as a `BuildArtifact` in `result.outputs`:
```ts icon="/icons/typescript.svg"
const result = await Bun.build({
entrypoints: ["./index.html"],
compile: true,
target: "browser",
});
const html = await result.outputs[0].text();
await Bun.write("output.html", html);
```
## Multiple HTML files
You can pass multiple HTML files as entrypoints. Each produces its own standalone HTML file:
```bash terminal icon="terminal"
bun build --compile --target=browser ./index.html ./about.html --outdir=dist
```
## Environment variables
Use `--env` to inline environment variables into the bundled JavaScript:
```bash terminal icon="terminal"
API_URL=https://api.example.com bun build --compile --target=browser --env=inline ./index.html --outdir=dist
```
References to `process.env.API_URL` in your JavaScript are replaced with the literal value at build time.
## Limitations
- **Code splitting** is not supported — `--splitting` cannot be used with `--compile --target=browser`
- **Large assets** increase file size since they're base64-encoded (33% overhead vs the raw binary)
- **External URLs** (CDN links, absolute URLs) are left as-is — only relative paths are inlined

View File

@@ -234,7 +234,7 @@
{
"group": "Asset Processing",
"icon": "image",
"pages": ["/bundler/html-static", "/bundler/css", "/bundler/loaders"]
"pages": ["/bundler/html-static", "/bundler/standalone-html", "/bundler/css", "/bundler/loaders"]
},
{
"group": "Single File Executable",

View File

@@ -2781,11 +2781,17 @@ declare module "bun" {
outdir?: string;
/**
* Create a standalone executable
* Create a standalone executable or self-contained HTML.
*
* When `true`, creates an executable for the current platform.
* When a target string, creates an executable for that platform.
*
* When used with `target: "browser"`, produces self-contained HTML files
* with all scripts, styles, and assets inlined. All `<script>` tags become
* inline `<script>` with bundled code, all `<link rel="stylesheet">` tags
* become inline `<style>` tags, and all asset references become `data:` URIs.
* All entrypoints must be HTML files. Cannot be used with `splitting`.
*
* @example
* ```ts
* // Create executable for current platform
@@ -2803,6 +2809,13 @@ declare module "bun" {
* compile: 'linux-x64',
* outfile: './my-app'
* });
*
* // Produce self-contained HTML
* await Bun.build({
* entrypoints: ['./index.html'],
* target: 'browser',
* compile: true,
* });
* ```
*/
compile?: boolean | Bun.Build.CompileTarget | CompileBuildOptions;

View File

@@ -183,13 +183,14 @@ pub fn addUrlForCss(
source: *const logger.Source,
mime_type_: ?[]const u8,
unique_key: ?[]const u8,
force_inline: bool,
) void {
{
const mime_type = if (mime_type_) |m| m else MimeType.byExtension(bun.strings.trimLeadingChar(std.fs.path.extension(source.path.text), '.')).value;
const contents = source.contents;
// TODO: make this configurable
const COPY_THRESHOLD = 128 * 1024; // 128kb
const should_copy = contents.len >= COPY_THRESHOLD and unique_key != null;
const should_copy = !force_inline and contents.len >= COPY_THRESHOLD and unique_key != null;
if (should_copy) return;
this.url_for_css = url_for_css: {

View File

@@ -977,45 +977,57 @@ pub const JSBundler = struct {
}
if (this.compile) |*compile| {
this.target = .bun;
// When compile + target=browser + all HTML entrypoints, produce standalone HTML.
// Otherwise, default to bun executable compile.
const has_all_html_entrypoints = brk: {
if (this.entry_points.count() == 0) break :brk false;
for (this.entry_points.keys()) |ep| {
if (!strings.hasSuffixComptime(ep, ".html")) break :brk false;
}
break :brk true;
};
const is_standalone_html = this.target == .browser and has_all_html_entrypoints;
if (!is_standalone_html) {
this.target = .bun;
const define_keys = compile.compile_target.defineKeys();
const define_values = compile.compile_target.defineValues();
for (define_keys, define_values) |key, value| {
try this.define.insert(key, value);
}
const base_public_path = bun.StandaloneModuleGraph.targetBasePublicPath(this.compile.?.compile_target.os, "root/");
try this.public_path.append(base_public_path);
// When using --compile, only `external` sourcemaps work, as we do not
// look at the source map comment. Override any other sourcemap type.
if (this.source_map != .none) {
this.source_map = .external;
}
if (compile.outfile.isEmpty()) {
const entry_point = this.entry_points.keys()[0];
var outfile = std.fs.path.basename(entry_point);
const ext = std.fs.path.extension(outfile);
if (ext.len > 0) {
outfile = outfile[0 .. outfile.len - ext.len];
const define_keys = compile.compile_target.defineKeys();
const define_values = compile.compile_target.defineValues();
for (define_keys, define_values) |key, value| {
try this.define.insert(key, value);
}
if (strings.eqlComptime(outfile, "index")) {
outfile = std.fs.path.basename(std.fs.path.dirname(entry_point) orelse "index");
const base_public_path = bun.StandaloneModuleGraph.targetBasePublicPath(this.compile.?.compile_target.os, "root/");
try this.public_path.append(base_public_path);
// When using --compile, only `external` sourcemaps work, as we do not
// look at the source map comment. Override any other sourcemap type.
if (this.source_map != .none) {
this.source_map = .external;
}
if (strings.eqlComptime(outfile, "bun")) {
outfile = std.fs.path.basename(std.fs.path.dirname(entry_point) orelse "bun");
}
if (compile.outfile.isEmpty()) {
const entry_point = this.entry_points.keys()[0];
var outfile = std.fs.path.basename(entry_point);
const ext = std.fs.path.extension(outfile);
if (ext.len > 0) {
outfile = outfile[0 .. outfile.len - ext.len];
}
// If argv[0] is "bun" or "bunx", we don't check if the binary is standalone
if (strings.eqlComptime(outfile, "bun") or strings.eqlComptime(outfile, "bunx")) {
return globalThis.throwInvalidArguments("cannot use compile with an output file named 'bun' because bun won't realize it's a standalone executable. Please choose a different name for compile.outfile", .{});
}
if (strings.eqlComptime(outfile, "index")) {
outfile = std.fs.path.basename(std.fs.path.dirname(entry_point) orelse "index");
}
try compile.outfile.appendSliceExact(outfile);
if (strings.eqlComptime(outfile, "bun")) {
outfile = std.fs.path.basename(std.fs.path.dirname(entry_point) orelse "bun");
}
// If argv[0] is "bun" or "bunx", we don't check if the binary is standalone
if (strings.eqlComptime(outfile, "bun") or strings.eqlComptime(outfile, "bunx")) {
return globalThis.throwInvalidArguments("cannot use compile with an output file named 'bun' because bun won't realize it's a standalone executable. Please choose a different name for compile.outfile", .{});
}
try compile.outfile.appendSliceExact(outfile);
}
}
}
@@ -1026,6 +1038,20 @@ pub const JSBundler = struct {
return globalThis.throwInvalidArguments("ESM bytecode requires compile: true. Use format: 'cjs' for bytecode without compile.", .{});
}
// Validate standalone HTML mode: compile + browser target + all HTML entrypoints
if (this.compile != null and this.target == .browser) {
const has_all_html = brk: {
if (this.entry_points.count() == 0) break :brk false;
for (this.entry_points.keys()) |ep| {
if (!strings.hasSuffixComptime(ep, ".html")) break :brk false;
}
break :brk true;
};
if (has_all_html and this.code_splitting) {
return globalThis.throwInvalidArguments("Cannot use compile with target 'browser' and splitting for standalone HTML", .{});
}
}
return this;
}

View File

@@ -1707,6 +1707,15 @@ pub fn NewWrappedHandler(comptime tls: bool) type {
pub fn onClose(this: WrappedSocket, socket: Socket, err: c_int, data: ?*anyopaque) bun.JSError!void {
if (comptime tls) {
// Clean up the raw TCP socket from upgradeTLS() — its onClose
// never fires because uws closes through the TLS context only.
defer {
if (!this.tcp.socket.isDetached()) {
this.tcp.socket.detach();
this.tcp.has_pending_activity.store(false, .release);
this.tcp.deref();
}
}
try TLSSocket.onClose(this.tls, socket, err, data);
} else {
try TLSSocket.onClose(this.tcp, socket, err, data);

View File

@@ -24,6 +24,18 @@ static inline bool isEscapeCharacter(Char c)
}
}
// SIMD comparison against exact escape character values. Used to refine
// the broad range match (0x10-0x1F / 0x90-0x9F) to only actual escape
// introducers: 0x1B, 0x90, 0x98, 0x9B, 0x9D, 0x9E, 0x9F.
template<typename SIMDType>
static auto exactEscapeMatch(std::conditional_t<sizeof(SIMDType) == 1, simde_uint8x16_t, simde_uint16x8_t> chunk)
{
if constexpr (sizeof(SIMDType) == 1)
return SIMD::equal<0x1b, 0x90, 0x98, 0x9b, 0x9d, 0x9e, 0x9f>(chunk);
else
return SIMD::equal<u'\x1b', u'\x90', u'\x98', u'\x9b', u'\x9d', u'\x9e', u'\x9f'>(chunk);
}
// Find the first escape character in a string using SIMD
template<typename Char>
static const Char* findEscapeCharacter(const Char* start, const Char* end)
@@ -43,8 +55,13 @@ static const Char* findEscapeCharacter(const Char* start, const Char* end)
const auto chunk = SIMD::load(reinterpret_cast<const SIMDType*>(it));
const auto chunkMasked = SIMD::bitAnd(chunk, escMask);
const auto chunkIsEsc = SIMD::equal(chunkMasked, escVector);
if (const auto index = SIMD::findFirstNonZeroIndex(chunkIsEsc))
return it + *index;
if (SIMD::findFirstNonZeroIndex(chunkIsEsc)) {
// Broad mask matched 0x10-0x1F / 0x90-0x9F. Refine with exact
// escape character comparison to filter out false positives.
const auto exactMatch = exactEscapeMatch<SIMDType>(chunk);
if (const auto exactIndex = SIMD::findFirstNonZeroIndex(exactMatch))
return it + *exactIndex;
}
}
// Check remaining characters

View File

@@ -641,13 +641,16 @@ JSC_DEFINE_CUSTOM_GETTER(errorInstanceLazyStackCustomGetter, (JSGlobalObject * g
OrdinalNumber column;
String sourceURL;
auto stackTrace = errorObject->stackTrace();
if (stackTrace == nullptr) {
return JSValue::encode(jsUndefined());
}
JSValue result = computeErrorInfoToJSValue(vm, *stackTrace, line, column, sourceURL, errorObject, nullptr);
stackTrace->clear();
errorObject->setStackFrames(vm, {});
JSValue result;
if (stackTrace == nullptr) {
WTF::Vector<JSC::StackFrame> emptyTrace;
result = computeErrorInfoToJSValue(vm, emptyTrace, line, column, sourceURL, errorObject, nullptr);
} else {
result = computeErrorInfoToJSValue(vm, *stackTrace, line, column, sourceURL, errorObject, nullptr);
stackTrace->clear();
errorObject->setStackFrames(vm, {});
}
RETURN_IF_EXCEPTION(scope, {});
errorObject->putDirect(vm, vm.propertyNames->stack, result, 0);
return JSValue::encode(result);
@@ -687,17 +690,27 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb
JSCStackTrace::getFramesForCaller(vm, callFrame, errorObject, caller, stackTrace, stackTraceLimit);
if (auto* instance = jsDynamicCast<JSC::ErrorInstance*>(errorObject)) {
// Force materialization before replacing the stack frames, so that JSC's
// internal lazy error info mechanism doesn't later see the replaced (possibly empty)
// stack trace and fail to create the stack property.
if (!instance->hasMaterializedErrorInfo())
instance->materializeErrorInfoIfNeeded(vm);
RETURN_IF_EXCEPTION(scope, {});
instance->setStackFrames(vm, WTF::move(stackTrace));
if (instance->hasMaterializedErrorInfo()) {
{
const auto& propertyName = vm.propertyNames->stack;
VM::DeletePropertyModeScope scope(vm, VM::DeletePropertyMode::IgnoreConfigurable);
VM::DeletePropertyModeScope deleteScope(vm, VM::DeletePropertyMode::IgnoreConfigurable);
DeletePropertySlot slot;
JSObject::deleteProperty(instance, globalObject, propertyName, slot);
if (auto* zigGlobalObject = jsDynamicCast<Zig::GlobalObject*>(globalObject)) {
instance->putDirectCustomAccessor(vm, vm.propertyNames->stack, zigGlobalObject->m_lazyStackCustomGetterSetter.get(zigGlobalObject), JSC::PropertyAttribute::CustomAccessor | 0);
} else {
instance->putDirectCustomAccessor(vm, vm.propertyNames->stack, CustomGetterSetter::create(vm, errorInstanceLazyStackCustomGetter, errorInstanceLazyStackCustomSetter), JSC::PropertyAttribute::CustomAccessor | 0);
}
}
RETURN_IF_EXCEPTION(scope, {});
if (auto* zigGlobalObject = jsDynamicCast<Zig::GlobalObject*>(globalObject)) {
instance->putDirectCustomAccessor(vm, vm.propertyNames->stack, zigGlobalObject->m_lazyStackCustomGetterSetter.get(zigGlobalObject), JSC::PropertyAttribute::CustomAccessor | 0);
} else {
instance->putDirectCustomAccessor(vm, vm.propertyNames->stack, CustomGetterSetter::create(vm, errorInstanceLazyStackCustomGetter, errorInstanceLazyStackCustomSetter), JSC::PropertyAttribute::CustomAccessor | 0);
}
} else {
OrdinalNumber line;

View File

@@ -152,16 +152,18 @@ JSC::JSValue createNodeURLBinding(Zig::GlobalObject* globalObject)
ASSERT(domainToAsciiFunction);
auto domainToUnicodeFunction = JSC::JSFunction::create(vm, globalObject, 1, "domainToUnicode"_s, jsDomainToUnicode, ImplementationVisibility::Public);
ASSERT(domainToUnicodeFunction);
binding->putByIndexInline(
binding->putDirectIndex(
globalObject,
(unsigned)0,
domainToAsciiFunction,
false);
binding->putByIndexInline(
0,
JSC::PutDirectIndexMode::PutDirectIndexLikePutDirect);
binding->putDirectIndex(
globalObject,
(unsigned)1,
domainToUnicodeFunction,
false);
0,
JSC::PutDirectIndexMode::PutDirectIndexLikePutDirect);
return binding;
}

View File

@@ -3026,22 +3026,20 @@ JSC::EncodedJSValue JSC__JSValue__fromEntries(JSC::JSGlobalObject* globalObject,
return JSC::JSValue::encode(JSC::constructEmptyObject(globalObject));
}
JSC::JSObject* object = nullptr;
{
JSC::ObjectInitializationScope initializationScope(vm);
object = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min(static_cast<unsigned int>(initialCapacity), JSFinalObject::maxInlineCapacity));
JSC::JSObject* object = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min(static_cast<unsigned int>(initialCapacity), JSFinalObject::maxInlineCapacity));
if (!clone) {
for (size_t i = 0; i < initialCapacity; ++i) {
object->putDirect(
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, Zig::toString(keys[i]))),
Zig::toJSStringGC(values[i], globalObject), 0);
}
} else {
for (size_t i = 0; i < initialCapacity; ++i) {
object->putDirect(vm, JSC::PropertyName(Zig::toIdentifier(keys[i], globalObject)),
Zig::toJSStringGC(values[i], globalObject), 0);
}
if (!clone) {
for (size_t i = 0; i < initialCapacity; ++i) {
object->putDirect(
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, Zig::toString(keys[i]))),
Zig::toJSStringGC(values[i], globalObject), 0);
RETURN_IF_EXCEPTION(scope, {});
}
} else {
for (size_t i = 0; i < initialCapacity; ++i) {
object->putDirect(vm, JSC::PropertyName(Zig::toIdentifier(keys[i], globalObject)),
Zig::toJSStringGC(values[i], globalObject), 0);
RETURN_IF_EXCEPTION(scope, {});
}
}

View File

@@ -55,6 +55,16 @@ pub const Chunk = struct {
return this.entry_point.is_entry_point;
}
/// Returns the HTML closing tag that must be escaped when this chunk's content
/// is inlined into a standalone HTML file (e.g. "</script" for JS, "</style" for CSS).
pub fn closingTagForContent(this: *const Chunk) []const u8 {
return switch (this.content) {
.javascript => "</script",
.css => "</style",
.html => unreachable,
};
}
pub fn getJSChunkForHTML(this: *const Chunk, chunks: []Chunk) ?*Chunk {
const entry_point_id = this.entry_point.entry_point_id;
for (chunks) |*other| {
@@ -137,6 +147,54 @@ pub const Chunk = struct {
return bun.default_allocator;
}
/// Count occurrences of a closing HTML tag (e.g. `</script`, `</style`) in content.
/// Used to calculate the extra bytes needed when escaping `</` → `<\/`.
fn countClosingTags(content: []const u8, close_tag: []const u8) usize {
const tag_suffix = close_tag[2..];
var count: usize = 0;
var remaining = content;
while (strings.indexOf(remaining, "</")) |idx| {
remaining = remaining[idx + 2 ..];
if (remaining.len >= tag_suffix.len and
strings.eqlCaseInsensitiveASCIIIgnoreLength(remaining[0..tag_suffix.len], tag_suffix))
{
count += 1;
remaining = remaining[tag_suffix.len..];
}
}
return count;
}
/// Copy `content` into `dest`, escaping occurrences of `close_tag` by
/// replacing `</` with `<\/`. Returns the number of bytes written.
/// Caller must ensure `dest` has room for `content.len + countClosingTags(...)` bytes.
fn memcpyEscapingClosingTags(dest: []u8, content: []const u8, close_tag: []const u8) usize {
const tag_suffix = close_tag[2..];
var remaining = content;
var dst: usize = 0;
while (strings.indexOf(remaining, "</")) |idx| {
@memcpy(dest[dst..][0..idx], remaining[0..idx]);
dst += idx;
remaining = remaining[idx + 2 ..];
if (remaining.len >= tag_suffix.len and
strings.eqlCaseInsensitiveASCIIIgnoreLength(remaining[0..tag_suffix.len], tag_suffix))
{
dest[dst] = '<';
dest[dst + 1] = '\\';
dest[dst + 2] = '/';
dst += 3;
} else {
dest[dst] = '<';
dest[dst + 1] = '/';
dst += 2;
}
}
@memcpy(dest[dst..][0..remaining.len], remaining);
dst += remaining.len;
return dst;
}
pub const CodeResult = struct {
buffer: []u8,
shifts: []SourceMap.SourceMapShifts,
@@ -179,6 +237,40 @@ pub const Chunk = struct {
display_size,
force_absolute_path,
source_map_shifts,
null,
),
};
}
/// Like `code()` but with standalone HTML support.
/// When `standalone_chunk_contents` is provided, chunk piece references are
/// resolved to inline code content instead of file paths. Asset references
/// are resolved to data: URIs from url_for_css.
pub fn codeStandalone(
this: *IntermediateOutput,
allocator_to_use: ?std.mem.Allocator,
parse_graph: *const Graph,
linker_graph: *const LinkerGraph,
import_prefix: []const u8,
chunk: *Chunk,
chunks: []Chunk,
display_size: ?*usize,
force_absolute_path: bool,
enable_source_map_shifts: bool,
standalone_chunk_contents: []const ?[]const u8,
) bun.OOM!CodeResult {
return switch (enable_source_map_shifts) {
inline else => |source_map_shifts| this.codeWithSourceMapShifts(
allocator_to_use,
parse_graph,
linker_graph,
import_prefix,
chunk,
chunks,
display_size,
force_absolute_path,
source_map_shifts,
standalone_chunk_contents,
),
};
}
@@ -194,6 +286,7 @@ pub const Chunk = struct {
display_size: ?*usize,
force_absolute_path: bool,
comptime enable_source_map_shifts: bool,
standalone_chunk_contents: ?[]const ?[]const u8,
) bun.OOM!CodeResult {
const additional_files = graph.input_files.items(.additional_files);
const unique_key_for_additional_files = graph.input_files.items(.unique_key_for_additional_file);
@@ -219,12 +312,37 @@ pub const Chunk = struct {
if (strings.eqlComptime(from_chunk_dir, "."))
from_chunk_dir = "";
const urls_for_css = if (standalone_chunk_contents != null) graph.ast.items(.url_for_css) else &[_][]const u8{};
for (pieces.slice()) |piece| {
count += piece.data_len;
switch (piece.query.kind) {
.chunk, .asset, .scb, .html_import => {
const index = piece.query.index;
// In standalone mode, inline chunk content and asset data URIs
if (standalone_chunk_contents) |scc| {
switch (piece.query.kind) {
.chunk => {
if (scc[index]) |content| {
// Account for escaping </script or </style inside inline content.
// Each occurrence of the closing tag adds 1 byte (`</` → `<\/`).
count += content.len + countClosingTags(content, chunks[index].closingTagForContent());
continue;
}
},
.asset => {
// Use data: URI from url_for_css if available
if (index < urls_for_css.len and urls_for_css[index].len > 0) {
count += urls_for_css[index].len;
continue;
}
},
else => {},
}
}
const file_path = switch (piece.query.kind) {
.asset => brk: {
const files = additional_files[index];
@@ -293,6 +411,37 @@ pub const Chunk = struct {
switch (piece.query.kind) {
.asset, .chunk, .scb, .html_import => {
const index = piece.query.index;
// In standalone mode, inline chunk content and asset data URIs
if (standalone_chunk_contents) |scc| {
const inline_content: ?[]const u8 = switch (piece.query.kind) {
.chunk => scc[index],
.asset => if (index < urls_for_css.len and urls_for_css[index].len > 0) urls_for_css[index] else null,
else => null,
};
if (inline_content) |content| {
if (enable_source_map_shifts) {
switch (piece.query.kind) {
.chunk => shift.before.advance(chunks[index].unique_key),
.asset => shift.before.advance(unique_key_for_additional_files[index]),
else => {},
}
shift.after.advance(content);
shifts.appendAssumeCapacity(shift);
}
// For chunk content, escape closing tags (</script, </style)
// that would prematurely terminate the inline tag.
if (piece.query.kind == .chunk) {
const written = memcpyEscapingClosingTags(remain, content, chunks[index].closingTagForContent());
remain = remain[written..];
} else {
@memcpy(remain[0..content.len], content);
remain = remain[content.len..];
}
continue;
}
}
const file_path = switch (piece.query.kind) {
.asset => brk: {
const files = additional_files[index];

View File

@@ -68,6 +68,7 @@ pub const LinkerContext = struct {
banner: []const u8 = "",
footer: []const u8 = "",
css_chunking: bool = false,
compile_to_standalone_html: bool = false,
source_maps: options.SourceMapOption = .none,
target: options.Target = .browser,
compile: bool = false,

View File

@@ -378,7 +378,7 @@ fn getAST(
.data = source.contents,
}, Logger.Loc{ .start = 0 });
var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, source, "")).?);
ast.addUrlForCss(allocator, source, "text/plain", null);
ast.addUrlForCss(allocator, source, "text/plain", null, transpiler.options.compile_to_standalone_html);
return ast;
},
.md => {
@@ -394,7 +394,7 @@ fn getAST(
.data = html,
}, Logger.Loc{ .start = 0 });
var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, source, "")).?);
ast.addUrlForCss(allocator, source, "text/html", null);
ast.addUrlForCss(allocator, source, "text/html", null, transpiler.options.compile_to_standalone_html);
return ast;
},
@@ -645,7 +645,7 @@ fn getAST(
.content_hash = content_hash,
};
var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, source, "")).?);
ast.addUrlForCss(allocator, source, null, unique_key);
ast.addUrlForCss(allocator, source, null, unique_key, transpiler.options.compile_to_standalone_html);
return ast;
},
}

View File

@@ -965,6 +965,7 @@ pub const BundleV2 = struct {
this.linker.options.banner = transpiler.options.banner;
this.linker.options.footer = transpiler.options.footer;
this.linker.options.css_chunking = transpiler.options.css_chunking;
this.linker.options.compile_to_standalone_html = transpiler.options.compile_to_standalone_html;
this.linker.options.source_maps = transpiler.options.source_map;
this.linker.options.tree_shaking = transpiler.options.tree_shaking;
this.linker.options.public_path = transpiler.options.public_path;
@@ -1992,6 +1993,19 @@ pub const BundleV2 = struct {
transpiler.options.emit_dce_annotations = config.emit_dce_annotations orelse !config.minify.whitespace;
transpiler.options.ignore_dce_annotations = config.ignore_dce_annotations;
transpiler.options.css_chunking = config.css_chunking;
transpiler.options.compile_to_standalone_html = brk: {
if (config.compile == null or config.target != .browser) break :brk false;
// Only activate standalone HTML when all entrypoints are HTML files
for (config.entry_points.keys()) |ep| {
if (!bun.strings.hasSuffixComptime(ep, ".html")) break :brk false;
}
break :brk config.entry_points.count() > 0;
};
// When compiling to standalone HTML, don't use the bun executable compile path
if (transpiler.options.compile_to_standalone_html) {
transpiler.options.compile = false;
config.compile = null;
}
transpiler.options.banner = config.banner.slice();
transpiler.options.footer = config.footer.slice();
transpiler.options.react_fast_refresh = config.react_fast_refresh;

View File

@@ -97,7 +97,8 @@ pub fn calculateOutputFileListCapacity(c: *const bun.bundle_v2.LinkerContext, ch
// module_info is generated for ESM bytecode in --compile builds
const module_info_count = if (c.options.generate_bytecode_cache and c.options.output_format == .esm and c.options.compile) bytecode_count else 0;
return .{ @intCast(chunks.len + source_map_count + bytecode_count + module_info_count + c.parse_graph.additional_output_files.items.len), @intCast(source_map_count + bytecode_count + module_info_count) };
const additional_output_files_count = if (c.options.compile_to_standalone_html) 0 else c.parse_graph.additional_output_files.items.len;
return .{ @intCast(chunks.len + source_map_count + bytecode_count + module_info_count + additional_output_files_count), @intCast(source_map_count + bytecode_count + module_info_count) };
}
pub fn insertForChunk(this: *OutputFileList, output_file: options.OutputFile) u32 {

View File

@@ -382,7 +382,8 @@ pub fn generateChunksInParallel(
var output_files = try OutputFileListBuilder.init(bun.default_allocator, c, chunks, c.parse_graph.additional_output_files.items.len);
const root_path = c.resolver.opts.output_dir;
const more_than_one_output = c.parse_graph.additional_output_files.items.len > 0 or c.options.generate_bytecode_cache or (has_css_chunk and has_js_chunk) or (has_html_chunk and (has_js_chunk or has_css_chunk));
const is_standalone = c.options.compile_to_standalone_html;
const more_than_one_output = !is_standalone and (c.parse_graph.additional_output_files.items.len > 0 or c.options.generate_bytecode_cache or (has_css_chunk and has_js_chunk) or (has_html_chunk and (has_js_chunk or has_css_chunk)));
if (!c.resolver.opts.compile and more_than_one_output and !c.resolver.opts.supports_multiple_outputs) {
try c.log.addError(null, Logger.Loc.Empty, "cannot write multiple output files without an output directory");
@@ -393,13 +394,73 @@ pub fn generateChunksInParallel(
var static_route_visitor = StaticRouteVisitor{ .c = c, .visited = bun.handleOom(bun.bit_set.AutoBitSet.initEmpty(bun.default_allocator, c.graph.files.len)) };
defer static_route_visitor.deinit();
// For standalone mode, resolve JS/CSS chunks so we can inline their content into HTML.
// Closing tag escaping (</script → <\\/script, </style → <\\/style) is handled during
// the HTML assembly step in codeWithSourceMapShifts, not here.
var standalone_chunk_contents: ?[]?[]const u8 = null;
defer if (standalone_chunk_contents) |scc| {
for (scc) |maybe_buf| {
if (maybe_buf) |buf| {
if (buf.len > 0)
Chunk.IntermediateOutput.allocatorForSize(buf.len).free(@constCast(buf));
}
}
bun.default_allocator.free(scc);
};
if (is_standalone) {
const scc = bun.handleOom(bun.default_allocator.alloc(?[]const u8, chunks.len));
@memset(scc, null);
standalone_chunk_contents = scc;
for (chunks, 0..) |*chunk_item, ci| {
if (chunk_item.content == .html) continue;
var ds: usize = 0;
scc[ci] = (chunk_item.intermediate_output.code(
null,
c.parse_graph,
&c.graph,
c.options.public_path,
chunk_item,
chunks,
&ds,
false,
false,
) catch |err| bun.handleOom(err)).buffer;
}
}
// Don't write to disk if compile mode is enabled - we need buffer values for compilation
const is_compile = bundler.transpiler.options.compile;
if (root_path.len > 0 and !is_compile) {
try c.writeOutputFilesToDisk(root_path, chunks, &output_files);
try c.writeOutputFilesToDisk(root_path, chunks, &output_files, standalone_chunk_contents);
} else {
// In-memory build
// In-memory build (also used for standalone mode)
for (chunks, 0..) |*chunk, chunk_index_in_chunks_list| {
// In standalone mode, non-HTML chunks were already resolved in the first pass.
// Insert a placeholder output file to keep chunk indices aligned.
if (is_standalone and chunk.content != .html) {
_ = output_files.insertForChunk(options.OutputFile.init(.{
.data = .{ .buffer = .{ .data = &.{}, .allocator = bun.default_allocator } },
.hash = null,
.loader = chunk.content.loader(),
.input_path = "",
.display_size = 0,
.output_kind = .chunk,
.input_loader = .js,
.output_path = "",
.is_executable = false,
.source_map_index = null,
.bytecode_index = null,
.module_info_index = null,
.side = .client,
.entry_point_index = null,
.referenced_css_chunks = &.{},
.bake_extra = .{},
}));
continue;
}
var display_size: usize = 0;
const public_path = if (chunk.flags.is_browser_chunk_from_server_build)
@@ -407,18 +468,32 @@ pub fn generateChunksInParallel(
else
c.options.public_path;
const _code_result = chunk.intermediate_output.code(
null,
c.parse_graph,
&c.graph,
public_path,
chunk,
chunks,
&display_size,
c.resolver.opts.compile and !chunk.flags.is_browser_chunk_from_server_build,
chunk.content.sourcemap(c.options.source_maps) != .none,
);
var code_result = _code_result catch @panic("Failed to allocate memory for output file");
const _code_result = if (is_standalone and chunk.content == .html)
chunk.intermediate_output.codeStandalone(
null,
c.parse_graph,
&c.graph,
public_path,
chunk,
chunks,
&display_size,
false,
false,
standalone_chunk_contents.?,
)
else
chunk.intermediate_output.code(
null,
c.parse_graph,
&c.graph,
public_path,
chunk,
chunks,
&display_size,
c.resolver.opts.compile and !chunk.flags.is_browser_chunk_from_server_build,
chunk.content.sourcemap(c.options.source_maps) != .none,
);
var code_result = _code_result catch |err| bun.handleOom(err);
var sourcemap_output_file: ?options.OutputFile = null;
const input_path = try bun.default_allocator.dupe(
@@ -670,7 +745,26 @@ pub fn generateChunksInParallel(
bun.assertf(chunk_index == chunk_index_in_chunks_list, "chunk_index ({d}) != chunk_index_in_chunks_list ({d})", .{ chunk_index, chunk_index_in_chunks_list });
}
output_files.insertAdditionalOutputFiles(c.parse_graph.additional_output_files.items);
if (!is_standalone) {
output_files.insertAdditionalOutputFiles(c.parse_graph.additional_output_files.items);
}
}
if (is_standalone) {
// For standalone mode, filter to only HTML output files.
// Deinit dropped items to free their heap allocations (paths, buffers).
var result = output_files.take();
var write_idx: usize = 0;
for (result.items) |*item| {
if (item.loader == .html) {
result.items[write_idx] = item.*;
write_idx += 1;
} else {
item.deinit();
}
}
result.items.len = write_idx;
return result;
}
return output_files.take();

View File

@@ -42,6 +42,7 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
chunk: *Chunk,
chunks: []Chunk,
minify_whitespace: bool,
compile_to_standalone_html: bool,
output: std.array_list.Managed(u8),
end_tag_indices: struct {
head: ?u32 = 0,
@@ -49,6 +50,7 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
html: ?u32 = 0,
},
added_head_tags: bool,
added_body_script: bool,
pub fn onWriteHTML(this: *@This(), bytes: []const u8) void {
bun.handleOom(this.output.appendSlice(bytes));
@@ -104,6 +106,18 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
element.remove();
return;
}
if (this.compile_to_standalone_html and import_record.source_index.isValid()) {
// In standalone HTML mode, inline assets as data: URIs
const url_for_css = this.linker.parse_graph.ast.items(.url_for_css)[import_record.source_index.get()];
if (url_for_css.len > 0) {
element.setAttribute(url_attribute, url_for_css) catch {
std.debug.panic("unexpected error from Element.setAttribute", .{});
};
return;
}
}
if (unique_key_for_additional_files.len > 0) {
// Replace the external href/src with the unique key so that we later will rewrite it to the final URL or pathname
element.setAttribute(url_attribute, unique_key_for_additional_files) catch {
@@ -142,17 +156,39 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
try endTag.before(slice, true);
}
/// Insert inline script before </body> so DOM elements are available.
fn addBodyTags(this: *@This(), endTag: *lol.EndTag) !void {
if (this.added_body_script) return;
this.added_body_script = true;
var html_appender = std.heap.stackFallback(256, bun.default_allocator);
const allocator = html_appender.get();
if (this.chunk.getJSChunkForHTML(this.chunks)) |js_chunk| {
const script = bun.handleOom(std.fmt.allocPrintSentinel(allocator, "<script type=\"module\">{s}</script>", .{js_chunk.unique_key}, 0));
defer allocator.free(script);
try endTag.before(script, true);
}
}
fn getHeadTags(this: *@This(), allocator: std.mem.Allocator) bun.BoundedArray([]const u8, 2) {
var array: bun.BoundedArray([]const u8, 2) = .{};
// Put CSS before JS to reduce changes of flash of unstyled content
if (this.chunk.getCSSChunkForHTML(this.chunks)) |css_chunk| {
const link_tag = bun.handleOom(std.fmt.allocPrintSentinel(allocator, "<link rel=\"stylesheet\" crossorigin href=\"{s}\">", .{css_chunk.unique_key}, 0));
array.appendAssumeCapacity(link_tag);
}
if (this.chunk.getJSChunkForHTML(this.chunks)) |js_chunk| {
// type="module" scripts do not block rendering, so it is okay to put them in head
const script = bun.handleOom(std.fmt.allocPrintSentinel(allocator, "<script type=\"module\" crossorigin src=\"{s}\"></script>", .{js_chunk.unique_key}, 0));
array.appendAssumeCapacity(script);
if (this.compile_to_standalone_html) {
// In standalone HTML mode, only put CSS in <head>; JS goes before </body>
if (this.chunk.getCSSChunkForHTML(this.chunks)) |css_chunk| {
const style_tag = bun.handleOom(std.fmt.allocPrintSentinel(allocator, "<style>{s}</style>", .{css_chunk.unique_key}, 0));
array.appendAssumeCapacity(style_tag);
}
} else {
// Put CSS before JS to reduce chances of flash of unstyled content
if (this.chunk.getCSSChunkForHTML(this.chunks)) |css_chunk| {
const link_tag = bun.handleOom(std.fmt.allocPrintSentinel(allocator, "<link rel=\"stylesheet\" crossorigin href=\"{s}\">", .{css_chunk.unique_key}, 0));
array.appendAssumeCapacity(link_tag);
}
if (this.chunk.getJSChunkForHTML(this.chunks)) |js_chunk| {
// type="module" scripts do not block rendering, so it is okay to put them in head
const script = bun.handleOom(std.fmt.allocPrintSentinel(allocator, "<script type=\"module\" crossorigin src=\"{s}\"></script>", .{js_chunk.unique_key}, 0));
array.appendAssumeCapacity(script);
}
}
return array;
}
@@ -170,7 +206,12 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
fn endBodyTagHandler(end: *lol.EndTag, opaque_this: ?*anyopaque) callconv(.c) lol.Directive {
const this: *@This() = @ptrCast(@alignCast(opaque_this.?));
if (this.linker.dev_server == null) {
this.addHeadTags(end) catch return .stop;
if (this.compile_to_standalone_html) {
// In standalone mode, insert JS before </body> so DOM is available
this.addBodyTags(end) catch return .stop;
} else {
this.addHeadTags(end) catch return .stop;
}
} else {
this.end_tag_indices.body = @intCast(this.output.items.len);
}
@@ -180,7 +221,13 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
fn endHtmlTagHandler(end: *lol.EndTag, opaque_this: ?*anyopaque) callconv(.c) lol.Directive {
const this: *@This() = @ptrCast(@alignCast(opaque_this.?));
if (this.linker.dev_server == null) {
this.addHeadTags(end) catch return .stop;
if (this.compile_to_standalone_html) {
// Fallback: if no </body> was found, insert both CSS and JS before </html>
this.addHeadTags(end) catch return .stop;
this.addBodyTags(end) catch return .stop;
} else {
this.addHeadTags(end) catch return .stop;
}
} else {
this.end_tag_indices.html = @intCast(this.output.items.len);
}
@@ -199,6 +246,7 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
.log = c.log,
.allocator = worker.allocator,
.minify_whitespace = c.options.minify_whitespace,
.compile_to_standalone_html = c.options.compile_to_standalone_html,
.chunk = chunk,
.chunks = chunks,
.output = std.array_list.Managed(u8).init(output_allocator),
@@ -209,6 +257,7 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
.head = null,
},
.added_head_tags = false,
.added_body_script = false,
};
HTMLScanner.HTMLProcessor(HTMLLoader, true).run(
@@ -233,14 +282,27 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
break :brk html;
break :brk @intCast(html_loader.output.items.len); // inject at end of file.
} else brk: {
if (!html_loader.added_head_tags) {
if (!html_loader.added_head_tags or !html_loader.added_body_script) {
@branchHint(.cold); // this is if the document is missing all head, body, and html elements.
var html_appender = std.heap.stackFallback(256, bun.default_allocator);
const allocator = html_appender.get();
const slices = html_loader.getHeadTags(allocator);
for (slices.slice()) |slice| {
bun.handleOom(html_loader.output.appendSlice(slice));
allocator.free(slice);
if (!html_loader.added_head_tags) {
const slices = html_loader.getHeadTags(allocator);
for (slices.slice()) |slice| {
bun.handleOom(html_loader.output.appendSlice(slice));
allocator.free(slice);
}
html_loader.added_head_tags = true;
}
if (!html_loader.added_body_script) {
if (html_loader.compile_to_standalone_html) {
if (html_loader.chunk.getJSChunkForHTML(html_loader.chunks)) |js_chunk| {
const script = bun.handleOom(std.fmt.allocPrintSentinel(allocator, "<script type=\"module\">{s}</script>", .{js_chunk.unique_key}, 0));
defer allocator.free(script);
bun.handleOom(html_loader.output.appendSlice(script));
}
}
html_loader.added_body_script = true;
}
}
break :brk if (Environment.isDebug) undefined else 0; // value is ignored. fail loud if hit in debug

View File

@@ -3,6 +3,7 @@ pub fn writeOutputFilesToDisk(
root_path: string,
chunks: []Chunk,
output_files: *OutputFileListBuilder,
standalone_chunk_contents: ?[]const ?[]const u8,
) !void {
const trace = bun.perf.trace("Bundler.writeOutputFilesToDisk");
defer trace.end();
@@ -42,6 +43,29 @@ pub fn writeOutputFilesToDisk(
const bv2: *bundler.BundleV2 = @fieldParentPtr("linker", c);
for (chunks, 0..) |*chunk, chunk_index_in_chunks_list| {
// In standalone mode, only write HTML chunks to disk.
// Insert placeholder output files for non-HTML chunks to keep indices aligned.
if (standalone_chunk_contents != null and chunk.content != .html) {
_ = output_files.insertForChunk(options.OutputFile.init(.{
.data = .{ .saved = 0 },
.hash = null,
.loader = chunk.content.loader(),
.input_path = "",
.display_size = 0,
.output_kind = .chunk,
.input_loader = .js,
.output_path = "",
.is_executable = false,
.source_map_index = null,
.bytecode_index = null,
.module_info_index = null,
.side = .client,
.entry_point_index = null,
.referenced_css_chunks = &.{},
}));
continue;
}
const trace2 = bun.perf.trace("Bundler.writeChunkToDisk");
defer trace2.end();
defer max_heap_allocator.reset();
@@ -65,17 +89,31 @@ pub fn writeOutputFilesToDisk(
else
c.resolver.opts.public_path;
var code_result = chunk.intermediate_output.code(
code_allocator,
c.parse_graph,
&c.graph,
public_path,
chunk,
chunks,
&display_size,
c.resolver.opts.compile and !chunk.flags.is_browser_chunk_from_server_build,
chunk.content.sourcemap(c.options.source_maps) != .none,
) catch |err| bun.Output.panic("Failed to create output chunk: {s}", .{@errorName(err)});
var code_result = if (standalone_chunk_contents) |scc|
chunk.intermediate_output.codeStandalone(
code_allocator,
c.parse_graph,
&c.graph,
public_path,
chunk,
chunks,
&display_size,
false,
false,
scc,
) catch |err| bun.Output.panic("Failed to create output chunk: {s}", .{@errorName(err)})
else
chunk.intermediate_output.code(
code_allocator,
c.parse_graph,
&c.graph,
public_path,
chunk,
chunks,
&display_size,
c.resolver.opts.compile and !chunk.flags.is_browser_chunk_from_server_build,
chunk.content.sourcemap(c.options.source_maps) != .none,
) catch |err| bun.Output.panic("Failed to create output chunk: {s}", .{@errorName(err)});
var source_map_output_file: ?options.OutputFile = null;
@@ -318,7 +356,7 @@ pub fn writeOutputFilesToDisk(
.js,
.hash = chunk.template.placeholder.hash,
.output_kind = output_kind,
.loader = .js,
.loader = chunk.content.loader(),
.source_map_index = source_map_index,
.bytecode_index = bytecode_index,
.size = @as(u32, @truncate(code_result.buffer.len)),
@@ -344,10 +382,15 @@ pub fn writeOutputFilesToDisk(
},
}));
// We want the chunk index to remain the same in `output_files` so the indices in `OutputFile.referenced_css_chunks` work
bun.assertf(chunk_index == chunk_index_in_chunks_list, "chunk_index ({d}) != chunk_index_in_chunks_list ({d})", .{ chunk_index, chunk_index_in_chunks_list });
// We want the chunk index to remain the same in `output_files` so the indices in `OutputFile.referenced_css_chunks` work.
// In standalone mode, non-HTML chunks are skipped so this invariant doesn't apply.
if (standalone_chunk_contents == null)
bun.assertf(chunk_index == chunk_index_in_chunks_list, "chunk_index ({d}) != chunk_index_in_chunks_list ({d})", .{ chunk_index, chunk_index_in_chunks_list });
}
// In standalone mode, additional output files (assets) are inlined into the HTML.
if (standalone_chunk_contents != null) return;
{
const additional_output_files = output_files.getMutableAdditionalOutputFiles();
output_files.total_insertions += @intCast(additional_output_files.len);

View File

@@ -460,7 +460,6 @@ pub const Command = struct {
banner: []const u8 = "",
footer: []const u8 = "",
css_chunking: bool = false,
bake: bool = false,
bake_debug_dump_server: bool = false,
bake_debug_disable_minify: bool = false,

View File

@@ -3,6 +3,7 @@ pub const BuildCommand = struct {
Global.configureAllocator(.{ .long_running = true });
const allocator = ctx.allocator;
var log = ctx.log;
const user_requested_browser_target = ctx.args.target != null and ctx.args.target.? == .browser;
if (ctx.bundler_options.compile or ctx.bundler_options.bytecode) {
// set this early so that externals are set up correctly and define is right
ctx.args.target = .bun;
@@ -98,45 +99,76 @@ pub const BuildCommand = struct {
var was_renamed_from_index = false;
if (ctx.bundler_options.compile) {
if (ctx.bundler_options.outdir.len > 0) {
Output.prettyErrorln("<r><red>error<r><d>:<r> cannot use --compile with --outdir", .{});
Global.exit(1);
return;
}
const base_public_path = bun.StandaloneModuleGraph.targetBasePublicPath(compile_target.os, "root/");
this_transpiler.options.public_path = base_public_path;
if (outfile.len == 0) {
outfile = std.fs.path.basename(this_transpiler.options.entry_points[0]);
const ext = std.fs.path.extension(outfile);
if (ext.len > 0) {
outfile = outfile[0 .. outfile.len - ext.len];
}
if (strings.eqlComptime(outfile, "index")) {
outfile = std.fs.path.basename(std.fs.path.dirname(this_transpiler.options.entry_points[0]) orelse "index");
was_renamed_from_index = !strings.eqlComptime(outfile, "index");
}
if (strings.eqlComptime(outfile, "bun")) {
outfile = std.fs.path.basename(std.fs.path.dirname(this_transpiler.options.entry_points[0]) orelse "bun");
}
}
// If argv[0] is "bun" or "bunx", we don't check if the binary is standalone
if (strings.eqlComptime(outfile, "bun") or strings.eqlComptime(outfile, "bunx")) {
Output.prettyErrorln("<r><red>error<r><d>:<r> cannot use --compile with an output file named 'bun' because bun won't realize it's a standalone executable. Please choose a different name for --outfile", .{});
Global.exit(1);
return;
}
if (ctx.bundler_options.transform_only) {
Output.prettyErrorln("<r><red>error<r><d>:<r> --compile does not support --no-bundle", .{});
Global.exit(1);
return;
}
// Check if all entrypoints are HTML files for standalone HTML mode
const has_all_html_entrypoints = brk: {
if (this_transpiler.options.entry_points.len == 0) break :brk false;
for (this_transpiler.options.entry_points) |entry_point| {
if (!strings.hasSuffixComptime(entry_point, ".html")) break :brk false;
}
break :brk true;
};
if (user_requested_browser_target and has_all_html_entrypoints) {
// --compile --target=browser with all HTML entrypoints: produce self-contained HTML
ctx.args.target = .browser;
if (ctx.bundler_options.code_splitting) {
Output.prettyErrorln("<r><red>error<r><d>:<r> cannot use --compile --target browser with --splitting", .{});
Global.exit(1);
return;
}
this_transpiler.options.compile_to_standalone_html = true;
// This is not a bun executable compile - clear compile flags
this_transpiler.options.compile = false;
ctx.bundler_options.compile = false;
if (ctx.bundler_options.outdir.len == 0 and outfile.len == 0) {
outfile = std.fs.path.basename(this_transpiler.options.entry_points[0]);
}
this_transpiler.options.supports_multiple_outputs = ctx.bundler_options.outdir.len > 0;
} else {
// Standard --compile: produce standalone bun executable
if (ctx.bundler_options.outdir.len > 0) {
Output.prettyErrorln("<r><red>error<r><d>:<r> cannot use --compile with --outdir", .{});
Global.exit(1);
return;
}
const base_public_path = bun.StandaloneModuleGraph.targetBasePublicPath(compile_target.os, "root/");
this_transpiler.options.public_path = base_public_path;
if (outfile.len == 0) {
outfile = std.fs.path.basename(this_transpiler.options.entry_points[0]);
const ext = std.fs.path.extension(outfile);
if (ext.len > 0) {
outfile = outfile[0 .. outfile.len - ext.len];
}
if (strings.eqlComptime(outfile, "index")) {
outfile = std.fs.path.basename(std.fs.path.dirname(this_transpiler.options.entry_points[0]) orelse "index");
was_renamed_from_index = !strings.eqlComptime(outfile, "index");
}
if (strings.eqlComptime(outfile, "bun")) {
outfile = std.fs.path.basename(std.fs.path.dirname(this_transpiler.options.entry_points[0]) orelse "bun");
}
}
// If argv[0] is "bun" or "bunx", we don't check if the binary is standalone
if (strings.eqlComptime(outfile, "bun") or strings.eqlComptime(outfile, "bunx")) {
Output.prettyErrorln("<r><red>error<r><d>:<r> cannot use --compile with an output file named 'bun' because bun won't realize it's a standalone executable. Please choose a different name for --outfile", .{});
Global.exit(1);
return;
}
}
}
if (ctx.bundler_options.transform_only) {

View File

@@ -378,10 +378,15 @@ class AssertionError extends Error {
this.operator = operator;
}
ErrorCaptureStackTrace(this, stackStartFn || stackStartFunction);
// JSC::Interpreter::getStackTrace() sometimes short-circuits without creating a .stack property.
// e.g.: https://github.com/oven-sh/WebKit/blob/e32c6356625cfacebff0c61d182f759abf6f508a/Source/JavaScriptCore/interpreter/Interpreter.cpp#L501
if ($isUndefinedOrNull(this.stack)) {
ErrorCaptureStackTrace(this, AssertionError);
// When all stack frames are above the stackStartFn (e.g. in async
// contexts), captureStackTrace produces a stack with just the error
// message and no frame lines. Retry with AssertionError as the filter
// so we get at least the frames below the constructor.
{
const s = this.stack;
if ($isUndefinedOrNull(s) || (typeof s === "string" && s.indexOf("\n at ") === -1)) {
ErrorCaptureStackTrace(this, AssertionError);
}
}
// Create error message including the error code in the name.
this.stack; // eslint-disable-line no-unused-expressions

View File

@@ -92,43 +92,55 @@ const kDeferredTimeouts = Symbol("deferredTimeouts");
const kEmptyObject = Object.freeze(Object.create(null));
export const enum ClientRequestEmitState {
socket = 1,
prefinish = 2,
finish = 3,
response = 4,
}
// These are declared as plain objects instead of `const enum` to prevent the
// TypeScript enum reverse-mapping pattern (e.g. `Enum[Enum["x"] = 0] = "x"`)
// from triggering setters on `Object.prototype` during module initialization.
// See: https://github.com/oven-sh/bun/issues/24336
export const ClientRequestEmitState = {
socket: 1,
prefinish: 2,
finish: 3,
response: 4,
} as const;
export type ClientRequestEmitState = (typeof ClientRequestEmitState)[keyof typeof ClientRequestEmitState];
export const enum NodeHTTPResponseAbortEvent {
none = 0,
abort = 1,
timeout = 2,
}
export const enum NodeHTTPIncomingRequestType {
FetchRequest,
FetchResponse,
NodeHTTPResponse,
}
export const enum NodeHTTPBodyReadState {
none,
pending = 1 << 1,
done = 1 << 2,
hasBufferedDataDuringPause = 1 << 3,
}
export const NodeHTTPResponseAbortEvent = {
none: 0,
abort: 1,
timeout: 2,
} as const;
export type NodeHTTPResponseAbortEvent = (typeof NodeHTTPResponseAbortEvent)[keyof typeof NodeHTTPResponseAbortEvent];
export const NodeHTTPIncomingRequestType = {
FetchRequest: 0,
FetchResponse: 1,
NodeHTTPResponse: 2,
} as const;
export type NodeHTTPIncomingRequestType =
(typeof NodeHTTPIncomingRequestType)[keyof typeof NodeHTTPIncomingRequestType];
export const NodeHTTPBodyReadState = {
none: 0,
pending: 1 << 1,
done: 1 << 2,
hasBufferedDataDuringPause: 1 << 3,
} as const;
export type NodeHTTPBodyReadState = (typeof NodeHTTPBodyReadState)[keyof typeof NodeHTTPBodyReadState];
// Must be kept in sync with NodeHTTPResponse.Flags
export const enum NodeHTTPResponseFlags {
socket_closed = 1 << 0,
request_has_completed = 1 << 1,
export const NodeHTTPResponseFlags = {
socket_closed: 1 << 0,
request_has_completed: 1 << 1,
closed_or_completed: (1 << 0) | (1 << 1),
} as const;
export type NodeHTTPResponseFlags = (typeof NodeHTTPResponseFlags)[keyof typeof NodeHTTPResponseFlags];
closed_or_completed = socket_closed | request_has_completed,
}
export const enum NodeHTTPHeaderState {
none,
assigned,
sent,
}
export const NodeHTTPHeaderState = {
none: 0,
assigned: 1,
sent: 2,
} as const;
export type NodeHTTPHeaderState = (typeof NodeHTTPHeaderState)[keyof typeof NodeHTTPHeaderState];
function emitErrorNextTickIfErrorListenerNT(self, err, cb) {
process.nextTick(emitErrorNextTickIfErrorListener, self, err, cb);

View File

@@ -51,6 +51,15 @@ function onError(msg, err, callback) {
process.nextTick(emitErrorNt, msg, err, callback);
}
function isHTTPHeaderStateSentOrAssigned(state) {
return state === NodeHTTPHeaderState.sent || state === NodeHTTPHeaderState.assigned;
}
function throwHeadersSentIfNecessary(self, action) {
if (self._header != null || isHTTPHeaderStateSentOrAssigned(self[headerStateSymbol])) {
throw $ERR_HTTP_HEADERS_SENT(action);
}
}
function write_(msg, chunk, encoding, callback, fromEnd) {
if (typeof callback !== "function") callback = nop;
@@ -252,18 +261,14 @@ const OutgoingMessagePrototype = {
removeHeader(name) {
validateString(name, "name");
if ((this._header !== undefined && this._header !== null) || this[headerStateSymbol] === NodeHTTPHeaderState.sent) {
throw $ERR_HTTP_HEADERS_SENT("remove");
}
throwHeadersSentIfNecessary(this, "remove");
const headers = this[headersSymbol];
if (!headers) return;
headers.delete(name);
},
setHeader(name, value) {
if ((this._header !== undefined && this._header !== null) || this[headerStateSymbol] == NodeHTTPHeaderState.sent) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
throwHeadersSentIfNecessary(this, "set");
validateHeaderName(name);
validateHeaderValue(name, value);
const headers = (this[headersSymbol] ??= new Headers());
@@ -271,9 +276,7 @@ const OutgoingMessagePrototype = {
return this;
},
setHeaders(headers) {
if ((this._header != null) || this[headerStateSymbol] === NodeHTTPHeaderState.sent) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
throwHeadersSentIfNecessary(this, "set");
if (!headers || $isArray(headers) || typeof headers.keys !== "function" || typeof headers.get !== "function") {
throw $ERR_INVALID_ARG_TYPE("headers", ["Headers", "Map"], headers);

View File

@@ -1833,6 +1833,7 @@ pub const BundleOptions = struct {
debugger: bool = false,
compile: bool = false,
compile_to_standalone_html: bool = false,
metafile: bool = false,
/// Path to write JSON metafile (for Bun.build API)
metafile_json_path: []const u8 = "",

View File

@@ -0,0 +1,519 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { existsSync } from "node:fs";
describe("compile --target=browser", () => {
test("inlines JS and CSS into HTML", async () => {
using dir = tempDir("compile-browser-basic", {
"index.html": `<!DOCTYPE html>
<html>
<head><link rel="stylesheet" href="./style.css"></head>
<body><script src="./app.js"></script></body>
</html>`,
"style.css": `body { color: red; }`,
"app.js": `console.log("hello");`,
});
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
expect(result.outputs[0].loader).toBe("html");
const html = await result.outputs[0].text();
expect(html).toContain("<style>");
expect(html).toContain("color: red");
expect(html).toContain("</style>");
expect(html).toContain('<script type="module">');
expect(html).toContain('console.log("hello")');
expect(html).toContain("</script>");
// Should NOT have external references
expect(html).not.toContain('src="');
expect(html).not.toContain('href="');
});
test("uses type=module on inline scripts", async () => {
using dir = tempDir("compile-browser-module", {
"index.html": `<!DOCTYPE html><html><body><script src="./app.js"></script></body></html>`,
"app.js": `console.log("module");`,
});
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
const html = await result.outputs[0].text();
expect(html).toContain('<script type="module">');
expect(html).not.toMatch(/<script>(?!<)/);
});
test("top-level await works with inline scripts", async () => {
using dir = tempDir("compile-browser-tla", {
"index.html": `<!DOCTYPE html><html><body><script src="./app.js"></script></body></html>`,
"app.js": `const data = await Promise.resolve(42);
console.log(data);`,
});
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
const html = await result.outputs[0].text();
expect(html).toContain('<script type="module">');
expect(html).toContain("await");
});
test("escapes </script> in inlined JS", async () => {
using dir = tempDir("compile-browser-escape-script", {
"index.html": `<!DOCTYPE html><html><body><script src="./app.js"></script></body></html>`,
"app.js": `const x = "</script>";
console.log(x);`,
});
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
const html = await result.outputs[0].text();
// The literal </script> inside JS must be escaped so it doesn't close the tag
// Count actual </script> occurrences - should be exactly 1 (the closing tag)
const scriptCloseCount = html.split("</script>").length - 1;
expect(scriptCloseCount).toBe(1);
// The escaped version should be present
expect(html).toContain("<\\/script>");
});
test("escapes </style> in inlined CSS", async () => {
using dir = tempDir("compile-browser-escape-style", {
"index.html": `<!DOCTYPE html>
<html><head><link rel="stylesheet" href="./style.css"></head><body></body></html>`,
"style.css": `body::after { content: "</style>"; }`,
});
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
const html = await result.outputs[0].text();
// The literal </style> inside CSS must be escaped
const styleCloseCount = html.split("</style>").length - 1;
expect(styleCloseCount).toBe(1);
});
test("deep import chain with re-exports and multiple files", async () => {
using dir = tempDir("compile-browser-deep-chain", {
"index.html": `<!DOCTYPE html><html><body><script src="./app.js"></script></body></html>`,
"app.js": `import { renderApp } from "./components/App.js";
import { initRouter } from "./router/index.js";
import { createStore } from "./store/index.js";
const store = createStore({ count: 0 });
initRouter(store);
renderApp(store);`,
"components/App.js": `import { Header } from "./Header.js";
import { Footer } from "./Footer.js";
import { Counter } from "./Counter.js";
export function renderApp(store) {
document.body.innerHTML = Header() + Counter(store) + Footer();
}`,
"components/Header.js": `import { APP_NAME } from "../config.js";
export function Header() { return "<header>" + APP_NAME + "</header>"; }`,
"components/Footer.js": `import { APP_VERSION } from "../config.js";
export function Footer() { return "<footer>v" + APP_VERSION + "</footer>"; }`,
"components/Counter.js": `import { formatNumber } from "../utils/format.js";
export function Counter(store) {
return "<div>Count: " + formatNumber(store.count) + "</div>";
}`,
"router/index.js": `import { parseRoute } from "./parser.js";
import { matchRoute } from "./matcher.js";
export function initRouter(store) {
const route = parseRoute(window.location.pathname);
matchRoute(route, store);
}`,
"router/parser.js": `export function parseRoute(path) {
return path.split("/").filter(Boolean);
}`,
"router/matcher.js": `import { log } from "../utils/logger.js";
export function matchRoute(route, store) {
log("Matching route: " + route.join("/"));
}`,
"store/index.js": `import { log } from "../utils/logger.js";
export function createStore(initial) {
log("Store created");
return { ...initial };
}`,
"utils/format.js": `export function formatNumber(n) { return n.toLocaleString(); }`,
"utils/logger.js": `export function log(msg) { console.log("[LOG] " + msg); }`,
"config.js": `export const APP_NAME = "MyApp";
export const APP_VERSION = "1.0.0";`,
});
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
const html = await result.outputs[0].text();
// All modules from the deep chain should be bundled
expect(html).toContain("MyApp");
expect(html).toContain("1.0.0");
expect(html).toContain("renderApp");
expect(html).toContain("initRouter");
expect(html).toContain("createStore");
expect(html).toContain("formatNumber");
expect(html).toContain("[LOG]");
// Single output, no external refs
expect(html).not.toContain('src="');
expect(html).toContain('<script type="module">');
});
test("CSS imported from JS and via link tag (deduplicated)", async () => {
using dir = tempDir("compile-browser-css-dedup", {
"index.html": `<!DOCTYPE html>
<html>
<head><link rel="stylesheet" href="./shared.css"></head>
<body><script src="./app.js"></script></body>
</html>`,
"app.js": `import "./shared.css";
import "./components.css";
console.log("app with css");`,
"shared.css": `body { margin: 0; font-family: sans-serif; }`,
"components.css": `@import "./buttons.css";
.card { border: 1px solid #ccc; padding: 16px; }`,
"buttons.css": `.btn { padding: 8px 16px; cursor: pointer; }
.btn-primary { background: #007bff; color: white; }`,
});
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
const html = await result.outputs[0].text();
expect(html).toContain("<style>");
expect(html).toContain("</style>");
// shared.css content
expect(html).toContain("font-family:");
expect(html).toContain("sans-serif");
// components.css content
expect(html).toContain(".card");
expect(html).toContain("padding:");
// nested buttons.css content
expect(html).toContain(".btn");
expect(html).toContain(".btn-primary");
expect(html).toContain("cursor: pointer");
// JS should be inlined
expect(html).toContain('console.log("app with css")');
// No external refs
expect(html).not.toContain('href="');
expect(html).not.toContain("@import");
});
test("nested CSS @import chain", async () => {
using dir = tempDir("compile-browser-css-chain", {
"index.html": `<!DOCTYPE html>
<html><head><link rel="stylesheet" href="./main.css"></head><body></body></html>`,
"main.css": `@import "./base.css";
body { color: blue; }`,
"base.css": `@import "./reset.css";
* { box-sizing: border-box; }`,
"reset.css": `html, body { margin: 0; padding: 0; }`,
});
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
const html = await result.outputs[0].text();
expect(html).toContain("<style>");
// All three CSS files bundled together
expect(html).toContain("margin: 0");
expect(html).toContain("padding: 0");
expect(html).toContain("box-sizing: border-box");
expect(html).toMatch(/color:?\s*(blue|#00f)/);
expect(html).not.toContain("@import");
});
test("Bun.build() with outdir writes files to disk", async () => {
using dir = tempDir("compile-browser-outdir", {
"index.html": `<!DOCTYPE html>
<html><head><link rel="stylesheet" href="./style.css"></head>
<body><script src="./app.js"></script></body></html>`,
"style.css": `h1 { font-weight: bold; }`,
"app.js": `console.log("outdir test");`,
});
const outdir = `${dir}/dist`;
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
outdir,
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
// Verify the file was actually written to disk
expect(existsSync(`${outdir}/index.html`)).toBe(true);
const html = await Bun.file(`${outdir}/index.html`).text();
expect(html).toContain("<style>");
expect(html).toContain("font-weight: bold");
expect(html).toContain('<script type="module">');
expect(html).toContain('console.log("outdir test")');
});
test("Bun.build() with outdir and image assets", async () => {
const pixel = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4DwAAAQEABRjYTgAAAABJRU5ErkJggg==",
"base64",
);
using dir = tempDir("compile-browser-outdir-assets", {
"index.html": `<!DOCTYPE html>
<html><body><img src="./logo.png"><script src="./app.js"></script></body></html>`,
"logo.png": pixel,
"app.js": `console.log("outdir with assets");`,
});
const outdir = `${dir}/dist`;
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
outdir,
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
expect(existsSync(`${outdir}/index.html`)).toBe(true);
const html = await Bun.file(`${outdir}/index.html`).text();
expect(html).toContain('src="data:image/png;base64,');
expect(html).toContain('console.log("outdir with assets")');
});
test("inlines images as data: URIs", async () => {
// 1x1 red PNG
const pixel = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4DwAAAQEABRjYTgAAAABJRU5ErkJggg==",
"base64",
);
using dir = tempDir("compile-browser-image", {
"index.html": `<!DOCTYPE html>
<html><body><img src="./pixel.png"><script src="./app.js"></script></body></html>`,
"pixel.png": pixel,
"app.js": `console.log("with image");`,
});
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
const html = await result.outputs[0].text();
expect(html).toContain('src="data:image/png;base64,');
expect(html).toContain('console.log("with image")');
});
test("handles CSS url() references", async () => {
const pixel = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4DwAAAQEABRjYTgAAAABJRU5ErkJggg==",
"base64",
);
using dir = tempDir("compile-browser-css-url", {
"index.html": `<!DOCTYPE html>
<html><head><link rel="stylesheet" href="./style.css"></head><body></body></html>`,
"style.css": `body { background: url("./bg.png") no-repeat; }`,
"bg.png": pixel,
});
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
const html = await result.outputs[0].text();
expect(html).toContain("data:image/png;base64,");
expect(html).toContain("<style>");
});
test("non-HTML entrypoints with compile+browser falls back to normal compile", async () => {
using dir = tempDir("compile-browser-no-html", {
"app.js": `console.log("no html");`,
});
// compile: true + target: "browser" with non-HTML entrypoints should
// fall back to normal bun executable compile (not standalone HTML)
const result = await Bun.build({
entrypoints: [`${dir}/app.js`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
});
test("CLI --compile --target=browser with non-HTML falls back to normal compile", async () => {
using dir = tempDir("compile-browser-cli-no-html", {
"app.js": `console.log("test");`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--compile", "--target=browser", `${dir}/app.js`],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [_stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Non-HTML entrypoints with --compile --target=browser should fall back to normal bun compile
expect(exitCode).toBe(0);
});
test("fails with splitting", async () => {
using dir = tempDir("compile-browser-splitting", {
"index.html": `<!DOCTYPE html><html><body><script src="./app.js"></script></body></html>`,
"app.js": `console.log("test");`,
});
expect(() =>
Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
splitting: true,
}),
).toThrow();
});
test("CLI --compile --target=browser produces single file", async () => {
using dir = tempDir("compile-browser-cli", {
"index.html": `<!DOCTYPE html>
<html><head><link rel="stylesheet" href="./style.css"></head>
<body><script src="./app.js"></script></body></html>`,
"style.css": `h1 { font-weight: bold; }`,
"app.js": `console.log("cli test");`,
});
const outdir = `${dir}/out`;
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--compile", "--target=browser", `${dir}/index.html`, "--outdir", outdir],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Check only HTML file exists in output
const glob = new Bun.Glob("**/*");
const files = Array.from(glob.scanSync({ cwd: outdir }));
expect(files).toEqual(["index.html"]);
// Verify content
const html = await Bun.file(`${outdir}/index.html`).text();
expect(html).toContain("<style>");
expect(html).toContain("font-weight: bold");
expect(html).toContain('<script type="module">');
expect(html).toContain('console.log("cli test")');
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("malformed HTML without closing tags still inlines JS and CSS", async () => {
// This tests the cold fallback path when no </head>, </body>, or </html> tags exist.
// The document is just a fragment - the loader must still inject both CSS and JS.
using dir = tempDir("compile-browser-malformed", {
"index.html": `<div id="app"></div><link rel="stylesheet" href="./style.css"><script src="./app.js"></script>`,
"style.css": `#app { color: green; }`,
"app.js": `console.log("malformed html");`,
});
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
const html = await result.outputs[0].text();
// CSS should be inlined
expect(html).toContain("<style>");
expect(html).toContain("color: green");
// JS should also be inlined (this was the bug - JS was dropped in fallback path)
expect(html).toContain('<script type="module">');
expect(html).toContain('console.log("malformed html")');
});
test("minification works", async () => {
using dir = tempDir("compile-browser-minify", {
"index.html": `<!DOCTYPE html>
<html><head><link rel="stylesheet" href="./style.css"></head>
<body><script src="./app.js"></script></body></html>`,
"style.css": `body {
color: red;
background: blue;
}`,
"app.js": `const message = "hello world";
console.log(message);`,
});
const result = await Bun.build({
entrypoints: [`${dir}/index.html`],
compile: true,
target: "browser",
minify: true,
});
expect(result.success).toBe(true);
const html = await result.outputs[0].text();
expect(html).toContain("<style>");
expect(html).toContain("</style>");
expect(html).toContain('<script type="module">');
expect(html).toContain("</script>");
});
});

View File

@@ -91,6 +91,30 @@ it("should find files", () => {
expect(Object.values(routes).length).toBe(Object.values(fixture).length);
});
it("should handle routes under GC pressure", () => {
// Regression test for BUN-1K54: fromEntries used ObjectInitializationScope
// with putDirect, which could crash when GC triggers during string allocation.
const files = Array.from({ length: 128 }, (_, i) => `route${i}/index.tsx`);
const { dir } = make(files);
const router = new FileSystemRouter({
dir,
fileExtensions: [".tsx"],
style: "nextjs",
});
// Access routes repeatedly with GC pressure to exercise the fromEntries path
for (let i = 0; i < 10; i++) {
Bun.gc(true);
const routes = router.routes;
const keys = Object.keys(routes);
expect(keys.length).toBe(128);
for (let j = 0; j < 128; j++) {
expect(routes[`/route${j}`]).toBe(`${dir}/route${j}/index.tsx`);
}
}
});
it("should handle empty dirs", () => {
const { dir } = make([]);

View File

@@ -754,3 +754,39 @@ test("CallFrame.p.isAsync", async () => {
expect(prepare).toHaveBeenCalledTimes(1);
});
test("captureStackTrace with constructor function not in stack returns error string", () => {
// When the second argument to captureStackTrace is a function that isn't in
// the call stack, all frames are filtered out and .stack should still return
// the error name and message (matching Node.js behavior).
function notInStack() {}
// Case 1: stack not accessed before captureStackTrace
{
const e = new Error("test");
Error.captureStackTrace(e, notInStack);
expect(e.stack).toBe("Error: test");
}
// Case 2: stack accessed before captureStackTrace
{
const e = new Error("test");
void e.stack;
Error.captureStackTrace(e, notInStack);
expect(e.stack).toBe("Error: test");
}
// Case 3: empty message
{
const e = new Error();
Error.captureStackTrace(e, notInStack);
expect(e.stack).toBe("Error");
}
// Case 4: custom error name
{
const e = new TypeError("bad type");
Error.captureStackTrace(e, notInStack);
expect(e.stack).toBe("TypeError: bad type");
}
});

View File

@@ -0,0 +1,63 @@
// Regression test for TLS upgrade raw socket leak (#12117, #24118, #25948)
// When a TCP socket is upgraded to TLS via tls.connect({ socket }),
// both a TLS wrapper and a raw TCP wrapper are created in Zig.
// Previously, the raw socket's has_pending_activity was never set to
// false on close, causing it (and all its retained objects) to leak.
import { describe, expect, it } from "bun:test";
import { tls as COMMON_CERT, expectMaxObjectTypeCount } from "harness";
import { once } from "node:events";
import net from "node:net";
import tls from "node:tls";
describe("TLS upgrade", () => {
it("should not leak TLSSocket objects after close", async () => {
// Create a TLS server that echoes data and closes
const server = tls.createServer(
{
key: COMMON_CERT.key,
cert: COMMON_CERT.cert,
},
socket => {
socket.end("hello");
},
);
await once(server.listen(0, "127.0.0.1"), "listening");
const port = (server.address() as net.AddressInfo).port;
// Simulate the MongoDB driver pattern: create a plain TCP socket,
// then upgrade it to TLS via tls.connect({ socket }).
// Do this multiple times to accumulate leaked objects.
const iterations = 50;
try {
for (let i = 0; i < iterations; i++) {
const tcpSocket = net.createConnection({ host: "127.0.0.1", port });
await once(tcpSocket, "connect");
const tlsSocket = tls.connect({
socket: tcpSocket,
ca: COMMON_CERT.cert,
rejectUnauthorized: false,
});
await once(tlsSocket, "secureConnect");
// Read any data and destroy the TLS socket (simulates SDAM close)
tlsSocket.on("data", () => {});
tlsSocket.destroy();
await once(tlsSocket, "close");
}
} finally {
server.close();
await once(server, "close");
}
// After all connections are closed and GC runs, the TLSSocket count
// should be low. Before the fix, each iteration would leak 1 raw
// TLSSocket (the TCP wrapper from upgradeTLS), accumulating over time.
// Allow some slack for prototypes/structures (typically 2-3 baseline).
await expectMaxObjectTypeCount(expect, "TLSSocket", 10, 1000);
});
});

View File

@@ -0,0 +1,77 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// https://github.com/oven-sh/bun/issues/24336
// require('http') should not trigger Object.prototype setters during module loading.
// Node.js produces no output for both CJS and ESM, and Bun should match that behavior.
test("require('http') does not trigger Object.prototype[0] setter", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
Object.defineProperty(Object.prototype, '0', {
set() { console.log('SETTER_TRIGGERED'); }
});
require('http');
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("require('url') does not trigger Object.prototype[0] setter", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
Object.defineProperty(Object.prototype, '0', {
set() { console.log('SETTER_TRIGGERED'); }
});
require('url');
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("require('util') does not trigger Object.prototype[0] setter", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
Object.defineProperty(Object.prototype, '0', {
set() { console.log('SETTER_TRIGGERED'); }
});
require('util');
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});

View File

@@ -1,8 +1,44 @@
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/27014
test("Bun.stripANSI does not hang on non-ANSI control characters", () => {
// Bun.stripANSI() hangs on strings with control characters in 0x10-0x1F
// that are not actual ANSI escape introducers (e.g. 0x16 SYN, 0x19 EM).
test("stripANSI does not hang on non-escape control characters", () => {
// This input contains 0x16, 0x19, 0x13, 0x14 which are in the 0x10-0x1F
// range but are NOT ANSI escape introducers.
const s = "\u0016zo\u00BAd\u0019\u00E8\u00E0\u0013?\u00C1+\u0014d\u00D3\u00E9";
const result = Bun.stripANSI(s);
expect(result).toBe(s);
});
test("stripANSI still strips real ANSI escape sequences", () => {
// ESC [ 31m = red color, ESC [ 0m = reset
const input = "\x1b[31mhello\x1b[0m";
expect(Bun.stripANSI(input)).toBe("hello");
});
test("stripANSI handles mix of false-positive control chars and real escapes", () => {
// 0x16 (SYN) should be preserved, but \x1b[31m should be stripped
const input = "\x16before\x1b[31mcolored\x1b[0mafter\x19end";
expect(Bun.stripANSI(input)).toBe("\x16beforecoloredafter\x19end");
});
test("stripANSI handles string of only non-escape control characters", () => {
const input = "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1c\x1d\x1e\x1f";
expect(Bun.stripANSI(input)).toBe(input);
});
test("stripANSI finds real escape after false positives in same SIMD chunk", () => {
// Place false-positive control chars followed by a real ESC within 16 bytes
// so they land in the same SIMD chunk. The fix must scan past false positives
// within a chunk to find the real escape character.
const input = "\x10\x11\x12\x1b[31mred\x1b[0m";
expect(Bun.stripANSI(input)).toBe("\x10\x11\x12red");
});
test("stripANSI handles many false positives followed by real escape in same chunk", () => {
// Fill most of a 16-byte SIMD chunk with false positives, then a real escape
// at the end of the chunk. This tests that the entire chunk is scanned.
const input = "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1c\x1d\x1b[1m!\x1b[0m";
expect(Bun.stripANSI(input)).toBe("\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1c\x1d!");
});