From 6c567b26a6363e5cbd290bd18b70b451963cde7b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 7 Nov 2026 14:08:01 +0000 Subject: [PATCH 1/5] docs: generate the Output blocks of the minifier page from `bun build` docs/bundler/minifier.mdx shows an Input and an Output for each transformation of the minifier. The Outputs were typed by hand, and `bun build` does not print 12 of them under the Mode that the section names. Nine more sections name a flag that the transformation does not need. scripts/minifier-docs.ts now generates every Output block. It saves the Input as input.ts, runs `bun build ./input.ts` with the flags on the Mode line, and writes what Bun prints. Without --write it lists the stale sections and exits with 1. test/bundler/minifier-docs.test.ts generates the page again with the bun under test. It fails when the page differs, and when a Mode line names a flag that changes nothing in its section. On the page, each Input is now a complete program and each Mode line is the exact flags. The rows that no transformation stands behind are gone, and so is the section about import.meta: Bun inlines those properties only in a CommonJS bundle, and not as part of minification. --- .prettierignore | 2 +- docs/bundler/minifier.mdx | 707 +++++++++++++++-------------- scripts/minifier-docs.ts | 285 ++++++++++++ test/bundler/minifier-docs.test.ts | 18 + 4 files changed, 662 insertions(+), 350 deletions(-) create mode 100644 scripts/minifier-docs.ts create mode 100644 test/bundler/minifier-docs.test.ts diff --git a/.prettierignore b/.prettierignore index cb3dfce2f0c3..42bdd0ce8e03 100644 --- a/.prettierignore +++ b/.prettierignore @@ -16,7 +16,7 @@ test/expected-durations.json # vendored, generated bundle of github.com/jarred-sumner/xmac (see its header) scripts/build/xmac.mjs -# the output codeblocks need to stay minified +# scripts/minifier-docs.ts generates the Output blocks, and test/bundler/minifier-docs.test.ts compares them byte for byte docs/bundler/minifier.mdx # Prettier would reformat the code examples, and test/cli/lint/rule-docs.test.ts runs them exactly as written docs/lint/rules diff --git a/docs/bundler/minifier.mdx b/docs/bundler/minifier.mdx index 3f5c0a58a971..b90dfd775ba0 100644 --- a/docs/bundler/minifier.mdx +++ b/docs/bundler/minifier.mdx @@ -96,6 +96,12 @@ Renames local variables and function names to shorter identifiers using frequenc ## All Transformations +To get the Output of a section, save its Input as `input.ts` and run `bun build ./input.ts` with the flags on the **Mode** line. `Always active` means that the transformation needs no flag. + +`bun build` bundles the Input, so a `let` or `const` at the top level of the file becomes `var`. Unless you minify whitespace, Bun also writes a comment such as `// input.ts` above the code of each source file. The Output blocks leave that comment out. + +{/* scripts/minifier-docs.ts generates the Output blocks. After you change a section, run: bun bd scripts/minifier-docs.ts --write */} + ### Boolean literal shortening **Mode:** `--minify-syntax` @@ -103,13 +109,13 @@ Renames local variables and function names to shorter identifiers using frequenc Converts boolean literals to shorter expressions. ```ts Input -true -false +console.log(true); +console.log(false); ``` ```js Output -!0 -!1 +console.log(!0); +console.log(!1); ``` ### Boolean algebra optimizations @@ -119,19 +125,22 @@ false Simplifies boolean expressions using logical rules. ```ts Input -if (!!x) y; -if (x && true) y; -if (x || false) y; -!true -!false +if (!!x) y(); +if (x && true) y(); +if (x || false) y(); +console.log(!(a == b)); +console.log(!(a !== b)); ``` ```js Output -if(x)y; -if(x)y; -if(x)y; -!1 -!0 +if (x) + y(); +if (x) + y(); +if (x) + y(); +console.log(a != b); +console.log(a === b); ``` ### Undefined shortening @@ -141,13 +150,11 @@ if(x)y; Replaces `undefined` with shorter equivalent. ```ts Input -undefined -let x = undefined; +console.log(undefined); ``` ```js Output -void 0 -let x=void 0; +console.log(void 0); ``` ### Undefined equality optimization @@ -157,13 +164,13 @@ let x=void 0; Optimizes loose equality checks with undefined. ```ts Input -x == undefined -x != undefined +console.log(x == undefined); +console.log(x != undefined); ``` ```js Output -x == null -x != null +console.log(x == null); +console.log(x != null); ``` ### Infinity shortening @@ -173,63 +180,73 @@ x != null Converts Infinity to mathematical expressions. ```ts Input -Infinity --Infinity +console.log(Infinity); +console.log(-Infinity); ``` ```js Output -1/0 --1/0 +console.log(1 / 0); +console.log(-1 / 0); ``` ### Typeof optimizations **Mode:** `--minify-syntax` -Optimizes typeof comparisons and evaluates constant typeof expressions. +Shortens a comparison of `typeof` with `"undefined"`. ```ts Input -typeof x === 'undefined' -typeof x !== 'undefined' -typeof require -typeof null -typeof true -typeof 123 -typeof "str" -typeof 123n +console.log(typeof x === "undefined"); +console.log(typeof x !== "undefined"); ``` ```js Output -typeof x>'u' -typeof x<'u' -"function" -"object" -"boolean" -"number" -"string" -"bigint" +console.log(typeof x > "u"); +console.log(typeof x < "u"); +``` + +### Typeof constant folding + +**Mode:** Always active + +Evaluates `typeof` of a literal at compile time. + +```ts Input +console.log(typeof null); +console.log(typeof true); +console.log(typeof 123); +console.log(typeof "str"); +console.log(typeof 123n); +``` + +```js Output +console.log("object"); +console.log("boolean"); +console.log("number"); +console.log("string"); +console.log("bigint"); ``` ### Number formatting -**Mode:** `--minify-syntax` +**Mode:** Always active -Formats numbers in the most compact representation. +Prints a power of ten from `1e4` to `1e9` with an exponent, and a whole number without its fraction. ```ts Input -10000 -100000 -1000000 -1.0 --42.0 +console.log(10000); +console.log(100000); +console.log(1000000); +console.log(1.0); +console.log(-42.0); ``` ```js Output -1e4 -1e5 -1e6 -1 --42 +console.log(1e4); +console.log(1e5); +console.log(1e6); +console.log(1); +console.log(-42); ``` ### Arithmetic constant folding @@ -239,21 +256,21 @@ Formats numbers in the most compact representation. Evaluates arithmetic operations at compile time. ```ts Input -1 + 2 -10 - 5 -3 * 4 -10 / 2 -10 % 3 -2 ** 3 +console.log(1 + 2); +console.log(10 - 5); +console.log(3 * 4); +console.log(10 / 2); +console.log(10 % 3); +console.log(2 ** 3); ``` ```js Output -3 -5 -12 -5 -1 -8 +console.log(3); +console.log(5); +console.log(12); +console.log(5); +console.log(1); +console.log(8); ``` ### Bitwise constant folding @@ -263,21 +280,21 @@ Evaluates arithmetic operations at compile time. Evaluates bitwise operations at compile time. ```ts Input -5 & 3 -5 | 3 -5 ^ 3 -8 << 2 -32 >> 2 -~5 +console.log(5 & 3); +console.log(5 | 3); +console.log(5 ^ 3); +console.log(8 << 2); +console.log(32 >> 2); +console.log(~5); ``` ```js Output -1 -7 -6 -32 -8 --6 +console.log(1); +console.log(7); +console.log(6); +console.log(32); +console.log(8); +console.log(-6); ``` ### String concatenation @@ -287,15 +304,15 @@ Evaluates bitwise operations at compile time. Combines string literals at compile time. ```ts Input -"a" + "b" -"x" + 123 -"foo" + "bar" + "baz" +console.log("a" + "b"); +console.log("x" + 123); +console.log("foo" + "bar" + "baz"); ``` ```js Output -"ab" -"x123" -"foobarbaz" +console.log("ab"); +console.log("x123"); +console.log("foobarbaz"); ``` ### String indexing @@ -305,13 +322,13 @@ Combines string literals at compile time. Evaluates string character access at compile time. ```ts Input -"foo"[2] -"hello"[0] +console.log("foo"[2]); +console.log("hello"[0]); ``` ```js Output -"o" -"h" +console.log("o"); +console.log("h"); ``` ### Template literal folding @@ -321,48 +338,47 @@ Evaluates string character access at compile time. Evaluates template literals with constant expressions. ```ts Input -`a${123}b` -`result: ${5 + 10}` +console.log(`a${123}b`); +console.log(`result: ${5 + 10}`); ``` ```js Output -"a123b" -"result: 15" +console.log("a123b"); +console.log("result: 15"); ``` ### Template literal to string conversion **Mode:** `--minify-syntax` -Converts template literals with no substitutions to regular strings. +Converts template literals with no substitutions to regular strings. Bun keeps a template literal that has a line break, because the line break needs no escape there. ```ts Input -`Hello World` -`Line 1 -Line 2` +console.log(`Hello World`); +console.log(`Line 1 +Line 2`); ``` ```js Output -"Hello World" -"Line 1\nLine 2" +console.log("Hello World"); +console.log(`Line 1 +Line 2`); ``` ### String quote optimization -**Mode:** `--minify-syntax` +**Mode:** Always active -Chooses the optimal quote character to minimize escapes. +Chooses the quote character that needs the fewest escapes. ```ts Input -"It's a string" -'He said "hello"' -`Simple string` +console.log('It\'s a string'); +console.log("He said \"hello\""); ``` ```js Output -"It's a string" -'He said "hello"' -"Simple string" +console.log("It's a string"); +console.log('He said "hello"'); ``` ### Array spread inlining @@ -372,13 +388,13 @@ Chooses the optimal quote character to minimize escapes. Inlines array spread operations with constant arrays. ```ts Input -[1, ...[2, 3], 4] -[...[a, b]] +console.log([1, ...[2, 3], 4]); +console.log([...[a, b]]); ``` ```js Output -[1,2,3,4] -[a,b] +console.log([1, 2, 3, 4]); +console.log([a, b]); ``` ### Array indexing @@ -388,15 +404,15 @@ Inlines array spread operations with constant arrays. Evaluates constant array access at compile time. ```ts Input -[x][0] -['a', 'b', 'c'][1] -['a', , 'c'][1] +console.log([x][0]); +console.log(["a", "b", "c"][1]); +console.log(["a", , "c"][1]); ``` ```js Output -x -'b' -void 0 +console.log(x); +console.log("b"); +console.log(void 0); ``` ### Property access optimization @@ -406,17 +422,17 @@ void 0 Converts bracket notation to dot notation when possible. ```ts Input -obj["property"] -obj["validName"] -obj["123"] -obj["invalid-name"] +console.log(obj["property"]); +console.log(obj["validName"]); +console.log(obj["123"]); +console.log(obj["invalid-name"]); ``` ```js Output -obj.property -obj.validName -obj["123"] -obj["invalid-name"] +console.log(obj.property); +console.log(obj.validName); +console.log(obj["123"]); +console.log(obj["invalid-name"]); ``` ### Comparison folding @@ -426,57 +442,57 @@ obj["invalid-name"] Evaluates constant comparisons at compile time. ```ts Input -3 < 5 -5 > 3 -3 <= 3 -5 >= 6 -"a" < "b" +console.log(3 < 5); +console.log(5 > 3); +console.log(3 <= 3); +console.log(5 >= 6); +console.log("a" < "b"); ``` ```js Output -!0 -!0 -!0 -!1 -!0 +console.log(!0); +console.log(!0); +console.log(!0); +console.log(!1); +console.log(!0); ``` ### Logical operation folding -**Mode:** `--minify-syntax` +**Mode:** Always active Simplifies logical operations with constant values. ```ts Input -true && x -false && x -true || x -false || x +console.log(true && x); +console.log(false && x); +console.log(true || x); +console.log(false || x); ``` ```js Output -x -!1 -!0 -x +console.log(x); +console.log(false); +console.log(true); +console.log(x); ``` ### Nullish coalescing folding -**Mode:** `--minify-syntax` +**Mode:** Always active Evaluates nullish coalescing with known values. ```ts Input -null ?? x -undefined ?? x -42 ?? x +console.log(null ?? x); +console.log(undefined ?? x); +console.log(42 ?? x); ``` ```js Output -x -x -42 +console.log(x); +console.log(x); +console.log(42); ``` ### Comma expression simplification @@ -486,55 +502,45 @@ x Removes side-effect-free expressions from comma sequences. ```ts Input -(0, x) -(123, "str", x) +console.log((0, x)); +console.log((123, "str", x)); ``` ```js Output -x -x +console.log(x); +console.log(x); ``` ### Ternary conditional folding -**Mode:** `--minify-syntax` +**Mode:** Always active Evaluates conditional expressions with constant conditions. ```ts Input -true ? a : b -false ? a : b -x ? true : false -x ? false : true +console.log(true ? a : b); +console.log(false ? a : b); ``` ```js Output -a -b -x ? !0 : !1 -x ? !1 : !0 +console.log(a); +console.log(b); ``` ### Unary expression folding -**Mode:** `--minify-syntax` +**Mode:** Always active Simplifies unary operations. ```ts Input -+123 -+"123" --(-x) -~~x -!!x +console.log(+123); +console.log(+"123"); ``` ```js Output -123 -123 -- -x -~~x -!!x +console.log(123); +console.log(123); ``` ### Double negation removal @@ -544,13 +550,14 @@ Simplifies unary operations. Removes unnecessary double negations. ```ts Input -if (!!x) y; -!!!x +if (!!x) y(); +console.log(!!!x); ``` ```js Output -if(x)y; -!x +if (x) + y(); +console.log(!x); ``` ### If statement optimization @@ -560,24 +567,19 @@ if(x)y; Optimizes if statements with constant conditions. ```ts Input -if (true) x; -if (false) x; -if (x) { a; } -if (x) {} else y; +if (true) a(); +if (false) b(); ``` ```js Output -x; -// removed -if(x)a; -if(!x)y; +a(); ``` ### Dead code elimination **Mode:** `--minify-syntax` -Removes unreachable code and code without side effects. +Removes code that can never run: an `if (false)` statement, and the statements after a `return`. ```ts Input if (false) { @@ -587,26 +589,14 @@ function foo() { return x; deadCode(); } +console.log(foo()); ``` ```js Output -function foo(){return x} -``` - -### Unreachable branch removal - -**Mode:** `--minify-syntax` - -Removes branches that can never execute. - -```ts Input -while (false) { - neverRuns(); +function foo() { + return x; } -``` - -```js Output -// removed entirely +console.log(foo()); ``` ### Empty block removal @@ -621,8 +611,8 @@ if (x) { } ``` ```js Output -; -// removed +if (x) + ; ``` ### Single statement block unwrapping @@ -638,22 +628,23 @@ if (condition) { ``` ```js Output -if(condition)doSomething(); +if (condition) + doSomething(); ``` ### TypeScript enum inlining -**Mode:** `--minify-syntax` +**Mode:** Always active Inlines TypeScript enum values at compile time. ```ts Input enum Color { Red, Green, Blue } -const x = Color.Red; +console.log(Color.Red); ``` ```js Output -const x=0; +console.log(0 /* Red */); ``` ### Pure annotation support @@ -663,12 +654,14 @@ const x=0; Respects `/*@__PURE__*/` annotations for tree shaking (removing unused code). ```ts Input -const x = /*@__PURE__*/ expensive(); -// If x is unused... +const unused = /*@__PURE__*/ expensive(); +const used = /*@__PURE__*/ expensive(); +console.log(used); ``` ```js Output -// removed entirely +var used = /* @__PURE__ */ expensive(); +console.log(used); ``` ### Identifier renaming @@ -682,15 +675,20 @@ function calculateSum(firstNumber, secondNumber) { const result = firstNumber + secondNumber; return result; } +console.log(calculateSum(1, 2)); ``` ```js Output -function n(t,u){const c=t+u;return c} +function o(c, l) { + const n = c + l; + return n; +} +console.log(o(1, 2)); ``` **Naming strategy:** -- Most frequently used identifiers get the shortest names; Bun orders the alphabet by how often each character appears in the source, so the first names are usually letters like t, e and n rather than a, b, c +- Most frequently used identifiers get the shortest names; Bun orders the alphabet by how often each character appears in the source, so the names depend on the source - Single characters: a-z, A-Z and `_` (53 names; `$` alone is reserved) - Two characters: the second character can also be a digit (up to 3,456 names) - Three characters and beyond as needed @@ -712,27 +710,30 @@ Removes all unnecessary whitespace. function add(a, b) { return a + b; } -let x = 10; +console.log(add(1, 2)); ``` ```js Output -function add(a,b){return a+b}let x=10; +function add(a,b){return a+b}console.log(add(1,2)); ``` ### Semicolon optimization **Mode:** `--minify-whitespace` -Inserts semicolons only when necessary. +Prints no semicolon after the last statement of a block. ```ts Input -let a = 1; -let b = 2; -return a + b; +function sum() { + let a = 1; + let b = 2; + return a + b; +} +console.log(sum()); ``` ```js Output -let a=1;let b=2;return a+b +function sum(){let a=1;let b=2;return a+b}console.log(sum()); ``` ### Operator spacing removal @@ -742,33 +743,33 @@ let a=1;let b=2;return a+b Removes spaces around operators. ```ts Input -a + b -x = y * z -foo && bar || baz +console.log(a + b); +x = y * z; +console.log(foo && bar || baz); ``` ```js Output -a+b -x=y*z -foo&&bar||baz +console.log(a+b);x=y*z;console.log(foo&&bar||baz); ``` ### Comment removal -**Mode:** `--minify-whitespace` +**Mode:** Always active -Removes comments except important license comments. +Removes comments. Bun keeps a comment that starts with `/*!` or `//!`. ```ts Input // This comment is removed /* So is this */ /*! But this license comment is kept */ function test() { /* inline comment */ } +test(); ``` ```js Output /*! But this license comment is kept */ -function test(){} +function test() {} +test(); ``` ### Object and array formatting @@ -778,15 +779,15 @@ function test(){} Removes whitespace in object and array literals. ```ts Input -const obj = { +console.log({ name: "John", age: 30 -}; -const arr = [1, 2, 3]; +}); +console.log([1, 2, 3]); ``` ```js Output -const obj={name:"John",age:30};const arr=[1,2,3]; +console.log({name:"John",age:30});console.log([1,2,3]); ``` ### Control flow formatting @@ -805,7 +806,7 @@ for (let i = 0; i < 10; i++) { ``` ```js Output -if(condition)doSomething();for(let i=0;i<10;i++)console.log(i); +if(condition){doSomething()}for(let i=0;i<10;i++){console.log(i)} ``` ### Function formatting @@ -818,11 +819,11 @@ Removes whitespace in function declarations. function myFunction(param1, param2) { return param1 + param2; } -const arrow = (a, b) => a + b; +console.log(myFunction, (a, b) => a + b); ``` ```js Output -function myFunction(param1,param2){return param1+param2}const arrow=(a,b)=>a+b; +function myFunction(param1,param2){return param1+param2}console.log(myFunction,(a,b)=>a+b); ``` ### Parentheses minimization @@ -832,15 +833,15 @@ function myFunction(param1,param2){return param1+param2}const arrow=(a,b)=>a+b; Only adds parentheses when necessary for operator precedence. ```ts Input -(a + b) * c -a + (b * c) -((x)) +console.log((a + b) * c); +console.log(a + (b * c)); +console.log(((x))); ``` ```js Output -(a+b)*c -a+b*c -x +console.log((a + b) * c); +console.log(a + b * c); +console.log(x); ``` ### Template literal value folding @@ -850,19 +851,19 @@ x Converts non-string interpolated values to strings and folds them into the template. ```ts Input -`hello ${123}` -`value: ${true}` -`result: ${null}` -`status: ${undefined}` -`big: ${10n}` +console.log(`hello ${123}`); +console.log(`value: ${true}`); +console.log(`result: ${null}`); +console.log(`status: ${undefined}`); +console.log(`big: ${10n}`); ``` ```js Output -"hello 123" -"value: true" -"result: null" -"status: undefined" -"big: 10" +console.log("hello 123"); +console.log("value: true"); +console.log("result: null"); +console.log("status: undefined"); +console.log("big: 10"); ``` ### String length constant folding @@ -872,13 +873,13 @@ Converts non-string interpolated values to strings and folds them into the templ Evaluates `.length` property on string literals at compile time. ```ts Input -"hello world".length -"test".length +console.log("hello world".length); +console.log("test".length); ``` ```js Output -11 -4 +console.log(11); +console.log(4); ``` ### Constructor call simplification @@ -888,19 +889,22 @@ Evaluates `.length` property on string literals at compile time. Simplifies constructor calls for built-in types. ```ts Input -new Object() -new Object(null) -new Object({a: 1}) -new Array() -new Array(x, y) +console.log(new Object()); +console.log(new Object(null)); +console.log(new Object({ a: 1 })); +console.log(new Array()); +console.log(new Array(x, y)); ``` ```js Output -{} -{} -{a:1} -[] -[x,y] +console.log({}); +console.log({}); +console.log({ a: 1 }); +console.log([]); +console.log([ + x, + y +]); ``` ### Single property object inlining @@ -910,11 +914,11 @@ new Array(x, y) Inlines property access for objects with a single property. ```ts Input -({fn: () => console.log('hi')}).fn +console.log({ fn: () => console.log("hi") }.fn); ``` ```js Output -() => console.log('hi') +console.log(() => console.log("hi")); ``` ### String charCodeAt constant folding @@ -924,13 +928,13 @@ Inlines property access for objects with a single property. Evaluates `charCodeAt()` on string literals for ASCII characters. ```ts Input -"hello".charCodeAt(1) -"A".charCodeAt(0) +console.log("hello".charCodeAt(1)); +console.log("A".charCodeAt(0)); ``` ```js Output -101 -65 +console.log(101); +console.log(65); ``` ### Void 0 equality to null equality @@ -940,13 +944,13 @@ Evaluates `charCodeAt()` on string literals for ASCII characters. Converts loose equality checks with `void 0` to `null` since they're equivalent. ```ts Input -x == void 0 -x != void 0 +console.log(x == void 0); +console.log(x != void 0); ``` ```js Output -x == null -x != null +console.log(x == null); +console.log(x != null); ``` ### Negation operator optimization @@ -956,33 +960,13 @@ x != null Moves negation operator through comma expressions. ```ts Input --(a, b) --(x, y, z) -``` - -```js Output -a,-b -x,y,-z -``` - -### Import.meta property inlining - -**Mode:** Bundle mode - -Inlines `import.meta` properties at build time when values are known. - -```ts Input -import.meta.dir -import.meta.file -import.meta.path -import.meta.url +console.log(-(a, b)); +console.log(-(x, y, z)); ``` ```js Output -"/path/to/directory" -"filename.js" -"/full/path/to/file.js" -"file:///full/path/to/file.js" +console.log((a, -b)); +console.log((x, y, -z)); ``` ### Variable declaration merging @@ -994,29 +978,35 @@ Merges adjacent variable declarations of the same type. ```ts Input let a = 1; let b = 2; -const c = 3; -const d = 4; +let c = 3; +console.log(a, b, c); ``` ```js Output -let a=1,b=2; -const c=3,d=4; +var a = 1, b = 2, c = 3; +console.log(a, b, c); ``` ### Expression statement merging **Mode:** `--minify-syntax` -Merges adjacent expression statements using comma operator. +Merges adjacent expression statements in a function body or a block using the comma operator. Statements at the top level of a file stay separate. ```ts Input -console.log(1); -console.log(2); -console.log(3); +function log() { + console.log(1); + console.log(2); + console.log(3); +} +log(); ``` ```js Output -console.log(1),console.log(2),console.log(3); +function log() { + console.log(1), console.log(2), console.log(3); +} +log(); ``` ### Return statement merging @@ -1026,12 +1016,18 @@ console.log(1),console.log(2),console.log(3); Merges expressions before return with comma operator. ```ts Input -console.log(x); -return y; +function f() { + console.log(x); + return y; +} +f(); ``` ```js Output -return console.log(x),y; +function f() { + return console.log(x), y; +} +f(); ``` ### Throw statement merging @@ -1041,46 +1037,56 @@ return console.log(x),y; Merges expressions before throw with comma operator. ```ts Input -console.log(x); -throw new Error(); +function f() { + console.log(x); + throw err; +} +f(); ``` ```js Output -throw(console.log(x),new Error()); +function f() { + throw console.log(x), err; +} +f(); ``` ### TypeScript enum cross-module inlining -**Mode:** `--minify-syntax` (bundle mode) +**Mode:** Always active Inlines enum values across module boundaries. -```ts Input -// lib.ts +```ts lib.ts export enum Color { Red, Green, Blue } +``` -// Input (main.ts) -import { Color } from './lib'; -const x = Color.Red; +```ts Input +import { Color } from "./lib"; +console.log(Color.Red); ``` ```js Output -const x=0; +console.log(0 /* Red */); ``` ### Computed property enum inlining **Mode:** `--minify-syntax` -Inlines enum values used as computed object properties. +Turns a computed key that is a member of an imported enum into a plain key. + +```ts keys.ts +export enum Keys { FOO = "foo" } +``` ```ts Input -enum Keys { FOO = 'foo' } -const obj = { [Keys.FOO]: value } +import { Keys } from "./keys"; +console.log({ [Keys.FOO]: value }); ``` ```js Output -const obj={foo:value} +console.log({ foo: value }); ``` ### Arrow function body shortening @@ -1090,13 +1096,13 @@ const obj={foo:value} Uses expression body syntax when an arrow function only returns a value. ```ts Input -() => { return x; } -(a) => { return a + 1; } +console.log(() => { return x; }); +console.log((a) => { return a + 1; }); ``` ```js Output -() => x -a => a + 1 +console.log(() => x); +console.log((a) => a + 1); ``` ### Object property shorthand @@ -1106,13 +1112,13 @@ a => a + 1 Uses shorthand syntax when property name and value identifier match. ```ts Input -{ x: x, y: y } -{ name: name, age: age } +console.log({ x: x, y: y }); +console.log({ name: name, age: age }); ``` ```js Output -{ x, y } -{ name, age } +console.log({ x, y }); +console.log({ name, age }); ``` ### Drop debugger statements @@ -1126,17 +1132,21 @@ function test() { debugger; return x; } +console.log(test()); ``` ```js Output -function test(){return x} +function test() { + return x; +} +console.log(test()); ``` ### Drop console calls **Mode:** `--drop=console` -Removes all `console.*` method calls from code. +Removes all `console.*` method calls from code. If your code uses the value of a call, Bun puts `undefined` in its place. ```ts Input console.log("debug"); @@ -1145,25 +1155,23 @@ x = console.error("error"); ``` ```js Output -void 0; -void 0; -x=void 0; +x = undefined; ``` ### Drop custom function calls -**Mode:** `--drop=` +**Mode:** `--drop=assert` -Removes calls to specified global functions and their methods. +`--drop=` removes calls to the global function `` and to its methods. If your code uses the value of a call, Bun puts `undefined` in its place. ```ts Input assert(condition); assert.equal(a, b); +x = assert(condition); ``` -```js Output with --drop=assert -void 0; -void 0; +```js Output +x = undefined; ``` ## Keep Names @@ -1191,9 +1199,11 @@ await Bun.build({ ## Combined Example +**Mode:** `--minify` + Using all three minification modes together: -```ts input.ts (176 bytes) +```ts Input const myVariable = 42; const myFunction = () => { @@ -1205,8 +1215,7 @@ const myFunction = () => { const output = myFunction(); ``` -```js output.js -// Output with --minify (20 bytes, 89% reduction) +```js Output var t=()=>42,e=t(); ``` diff --git a/scripts/minifier-docs.ts b/scripts/minifier-docs.ts new file mode 100644 index 000000000000..d75f16a7c139 --- /dev/null +++ b/scripts/minifier-docs.ts @@ -0,0 +1,285 @@ +// Generates the Output blocks of `docs/bundler/minifier.mdx`. +// +// You write a section's heading, its `**Mode:**` line, its description and its Input by hand. The Mode line is the +// `bun build` flags of the section, each in a code span, or `Always active` for none. This script saves the Input as +// `input.ts`, runs `bun build ./input.ts ` and puts what that prints into the section's Output block: +// +// bun bd scripts/minifier-docs.ts --write +// +// It builds with the bun that runs it, or with `--bun=`. Without `--write` it lists the sections that +// are out of date and exits with 1. +// +// `test/bundler/minifier-docs.test.ts` fails if an Output block is stale. A change to the minifier that changes what +// a section prints needs `--write` with the new build. +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, relative } from "node:path"; + +export const pagePath = join(import.meta.dirname, "..", "docs", "bundler", "minifier.mdx"); + +type Section = { + title: string; + /** 1-based line of the `**Mode:**` line. */ + line: number; + flags: string[]; + /** File name to source. `input.ts` is the entry point. */ + files: Record; + /** The lines of the page that the body of the Output block covers: the first one, and one past the last. */ + output: [start: number, end: number]; +}; + +/** Runs `bun build` with these arguments in `cwd`. */ +export type Build = (cwd: string, args: string[]) => Promise<{ exitCode: number; stderr: string }>; + +/** `bun build` inlines `process.env.NODE_ENV`, which `bun test` sets, so every caller builds without it. */ +export function buildWith(bun: string, env: Record): Build { + return async (cwd, args) => { + await using proc = Bun.spawn({ + cmd: [bun, "build", ...args], + env: { ...env, NODE_ENV: undefined, BUN_ENV: undefined, NO_COLOR: "1" }, + cwd, + stdout: "ignore", + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + return { exitCode, stderr }; + }; +} + +// The Mode lines become the argv of a `bun build` that runs in CI, so they can only name the flags listed here. +const FLAG = /^--(?:minify(?:-syntax|-whitespace|-identifiers)?|drop=[A-Za-z_$][\w$]*)$/; +const NO_FLAGS = "Always active"; +const ENTRY = "input.ts"; +/** Under this heading every `###` heading is a section. */ +const EVERY = "## All Transformations"; + +function flagsOf(mode: string) { + if (mode === NO_FLAGS) return []; + const flags = mode.split(" ").map(it => /^`([^`]+)`$/.exec(it)?.[1] ?? ""); + if (flags.every(it => FLAG.test(it)) && new Set(flags).size === flags.length) return flags; + return undefined; +} + +/** Every section that has a `**Mode:**` line. `problems` lists what keeps the page from being generated. */ +function sectionsOf(page: string) { + const lines = page.split("\n"); + const sections: Section[] = []; + const problems: string[] = []; + let heading = { text: "", line: 0, needsMode: false }; + let underEvery = false; + let open: { section: Section; inputs: number; outputs: number; hasFlags: boolean } | undefined; + + const close = () => { + if (open) { + const where = `line ${open.section.line}: "${open.section.title}"`; + if (open.inputs !== 1) problems.push(`${where} has ${open.inputs} \`\`\`ts Input blocks. It needs one.`); + if (open.outputs !== 1) problems.push(`${where} has ${open.outputs} \`\`\`js Output blocks. It needs one.`); + if (open.inputs === 1 && open.outputs === 1 && open.hasFlags) sections.push(open.section); + } else if (heading.needsMode) { + problems.push(`line ${heading.line}: "${heading.text}" has no **Mode:** line`); + } + open = undefined; + }; + + for (let i = 0; i < lines.length; i++) { + const at = `line ${i + 1}`; + const title = /^(#{2,3}) (.+)$/.exec(lines[i]); + if (title) { + close(); + if (title[1] === "##") underEvery = lines[i] === EVERY; + heading = { text: title[2], line: i + 1, needsMode: underEvery && title[1] === "###" }; + continue; + } + + const mode = /^\*\*Mode:\*\* (.+)$/.exec(lines[i]); + if (mode) { + const flags = flagsOf(mode[1]); + if (open) { + problems.push(`${at}: "${heading.text}" has a second **Mode:** line`); + continue; + } + if (!flags) + problems.push( + `${at}: the Mode of "${heading.text}" has to be \`${NO_FLAGS}\`, or flags from the list in scripts/minifier-docs.ts, each in a code span`, + ); + const section: Section = { title: heading.text, line: i + 1, flags: flags ?? [], files: {}, output: [0, 0] }; + open = { section, inputs: 0, outputs: 0, hasFlags: flags !== undefined }; + continue; + } + + const fence = /^```(\w*) ?(.*)$/.exec(lines[i]); + if (!fence) continue; + const end = lines.indexOf("```", i + 1); + if (end < 0) { + problems.push(`${at}: this code block has no end`); + break; + } + const [, language, name] = fence; + if (name === "Output") { + if (!open) + problems.push(`${at}: no section with a **Mode:** line has this Output block, so nothing generates it`); + else if (language !== "js") problems.push(`${at}: an Output block is \`\`\`js Output`); + else if (!open.inputs) problems.push(`${at}: the Output block comes after the Input block`); + else if (!open.outputs++) open.section.output = [i + 1, end]; + } else if (open) { + const file = name === "Input" ? ENTRY : name; + // Lower case, so that two names are two files on every file system. + if (language !== "ts" || !/^[a-z0-9_-]+\.ts$/.test(file) || name === ENTRY) + problems.push(`${at}: a code block in a section is \`\`\`ts Input, \`\`\`ts .ts or \`\`\`js Output`); + else if (file in open.section.files) problems.push(`${at}: the section has two blocks for ${file}`); + else if (open.outputs) problems.push(`${at}: the Output block is the last block of a section`); + else { + open.section.files[file] = lines.slice(i + 1, end).join("\n") + "\n"; + if (file === ENTRY) open.inputs++; + } + } + i = end; + } + close(); + if (!lines.includes(EVERY)) problems.push(`the page has no "${EVERY}" heading`); + return { lines, sections, problems }; +} + +const keyOf = (flags: string[]) => flags.join(" "); +const without = (flags: string[], flag: string) => flags.filter(it => it !== flag); + +/** + * Builds every section with its flags, and once more without each one of them. That's one `bun build` for each list + * of flags, whatever the number of sections. + * + * `outputs` has each section's Output block. `problems` also lists a flag that changes nothing in its section: the + * Mode line says the transformation needs that flag, and it doesn't. + */ +async function outputsOf(sections: Section[], dir: string, build: Build) { + const problems: string[] = []; + const groups = new Map(); + const ask = (section: Section, flags: string[]) => { + let group = groups.get(keyOf(flags)); + if (!group) groups.set(keyOf(flags), (group = { flags, sections: [] })); + group.sections.push(section); + }; + for (const section of sections) { + ask(section, section.flags); + for (const flag of section.flags) ask(section, without(section.flags, flag)); + } + + // Nothing above `src` has a say in what Bun prints. + const source = join(dir, "src"); + mkdirSync(source, { recursive: true }); + writeFileSync(join(source, "package.json"), "{}\n"); + writeFileSync(join(source, "tsconfig.json"), "{}\n"); + const folderOf = (section: Section) => String(sections.indexOf(section)); + for (const section of sections) { + for (const [name, text] of Object.entries(section.files)) { + const path = join(source, folderOf(section), name); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, text); + } + } + + const printed = new Map(); + const failed = await Promise.all( + [...groups.values()].map(async (group, index) => { + const command = ["bun build", ...group.flags].join(" "); + const outdir = join(dir, "out", String(index)); + const entries = group.sections.map(it => `./${folderOf(it)}/${ENTRY}`); + try { + const built = await build(source, [...entries, ...group.flags, "--root", ".", "--outdir", outdir]); + if (built.exitCode !== 0) { + // The path of a file in an error becomes the title of its section. + const errors = built.stderr.replaceAll(/\S*[\\/]src[\\/](\d+)[\\/]/g, (path, folder) => { + const section = sections[Number(folder)]; + return section ? `"${section.title}": ` : path; + }); + return { command, exitCode: built.exitCode, errors: errors.trim() }; + } + for (const section of group.sections) { + const folder = folderOf(section); + const path = join(outdir, folder, "input.js"); + if (!existsSync(path)) { + problems.push(`\`${command}\` wrote no file for "${section.title}"`); + continue; + } + // Above the code of each source file there is a comment with its path, unless whitespace is minified. + const names = Object.keys(section.files).map(it => it.replaceAll(".", "\\.")); + const comment = new RegExp(`^// ${folder}[\\\\/](?:${names.join("|")})\n`, "gm"); + const text = readFileSync(path, "utf8").replaceAll(comment, "").trimEnd(); + printed.set(`${folder}\0${keyOf(group.flags)}`, text); + } + return undefined; + } catch (error) { + return { command, exitCode: undefined, errors: String(error) }; + } + }), + ); + // A section that doesn't build fails every build it is in, with the same errors. + const seen = new Set(); + for (const failure of failed) { + if (!failure || (failure.errors && seen.has(failure.errors))) continue; + seen.add(failure.errors); + const code = failure.exitCode === undefined ? "" : ` with exit code ${failure.exitCode}`; + problems.push(`\`${failure.command}\` failed${code}:\n${failure.errors}`); + } + + const outputs = new Map(); + for (const section of sections) { + const under = (flags: string[]) => printed.get(`${folderOf(section)}\0${keyOf(flags)}`); + const output = under(section.flags); + if (output === undefined) continue; + outputs.set(section, output); + if (output === "") problems.push(`line ${section.line}: "${section.title}" prints nothing`); + for (const flag of section.flags) { + if (under(without(section.flags, flag)) === output) + problems.push(`line ${section.line}: "${section.title}" prints the same without \`${flag}\``); + } + } + return { outputs, problems }; +} + +/** The page with every Output block as `outputs` has it, and the titles of the sections whose block was different. */ +function withOutputs(lines: string[], sections: Section[], outputs: Map) { + const next = [...lines]; + const stale: string[] = []; + // From the last section up, so that the line numbers above it stay right. + for (const section of sections.toReversed()) { + const output = outputs.get(section); + if (output === undefined) continue; + const [start, end] = section.output; + if (lines.slice(start, end).join("\n") === output) continue; + stale.unshift(section.title); + next.splice(start, end - start, ...output.split("\n")); + } + return { text: next.join("\n"), stale }; +} + +/** + * The page as it should be, the sections whose Output block is stale, and what is wrong with the page. `dir` is an + * empty folder to build in. + */ +export async function generated(page: string, dir: string, build: Build) { + const { lines, sections, problems } = sectionsOf(page); + const { outputs, problems: more } = await outputsOf(sections, dir, build); + return { ...withOutputs(lines, sections, outputs), sections: sections.length, problems: [...problems, ...more] }; +} + +if (import.meta.main) { + const flag = (name: string) => process.argv.find(it => it.startsWith(`--${name}=`))?.slice(name.length + 3); + const bun = flag("bun") ?? process.execPath; + const write = process.argv.includes("--write"); + const before = readFileSync(pagePath, "utf8").replaceAll("\r\n", "\n"); + const dir = mkdtempSync(join(tmpdir(), "bun-minifier-docs-")); + try { + const { text, stale, problems } = await generated(before, dir, buildWith(bun, process.env)); + for (const problem of problems) console.error(problem); + if (write && text !== before) writeFileSync(pagePath, text); + if (!write) for (const title of stale) console.log(`out of date: ${title}`); + const revision = Bun.spawnSync({ cmd: [bun, "--revision"] }) + .stdout.toString() + .trim(); + const page = relative(process.cwd(), pagePath); + console.log(`${stale.length} Output blocks ${write ? "written" : "out of date"} in ${page} (bun ${revision})`); + if (problems.length || (stale.length && !write)) process.exitCode = 1; + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} diff --git a/test/bundler/minifier-docs.test.ts b/test/bundler/minifier-docs.test.ts new file mode 100644 index 000000000000..5d576f5e509a --- /dev/null +++ b/test/bundler/minifier-docs.test.ts @@ -0,0 +1,18 @@ +// docs/bundler/minifier.mdx shows an Input and an Output for each transformation of the minifier. +// scripts/minifier-docs.ts generates the Output blocks with `bun build`. This test generates them again with the bun +// under test. +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; +import { readFileSync } from "node:fs"; +import { buildWith, generated, pagePath } from "../../scripts/minifier-docs.ts"; + +test("docs/bundler/minifier.mdx shows what `bun build` prints", async () => { + using dir = tempDir("minifier-docs", {}); + const page = readFileSync(pagePath, "utf8").replaceAll("\r\n", "\n"); + const { text, stale, problems, sections } = await generated(page, String(dir), buildWith(bunExe(), bunEnv)); + + expect(problems).toEqual([]); + expect(sections).toBeGreaterThan(50); + const update = "bun bd scripts/minifier-docs.ts --write"; + expect(page, `Out of date: ${stale.join(", ")}. If the new Output is right, run \`${update}\`.`).toBe(text); +}); From 781071e492acf42c5dff49331f17eb80ca135b7e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 7 Nov 2026 14:40:45 +0000 Subject: [PATCH 2/5] minifier-docs: keep Input lines that look like a comment with a path Bun writes a comment with the path above the code of each source file in a bundle. The generator takes that comment out of an Output. It matched the comment with a pattern, so it also took out a line of a legal comment or of a template literal that had the same text. The folders that the generator builds in now have a name that no Input contains, and the generator removes only the lines that are exactly the comment of one of the section's files. Also resolve a relative --bun path before the builds run in another folder, and report a --bun path that cannot run in one line. --- scripts/minifier-docs.ts | 40 +++++++++++++++++++----------- test/bundler/minifier-docs.test.ts | 28 ++++++++++++++++++++- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/scripts/minifier-docs.ts b/scripts/minifier-docs.ts index d75f16a7c139..23dcaec23ee5 100644 --- a/scripts/minifier-docs.ts +++ b/scripts/minifier-docs.ts @@ -13,7 +13,7 @@ // a section prints needs `--write` with the new build. import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, join, relative } from "node:path"; +import { dirname, join, relative, resolve } from "node:path"; export const pagePath = join(import.meta.dirname, "..", "docs", "bundler", "minifier.mdx"); @@ -148,7 +148,8 @@ const without = (flags: string[], flag: string) => flags.filter(it => it !== fla * of flags, whatever the number of sections. * * `outputs` has each section's Output block. `problems` also lists a flag that changes nothing in its section: the - * Mode line says the transformation needs that flag, and it doesn't. + * Mode line says the transformation needs that flag, and it doesn't. A flag for whitespace changes almost every + * Output, so this finds the syntax, identifier and drop flags that a section doesn't need. */ async function outputsOf(sections: Section[], dir: string, build: Build) { const problems: string[] = []; @@ -168,7 +169,12 @@ async function outputsOf(sections: Section[], dir: string, build: Build) { mkdirSync(source, { recursive: true }); writeFileSync(join(source, "package.json"), "{}\n"); writeFileSync(join(source, "tsconfig.json"), "{}\n"); - const folderOf = (section: Section) => String(sections.indexOf(section)); + // Unless whitespace is minified, Bun writes a comment with the path above the code of each source file, such as + // `// section-3/input.ts`. No Input has the name of a folder in it, so such a line is never a line of an Input. + let prefix = "section-"; + while (sections.some(it => Object.values(it.files).some(text => text.includes(prefix)))) prefix += "-"; + const folderOf = (section: Section) => prefix + sections.indexOf(section); + const sectionIn = new Map(sections.map(it => [folderOf(it), it])); for (const section of sections) { for (const [name, text] of Object.entries(section.files)) { const path = join(source, folderOf(section), name); @@ -187,8 +193,8 @@ async function outputsOf(sections: Section[], dir: string, build: Build) { const built = await build(source, [...entries, ...group.flags, "--root", ".", "--outdir", outdir]); if (built.exitCode !== 0) { // The path of a file in an error becomes the title of its section. - const errors = built.stderr.replaceAll(/\S*[\\/]src[\\/](\d+)[\\/]/g, (path, folder) => { - const section = sections[Number(folder)]; + const errors = built.stderr.replaceAll(/\S*[\\/]src[\\/]([^\\/\s]+)[\\/]/g, (path, folder) => { + const section = sectionIn.get(folder); return section ? `"${section.title}": ` : path; }); return { command, exitCode: built.exitCode, errors: errors.trim() }; @@ -200,11 +206,10 @@ async function outputsOf(sections: Section[], dir: string, build: Build) { problems.push(`\`${command}\` wrote no file for "${section.title}"`); continue; } - // Above the code of each source file there is a comment with its path, unless whitespace is minified. - const names = Object.keys(section.files).map(it => it.replaceAll(".", "\\.")); - const comment = new RegExp(`^// ${folder}[\\\\/](?:${names.join("|")})\n`, "gm"); - const text = readFileSync(path, "utf8").replaceAll(comment, "").trimEnd(); - printed.set(`${folder}\0${keyOf(group.flags)}`, text); + const comments = new Set(Object.keys(section.files).map(name => `// ${folder}/${name}`)); + const lines = readFileSync(path, "utf8").split("\n"); + const text = lines.filter(line => !comments.has(line)).join("\n"); + printed.set(`${folder}\0${keyOf(group.flags)}`, text.trimEnd()); } return undefined; } catch (error) { @@ -264,7 +269,17 @@ export async function generated(page: string, dir: string, build: Build) { if (import.meta.main) { const flag = (name: string) => process.argv.find(it => it.startsWith(`--${name}=`))?.slice(name.length + 3); - const bun = flag("bun") ?? process.execPath; + // A build runs in another folder, so a relative path has to be resolved here. + const bun = resolve(flag("bun") ?? process.execPath); + let revision: string; + try { + revision = Bun.spawnSync({ cmd: [bun, "--revision"] }) + .stdout.toString() + .trim(); + } catch (error) { + console.error(`error: cannot run "${bun}": ${error instanceof Error ? error.message : error}`); + process.exit(1); + } const write = process.argv.includes("--write"); const before = readFileSync(pagePath, "utf8").replaceAll("\r\n", "\n"); const dir = mkdtempSync(join(tmpdir(), "bun-minifier-docs-")); @@ -273,9 +288,6 @@ if (import.meta.main) { for (const problem of problems) console.error(problem); if (write && text !== before) writeFileSync(pagePath, text); if (!write) for (const title of stale) console.log(`out of date: ${title}`); - const revision = Bun.spawnSync({ cmd: [bun, "--revision"] }) - .stdout.toString() - .trim(); const page = relative(process.cwd(), pagePath); console.log(`${stale.length} Output blocks ${write ? "written" : "out of date"} in ${page} (bun ${revision})`); if (problems.length || (stale.length && !write)) process.exitCode = 1; diff --git a/test/bundler/minifier-docs.test.ts b/test/bundler/minifier-docs.test.ts index 5d576f5e509a..f1d219626cd1 100644 --- a/test/bundler/minifier-docs.test.ts +++ b/test/bundler/minifier-docs.test.ts @@ -6,7 +6,7 @@ import { bunEnv, bunExe, tempDir } from "harness"; import { readFileSync } from "node:fs"; import { buildWith, generated, pagePath } from "../../scripts/minifier-docs.ts"; -test("docs/bundler/minifier.mdx shows what `bun build` prints", async () => { +test.concurrent("docs/bundler/minifier.mdx shows what `bun build` prints", async () => { using dir = tempDir("minifier-docs", {}); const page = readFileSync(pagePath, "utf8").replaceAll("\r\n", "\n"); const { text, stale, problems, sections } = await generated(page, String(dir), buildWith(bunExe(), bunEnv)); @@ -16,3 +16,29 @@ test("docs/bundler/minifier.mdx shows what `bun build` prints", async () => { const update = "bun bd scripts/minifier-docs.ts --write"; expect(page, `Out of date: ${stale.join(", ")}. If the new Output is right, run \`${update}\`.`).toBe(text); }); + +// The generator takes the comment with the path, which Bun writes above the code of a source file, out of an Output. +test.concurrent("an Output keeps the lines of its Input that look like a comment with a path", async () => { + using dir = tempDir("minifier-docs-path-comment", {}); + const kept = ["/*! A comment that Bun keeps", "// 0/input.ts", "// section-0/input.ts", "*/", "console.log(1);"]; + const page = (output: string[]) => + [ + "## All Transformations", + "", + "### Comments", + "", + "**Mode:** Always active", + "", + "```ts Input", + ...kept, + "```", + "", + "```js Output", + ...output, + "```", + "", + ].join("\n"); + const { text, problems } = await generated(page([]), String(dir), buildWith(bunExe(), bunEnv)); + + expect({ text, problems }).toEqual({ text: page(kept), problems: [] }); +}); From 8c368ad0dfbac941e52c8548a2f5156c6841db81 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:05:42 +0000 Subject: [PATCH 3/5] minifier-docs: look a --bun name up on PATH, give each build two threads After the last change every --bun value was resolved as a path, so `--bun=bun` named a file in the working folder. A value with no folder in it is now looked up on PATH, as a shell does. A relative path is still resolved against the working folder. Each `bun build` started one thread for each core. The generator runs eight small builds side by side, so on a debug build the threads cost more time than the builds. Each build now gets two threads. On a debug build with about 4 usable cores the page test takes 1.5 to 3.4 s, where it took 2.3 to 5.2 s and passed the 5 s default in half of the runs. --- scripts/minifier-docs.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/minifier-docs.ts b/scripts/minifier-docs.ts index 23dcaec23ee5..e765b25a1318 100644 --- a/scripts/minifier-docs.ts +++ b/scripts/minifier-docs.ts @@ -31,12 +31,15 @@ type Section = { /** Runs `bun build` with these arguments in `cwd`. */ export type Build = (cwd: string, args: string[]) => Promise<{ exitCode: number; stderr: string }>; -/** `bun build` inlines `process.env.NODE_ENV`, which `bun test` sets, so every caller builds without it. */ +/** + * `bun build` inlines `process.env.NODE_ENV`, which `bun test` sets, so every caller builds without it. The builds are + * small and run side by side, so each one gets two threads and not one for each core. + */ export function buildWith(bun: string, env: Record): Build { return async (cwd, args) => { await using proc = Bun.spawn({ cmd: [bun, "build", ...args], - env: { ...env, NODE_ENV: undefined, BUN_ENV: undefined, NO_COLOR: "1" }, + env: { ...env, NODE_ENV: undefined, BUN_ENV: undefined, NO_COLOR: "1", UV_THREADPOOL_SIZE: "2" }, cwd, stdout: "ignore", stderr: "pipe", @@ -269,8 +272,9 @@ export async function generated(page: string, dir: string, build: Build) { if (import.meta.main) { const flag = (name: string) => process.argv.find(it => it.startsWith(`--${name}=`))?.slice(name.length + 3); - // A build runs in another folder, so a relative path has to be resolved here. - const bun = resolve(flag("bun") ?? process.execPath); + // A build runs in another folder, so a relative path has to be resolved here. A name with no folder is a command. + const named = flag("bun") ?? process.execPath; + const bun = /[\\/]/.test(named) ? resolve(named) : (Bun.which(named) ?? named); let revision: string; try { revision = Bun.spawnSync({ cmd: [bun, "--revision"] }) From 26fcf0539aeaab8ec5df0457ce7d9eaedd85793d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 8 Nov 2026 06:12:45 +0000 Subject: [PATCH 4/5] docs(minifier): show which calls --drop keeps The page said that `--drop=console` removes all `console.*` calls. Bun removes the calls that are written as `console.(...)`. It keeps `globalThis.console.log()` and `console["log"]()`, and it removes the call with brackets only when `--minify-syntax` is also set. The Inputs of "Drop console calls" and "Drop custom function calls" now have such calls, so their Outputs show what stays, and the test fails when a build changes that. --- docs/bundler/minifier.mdx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/bundler/minifier.mdx b/docs/bundler/minifier.mdx index b90dfd775ba0..f80693facb34 100644 --- a/docs/bundler/minifier.mdx +++ b/docs/bundler/minifier.mdx @@ -1146,32 +1146,38 @@ console.log(test()); **Mode:** `--drop=console` -Removes all `console.*` method calls from code. If your code uses the value of a call, Bun puts `undefined` in its place. +Removes the calls that are written as `console.(...)`. If your code uses the value of a call, Bun puts `undefined` in its place. Bun keeps a call that reaches `console` in another way, such as `globalThis.console.log()` or `console["log"]()`. If you also set `--minify-syntax`, Bun removes the call with brackets too. ```ts Input console.log("debug"); console.warn("warning"); x = console.error("error"); +globalThis.console.log("kept"); +console["log"]("kept"); ``` ```js Output x = undefined; +globalThis.console.log("kept"); +console["log"]("kept"); ``` ### Drop custom function calls **Mode:** `--drop=assert` -`--drop=` removes calls to the global function `` and to its methods. If your code uses the value of a call, Bun puts `undefined` in its place. +`--drop=` removes calls to the global function `` and to its methods. If your code uses the value of a call, Bun puts `undefined` in its place. As with `console`, Bun keeps a method call that is written with brackets, unless you also set `--minify-syntax`. ```ts Input assert(condition); assert.equal(a, b); x = assert(condition); +assert["equal"](a, b); ``` ```js Output x = undefined; +assert["equal"](a, b); ``` ## Keep Names From e56b8a6a3df8fa9633847e3745a9632f27560e38 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 8 Nov 2026 16:53:01 +0000 Subject: [PATCH 5/5] docs(minifier): give the parameters of four Inputs a type Three Inputs were complete programs whose only type errors were parameters with no type (TS7006 under the tsconfig that `bun init` writes): "Identifier renaming", "Whitespace removal" and "Function formatting". Their parameters now have types, and so does the arrow function in "Arrow function body shortening". The Output of "Identifier renaming" has other short names, because Bun picks the names from the characters of the source. The other Outputs are the same. --- docs/bundler/minifier.mdx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/bundler/minifier.mdx b/docs/bundler/minifier.mdx index f80693facb34..b61c3558aeab 100644 --- a/docs/bundler/minifier.mdx +++ b/docs/bundler/minifier.mdx @@ -671,7 +671,7 @@ console.log(used); Renames local variables to shorter names based on usage frequency. ```ts Input -function calculateSum(firstNumber, secondNumber) { +function calculateSum(firstNumber: number, secondNumber: number) { const result = firstNumber + secondNumber; return result; } @@ -679,11 +679,11 @@ console.log(calculateSum(1, 2)); ``` ```js Output -function o(c, l) { - const n = c + l; - return n; +function c(n, u) { + const e = n + u; + return e; } -console.log(o(1, 2)); +console.log(c(1, 2)); ``` **Naming strategy:** @@ -707,7 +707,7 @@ console.log(o(1, 2)); Removes all unnecessary whitespace. ```ts Input -function add(a, b) { +function add(a: number, b: number) { return a + b; } console.log(add(1, 2)); @@ -816,10 +816,10 @@ if(condition){doSomething()}for(let i=0;i<10;i++){console.log(i)} Removes whitespace in function declarations. ```ts Input -function myFunction(param1, param2) { +function myFunction(param1: number, param2: number) { return param1 + param2; } -console.log(myFunction, (a, b) => a + b); +console.log(myFunction, (a: number, b: number) => a + b); ``` ```js Output @@ -1097,7 +1097,7 @@ Uses expression body syntax when an arrow function only returns a value. ```ts Input console.log(() => { return x; }); -console.log((a) => { return a + 1; }); +console.log((a: number) => { return a + 1; }); ``` ```js Output