Compare commits

..

1 Commits

Author SHA1 Message Date
Claude Bot
e641fe78f4 fix(node): coerce process.env values to strings like Node.js
Node.js coerces all values assigned to process.env to strings (e.g.,
`process.env.FOO = undefined` results in `"undefined"`). Bun was
storing raw JavaScript values, breaking tools like Vite 8 + rolldown
that expect string values from process.env.

Adds a Proxy wrapper for process.env on POSIX (matching the existing
Windows approach) that coerces values via `'' + value`, which also
throws for Symbols to match Node.js behavior.

Fixes #26388

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-14 23:24:46 +00:00
61 changed files with 417 additions and 2820 deletions

View File

@@ -198,16 +198,13 @@ const myPlugin: BunPlugin = {
};
```
The builder object provides some methods for hooking into parts of the bundling process. Bun implements `onStart`, `onEnd`, `onResolve`, and `onLoad`. It does not yet implement the esbuild hooks `onDispose` and `resolve`. `initialOptions` is partially implemented, being read-only and only having a subset of esbuild's options; use `config` (same thing but with Bun's `BuildConfig` format) instead.
The builder object provides some methods for hooking into parts of the bundling process. Bun implements `onResolve` and `onLoad`; it does not yet implement the esbuild hooks `onStart`, `onEnd`, and `onDispose`, and `resolve` utilities. `initialOptions` is partially implemented, being read-only and only having a subset of esbuild's options; use `config` (same thing but with Bun's `BuildConfig` format) instead.
```ts title="myPlugin.ts" icon="/icons/typescript.svg"
import type { BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "my-plugin",
setup(builder) {
builder.onStart(() => {
/* called when the bundle starts */
});
builder.onResolve(
{
/* onResolve.options */
@@ -228,9 +225,6 @@ const myPlugin: BunPlugin = {
};
},
);
builder.onEnd(result => {
/* called when the bundle is complete */
});
},
};
```

View File

@@ -1184,8 +1184,7 @@ 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`
- `--target=browser` (without HTML entrypoints — see [Standalone HTML](/bundler/standalone-html) for `--compile --target=browser` with `.html` files)
- `--target=node` or `--target=browser`
- `--no-bundle` - we always bundle everything into the executable.
---

View File

@@ -481,16 +481,6 @@ 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

