Skip to content

Commit 44e1deb

Browse files
fix: modifyNestedForwardRef option. (#10)
1 parent 76ef0ab commit 44e1deb

5 files changed

Lines changed: 119 additions & 31 deletions

File tree

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@knighted/display-name",
3-
"version": "1.0.0-rc.2",
3+
"version": "1.0.0-rc.3",
44
"description": "Codemod to add a React displayName to function components.",
55
"type": "module",
66
"exports": {

src/displayName.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const collectDisplayNames = async (node: Node, code: MagicString) => {
5757

5858
return foundDisplayNames
5959
}
60-
const collectReactPragmas = async (node: Node) => {
60+
const detectReactPragmas = async (node: Node) => {
6161
let isReact = false
6262
let isReactMemo = false
6363
let isReactForwardRef = false
@@ -136,11 +136,6 @@ const isForwardRef = (node: CallExpression, scopes: Scope[]) => {
136136
isReactMember('forwardRef', node, scopes)
137137
)
138138
}
139-
const hasNestedForwardRef = (node: CallExpression, scopes: Scope[]) => {
140-
const arg = node.arguments[0]
141-
142-
return arg.type === 'CallExpression' && isForwardRef(arg, scopes)
143-
}
144139
const isMemoWrapped = (parent: Node, scopes: Scope[]) => {
145140
return parent.type === 'CallExpression' && isMemo(parent, scopes)
146141
}
@@ -164,7 +159,7 @@ const defaultOptions = {
164159
const modify = async (source: string, options: Options = defaultOptions) => {
165160
const ast = parseSync('file.tsx', source)
166161
const code = new MagicString(source)
167-
const { isReact, isReactMemo, isReactForwardRef } = await collectReactPragmas(
162+
const { isReact, isReactMemo, isReactForwardRef } = await detectReactPragmas(
168163
ast.program,
169164
)
170165

@@ -175,7 +170,11 @@ const modify = async (source: string, options: Options = defaultOptions) => {
175170
...defaultOptions,
176171
...options,
177172
}
178-
const addDisplayName = (ancestors: Node[], call: CallExpression) => {
173+
const addDisplayName = (
174+
ancestors: Node[],
175+
call: CallExpression,
176+
memoWrapped = false,
177+
) => {
179178
const declaratorIndex = ancestors.findLastIndex(
180179
ancestor => ancestor.type === 'VariableDeclarator',
181180
)
@@ -205,8 +204,11 @@ const modify = async (source: string, options: Options = defaultOptions) => {
205204
}
206205
}
207206

208-
// Pragma directly assigned to a variable
209-
if (declarator.init === call && !foundDisplayNames.includes(name)) {
207+
// Pragma directly assigned to a variable or forwardRef wrapped with memo
208+
if (
209+
(declarator.init === call || memoWrapped) &&
210+
!foundDisplayNames.includes(name)
211+
) {
210212
append(name)
211213
}
212214

@@ -320,19 +322,15 @@ const modify = async (source: string, options: Options = defaultOptions) => {
320322
!node.arguments[0].id
321323
) {
322324
if (isMemo(node, scopes)) {
323-
const nestedForwardRef = hasNestedForwardRef(node, scopes)
324-
325-
if (!nestedForwardRef || (nestedForwardRef && opts.modifyNestedForwardRef)) {
326-
addDisplayName(ancestors, node)
327-
}
325+
addDisplayName(ancestors, node)
328326
}
329327

330328
if (isForwardRef(node, scopes)) {
331329
const parent = ancestors[ancestors.length - 2]
332330
const memoWrapped = isMemoWrapped(parent, scopes)
333331

334332
if (!memoWrapped || (memoWrapped && opts.modifyNestedForwardRef)) {
335-
addDisplayName(ancestors, node)
333+
addDisplayName(ancestors, node, memoWrapped)
336334
}
337335
}
338336
}

test/displayName.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { resolve } from 'node:path'
44
import { rm, writeFile } from 'node:fs/promises'
55
import { spawnSync } from 'node:child_process'
66

7-
import { modifyFile } from '../src/displayName.js'
7+
import { modify, modifyFile } from '../src/displayName.js'
88

99
describe('@knighted/displayName', () => {
1010
it('transforms files', async t => {
@@ -22,6 +22,27 @@ describe('@knighted/displayName', () => {
2222
assert.ok(code.indexOf("Foo.displayName = 'Foo'") === -1)
2323
assert.ok(code.indexOf("NamedFuncExpr.displayName = 'NamedFuncExpr'") === -1)
2424

25+
// Ignored forwardRef wrapped in memo
26+
assert.ok(
27+
code.indexOf("MemoWrappedForward.displayName = 'MemoWrappedForward'") === -1,
28+
)
29+
30+
// Ignores shadowed React/memo/forwardRef
31+
assert.ok(code.indexOf("ShadowedMemo.displayName = 'ShadowedMemo'") === -1)
32+
assert.ok(code.indexOf("ShadowedReactMemo.displayName = 'ShadowedReactMemo'") === -1)
33+
assert.ok(
34+
code.indexOf("ShadowedReactForward.displayName = 'ShadowedReactForward'") === -1,
35+
)
36+
assert.ok(code.indexOf("ShadowedForward.displayName = 'ShadowedForward'") === -1)
37+
38+
// Adds displayName to memo/forwardRef
39+
assert.ok(code.indexOf("Memo.displayName = 'Memo'") !== -1)
40+
assert.ok(code.indexOf("ForwardRef.displayName = 'ForwardRef'") !== -1)
41+
42+
// Adds displayName to React.memo/React.forwardRef
43+
assert.ok(code.indexOf("ReactMemo.displayName = 'ReactMemo'") !== -1)
44+
assert.ok(code.indexOf("ReactForwardRef.displayName = 'ReactForwardRef'") !== -1)
45+
2546
// Correctly identifies namespaced displayNames already present
2647
assert.equal(
2748
[
@@ -31,6 +52,7 @@ describe('@knighted/displayName', () => {
3152
].length,
3253
1,
3354
)
55+
3456
const { status: lint } = spawnSync('eslint', [write], { stdio: 'inherit' })
3557
assert.equal(lint, 0)
3658
const { status: types } = spawnSync(
@@ -85,6 +107,74 @@ describe('@knighted/displayName', () => {
85107
assert.equal(types, 0)
86108
})
87109

110+
it('requires memo, forwardRef or React.memo/React.forwardRef to be in scope', async () => {
111+
const components = `
112+
const Foo = memo(() => {
113+
return <div>foo</div>
114+
})
115+
const Bar = forwardRef(() => {
116+
return <span>bar</span>
117+
})
118+
const Baz = React.memo(() => {
119+
return <p>baz</p>
120+
})
121+
const Qux = React.forwardRef(() => {
122+
return <p>qux</p>
123+
})
124+
`
125+
let code = await modify(components)
126+
127+
assert.ok(code.indexOf('Foo.displayName = "Foo"') === -1)
128+
assert.ok(code.indexOf('Bar.displayName = "Bar"') === -1)
129+
assert.ok(code.indexOf('Baz.displayName = "Baz"') === -1)
130+
assert.ok(code.indexOf('Qux.displayName = "Qux"') === -1)
131+
132+
let src = `
133+
import React, { memo, forwardRef } from 'react'
134+
${components}
135+
`
136+
code = await modify(src)
137+
138+
assert.ok(code.indexOf("Foo.displayName = 'Foo'") !== -1)
139+
assert.ok(code.indexOf("Bar.displayName = 'Bar'") !== -1)
140+
assert.ok(code.indexOf("Baz.displayName = 'Baz'") !== -1)
141+
assert.ok(code.indexOf("Qux.displayName = 'Qux'") !== -1)
142+
143+
// It ignores shadowed React/memo/forwardRef
144+
src = `
145+
import React, { memo, forwardRef } from 'react'
146+
147+
function Shadow() {
148+
const memo = (cb) => cb()
149+
const forwardRef = (cb) => cb()
150+
const React = { memo, forwardRef }
151+
152+
${components}
153+
}
154+
`
155+
156+
code = await modify(src)
157+
assert.ok(code.indexOf("Foo.displayName = 'Foo'") === -1)
158+
assert.ok(code.indexOf("Bar.displayName = 'Bar'") === -1)
159+
assert.ok(code.indexOf("Baz.displayName = 'Baz'") === -1)
160+
assert.ok(code.indexOf("Qux.displayName = 'Qux'") === -1)
161+
})
162+
163+
it('has option to add displayName for wrapped forwardRef', async () => {
164+
const src = `
165+
import { forwardRef, memo } from 'react'
166+
167+
const WrappedForwardRef = memo(forwardRef(() => {
168+
return <div>foo</div>
169+
}))
170+
`
171+
let code = await modify(src, { modifyNestedForwardRef: true })
172+
173+
assert.ok(code.indexOf("WrappedForwardRef.displayName = 'WrappedForwardRef'") !== -1)
174+
code = await modify(src, { modifyNestedForwardRef: false })
175+
assert.ok(code.indexOf("WrappedForwardRef.displayName = 'WrappedForwardRef'") === -1)
176+
})
177+
88178
it.skip('works with params shadowing', async t => {
89179
// @TODO collect coverage for params scopes
90180
const read = resolve(import.meta.dirname, './fixtures/params.tsx')

test/fixtures/react.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const Baz = function (props: object) {
1212
return <p>baz</p>
1313
}
1414

15-
const Qux = memo(() => {
15+
const Memo = memo(() => {
1616
return (
1717
<div>
1818
{'qux'}
@@ -21,13 +21,13 @@ const Qux = memo(() => {
2121
)
2222
})
2323

24-
const Quxx = memo(
24+
const MemoWrappedForward = memo(
2525
forwardRef<HTMLDivElement, object>((props, ref) => {
2626
return <div ref={ref}>quxx</div>
2727
}),
2828
)
2929

30-
const Ref = forwardRef<HTMLDivElement, object>((props, ref) => {
30+
const ForwardRef = forwardRef<HTMLDivElement, object>((props, ref) => {
3131
return <div ref={ref}>ref</div>
3232
})
3333

@@ -103,18 +103,18 @@ const MixedShadowed = function () {
103103
const { forwardRef } = { forwardRef: () => null } as ForwardRefObject
104104
const [React] = [{ memo, forwardRef }] as const
105105

106-
const Comp = memo(() => {
106+
const ShadowedMemo = memo(() => {
107107
return <div>shadowed</div>
108108
})
109109
// eslint-disable-next-line react/display-name
110-
const ReactMemo = React.memo(() => null)
110+
const ShadowedReactMemo = React.memo(() => null)
111111
// eslint-disable-next-line react/display-name
112-
const ReactForward = React.forwardRef((props, ref) => {
112+
const ShadowedReactForward = React.forwardRef((props, ref) => {
113113
return `${props} ${ref}`
114114
})
115-
const OtherComp = forwardRef((props, ref) => `${props} ${ref}`)
115+
const ShadowedForward = forwardRef((props, ref) => `${props} ${ref}`)
116116

117-
return [Comp, ReactMemo, ReactForward, OtherComp]
117+
return [ShadowedMemo, ShadowedReactMemo, ShadowedReactForward, ShadowedForward]
118118
}
119119

120120
const Shadowed = function () {
@@ -138,9 +138,9 @@ export {
138138
Foo,
139139
Bar,
140140
Baz,
141-
Qux,
142-
Quxx,
143-
Ref,
141+
Memo,
142+
MemoWrappedForward,
143+
ForwardRef,
144144
NestedMemo,
145145
NestedForwardRef,
146146
FuncExpr,

0 commit comments

Comments
 (0)