@@ -42,109 +42,128 @@ const DEFAULT_INTERNAL_MODULE_MAP: Record<string, string> = {
4242 types : '../types' ,
4343} ;
4444
45- export class ImportMap {
46- protected readonly _imports : Map < string , Set < string > > = new Map ( ) ;
45+ type ImportInput = string ;
46+ type Module = string ;
47+ type UsedIdentifier = string ;
48+ type ImportInfo = Readonly < {
49+ importedIdentifier : string ;
50+ isType : boolean ;
51+ usedIdentifier : UsedIdentifier ;
52+ } > ;
4753
48- protected readonly _aliases : Map < string , Record < string , string > > = new Map ( ) ;
54+ export type ImportMap = ReadonlyMap < Module , ReadonlyMap < UsedIdentifier , ImportInfo > > ;
4955
50- add ( module : string , imports : Set < string > | string [ ] | string ) : ImportMap {
51- const newImports = new Set ( typeof imports === 'string' ? [ imports ] : imports ) ;
52- if ( newImports . size === 0 ) return this ;
53- const currentImports = this . _imports . get ( module ) ?? new Set ( ) ;
54- newImports . forEach ( i => currentImports . add ( i ) ) ;
55- this . _imports . set ( module , currentImports ) ;
56- return this ;
57- }
56+ export function createImportMap ( ) : ImportMap {
57+ return Object . freeze ( new Map ( ) ) ;
58+ }
5859
59- remove ( module : string , imports : Set < string > | string [ ] | string ) : ImportMap {
60- const importsToRemove = new Set ( typeof imports === 'string' ? [ imports ] : imports ) ;
61- if ( importsToRemove . size === 0 ) return this ;
62- const currentImports = this . _imports . get ( module ) ?? new Set ( ) ;
63- importsToRemove . forEach ( i => currentImports . delete ( i ) ) ;
64- if ( currentImports . size === 0 ) {
65- this . _imports . delete ( module ) ;
66- } else {
67- this . _imports . set ( module , currentImports ) ;
68- }
69- return this ;
70- }
60+ export function parseImportInput ( input : ImportInput ) : ImportInfo {
61+ const matches = input . match ( / ^ ( t y p e ) ? ( [ ^ ] + ) (?: a s ( .+ ) ) ? $ / ) ;
62+ if ( ! matches ) return Object . freeze ( { importedIdentifier : input , isType : false , usedIdentifier : input } ) ;
7163
72- mergeWith ( ...others : ImportMap [ ] ) : ImportMap {
73- others . forEach ( other => {
74- other . _imports . forEach ( ( imports , module ) => {
75- this . add ( module , imports ) ;
76- } ) ;
77- other . _aliases . forEach ( ( aliases , module ) => {
78- Object . entries ( aliases ) . forEach ( ( [ name , alias ] ) => {
79- this . addAlias ( module , name , alias ) ;
80- } ) ;
81- } ) ;
82- } ) ;
83- return this ;
84- }
64+ const [ _ , isType , name , alias ] = matches ;
65+ return Object . freeze ( {
66+ importedIdentifier : name ,
67+ isType : ! ! isType ,
68+ usedIdentifier : alias ?? name ,
69+ } ) ;
70+ }
71+
72+ export function addToImportMap ( importMap : ImportMap , module : Module , imports : ImportInput [ ] ) : ImportMap {
73+ const parsedImports = imports . map ( parseImportInput ) . map ( i => [ i . usedIdentifier , i ] as const ) ;
74+ return mergeImportMaps ( [ importMap , new Map ( [ [ module , new Map ( parsedImports ) ] ] ) ] ) ;
75+ }
8576
86- addAlias ( module : string , name : string , alias : string ) : ImportMap {
87- const currentAliases = this . _aliases . get ( module ) ?? { } ;
88- currentAliases [ name ] = alias ;
89- this . _aliases . set ( module , currentAliases ) ;
90- return this ;
77+ export function removeFromImportMap (
78+ importMap : ImportMap ,
79+ module : Module ,
80+ usedIdentifiers : UsedIdentifier [ ] ,
81+ ) : ImportMap {
82+ const newMap = new Map ( importMap ) ;
83+ const newModuleMap = new Map ( newMap . get ( module ) ) ;
84+ usedIdentifiers . forEach ( usedIdentifier => {
85+ newModuleMap . delete ( usedIdentifier ) ;
86+ } ) ;
87+ if ( newModuleMap . size === 0 ) {
88+ newMap . delete ( module ) ;
89+ } else {
90+ newMap . set ( module , newModuleMap ) ;
9191 }
92+ return Object . freeze ( newMap ) ;
93+ }
9294
93- isEmpty ( ) : boolean {
94- return this . _imports . size === 0 ;
95+ export function mergeImportMaps ( importMaps : ImportMap [ ] ) : ImportMap {
96+ if ( importMaps . length === 0 ) return createImportMap ( ) ;
97+ if ( importMaps . length === 1 ) return importMaps [ 0 ] ;
98+ const mergedMap = new Map ( importMaps [ 0 ] ) ;
99+ for ( const map of importMaps . slice ( 1 ) ) {
100+ for ( const [ module , imports ] of map ) {
101+ const mergedModuleMap = ( mergedMap . get ( module ) ?? new Map ( ) ) as Map < UsedIdentifier , ImportInfo > ;
102+ for ( const [ usedIdentifier , importInfo ] of imports ) {
103+ const existingImportInfo = mergedModuleMap . get ( usedIdentifier ) ;
104+ // If two identical imports exist such that
105+ // one is a type import and the other is not,
106+ // then we must only keep the non-type import.
107+ const shouldOverwriteTypeOnly =
108+ existingImportInfo &&
109+ existingImportInfo . importedIdentifier === importInfo . importedIdentifier &&
110+ existingImportInfo . isType &&
111+ ! importInfo . isType ;
112+ if ( ! existingImportInfo || shouldOverwriteTypeOnly ) {
113+ mergedModuleMap . set ( usedIdentifier , importInfo ) ;
114+ }
115+ }
116+ mergedMap . set ( module , mergedModuleMap ) ;
117+ }
95118 }
119+ return Object . freeze ( mergedMap ) ;
120+ }
121+
122+ export function importMapToString (
123+ importMap : ImportMap ,
124+ dependencyMap : Record < string , string > = { } ,
125+ useGranularImports = false ,
126+ ) : string {
127+ const resolvedMap = resolveImportMapModules ( importMap , dependencyMap , useGranularImports ) ;
96128
97- resolve ( dependencies : Record < string , string > = { } , useGranularImports = false ) : Map < string , Set < string > > {
98- // Resolve aliases.
99- const aliasedMap = new Map < string , Set < string > > (
100- [ ...this . _imports . entries ( ) ] . map ( ( [ module , imports ] ) => {
101- const aliasMap = this . _aliases . get ( module ) ?? { } ;
102- const joinedImports = [ ...imports ] . map ( i => ( aliasMap [ i ] ? `${ i } as ${ aliasMap [ i ] } ` : i ) ) ;
103- return [ module , new Set ( joinedImports ) ] ;
104- } ) ,
105- ) ;
129+ return [ ...resolvedMap . entries ( ) ]
130+ . sort ( ( [ a ] , [ b ] ) => {
131+ const relative = Number ( a . startsWith ( '.' ) ) - Number ( b . startsWith ( '.' ) ) ;
132+ // Relative imports go last.
133+ if ( relative !== 0 ) return relative ;
134+ // Otherwise, sort alphabetically.
135+ return a . localeCompare ( b ) ;
136+ } )
137+ . map ( ( [ module , imports ] ) => {
138+ const innerImports = [ ...imports . values ( ) ]
139+ . map ( importInfoToString )
140+ . sort ( ( a , b ) => a . localeCompare ( b ) )
141+ . join ( ', ' ) ;
142+ return `import { ${ innerImports } } from '${ module } ';` ;
143+ } )
144+ . join ( '\n' ) ;
145+ }
106146
107- // Resolve dependency mappings.
108- const dependencyMap = {
109- ...( useGranularImports ? DEFAULT_GRANULAR_EXTERNAL_MODULE_MAP : DEFAULT_EXTERNAL_MODULE_MAP ) ,
110- ...DEFAULT_INTERNAL_MODULE_MAP ,
111- ...dependencies ,
112- } ;
113- const resolvedMap = new Map < string , Set < string > > ( ) ;
114- aliasedMap . forEach ( ( imports , module ) => {
115- const resolvedModule : string = dependencyMap [ module ] ?? module ;
116- const currentImports = resolvedMap . get ( resolvedModule ) ?? new Set ( ) ;
117- imports . forEach ( i => currentImports . add ( i ) ) ;
118- resolvedMap . set ( resolvedModule , currentImports ) ;
119- } ) ;
147+ function resolveImportMapModules (
148+ importMap : ImportMap ,
149+ dependencyMap : Record < string , string > ,
150+ useGranularImports : boolean ,
151+ ) : ImportMap {
152+ const dependencyMapWithDefaults = {
153+ ...( useGranularImports ? DEFAULT_GRANULAR_EXTERNAL_MODULE_MAP : DEFAULT_EXTERNAL_MODULE_MAP ) ,
154+ ...DEFAULT_INTERNAL_MODULE_MAP ,
155+ ...dependencyMap ,
156+ } ;
120157
121- return resolvedMap ;
122- }
158+ return mergeImportMaps (
159+ [ ...importMap . entries ( ) ] . map ( ( [ module , imports ] ) => {
160+ const resolvedModule = dependencyMapWithDefaults [ module ] ?? module ;
161+ return new Map ( [ [ resolvedModule , imports ] ] ) ;
162+ } ) ,
163+ ) ;
164+ }
123165
124- toString ( dependencies : Record < string , string > = { } , useGranularImports = false ) : string {
125- return [ ...this . resolve ( dependencies , useGranularImports ) . entries ( ) ]
126- . sort ( ( [ a ] , [ b ] ) => {
127- const aIsRelative = a . startsWith ( '.' ) ;
128- const bIsRelative = b . startsWith ( '.' ) ;
129- if ( aIsRelative && ! bIsRelative ) return 1 ;
130- if ( ! aIsRelative && bIsRelative ) return - 1 ;
131- return a . localeCompare ( b ) ;
132- } )
133- . map ( ( [ module , imports ] ) => {
134- const joinedImports = [ ...imports ]
135- . sort ( )
136- . filter ( i => {
137- // import of a type can either be '<Type>' or 'type <Type>', so
138- // we filter out 'type <Type>' variation if there is a '<Type>'
139- const name = i . split ( ' ' ) ;
140- if ( name . length > 1 ) {
141- return ! imports . has ( name [ 1 ] ) ;
142- }
143- return true ;
144- } )
145- . join ( ', ' ) ;
146- return `import { ${ joinedImports } } from '${ module } ';` ;
147- } )
148- . join ( '\n' ) ;
149- }
166+ function importInfoToString ( { importedIdentifier, isType, usedIdentifier } : ImportInfo ) : string {
167+ const alias = importedIdentifier !== usedIdentifier ? ` as ${ usedIdentifier } ` : '' ;
168+ return `${ isType ? 'type ' : '' } ${ importedIdentifier } ${ alias } ` ;
150169}
0 commit comments