@@ -15,7 +15,6 @@ Plugins can register callbacks to be run at various points in the lifecycle of a
- `onResolve()`: Run before a module is resolved
- `onLoad()`: Run before a module is loaded
- `onBeforeParse()`: Run zero-copy native addons in the parser thread before a file is parsed
- `onEnd()`: Run after the bundle is complete
## Reference
@@ -40,7 +39,6 @@ type PluginBuilder = {
exports?: Record<string, any>;
},
) => void;
onEnd(callback: (result: BuildOutput) => void | Promise<void>): void;
config: BuildConfig;
};
@@ -425,53 +423,3 @@ This lifecycle callback is run immediately before a file is parsed by Bun's bund
As input, it receives the file's contents and can optionally return new source code.
<Info>This callback can be called from any thread and so the napi module implementation must be thread-safe.</Info>
### onEnd
```ts
onEnd(callback: (result: BuildOutput) => void | Promise<void>): void;
```
Registers a callback to be run after the bundle is complete. The callback receives the [`BuildOutput`](/docs/bundler#outputs) object containing the build results, including output files and any build messages.
```ts title="index.ts" icon="/icons/typescript.svg"
const result = await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
plugins: [
{
name: "onEnd example",
setup(build) {
build.onEnd(result => {
console.log(`Build completed with ${result.outputs.length} files`);
for (const log of result.logs) {
console.log(log);
}
});
},
},
],
});
```
The callback can return a `Promise`. The build output promise from `Bun.build()` will not resolve until all `onEnd()` callbacks have completed.
```ts title="index.ts" icon="/icons/typescript.svg"
const result = await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
plugins: [
{
name: "Upload to S3",
setup(build) {
build.onEnd(async result => {
if (!result.success) return;
for (const output of result.outputs) {
await uploadToS3(output);
}
});
},
},
],
});
```

View File

@@ -1,314 +0,0 @@
---
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/standalone-html", "/bundler/css", "/bundler/loaders"]
"pages": ["/bundler/html-static", "/bundler/css", "/bundler/loaders"]
},
{
"group": "Single File Executable",

View File

@@ -2781,17 +2781,11 @@ declare module "bun" {
outdir?: string;
/**
* Create a standalone executable or self-contained HTML.
* Create a standalone executable
*
* 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
@@ -2809,13 +2803,6 @@ 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,14 +183,13 @@ 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 = !force_inline and contents.len >= COPY_THRESHOLD and unique_key != null;
const should_copy = contents.len >= COPY_THRESHOLD and unique_key != null;
if (should_copy) return;
this.url_for_css = url_for_css: {

View File

@@ -1140,14 +1140,14 @@ export fn Bun__runVirtualModule(globalObject: *JSGlobalObject, specifier_ptr: *c
fn getHardcodedModule(jsc_vm: *VirtualMachine, specifier: bun.String, hardcoded: HardcodedModule) ?ResolvedSource {
analytics.Features.builtin_modules.insert(hardcoded);
return switch (hardcoded) {
.@"bun:main" => if (jsc_vm.entry_point.generated) .{
.@"bun:main" => .{
.allocator = null,
.source_code = bun.String.cloneUTF8(jsc_vm.entry_point.source.contents),
.specifier = specifier,
.source_url = specifier,
.tag = .esm,
.source_code_needs_deref = true,
} else null,
},
.@"bun:internal-for-testing" => {
if (!Environment.isDebug) {
if (!is_allowed_to_use_internal_testing_apis)

View File

@@ -1616,7 +1616,7 @@ fn _resolve(
if (strings.eqlComptime(std.fs.path.basename(specifier), Runtime.Runtime.Imports.alt_name)) {
ret.path = Runtime.Runtime.Imports.Name;
return;
} else if (strings.eqlComptime(specifier, main_file_name) and jsc_vm.entry_point.generated) {
} else if (strings.eqlComptime(specifier, main_file_name)) {
ret.result = null;
ret.path = jsc_vm.entry_point.source.path.text;
return;

View File

@@ -977,57 +977,45 @@ pub const JSBundler = struct {
}
if (this.compile) |*compile| {
// 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;
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 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 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, "index")) {
outfile = std.fs.path.basename(std.fs.path.dirname(entry_point) orelse "index");
}
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 (strings.eqlComptime(outfile, "index")) {
outfile = std.fs.path.basename(std.fs.path.dirname(entry_point) orelse "index");
}
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);
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);
}
}
@@ -1038,20 +1026,6 @@ 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

@@ -4,7 +4,7 @@ const TimerObjectInternals = @This();
/// Identifier for this timer that is exposed to JavaScript (by `+timer`)
id: i32 = -1,
interval: u31 = 0,
this_value: jsc.JSRef = .empty(),
strong_this: jsc.Strong.Optional = .empty,
flags: Flags = .{},
/// Used by:
@@ -76,41 +76,31 @@ pub fn runImmediateTask(this: *TimerObjectInternals, vm: *VirtualMachine) bool {
// loop alive other than setImmediates
(!this.flags.is_keeping_event_loop_alive and !vm.isEventLoopAliveExcludingImmediates()))
{
this.setEnableKeepingEventLoopAlive(vm, false);
this.this_value.downgrade();
this.deref();
return false;
}
const timer = this.this_value.tryGet() orelse {
const timer = this.strong_this.get() orelse {
if (Environment.isDebug) {
@panic("TimerObjectInternals.runImmediateTask: this_object is null");
}
this.setEnableKeepingEventLoopAlive(vm, false);
this.deref();
return false;
};
const globalThis = vm.global;
this.this_value.downgrade();
this.strong_this.deinit();
this.eventLoopTimer().state = .FIRED;
this.setEnableKeepingEventLoopAlive(vm, false);
timer.ensureStillAlive();
vm.eventLoop().enter();
const callback = ImmediateObject.js.callbackGetCached(timer).?;
const arguments = ImmediateObject.js.argumentsGetCached(timer).?;
this.ref();
const exception_thrown = this.run(globalThis, timer, callback, arguments, this.asyncID(), vm);
this.deref();
const exception_thrown = brk: {
this.ref();
defer {
if (this.eventLoopTimer().state == .FIRED) {
this.deref();
}
this.deref();
}
break :brk this.run(globalThis, timer, callback, arguments, this.asyncID(), vm);
};
// --- after this point, the timer is no longer guaranteed to be alive ---
if (this.eventLoopTimer().state == .FIRED) {
this.deref();
}
vm.eventLoop().exitMaybeDrainMicrotasks(!exception_thrown) catch return true;
@@ -130,13 +120,7 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
this.eventLoopTimer().state = .FIRED;
const globalThis = vm.global;
const this_object = this.this_value.tryGet() orelse {
this.setEnableKeepingEventLoopAlive(vm, false);
this.flags.has_cleared_timer = true;
this.this_value.downgrade();
this.deref();
return;
};
const this_object = this.strong_this.get().?;
const callback: JSValue, const arguments: JSValue, var idle_timeout: JSValue, var repeat: JSValue = switch (kind) {
.setImmediate => .{
@@ -159,7 +143,7 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
}
this.setEnableKeepingEventLoopAlive(vm, false);
this.flags.has_cleared_timer = true;
this.this_value.downgrade();
this.strong_this.deinit();
this.deref();
return;
@@ -168,7 +152,7 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
var time_before_call: timespec = undefined;
if (kind != .setInterval) {
this.this_value.downgrade();
this.strong_this.clearWithoutDeallocation();
} else {
time_before_call = timespec.msFromNow(.allow_mocked_time, this.interval);
}
@@ -255,7 +239,7 @@ fn convertToInterval(this: *TimerObjectInternals, global: *JSGlobalObject, timer
// https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L613
TimeoutObject.js.idleTimeoutSetCached(timer, global, repeat);
this.this_value.setStrong(timer, global);
this.strong_this.set(global, timer);
this.flags.kind = .setInterval;
this.interval = new_interval;
this.reschedule(timer, vm, global);
@@ -313,7 +297,7 @@ pub fn init(
this.reschedule(timer, vm, global);
}
this.this_value.setStrong(timer, global);
this.strong_this.set(global, timer);
}
pub fn doRef(this: *TimerObjectInternals, _: *jsc.JSGlobalObject, this_value: JSValue) JSValue {
@@ -343,7 +327,7 @@ pub fn doRefresh(this: *TimerObjectInternals, globalObject: *jsc.JSGlobalObject,
return this_value;
}
this.this_value.setStrong(this_value, globalObject);
this.strong_this.set(globalObject, this_value);
this.reschedule(this_value, VirtualMachine.get(), globalObject);
return this_value;
@@ -366,18 +350,12 @@ pub fn cancel(this: *TimerObjectInternals, vm: *VirtualMachine) void {
this.setEnableKeepingEventLoopAlive(vm, false);
this.flags.has_cleared_timer = true;
if (this.flags.kind == .setImmediate) {
// Release the strong reference so the GC can collect the JS object.
// The immediate task is still in the event loop queue and will be skipped
// by runImmediateTask when it sees has_cleared_timer == true.
this.this_value.downgrade();
return;
}
if (this.flags.kind == .setImmediate) return;
const was_active = this.eventLoopTimer().state == .ACTIVE;
this.eventLoopTimer().state = .CANCELLED;
this.this_value.downgrade();
this.strong_this.deinit();
if (was_active) {
vm.timer.remove(this.eventLoopTimer());
@@ -464,12 +442,12 @@ pub fn getDestroyed(this: *TimerObjectInternals) bool {
}
pub fn finalize(this: *TimerObjectInternals) void {
this.this_value.finalize();
this.strong_this.deinit();
this.deref();
}
pub fn deinit(this: *TimerObjectInternals) void {
this.this_value.deinit();
this.strong_this.deinit();
const vm = VirtualMachine.get();
const kind = this.flags.kind;

View File

@@ -1707,15 +1707,6 @@ 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,18 +24,6 @@ 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)
@@ -55,13 +43,8 @@ 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 (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;
}
if (const auto index = SIMD::findFirstNonZeroIndex(chunkIsEsc))
return it + *index;
}
// Check remaining characters

View File

@@ -641,16 +641,13 @@ JSC_DEFINE_CUSTOM_GETTER(errorInstanceLazyStackCustomGetter, (JSGlobalObject * g
OrdinalNumber column;
String sourceURL;
auto stackTrace = errorObject->stackTrace();
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 JSValue::encode(jsUndefined());
}
JSValue 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);
@@ -690,27 +687,17 @@ 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 deleteScope(vm, VM::DeletePropertyMode::IgnoreConfigurable);
VM::DeletePropertyModeScope scope(vm, VM::DeletePropertyMode::IgnoreConfigurable);
DeletePropertySlot slot;
JSObject::deleteProperty(instance, globalObject, propertyName, slot);
}
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);
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

@@ -391,7 +391,16 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject)
args.append(object);
args.append(keyArray);
args.append(editWindowsEnvVar);
auto clientData = WebCore::clientData(vm);
#else
// Wrap the env object in a Proxy that coerces all assigned values to strings.
// This matches Node.js behavior where `process.env.FOO = undefined` results in
// `process.env.FOO === "undefined"` (string), not `undefined` (the value).
JSC::JSFunction* getSourceEvent = JSC::JSFunction::create(vm, globalObject, processObjectInternalsPosixEnvCodeGenerator(vm), globalObject);
RETURN_IF_EXCEPTION(scope, {});
JSC::MarkedArgumentBuffer args;
args.append(object);
#endif
JSC::CallData callData = JSC::getCallData(getSourceEvent);
NakedPtr<JSC::Exception> returnedException = nullptr;
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, getSourceEvent, callData, globalObject->globalThis(), args, returnedException);
@@ -403,8 +412,5 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject)
}
RELEASE_AND_RETURN(scope, result);
#else
return object;
#endif
}
}

View File

@@ -3026,20 +3026,22 @@ JSC::EncodedJSValue JSC__JSValue__fromEntries(JSC::JSGlobalObject* globalObject,
return JSC::JSValue::encode(JSC::constructEmptyObject(globalObject));
}
JSC::JSObject* object = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min(static_cast<unsigned int>(initialCapacity), JSFinalObject::maxInlineCapacity));
JSC::JSObject* object = nullptr;
{
JSC::ObjectInitializationScope initializationScope(vm);
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);
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, {});
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);
}
}
}

View File

@@ -1154,14 +1154,6 @@ pub const FetchTasklet = struct {
}
}
/// Whether the request body should skip chunked transfer encoding framing.
/// True for upgraded connections (e.g. WebSocket) or when the user explicitly
/// set Content-Length without setting Transfer-Encoding.
fn skipChunkedFraming(this: *const FetchTasklet) bool {
return this.upgraded_connection or
(this.request_headers.get("content-length") != null and this.request_headers.get("transfer-encoding") == null);
}
pub fn writeRequestData(this: *FetchTasklet, data: []const u8) ResumableSinkBackpressure {
log("writeRequestData {}", .{data.len});
if (this.signal) |signal| {
@@ -1183,7 +1175,7 @@ pub const FetchTasklet = struct {
// dont have backpressure so we will schedule the data to be written
// if we have backpressure the onWritable will drain the buffer
needs_schedule = stream_buffer.isEmpty();
if (this.skipChunkedFraming()) {
if (this.upgraded_connection) {
bun.handleOom(stream_buffer.write(data));
} else {
//16 is the max size of a hex number size that represents 64 bits + 2 for the \r\n
@@ -1217,14 +1209,15 @@ pub const FetchTasklet = struct {
}
this.abortTask();
} else {
if (!this.skipChunkedFraming()) {
// Using chunked transfer encoding, send the terminating chunk
if (!this.upgraded_connection) {
// If is not upgraded we need to send the terminating chunk
const thread_safe_stream_buffer = this.request_body_streaming_buffer orelse return;
const stream_buffer = thread_safe_stream_buffer.acquire();
defer thread_safe_stream_buffer.release();
bun.handleOom(stream_buffer.write(http.end_of_chunked_http1_1_encoding_response_body));
}
if (this.http) |http_| {
// just tell to write the end of the chunked encoding aka 0\r\n\r\n
http.http_thread.scheduleRequestWrite(http_, .end);
}
}

View File

@@ -55,16 +55,6 @@ 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| {
@@ -78,16 +68,6 @@ pub const Chunk = struct {
}
pub fn getCSSChunkForHTML(this: *const Chunk, chunks: []Chunk) ?*Chunk {
// Look up the CSS chunk via the JS chunk's css_chunks indices.
// This correctly handles deduplicated CSS chunks that are shared
// across multiple HTML entry points (see issue #23668).
if (this.getJSChunkForHTML(chunks)) |js_chunk| {
const css_chunk_indices = js_chunk.content.javascript.css_chunks;
if (css_chunk_indices.len > 0) {
return &chunks[css_chunk_indices[0]];
}
}
// Fallback: match by entry_point_id for cases without a JS chunk.
const entry_point_id = this.entry_point.entry_point_id;
for (chunks) |*other| {
if (other.content == .css) {
@@ -147,54 +127,6 @@ 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,
@@ -237,40 +169,6 @@ 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,
),
};
}
@@ -286,7 +184,6 @@ 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);
@@ -312,37 +209,12 @@ 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];
@@ -411,37 +283,6 @@ 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,7 +68,6 @@ 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, transpiler.options.compile_to_standalone_html);
ast.addUrlForCss(allocator, source, "text/plain", null);
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, transpiler.options.compile_to_standalone_html);
ast.addUrlForCss(allocator, source, "text/html", null);
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, transpiler.options.compile_to_standalone_html);
ast.addUrlForCss(allocator, source, null, unique_key);
return ast;
},
}

View File

@@ -965,7 +965,6 @@ 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;
@@ -1993,19 +1992,6 @@ 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;
@@ -3697,20 +3683,7 @@ pub const BundleV2 = struct {
}
}
const import_record_loader = brk: {
const resolved_loader = import_record.loader orelse path.loader(&transpiler.options.loaders) orelse .file;
// When an HTML file references a URL asset (e.g. <link rel="manifest" href="./manifest.json" />),
// the file must be copied to the output directory as-is. If the resolved loader would
// parse/transform the file (e.g. .json, .toml) rather than copy it, force the .file loader
// so that `shouldCopyForBundling()` returns true and the asset is emitted.
// Only do this for HTML sources — CSS url() imports should retain their original behavior.
if (loader == .html and import_record.kind == .url and !resolved_loader.shouldCopyForBundling() and
!resolved_loader.isJavaScriptLike() and !resolved_loader.isCSS() and resolved_loader != .html)
{
break :brk Loader.file;
}
break :brk resolved_loader;
};
const import_record_loader = import_record.loader orelse path.loader(&transpiler.options.loaders) orelse .file;
import_record.loader = import_record_loader;
const is_html_entrypoint = import_record_loader == .html and target.isServerSide() and this.transpiler.options.dev_server == null;

View File

@@ -150,7 +150,6 @@ pub const ClientEntryPoint = struct {
pub const ServerEntryPoint = struct {
source: logger.Source = undefined,
generated: bool = false,
pub fn generate(
entry: *ServerEntryPoint,
@@ -231,7 +230,6 @@ pub const ServerEntryPoint = struct {
entry.source = logger.Source.initPathString(name, code);
entry.source.path.text = name;
entry.source.path.namespace = "server-entry";
entry.generated = true;
}
};

View File

@@ -97,8 +97,7 @@ 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;
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) };
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) };
}
pub fn insertForChunk(this: *OutputFileList, output_file: options.OutputFile) u32 {

View File

@@ -22,6 +22,7 @@ pub noinline fn computeChunks(
const entry_source_indices = this.graph.entry_points.items(.source_index);
const css_asts = this.graph.ast.items(.css);
const css_chunking = this.options.css_chunking;
var html_chunks = bun.StringArrayHashMap(Chunk).init(temp_allocator);
const loaders = this.parse_graph.input_files.items(.loader);
const ast_targets = this.graph.ast.items(.target);
@@ -147,11 +148,10 @@ pub noinline fn computeChunks(
if (css_source_indices.len > 0) {
const order = this.findImportedFilesInCSSOrder(temp_allocator, css_source_indices.slice());
// Always use content-based hashing for CSS chunk deduplication.
// This ensures that when multiple JS entry points import the
// same CSS files, they share a single CSS output chunk rather
// than producing duplicates that collide on hash-based naming.
const hash_to_use = brk: {
const use_content_based_key = css_chunking or has_server_html_imports;
const hash_to_use = if (!use_content_based_key)
bun.hash(try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len)))
else brk: {
var hasher = std.hash.Wyhash.init(5);
bun.writeAnyToHasher(&hasher, order.len);
for (order.slice()) |x| x.hash(&hasher);
@@ -322,10 +322,7 @@ pub noinline fn computeChunks(
const remapped_css_indexes = try temp_allocator.alloc(u32, css_chunks.count());
const css_chunk_values = css_chunks.values();
// Use sorted_chunks.len as the starting index because HTML chunks
// may be interleaved with JS chunks, so js_chunks.count() would be
// incorrect when HTML entry points are present.
for (sorted_css_keys, sorted_chunks.len..) |key, sorted_index| {
for (sorted_css_keys, js_chunks.count()..) |key, sorted_index| {
const index = css_chunks.getIndex(key) orelse unreachable;
sorted_chunks.appendAssumeCapacity(css_chunk_values[index]);
remapped_css_indexes[index] = @intCast(sorted_index);

View File

@@ -382,8 +382,7 @@ 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 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)));
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));
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");
@@ -394,73 +393,13 @@ 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, standalone_chunk_contents);
try c.writeOutputFilesToDisk(root_path, chunks, &output_files);
} else {
// In-memory build (also used for standalone mode)
// In-memory build
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)
@@ -468,32 +407,18 @@ pub fn generateChunksInParallel(
else
c.options.public_path;
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);
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");
var sourcemap_output_file: ?options.OutputFile = null;
const input_path = try bun.default_allocator.dupe(
@@ -745,26 +670,7 @@ 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 });
}
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;
output_files.insertAdditionalOutputFiles(c.parse_graph.additional_output_files.items);
}
return output_files.take();

View File

@@ -42,7 +42,6 @@ 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,
@@ -50,7 +49,6 @@ 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));
@@ -106,18 +104,6 @@ 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 {
@@ -156,39 +142,17 @@ 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) = .{};
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);
}
// 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);
}
return array;
}
@@ -206,12 +170,7 @@ 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) {
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;
}
this.addHeadTags(end) catch return .stop;
} else {
this.end_tag_indices.body = @intCast(this.output.items.len);
}
@@ -221,13 +180,7 @@ 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) {
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;
}
this.addHeadTags(end) catch return .stop;
} else {
this.end_tag_indices.html = @intCast(this.output.items.len);
}
@@ -246,7 +199,6 @@ 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),
@@ -257,7 +209,6 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
.head = null,
},
.added_head_tags = false,
.added_body_script = false,
};
HTMLScanner.HTMLProcessor(HTMLLoader, true).run(
@@ -282,27 +233,14 @@ 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 or !html_loader.added_body_script) {
if (!html_loader.added_head_tags) {
@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();
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;
const slices = html_loader.getHeadTags(allocator);
for (slices.slice()) |slice| {
bun.handleOom(html_loader.output.appendSlice(slice));
allocator.free(slice);
}
}
break :brk if (Environment.isDebug) undefined else 0; // value is ignored. fail loud if hit in debug

View File

@@ -3,7 +3,6 @@ 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();
@@ -43,29 +42,6 @@ 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();
@@ -89,31 +65,17 @@ pub fn writeOutputFilesToDisk(
else
c.resolver.opts.public_path;
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 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 source_map_output_file: ?options.OutputFile = null;
@@ -356,7 +318,7 @@ pub fn writeOutputFilesToDisk(
.js,
.hash = chunk.template.placeholder.hash,
.output_kind = output_kind,
.loader = chunk.content.loader(),
.loader = .js,
.source_map_index = source_map_index,
.bytecode_index = bytecode_index,
.size = @as(u32, @truncate(code_result.buffer.len)),
@@ -382,15 +344,10 @@ pub fn writeOutputFilesToDisk(
},
}));
// 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 });
// 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 });
}
// 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,6 +460,7 @@ 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,7 +3,6 @@ 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;
@@ -99,75 +98,44 @@ pub const BuildCommand = struct {
var was_renamed_from_index = false;
if (ctx.bundler_options.compile) {
if (ctx.bundler_options.transform_only) {
Output.prettyErrorln("<r><red>error<r><d>:<r> --compile does not support --no-bundle", .{});
if (ctx.bundler_options.outdir.len > 0) {
Output.prettyErrorln("<r><red>error<r><d>:<r> cannot use --compile with --outdir", .{});
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;
};
const base_public_path = bun.StandaloneModuleGraph.targetBasePublicPath(compile_target.os, "root/");
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.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];
}
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]);
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");
}
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;
if (strings.eqlComptime(outfile, "bun")) {
outfile = std.fs.path.basename(std.fs.path.dirname(this_transpiler.options.entry_points[0]) orelse "bun");
}
}
const base_public_path = bun.StandaloneModuleGraph.targetBasePublicPath(compile_target.os, "root/");
// 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;
}
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;
}
}

View File

@@ -719,21 +719,7 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
if (body_len > 0 or this.method.hasRequestBody()) {
if (this.flags.is_streaming_request_body) {
if (original_content_length) |content_length| {
if (add_transfer_encoding) {
// User explicitly set Content-Length and did not set Transfer-Encoding;
// preserve Content-Length instead of using chunked encoding.
// This matches Node.js behavior where an explicit Content-Length is always honored.
request_headers_buf[header_count] = .{
.name = content_length_header_name,
.value = content_length,
};
header_count += 1;
}
// If !add_transfer_encoding, the user explicitly set Transfer-Encoding,
// which was already added to request_headers_buf. We respect that and
// do not add Content-Length (they are mutually exclusive per HTTP/1.1).
} else if (add_transfer_encoding and this.flags.upgrade_state == .none) {
if (add_transfer_encoding and this.flags.upgrade_state == .none) {
request_headers_buf[header_count] = chunked_encoded_header;
header_count += 1;
}

View File

@@ -623,17 +623,6 @@ pub const PackageInstaller = struct {
// else => unreachable,
// };
// If a newly computed integrity hash is available (e.g. for a GitHub
// tarball) and the lockfile doesn't already have one, persist it so
// the lockfile gets re-saved with the hash.
if (data.integrity.tag.isSupported()) {
var pkg_metas = this.lockfile.packages.items(.meta);
if (!pkg_metas[package_id].integrity.tag.isSupported()) {
pkg_metas[package_id].integrity = data.integrity;
this.manager.options.enable.force_save_lockfile = true;
}
}
if (this.manager.task_queue.fetchRemove(task_id)) |removed| {
var callbacks = removed.value;
defer callbacks.deinit(this.manager.allocator);

View File

@@ -133,12 +133,6 @@ pub fn processExtractedTarballPackage(
break :package pkg;
};
// Store the tarball integrity hash so the lockfile can pin the
// exact content downloaded from the remote (GitHub) server.
if (data.integrity.tag.isSupported()) {
package.meta.integrity = data.integrity;
}
package = manager.lockfile.appendPackage(package) catch unreachable;
package_id.* = package.meta.id;

View File

@@ -23,26 +23,7 @@ pub inline fn run(this: *const ExtractTarball, log: *logger.Log, bytes: []const
return error.IntegrityCheckFailed;
}
}
var result = try this.extract(log, bytes);
// Compute and store SHA-512 integrity hash for GitHub tarballs so the
// lockfile can pin the exact tarball content. On subsequent installs the
// hash stored in the lockfile is forwarded via this.integrity and verified
// above, preventing a compromised server from silently swapping the tarball.
if (this.resolution.tag == .github) {
if (this.integrity.tag.isSupported()) {
// Re-installing with an existing lockfile: integrity was already
// verified above, propagate the known value to ExtractData so that
// the lockfile keeps it on re-serialisation.
result.integrity = this.integrity;
} else {
// First install (no integrity in the lockfile yet): compute it.
result.integrity = .{ .tag = .sha512 };
Crypto.SHA512.hash(bytes, result.integrity.value[0..Crypto.SHA512.digest]);
}
}
return result;
return this.extract(log, bytes);
}
pub fn buildURL(
@@ -566,7 +547,6 @@ const string = []const u8;
const Npm = @import("./npm.zig");
const std = @import("std");
const Crypto = @import("../sha.zig").Hashers;
const FileSystem = @import("../fs.zig").FileSystem;
const Integrity = @import("./integrity.zig").Integrity;
const Resolution = @import("./resolution.zig").Resolution;

View File

@@ -209,7 +209,6 @@ pub const ExtractData = struct {
path: string = "",
buf: []u8 = "",
} = null,
integrity: Integrity = .{},
};
pub const DependencyInstallContext = struct {
@@ -272,7 +271,6 @@ pub const VersionSlice = external.VersionSlice;
pub const Dependency = @import("./dependency.zig");
pub const Behavior = @import("./dependency.zig").Behavior;
pub const Integrity = @import("./integrity.zig").Integrity;
pub const Lockfile = @import("./lockfile.zig");
pub const PatchedDep = Lockfile.PatchedDep;

View File

@@ -644,16 +644,9 @@ pub const Stringifier = struct {
&path_buf,
);
if (pkg_meta.integrity.tag.isSupported()) {
try writer.print(", {f}, \"{f}\"]", .{
repo.resolved.fmtJson(buf, .{}),
pkg_meta.integrity,
});
} else {
try writer.print(", {f}]", .{
repo.resolved.fmtJson(buf, .{}),
});
}
try writer.print(", {f}]", .{
repo.resolved.fmtJson(buf, .{}),
});
},
else => unreachable,
}
@@ -1892,15 +1885,6 @@ pub fn parseIntoBinaryLockfile(
};
@field(res.value, @tagName(tag)).resolved = try string_buf.append(bun_tag_str);
// Optional integrity hash (added to pin tarball content)
if (i < pkg_info.len) {
const integrity_expr = pkg_info.at(i);
if (integrity_expr.asString(allocator)) |integrity_str| {
pkg.meta.integrity = Integrity.parse(integrity_str);
i += 1;
}
}
},
else => {},
}

View File

@@ -393,7 +393,9 @@ export function windowsEnv(
set(_, p, value) {
const k = String(p).toUpperCase();
$assert(typeof p === "string"); // proxy is only string and symbol. the symbol would have thrown by now
value = String(value); // If toString() throws, we want to avoid it existing in the envMapList
// Use string concatenation to coerce value to string. This throws for Symbols,
// matching Node.js behavior, and ensures the value is always a string.
value = "" + value;
if (!(k in internalEnv) && !envMapList.includes(p)) {
envMapList.push(p);
}
@@ -434,6 +436,42 @@ export function windowsEnv(
});
}
export function posixEnv(internalEnv: InternalEnvMap) {
return new Proxy(internalEnv, {
get(target, p) {
return typeof p === "string" ? target[p] : undefined;
},
set(target, p, value) {
const k = String(p);
// Coerce all values to strings to match Node.js behavior.
// Use string concatenation ('' + value) instead of String(value) because
// concatenation throws for Symbols, matching Node.js behavior.
value = "" + value;
target[k] = value;
return true;
},
has(target, p) {
return typeof p !== "symbol" ? String(p) in target : false;
},
deleteProperty(target, p) {
return typeof p !== "symbol" ? delete target[String(p)] : false;
},
defineProperty(target, p, attributes) {
const k = String(p);
if ("value" in attributes) {
attributes = { ...attributes, value: "" + attributes.value };
}
return $Object.$defineProperty(target, k, attributes);
},
getOwnPropertyDescriptor(target, p) {
return typeof p === "string" ? Reflect.getOwnPropertyDescriptor(target, p) : undefined;
},
ownKeys(target) {
return Reflect.ownKeys(target);
},
});
}
export function getChannel() {
const EventEmitter = require("node:events");
const setRef = $newZigFunction("node_cluster_binding.zig", "setRef", 1);

View File

@@ -378,15 +378,10 @@ class AssertionError extends Error {
this.operator = operator;
}
ErrorCaptureStackTrace(this, stackStartFn || stackStartFunction);
// 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);
}
// 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);
}
// Create error message including the error code in the name.
this.stack; // eslint-disable-line no-unused-expressions

View File

@@ -51,15 +51,6 @@ 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;
@@ -261,14 +252,18 @@ const OutgoingMessagePrototype = {
removeHeader(name) {
validateString(name, "name");
throwHeadersSentIfNecessary(this, "remove");
if ((this._header !== undefined && this._header !== null) || this[headerStateSymbol] === NodeHTTPHeaderState.sent) {
throw $ERR_HTTP_HEADERS_SENT("remove");
}
const headers = this[headersSymbol];
if (!headers) return;
headers.delete(name);
},
setHeader(name, value) {
throwHeadersSentIfNecessary(this, "set");
if ((this._header !== undefined && this._header !== null) || this[headerStateSymbol] == NodeHTTPHeaderState.sent) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
validateHeaderName(name);
validateHeaderValue(name, value);
const headers = (this[headersSymbol] ??= new Headers());
@@ -276,7 +271,9 @@ const OutgoingMessagePrototype = {
return this;
},
setHeaders(headers) {
throwHeadersSentIfNecessary(this, "set");
if (this._header || this[headerStateSymbol] !== NodeHTTPHeaderState.none) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
if (!headers || $isArray(headers) || typeof headers.keys !== "function" || typeof headers.get !== "function") {
throw $ERR_INVALID_ARG_TYPE("headers", ["Headers", "Map"], headers);

View File

@@ -766,13 +766,19 @@ pub extern fn napi_type_tag_object(env: napi_env, _: napi_value, _: [*c]const na
pub extern fn napi_check_object_type_tag(env: napi_env, _: napi_value, _: [*c]const napi_type_tag, _: *bool) napi_status;
// do nothing for both of these
pub export fn napi_open_callback_scope(_: napi_env, _: napi_value, _: *anyopaque, _: *anyopaque) napi_status {
pub export fn napi_open_callback_scope(env_: napi_env, _: napi_value, _: *anyopaque, _: *anyopaque) napi_status {
log("napi_open_callback_scope", .{});
return @intFromEnum(NapiStatus.ok);
const env = env_ orelse {
return envIsNull();
};
return env.ok();
}
pub export fn napi_close_callback_scope(_: napi_env, _: *anyopaque) napi_status {
pub export fn napi_close_callback_scope(env_: napi_env, _: *anyopaque) napi_status {
log("napi_close_callback_scope", .{});
return @intFromEnum(NapiStatus.ok);
const env = env_ orelse {
return envIsNull();
};
return env.ok();
}
pub extern fn napi_throw(env: napi_env, @"error": napi_value) napi_status;
pub extern fn napi_throw_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status;

View File

@@ -1833,7 +1833,6 @@ 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

@@ -12,32 +12,24 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __hasOwnProp = Object.prototype.hasOwnProperty;
// Shared getter/setter functions: .bind(obj, key) avoids creating a closure
// and JSLexicalEnvironment per property. BoundFunction is much cheaper.
// Must be regular functions (not arrows) so .bind() can set `this`.
function __accessProp(key) {
return this[key];
}
// This is used to implement "export * from" statements. It copies properties
// from the imported module to the current module's ESM export object. If the
// current module is an entry point and the target format is CommonJS, we
// also copy the properties to "module.exports" in addition to our module's
// internal ESM export object.
export var __reExport = (target, mod, secondTarget) => {
var keys = __getOwnPropNames(mod);
for (let key of keys)
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(target, key) && key !== "default")
__defProp(target, key, {
get: __accessProp.bind(mod, key),
get: () => mod[key],
enumerable: true,
});
if (secondTarget) {
for (let key of keys)
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(secondTarget, key) && key !== "default")
__defProp(secondTarget, key, {
get: __accessProp.bind(mod, key),
get: () => mod[key],
enumerable: true,
});
@@ -45,22 +37,11 @@ export var __reExport = (target, mod, secondTarget) => {
}
};
/*__PURE__*/
var __toESMCache_node;
/*__PURE__*/
var __toESMCache_esm;
// Converts the module from CommonJS to ESM. When in node mode (i.e. in an
// ".mjs" file, package.json has "type: module", or the "__esModule" export
// in the CommonJS file is falsy or missing), the "default" property is
// overridden to point to the original CommonJS exports object instead.
export var __toESM = (mod, isNodeMode, target) => {
var canCache = mod != null && typeof mod === "object";
if (canCache) {
var cache = isNodeMode ? (__toESMCache_node ??= new WeakMap()) : (__toESMCache_esm ??= new WeakMap());
var cached = cache.get(mod);
if (cached) return cached;
}
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to =
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
@@ -72,34 +53,34 @@ export var __toESM = (mod, isNodeMode, target) => {
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: __accessProp.bind(mod, key),
get: () => mod[key],
enumerable: true,
});
if (canCache) cache.set(mod, to);
return to;
};
// Converts the module from ESM to CommonJS. This clones the input module
// object with the addition of a non-enumerable "__esModule" property set
// to "true", which overwrites any existing export named "__esModule".
export var __toCommonJS = from => {
var entry = (__moduleCache ??= new WeakMap()).get(from),
var __moduleCache = /* @__PURE__ */ new WeakMap();
export var __toCommonJS = /* @__PURE__ */ from => {
var entry = __moduleCache.get(from),
desc;
if (entry) return entry;
entry = __defProp({}, "__esModule", { value: true });
if ((from && typeof from === "object") || typeof from === "function")
for (var key of __getOwnPropNames(from))
if (!__hasOwnProp.call(entry, key))
__getOwnPropNames(from).map(
key =>
!__hasOwnProp.call(entry, key) &&
__defProp(entry, key, {
get: __accessProp.bind(from, key),
get: () => from[key],
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable,
});
}),
);
__moduleCache.set(from, entry);
return entry;
};
/*__PURE__*/
var __moduleCache;
// When you do know the module is CJS
export var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -116,10 +97,6 @@ export var __name = (target, name) => {
// ESM export -> CJS export
// except, writable incase something re-exports
var __returnValue = v => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
export var __export = /* @__PURE__ */ (target, all) => {
for (var name in all)
@@ -127,19 +104,15 @@ export var __export = /* @__PURE__ */ (target, all) => {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name),
set: newValue => (all[name] = () => newValue),
});
};
function __exportValueSetter(name, newValue) {
this[name] = newValue;
}
export var __exportValue = (target, all) => {
for (var name in all) {
__defProp(target, name, {
get: __accessProp.bind(all, name),
set: __exportValueSetter.bind(all, name),
get: () => all[name],
set: newValue => (all[name] = newValue),
enumerable: true,
configurable: true,
});

View File

@@ -2,17 +2,13 @@
exports[`Bun.build Bun.write(BuildArtifact) 1`] = `
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
set: (newValue) => all[name] = () => newValue
});
};
@@ -35,17 +31,13 @@ NS.then(({ fn: fn2 }) => {
exports[`Bun.build outdir + reading out blobs works 1`] = `
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
set: (newValue) => all[name] = () => newValue
});
};
@@ -66,27 +58,23 @@ NS.then(({ fn: fn2 }) => {
"
`;
exports[`Bun.build BuildArtifact properties: hash 1`] = `"est79qzq"`;
exports[`Bun.build BuildArtifact properties: hash 1`] = `"d1c7nm6t"`;
exports[`Bun.build BuildArtifact properties + entry.naming: hash 1`] = `"7gfnt0h6"`;
exports[`Bun.build BuildArtifact properties + entry.naming: hash 1`] = `"rm7e36cf"`;
exports[`Bun.build BuildArtifact properties sourcemap: hash index.js 1`] = `"est79qzq"`;
exports[`Bun.build BuildArtifact properties sourcemap: hash index.js 1`] = `"d1c7nm6t"`;
exports[`Bun.build BuildArtifact properties sourcemap: hash index.js.map 1`] = `"00000000"`;
exports[`Bun.build new Response(BuildArtifact) sets content type: response text 1`] = `
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
set: (newValue) => all[name] = () => newValue
});
};

