mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-06 17:34:25 +00:00
fix(codegen): drop redundant whitespace in minified TypeScript output (#23038)
Removes redundant whitespace from minified output across many TypeScript constructs. Each change is minify-only: pretty-print output is byte-identical, all outputs round-trip, and a conservative `type_starts_with_non_identifier` helper only drops a space when the following token definitely begins with punctuation.
### Fixes
- **Decorators** — `@dec()class`, `export@dec class`, `accessor#x`
- **`as` / `satisfies`** — `foo()as X`, `` `s`as const ``, `as{a:1}`, `as(x)=>y`, `as[T]`
- **Non-null `!`** — `opts!.x`, `x!;`, `a!()`; the separator is kept only before `=`/`==`/`===` (which would retokenize as `!=`/`!==`), in both binary and assignment positions
- **`enum`** — `enum E{…}`
- **Conditional types** — `T extends[U]?X:Y`, `Foo<T>extends…`; the `? ?C` separator is preserved for a prefix JSDoc-nullable true branch
- **Heritage & constraints** — `class A<T>extends B<T>implements I`, `interface I<T=string>extends…`, `<T extends[a,b]>`
- **Type operators / constructors** — `readonly[…]`, `keyof[a,b]`, `readonly(keyof T)[]`, `new()=>T`, mapped-type `readonly[P in …]`, `export type*from`
### Verification
A whitespace analyzer over ~400 real-world `.ts` files went from **1410 → 4** removable spaces. The remaining 4 are a deliberate `i-->0` tie (handled elsewhere) and two higher-risk edge cases (`f<T>,` instantiation, mapped `in` before a template literal).
`oxc_codegen` and `oxc_minifier` suites pass, `just minsize` shows no drift, clippy is clean, and regression tests were added for each construct.
This commit is contained in:
5 files changed
+275
-49
No files matched your search
+128
-40
@@ -984,7 +984,10 @@ impl Gen for ImportDeclaration<'_> {
|
||||
let local_name = p.get_binding_identifier_name(&spec.local);
|
||||
let imported_name = get_module_export_name(&spec.imported, p);
|
||||
if imported_name != local_name {
|
||||
p.print_str(" as ");
|
||||
p.print_soft_space();
|
||||
p.print_space_before_identifier();
|
||||
p.print_str("as");
|
||||
p.print_soft_space();
|
||||
spec.local.print(p, ctx);
|
||||
}
|
||||
}
|
||||
@@ -1055,7 +1058,12 @@ impl Gen for ExportNamedDeclaration<'_> {
|
||||
p.add_source_mapping(self.span);
|
||||
p.print_str("export");
|
||||
if let Some(decl) = &self.declaration {
|
||||
p.print_hard_space();
|
||||
// A decorated class starts with `@`, so no space is needed after `export`.
|
||||
if matches!(decl, Declaration::ClassDeclaration(c) if !c.decorators.is_empty()) {
|
||||
p.print_soft_space();
|
||||
} else {
|
||||
p.print_hard_space();
|
||||
}
|
||||
match decl {
|
||||
Declaration::VariableDeclaration(decl) => decl.print(p, ctx),
|
||||
Declaration::FunctionDeclaration(decl) => decl.print(p, ctx),
|
||||
@@ -1151,7 +1159,10 @@ impl Gen for ExportSpecifier<'_> {
|
||||
let local_name = get_module_export_name(&self.local, p);
|
||||
let exported_name = get_module_export_name(&self.exported, p);
|
||||
if local_name != exported_name {
|
||||
p.print_str(" as ");
|
||||
p.print_soft_space();
|
||||
p.print_space_before_identifier();
|
||||
p.print_str("as");
|
||||
p.print_soft_space();
|
||||
if let Some(comments) = p.get_comments(self.exported.span().start) {
|
||||
p.print_comments(&comments);
|
||||
p.print_soft_space();
|
||||
@@ -1179,15 +1190,15 @@ impl Gen for ExportAllDeclaration<'_> {
|
||||
p.add_source_mapping(self.span);
|
||||
p.print_str("export");
|
||||
if self.export_kind.is_type() {
|
||||
p.print_str(" type ");
|
||||
} else {
|
||||
p.print_soft_space();
|
||||
p.print_str(" type");
|
||||
}
|
||||
p.print_soft_space();
|
||||
p.print_ascii_byte(b'*');
|
||||
|
||||
if let Some(exported) = &self.exported {
|
||||
p.print_soft_space();
|
||||
p.print_str("as ");
|
||||
p.print_str("as");
|
||||
p.print_soft_space();
|
||||
exported.print(p, ctx);
|
||||
p.print_hard_space();
|
||||
} else {
|
||||
@@ -2023,6 +2034,14 @@ impl GenExpr for AssignmentExpression<'_> {
|
||||
if !cjs_module_lexer::try_print_exports_computed_target(p, &self.left, ctx) {
|
||||
self.left.print(p, ctx);
|
||||
}
|
||||
// `a! = b`: keep a separator so the non-null `!` doesn't glue to `=` as `!=`.
|
||||
if p.options.minify
|
||||
&& self.operator == AssignmentOperator::Assign
|
||||
&& p.prev_op == Some(Operator::Unary(UnaryOperator::LogicalNot))
|
||||
&& p.prev_op_end == p.code_len()
|
||||
{
|
||||
p.print_hard_space();
|
||||
}
|
||||
p.print_soft_space();
|
||||
p.print_str(self.operator.as_str());
|
||||
p.print_soft_space();
|
||||
@@ -2385,7 +2404,14 @@ impl GenExpr for TSAsExpression<'_> {
|
||||
|
||||
p.wrap(wrap, |p| {
|
||||
self.expression.print_expr(p, Precedence::Exponentiation, ctx);
|
||||
p.print_str(" as ");
|
||||
p.print_soft_space();
|
||||
p.print_space_before_identifier();
|
||||
p.print_str("as");
|
||||
if type_starts_with_non_identifier(&self.type_annotation) {
|
||||
p.print_soft_space();
|
||||
} else {
|
||||
p.print_hard_space();
|
||||
}
|
||||
self.type_annotation.print(p, ctx);
|
||||
});
|
||||
}
|
||||
@@ -2397,7 +2423,14 @@ impl GenExpr for TSSatisfiesExpression<'_> {
|
||||
|
||||
p.wrap(wrap, |p| {
|
||||
self.expression.print_expr(p, Precedence::Exponentiation, ctx);
|
||||
p.print_str(" satisfies ");
|
||||
p.print_soft_space();
|
||||
p.print_space_before_identifier();
|
||||
p.print_str("satisfies");
|
||||
if type_starts_with_non_identifier(&self.type_annotation) {
|
||||
p.print_soft_space();
|
||||
} else {
|
||||
p.print_hard_space();
|
||||
}
|
||||
self.type_annotation.print(p, ctx);
|
||||
});
|
||||
}
|
||||
@@ -2407,9 +2440,9 @@ impl GenExpr for TSNonNullExpression<'_> {
|
||||
fn gen_expr(&self, p: &mut Codegen, _precedence: Precedence, ctx: Context) {
|
||||
self.expression.print_expr(p, Precedence::Postfix, ctx);
|
||||
p.print_ascii_byte(b'!');
|
||||
if p.options.minify {
|
||||
p.print_hard_space();
|
||||
}
|
||||
// Remember the `!` so a following `=` doesn't retokenize as `!=`/`!==`.
|
||||
p.prev_op = Some(Operator::Unary(UnaryOperator::LogicalNot));
|
||||
p.prev_op_end = p.code().len();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2482,14 +2515,18 @@ impl Gen for Class<'_> {
|
||||
type_parameters.print(p, ctx);
|
||||
}
|
||||
if let Some(super_class) = self.super_class.as_ref() {
|
||||
p.print_str(" extends ");
|
||||
p.print_soft_space();
|
||||
p.print_space_before_identifier();
|
||||
p.print_str("extends ");
|
||||
super_class.print_expr(p, Precedence::Postfix, Context::empty());
|
||||
if let Some(super_type_parameters) = &self.super_type_arguments {
|
||||
super_type_parameters.print(p, ctx);
|
||||
}
|
||||
}
|
||||
if !self.implements.is_empty() {
|
||||
p.print_str(" implements ");
|
||||
p.print_soft_space();
|
||||
p.print_space_before_identifier();
|
||||
p.print_str("implements ");
|
||||
p.print_list(&self.implements, ctx);
|
||||
}
|
||||
p.print_soft_space();
|
||||
@@ -2952,6 +2989,8 @@ impl Gen for AccessorProperty<'_> {
|
||||
if self.computed {
|
||||
p.print_soft_space();
|
||||
p.print_ascii_byte(b'[');
|
||||
} else if matches!(self.key, PropertyKey::PrivateIdentifier(_)) {
|
||||
p.print_soft_space();
|
||||
} else {
|
||||
p.print_hard_space();
|
||||
}
|
||||
@@ -3318,6 +3357,33 @@ impl Gen for TSIntersectionType<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `ty` prints starting with a non-identifier byte, so a preceding keyword
|
||||
/// (`as`, `extends`, `keyof`, ...) needs no separating space when minifying.
|
||||
/// Conservative: only types that unconditionally start with punctuation are listed,
|
||||
/// so the worst case is keeping an unnecessary space, never dropping a required one.
|
||||
fn type_starts_with_non_identifier(ty: &TSType) -> bool {
|
||||
match ty {
|
||||
TSType::TSTupleType(_)
|
||||
| TSType::TSTypeLiteral(_)
|
||||
| TSType::TSMappedType(_)
|
||||
| TSType::TSFunctionType(_)
|
||||
| TSType::TSParenthesizedType(_)
|
||||
| TSType::TSTemplateLiteralType(_) => true,
|
||||
TSType::TSArrayType(arr) => {
|
||||
parenthesize_check_type_of_postfix_type(&arr.element_type)
|
||||
|| type_starts_with_non_identifier(&arr.element_type)
|
||||
}
|
||||
TSType::TSLiteralType(lit) => {
|
||||
matches!(&lit.literal, TSLiteral::TemplateLiteral(_) | TSLiteral::StringLiteral(_))
|
||||
}
|
||||
TSType::TSUnionType(u) => u.types.first().is_some_and(type_starts_with_non_identifier),
|
||||
TSType::TSIntersectionType(i) => {
|
||||
i.types.first().is_some_and(type_starts_with_non_identifier)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
impl Gen for TSConditionalType<'_> {
|
||||
fn r#gen(&self, p: &mut Codegen, ctx: Context) {
|
||||
p.wrap(
|
||||
@@ -3333,21 +3399,31 @@ impl Gen for TSConditionalType<'_> {
|
||||
);
|
||||
p.print_soft_space();
|
||||
p.print_space_before_identifier();
|
||||
p.print_str("extends ");
|
||||
p.wrap(matches!(self.extends_type, TSType::TSConditionalType(_)), |p| {
|
||||
p.print_str("extends");
|
||||
let extends_wrapped = matches!(self.extends_type, TSType::TSConditionalType(_));
|
||||
if extends_wrapped || type_starts_with_non_identifier(&self.extends_type) {
|
||||
p.print_soft_space();
|
||||
} else {
|
||||
p.print_hard_space();
|
||||
}
|
||||
p.wrap(extends_wrapped, |p| {
|
||||
self.extends_type.print(p, ctx);
|
||||
});
|
||||
p.print_soft_space();
|
||||
// Avoid merging into `??` when the extends type ends with `?` (postfix
|
||||
// JSDoc nullable, e.g. `A extends C? ? D : E`).
|
||||
// Avoid `??` when the extends type ends with a postfix nullable `?`.
|
||||
if p.last_byte() == Some(b'?') {
|
||||
p.print_hard_space();
|
||||
}
|
||||
p.print_str("?");
|
||||
p.print_soft_space();
|
||||
p.print_ascii_byte(b'?');
|
||||
// Avoid `??` when the true branch starts with a prefix nullable `?`.
|
||||
if matches!(&self.true_type, TSType::JSDocNullableType(t) if !t.postfix) {
|
||||
p.print_hard_space();
|
||||
} else {
|
||||
p.print_soft_space();
|
||||
}
|
||||
self.true_type.print(p, ctx);
|
||||
p.print_soft_space();
|
||||
p.print_str(":");
|
||||
p.print_ascii_byte(b':');
|
||||
p.print_soft_space();
|
||||
self.false_type.print(p, ctx);
|
||||
}
|
||||
@@ -3376,11 +3452,14 @@ impl Gen for TSMappedType<'_> {
|
||||
p.print_ascii_byte(b'{');
|
||||
p.print_soft_space();
|
||||
match self.readonly {
|
||||
Some(TSMappedTypeModifierOperator::True) => p.print_str("readonly "),
|
||||
Some(TSMappedTypeModifierOperator::Plus) => p.print_str("+readonly "),
|
||||
Some(TSMappedTypeModifierOperator::Minus) => p.print_str("-readonly "),
|
||||
Some(TSMappedTypeModifierOperator::True) => p.print_str("readonly"),
|
||||
Some(TSMappedTypeModifierOperator::Plus) => p.print_str("+readonly"),
|
||||
Some(TSMappedTypeModifierOperator::Minus) => p.print_str("-readonly"),
|
||||
None => {}
|
||||
}
|
||||
if self.readonly.is_some() {
|
||||
p.print_soft_space();
|
||||
}
|
||||
p.print_ascii_byte(b'[');
|
||||
self.key.print(p, ctx);
|
||||
p.print_str(" in ");
|
||||
@@ -3417,20 +3496,22 @@ impl Gen for TSQualifiedName<'_> {
|
||||
impl Gen for TSTypeOperator<'_> {
|
||||
fn r#gen(&self, p: &mut Codegen, ctx: Context) {
|
||||
p.print_str(self.operator.to_str());
|
||||
p.print_hard_space();
|
||||
p.wrap(
|
||||
matches!(
|
||||
&self.type_annotation,
|
||||
TSType::TSUnionType(_)
|
||||
| TSType::TSIntersectionType(_)
|
||||
| TSType::TSFunctionType(_)
|
||||
| TSType::TSConstructorType(_)
|
||||
| TSType::TSConditionalType(_)
|
||||
),
|
||||
|p| {
|
||||
self.type_annotation.print(p, ctx);
|
||||
},
|
||||
let wrap = matches!(
|
||||
&self.type_annotation,
|
||||
TSType::TSUnionType(_)
|
||||
| TSType::TSIntersectionType(_)
|
||||
| TSType::TSFunctionType(_)
|
||||
| TSType::TSConstructorType(_)
|
||||
| TSType::TSConditionalType(_)
|
||||
);
|
||||
if wrap || type_starts_with_non_identifier(&self.type_annotation) {
|
||||
p.print_soft_space();
|
||||
} else {
|
||||
p.print_hard_space();
|
||||
}
|
||||
p.wrap(wrap, |p| {
|
||||
self.type_annotation.print(p, ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3567,7 +3648,12 @@ impl Gen for TSTypeParameter<'_> {
|
||||
}
|
||||
self.name.print(p, ctx);
|
||||
if let Some(constraint) = &self.constraint {
|
||||
p.print_str(" extends ");
|
||||
p.print_str(" extends");
|
||||
if type_starts_with_non_identifier(constraint) {
|
||||
p.print_soft_space();
|
||||
} else {
|
||||
p.print_hard_space();
|
||||
}
|
||||
constraint.print(p, ctx);
|
||||
}
|
||||
if let Some(default) = &self.default {
|
||||
@@ -3960,7 +4046,9 @@ impl Gen for TSInterfaceDeclaration<'_> {
|
||||
type_parameters.print(p, ctx);
|
||||
}
|
||||
if !self.extends.is_empty() {
|
||||
p.print_str(" extends ");
|
||||
p.print_soft_space();
|
||||
p.print_space_before_identifier();
|
||||
p.print_str("extends ");
|
||||
p.print_list(&self.extends, ctx);
|
||||
}
|
||||
p.print_soft_space();
|
||||
@@ -3996,7 +4084,7 @@ impl Gen for TSEnumDeclaration<'_> {
|
||||
}
|
||||
p.print_str("enum ");
|
||||
self.id.print(p, ctx);
|
||||
p.print_space_before_identifier();
|
||||
p.print_soft_space();
|
||||
self.body.print(p, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,6 +847,12 @@ impl<'a> Codegen<'a> {
|
||||
// `prev == UnaryOperator::LogicalNot` which means last byte is ASCII,
|
||||
// and therefore previous character is 1 byte from end of buffer
|
||||
&& self.code.peek_nth_byte_back(1) == Some(b'<'))
|
||||
// `a! == b`: keep the non-null `!` from gluing to `=` as `!=`/`!==`.
|
||||
|| (prev == un_op_not
|
||||
&& matches!(
|
||||
next,
|
||||
Operator::Binary(BinaryOperator::Equality | BinaryOperator::StrictEquality)
|
||||
))
|
||||
{
|
||||
self.print_hard_space();
|
||||
}
|
||||
@@ -866,7 +872,10 @@ impl<'a> Codegen<'a> {
|
||||
fn print_decorators(&mut self, decorators: &[Decorator<'_>], ctx: Context) {
|
||||
for decorator in decorators {
|
||||
decorator.print(self, ctx);
|
||||
self.print_hard_space();
|
||||
// Only separate from the following token when the decorator ends in an
|
||||
// identifier char (`@dec class`); `@dec() class` can be `@dec()class`.
|
||||
self.print_soft_space();
|
||||
self.print_space_before_identifier();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,12 @@ fn module_decl() {
|
||||
test_minify("export default 5", "export default 5;");
|
||||
test_minify("export default foo", "export default foo;");
|
||||
test_minify("export default function(){}", "export default function(){}");
|
||||
|
||||
// `as` next to a string export/import name needs no space.
|
||||
test_minify("export { foo as \"name\" }", "export{foo as\"name\"};");
|
||||
test_minify("import { \"y\" as z } from \"m\"", "import{\"y\"as z}from\"m\";");
|
||||
test_minify("export * as \"ns\" from \"m\"", "export*as\"ns\" from\"m\";");
|
||||
test_minify("export { foo as bar }", "export{foo as bar};");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -110,7 +110,7 @@ interface A{a:string;"b":number;"c"():void;}
|
||||
########## 25
|
||||
enum A { a, 'b' }
|
||||
----------
|
||||
enum A {a,"b"}
|
||||
enum A{a,"b"}
|
||||
########## 26
|
||||
module 'a'
|
||||
----------
|
||||
@@ -122,7 +122,7 @@ declare module "a";
|
||||
########## 28
|
||||
a = x!;
|
||||
----------
|
||||
a=x! ;
|
||||
a=x!;
|
||||
########## 29
|
||||
b = (x as y);
|
||||
----------
|
||||
@@ -142,7 +142,7 @@ d=x satisfies y;
|
||||
########## 33
|
||||
export @x declare abstract class C {}
|
||||
----------
|
||||
export @x declare abstract class C{}
|
||||
export@x declare abstract class C{}
|
||||
########## 34
|
||||
div<T>``
|
||||
----------
|
||||
@@ -170,15 +170,15 @@ export type Component<Props=any,RawBindings=any,D=any,C extends ComputedOptions=
|
||||
########## 37
|
||||
(a || b) as any
|
||||
----------
|
||||
(a||b) as any;
|
||||
(a||b)as any;
|
||||
########## 38
|
||||
(a ** b) as any
|
||||
----------
|
||||
(a**b) as any;
|
||||
(a**b)as any;
|
||||
########## 39
|
||||
(function g() {}) as any
|
||||
----------
|
||||
(function g(){}) as any;
|
||||
(function g(){})as any;
|
||||
########## 40
|
||||
|
||||
import defaultExport from "module-name";
|
||||
@@ -215,7 +215,7 @@ export { default, /* …, */ } from "module-name";
|
||||
export { default as name16 } from "module-name";
|
||||
|
||||
----------
|
||||
import defaultExport from"module-name";import*as name from"module-name";import{export1}from"module-name";import{export1 as alias1}from"module-name";import{default as alias}from"module-name";import{export1,export2}from"module-name";import{export1,export2 as alias2}from"module-name";import{"string name" as alias}from"module-name";import defaultExport,{export1}from"module-name";import defaultExport,*as name from"module-name";import"module-name";import{}from"mod";export let name1,name2;export const name3=1,name4=2;export function functionName(){}export class ClassName{}export function*generatorFunctionName(){}export const{name5,name2:bar}=o;export const[name6,name7]=array;export{name8,name81};export{variable1 as name9,variable2 as name10,name82};export{variable1 as "string name"};export{name1 as default1};export*from"module-name";export*as name11 from"module-name";export{name12,nameN}from"module-name";export{import1 as name13,import2 as name14,name15}from"module-name";export{default}from"module-name";export{default as name16}from"module-name";
|
||||
import defaultExport from"module-name";import*as name from"module-name";import{export1}from"module-name";import{export1 as alias1}from"module-name";import{default as alias}from"module-name";import{export1,export2}from"module-name";import{export1,export2 as alias2}from"module-name";import{"string name"as alias}from"module-name";import defaultExport,{export1}from"module-name";import defaultExport,*as name from"module-name";import"module-name";import{}from"mod";export let name1,name2;export const name3=1,name4=2;export function functionName(){}export class ClassName{}export function*generatorFunctionName(){}export const{name5,name2:bar}=o;export const[name6,name7]=array;export{name8,name81};export{variable1 as name9,variable2 as name10,name82};export{variable1 as"string name"};export{name1 as default1};export*from"module-name";export*as name11 from"module-name";export{name12,nameN}from"module-name";export{import1 as name13,import2 as name14,name15}from"module-name";export{default}from"module-name";export{default as name16}from"module-name";
|
||||
########## 41
|
||||
|
||||
import a = require("a");
|
||||
|
||||
@@ -214,7 +214,7 @@ fn minify_ts_type_space() {
|
||||
};
|
||||
// Conditional type: `?`/`:` are tight like a JS conditional expression.
|
||||
min("type T = A extends B ? C : D;", "type T=A extends B?C:D;");
|
||||
min("type T = A extends {} ? B : C;", "type T=A extends {}?B:C;");
|
||||
min("type T = A extends {} ? B : C;", "type T=A extends{}?B:C;");
|
||||
// Constructor type: no space after `new` before `(`/`<`.
|
||||
min("type N = new () => Foo;", "type N=new()=>Foo;");
|
||||
min("type N = abstract new (x: number) => Foo;", "type N=abstract new(x:number)=>Foo;");
|
||||
@@ -223,6 +223,129 @@ fn minify_ts_type_space() {
|
||||
min("type T = A extends C? ? D : E;", "type T=A extends C? ?D:E;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minify_decorator_space() {
|
||||
let min = |src: &str, expected: &str| {
|
||||
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
||||
};
|
||||
// `@dec class` keeps a space (decorator ends in an identifier char), but
|
||||
// `@dec() class` does not (`)` is not an identifier char).
|
||||
min("@dec class C {}", "@dec class C{}");
|
||||
min("@dec() class C {}", "@dec()class C{}");
|
||||
min("@a.b @c class C {}", "@a.b @c class C{}");
|
||||
// `export` before a decorated class needs no space (`@` is not an identifier char).
|
||||
min("export @dec class C {}", "export@dec class C{}");
|
||||
min("export @dec() class C {}", "export@dec()class C{}");
|
||||
// `accessor` before a private name needs no space.
|
||||
min("class C { accessor #x = 1 }", "class C{accessor#x=1}");
|
||||
min("class C { accessor x = 1 }", "class C{accessor x=1}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minify_ts_operator_space() {
|
||||
let min = |src: &str, expected: &str| {
|
||||
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
||||
};
|
||||
// `as`/`satisfies` need no leading space after a non-identifier char.
|
||||
min("x = foo() as Bar;", "x=foo()as Bar;");
|
||||
min("x = arr[0] as Bar;", "x=arr[0]as Bar;");
|
||||
min("x = `s` as const;", "x=`s`as const;");
|
||||
min("x = foo() satisfies Bar;", "x=foo()satisfies Bar;");
|
||||
// ...but a space is required after an identifier.
|
||||
min("x = y as Bar;", "x=y as Bar;");
|
||||
min("x = y satisfies Bar;", "x=y satisfies Bar;");
|
||||
// No trailing space when the target type starts with punctuation.
|
||||
min("x = y as { a: 1 };", "x=y as{a:1;};");
|
||||
min("x = y as (a: number) => void;", "x=y as(a:number)=>void;");
|
||||
min("x = y satisfies [number];", "x=y satisfies[number];");
|
||||
// A non-null `!` needs no trailing space before a member, statement end, or call.
|
||||
min("x = a!.b;", "x=a!.b;");
|
||||
min("a!;", "a!;");
|
||||
min("x = a!();", "x=a!();");
|
||||
// ...but must keep one before `=`/`==`/`===` so it does not become `!=`/`!==`.
|
||||
min("a! = b;", "a! =b;");
|
||||
min("x = a! == b;", "x=a! ==b;");
|
||||
min("x = a! === b;", "x=a! ===b;");
|
||||
// `!=`/`!==` do not retokenize, so no space is needed.
|
||||
min("x = a! != b;", "x=a!!=b;");
|
||||
// `enum` body brace needs no leading space.
|
||||
min("enum E { A, B }", "enum E{A,B}");
|
||||
min("const enum F { X = 1 }", "const enum F{X=1}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minify_conditional_type_space() {
|
||||
let min = |src: &str, expected: &str| {
|
||||
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
||||
};
|
||||
// `extends`/`?`/`:` need no surrounding space next to non-identifier tokens.
|
||||
min("type A<T> = T extends string ? 1 : 2;", "type A<T>=T extends string?1:2;");
|
||||
min("type B<T> = Foo<T> extends string ? X : Y;", "type B<T>=Foo<T>extends string?X:Y;");
|
||||
min("type C<T> = T[K] extends string ? X : Y;", "type C<T>=T[K]extends string?X:Y;");
|
||||
// A prefix JSDoc nullable true branch must keep the `? ` separator (no `??`).
|
||||
min("type D<T> = T extends B ? ?C : D;", "type D<T>=T extends B? ?C:D;");
|
||||
// `extends`/`keyof` need no trailing space before a punctuation-starting type.
|
||||
min("type E<T> = T extends [infer U] ? U : never;", "type E<T>=T extends[infer U]?U:never;");
|
||||
min("type F<T> = T extends { a: 1 } ? 1 : 2;", "type F<T>=T extends{a:1;}?1:2;");
|
||||
min("type G = keyof [a, b];", "type G=keyof[a,b];");
|
||||
// ...but keep it before an identifier-starting type.
|
||||
min("type H<T> = T extends Base ? 1 : 2;", "type H<T>=T extends Base?1:2;");
|
||||
// `readonly`/`extends` before a parenthesized array element need no trailing space.
|
||||
min("type I<T> = readonly (keyof T)[];", "type I<T>=readonly(keyof T)[];");
|
||||
min(
|
||||
"type J<T> = T extends (infer A)[] ? A : never;",
|
||||
"type J<T>=T extends(infer A)[]?A:never;",
|
||||
);
|
||||
min("type K = readonly string[];", "type K=readonly string[];");
|
||||
// `new` constructor type needs no space before `(`/`<`.
|
||||
min("type L = new () => Foo;", "type L=new()=>Foo;");
|
||||
min("type M = abstract new (x: number) => Bar;", "type M=abstract new(x:number)=>Bar;");
|
||||
// A type-parameter constraint needs no trailing space before a punctuation-starting type.
|
||||
min("type N<T extends (...a: any) => any> = T;", "type N<T extends(...a:any)=>any>=T;");
|
||||
min("type O<T extends [a, b]> = T;", "type O<T extends[a,b]>=T;");
|
||||
min("type P<T extends Base> = T;", "type P<T extends Base>=T;");
|
||||
// `as`/`satisfies` before a union/intersection whose first member starts with punctuation.
|
||||
min("x = res as { a: 1 } | undefined;", "x=res as{a:1;}|undefined;");
|
||||
min("x = res as Foo | Bar;", "x=res as Foo|Bar;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minify_export_type_star() {
|
||||
let min = |src: &str, expected: &str| {
|
||||
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
||||
};
|
||||
min("export type * from \"m\";", "export type*from\"m\";");
|
||||
min("export * from \"m\";", "export*from\"m\";");
|
||||
min("export type * as ns from \"m\";", "export type*as ns from\"m\";");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minify_heritage_and_mapped_space() {
|
||||
let min = |src: &str, expected: &str| {
|
||||
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
||||
};
|
||||
// Heritage `extends`/`implements` need no leading space after type-argument `>`.
|
||||
min(
|
||||
"class A<T> extends Base<T> implements I, J {}",
|
||||
"class A<T>extends Base<T>implements I,J{}",
|
||||
);
|
||||
min("class B extends Base implements I {}", "class B extends Base implements I{}");
|
||||
min(
|
||||
"interface I<T = string> extends Omit<A, B>, C {}",
|
||||
"interface I<T=string>extends Omit<A,B>,C{}",
|
||||
);
|
||||
min("interface J extends K {}", "interface J extends K{}");
|
||||
// Mapped-type `readonly` needs no trailing space before `[`.
|
||||
min(
|
||||
"type M<T> = { readonly [P in keyof T]: T[P] };",
|
||||
"type M<T>={readonly[P in keyof T]:T[P]};",
|
||||
);
|
||||
min(
|
||||
"type N<T> = { -readonly [P in keyof T]-?: T[P] };",
|
||||
"type N<T>={-readonly[P in keyof T]-?:T[P]};",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ts_as_expression_in_binary_expr() {
|
||||
test_idempotency("key in (that as object)");
|
||||
|
||||
Reference in new issue
Block a user