From 549a14b7f521020d3860176e735142aec2203935 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:58:52 +0000 Subject: [PATCH] js_printer: parenthesize missing-import void 0 on LHS of ** A namespace-import property with no matching export is rewritten to EImportIdentifier with import_item_status = Missing, which the printer replaces with undefined. Under --minify-syntax that emits 'void 0', and on the left of ** it was printed at Level::Exponentiation, below print_undefined's Level::Prefix wrap threshold, producing the SyntaxError 'void 0 ** 2'. Bump left_level to Call for EImportIdentifier when minify_syntax is on, mirroring the existing EBoolean arm. The non-Missing cases print an identifier or a member access, neither of which consults the incoming level, so the bump is a no-op there. --- src/js_printer/lib.rs | 6 ++++++ test/bundler/esbuild/importstar.test.ts | 28 ++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 028828a4722e..dd467cbdda08 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -1533,6 +1533,12 @@ pub mod __gated_printer { v.left_level = Level::Call; } } + ExprData::EImportIdentifier(_) => { + // When minifying, a missing import prints as "void 0" + if self.options.minify_syntax { + v.left_level = Level::Call; + } + } _ => {} } } diff --git a/test/bundler/esbuild/importstar.test.ts b/test/bundler/esbuild/importstar.test.ts index 8743b1df9ac4..11d977593ae8 100644 --- a/test/bundler/esbuild/importstar.test.ts +++ b/test/bundler/esbuild/importstar.test.ts @@ -1,4 +1,4 @@ -import { describe } from "bun:test"; +import { describe, expect } from "bun:test"; import { itBundled } from "../expectBundled"; // Tests ported from: @@ -862,6 +862,32 @@ describe("bundler", () => { "/entry.js": [`Import "foo" will always be undefined because there is no matching export in "foo.js"`], }, }); + itBundled("importstar/NamespaceImportMissingES6ExponentiationLHS", { + files: { + "/entry.js": /* js */ ` + import * as ns from './foo' + // A missing import prints as "void 0" under minify_syntax, which is not + // a valid left operand of ** unless parenthesized. + console.log(ns.nope ** 2, 2 ** ns.nope, ns.x ** 2) + `, + "/foo.js": `export const x = 3`, + }, + minifySyntax: true, + run: { + stdout: "NaN NaN 9", + }, + bundleWarnings: { + "/entry.js": [`Import "nope" will always be undefined because there is no matching export in "foo.js"`], + }, + onAfterBundle(api) { + const out = api.readFile("/out.js"); + expect(out).toContain("(void 0) ** 2"); + expect(out).toContain("2 ** void 0"); + // The existing export and the right operand stay unwrapped. + expect(out).not.toContain("(2 ** void 0)"); + expect(out).not.toMatch(/\(\w+\) \*\* 2/); + }, + }); itBundled("importstar/ExportOtherCommonJS", { files: { "/entry.js": `export {bar} from './foo'`,