View File

@@ -1113,7 +1113,7 @@ describe("bundler", () => {
snapshotSourceMap: {
"entry.js.map": {
files: ["../node_modules/react/index.js", "../entry.js"],
mappingsExactMatch: "miBACA,WAAW,IAAQ,EAAE,ICDrB,eACA,QAAQ,IAAI,CAAK",
mappingsExactMatch: "qYACA,WAAW,IAAQ,EAAE,ICDrB,eACA,QAAQ,IAAI,CAAK",
},
},
});

View File

@@ -843,131 +843,4 @@ body {
api.expectFile("out/" + jsFile).toContain("sourceMappingURL");
},
});
// Test that multiple HTML entrypoints sharing the same CSS file both get
// the CSS link tag in production mode (css_chunking deduplication).
// Regression test for https://github.com/oven-sh/bun/issues/23668
itBundled("html/SharedCSSProductionMultipleEntries", {
outdir: "out/",
production: true,
files: {
"/entry1.html": `<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./global.css" />
</head>
<body>
<div id="root"></div>
<script src="./main1.tsx"></script>
</body>
</html>`,
"/entry2.html": `<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./global.css" />
</head>
<body>
<div id="root"></div>
<script src="./main2.tsx"></script>
</body>
</html>`,
"/global.css": `h1 { font-size: 24px; }`,
"/main1.tsx": `console.log("entry1");`,
"/main2.tsx": `console.log("entry2");`,
},
entryPoints: ["/entry1.html", "/entry2.html"],
onAfterBundle(api) {
const entry1Html = api.readFile("out/entry1.html");
const entry2Html = api.readFile("out/entry2.html");
// Both HTML files must contain a CSS link tag
const cssMatch1 = entry1Html.match(/href="(.*\.css)"/);
const cssMatch2 = entry2Html.match(/href="(.*\.css)"/);
expect(cssMatch1).not.toBeNull();
expect(cssMatch2).not.toBeNull();
// Both should reference the same deduplicated CSS chunk
expect(cssMatch1![1]).toBe(cssMatch2![1]);
// The CSS file should contain the shared styles
const cssContent = api.readFile("out/" + cssMatch1![1]);
expect(cssContent).toContain("font-size");
// Both HTML files should also have their respective JS bundles
expect(entry1Html).toMatch(/src=".*\.js"/);
expect(entry2Html).toMatch(/src=".*\.js"/);
},
});
// Test manifest.json is copied as an asset and link href is rewritten
itBundled("html/manifest-json", {
outdir: "out/",
files: {
"/index.html": `
<!DOCTYPE html>
<html>
<head>
<link rel="manifest" href="./manifest.json" />
</head>
<body>
<h1>App</h1>
<script src="./app.js"></script>
</body>
</html>`,
"/manifest.json": JSON.stringify({
name: "My App",
short_name: "App",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#000000",
}),
"/app.js": "console.log('hello')",
},
entryPoints: ["/index.html"],
onAfterBundle(api) {
const htmlContent = api.readFile("out/index.html");
// The original manifest.json reference should be rewritten to a hashed filename
expect(htmlContent).not.toContain('manifest.json"');
expect(htmlContent).toMatch(/href="(?:\.\/|\/)?manifest-[a-zA-Z0-9]+\.json"/);
// Extract the hashed manifest filename and verify its content
const manifestMatch = htmlContent.match(/href="(?:\.\/|\/)?(manifest-[a-zA-Z0-9]+\.json)"/);
expect(manifestMatch).not.toBeNull();
const manifestContent = api.readFile("out/" + manifestMatch![1]);
expect(manifestContent).toContain('"name"');
expect(manifestContent).toContain('"My App"');
},
});
// Test that other non-JS/CSS file types referenced via URL imports are copied as assets
itBundled("html/xml-asset", {
outdir: "out/",
files: {
"/index.html": `
<!DOCTYPE html>
<html>
<head>
<link rel="manifest" href="./site.webmanifest" />
</head>
<body>
<h1>App</h1>
</body>
</html>`,
"/site.webmanifest": JSON.stringify({
name: "My App",
icons: [{ src: "/icon.png", sizes: "192x192" }],
}),
},
entryPoints: ["/index.html"],
onAfterBundle(api) {
const htmlContent = api.readFile("out/index.html");
// The webmanifest reference should be rewritten to a hashed filename
expect(htmlContent).not.toContain("site.webmanifest");
expect(htmlContent).toMatch(/href=".*\.webmanifest"/);
},
});
});

