mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <Jarred-Sumner@users.noreply.github.com>
341 lines
11 KiB
TypeScript
341 lines
11 KiB
TypeScript
import { describe, expect } from "bun:test";
|
|
import { itBundled } from "./expectBundled";
|
|
|
|
describe("bundler", () => {
|
|
describe("minify/Symbol.for", () => {
|
|
// Test basic Symbol.for removal when unused
|
|
itBundled("minify/SymbolForUnused", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
// These should be removed when minifySyntax is true
|
|
Symbol.for("test1");
|
|
Symbol.for("test2");
|
|
Symbol.for(\`test3\`);
|
|
Symbol.for("test" + 4); // This has a side effect (string concatenation)
|
|
|
|
// Keep reference to prove concatenation happened
|
|
var sideEffect = "test" + 4;
|
|
Symbol.for(sideEffect);
|
|
|
|
// These should NOT be removed (used values)
|
|
const s1 = Symbol.for("used1");
|
|
let s2 = Symbol.for("used2");
|
|
var s3 = Symbol.for("used3");
|
|
|
|
// Function argument - should not be removed
|
|
console.log(Symbol.for("argument"));
|
|
|
|
// Property access - should not be removed
|
|
const obj = { prop: Symbol.for("property") };
|
|
|
|
// Return value - should not be removed
|
|
function getSymbol() {
|
|
return Symbol.for("return");
|
|
}
|
|
|
|
capture(s1, s2, s3, obj.prop, getSymbol(), sideEffect);
|
|
`,
|
|
},
|
|
minifySyntax: true,
|
|
onAfterBundle(api) {
|
|
const output = api.readFile("/out.js");
|
|
|
|
// Should remove unused Symbol.for calls
|
|
expect(output).not.toContain('Symbol.for("test1")');
|
|
expect(output).not.toContain('Symbol.for("test2")');
|
|
expect(output).not.toContain("Symbol.for(`test3`)");
|
|
|
|
// Should keep the concatenation because sideEffect variable is used
|
|
expect(output).toContain("test4");
|
|
|
|
// Should keep used Symbol.for calls
|
|
expect(output).toContain('Symbol.for("used1")');
|
|
expect(output).toContain('Symbol.for("used2")');
|
|
expect(output).toContain('Symbol.for("used3")');
|
|
expect(output).toContain('Symbol.for("argument")');
|
|
expect(output).toContain('Symbol.for("property")');
|
|
expect(output).toContain('Symbol.for("return")');
|
|
},
|
|
});
|
|
|
|
// Test that Symbol.for is not removed when minifySyntax is false
|
|
itBundled("minify/SymbolForNoMinifySyntax", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
Symbol.for("test1");
|
|
Symbol.for("test2");
|
|
const s = Symbol.for("test3");
|
|
capture(s);
|
|
`,
|
|
},
|
|
minifySyntax: false,
|
|
onAfterBundle(api) {
|
|
const output = api.readFile("/out.js");
|
|
|
|
// Should keep all Symbol.for calls when minifySyntax is false
|
|
expect(output).toContain('Symbol.for("test1")');
|
|
expect(output).toContain('Symbol.for("test2")');
|
|
expect(output).toContain('Symbol.for("test3")');
|
|
},
|
|
});
|
|
|
|
// Test interaction with other minification options
|
|
itBundled("minify/SymbolForWithWhitespace", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
// Unused calls should be removed
|
|
Symbol.for("remove-me-1");
|
|
Symbol.for("remove-me-2");
|
|
|
|
// Used call should remain
|
|
const sym = Symbol.for("keep-me");
|
|
|
|
// Test with complex expressions
|
|
const ab = "a" + "b";
|
|
Symbol.for(ab); // Keep the variable to ensure concatenation happens
|
|
Symbol.for(\`template\`);
|
|
|
|
capture(sym, ab);
|
|
`,
|
|
},
|
|
minifySyntax: true,
|
|
minifyWhitespace: true,
|
|
onAfterBundle(api) {
|
|
const output = api.readFile("/out.js");
|
|
|
|
// Should remove unused calls
|
|
expect(output).not.toContain("remove-me-1");
|
|
expect(output).not.toContain("remove-me-2");
|
|
|
|
// Should keep used call
|
|
expect(output).toContain('Symbol.for("keep-me")');
|
|
|
|
// Should keep side effect (the concatenation is kept because ab is used)
|
|
expect(output).toContain("ab");
|
|
},
|
|
});
|
|
|
|
// Test edge cases
|
|
itBundled("minify/SymbolForEdgeCases", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
// Optional chaining - these are preserved because optional chaining has observable behavior
|
|
Symbol?.for("optional1");
|
|
Symbol?.for?.("optional2");
|
|
|
|
// In conditional - these should be optimized based on the condition
|
|
true && Symbol.for("conditional1");
|
|
false || Symbol.for("conditional2");
|
|
|
|
// In ternary - these should be optimized based on the condition
|
|
true ? Symbol.for("ternary1") : null;
|
|
false ? null : Symbol.for("ternary2");
|
|
|
|
// Nested calls
|
|
Symbol.for(Symbol.for("nested"));
|
|
|
|
// With spread
|
|
const arr = [...[Symbol.for("spread")]];
|
|
|
|
// Property key
|
|
const obj = {
|
|
[Symbol.for("key")]: "value"
|
|
};
|
|
|
|
capture(arr, obj);
|
|
`,
|
|
},
|
|
minifySyntax: true,
|
|
onAfterBundle(api) {
|
|
const output = api.readFile("/out.js");
|
|
|
|
// Optional chaining preserves the call because it has observable behavior (checking if Symbol exists)
|
|
expect(output).toContain("optional1");
|
|
expect(output).toContain("optional2");
|
|
|
|
// All the conditional/ternary expressions were optimized away completely
|
|
// because they evaluate to unused Symbol.for calls
|
|
expect(output).not.toContain("conditional1");
|
|
expect(output).not.toContain("conditional2");
|
|
expect(output).not.toContain("ternary1");
|
|
expect(output).not.toContain("ternary2");
|
|
|
|
// Nested call was also optimized away
|
|
expect(output).not.toContain("nested");
|
|
|
|
// Used in spread - should keep
|
|
expect(output).toContain('Symbol.for("spread")');
|
|
|
|
// Used as property key - should keep
|
|
expect(output).toContain('Symbol.for("key")');
|
|
},
|
|
});
|
|
|
|
// Test that Symbol.keyFor is not affected (it's still in the property access list)
|
|
itBundled("minify/SymbolKeyForNotAffected", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
// Symbol.keyFor should still be removed as a property access
|
|
Symbol.keyFor;
|
|
|
|
// But not when called
|
|
const sym = Symbol.for("test");
|
|
const key = Symbol.keyFor(sym);
|
|
|
|
capture(key);
|
|
`,
|
|
},
|
|
minifySyntax: true,
|
|
onAfterBundle(api) {
|
|
const output = api.readFile("/out.js");
|
|
|
|
// The unused property access "Symbol.keyFor;" should be removed
|
|
// But the function call "Symbol.keyFor(sym)" should remain
|
|
// So we should find exactly one occurrence of "Symbol.keyFor"
|
|
const keyForMatches = output.match(/Symbol\.keyFor/g) || [];
|
|
expect(keyForMatches.length).toBe(1);
|
|
|
|
// Function call should remain
|
|
expect(output).toContain("Symbol.keyFor(");
|
|
},
|
|
});
|
|
|
|
// Test interaction with production mode
|
|
itBundled("minify/SymbolForProduction", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
// Unused
|
|
Symbol.for("remove-in-prod");
|
|
|
|
// Used
|
|
const s = Symbol.for("keep-in-prod");
|
|
|
|
// Side effects
|
|
Symbol.for(someGlobal);
|
|
|
|
capture(s);
|
|
`,
|
|
},
|
|
production: true, // This enables minifySyntax
|
|
onAfterBundle(api) {
|
|
const output = api.readFile("/out.js");
|
|
|
|
// Should remove unused
|
|
expect(output).not.toContain("remove-in-prod");
|
|
|
|
// Should keep used
|
|
expect(output).toContain("keep-in-prod");
|
|
|
|
// Should keep side effects
|
|
expect(output).toContain("someGlobal");
|
|
},
|
|
});
|
|
|
|
// Test with bundling disabled (transform mode)
|
|
itBundled("minify/SymbolForTransformMode", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
Symbol.for("unused");
|
|
const used = Symbol.for("used");
|
|
export { used };
|
|
`,
|
|
},
|
|
bundling: false,
|
|
minifySyntax: true,
|
|
onAfterBundle(api) {
|
|
const output = api.readFile("/out.js");
|
|
|
|
// Should remove unused in transform mode too
|
|
expect(output).not.toContain('"unused"');
|
|
|
|
// Should keep used
|
|
expect(output).toContain('Symbol.for("used")');
|
|
},
|
|
});
|
|
|
|
// Test interaction with tree shaking
|
|
itBundled("minify/SymbolForTreeShaking", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import { sym } from "./lib.js";
|
|
|
|
// This should be removed
|
|
Symbol.for("entry-unused");
|
|
|
|
capture(sym);
|
|
`,
|
|
"/lib.js": /* js */ `
|
|
// This should be removed (unused export)
|
|
export const unused = Symbol.for("lib-unused-export");
|
|
|
|
// This should be kept (used export)
|
|
export const sym = Symbol.for("lib-used-export");
|
|
|
|
// This should be removed (not exported)
|
|
Symbol.for("lib-internal");
|
|
`,
|
|
},
|
|
minifySyntax: true,
|
|
treeShaking: true,
|
|
onAfterBundle(api) {
|
|
const output = api.readFile("/out.js");
|
|
|
|
// Should remove all unused Symbol.for calls
|
|
expect(output).not.toContain("entry-unused");
|
|
expect(output).not.toContain("lib-unused-export");
|
|
expect(output).not.toContain("lib-internal");
|
|
|
|
// Should keep used Symbol.for call
|
|
expect(output).toContain("lib-used-export");
|
|
},
|
|
});
|
|
|
|
// Test that Symbol.for is still called at runtime when overridden
|
|
itBundled("minify/SymbolForRuntimeOverride", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
let callCount = 0;
|
|
const originalSymbolFor = Symbol.for;
|
|
|
|
// Override Symbol.for to count calls
|
|
Symbol.for = function(key) {
|
|
callCount++;
|
|
return originalSymbolFor.call(this, key);
|
|
};
|
|
|
|
// These unused calls should be removed at bundle time
|
|
Symbol.for("unused1");
|
|
Symbol.for("unused2");
|
|
|
|
// These used calls should remain and increment callCount
|
|
const s1 = Symbol.for("used1");
|
|
const s2 = Symbol.for("used2");
|
|
|
|
// Restore original
|
|
Symbol.for = originalSymbolFor;
|
|
|
|
// Verify that Symbol.for was called for the used symbols
|
|
if (callCount !== 2) {
|
|
throw new Error(\`Expected 2 calls to Symbol.for, got \${callCount}\`);
|
|
}
|
|
|
|
// Verify the symbols work correctly
|
|
if (s1 !== Symbol.for("used1")) {
|
|
throw new Error("Symbol s1 mismatch");
|
|
}
|
|
if (s2 !== Symbol.for("used2")) {
|
|
throw new Error("Symbol s2 mismatch");
|
|
}
|
|
|
|
console.log("PASS");
|
|
`,
|
|
},
|
|
|
|
minifySyntax: true,
|
|
run: {
|
|
stdout: "PASS",
|
|
},
|
|
});
|
|
});
|
|
});
|