diff --git a/.prettierignore b/.prettierignore index b559dd93fb9d..b43b1bb90a70 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..b61c3558aeab 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 @@ -678,19 +671,24 @@ const x = /*@__PURE__*/ expensive(); 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; } +console.log(calculateSum(1, 2)); ``` ```js Output -function n(t,u){const c=t+u;return c} +function c(n, u) { + const e = n + u; + return e; +} +console.log(c(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 @@ -709,30 +707,33 @@ function n(t,u){const c=t+u;return c} Removes all unnecessary whitespace. ```ts Input -function add(a, b) { +function add(a: number, b: number) { 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 @@ -815,14 +816,14 @@ 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; } -const arrow = (a, b) => a + b; +console.log(myFunction, (a: number, b: number) => 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: number) => { 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,44 +1132,52 @@ 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 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 -void 0; -void 0; -x=void 0; +x = undefined; +globalThis.console.log("kept"); +console["log"]("kept"); ``` ### 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. 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 with --drop=assert -void 0; -void 0; +```js Output +x = undefined; +assert["equal"](a, b); ``` ## Keep Names @@ -1191,9 +1205,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 +1221,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..e765b25a1318 --- /dev/null +++ b/scripts/minifier-docs.ts @@ -0,0 +1,301 @@ +// 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, resolve } 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. 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", UV_THREADPOOL_SIZE: "2" }, + 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. 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[] = []; + 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"); + // 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); + 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[\\/]([^\\/\s]+)[\\/]/g, (path, folder) => { + const section = sectionIn.get(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; + } + 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) { + 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); + // 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"] }) + .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-")); + 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 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..f1d219626cd1 --- /dev/null +++ b/test/bundler/minifier-docs.test.ts @@ -0,0 +1,44 @@ +// 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.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)); + + 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); +}); + +// 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: [] }); +});