@@ -168,127 +168,124 @@ function literalSpecifier(node: unknown): string | undefined {
168168 return undefined ;
169169}
170170
171- function scanDynamicImports ( source : string ) : ImportEdge [ ] {
172- const edges : ImportEdge [ ] = [ ] ;
173- const parsed = parseSync ( 'layering-imports.ts' , source ) ;
174- const destructured = destructuredDynamicImportBindings ( parsed . program ) ;
175- visitAst ( parsed . program , ( node ) => {
176- if ( node . type !== 'ImportExpression' ) return ;
177- const spec = literalSpecifier ( node . source ) ;
178- if ( spec === undefined ) return ;
179- const start = node . start as number | undefined ;
180- const capture = typeof start === 'number' ? destructured . get ( start ) : undefined ;
181- edges . push ( {
182- spec,
183- dynamic : true ,
184- typeOnly : false ,
185- line : sourceLine ( source , start ) ,
186- symbols : capture ? [ ...capture . symbols ] : [ ] ,
187- bindingResidue : capture ?. residue ?? false ,
188- } ) ;
189- } ) ;
190- return edges ;
191- }
192-
193- function scanSideEffectImport ( line : string , lineNo : number ) : ImportEdge | null {
194- const match = / ^ \s * i m p o r t \s + [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / . exec ( line ) ;
195- return match
196- ? {
197- spec : match [ 1 ] ! ,
198- dynamic : false ,
199- typeOnly : false ,
200- line : lineNo ,
201- symbols : [ ] ,
202- bindingResidue : false ,
203- }
204- : null ;
205- }
171+ type LocatedImportEdge = { edge : ImportEdge ; start : number ; order : number } ;
206172
207- function withoutImportComments ( statement : string ) : string {
208- return statement . replaceAll (
209- / ( [ " ' ] ) (?: \\ .| (? ! \1) [ ^ \\ \r \n ] ) * ?\1| \/ \* [ \s \S ] * ?\* \/ | \/ \/ [ ^ \n ] * / g,
210- ( match ) => ( match . startsWith ( '/*' ) ? ' ' : match . startsWith ( '//' ) ? '\n' : match ) ,
211- ) ;
212- }
213-
214- type NamedSpecifier = { name : string ; typeOnly : boolean } ;
215- type ParsedNamedSpecifiers = { index : number ; specifiers : NamedSpecifier [ ] } ;
216-
217- function parseNamedSpecifiers ( statement : string ) : ParsedNamedSpecifiers | null {
218- const named = / \{ ( [ \s \S ] * ?) \} / . exec ( statement ) ;
219- if ( ! named ) return null ;
220-
221- const specifiers : NamedSpecifier [ ] = [ ] ;
222- for ( const specifier of named [ 1 ] ! . split ( ',' ) ) {
223- const trimmed = specifier . trim ( ) ;
224- const typeOnly = / ^ t y p e \b / . test ( trimmed ) ;
225- const sourceName = trimmed . replace ( / ^ t y p e \s + / , '' ) ;
226- const name = / ^ [ A - Z a - z _ $ ] [ \w $ ] * / . exec ( sourceName ) ?. [ 0 ] ;
227- if ( name ) specifiers . push ( { name, typeOnly } ) ;
228- }
229- return { index : named . index , specifiers } ;
173+ function importedName ( node : unknown ) : string | undefined {
174+ if ( node === null || typeof node !== 'object' ) return undefined ;
175+ const record = node as Record < string , unknown > ;
176+ return record . type === 'Identifier' && typeof record . name === 'string' ? record . name : undefined ;
230177}
231178
232- function importedSymbols ( statement : string ) : string [ ] {
233- const parsed = parseNamedSpecifiers ( statement ) ;
234- return [ ...new Set ( parsed ?. specifiers . map ( ( { name } ) => name ) ?? [ ] ) ] ;
179+ function namedSymbols (
180+ specifiers : readonly Record < string , unknown > [ ] ,
181+ importedField : 'imported' | 'local' ,
182+ ) : string [ ] {
183+ const symbols = specifiers . flatMap ( ( specifier ) => {
184+ const name = importedName ( specifier [ importedField ] ) ;
185+ return specifier . type ===
186+ ( importedField === 'imported' ? 'ImportSpecifier' : 'ExportSpecifier' ) && name !== undefined
187+ ? [ name ]
188+ : [ ] ;
189+ } ) ;
190+ return [ ...new Set ( symbols ) ] ;
235191}
236192
237- function statementIsTypeOnly ( statement : string ) : boolean {
238- if ( / ^ \s * (?: i m p o r t | e x p o r t ) \s + t y p e \b / . test ( statement ) ) return true ;
239- const parsed = parseNamedSpecifiers ( statement ) ;
240- if ( ! parsed ) return false ;
241- const prefix = statement
242- . slice ( 0 , parsed . index )
243- . replace ( / ^ \s * (?: i m p o r t | e x p o r t ) \s + / , '' )
244- . trim ( )
245- . replace ( / , $ / , '' )
246- . trim ( ) ;
247- if ( prefix . length > 0 ) return false ;
248- return parsed . specifiers . length > 0 && parsed . specifiers . every ( ( { typeOnly } ) => typeOnly ) ;
193+ function specifierTypeOnly (
194+ declaration : Record < string , unknown > ,
195+ specifiers : readonly Record < string , unknown > [ ] ,
196+ kindField : 'importKind' | 'exportKind' ,
197+ ) : boolean {
198+ if ( declaration [ kindField ] === 'type' ) return true ;
199+ return specifiers . length > 0 && specifiers . every ( ( specifier ) => specifier [ kindField ] === 'type' ) ;
249200}
250201
251- function scanFromImport ( lines : string [ ] , index : number ) : ImportEdge | null {
252- const fromMatch = / (?: ^ | [ \s ; } ] ) f r o m \s + [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / . exec ( lines [ index ] ! ) ;
253- if ( ! fromMatch ) return null ;
254-
255- let start = index ;
256- while ( start >= 0 && ! / ^ \s * (?: i m p o r t | e x p o r t ) \b / . test ( lines [ start ] ! ) ) start -- ;
257- if ( start < 0 ) return null ;
258-
259- const statement = lines . slice ( start , index + 1 ) . join ( '\n' ) ;
260- const normalizedStatement = withoutImportComments ( statement ) ;
202+ function staticImportEdge (
203+ source : string ,
204+ node : Record < string , unknown > ,
205+ specifierNode : unknown ,
206+ typeOnly : boolean ,
207+ symbols : string [ ] ,
208+ ) : ImportEdge | null {
209+ const spec = literalSpecifier ( specifierNode ) ;
210+ if ( spec === undefined ) return null ;
211+ const start = node . start as number | undefined ;
261212 return {
262- spec : fromMatch [ 1 ] ! ,
213+ spec,
263214 dynamic : false ,
264- typeOnly : statementIsTypeOnly ( normalizedStatement ) ,
265- line : start + 1 ,
266- symbols : importedSymbols ( normalizedStatement ) ,
215+ typeOnly,
216+ line : sourceLine ( source , start ) ,
217+ symbols,
267218 bindingResidue : false ,
268219 } ;
269220}
270221
271222export function parseImports ( source : string ) : ImportEdge [ ] {
272- const lines = source . split ( '\n' ) ;
273- const dynamicImports = scanDynamicImports ( source ) ;
274- const dynamicImportsByLine = new Map < number , ImportEdge [ ] > ( ) ;
275- for ( const edge of dynamicImports ) {
276- const lineEdges = dynamicImportsByLine . get ( edge . line ) ?? [ ] ;
277- lineEdges . push ( edge ) ;
278- dynamicImportsByLine . set ( edge . line , lineEdges ) ;
279- }
280- const edges : ImportEdge [ ] = [ ] ;
281- for ( let index = 0 ; index < lines . length ; index ++ ) {
282- edges . push ( ...( dynamicImportsByLine . get ( index + 1 ) ?? [ ] ) ) ;
283- const sideEffect = scanSideEffectImport ( lines [ index ] ! , index + 1 ) ;
284- if ( sideEffect ) {
285- edges . push ( sideEffect ) ;
286- continue ;
223+ const parsed = parseSync ( 'layering-imports.ts' , source ) ;
224+ const destructured = destructuredDynamicImportBindings ( parsed . program ) ;
225+ const located : LocatedImportEdge [ ] = [ ] ;
226+ let order = 0 ;
227+
228+ visitAst ( parsed . program , ( node ) => {
229+ const start = typeof node . start === 'number' ? node . start : 0 ;
230+ if ( node . type === 'ImportExpression' ) {
231+ const spec = literalSpecifier ( node . source ) ;
232+ if ( spec === undefined ) return ;
233+ const capture = destructured . get ( start ) ;
234+ located . push ( {
235+ start,
236+ order : order ++ ,
237+ edge : {
238+ spec,
239+ dynamic : true ,
240+ typeOnly : false ,
241+ line : sourceLine ( source , start ) ,
242+ symbols : capture ? [ ...capture . symbols ] : [ ] ,
243+ bindingResidue : capture ?. residue ?? false ,
244+ } ,
245+ } ) ;
246+ return ;
287247 }
288- const fromImport = scanFromImport ( lines , index ) ;
289- if ( fromImport ) edges . push ( fromImport ) ;
290- }
291- return edges ;
248+
249+ let edge : ImportEdge | null = null ;
250+ if ( node . type === 'ImportDeclaration' ) {
251+ const specifiers = Array . isArray ( node . specifiers )
252+ ? ( node . specifiers as Record < string , unknown > [ ] )
253+ : [ ] ;
254+ edge = staticImportEdge (
255+ source ,
256+ node ,
257+ node . source ,
258+ specifierTypeOnly ( node , specifiers , 'importKind' ) ,
259+ namedSymbols ( specifiers , 'imported' ) ,
260+ ) ;
261+ } else if ( node . type === 'ExportNamedDeclaration' && node . source ) {
262+ const specifiers = Array . isArray ( node . specifiers )
263+ ? ( node . specifiers as Record < string , unknown > [ ] )
264+ : [ ] ;
265+ edge = staticImportEdge (
266+ source ,
267+ node ,
268+ node . source ,
269+ specifierTypeOnly ( node , specifiers , 'exportKind' ) ,
270+ namedSymbols ( specifiers , 'local' ) ,
271+ ) ;
272+ } else if ( node . type === 'ExportAllDeclaration' ) {
273+ edge = staticImportEdge ( source , node , node . source , node . exportKind === 'type' , [ ] ) ;
274+ } else if ( node . type === 'TSImportType' ) {
275+ edge = staticImportEdge ( source , node , node . source , true , [ ] ) ;
276+ }
277+ if ( edge ) located . push ( { edge, start, order : order ++ } ) ;
278+ } ) ;
279+
280+ return located
281+ . sort (
282+ ( left , right ) =>
283+ left . edge . line - right . edge . line ||
284+ Number ( right . edge . dynamic ) - Number ( left . edge . dynamic ) ||
285+ left . start - right . start ||
286+ left . order - right . order ,
287+ )
288+ . map ( ( { edge } ) => edge ) ;
292289}
293290
294291export function topFolder ( file : string ) : string {
0 commit comments