mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 20:39:05 +00:00
## Summary
- Refactored `maybeMarkConstructorAsPure` to `minifyGlobalConstructor`
that returns `?Expr`
- Added minification optimizations for global constructors that work
identically with/without `new`
- Converts constructors to more compact forms: `new Object()` → `{}`,
`new Array()` → `[]`, etc.
- Fixed issue where minification was incorrectly applied to runtime
node_modules code
## Details
This PR refactors the existing `maybeMarkConstructorAsPure` function to
`minifyGlobalConstructor` and changes it to return an optional
expression. This enables powerful minification optimizations for global
constructors.
### Optimizations Added:
#### 1. Error Constructors (4 bytes saved each)
- `new Error(...)` → `Error(...)`
- `new TypeError(...)` → `TypeError(...)`
- `new SyntaxError(...)` → `SyntaxError(...)`
- `new RangeError(...)` → `RangeError(...)`
- `new ReferenceError(...)` → `ReferenceError(...)`
- `new EvalError(...)` → `EvalError(...)`
- `new URIError(...)` → `URIError(...)`
- `new AggregateError(...)` → `AggregateError(...)`
#### 2. Object Constructor
- `new Object()` → `{}` (11 bytes saved)
- `new Object({a: 1})` → `{a: 1}` (11 bytes saved)
- `new Object([1, 2])` → `[1, 2]` (11 bytes saved)
- `new Object(null)` → `{}` (15 bytes saved)
- `new Object(undefined)` → `{}` (20 bytes saved)
#### 3. Array Constructor
- `new Array()` → `[]` (10 bytes saved)
- `new Array(1, 2, 3)` → `[1, 2, 3]` (9 bytes saved)
- `new Array(5)` → `Array(5)` (4 bytes saved, preserves sparse array
semantics)
#### 4. Function and RegExp Constructors
- `new Function(...)` → `Function(...)` (4 bytes saved)
- `new RegExp(...)` → `RegExp(...)` (4 bytes saved)
### Important Fixes:
- Added check to prevent minification of node_modules code at runtime
(only applies during bundling)
- Preserved sparse array semantics for `new Array(number)`
- Extracted `callFromNew` helper to reduce code duplication
### Size Impact:
- React SSR bundle: 463 bytes saved
- Each optimization safely preserves JavaScript semantics
## Test plan
✅ All tests pass:
- Added comprehensive tests in `bundler_minify.test.ts`
- Verified Error constructors work identically with/without `new`
- Tested Object/Array literal conversions
- Ensured sparse array semantics are preserved
- Updated source map positions in `bundler_npm.test.ts`
🤖 Generated with [Claude Code](https://claude.ai/code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
202 lines
10 KiB
JavaScript
202 lines
10 KiB
JavaScript
import { describe, expect, jest, test } from "bun:test";
|
|
|
|
test("error.cause", () => {
|
|
const err = new Error("error 1");
|
|
const err2 = new Error("error 2", { cause: err });
|
|
expect(
|
|
Bun.inspect(err2)
|
|
.replaceAll("\\", "/")
|
|
.replaceAll(import.meta.dir.replaceAll("\\", "/"), "[dir]"),
|
|
).toMatchInlineSnapshot(`
|
|
"1 | import { describe, expect, jest, test } from "bun:test";
|
|
2 |
|
|
3 | test("error.cause", () => {
|
|
4 | const err = new Error("error 1");
|
|
5 | const err2 = new Error("error 2", { cause: err });
|
|
^
|
|
error: error 2
|
|
at <anonymous> ([dir]/inspect-error.test.js:5:20)
|
|
|
|
1 | import { describe, expect, jest, test } from "bun:test";
|
|
2 |
|
|
3 | test("error.cause", () => {
|
|
4 | const err = new Error("error 1");
|
|
^
|
|
error: error 1
|
|
at <anonymous> ([dir]/inspect-error.test.js:4:19)
|
|
"
|
|
`);
|
|
});
|
|
|
|
test("Error", () => {
|
|
const err = new Error("my message");
|
|
expect(
|
|
Bun.inspect(err)
|
|
.replaceAll("\\", "/")
|
|
.replaceAll(import.meta.dir.replaceAll("\\", "/"), "[dir]"),
|
|
).toMatchInlineSnapshot(`
|
|
"27 | "
|
|
28 | \`);
|
|
29 | });
|
|
30 |
|
|
31 | test("Error", () => {
|
|
32 | const err = new Error("my message");
|
|
^
|
|
error: my message
|
|
at <anonymous> ([dir]/inspect-error.test.js:32:19)
|
|
"
|
|
`);
|
|
});
|
|
|
|
test("BuildMessage", async () => {
|
|
try {
|
|
await import("./inspect-error-fixture-bad.js");
|
|
expect.unreachable();
|
|
} catch (e) {
|
|
expect(
|
|
Bun.inspect(e)
|
|
.replaceAll("\\", "/")
|
|
.replaceAll(import.meta.dir.replaceAll("\\", "/"), "[dir]"),
|
|
).toMatchInlineSnapshot(`
|
|
"2 | const duplicateConstDecl = 456;
|
|
^
|
|
error: "duplicateConstDecl" has already been declared
|
|
at [dir]/inspect-error-fixture-bad.js:2:7
|
|
|
|
1 | const duplicateConstDecl = 123;
|
|
^
|
|
note: "duplicateConstDecl" was originally declared here
|
|
at [dir]/inspect-error-fixture-bad.js:1:7"
|
|
`);
|
|
}
|
|
});
|
|
|
|
function ansiRegex({ onlyFirst = false } = {}) {
|
|
const pattern = [
|
|
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
|
|
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))",
|
|
].join("|");
|
|
|
|
return new RegExp(pattern, onlyFirst ? undefined : "g");
|
|
}
|
|
const stripANSIColors = str => str.replace(ansiRegex(), "");
|
|
const normalizeError = str => {
|
|
// remove debug-only stack trace frames
|
|
// like "at require (:1:21)"
|
|
if (str.includes(" (:")) {
|
|
const splits = str.split("\n");
|
|
for (let i = 0; i < splits.length; i++) {
|
|
if (splits[i].includes(" (:")) {
|
|
splits.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
return splits.join("\n");
|
|
}
|
|
|
|
return str;
|
|
};
|
|
|
|
test("Error inside minified file (no color) ", () => {
|
|
try {
|
|
require("./inspect-error-fixture.min.js");
|
|
expect.unreachable();
|
|
} catch (e) {
|
|
expect(
|
|
normalizeError(
|
|
Bun.inspect(e)
|
|
.replaceAll("\\", "/")
|
|
.replaceAll(import.meta.dir.replaceAll("\\", "/"), "[dir]")
|
|
.trim(),
|
|
),
|
|
).toMatchInlineSnapshot(`
|
|
"21 | exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=Z;
|
|
22 | exports.cache=function(a){return function(){var b=U.current;if(!b)return a.apply(null,arguments);var c=b.getCacheForType(V);b=c.get(a);void 0===b&&(b=W(),c.set(a,b));c=0;for(var f=arguments.length;c<f;c++){var d=arguments[c];if("function"===typeof d||"object"===typeof d&&null!==d){var e=b.o;null===e&&(b.o=e=new WeakMap);b=e.get(d);void 0===b&&(b=W(),e.set(d,b))}else e=b.p,null===e&&(b.p=e=new Map),b=e.get(d),void 0===b&&(b=W(),e.set(d,b))}if(1===b.s)return b.v;if(2===b.s)throw b.v;try{var g=a.apply(null,
|
|
23 | arguments);c=b;c.s=1;return c.v=g}catch(h){throw g=b,g.s=2,g.v=h,h;}}};
|
|
24 | exports.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+a+".");var f=C({},a.props),d=a.key,e=a.ref,g=a._owner;if(null!=b){void 0!==b.ref&&(e=b.ref,g=K.current);void 0!==b.key&&(d=""+b.key);if(a.type&&a.type.defaultProps)var h=a.type.defaultProps;for(k in b)J.call(b,k)&&!L.hasOwnProperty(k)&&(f[k]=void 0===b[k]&&void 0!==h?h[k]:b[k])}var k=arguments.length-2;if(1===k)f.children=c;else if(1<k){h=Array(k);
|
|
25 | for(var m=0;m<k;m++)h[m]=arguments[m+2];f.children=h}return{$$typeof:l,type:a.type,key:d,ref:e,props:f,_owner:g}};exports.createContext=function(a){a={$$typeof:u,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null};a.Provider={$$typeof:t,_context:a};return a.Consumer=a};exports.createElement=M;exports.createFactory=function(a){var b=M.bind(null,a);b.type=a;return b};exports.createRef=function(){return{current:null}};
|
|
26 | exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};expo
|
|
|
|
error: error inside long minified file!
|
|
at <anonymous> ([dir]/inspect-error-fixture.min.js:26:2850)
|
|
at <anonymous> ([dir]/inspect-error-fixture.min.js:26:2890)
|
|
at <anonymous> ([dir]/inspect-error.test.js:101:7)"
|
|
`);
|
|
}
|
|
});
|
|
|
|
test("Error inside minified file (color) ", () => {
|
|
try {
|
|
require("./inspect-error-fixture.min.js");
|
|
expect.unreachable();
|
|
} catch (e) {
|
|
expect(
|
|
// TODO: remove this workaround once snapshots work better
|
|
normalizeError(
|
|
stripANSIColors(
|
|
Bun.inspect(e, { colors: true })
|
|
.replaceAll("\\", "/")
|
|
.replaceAll(import.meta.dir.replaceAll("\\", "/"), "[dir]")
|
|
.trim(),
|
|
).trim(),
|
|
),
|
|
).toMatchInlineSnapshot(`
|
|
"21 | exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=Z;
|
|
22 | exports.cache=function(a){return function(){var b=U.current;if(!b)return a.apply(null,arguments);var c=b.getCacheForType(V);b=c.get(a);void 0===b&&(b=W(),c.set(a,b));c=0;for(var f=arguments.length;c<f;c++){var d=arguments[c];if("function"===typeof d||"object"===typeof d&&null!==d){var e=b.o;null===e&&(b.o=e=new WeakMap);b=e.get(d);void 0===b&&(b=W(),e.set(d,b))}else e=b.p,null===e&&(b.p=e=new Map),b=e.get(d),void 0===b&&(b=W(),e.set(d,b))}if(1===b.s)return b.v;if(2===b.s)throw b.v;try{var g=a.apply(null,
|
|
23 | arguments);c=b;c.s=1;return c.v=g}catch(h){throw g=b,g.s=2,g.v=h,h;}}};
|
|
24 | exports.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+a+".");var f=C({},a.props),d=a.key,e=a.ref,g=a._owner;if(null!=b){void 0!==b.ref&&(e=b.ref,g=K.current);void 0!==b.key&&(d=""+b.key);if(a.type&&a.type.defaultProps)var h=a.type.defaultProps;for(k in b)J.call(b,k)&&!L.hasOwnProperty(k)&&(f[k]=void 0===b[k]&&void 0!==h?h[k]:b[k])}var k=arguments.length-2;if(1===k)f.children=c;else if(1<k){h=Array(k);
|
|
25 | for(var m=0;m<k;m++)h[m]=arguments[m+2];f.children=h}return{$$typeof:l,type:a.type,key:d,ref:e,props:f,_owner:g}};exports.createContext=function(a){a={$$typeof:u,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null};a.Provider={$$typeof:t,_context:a};return a.Consumer=a};exports.createElement=M;exports.createFactory=function(a){var b=M.bind(null,a);b.type=a;return b};exports.createRef=function(){return{current:null}};
|
|
26 | exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.forwardRef=function(a){return{$$typeof:v,render:a}};expo | ... truncated
|
|
|
|
error: error inside long minified file!
|
|
at <anonymous> ([dir]/inspect-error-fixture.min.js:26:2850)
|
|
at <anonymous> ([dir]/inspect-error-fixture.min.js:26:2890)
|
|
at <anonymous> ([dir]/inspect-error.test.js:129:7)"
|
|
`);
|
|
}
|
|
});
|
|
|
|
test("Inserted originalLine and originalColumn do not appear in node:util.inspect", () => {
|
|
const err = new Error("my message");
|
|
expect(
|
|
require("util")
|
|
.inspect(err)
|
|
.replaceAll("\\", "/")
|
|
.replaceAll(import.meta.path.replaceAll("\\", "/"), "[file]"),
|
|
).toMatchInlineSnapshot(`
|
|
"Error: my message
|
|
at <anonymous> ([file]:160:19)"
|
|
`);
|
|
});
|
|
|
|
describe("observable properties", () => {
|
|
for (let property of ["sourceURL", "line", "column"]) {
|
|
test(`${property} is observable`, () => {
|
|
const mock = jest.fn();
|
|
const err = new Error("my message");
|
|
Object.defineProperty(err, property, {
|
|
get: mock,
|
|
enumerable: true,
|
|
configurable: true,
|
|
});
|
|
expect(mock).not.toHaveBeenCalled();
|
|
Bun.inspect(err);
|
|
expect(mock).not.toHaveBeenCalled();
|
|
});
|
|
}
|
|
});
|
|
|
|
test("error.stack throwing an error doesn't lead to a crash", () => {
|
|
const err = new Error("my message");
|
|
Object.defineProperty(err, "stack", {
|
|
get: () => {
|
|
throw new Error("my message");
|
|
},
|
|
enumerable: true,
|
|
configurable: true,
|
|
});
|
|
expect(() => {
|
|
throw err;
|
|
}).toThrow();
|
|
});
|