mirror of
https://github.com/oven-sh/bun
synced 2026-02-25 11:07:19 +01:00
Compare commits
1 Commits
claude/fix
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e641fe78f4 |
@@ -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 */
|
||||
});
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
15
packages/bun-types/bun.d.ts
vendored
15
packages/bun-types/bun.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
src/http.zig
16
src/http.zig
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => {},
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = "",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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"/);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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
|
||||
"
|
||||
`);
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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>");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
14
test/js/third_party/mongodb/mongodb.test.ts
vendored
14
test/js/third_party/mongodb/mongodb.test.ts
vendored
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
117
test/regression/issue/26388.test.ts
Normal file
117
test/regression/issue/26388.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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!");
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
"
|
||||
`);
|
||||
|
||||
Reference in New Issue
Block a user