Skip to content

Commit e99a497

Browse files
authored
fix: prevent removing JSX type parameter imports (#43)
1 parent 83ecda2 commit e99a497

File tree

3 files changed

+75
-8
lines changed

3 files changed

+75
-8
lines changed

__tests__/__snapshots__/transform.spec.js.snap

+11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ exports[`does not remove parameter decorators 1`] = `
1818
"
1919
`;
2020

21+
exports[`preserves imports of JSX type parameters 1`] = `
22+
"
23+
import { SomeComponent } from './some-path';
24+
import { SomeType } from './another-path';
25+
26+
const App = () => {
27+
return <SomeComponent<SomeType> />;
28+
};
29+
"
30+
`;
31+
2132
exports[`preserves specified import identifiers 1`] = `
2233
"
2334
import { React, useState } from 'react';

__tests__/transform.spec.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,18 @@ it('preserves specified import identifiers', () => {
135135
};
136136
`;
137137

138-
expect(() => transform(code, ['React'])).not.toThrow();
139138
expect(transform(code, ['React'])).toMatchSnapshot();
140139
});
140+
141+
it('preserves imports of JSX type parameters', () => {
142+
const code = `
143+
import { SomeComponent } from './some-path';
144+
import { SomeType } from './another-path';
145+
146+
const App = () => {
147+
return <SomeComponent<SomeType> />;
148+
};
149+
`;
150+
151+
expect(transform(code)).toMatchSnapshot();
152+
});

src/plugin.ts

+51-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,26 @@ type ImportSpecifier =
55
| types.ImportNamespaceSpecifier
66
| types.ImportSpecifier;
77

8-
const decorators = new Set();
8+
class ImportTracker {
9+
private decorators = new Set<string>();
10+
private typeParameters = new Set<string>();
11+
12+
addDecorator(name: string) {
13+
this.decorators.add(name);
14+
}
15+
16+
addTypeParameter(name: string) {
17+
this.typeParameters.add(name);
18+
}
19+
20+
shouldPreserve(name: string, userPreserved?: string[]) {
21+
return (
22+
this.decorators.has(name) ||
23+
this.typeParameters.has(name) ||
24+
userPreserved?.includes(name)
25+
);
26+
}
27+
}
928

1029
function isUnusedImportSpecifier(path: NodePath<ImportSpecifier>) {
1130
return !path.scope.bindings[path.node.local.name].referenced;
@@ -15,36 +34,61 @@ function hasAllSpecifiersRemoved(path: NodePath<types.ImportDeclaration>) {
1534
return path.get('specifiers').length === 0;
1635
}
1736

18-
function collectParameterDecorators(path: NodePath<types.TSParameterProperty>) {
37+
function collectParameterDecorators(
38+
path: NodePath<types.TSParameterProperty>,
39+
tracker: ImportTracker,
40+
) {
1941
path.node.decorators?.forEach(({ expression }) => {
2042
if (expression.type !== 'CallExpression') return;
2143

2244
if (expression.callee.type === 'Identifier') {
23-
decorators.add(expression.callee.name);
45+
tracker.addDecorator(expression.callee.name);
2446
}
2547

2648
expression.arguments.forEach((arg) => {
2749
if (arg.type === 'Identifier') {
28-
decorators.add(arg.name);
50+
tracker.addDecorator(arg.name);
2951
}
3052
});
3153
});
3254
}
3355

56+
function collectJSXTypeParameters(
57+
path: NodePath<types.JSXElement>,
58+
tracker: ImportTracker,
59+
) {
60+
const openingElement = path.node.openingElement;
61+
if (openingElement.typeParameters) {
62+
openingElement.typeParameters.params.forEach((param) => {
63+
if (
64+
param.type === 'TSTypeReference' &&
65+
param.typeName.type === 'Identifier'
66+
) {
67+
tracker.addTypeParameter(param.typeName.name);
68+
}
69+
});
70+
}
71+
}
72+
3473
export function removeUnusedImports(preserve?: string[]): PluginObj {
74+
const tracker = new ImportTracker();
75+
3576
return {
3677
visitor: {
3778
Program(path: NodePath<types.Program>) {
3879
path.traverse({
3980
TSParameterProperty(path: NodePath<types.TSParameterProperty>) {
40-
collectParameterDecorators(path);
81+
collectParameterDecorators(path, tracker);
82+
},
83+
JSXElement(path: NodePath<types.JSXElement>) {
84+
collectJSXTypeParameters(path, tracker);
4185
},
4286
});
4387
},
4488
ImportDeclaration(path: NodePath<types.ImportDeclaration>) {
4589
path.get('specifiers').forEach((specifier) => {
46-
if (decorators.has(specifier.node.local.name)) return;
47-
if (preserve?.includes(specifier.node.local.name)) return;
90+
const localName = specifier.node.local.name;
91+
if (tracker.shouldPreserve(localName, preserve)) return;
4892

4993
isUnusedImportSpecifier(specifier) && specifier.remove();
5094
hasAllSpecifiersRemoved(path) && path.remove();

0 commit comments

Comments
 (0)