diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index f08eabcf0..c6319bed2 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -1981,6 +1981,30 @@ fn strip_typescript(allocator: &Allocator, path: &str, code: &str) -> String { // In oxc 0.129.0, parameter properties generate field declarations which we need to remove. let param_property_names = collect_parameter_property_names(&program); + // An import whose specifiers all carry inline `type` modifiers has no runtime + // bindings. Promote it to `import type` so `only_remove_type_imports` elides + // the whole statement instead of leaving a bare side-effect import behind. + // This intentionally diverges from tsc's verbatimModuleSyntax (which keeps + // `import {}` for side effects) to match Angular CLI's esbuild pipeline; + // side effects need an explicit `import 'mod'`, which is left untouched. + for stmt in &mut program.body { + if let Statement::ImportDeclaration(import_decl) = stmt + && import_decl.import_kind == ImportOrExportKind::Value + && import_decl.specifiers.as_ref().is_some_and(|specs| { + !specs.is_empty() + && specs.iter().all(|spec| { + matches!( + spec, + ImportDeclarationSpecifier::ImportSpecifier(s) + if s.import_kind.is_type() + ) + }) + }) + { + import_decl.import_kind = ImportOrExportKind::Type; + } + } + let semantic_ret = oxc_semantic::SemanticBuilder::new().with_excess_capacity(2.0).build(&program); diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs index 8858f6b68..d20838aa3 100644 --- a/crates/oxc_angular_compiler/tests/integration_test.rs +++ b/crates/oxc_angular_compiler/tests/integration_test.rs @@ -7259,6 +7259,74 @@ export class AppComponent { insta::assert_snapshot!("jit_constructor_deps", result.code); } +#[test] +fn test_jit_all_inline_type_import_elided() { + // An import whose specifiers all carry inline `type` modifiers must be + // elided entirely, not left behind as a bare side-effect import. + let allocator = Allocator::default(); + let source = r#" +import './side-effect'; +import { Pipe, type PipeTransform } from '@angular/core'; +import { type Get } from 'type-fest'; +import type { Whole } from './whole'; +import Def, { type Partial } from './default-mixed'; + +@Pipe({ name: 'demo' }) +export class DemoPipe implements PipeTransform { + transform(v: string) { return Def(v as unknown as Whole as Partial as string); } +} + +export let sample: Get<{ a: 1 }, 'a'> | undefined; +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = transform_angular_file(&allocator, "demo.pipe.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + assert!( + !result.code.contains("type-fest"), + "All-inline-type import should be elided, not kept as a side-effect import. Got:\n{}", + result.code + ); + assert!( + !result.code.contains("./whole"), + "`import type {{ ... }}` statement should be elided. Got:\n{}", + result.code + ); + + // Mixed import keeps its value binding, drops the type-only one + assert!( + result.code.contains("import { Pipe } from"), + "Value binding of mixed import should be preserved. Got:\n{}", + result.code + ); + assert!( + !result.code.contains("PipeTransform"), + "Type-only binding of mixed import should be dropped. Got:\n{}", + result.code + ); + + // Default import alongside an inline-type specifier keeps the default + // binding and drops the type-only sibling + assert!( + result.code.contains("import Def from"), + "Default import with inline-type sibling should be preserved. Got:\n{}", + result.code + ); + assert!( + !result.code.contains("Partial"), + "Inline-type sibling of a default import should be dropped. Got:\n{}", + result.code + ); + + // Deliberate side-effect imports (no specifier list) must survive + assert!( + result.code.contains("import \"./side-effect\""), + "Bare side-effect import should be preserved. Got:\n{}", + result.code + ); +} + #[test] fn test_jit_component_class_restructuring() { // JIT should restructure: export class X {} → let X = class X {}; X = __decorate([...], X); export { X };