@@ -222,6 +222,182 @@ void test('resolves tsconfig path aliases when mapping component types', () => {
222222 }
223223} )
224224
225+ void test ( 'clear() resets all caches and re-analyses from scratch' , ( ) => {
226+ const project = createProject ( {
227+ 'Page.tsx' : [
228+ "import Button from './Button';" ,
229+ '' ,
230+ 'export default function Page() {' ,
231+ ' return <Button />;' ,
232+ '}' ,
233+ ] . join ( '\n' ) ,
234+ 'Button.tsx' : [
235+ "'use client';" ,
236+ '' ,
237+ 'export default function Button() {' ,
238+ ' return <button />;' ,
239+ '}' ,
240+ ] . join ( '\n' ) ,
241+ } )
242+
243+ try {
244+ const analyzer = createAnalyzer ( project . host )
245+ const filePath = project . filePath ( 'Page.tsx' )
246+ const source = project . readFile ( 'Page.tsx' )
247+ const sig = project . signature ( 'Page.tsx' )
248+
249+ const before = analyzer . analyzeDocument ( filePath , source , sig )
250+ assert . equal ( before . length , 1 )
251+ assert . equal ( before [ 0 ] ?. kind , 'client' )
252+
253+ analyzer . clear ( )
254+
255+ const after = analyzer . analyzeDocument ( filePath , source , sig )
256+ assert . equal ( after . length , 1 )
257+ assert . equal ( after [ 0 ] ?. kind , 'client' )
258+ } finally {
259+ project [ Symbol . dispose ] ( )
260+ }
261+ } )
262+
263+ void test ( 'recognizes forwardRef-wrapped local components' , ( ) => {
264+ const project = createProject ( {
265+ 'Card.tsx' : [
266+ "'use client';" ,
267+ '' ,
268+ 'const Button = forwardRef((props) => <button />);' ,
269+ '' ,
270+ 'export function Card() {' ,
271+ ' return <Button />;' ,
272+ '}' ,
273+ ] . join ( '\n' ) ,
274+ } )
275+
276+ try {
277+ const analyzer = createAnalyzer ( project . host )
278+ const filePath = project . filePath ( 'Card.tsx' )
279+ const usages = analyzer . analyzeDocument (
280+ filePath ,
281+ project . readFile ( 'Card.tsx' ) ,
282+ project . signature ( 'Card.tsx' ) ,
283+ )
284+
285+ assert . equal ( usages . length , 1 )
286+ assert . equal ( usages [ 0 ] ?. kind , 'client' )
287+ assert . equal ( usages [ 0 ] ?. tagName , 'Button' )
288+ } finally {
289+ project [ Symbol . dispose ] ( )
290+ }
291+ } )
292+
293+ void test ( 'resolves namespaced JSX like <UI.Button />' , ( ) => {
294+ const project = createProject ( {
295+ 'Page.tsx' : [
296+ "import * as UI from './ui';" ,
297+ '' ,
298+ 'export default function Page() {' ,
299+ ' return <UI.Button />;' ,
300+ '}' ,
301+ ] . join ( '\n' ) ,
302+ 'ui.tsx' : [
303+ "'use client';" ,
304+ '' ,
305+ 'export function Button() {' ,
306+ ' return <button />;' ,
307+ '}' ,
308+ ] . join ( '\n' ) ,
309+ } )
310+
311+ try {
312+ const analyzer = createAnalyzer ( project . host )
313+ const filePath = project . filePath ( 'Page.tsx' )
314+ const source = project . readFile ( 'Page.tsx' )
315+ const usages = analyzer . analyzeDocument (
316+ filePath ,
317+ source ,
318+ project . signature ( 'Page.tsx' ) ,
319+ )
320+
321+ assert . equal ( usages . length , 1 )
322+ assert . equal ( usages [ 0 ] ?. kind , 'client' )
323+ assert . equal ( usages [ 0 ] ?. tagName , 'UI.Button' )
324+ } finally {
325+ project [ Symbol . dispose ] ( )
326+ }
327+ } )
328+
329+ void test ( 'resolves bare package imports through node_modules' , ( ) => {
330+ const project = createProject ( {
331+ 'Page.tsx' : [
332+ "import Button from 'my-ui';" ,
333+ '' ,
334+ 'export default function Page() {' ,
335+ ' return <Button />;' ,
336+ '}' ,
337+ ] . join ( '\n' ) ,
338+ 'node_modules/my-ui/package.json' : JSON . stringify ( {
339+ name : 'my-ui' ,
340+ main : './index.tsx' ,
341+ } ) ,
342+ 'node_modules/my-ui/index.tsx' : [
343+ "'use client';" ,
344+ '' ,
345+ 'export default function Button() {' ,
346+ ' return <button />;' ,
347+ '}' ,
348+ ] . join ( '\n' ) ,
349+ } )
350+
351+ try {
352+ const analyzer = createAnalyzer ( project . host )
353+ const filePath = project . filePath ( 'Page.tsx' )
354+ const usages = analyzer . analyzeDocument (
355+ filePath ,
356+ project . readFile ( 'Page.tsx' ) ,
357+ project . signature ( 'Page.tsx' ) ,
358+ )
359+
360+ assert . equal ( usages . length , 1 )
361+ assert . equal ( usages [ 0 ] ?. kind , 'client' )
362+ assert . equal ( usages [ 0 ] ?. tagName , 'Button' )
363+ } finally {
364+ project [ Symbol . dispose ] ( )
365+ }
366+ } )
367+
368+ void test ( 'resolves deeply nested namespaced JSX like <UI.Forms.Input />' , ( ) => {
369+ const project = createProject ( {
370+ 'Page.tsx' : [
371+ "import * as UI from './ui';" ,
372+ '' ,
373+ 'export default function Page() {' ,
374+ ' return <UI.Forms.Input />;' ,
375+ '}' ,
376+ ] . join ( '\n' ) ,
377+ 'ui.tsx' : [
378+ "'use client';" ,
379+ '' ,
380+ 'export const Forms = { Input: () => <input /> };' ,
381+ ] . join ( '\n' ) ,
382+ } )
383+
384+ try {
385+ const analyzer = createAnalyzer ( project . host )
386+ const filePath = project . filePath ( 'Page.tsx' )
387+ const usages = analyzer . analyzeDocument (
388+ filePath ,
389+ project . readFile ( 'Page.tsx' ) ,
390+ project . signature ( 'Page.tsx' ) ,
391+ )
392+
393+ assert . equal ( usages . length , 1 )
394+ assert . equal ( usages [ 0 ] ?. kind , 'client' )
395+ assert . equal ( usages [ 0 ] ?. tagName , 'UI.Forms.Input' )
396+ } finally {
397+ project [ Symbol . dispose ] ( )
398+ }
399+ } )
400+
225401function createAnalyzer ( host : SourceHost ) : ComponentLensAnalyzer {
226402 const resolver = new ImportResolver ( host )
227403 return new ComponentLensAnalyzer ( host , resolver )
0 commit comments