View File

@@ -57,17 +57,17 @@ describe("bundler", () => {
"../entry.tsx",
],
mappings: [
["react.development.js:524:'getContextName'", "1:5567:Y1"],
["react.development.js:524:'getContextName'", "1:5412:Y1"],
["react.development.js:2495:'actScopeDepth'", "23:4082:GJ++"],
["react.development.js:696:''Component'", '1:7629:\'Component "%s"'],
["entry.tsx:6:'\"Content-Type\"'", '100:18808:"Content-Type"'],
["entry.tsx:11:'<html>'", "100:19062:void"],
["entry.tsx:23:'await'", "100:19161:await"],
["react.development.js:696:''Component'", '1:7474:\'Component "%s"'],
["entry.tsx:6:'\"Content-Type\"'", '100:18809:"Content-Type"'],
["entry.tsx:11:'<html>'", "100:19063:void"],
["entry.tsx:23:'await'", "100:19163:await"],
],
},
},
expectExactFilesize: {
"out/entry.js": 221895,
"out/entry.js": 221720,
},
run: {
stdout: "<!DOCTYPE html><html><body><h1>Hello World</h1><p>This is an example.</p></body></html>",

View File

@@ -76,17 +76,13 @@ describe("bundler", () => {
expect(bundled).toMatchInlineSnapshot(`
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
set: (newValue) => all[name] = () => newValue
});
};
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -164,7 +160,7 @@ describe("bundler", () => {
var { AsyncEntryPoint: AsyncEntryPoint2 } = await Promise.resolve().then(() => exports_AsyncEntryPoint);
AsyncEntryPoint2();
//# debugId=42062903F19477CF64756E2164756E21
//# debugId=5E85CC0956C6307964756E2164756E21
//# sourceMappingURL=out.js.map
"
`);
@@ -341,17 +337,13 @@ describe("bundler", () => {
expect(bundled).toMatchInlineSnapshot(`
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
set: (newValue) => all[name] = () => newValue
});
};
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -410,7 +402,7 @@ describe("bundler", () => {
var { AsyncEntryPoint: AsyncEntryPoint2 } = await Promise.resolve().then(() => exports_AsyncEntryPoint);
AsyncEntryPoint2();
//# debugId=BF876FBF618133C264756E2164756E21
//# debugId=C92CBF0103732ECC64756E2164756E21
//# sourceMappingURL=out.js.map
"
`);

View File

@@ -2150,7 +2150,10 @@ c {
toplevel-tilde.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
`, */
});
itBundled("css/MetafileCSSBundleTwoToOne", {
// TODO: Bun's bundler doesn't support multiple entry points generating CSS outputs
// with identical content hashes to the same output path. This test exposes that
// limitation. Skip until the bundler can deduplicate or handle this case.
itBundled.skip("css/MetafileCSSBundleTwoToOne", {
files: {
"/foo/entry.js": /* js */ `
import '../common.css'

View File

@@ -103,11 +103,11 @@ console.log(favicon);
"files": [
{
"input": "client.html",
"path": "./client-b5m4ng86.js",
"path": "./client-s249t5qg.js",
"loader": "js",
"isEntry": true,
"headers": {
"etag": "Ax71YVYyZQc",
"etag": "fxoJ6L-0X3o",
"content-type": "text/javascript;charset=utf-8"
}
},

View File

@@ -1,519 +0,0 @@
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

@@ -1,255 +0,0 @@
import { file } from "bun";
import { describe, expect, test } from "bun:test";
import { rm } from "fs/promises";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
// Each test uses its own BUN_INSTALL_CACHE_DIR inside the temp dir for full
// isolation. This avoids interfering with the global cache or other tests.
function envWithCache(dir: string) {
return { ...bunEnv, BUN_INSTALL_CACHE_DIR: join(String(dir), ".bun-cache") };
}
describe.concurrent("GitHub tarball integrity", () => {
test("should store integrity hash in lockfile for GitHub dependencies", async () => {
using dir = tempDir("github-integrity", {
"package.json": JSON.stringify({
name: "test-github-integrity",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
}),
});
const env = envWithCache(dir);
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("Saved lockfile");
expect(exitCode).toBe(0);
const lockfileContent = await file(join(String(dir), "bun.lock")).text();
// The lockfile should contain a sha512 integrity hash for the GitHub dependency
expect(lockfileContent).toContain("sha512-");
// The resolved commit hash should be present
expect(lockfileContent).toContain("jonschlinkert-is-number-98e8ff1");
// Verify the format: the integrity appears after the resolved commit hash
expect(lockfileContent).toMatch(/"jonschlinkert-is-number-98e8ff1",\s*"sha512-/);
});
test("should verify integrity passes on re-install with matching hash", async () => {
using dir = tempDir("github-integrity-match", {
"package.json": JSON.stringify({
name: "test-github-integrity-match",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
}),
});
const env = envWithCache(dir);
// First install to generate lockfile with correct integrity
await using proc1 = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout1, stderr1, exitCode1] = await Promise.all([proc1.stdout.text(), proc1.stderr.text(), proc1.exited]);
expect(stderr1).not.toContain("error:");
expect(exitCode1).toBe(0);
// Read the generated lockfile and extract the integrity hash adjacent to
// the GitHub resolved entry to avoid accidentally matching an npm hash.
const lockfileContent = await file(join(String(dir), "bun.lock")).text();
const integrityMatch = lockfileContent.match(/"jonschlinkert-is-number-98e8ff1",\s*"(sha512-[A-Za-z0-9+/]+=*)"/);
expect(integrityMatch).not.toBeNull();
const integrityHash = integrityMatch![1];
// Clear cache and node_modules, then re-install with the same lockfile
await rm(join(String(dir), ".bun-cache"), { recursive: true, force: true });
await rm(join(String(dir), "node_modules"), { recursive: true, force: true });
await using proc2 = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
// Should succeed because the integrity matches
expect(stderr2).not.toContain("Integrity check failed");
expect(exitCode2).toBe(0);
// Lockfile should still contain the same integrity hash
const lockfileContent2 = await file(join(String(dir), "bun.lock")).text();
expect(lockfileContent2).toContain(integrityHash);
});
test("should reject GitHub tarball when integrity check fails", async () => {
using dir = tempDir("github-integrity-reject", {
"package.json": JSON.stringify({
name: "test-github-integrity-reject",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
}),
// Pre-create a lockfile with an invalid integrity hash (valid base64, 64 zero bytes)
"bun.lock": JSON.stringify({
lockfileVersion: 1,
configVersion: 1,
workspaces: {
"": {
name: "test-github-integrity-reject",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
},
},
packages: {
"is-number": [
"is-number@github:jonschlinkert/is-number#98e8ff1",
{},
"jonschlinkert-is-number-98e8ff1",
"sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
],
},
}),
});
// Fresh per-test cache ensures the tarball must be downloaded from the network
const env = envWithCache(dir);
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("Integrity check failed");
expect(exitCode).not.toBe(0);
});
test("should update lockfile with integrity when old format has none", async () => {
using dir = tempDir("github-integrity-upgrade", {
"package.json": JSON.stringify({
name: "test-github-integrity-upgrade",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
}),
// Pre-create a lockfile in the old format (no integrity hash)
"bun.lock": JSON.stringify({
lockfileVersion: 1,
configVersion: 1,
workspaces: {
"": {
name: "test-github-integrity-upgrade",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
},
},
packages: {
"is-number": ["is-number@github:jonschlinkert/is-number#98e8ff1", {}, "jonschlinkert-is-number-98e8ff1"],
},
}),
});
// Fresh per-test cache ensures the tarball must be downloaded
const env = envWithCache(dir);
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Should succeed without errors
expect(stderr).not.toContain("Integrity check failed");
expect(stderr).not.toContain("error:");
// The lockfile should be re-saved with the new integrity hash
expect(stderr).toContain("Saved lockfile");
expect(exitCode).toBe(0);
// Verify the lockfile now contains the integrity hash
const lockfileContent = await file(join(String(dir), "bun.lock")).text();
expect(lockfileContent).toContain("sha512-");
expect(lockfileContent).toMatch(/"jonschlinkert-is-number-98e8ff1",\s*"sha512-/);
});
test("should accept GitHub dependency from cache without re-downloading", async () => {
// Use a shared cache dir for both installs so the second is a true cache hit
using dir = tempDir("github-integrity-cached", {
"package.json": JSON.stringify({
name: "test-github-integrity-cached",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
}),
});
const env = envWithCache(dir);
// First install warms the per-test cache
await using proc1 = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout1, stderr1, exitCode1] = await Promise.all([proc1.stdout.text(), proc1.stderr.text(), proc1.exited]);
expect(stderr1).not.toContain("error:");
expect(exitCode1).toBe(0);
// Remove node_modules but keep the cache
await rm(join(String(dir), "node_modules"), { recursive: true, force: true });
// Strip the integrity from the lockfile to simulate an old-format lockfile
// that should still work when the cache already has the package
const lockfileContent = await file(join(String(dir), "bun.lock")).text();
const stripped = lockfileContent.replace(/,\s*"sha512-[^"]*"/, "");
await Bun.write(join(String(dir), "bun.lock"), stripped);
// Second install should hit the cache and succeed without re-downloading
await using proc2 = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
// Should succeed without integrity errors (package served from cache)
expect(stderr2).not.toContain("Integrity check failed");
expect(stderr2).not.toContain("error:");
expect(exitCode2).toBe(0);
});
});

View File

@@ -634,42 +634,3 @@ test.concurrent("bun serve files with correct Content-Type headers", async () =>
// The process will be automatically cleaned up by 'await using'
}
});
test("importing bun:main from HTML entry preload does not crash", async () => {
const dir = tempDirWithFiles("html-entry-bun-main", {
"index.html": /*html*/ `
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body><h1>Hello</h1></body>
</html>
`,
"preload.mjs": /*js*/ `
try {
await import("bun:main");
} catch {}
// Signal that preload ran successfully without crashing
console.log("PRELOAD_OK");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--preload", "./preload.mjs", "index.html", "--port=0"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const decoder = new TextDecoder();
let text = "";
for await (const chunk of proc.stdout) {
text += decoder.decode(chunk, { stream: true });
if (text.includes("http://")) break;
}
expect(text).toContain("PRELOAD_OK");
proc.kill();
await proc.exited;
});

View File

@@ -91,30 +91,6 @@ 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,39 +754,3 @@ 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

@@ -5,18 +5,10 @@ import { MongoClient } from "mongodb";
const databaseUrl = getSecret("TLS_MONGODB_DATABASE_URL");
describe.skipIf(!databaseUrl)("mongodb", () => {
test("should connect and inspect", async () => {
const client = new MongoClient(databaseUrl!, {
serverSelectionTimeoutMS: 10000,
});
test("should connect and inpect", async () => {
const client = new MongoClient(databaseUrl!);
let clientConnection: MongoClient;
try {
clientConnection = await client.connect();
} catch (e) {
console.error("Failed to connect to MongoDB, skipping:", (e as Error).message);
return;
}
const clientConnection = await client.connect();
try {
const db = client.db("bun");

View File

@@ -1,63 +0,0 @@
// 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,117 @@
import { afterEach, describe, expect, test } from "bun:test";
// Issue #26388: process.env should coerce values to strings like Node.js does
// When assigning undefined, null, numbers, or objects to process.env properties,
// Node.js converts them to strings, but Bun was storing the actual JavaScript values.
const TEST_ENV_KEYS = [
"TEST_UNDEFINED",
"TEST_JSON_UNDEFINED",
"TEST_NULL",
"TEST_NUMBER",
"TEST_TRUE",
"TEST_FALSE",
"TEST_OBJECT",
"TEST_ARRAY",
"TEST_STRING",
"TEST_EMPTY",
"TEST_CUSTOM_TOSTRING",
"TEST_SYMBOL",
"TEST_OVERWRITE",
];
describe("process.env string coercion", () => {
afterEach(() => {
for (const key of TEST_ENV_KEYS) {
delete process.env[key];
}
});
test("undefined is coerced to 'undefined' string", () => {
process.env.TEST_UNDEFINED = undefined as unknown as string;
expect(process.env.TEST_UNDEFINED).toBe("undefined");
expect(typeof process.env.TEST_UNDEFINED).toBe("string");
});
test("JSON.stringify(undefined) is coerced to 'undefined' string", () => {
// JSON.stringify(undefined) returns undefined (not the string "undefined")
// This is the exact case that breaks Vite 8 + rolldown
process.env.TEST_JSON_UNDEFINED = JSON.stringify(undefined) as unknown as string;
expect(process.env.TEST_JSON_UNDEFINED).toBe("undefined");
expect(typeof process.env.TEST_JSON_UNDEFINED).toBe("string");
});
test("null is coerced to 'null' string", () => {
process.env.TEST_NULL = null as unknown as string;
expect(process.env.TEST_NULL).toBe("null");
expect(typeof process.env.TEST_NULL).toBe("string");
});
test("number is coerced to string", () => {
process.env.TEST_NUMBER = 123 as unknown as string;
expect(process.env.TEST_NUMBER).toBe("123");
expect(typeof process.env.TEST_NUMBER).toBe("string");
});
test("boolean true is coerced to 'true' string", () => {
process.env.TEST_TRUE = true as unknown as string;
expect(process.env.TEST_TRUE).toBe("true");
expect(typeof process.env.TEST_TRUE).toBe("string");
});
test("boolean false is coerced to 'false' string", () => {
process.env.TEST_FALSE = false as unknown as string;
expect(process.env.TEST_FALSE).toBe("false");
expect(typeof process.env.TEST_FALSE).toBe("string");
});
test("object is coerced to '[object Object]' string", () => {
process.env.TEST_OBJECT = { foo: "bar" } as unknown as string;
expect(process.env.TEST_OBJECT).toBe("[object Object]");
expect(typeof process.env.TEST_OBJECT).toBe("string");
});
test("array is coerced to comma-separated string", () => {
process.env.TEST_ARRAY = [1, 2, 3] as unknown as string;
expect(process.env.TEST_ARRAY).toBe("1,2,3");
expect(typeof process.env.TEST_ARRAY).toBe("string");
});
test("string stays as string", () => {
process.env.TEST_STRING = "hello";
expect(process.env.TEST_STRING).toBe("hello");
expect(typeof process.env.TEST_STRING).toBe("string");
});
test("empty string stays as empty string", () => {
process.env.TEST_EMPTY = "";
expect(process.env.TEST_EMPTY).toBe("");
expect(typeof process.env.TEST_EMPTY).toBe("string");
});
test("object with custom toString() uses it", () => {
const obj = {
toString() {
return "custom-string";
},
};
process.env.TEST_CUSTOM_TOSTRING = obj as unknown as string;
expect(process.env.TEST_CUSTOM_TOSTRING).toBe("custom-string");
expect(typeof process.env.TEST_CUSTOM_TOSTRING).toBe("string");
});
test("Symbol throws TypeError", () => {
expect(() => {
process.env.TEST_SYMBOL = Symbol("test") as unknown as string;
}).toThrow();
});
test("overwriting existing env var coerces to string", () => {
process.env.TEST_OVERWRITE = "initial";
expect(process.env.TEST_OVERWRITE).toBe("initial");
process.env.TEST_OVERWRITE = 456 as unknown as string;
expect(process.env.TEST_OVERWRITE).toBe("456");
expect(typeof process.env.TEST_OVERWRITE).toBe("string");
});
});

View File

@@ -1,44 +1,8 @@
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/27014
// 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.
test("Bun.stripANSI does not hang on non-ANSI control characters", () => {
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!");
});

View File

@@ -1,89 +0,0 @@
import { expect, test } from "bun:test";
import http from "node:http";
test("ClientRequest.setHeaders should not throw ERR_HTTP_HEADERS_SENT on new request", async () => {
await using server = Bun.serve({
port: 0,
fetch(req) {
return new Response(req.headers.get("x-test") ?? "missing");
},
});
const { resolve, reject, promise } = Promise.withResolvers<string>();
const req = http.request(`http://localhost:${server.port}/test`, { method: "GET" }, res => {
let data = "";
res.on("data", (chunk: Buffer) => {
data += chunk.toString();
});
res.on("end", () => resolve(data));
});
req.on("error", reject);
// This should not throw - headers haven't been sent yet
req.setHeaders(new Headers({ "x-test": "value" }));
req.end();
const body = await promise;
expect(body).toBe("value");
});
test("ClientRequest.setHeaders works with Map", async () => {
await using server = Bun.serve({
port: 0,
fetch(req) {
return new Response(req.headers.get("x-map-test") ?? "missing");
},
});
const { resolve, reject, promise } = Promise.withResolvers<string>();
const req = http.request(`http://localhost:${server.port}/test`, { method: "GET" }, res => {
let data = "";
res.on("data", (chunk: Buffer) => {
data += chunk.toString();
});
res.on("end", () => resolve(data));
});
req.on("error", reject);
req.setHeaders(new Map([["x-map-test", "map-value"]]));
req.end();
const body = await promise;
expect(body).toBe("map-value");
});
test("ServerResponse.setHeaders should not throw before headers are sent", async () => {
const { resolve, reject, promise } = Promise.withResolvers<string>();
const server = http.createServer((req, res) => {
// This should not throw - headers haven't been sent yet
res.setHeaders(new Headers({ "x-custom": "server-value" }));
res.writeHead(200);
res.end("ok");
});
try {
server.listen(0, () => {
const port = (server.address() as any).port;
try {
const req = http.request(`http://localhost:${port}/test`, res => {
resolve(res.headers["x-custom"] as string);
});
req.on("error", reject);
req.end();
} catch (e) {
reject(e);
}
});
expect(await promise).toBe("server-value");
} finally {
server.close();
}
});

View File

@@ -1,336 +0,0 @@
import { describe, expect, test } from "bun:test";
import http from "node:http";
// Regression test for https://github.com/oven-sh/bun/issues/27061
// When http.ClientRequest.write() is called more than once (streaming data in chunks),
// Bun was stripping the explicitly-set Content-Length header and switching to
// Transfer-Encoding: chunked. Node.js preserves Content-Length in all cases.
describe("node:http ClientRequest preserves explicit Content-Length", () => {
test("with multiple req.write() calls", async () => {
const { promise, resolve, reject } = Promise.withResolvers<{
contentLength: string | undefined;
transferEncoding: string | undefined;
bodyLength: number;
}>();
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve({
contentLength: req.headers["content-length"],
transferEncoding: req.headers["transfer-encoding"],
bodyLength: Buffer.concat(chunks).length,
});
res.writeHead(200);
res.end("ok");
});
});
await new Promise<void>(res => server.listen(0, "127.0.0.1", res));
const port = (server.address() as any).port;
try {
const chunk1 = Buffer.alloc(100, "a");
const chunk2 = Buffer.alloc(100, "b");
const totalLength = chunk1.length + chunk2.length;
const req = http.request({
hostname: "127.0.0.1",
port,
method: "POST",
headers: {
"Content-Length": totalLength.toString(),
},
});
await new Promise<void>((res, rej) => {
req.on("error", rej);
req.on("response", () => res());
req.write(chunk1);
req.write(chunk2);
req.end();
});
const result = await promise;
expect(result.contentLength).toBe("200");
expect(result.transferEncoding).toBeUndefined();
expect(result.bodyLength).toBe(200);
} finally {
server.close();
}
});
test("with req.write() + req.end(data)", async () => {
const { promise, resolve, reject } = Promise.withResolvers<{
contentLength: string | undefined;
transferEncoding: string | undefined;
bodyLength: number;
}>();
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve({
contentLength: req.headers["content-length"],
transferEncoding: req.headers["transfer-encoding"],
bodyLength: Buffer.concat(chunks).length,
});
res.writeHead(200);
res.end("ok");
});
});
await new Promise<void>(res => server.listen(0, "127.0.0.1", res));
const port = (server.address() as any).port;
try {
const chunk1 = Buffer.alloc(100, "a");
const chunk2 = Buffer.alloc(100, "b");
const totalLength = chunk1.length + chunk2.length;
const req = http.request({
hostname: "127.0.0.1",
port,
method: "POST",
headers: {
"Content-Length": totalLength.toString(),
},
});
await new Promise<void>((res, rej) => {
req.on("error", rej);
req.on("response", () => res());
req.write(chunk1);
req.end(chunk2);
});
const result = await promise;
expect(result.contentLength).toBe("200");
expect(result.transferEncoding).toBeUndefined();
expect(result.bodyLength).toBe(200);
} finally {
server.close();
}
});
test("with three req.write() calls", async () => {
const { promise, resolve, reject } = Promise.withResolvers<{
contentLength: string | undefined;
transferEncoding: string | undefined;
bodyLength: number;
}>();
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve({
contentLength: req.headers["content-length"],
transferEncoding: req.headers["transfer-encoding"],
bodyLength: Buffer.concat(chunks).length,
});
res.writeHead(200);
res.end("ok");
});
});
await new Promise<void>(res => server.listen(0, "127.0.0.1", res));
const port = (server.address() as any).port;
try {
const chunk1 = Buffer.alloc(100, "a");
const chunk2 = Buffer.alloc(100, "b");
const chunk3 = Buffer.alloc(100, "c");
const totalLength = chunk1.length + chunk2.length + chunk3.length;
const req = http.request({
hostname: "127.0.0.1",
port,
method: "POST",
headers: {
"Content-Length": totalLength.toString(),
},
});
await new Promise<void>((res, rej) => {
req.on("error", rej);
req.on("response", () => res());
req.write(chunk1);
req.write(chunk2);
req.write(chunk3);
req.end();
});
const result = await promise;
expect(result.contentLength).toBe("300");
expect(result.transferEncoding).toBeUndefined();
expect(result.bodyLength).toBe(300);
} finally {
server.close();
}
});
test("single req.write() still works", async () => {
const { promise, resolve, reject } = Promise.withResolvers<{
contentLength: string | undefined;
transferEncoding: string | undefined;
bodyLength: number;
}>();
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve({
contentLength: req.headers["content-length"],
transferEncoding: req.headers["transfer-encoding"],
bodyLength: Buffer.concat(chunks).length,
});
res.writeHead(200);
res.end("ok");
});
});
await new Promise<void>(res => server.listen(0, "127.0.0.1", res));
const port = (server.address() as any).port;
try {
const data = Buffer.alloc(200, "x");
const req = http.request({
hostname: "127.0.0.1",
port,
method: "POST",
headers: {
"Content-Length": data.length.toString(),
},
});
await new Promise<void>((res, rej) => {
req.on("error", rej);
req.on("response", () => res());
req.write(data);
req.end();
});
const result = await promise;
expect(result.contentLength).toBe("200");
expect(result.transferEncoding).toBeUndefined();
expect(result.bodyLength).toBe(200);
} finally {
server.close();
}
});
test("without explicit Content-Length still uses chunked encoding", async () => {
const { promise, resolve, reject } = Promise.withResolvers<{
contentLength: string | undefined;
transferEncoding: string | undefined;
bodyLength: number;
}>();
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve({
contentLength: req.headers["content-length"],
transferEncoding: req.headers["transfer-encoding"],
bodyLength: Buffer.concat(chunks).length,
});
res.writeHead(200);
res.end("ok");
});
});
await new Promise<void>(res => server.listen(0, "127.0.0.1", res));
const port = (server.address() as any).port;
try {
const chunk1 = Buffer.alloc(100, "a");
const chunk2 = Buffer.alloc(100, "b");
const req = http.request({
hostname: "127.0.0.1",
port,
method: "POST",
// No Content-Length header
});
await new Promise<void>((res, rej) => {
req.on("error", rej);
req.on("response", () => res());
req.write(chunk1);
req.write(chunk2);
req.end();
});
const result = await promise;
// Without explicit Content-Length, chunked encoding should be used
expect(result.transferEncoding).toBe("chunked");
expect(result.bodyLength).toBe(200);
} finally {
server.close();
}
});
test("explicit Transfer-Encoding takes precedence over Content-Length", async () => {
const { promise, resolve } = Promise.withResolvers<{
contentLength: string | undefined;
transferEncoding: string | undefined;
bodyLength: number;
}>();
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve({
contentLength: req.headers["content-length"],
transferEncoding: req.headers["transfer-encoding"],
bodyLength: Buffer.concat(chunks).length,
});
res.writeHead(200);
res.end("ok");
});
});
await new Promise<void>(res => server.listen(0, "127.0.0.1", res));
const port = (server.address() as any).port;
try {
const chunk1 = Buffer.alloc(100, "a");
const chunk2 = Buffer.alloc(100, "b");
const req = http.request({
hostname: "127.0.0.1",
port,
method: "POST",
headers: {
"Content-Length": "200",
"Transfer-Encoding": "chunked",
},
});
await new Promise<void>((res, rej) => {
req.on("error", rej);
req.on("response", () => res());
req.write(chunk1);
req.write(chunk2);
req.end();
});
const result = await promise;
// When user explicitly sets Transfer-Encoding, it should be used
// and Content-Length should not be added
expect(result.transferEncoding).toBe("chunked");
expect(result.contentLength).toBeUndefined();
expect(result.bodyLength).toBe(200);
} finally {
server.close();
}
});
});

View File

@@ -92,17 +92,13 @@ test("cyclic imports with async dependencies should generate async wrappers", as
expect(bundled).toMatchInlineSnapshot(`
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
set: (newValue) => all[name] = () => newValue
});
};
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -180,7 +176,7 @@ test("cyclic imports with async dependencies should generate async wrappers", as
var { AsyncEntryPoint: AsyncEntryPoint2 } = await Promise.resolve().then(() => exports_AsyncEntryPoint);
AsyncEntryPoint2();
//# debugId=2020261114B67BB564756E2164756E21
//# debugId=986E7BD819E590FD64756E2164756E21
//# sourceMappingURL=entryBuild.js.map
"
`);