Skip to content

Commit b6bfa2c

Browse files
committed
fix(@ngtools/webpack): elide imports used only in type re-exports or type declarations
When `elideImports` removes unused import specifiers (such as after `removeIvyJitSupportCalls` removes `ɵsetClassMetadata`), it previously walked into `interface` declarations, `type` alias declarations, and type-only `export` declarations/specifiers (`export type { X }`, `export { type X }`). This caused it to mark those type-only imported symbols as used at runtime and rewrite the `ImportDeclaration` while retaining those specifiers. Because the `ImportDeclaration` AST node was replaced by a `before` transformer, TypeScript skipped its own type-only import elision on the cloned declaration, leaving the type-only specifier in the emitted JS output. With webpack 5.112.0+, unused import specifiers are still link-checked against the target module, causing builds with `strictExportPresence: true` to fail. Skip pure type declarations (`InterfaceDeclaration`, `TypeAliasDeclaration`, `TypeParameterDeclaration`) and type-only exports (`ExportDeclaration` and `ExportSpecifier` with `isTypeOnly`) when collecting used identifiers in `elideImports`. Closes #34580
1 parent 5deaace commit b6bfa2c

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

‎packages/angular/build/src/tools/angular/transformers/jit-bootstrap-transformer.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,17 @@ export function elideImports(
142142
return;
143143
}
144144

145+
// Pure type declarations and type-only exports are erased at runtime.
146+
if (
147+
ts.isInterfaceDeclaration(node) ||
148+
ts.isTypeAliasDeclaration(node) ||
149+
ts.isTypeParameterDeclaration(node) ||
150+
(ts.isExportDeclaration(node) && node.isTypeOnly) ||
151+
(ts.isExportSpecifier(node) && node.isTypeOnly)
152+
) {
153+
return;
154+
}
155+
145156
let symbol: ts.Symbol | undefined;
146157
switch (node.kind) {
147158
case ts.SyntaxKind.Identifier:

‎packages/ngtools/webpack/src/transformers/elide_imports.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ export function elideImports(
5959
return;
6060
}
6161

62+
// Pure type declarations and type-only exports are erased at runtime.
63+
if (
64+
ts.isInterfaceDeclaration(node) ||
65+
ts.isTypeAliasDeclaration(node) ||
66+
ts.isTypeParameterDeclaration(node) ||
67+
(ts.isExportDeclaration(node) && node.isTypeOnly) ||
68+
(ts.isExportSpecifier(node) && node.isTypeOnly)
69+
) {
70+
return;
71+
}
72+
6273
let symbol: ts.Symbol | undefined;
6374
switch (node.kind) {
6475
case ts.SyntaxKind.Identifier:

‎packages/ngtools/webpack/src/transformers/elide_imports_spec.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,47 @@ describe('elide_imports', () => {
414414

415415
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
416416
});
417+
418+
it('should remove import when specifiers are only used in type annotations, type-only re-exports, or interface extends', () => {
419+
const input = tags.stripIndent`
420+
import { Decorator } from './decorator';
421+
import { OnChanges, SimpleChanges } from './type';
422+
import { Service, Service2 } from './service';
423+
424+
export type { SimpleChanges };
425+
export { type Service2 };
426+
export interface ExtendedChanges extends OnChanges {
427+
extra: string;
428+
}
429+
430+
@Decorator()
431+
export class Foo {
432+
constructor(param: Service) {
433+
}
434+
}
435+
436+
${dummyNode}
437+
`;
438+
439+
const output = tags.stripIndent`
440+
import { __decorate } from "tslib";
441+
import { Decorator } from './decorator';
442+
443+
let Foo = class Foo { constructor(param) { } };
444+
Foo = __decorate([ Decorator() ], Foo);
445+
export { Foo };
446+
`;
447+
448+
const { program, compilerHost } = createTypescriptContext(
449+
input,
450+
additionalFiles,
451+
true,
452+
extraCompilerOptions,
453+
);
454+
const result = transformTypescript(undefined, [transformer(program)], program, compilerHost);
455+
456+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
457+
});
417458
});
418459

419460
it('keeps jsxFactory imports when configured', () => {

0 commit comments

Comments
 (0)