Skip to content

Commit a6b480b

Browse files
feat: support import aliases. (#13)
1 parent 44e1deb commit a6b480b

8 files changed

Lines changed: 521 additions & 130 deletions

File tree

package-lock.json

Lines changed: 309 additions & 50 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@knighted/display-name",
3-
"version": "1.0.0-rc.3",
3+
"version": "1.0.0-rc.4",
44
"description": "Codemod to add a React displayName to function components.",
55
"type": "module",
66
"exports": {
@@ -51,7 +51,7 @@
5151
"dependencies": {
5252
"@knighted/walk": "^1.0.0-rc.2",
5353
"magic-string": "^0.30.17",
54-
"oxc-parser": "^0.68.1"
54+
"oxc-parser": "^0.69.0"
5555
},
5656
"devDependencies": {
5757
"@eslint/js": "^9.26.0",

src/displayName.ts

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ type Scope = {
1919
type: string
2020
pragmas: Set<string>
2121
}
22-
type Pragma = (typeof pragmas)[number]
22+
type Pragmas = {
23+
react: string
24+
memo: string
25+
forwardRef: string
26+
}
2327

2428
const pascal = /^[A-Z][a-zA-Z0-9]*$/
25-
const pragmas = ['React', 'memo', 'forwardRef'] as const
26-
const isPragma = (name: string): name is Pragma => {
27-
return pragmas.includes(name as Pragma)
28-
}
2929
const isIdentifierName = (node: Node): node is IdentifierName => {
3030
return node.type === 'Identifier' && typeof node.name === 'string'
3131
}
@@ -58,44 +58,38 @@ const collectDisplayNames = async (node: Node, code: MagicString) => {
5858
return foundDisplayNames
5959
}
6060
const detectReactPragmas = async (node: Node) => {
61-
let isReact = false
62-
let isReactMemo = false
63-
let isReactForwardRef = false
61+
let react = ''
62+
let memo = ''
63+
let forwardRef = ''
6464

6565
await walk(node, {
6666
enter(node) {
6767
if (node.type === 'ImportDeclaration' && node.source.value === 'react') {
6868
for (const specifier of node.specifiers) {
69-
if (
70-
specifier.type === 'ImportDefaultSpecifier' &&
71-
specifier.local.name === 'React'
72-
) {
73-
isReact = true
69+
if (specifier.type === 'ImportDefaultSpecifier') {
70+
react = specifier.local.name
7471
}
7572

7673
if (
7774
specifier.type === 'ImportSpecifier' &&
7875
specifier.imported.type === 'Identifier'
7976
) {
80-
if (specifier.imported.name === 'memo' && specifier.local.name === 'memo') {
81-
isReactMemo = true
77+
if (specifier.imported.name === 'memo') {
78+
memo = specifier.local.name
8279
}
8380

84-
if (
85-
specifier.imported.name === 'forwardRef' &&
86-
specifier.local.name === 'forwardRef'
87-
) {
88-
isReactForwardRef = true
81+
if (specifier.imported.name === 'forwardRef') {
82+
forwardRef = specifier.local.name
8983
}
9084
}
9185
}
9286
}
9387
},
9488
})
9589

96-
return { isReact, isReactMemo, isReactForwardRef }
90+
return { react, memo, forwardRef }
9791
}
98-
const isPragmaShadowed = (pragma: Pragma, scopes: Scope[]) => {
92+
const isPragmaShadowed = (pragma: string, scopes: Scope[]) => {
9993
for (const scope of scopes) {
10094
if (scope.pragmas.has(pragma)) {
10195
return true
@@ -104,40 +98,45 @@ const isPragmaShadowed = (pragma: Pragma, scopes: Scope[]) => {
10498

10599
return false
106100
}
107-
const isReactMember = (pragma: Pragma, node: CallExpression, scopes: Scope[]) => {
101+
const isReactMember = (
102+
pragma: string,
103+
node: CallExpression,
104+
pragmas: Pragmas,
105+
scopes: Scope[],
106+
) => {
108107
const { callee } = node
109108

110109
return (
111110
callee.type === 'MemberExpression' &&
112111
callee.object.type === 'Identifier' &&
113-
callee.object.name === 'React' &&
114-
!isPragmaShadowed('React', scopes) &&
112+
callee.object.name === pragmas.react &&
115113
callee.property.type === 'Identifier' &&
116-
callee.property.name === pragma
114+
callee.property.name === pragma &&
115+
!isPragmaShadowed(pragmas.react, scopes)
117116
)
118117
}
119-
const isMemo = (node: CallExpression, scopes: Scope[]) => {
118+
const isMemo = (node: CallExpression, pragmas: Pragmas, scopes: Scope[]) => {
120119
const { callee } = node
121120

122121
return (
123122
(callee.type === 'Identifier' &&
124-
callee.name === 'memo' &&
125-
!isPragmaShadowed('memo', scopes)) ||
126-
isReactMember('memo', node, scopes)
123+
callee.name === pragmas.memo &&
124+
!isPragmaShadowed(pragmas.memo, scopes)) ||
125+
isReactMember('memo', node, pragmas, scopes)
127126
)
128127
}
129-
const isForwardRef = (node: CallExpression, scopes: Scope[]) => {
128+
const isForwardRef = (node: CallExpression, pragmas: Pragmas, scopes: Scope[]) => {
130129
const { callee } = node
131130

132131
return (
133132
(callee.type === 'Identifier' &&
134-
callee.name === 'forwardRef' &&
135-
!isPragmaShadowed('forwardRef', scopes)) ||
136-
isReactMember('forwardRef', node, scopes)
133+
callee.name === pragmas.forwardRef &&
134+
!isPragmaShadowed(pragmas.forwardRef, scopes)) ||
135+
isReactMember('forwardRef', node, pragmas, scopes)
137136
)
138137
}
139-
const isMemoWrapped = (parent: Node, scopes: Scope[]) => {
140-
return parent.type === 'CallExpression' && isMemo(parent, scopes)
138+
const isMemoWrapped = (parent: Node, pragmas: Pragmas, scopes: Scope[]) => {
139+
return parent.type === 'CallExpression' && isMemo(parent, pragmas, scopes)
141140
}
142141
const scopeNodes = [
143142
'FunctionDeclaration',
@@ -159,11 +158,13 @@ const defaultOptions = {
159158
const modify = async (source: string, options: Options = defaultOptions) => {
160159
const ast = parseSync('file.tsx', source)
161160
const code = new MagicString(source)
162-
const { isReact, isReactMemo, isReactForwardRef } = await detectReactPragmas(
163-
ast.program,
164-
)
161+
const { react, memo, forwardRef } = await detectReactPragmas(ast.program)
162+
const pragmas = { react, memo, forwardRef } satisfies Pragmas
163+
const isPragma = (name: string) => {
164+
return name === react || name === memo || name === forwardRef
165+
}
165166

166-
if (isReact || isReactMemo || isReactForwardRef) {
167+
if (react || memo || forwardRef) {
167168
const scopes: Scope[] = []
168169
const foundDisplayNames = await collectDisplayNames(ast.program, code)
169170
const opts = {
@@ -321,13 +322,13 @@ const modify = async (source: string, options: Options = defaultOptions) => {
321322
node.arguments[0].type === 'ArrowFunctionExpression') &&
322323
!node.arguments[0].id
323324
) {
324-
if (isMemo(node, scopes)) {
325+
if (isMemo(node, pragmas, scopes)) {
325326
addDisplayName(ancestors, node)
326327
}
327328

328-
if (isForwardRef(node, scopes)) {
329+
if (isForwardRef(node, pragmas, scopes)) {
329330
const parent = ancestors[ancestors.length - 2]
330-
const memoWrapped = isMemoWrapped(parent, scopes)
331+
const memoWrapped = isMemoWrapped(parent, pragmas, scopes)
331332

332333
if (!memoWrapped || (memoWrapped && opts.modifyNestedForwardRef)) {
333334
addDisplayName(ancestors, node, memoWrapped)

test/displayName.ts

Lines changed: 159 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ describe('@knighted/displayName', () => {
107107
assert.equal(types, 0)
108108
})
109109

110-
it('requires memo, forwardRef or React.memo/React.forwardRef to be in scope', async () => {
110+
it('requires memo, forwardRef or React to be in scope', async () => {
111111
const components = `
112112
const Foo = memo(() => {
113113
return <div>foo</div>
@@ -158,6 +158,36 @@ describe('@knighted/displayName', () => {
158158
assert.ok(code.indexOf("Bar.displayName = 'Bar'") === -1)
159159
assert.ok(code.indexOf("Baz.displayName = 'Baz'") === -1)
160160
assert.ok(code.indexOf("Qux.displayName = 'Qux'") === -1)
161+
162+
src = `
163+
import { memo } from 'react'
164+
${components}
165+
`
166+
code = await modify(src)
167+
assert.ok(code.indexOf("Foo.displayName = 'Foo'") !== -1)
168+
assert.ok(code.indexOf("Bar.displayName = 'Bar'") === -1)
169+
assert.ok(code.indexOf("Baz.displayName = 'Baz'") === -1)
170+
assert.ok(code.indexOf("Qux.displayName = 'Qux'") === -1)
171+
172+
src = `
173+
import { forwardRef } from 'react'
174+
${components}
175+
`
176+
code = await modify(src)
177+
assert.ok(code.indexOf("Foo.displayName = 'Foo'") === -1)
178+
assert.ok(code.indexOf("Bar.displayName = 'Bar'") !== -1)
179+
assert.ok(code.indexOf("Baz.displayName = 'Baz'") === -1)
180+
assert.ok(code.indexOf("Qux.displayName = 'Qux'") === -1)
181+
182+
src = `
183+
import React from 'react'
184+
${components}
185+
`
186+
code = await modify(src)
187+
assert.ok(code.indexOf("Foo.displayName = 'Foo'") === -1)
188+
assert.ok(code.indexOf("Bar.displayName = 'Bar'") === -1)
189+
assert.ok(code.indexOf("Baz.displayName = 'Baz'") !== -1)
190+
assert.ok(code.indexOf("Qux.displayName = 'Qux'") !== -1)
161191
})
162192

163193
it('has option to add displayName for wrapped forwardRef', async () => {
@@ -175,16 +205,135 @@ describe('@knighted/displayName', () => {
175205
assert.ok(code.indexOf("WrappedForwardRef.displayName = 'WrappedForwardRef'") === -1)
176206
})
177207

178-
it.skip('works with params shadowing', async t => {
179-
// @TODO collect coverage for params scopes
180-
const read = resolve(import.meta.dirname, './fixtures/params.tsx')
181-
const write = resolve(import.meta.dirname, './fixtures/params-modified.tsx')
182-
const code = await modifyFile(read)
208+
it('works with aliased React/memo/forwardRef', async () => {
209+
let src = `
210+
import { memo as me, forwardRef as fr } from 'react'
183211
184-
t.after(async () => {
185-
await rm(write, { force: true })
186-
})
212+
const Foo = me(() => {
213+
return <div>foo</div>
214+
})
215+
const Bar = fr(() => {
216+
return <span>bar</span>
217+
})
218+
const Baz = memo(() => {
219+
return <p>baz</p>
220+
})
221+
`
222+
let code = await modify(src)
187223

188-
await writeFile(write, code)
224+
assert.ok(code.indexOf("Foo.displayName = 'Foo'") !== -1)
225+
assert.ok(code.indexOf("Bar.displayName = 'Bar'") !== -1)
226+
assert.ok(code.indexOf("Baz.displayName = 'Baz'") === -1)
227+
228+
src = `
229+
import ReactAlias from 'react'
230+
231+
const Foo = ReactAlias.memo(() => {
232+
return <div>foo</div>
233+
})
234+
const Bar = ReactAlias.forwardRef(() => {
235+
return <span>bar</span>
236+
})
237+
const Baz = React.memo(() => {
238+
return <p>baz</p>
239+
})
240+
`
241+
code = await modify(src)
242+
assert.ok(code.indexOf("Foo.displayName = 'Foo'") !== -1)
243+
assert.ok(code.indexOf("Bar.displayName = 'Bar'") !== -1)
244+
assert.ok(code.indexOf("Baz.displayName = 'Baz'") === -1)
245+
246+
src = `
247+
import ReactAlias, { memo as me, forwardRef as fr } from 'react'
248+
249+
const Foo = ReactAlias.me(() => {
250+
return <div>foo</div>
251+
})
252+
const Bar = ReactAlias.fr(() => {
253+
return <span>bar</span>
254+
})
255+
const Baz = ReactAlias.memo(() => {
256+
return <p>baz</p>
257+
})
258+
const Qux = ReactAlias.forwardRef(() => {
259+
return <p>qux</p>
260+
})
261+
const Qux2 = me(() => {
262+
return <p>qux2</p>
263+
})
264+
const Qux3 = fr(() => {
265+
return <p>qux3</p>
266+
})
267+
`
268+
code = await modify(src)
269+
assert.ok(code.indexOf("Foo.displayName = 'Foo'") === -1)
270+
assert.ok(code.indexOf("Bar.displayName = 'Bar'") === -1)
271+
assert.ok(code.indexOf("Baz.displayName = 'Baz'") !== -1)
272+
assert.ok(code.indexOf("Qux.displayName = 'Qux'") !== -1)
273+
assert.ok(code.indexOf("Qux2.displayName = 'Qux2'") !== -1)
274+
assert.ok(code.indexOf("Qux3.displayName = 'Qux3'") !== -1)
275+
})
276+
277+
it('works with params shadowing', async () => {
278+
const src = `
279+
import { memo, forwardRef, type ReactNode } from 'react'
280+
281+
function wrapper(cb) {
282+
return cb()
283+
}
284+
285+
function ShadowedMemoParam(memo, forwardRef = memo) {
286+
const Foo = memo(() => {
287+
return <div>foo</div>
288+
})
289+
}
290+
291+
function shadowedForwardRefParam(forwardRef = wrapper) {
292+
const Bar = forwardRef((props, ref) => {
293+
return <div>foo</div>
294+
})
295+
}
296+
297+
function shadowedMemoRestParam(...rest) {
298+
const memo = rest[0]
299+
const Baz = memo(() => {
300+
return <div>foo</div>
301+
})
302+
}
303+
304+
function shadowingFuncs() {
305+
function memo(cb) {
306+
return cb()
307+
}
308+
const Qux = memo(() => {
309+
return <div>foo</div>
310+
})
311+
const Expr = function forwardRef(cb) {
312+
const Quxx = forwardRef(() => {
313+
return <div>foo</div>
314+
})
315+
return cb(Quxx)
316+
}
317+
}
318+
319+
function shadowTSParamProperty(public memo: () => ReactNode) {
320+
const Quxxx = memo(() => {
321+
return <div>foo</div>
322+
})
323+
}
324+
325+
const Memo = memo(() => {
326+
return <div>foo</div>
327+
})
328+
`
329+
const code = await modify(src)
330+
331+
assert.ok(code.indexOf("Foo.displayName = 'Foo'") === -1)
332+
assert.ok(code.indexOf("Bar.displayName = 'Bar'") === -1)
333+
assert.ok(code.indexOf("Baz.displayName = 'Baz'") === -1)
334+
assert.ok(code.indexOf("Qux.displayName = 'Qux'") === -1)
335+
assert.ok(code.indexOf("Quxx.displayName = 'Quxx'") === -1)
336+
assert.ok(code.indexOf("Quxxx.displayName = 'Quxxx'") === -1)
337+
assert.ok(code.indexOf("Memo.displayName = 'Memo'") !== -1)
189338
})
190339
})

test/fixtures/list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { forwardRef } from 'react'
1+
import { forwardRef } from 'react'
22

33
const List = forwardRef<HTMLUListElement, object>((props, ref) => {
44
const items = ['a', 'b', 'c']

0 commit comments

Comments
 (0)