diff --git a/.changeset/popular-spiders-bake.md b/.changeset/popular-spiders-bake.md new file mode 100644 index 000000000000..4414e72580e0 --- /dev/null +++ b/.changeset/popular-spiders-bake.md @@ -0,0 +1,6 @@ +--- +swc_core: patch +swc_fast_ts_strip: patch +--- + +feat(ts/fast-strip): Support type-only/uninstantiated namespaces diff --git a/bindings/binding_typescript_wasm/__tests__/__snapshots__/transform.js.snap b/bindings/binding_typescript_wasm/__tests__/__snapshots__/transform.js.snap index e6903c8993c9..05cb41d2fb60 100644 --- a/bindings/binding_typescript_wasm/__tests__/__snapshots__/transform.js.snap +++ b/bindings/binding_typescript_wasm/__tests__/__snapshots__/transform.js.snap @@ -122,8 +122,8 @@ exports[`transform in strip-only mode should throw an error when it encounters a "code": "UnsupportedSyntax", "message": " x TypeScript namespace declaration is not supported in strip-only mode ,---- - 1 | module foo {} - : ^^^^^^^^^^^^^ + 1 | module foo { export const m = 1; } + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ \`---- ", } @@ -134,8 +134,8 @@ exports[`transform in strip-only mode should throw an error when it encounters a "code": "UnsupportedSyntax", "message": " x TypeScript namespace declaration is not supported in strip-only mode ,---- - 1 | namespace Foo {} - : ^^^^^^^^^^^^^^^^ + 1 | namespace Foo { export const m = 1; } + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ \`---- ", } diff --git a/bindings/binding_typescript_wasm/__tests__/transform.js b/bindings/binding_typescript_wasm/__tests__/transform.js index dbaed85e381c..8f230d232a36 100644 --- a/bindings/binding_typescript_wasm/__tests__/transform.js +++ b/bindings/binding_typescript_wasm/__tests__/transform.js @@ -13,7 +13,7 @@ describe("transform", () => { export const foo: number = 1; type Foo = number; `, - {} + {}, ); expect(code).toMatchSnapshot(); }); @@ -21,15 +21,15 @@ describe("transform", () => { describe("in strip-only mode", () => { it("should remove declare enum", async () => { await expect( - swc.transform(`declare enum Foo {}`, {}) + swc.transform(`declare enum Foo {}`, {}), ).resolves.toMatchSnapshot(); await expect( swc.transform( `declare enum Foo { A }`, - {} - ) + {}, + ), ).resolves.toMatchSnapshot(); expect( swc.transform( @@ -37,8 +37,8 @@ describe("transform", () => { a = 2, b, }`, - {} - ) + {}, + ), ).resolves.toMatchSnapshot(); }); @@ -49,8 +49,8 @@ describe("transform", () => { type Foo = number; type Bar = string; const bar: Bar = "bar";`, - {} - ) + {}, + ), ).resolves.toMatchSnapshot(); }); @@ -59,8 +59,8 @@ describe("transform", () => { swc.transform( `const foo = 1; const bar: Bar = "bar";`, - {} - ) + {}, + ), ).resolves.toMatchSnapshot(); }); @@ -69,8 +69,8 @@ describe("transform", () => { swc.transform( `const foo = 1 as number; const bar = "bar";`, - {} - ) + {}, + ), ).resolves.toMatchSnapshot(); }); @@ -79,8 +79,8 @@ describe("transform", () => { swc.transform( `const foo = 1!; const bar = "bar";`, - {} - ) + {}, + ), ).resolves.toMatchSnapshot(); }); @@ -89,8 +89,8 @@ describe("transform", () => { swc.transform( `const foo = 1 satisfies number; const bar = "bar";`, - {} - ) + {}, + ), ).resolves.toMatchSnapshot(); }); @@ -102,8 +102,8 @@ describe("transform", () => { bar: "bar" as any as number, } satisfies number; const bar = "bar";`, - {} - ) + {}, + ), ).resolves.toMatchSnapshot(); }); @@ -111,23 +111,23 @@ describe("transform", () => { await expect( swc.transform("enum Foo {}", { mode: "strip-only", - }) + }), ).rejects.toMatchSnapshot(); }); it("should throw an error when it encounters a namespace", async () => { await expect( - swc.transform("namespace Foo {}", { + swc.transform("namespace Foo { export const m = 1; }", { mode: "strip-only", - }) + }), ).rejects.toMatchSnapshot(); }); it("should throw an error when it encounters a module", async () => { await expect( - swc.transform("module foo {}", { + swc.transform("module foo { export const m = 1; }", { mode: "strip-only", - }) + }), ).rejects.toMatchSnapshot(); }); @@ -135,26 +135,28 @@ describe("transform", () => { await expect( swc.transform("function foo() { await Promise.resolve(1); }", { mode: "strip-only", - }) + }), ).rejects.toMatchSnapshot(); }); - it("should report correct error for syntax error", async () => { await expect( swc.transform("function foo() { invalid syntax }", { mode: "strip-only", - }) + }), ).rejects.toMatchSnapshot(); }); it("should report correct error for unsupported syntax", async () => { await expect( - swc.transform(`enum Foo { + swc.transform( + `enum Foo { a, b - }`, { - mode: "strip-only", - }) + }`, + { + mode: "strip-only", + }, + ), ).rejects.toMatchSnapshot(); }); }); diff --git a/crates/swc_fast_ts_strip/src/lib.rs b/crates/swc_fast_ts_strip/src/lib.rs index 127d3ecbb3e6..8723111b165a 100644 --- a/crates/swc_fast_ts_strip/src/lib.rs +++ b/crates/swc_fast_ts_strip/src/lib.rs @@ -14,12 +14,12 @@ use swc_ecma_ast::{ ArrayPat, ArrowExpr, AutoAccessor, BindingIdent, Class, ClassDecl, ClassMethod, ClassProp, Constructor, Decl, DefaultDecl, DoWhileStmt, EsVersion, ExportAll, ExportDecl, ExportDefaultDecl, ExportSpecifier, FnDecl, ForInStmt, ForOfStmt, ForStmt, GetterProp, IfStmt, - ImportDecl, ImportSpecifier, NamedExport, ObjectPat, Param, Pat, PrivateMethod, PrivateProp, - Program, ReturnStmt, SetterProp, Stmt, ThrowStmt, TsAsExpr, TsConstAssertion, TsEnumDecl, - TsExportAssignment, TsImportEqualsDecl, TsIndexSignature, TsInstantiation, TsModuleDecl, - TsModuleName, TsNamespaceDecl, TsNonNullExpr, TsParamPropParam, TsSatisfiesExpr, - TsTypeAliasDecl, TsTypeAnn, TsTypeAssertion, TsTypeParamDecl, TsTypeParamInstantiation, - VarDeclarator, WhileStmt, YieldExpr, + ImportDecl, ImportSpecifier, ModuleDecl, ModuleItem, NamedExport, ObjectPat, Param, Pat, + PrivateMethod, PrivateProp, Program, ReturnStmt, SetterProp, Stmt, ThrowStmt, TsAsExpr, + TsConstAssertion, TsEnumDecl, TsExportAssignment, TsImportEqualsDecl, TsIndexSignature, + TsInstantiation, TsModuleDecl, TsModuleName, TsNamespaceBody, TsNamespaceDecl, TsNonNullExpr, + TsParamPropParam, TsSatisfiesExpr, TsTypeAliasDecl, TsTypeAnn, TsTypeAssertion, + TsTypeParamDecl, TsTypeParamInstantiation, VarDeclarator, WhileStmt, YieldExpr, }; use swc_ecma_parser::{ lexer::Lexer, @@ -1034,7 +1034,7 @@ impl Visit for TsStrip { } fn visit_export_decl(&mut self, n: &ExportDecl) { - if n.decl.is_ts_declare() { + if n.decl.is_ts_declare() || n.decl.is_uninstantiated() { self.add_replacement(n.span); self.fix_asi(n.span); return; @@ -1044,7 +1044,7 @@ impl Visit for TsStrip { } fn visit_export_default_decl(&mut self, n: &ExportDefaultDecl) { - if n.decl.is_ts_declare() { + if n.decl.is_ts_declare() || n.decl.is_uninstantiated() { self.add_replacement(n.span); self.fix_asi(n.span); return; @@ -1054,7 +1054,7 @@ impl Visit for TsStrip { } fn visit_decl(&mut self, n: &Decl) { - if n.is_ts_declare() { + if n.is_ts_declare() || n.is_uninstantiated() { self.add_replacement(n.span()); self.fix_asi(n.span()); return; @@ -1393,7 +1393,7 @@ trait IsTsDecl { impl IsTsDecl for Decl { fn is_ts_declare(&self) -> bool { match self { - Self::TsInterface { .. } | Self::TsTypeAlias(..) => true, + Self::TsInterface(..) | Self::TsTypeAlias(..) => true, Self::TsModule(module) => module.declare || matches!(module.id, TsModuleName::Str(..)), Self::TsEnum(ref r#enum) => r#enum.declare, @@ -1419,12 +1419,67 @@ impl IsTsDecl for DefaultDecl { fn is_ts_declare(&self) -> bool { match self { Self::Class(..) => false, - DefaultDecl::Fn(r#fn) => r#fn.function.body.is_none(), - DefaultDecl::TsInterfaceDecl(..) => true, + Self::Fn(r#fn) => r#fn.function.body.is_none(), + Self::TsInterfaceDecl(..) => true, } } } +trait IsUninstantiated { + fn is_uninstantiated(&self) -> bool; +} + +impl IsUninstantiated for TsNamespaceBody { + fn is_uninstantiated(&self) -> bool { + match self { + Self::TsModuleBlock(block) => { + block.body.iter().all(IsUninstantiated::is_uninstantiated) + } + Self::TsNamespaceDecl(decl) => decl.body.is_uninstantiated(), + } + } +} + +impl IsUninstantiated for ModuleItem { + fn is_uninstantiated(&self) -> bool { + match self { + Self::Stmt(stmt) => stmt.is_uninstantiated(), + Self::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. })) => { + decl.is_uninstantiated() + } + _ => false, + } + } +} + +impl IsUninstantiated for Stmt { + fn is_uninstantiated(&self) -> bool { + matches!(self, Self::Decl(decl) if decl.is_uninstantiated()) + } +} + +impl IsUninstantiated for TsModuleDecl { + fn is_uninstantiated(&self) -> bool { + matches!(&self.body, Some(body) if body.is_uninstantiated()) + } +} + +impl IsUninstantiated for Decl { + fn is_uninstantiated(&self) -> bool { + match self { + Self::TsInterface(..) | Self::TsTypeAlias(..) => true, + Self::TsModule(module) => module.is_uninstantiated(), + _ => false, + } + } +} + +impl IsUninstantiated for DefaultDecl { + fn is_uninstantiated(&self) -> bool { + matches!(self, Self::TsInterfaceDecl(..)) + } +} + trait U8Helper { fn is_utf8_char_boundary(&self) -> bool; } diff --git a/crates/swc_fast_ts_strip/tests/errors/issue-9977.swc-stderr b/crates/swc_fast_ts_strip/tests/errors/issue-9977.swc-stderr new file mode 100644 index 000000000000..feecbf6a0cb0 --- /dev/null +++ b/crates/swc_fast_ts_strip/tests/errors/issue-9977.swc-stderr @@ -0,0 +1,52 @@ + x TypeScript parameter property is not supported in strip-only mode + ,-[6:1] + 5 | // No parameter properties + 6 | constructor(public foo: string) { } + : ^^^^^^^^^^^ + 7 | } + `---- + x TypeScript namespace declaration is not supported in strip-only mode + ,-[9:1] + 8 | + 9 | ,-> namespace IllegalBecauseInstantiated { + 10 | | export const m = 1; + 11 | `-> } + `---- + x TypeScript namespace declaration is not supported in strip-only mode + ,-[13:1] + 12 | + 13 | ,-> namespace AlsoIllegalBecauseInstantiated { + 14 | | class PrivateClass { + 15 | | + 16 | | } + 17 | `-> } + `---- + x TypeScript namespace declaration is not supported in strip-only mode + ,-[19:1] + 18 | + 19 | ,-> namespace IllegalBecauseNestedInstantiated { + 20 | | namespace Nested { + 21 | | export const m = 1; + 22 | | } + 23 | `-> } + `---- + x TypeScript enum is not supported in strip-only mode + ,-[25:1] + 24 | + 25 | ,-> enum NotLegalEnum { + 26 | | B = 1 + 27 | `-> } + `---- + x TypeScript import equals declaration is not supported in strip-only mode + ,-[29:1] + 28 | + 29 | import NoGoodAlias = NotLegalEnum.B; + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + x TypeScript enum is not supported in strip-only mode + ,-[31:1] + 30 | + 31 | ,-> const enum NotLegalConstEnum { + 32 | | C = 2 + 33 | `-> } + `---- diff --git a/crates/swc_fast_ts_strip/tests/errors/issue-9977.ts b/crates/swc_fast_ts_strip/tests/errors/issue-9977.ts new file mode 100644 index 000000000000..bc9e6af36272 --- /dev/null +++ b/crates/swc_fast_ts_strip/tests/errors/issue-9977.ts @@ -0,0 +1,33 @@ +// @erasableSyntaxOnly: true +// @noEmit: true + +class MyClassErr { + // No parameter properties + constructor(public foo: string) { } +} + +namespace IllegalBecauseInstantiated { + export const m = 1; +} + +namespace AlsoIllegalBecauseInstantiated { + class PrivateClass { + + } +} + +namespace IllegalBecauseNestedInstantiated { + namespace Nested { + export const m = 1; + } +} + +enum NotLegalEnum { + B = 1 +} + +import NoGoodAlias = NotLegalEnum.B; + +const enum NotLegalConstEnum { + C = 2 +} diff --git a/crates/swc_fast_ts_strip/tests/errors/modules.swc-stderr b/crates/swc_fast_ts_strip/tests/errors/modules.swc-stderr index 6400d96291e3..b512cdccad83 100644 --- a/crates/swc_fast_ts_strip/tests/errors/modules.swc-stderr +++ b/crates/swc_fast_ts_strip/tests/errors/modules.swc-stderr @@ -1,11 +1,11 @@ x TypeScript namespace declaration is not supported in strip-only mode ,---- - 1 | module aModuleKeywordNamespace { } - : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 1 | module aModuleKeywordNamespace { export const m = 1; } + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `---- x TypeScript namespace declaration is not supported in strip-only mode ,-[3:1] 2 | - 3 | export module aModuleKeywordExportedNamespace { } - : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 3 | export module aModuleKeywordExportedNamespace { export const m = 1; } + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `---- diff --git a/crates/swc_fast_ts_strip/tests/errors/modules.ts b/crates/swc_fast_ts_strip/tests/errors/modules.ts index 9274355d33c0..96fac1969b3e 100644 --- a/crates/swc_fast_ts_strip/tests/errors/modules.ts +++ b/crates/swc_fast_ts_strip/tests/errors/modules.ts @@ -1,3 +1,3 @@ -module aModuleKeywordNamespace { } +module aModuleKeywordNamespace { export const m = 1; } -export module aModuleKeywordExportedNamespace { } \ No newline at end of file +export module aModuleKeywordExportedNamespace { export const m = 1; } \ No newline at end of file diff --git a/crates/swc_fast_ts_strip/tests/errors/namespaces.swc-stderr b/crates/swc_fast_ts_strip/tests/errors/namespaces.swc-stderr index 1de76bce5e24..48eb090328d7 100644 --- a/crates/swc_fast_ts_strip/tests/errors/namespaces.swc-stderr +++ b/crates/swc_fast_ts_strip/tests/errors/namespaces.swc-stderr @@ -1,11 +1,11 @@ x TypeScript namespace declaration is not supported in strip-only mode ,---- - 1 | namespace Foo { } - : ^^^^^^^^^^^^^^^^^ + 1 | namespace Foo { export const m = 1; } + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `---- x TypeScript namespace declaration is not supported in strip-only mode ,-[3:1] 2 | - 3 | export namespace Bar { } - : ^^^^^^^^^^^^^^^^^ + 3 | export namespace Bar { export const m = 1; } + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `---- diff --git a/crates/swc_fast_ts_strip/tests/errors/namespaces.ts b/crates/swc_fast_ts_strip/tests/errors/namespaces.ts index c19c32771565..fcdbf450c153 100644 --- a/crates/swc_fast_ts_strip/tests/errors/namespaces.ts +++ b/crates/swc_fast_ts_strip/tests/errors/namespaces.ts @@ -1,3 +1,3 @@ -namespace Foo { } +namespace Foo { export const m = 1; } -export namespace Bar { } +export namespace Bar { export const m = 1; } diff --git a/crates/swc_fast_ts_strip/tests/fixture/issue-9977.js b/crates/swc_fast_ts_strip/tests/fixture/issue-9977.js new file mode 100644 index 000000000000..04d774f8c748 --- /dev/null +++ b/crates/swc_fast_ts_strip/tests/fixture/issue-9977.js @@ -0,0 +1,36 @@ +// @erasableSyntaxOnly: true +// @noEmit: true + +// No errors after this point +class MyClassOk { + // Not a parameter property, ok + constructor(foo ) { } +} + +// Note for implementors: This should not be an error +// as this entire namespace block is fully erased + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/swc_fast_ts_strip/tests/fixture/issue-9977.swc-stderr b/crates/swc_fast_ts_strip/tests/fixture/issue-9977.swc-stderr new file mode 100644 index 000000000000..feecbf6a0cb0 --- /dev/null +++ b/crates/swc_fast_ts_strip/tests/fixture/issue-9977.swc-stderr @@ -0,0 +1,52 @@ + x TypeScript parameter property is not supported in strip-only mode + ,-[6:1] + 5 | // No parameter properties + 6 | constructor(public foo: string) { } + : ^^^^^^^^^^^ + 7 | } + `---- + x TypeScript namespace declaration is not supported in strip-only mode + ,-[9:1] + 8 | + 9 | ,-> namespace IllegalBecauseInstantiated { + 10 | | export const m = 1; + 11 | `-> } + `---- + x TypeScript namespace declaration is not supported in strip-only mode + ,-[13:1] + 12 | + 13 | ,-> namespace AlsoIllegalBecauseInstantiated { + 14 | | class PrivateClass { + 15 | | + 16 | | } + 17 | `-> } + `---- + x TypeScript namespace declaration is not supported in strip-only mode + ,-[19:1] + 18 | + 19 | ,-> namespace IllegalBecauseNestedInstantiated { + 20 | | namespace Nested { + 21 | | export const m = 1; + 22 | | } + 23 | `-> } + `---- + x TypeScript enum is not supported in strip-only mode + ,-[25:1] + 24 | + 25 | ,-> enum NotLegalEnum { + 26 | | B = 1 + 27 | `-> } + `---- + x TypeScript import equals declaration is not supported in strip-only mode + ,-[29:1] + 28 | + 29 | import NoGoodAlias = NotLegalEnum.B; + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + x TypeScript enum is not supported in strip-only mode + ,-[31:1] + 30 | + 31 | ,-> const enum NotLegalConstEnum { + 32 | | C = 2 + 33 | `-> } + `---- diff --git a/crates/swc_fast_ts_strip/tests/fixture/issue-9977.transform.js b/crates/swc_fast_ts_strip/tests/fixture/issue-9977.transform.js new file mode 100644 index 000000000000..fafd2b332734 --- /dev/null +++ b/crates/swc_fast_ts_strip/tests/fixture/issue-9977.transform.js @@ -0,0 +1,3 @@ +class MyClassOk { + constructor(foo){} +} diff --git a/crates/swc_fast_ts_strip/tests/fixture/issue-9977.ts b/crates/swc_fast_ts_strip/tests/fixture/issue-9977.ts new file mode 100644 index 000000000000..40eb639dd48a --- /dev/null +++ b/crates/swc_fast_ts_strip/tests/fixture/issue-9977.ts @@ -0,0 +1,36 @@ +// @erasableSyntaxOnly: true +// @noEmit: true + +// No errors after this point +class MyClassOk { + // Not a parameter property, ok + constructor(foo: string) { } +} + +// Note for implementors: This should not be an error +// as this entire namespace block is fully erased +namespace NotInstantiated { + export interface JustAType { } + export type ATypeInANamespace = {}; + namespace Nested { + export type ATypeInANamespace = {}; + } +} +declare namespace AmbientIsNotInstantiated { + export const stillOk = 12; +} + +declare enum LegalEnum { + A = 1 +} + +declare namespace AmbientStuff { + namespace Nested { + export const stillOk = 12; + } + enum EnumInAmbientContext { + B = 1 + } + + import FineAlias = EnumInAmbientContext.B; +} \ No newline at end of file