Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions src/js_parser/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
is_export: bool,
) -> usize {
let was_const = kind == LocalKind::KConst;
// A member of a namespace is not an export of the module.
let applies_entries =
IS_POSSIBLY_DECL_TO_REMOVE && self.enclosing_namespace_arg_ref.is_none();
let mut j: usize = 0;
// Iterate by index so kept entries can be written back through `decls[j]`
// while scanning ahead.
Expand All @@ -305,7 +308,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let mut replacement: Option<
bun_ptr::BackRef<crate::parser::Runtime::ReplaceableExport>,
> = None;
if IS_POSSIBLY_DECL_TO_REMOVE {
if applies_entries {
if let BData::BIdentifier(id) = decl.binding.data {
let id_ref = id.r#ref;
let name = self.load_name_from_ref(id_ref);
Expand Down Expand Up @@ -571,7 +574,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

if IS_POSSIBLY_DECL_TO_REMOVE {
if applies_entries {
self.is_control_flow_dead = orig_dead;
if let BData::BIdentifier(_) = decl.binding.data {
if let Some(_ptr) = replacement {
Expand All @@ -598,7 +601,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
},
},
);
} else if IS_POSSIBLY_DECL_TO_REMOVE {
} else if applies_entries {
if let BData::BIdentifier(id) = decl.binding.data {
let id_ref = id.r#ref;
let name = self.load_name_from_ref(id_ref);
Expand All @@ -613,18 +616,17 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// which is not mutated during the visit pass.
let replacer = _ptr.get();
if !self.replace_decl_and_possibly_remove(decl, replacer) {
let is_after = self.vis_scope().is_after_const_local_prefix;
self.visit_decl(
decl,
VisitDeclOpts {
was_anonymous_named_expr: false,
could_be_const_value: was_const && !is_after,
could_be_macro: false,
},
);
} else {
continue 'outer;
}
let is_after = self.vis_scope().is_after_const_local_prefix;
self.visit_decl(
decl,
VisitDeclOpts {
was_anonymous_named_expr: false,
could_be_const_value: was_const && !is_after,
could_be_macro: false,
},
);
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/js_parser/visit/visit_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let mark_as_dead = p.options.features.dead_code_elimination
&& data.func.flags.contains(flags::Function::IsExport)
&& p.options.features.replace_exports.count() > 0
&& p.enclosing_namespace_arg_ref.is_none()
&& p.is_export_to_eliminate(data.func.name.expect("infallible: name checked").ref_);
let original_is_dead = p.is_control_flow_dead;

Expand Down Expand Up @@ -1048,6 +1049,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let mark_as_dead = p.options.features.dead_code_elimination
&& data.is_export
&& p.options.features.replace_exports.count() > 0
&& p.enclosing_namespace_arg_ref.is_none()
&& p.is_export_to_eliminate(
data.class
.class_name
Expand Down
123 changes: 123 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2345,6 +2345,129 @@ export default class {
`);
});

describe("an export with no initializer", () => {
const eliminate = (names, options) => ({ exports: { eliminate: names }, ...options });
const replace = (entries, options) => ({ exports: { replace: entries }, ...options });
const printed = stdout => ({ stdout, stderr: "", exitCode: 0, signalCode: null });
const expected = rows => rows.map(([, , module, names]) => [module, module, names]);
const print = async rows => {
const modules = [];
for (const [options, source] of rows) {
const transpiler = new Bun.Transpiler({ loader: "ts", ...options });
const module = transpiler.transformSync(source);
modules.push([module, await transpiler.transform(source), transpiler.scan(source).exports]);
}
return modules;
};
const ns = `var NS;\n((NS) => {})(NS ||= {});\n`;

// [options, source, printed module, exports that scan() reports]
const rowsThatEliminate = [
[eliminate(["A"]), `export let A`, ``, []],
[eliminate(["A"]), `export var A: number`, ``, []],
[eliminate(["A"], { loader: "js" }), `export let A; A = 1;`, `A = 1;\n`, []],
[eliminate(["A"]), `export let A, B = 1`, `export let B = 1;\n`, ["B"]],
[eliminate(["A", "B"], { treeShaking: false }), `export let A, B`, ``, []],
[eliminate(["A"], { deadCodeElimination: false }), `export let A`, ``, []],
[eliminate(["A"]), `namespace NS { export let A }`, ns, []],
[eliminate(["A"]), `namespace NS { export declare const A: number }`, ns, []],
];
const sourcesAfterALoad = ["export let A", "export let A = 5", "export let B"];

// `eliminate` aborted the process, so these inputs run in one subprocess. It prints one line for each test.
let subprocess;
const run = () =>
(subprocess ??= (async () => {
using dir = tempDir("exports-no-initializer", {
"other.cjs": Array.from({ length: 50 }, (_, i) => `exports.v${i} = () => ["v", ${i}].join("-");`).join(
"\n",
),
});
const script = async (print, rows, sources, other) => {
console.log(JSON.stringify(await print(rows)));

const transpiler = new Bun.Transpiler({ loader: "ts", exports: { replace: { A: "bar", B: 1 } } });
require(other);
console.log(JSON.stringify(sources.map(source => transpiler.transformSync(source))));
};
const args = [rowsThatEliminate, sourcesAfterALoad, join(String(dir), "other.cjs")];
return await bunRun(["-e", `await (${script})(${print}, ...${JSON.stringify(args)})`]);
})());

it("eliminate", async () => {
const result = await run();
expect({ ...result, stdout: result.stdout.split("\n")[0] }).toEqual(
printed(JSON.stringify(expected(rowsThatEliminate))),
);
});

it("replace", async () => {
const rows = [
[replace({ A: 1 }), `export let A`, `export let A = 1;\n`, ["A"]],
[replace({ A: "bar" }), `export let A`, `export let A = "bar";\n`, ["A"]],
[replace({ A: "bar" }, { loader: "js" }), `export var A`, `export var A = "bar";\n`, ["A"]],
[replace({ A: ["N", "bar"] }), `export let A`, `export let N = "bar";\n`, ["N"]],
[replace({ A: 1 }), `export let A, B`, `export let A = 1;\nexport let B;\n`, ["A", "B"]],
[
replace({ B: 1 }, { treeShaking: false }),
`export let A, B, C`,
`export let A, B = 1, C;\n`,
["A", "B", "C"],
],
// The write of the module comes after the replacement.
[replace({ A: 1 }), `export let A; A = 2;`, `export let A = 1;\nA = 2;\n`, ["A"]],
// As with an initializer, the replaced export ends the prefix of constants to inline.
[
replace({ A: 1 }, { inline: true }),
`export let A; const B = 2; f(B)`,
`export let A = 1;\nconst B = 2;\nf(B);\n`,
["A"],
],
// A namespace member with no value prints what it prints with no `exports` option.
[replace({ A: 1 }), `namespace NS { export let A }`, ns, []],
[replace({ A: ["N", 1] }), `namespace NS { export declare const A: number }`, ns, []],
[
replace({ A: ["N", 1] }),
`namespace NS { let N; export let A }`,
`var NS;\n((NS) => {\n let N;\n})(NS ||= {});\n`,
[],
],
];
expect(await print(rows)).toEqual(expected(rows));
});

it("a member of a namespace is not an export of the module", async () => {
const member = `var NS;\n((NS) => {\n NS.A = 1;\n})(NS ||= {});\n`;
const rows = [
[eliminate(["A"]), `namespace NS { export let A = 1 }`, member, []],
[replace({ A: 2 }), `namespace NS { export let A = 1 }`, member, []],
[replace({ A: ["N", 2] }), `namespace NS { export const A = 1 }`, member, []],
[
eliminate(["A"]),
`namespace NS { export function A() { return f() } }`,
`var NS;\n((NS) => {\n function A() {\n return f();\n }\n NS.A = A;\n})(NS ||= {});\n`,
[],
],
[
replace({ K: 1 }),
`namespace NS { export class K { m() { return f() } } }`,
`var NS;\n((NS) => {\n\n class K {\n m() {\n return f();\n }\n }\n NS.K = K;\n})(NS ||= {});\n`,
[],
],
[eliminate(["A"]), `export namespace NS { export let A = 1 } export let A = 2`, `export ${member}`, ["NS"]],
];
expect(await print(rows)).toEqual(expected(rows));
});

it("replace with a string, after the load of another module", async () => {
const module = `export let A = "bar";\n`;
const result = await run();
expect({ ...result, stdout: result.stdout.split("\n")[1] }).toEqual(
printed(JSON.stringify([module, module, `export let B = 1;\n`])),
);
});
});

it("deletes dead exports and any imports only referenced in dead regions", () => {
const output = transpiler.transformSync(`
import deadFS from 'fs';
Expand Down
Loading