fix(codegen): drop redundant space after export default when minifying (#23024)

## What

Found with a self-written whitespace analyzer (for each space in oxc's minified output, remove it and check the result still canonicalizes to the same AST — if so the space was non-optimal). The most common hit:

`ExportDefaultDeclaration` printed `"export default "` with an unconditional trailing space, kept before non-identifier values when minifying:

| Input | Before | After |
| --- | --- | --- |
| `export default {a:1}` | `export default {a:1}` | `export default{a:1}` |
| `export default [1]` | `export default [1]` | `export default[1]` |
| `export default (a,b)` | `export default (a,b)` | `export default(a,b)` |
| `export default "s"` | `export default "s"` | ``export default`s` `` |

## Fix

Use a soft space (kept when pretty-printing) and let the value insert a hard space only before an identifier-like token — matching how `return` already works, and esbuild's `SExportDefault` printer (`print("export default")` + `printSpace()`).

Spaces that are required are preserved: `export default function(){}`, `export default 5`, `export default foo`, `export default class C{}`.

### TypeScript

Exposed a latent bug: `TSInterfaceDeclaration` printed `interface` without a `print_space_before_identifier()` guard, so `export default interface I {}` would have minified to `export defaultinterface`. Added the guard (same fix as #23013 for enums). `export default interface`, `export default abstract class`, and `export default <const>x` all verified to round-trip.

Pretty-print output is unchanged. Codegen (115) and minifier (506) suites pass.
This commit is contained in:
Boshen committed 2026-06-06 16:04:27 +00:00
1 parent 62965ae72c
commit cb299261f2
3 files changed
+21 -1

No files matched your search

+3 -1
View File
@@ -1216,7 +1216,8 @@ impl Gen for ExportDefaultDeclaration<'_> {
}
p.print_indent();
p.add_source_mapping(self.span);
p.print_str("export default ");
p.print_str("export default");
p.print_soft_space();
self.declaration.print(p, ctx);
}
}
@@ -3930,6 +3931,7 @@ impl Gen for TSTypeAliasDeclaration<'_> {
impl Gen for TSInterfaceDeclaration<'_> {
fn r#gen(&self, p: &mut Codegen, ctx: Context) {
p.print_space_before_identifier();
if self.declare {
p.print_str("declare ");
}
@@ -45,6 +45,13 @@ fn module_decl() {
"export { default } from './foo.js' with { type: 'json' }",
"export{default}from\"./foo.js\"with{type:\"json\"};",
);
test_minify("export default {a:1}", "export default{a:1};");
test_minify("export default [1]", "export default[1];");
test_minify("export default (a,b)", "export default(a,b);");
test_minify("export default 5", "export default 5;");
test_minify("export default foo", "export default foo;");
test_minify("export default function(){}", "export default function(){}");
}
#[test]
@@ -181,6 +181,17 @@ export import b = require("b");
snapshot_options("minify", &cases, &CodegenOptions::minify());
}
#[test]
fn minify_export_default() {
let min = |src: &str, expected: &str| {
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
};
// A leading `interface`/`abstract`/keyword needs the space; `{`/`<` does not.
min("export default interface I { x: number }", "export default interface I{x:number;}");
min("export default abstract class {}", "export default abstract class{}");
min("export default <const>x;", "export default<const>x;");
}
#[test]
fn ts_as_expression_in_binary_expr() {
test_idempotency("key in (that as object)");