Skip to content

Commit 306ce0d

Browse files
fix(jit): elide imports whose specifiers are all inline-type (#482)
In JIT mode, strip_typescript runs oxc's TS transform with only_remove_type_imports, which drops inline-type specifiers but keeps the emptied statement as a bare side-effect import. An import like `import { type Get } from 'type-fest'` therefore survived as `import "type-fest"`, failing Rolldown builds for types-only packages and defeating tree-shaking for ordinary ones. Promote any value import whose named specifiers all carry inline `type` modifiers to `import type` before the transformer runs, so the whole statement is elided. Mixed imports, default/namespace imports, and explicit bare side-effect imports are untouched. This matches Angular CLI's esbuild pipeline rather than tsc verbatimModuleSyntax, which would keep `import {}` for side effects.
1 parent 5d1144c commit 306ce0d

2 files changed

Lines changed: 92 additions & 0 deletions

File tree

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1981,6 +1981,30 @@ fn strip_typescript(allocator: &Allocator, path: &str, code: &str) -> String {
19811981
// In oxc 0.129.0, parameter properties generate field declarations which we need to remove.
19821982
let param_property_names = collect_parameter_property_names(&program);
19831983

1984+
// An import whose specifiers all carry inline `type` modifiers has no runtime
1985+
// bindings. Promote it to `import type` so `only_remove_type_imports` elides
1986+
// the whole statement instead of leaving a bare side-effect import behind.
1987+
// This intentionally diverges from tsc's verbatimModuleSyntax (which keeps
1988+
// `import {}` for side effects) to match Angular CLI's esbuild pipeline;
1989+
// side effects need an explicit `import 'mod'`, which is left untouched.
1990+
for stmt in &mut program.body {
1991+
if let Statement::ImportDeclaration(import_decl) = stmt
1992+
&& import_decl.import_kind == ImportOrExportKind::Value
1993+
&& import_decl.specifiers.as_ref().is_some_and(|specs| {
1994+
!specs.is_empty()
1995+
&& specs.iter().all(|spec| {
1996+
matches!(
1997+
spec,
1998+
ImportDeclarationSpecifier::ImportSpecifier(s)
1999+
if s.import_kind.is_type()
2000+
)
2001+
})
2002+
})
2003+
{
2004+
import_decl.import_kind = ImportOrExportKind::Type;
2005+
}
2006+
}
2007+
19842008
let semantic_ret =
19852009
oxc_semantic::SemanticBuilder::new().with_excess_capacity(2.0).build(&program);
19862010

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7259,6 +7259,74 @@ export class AppComponent {
72597259
insta::assert_snapshot!("jit_constructor_deps", result.code);
72607260
}
72617261

7262+
#[test]
7263+
fn test_jit_all_inline_type_import_elided() {
7264+
// An import whose specifiers all carry inline `type` modifiers must be
7265+
// elided entirely, not left behind as a bare side-effect import.
7266+
let allocator = Allocator::default();
7267+
let source = r#"
7268+
import './side-effect';
7269+
import { Pipe, type PipeTransform } from '@angular/core';
7270+
import { type Get } from 'type-fest';
7271+
import type { Whole } from './whole';
7272+
import Def, { type Partial } from './default-mixed';
7273+
7274+
@Pipe({ name: 'demo' })
7275+
export class DemoPipe implements PipeTransform {
7276+
transform(v: string) { return Def(v as unknown as Whole as Partial as string); }
7277+
}
7278+
7279+
export let sample: Get<{ a: 1 }, 'a'> | undefined;
7280+
"#;
7281+
7282+
let options = ComponentTransformOptions { jit: true, ..Default::default() };
7283+
let result = transform_angular_file(&allocator, "demo.pipe.ts", source, Some(&options), None);
7284+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
7285+
7286+
assert!(
7287+
!result.code.contains("type-fest"),
7288+
"All-inline-type import should be elided, not kept as a side-effect import. Got:\n{}",
7289+
result.code
7290+
);
7291+
assert!(
7292+
!result.code.contains("./whole"),
7293+
"`import type {{ ... }}` statement should be elided. Got:\n{}",
7294+
result.code
7295+
);
7296+
7297+
// Mixed import keeps its value binding, drops the type-only one
7298+
assert!(
7299+
result.code.contains("import { Pipe } from"),
7300+
"Value binding of mixed import should be preserved. Got:\n{}",
7301+
result.code
7302+
);
7303+
assert!(
7304+
!result.code.contains("PipeTransform"),
7305+
"Type-only binding of mixed import should be dropped. Got:\n{}",
7306+
result.code
7307+
);
7308+
7309+
// Default import alongside an inline-type specifier keeps the default
7310+
// binding and drops the type-only sibling
7311+
assert!(
7312+
result.code.contains("import Def from"),
7313+
"Default import with inline-type sibling should be preserved. Got:\n{}",
7314+
result.code
7315+
);
7316+
assert!(
7317+
!result.code.contains("Partial"),
7318+
"Inline-type sibling of a default import should be dropped. Got:\n{}",
7319+
result.code
7320+
);
7321+
7322+
// Deliberate side-effect imports (no specifier list) must survive
7323+
assert!(
7324+
result.code.contains("import \"./side-effect\""),
7325+
"Bare side-effect import should be preserved. Got:\n{}",
7326+
result.code
7327+
);
7328+
}
7329+
72627330
#[test]
72637331
fn test_jit_component_class_restructuring() {
72647332
// JIT should restructure: export class X {} → let X = class X {}; X = __decorate([...], X); export { X };

0 commit comments

Comments
 (0)