Skip to content

Commit 5b819d3

Browse files
feat: style option. (#14)
1 parent a6b480b commit 5b819d3

5 files changed

Lines changed: 320 additions & 15 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.4",
3+
"version": "1.0.0-rc.5",
44
"description": "Codemod to add a React displayName to function components.",
55
"type": "module",
66
"exports": {

src/displayName.ts

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type Options = {
1313
requirePascal?: boolean
1414
insertSemicolon?: boolean
1515
modifyNestedForwardRef?: boolean
16+
style?: 'displayName' | 'namedFuncExpr'
1617
}
1718
type Scope = {
1819
name: string
@@ -144,6 +145,7 @@ const scopeNodes = [
144145
'ArrowFunctionExpression',
145146
]
146147
const defaultOptions = {
148+
style: 'displayName',
147149
requirePascal: true,
148150
insertSemicolon: true,
149151
/**
@@ -190,17 +192,66 @@ const modify = async (source: string, options: Options = defaultOptions) => {
190192
) {
191193
let { name } = declarator.id
192194
const declName = name
193-
const append = (displayName: string) => {
195+
const update = (displayName: string) => {
194196
const declaration = ancestors[declaratorIndex - 1]
195197

196198
if (
197199
declaration.type === 'VariableDeclaration' &&
198200
(!opts.requirePascal || pascal.test(declName))
199201
) {
200-
code.appendRight(
201-
declaration.end,
202-
`\n${displayName}.displayName = '${displayName}'${opts.insertSemicolon ? ';' : ''}`,
203-
)
202+
if (opts.style === 'namedFuncExpr') {
203+
const func = call.arguments[0]
204+
205+
switch (func.type) {
206+
case 'FunctionExpression':
207+
{
208+
const params = func.params.map(param =>
209+
code.slice(param.start, param.end),
210+
)
211+
const body = func.body
212+
? code.slice(func.body.start, func.body.end)
213+
: '{}'
214+
const paramsWithBody = `${displayName}(${params.join(', ')}) ${body}`
215+
216+
code.update(
217+
func.start,
218+
func.end,
219+
func.generator
220+
? `function* ${paramsWithBody}`
221+
: `function ${paramsWithBody}`,
222+
)
223+
}
224+
break
225+
case 'ArrowFunctionExpression':
226+
{
227+
const params = func.params.map(param =>
228+
code.slice(param.start, param.end),
229+
)
230+
const body = code.slice(func.body.start, func.body.end)
231+
/**
232+
* If the body is an expression, it is the implicit return value.
233+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#function_body
234+
*/
235+
const bodyBlock =
236+
func.body.type === 'BlockStatement'
237+
? body
238+
: `{\nreturn ${body}\n}`
239+
240+
code.update(
241+
func.start,
242+
func.end,
243+
`function ${displayName}(${params.join(', ')}) ${bodyBlock}`,
244+
)
245+
}
246+
break
247+
}
248+
} else {
249+
code.appendRight(
250+
declaration.end,
251+
`\n${displayName}.displayName = '${displayName}'${opts.insertSemicolon ? ';' : ''}`,
252+
)
253+
}
254+
204255
foundDisplayNames.push(displayName)
205256
}
206257
}
@@ -210,7 +261,7 @@ const modify = async (source: string, options: Options = defaultOptions) => {
210261
(declarator.init === call || memoWrapped) &&
211262
!foundDisplayNames.includes(name)
212263
) {
213-
append(name)
264+
update(name)
214265
}
215266

216267
// Pragma assigned to some object property
@@ -226,10 +277,13 @@ const modify = async (source: string, options: Options = defaultOptions) => {
226277
parent = ancestors[ancestors.indexOf(parent) - 1]
227278
}
228279

229-
name = `${name}.${keys.reverse().join('.')}`
280+
name =
281+
opts.style === 'displayName'
282+
? `${name}.${keys.reverse().join('.')}`
283+
: keys[0]
230284

