1+ import type { AnalyzedFeature , FeatureType } from '../reporters/types'
12import { existsSync } from 'node:fs'
23import { readFile } from 'node:fs/promises'
34import { createRequire } from 'node:module'
5+ import { dirname , join } from 'pathe'
6+ import { resolvePackageJSON } from 'pkg-types'
47import { glob } from 'tinyglobby'
58import { parse } from 'vue-eslint-parser'
69
710const require = createRequire ( import . meta. url )
811
12+ async function loadImportMap ( cwd : string , targetPackage : string ) {
13+ try {
14+ const pkgPath = await resolvePackageJSON ( targetPackage , { url : cwd } )
15+ const pkgRoot = dirname ( pkgPath )
16+ const mapPath = join ( pkgRoot , 'dist/json/importMap.json' )
17+ if ( existsSync ( mapPath ) ) {
18+ const content = await readFile ( mapPath , 'utf8' )
19+ return JSON . parse ( content )
20+ }
21+ } catch ( error ) {
22+ console . warn ( 'Failed to load importMap.json' , error )
23+ }
24+ return null
25+ }
26+
27+ function getFeatureType ( name : string , isType = false , importMap ?: any ) : FeatureType {
28+ if ( isType ) {
29+ return 'type'
30+ }
31+ if ( importMap ?. components ?. [ name ] ) {
32+ return 'component'
33+ }
34+ if ( name . startsWith ( 'use' ) && name . length > 3 && name . at ( 3 ) ?. match ( / [ A - Z ] / ) ) {
35+ return 'composable'
36+ }
37+ if ( / ^ c r e a t e .* P l u g i n $ / . test ( name ) ) {
38+ return 'plugin'
39+ }
40+ if ( / ^ [ A - Z ] [ A - Z 0 - 9 _ ] * $ / . test ( name ) ) {
41+ return 'constant'
42+ }
43+ if ( / ^ [ A - Z ] / . test ( name ) ) {
44+ return 'component'
45+ }
46+ return 'util'
47+ }
48+
949function walk ( node : any , callback : ( node : any , parent : any ) => void , parent ?: any ) {
1050 if ( ! node || typeof node !== 'object' ) {
1151 return
@@ -35,20 +75,38 @@ export function analyzeCode (code: string, targetPackage = '@vuetify/v0') {
3575 parser : require . resolve ( '@typescript-eslint/parser' ) ,
3676 } )
3777
38- const found = new Set < string > ( )
39- const importedFromVuetify = new Set < string > ( )
78+ const found = new Map < string , { isType : boolean } > ( )
4079
4180 if ( ast . body ) {
81+ // eslint-disable-next-line complexity
4282 walk ( ast , ( node , parent ) => {
4383 // Static imports: import { X } from 'pkg'
4484 if ( node . type === 'ImportDeclaration' && typeof node . source . value === 'string' && ( node . source . value === targetPackage || node . source . value . startsWith ( `${ targetPackage } /` ) ) ) {
85+ const isDeclType = node . importKind === 'type'
4586 for ( const spec of node . specifiers ) {
87+ const isSpecType = spec . importKind === 'type'
88+ const isType = isDeclType || isSpecType
89+
4690 if ( spec . type === 'ImportSpecifier' && 'name' in spec . imported ) {
47- found . add ( spec . imported . name )
48- importedFromVuetify . add ( spec . local . name )
91+ const name = spec . imported . name
92+ const current = found . get ( name )
93+ if ( current ) {
94+ if ( ! isType ) {
95+ current . isType = false
96+ }
97+ } else {
98+ found . set ( name , { isType } )
99+ }
49100 } else if ( spec . type === 'ImportDefaultSpecifier' ) {
50- found . add ( 'default' )
51- importedFromVuetify . add ( spec . local . name )
101+ const name = 'default'
102+ const current = found . get ( name )
103+ if ( current ) {
104+ if ( ! isType ) {
105+ current . isType = false
106+ }
107+ } else {
108+ found . set ( name , { isType } )
109+ }
52110 }
53111 }
54112 }
@@ -59,10 +117,25 @@ export function analyzeCode (code: string, targetPackage = '@vuetify/v0') {
59117 && parent ?. type === 'MemberExpression' && parent . object === node ) {
60118 if ( parent . property . type === 'Identifier' && ! parent . computed ) {
61119 // .Prop
62- found . add ( parent . property . name )
120+ if ( parent . property . name === 'then' ) {
121+ return
122+ }
123+ const name = parent . property . name
124+ const current = found . get ( name )
125+ if ( current ) {
126+ current . isType = false
127+ } else {
128+ found . set ( name , { isType : false } )
129+ }
63130 } else if ( parent . property . type === 'Literal' ) {
64131 // ['Prop']
65- found . add ( parent . property . value )
132+ const name = parent . property . value
133+ const current = found . get ( name )
134+ if ( current ) {
135+ current . isType = false
136+ } else {
137+ found . set ( name , { isType : false } )
138+ }
66139 }
67140 }
68141 // Case 2: (await import('pkg')).Prop
@@ -81,42 +154,67 @@ export function analyzeCode (code: string, targetPackage = '@vuetify/v0') {
81154 const source = node . object . argument . source . value
82155 if ( source === targetPackage ) {
83156 if ( node . property . type === 'Identifier' && ! node . computed ) {
84- found . add ( node . property . name )
157+ const name = node . property . name
158+ const current = found . get ( name )
159+ if ( current ) {
160+ current . isType = false
161+ } else {
162+ found . set ( name , { isType : false } )
163+ }
85164 } else if ( node . property . type === 'Literal' ) {
86- found . add ( node . property . value )
165+ const name = node . property . value
166+ const current = found . get ( name )
167+ if ( current ) {
168+ current . isType = false
169+ } else {
170+ found . set ( name , { isType : false } )
171+ }
87172 }
88173 }
89174 }
90175 } )
91176 }
92177
93- return Array . from ( found )
178+ return found
94179}
95180
96- export async function analyzeProject ( cwd : string = process . cwd ( ) , targetPackage = '@vuetify/v0' ) {
181+ export async function analyzeProject ( cwd : string = process . cwd ( ) , targetPackage = '@vuetify/v0' ) : Promise < AnalyzedFeature [ ] > {
97182 if ( ! existsSync ( cwd ) ) {
98183 throw new Error ( `Directory ${ cwd } does not exist` )
99184 }
100185
101- const files = await glob ( [ '**/*.{vue,ts,js,tsx,jsx}' ] , {
102- cwd,
103- ignore : [ '**/node_modules/**' , '**/dist/**' , '**/.git/**' ] ,
104- absolute : true ,
105- } )
186+ const [ files , importMap ] = await Promise . all ( [
187+ glob ( [ '**/*.{vue,ts,js,tsx,jsx}' ] , {
188+ cwd,
189+ ignore : [ '**/node_modules/**' , '**/dist/**' , '**/.git/**' ] ,
190+ absolute : true ,
191+ } ) ,
192+ loadImportMap ( cwd , targetPackage ) ,
193+ ] )
106194
107- const features = new Set < string > ( )
195+ const features = new Map < string , { isType : boolean } > ( )
108196
109197 for ( const file of files ) {
110198 try {
111199 const code = await readFile ( file , 'utf8' )
112200 const fileFeatures = analyzeCode ( code , targetPackage )
113- for ( const feature of fileFeatures ) {
114- features . add ( feature )
201+ for ( const [ name , info ] of fileFeatures ) {
202+ const current = features . get ( name )
203+ if ( current ) {
204+ if ( ! info . isType ) {
205+ current . isType = false
206+ }
207+ } else {
208+ features . set ( name , { isType : info . isType } )
209+ }
115210 }
116211 } catch {
117212 // console.warn(`Failed to analyze ${file}:`, error)
118213 }
119214 }
120215
121- return Array . from ( features ) . toSorted ( )
216+ return Array . from ( features . keys ( ) ) . toSorted ( ) . map ( name => ( {
217+ name,
218+ type : getFeatureType ( name , features . get ( name ) ?. isType , importMap ) ,
219+ } ) )
122220}
0 commit comments