Skip to content

Commit 4efc925

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 4a8e056 commit 4efc925

3 files changed

Lines changed: 68 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: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,52 @@ describe('@ngtools/webpack transformers', () => {
425425

426426
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
427427
});
428+
429+
it('should remove import when specifiers are only used in type annotations, type-only re-exports, or interface extends', () => {
430+
const input = tags.stripIndent`
431+
import { Decorator } from './decorator';
432+
import { OnChanges, SimpleChanges } from './type';
433+
import { Service, Service2 } from './service';
434+
435+
export type { SimpleChanges };
436+
export { type Service2 };
437+
export interface ExtendedChanges extends OnChanges {
438+
extra: string;
439+
}
440+
441+
@Decorator()
442+
export class Foo {
443+
constructor(param: Service) {
444+
}
445+
}
446+
447+
${dummyNode}
448+
`;
449+
450+
const output = tags.stripIndent`
451+
import { __decorate } from "tslib";
452+
import { Decorator } from './decorator';
453+
454+
let Foo = class Foo { constructor(param) { } };
455+
Foo = __decorate([ Decorator() ], Foo);
456+
export { Foo };
457+
`;
458+
459+
const { program, compilerHost } = createTypescriptContext(
460+
input,
461+
additionalFiles,
462+
true,
463+
extraCompilerOptions,
464+
);
465+
const result = transformTypescript(
466+
undefined,
467+
[transformer(program)],
468+
program,
469+
compilerHost,
470+
);
471+
472+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
473+
});
428474
});
429475

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

0 commit comments

Comments
 (0)