231285
if (!foundDisplayNames.includes(name)) {
232-
append(name)
286+
update(name)
233287
}
234288
}
235289
}
@@ -330,7 +384,7 @@ const modify = async (source: string, options: Options = defaultOptions) => {
330384
const parent = ancestors[ancestors.length - 2]
331385
const memoWrapped = isMemoWrapped(parent, pragmas, scopes)
332386

333-
if (!memoWrapped || (memoWrapped && opts.modifyNestedForwardRef)) {
387+
if (!memoWrapped || opts.modifyNestedForwardRef) {
334388
addDisplayName(ancestors, node, memoWrapped)
335389
}
336390
}

test/displayName.ts

Lines changed: 198 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,7 @@ describe('@knighted/displayName', () => {
109109

110110
it('requires memo, forwardRef or React to be in scope', async () => {
111111
const components = `
112-
const Foo = memo(() => {
113-
return <div>foo</div>
114-
})
112+
const Foo = memo(() => <div>foo</div>)
115113
const Bar = forwardRef(() => {
116114
return <span>bar</span>
117115
})
@@ -272,6 +270,41 @@ describe('@knighted/displayName', () => {
272270
assert.ok(code.indexOf("Qux.displayName = 'Qux'") !== -1)
273271
assert.ok(code.indexOf("Qux2.displayName = 'Qux2'") !== -1)
274272
assert.ok(code.indexOf("Qux3.displayName = 'Qux3'") !== -1)
273+
274+
src = `
275+
import ReactAlias, { memo as me, forwardRef as fr } from 'react'
276+
277+
function ShadowedReact() {
278+
const ReactAlias = { memo: () => {}, forwardRef: () => {} }
279+
const Foo = ReactAlias.memo(() => {
280+
return <div>foo</div>
281+
})
282+
const Qux = ReactAlias.forwardRef(() => {
283+
return <span>qux</span>
284+
})
285+
const Memo = me(() => {
286+
return <div>foo</div>
287+
})
288+
}
289+
function ShadowedMemo() {
290+
const me = () => {}
291+
const Bar = me(() => {
292+
return <div>bar</div>
293+
})
294+
}
295+
function ShadowedForwardRef() {
296+
const fr = () => {}
297+
const Baz = fr(() => {
298+
return <div>baz</div>
299+
})
300+
}
301+
`
302+
code = await modify(src)
303+
assert.ok(code.indexOf("Foo.displayName = 'Foo'") === -1)
304+
assert.ok(code.indexOf("Bar.displayName = 'Bar'") === -1)
305+
assert.ok(code.indexOf("Baz.displayName = 'Baz'") === -1)
306+
assert.ok(code.indexOf("Qux.displayName = 'Qux'") === -1)
307+
assert.ok(code.indexOf("Memo.displayName = 'Memo'") !== -1)
275308
})
276309

277310
it('works with params shadowing', async () => {
@@ -336,4 +369,166 @@ describe('@knighted/displayName', () => {
336369
assert.ok(code.indexOf("Quxxx.displayName = 'Quxxx'") === -1)
337370
assert.ok(code.indexOf("Memo.displayName = 'Memo'") !== -1)
338371
})
372+
373+
it('has option to use named function expressions', async () => {
374+
let src = `
375+
import { memo, forwardRef } from 'react'
376+
const arePropsEqual = () => true
377+
const Foo = memo((props) => {
378+
return <div>foo</div>
379+
}, arePropsEqual)
380+
const Bar = forwardRef((props, ref) => {
381+
return <span>bar</span>
382+
})
383+
const Baz = forwardRef<HTMLSpanElement, { foo: string }>((props, ref) => {
384+
return <span ref={ref}>baz</span>
385+
})
386+
`
387+
let code = await modify(src, { style: 'namedFuncExpr' })
388+
389+
assert.equal(
390+
code.replace(/\s+/g, ''),
391+
`
392+
import { memo, forwardRef } from 'react'
393+
const arePropsEqual = () => true
394+
const Foo = memo(function Foo(props) {
395+
return <div>foo</div>
396+
}, arePropsEqual)
397+
const Bar = forwardRef(function Bar(props, ref) {
398+
return <span>bar</span>
399+
})
400+
const Baz = forwardRef<HTMLSpanElement, { foo: string }>(function Baz(props, ref) {
401+
return <span ref={ref}>baz</span>
402+
})
403+
`.replace(/\s+/g, ''),
404+
)
405+
406+
src = `
407+
import { memo, forwardRef } from 'react'
408+
const MemoWrapped = memo(forwardRef((props, ref) => {
409+
return <p>foo</p>
410+
}))
411+
`
412+
code = await modify(src, {
413+
style: 'namedFuncExpr',
414+
modifyNestedForwardRef: true,
415+
})
416+
assert.equal(
417+
code.replace(/\s+/g, ''),
418+
`
419+
import { memo, forwardRef } from 'react'
420+
const MemoWrapped = memo(forwardRef(function MemoWrapped(props, ref) {
421+
return <p>foo</p>
422+
}))
423+
`.replace(/\s+/g, ''),
424+
)
425+
426+
src = `
427+
import { memo, forwardRef } from 'react'
428+
const arePropsEqual = (prevProps: object, nextProps: object) => {
429+
return prevProps === nextProps
430+
}
431+
const Namespaced = {
432+
Foo: {
433+
Bar: memo((props) => {
434+
return <div>bar</div>
435+
}, arePropsEqual),
436+
Baz: forwardRef((props, ref) => {
437+
return <span>baz</span>
438+
}),
439+
}
440+
}
441+
`
442+
code = await modify(src, { style: 'namedFuncExpr' })
443+
assert.equal(
444+
code.replace(/\s+/g, ''),
445+
`
446+
import { memo, forwardRef } from 'react'
447+
const arePropsEqual = (prevProps: object, nextProps: object) => {
448+
return prevProps === nextProps
449+
}
450+
const Namespaced = {
451+
Foo: {
452+
Bar: memo(function Bar(props) {
453+
return <div>bar</div>
454+
}, arePropsEqual),
455+
Baz: forwardRef(function Baz(props, ref) {
456+
return <span>baz</span>
457+
}),
458+
}
459+
}
460+
`.replace(/\s+/g, ''),
461+
)
462+
463+
src = `
464+
import { memo, forwardRef } from 'react'
465+
const A = memo(function (props) {
466+
return <p>a</p>
467+
}, arePropsEqual)
468+
const B = forwardRef(function () {
469+
return <p>b</p>
470+
})
471+
`
472+
code = await modify(src, { style: 'namedFuncExpr' })
473+
assert.equal(
474+
code.replace(/\s+/g, ''),
475+
`
476+
import { memo, forwardRef } from 'react'
477+
const A = memo(function A(props) {
478+
return <p>a</p>
479+
}, arePropsEqual)
480+
const B = forwardRef(function B() {
481+
return <p>b</p>
482+
})
483+
`.replace(/\s+/g, ''),
484+
)
485+
})
486+
487+
it('the style option works with namedFuncExpr', async t => {
488+
const read = resolve(import.meta.dirname, './fixtures/style.tsx')
489+
const write = resolve(import.meta.dirname, './fixtures/style-modified.tsx')
490+
const code = await modifyFile(read, { style: 'namedFuncExpr' })
491+
const normalized = code.replace(/\s+/g, '')
492+
493+
t.after(async () => {
494+
await rm(write, { force: true })
495+
})
496+
497+
await writeFile(write, code)
498+
499+
// A present displayName should not be modified
500+
assert.ok(code.indexOf('function MemoDisplayName(props: Props)') === -1)
501+
assert.ok(code.indexOf('function ReactMemoDisplayName(props: Props)') === -1)
502+
503+
// Check function expressions
504+
assert.ok(
505+
normalized.indexOf(
506+
`
507+
const FuncExpr = memo(function FuncExpr(props: Props) {
508+
return <p>{props.foo}</p>
509+
})
510+
`.replace(/\s+/g, ''),
511+
) !== -1,
512+
)
513+
514+
// Check function generators
515+
assert.ok(
516+
normalized.indexOf(
517+
`
518+
const GeneratorFuncExpr: FC<Props> = memo(function* GeneratorFuncExpr(props) {
519+
yield <p>foo</p>
520+
}, arePropsEqual)
521+
`.replace(/\s+/g, ''),
522+
) !== -1,
523+
)
524+
525+
const { status: lint } = spawnSync('eslint', [write], { stdio: 'inherit' })
526+
assert.equal(lint, 0)
527+
const { status: types } = spawnSync(
528+
'tsc',
529+
['--noEmit', '--project', 'test/tsconfig.json'],
530+
{ stdio: 'inherit' },
531+
)
532+
assert.equal(types, 0)
533+
})
339534
})

0 commit comments

Comments
 (0)