importMap.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import { pipe } from '@codama/visitors-core';
  2. import { describe, expect, test } from 'vitest';
  3. import {
  4. addToImportMap,
  5. createImportMap,
  6. importMapToString,
  7. mergeImportMaps,
  8. parseImportInput,
  9. removeFromImportMap,
  10. } from '../../src/utils';
  11. describe('createImportMap', () => {
  12. test('it creates an empty import map', () => {
  13. expect(createImportMap()).toStrictEqual(new Map());
  14. });
  15. });
  16. describe('parseImportInput', () => {
  17. test('it parses simple identifiers', () => {
  18. expect(parseImportInput('myFunction')).toStrictEqual({
  19. importedIdentifier: 'myFunction',
  20. isType: false,
  21. usedIdentifier: 'myFunction',
  22. });
  23. });
  24. test('it parses type-only identifiers', () => {
  25. expect(parseImportInput('type MyType')).toStrictEqual({
  26. importedIdentifier: 'MyType',
  27. isType: true,
  28. usedIdentifier: 'MyType',
  29. });
  30. });
  31. test('it parses aliased identifiers', () => {
  32. expect(parseImportInput('myFunction as myAliasedFunction')).toStrictEqual({
  33. importedIdentifier: 'myFunction',
  34. isType: false,
  35. usedIdentifier: 'myAliasedFunction',
  36. });
  37. });
  38. test('it parses type-only aliased identifiers', () => {
  39. expect(parseImportInput('type MyType as MyAliasedType')).toStrictEqual({
  40. importedIdentifier: 'MyType',
  41. isType: true,
  42. usedIdentifier: 'MyAliasedType',
  43. });
  44. });
  45. test('it parses unrecognised patterns as-is', () => {
  46. expect(parseImportInput('!some weird #input')).toStrictEqual({
  47. importedIdentifier: '!some weird #input',
  48. isType: false,
  49. usedIdentifier: '!some weird #input',
  50. });
  51. });
  52. });
  53. describe('mergeImportMaps', () => {
  54. test('it returns an empty map when merging empty maps', () => {
  55. expect(mergeImportMaps([])).toStrictEqual(new Map());
  56. });
  57. test('it returns the first map as-is when merging a single map', () => {
  58. const map = createImportMap();
  59. expect(mergeImportMaps([map])).toBe(map);
  60. });
  61. test('it add all modules from the merged maps', () => {
  62. const map1 = addToImportMap(createImportMap(), 'module-a', ['import1']);
  63. const map2 = addToImportMap(createImportMap(), 'module-b', ['import2']);
  64. expect(mergeImportMaps([map1, map2])).toStrictEqual(
  65. new Map([
  66. ['module-a', new Map([['import1', parseImportInput('import1')]])],
  67. ['module-b', new Map([['import2', parseImportInput('import2')]])],
  68. ]),
  69. );
  70. });
  71. test('it merges imports from the same module', () => {
  72. const map1 = addToImportMap(createImportMap(), 'module-a', ['import1']);
  73. const map2 = addToImportMap(createImportMap(), 'module-a', ['import2']);
  74. expect(mergeImportMaps([map1, map2])).toStrictEqual(
  75. new Map([
  76. [
  77. 'module-a',
  78. new Map([
  79. ['import1', parseImportInput('import1')],
  80. ['import2', parseImportInput('import2')],
  81. ]),
  82. ],
  83. ]),
  84. );
  85. });
  86. test('it keeps the same import from the same module only once', () => {
  87. const map1 = addToImportMap(createImportMap(), 'module-a', ['import1']);
  88. const map2 = addToImportMap(createImportMap(), 'module-a', ['import1']);
  89. expect(mergeImportMaps([map1, map2])).toStrictEqual(
  90. new Map([['module-a', new Map([['import1', parseImportInput('import1')]])]]),
  91. );
  92. });
  93. test('it keeps multiple instances of the same import when one is aliased', () => {
  94. const map1 = addToImportMap(createImportMap(), 'module-a', ['import1']);
  95. const map2 = addToImportMap(createImportMap(), 'module-a', ['import1 as alias1']);
  96. expect(mergeImportMaps([map1, map2])).toStrictEqual(
  97. new Map([
  98. [
  99. 'module-a',
  100. new Map([
  101. ['import1', parseImportInput('import1')],
  102. ['alias1', parseImportInput('import1 as alias1')],
  103. ]),
  104. ],
  105. ]),
  106. );
  107. });
  108. test('it keeps multiple instances of the same import when both are aliased to different names', () => {
  109. const map1 = addToImportMap(createImportMap(), 'module-a', ['import1 as alias1']);
  110. const map2 = addToImportMap(createImportMap(), 'module-a', ['import1 as alias2']);
  111. expect(mergeImportMaps([map1, map2])).toStrictEqual(
  112. new Map([
  113. [
  114. 'module-a',
  115. new Map([
  116. ['alias1', parseImportInput('import1 as alias1')],
  117. ['alias2', parseImportInput('import1 as alias2')],
  118. ]),
  119. ],
  120. ]),
  121. );
  122. });
  123. test('it keeps the same import from the same module when aliased to the same name', () => {
  124. const map1 = addToImportMap(createImportMap(), 'module-a', ['import1 as alias1']);
  125. const map2 = addToImportMap(createImportMap(), 'module-a', ['import1 as alias1']);
  126. expect(mergeImportMaps([map1, map2])).toStrictEqual(
  127. new Map([['module-a', new Map([['alias1', parseImportInput('import1 as alias1')]])]]),
  128. );
  129. });
  130. test('it keeps the same type-only import from the same module only once', () => {
  131. const map1 = addToImportMap(createImportMap(), 'module-a', ['type import1']);
  132. const map2 = addToImportMap(createImportMap(), 'module-a', ['type import1']);
  133. expect(mergeImportMaps([map1, map2])).toStrictEqual(
  134. new Map([['module-a', new Map([['import1', parseImportInput('type import1')]])]]),
  135. );
  136. });
  137. test('it keeps the same type-only import with the same aliased name from the same module only once', () => {
  138. const map1 = addToImportMap(createImportMap(), 'module-a', ['type import1 as alias1']);
  139. const map2 = addToImportMap(createImportMap(), 'module-a', ['type import1 as alias1']);
  140. expect(mergeImportMaps([map1, map2])).toStrictEqual(
  141. new Map([['module-a', new Map([['alias1', parseImportInput('type import1 as alias1')]])]]),
  142. );
  143. });
  144. test('it only keep the non-type variant instead of the type-only variant when both have the same name', () => {
  145. const map1 = addToImportMap(createImportMap(), 'module-a', ['type import1']);
  146. const map2 = addToImportMap(createImportMap(), 'module-a', ['import1']);
  147. expect(mergeImportMaps([map1, map2])).toStrictEqual(
  148. new Map([['module-a', new Map([['import1', parseImportInput('import1')]])]]),
  149. );
  150. });
  151. test('it only keep the non-type variant instead of the type-only variant when both are aliased to the same name', () => {
  152. const map1 = addToImportMap(createImportMap(), 'module-a', ['type import1 as alias1']);
  153. const map2 = addToImportMap(createImportMap(), 'module-a', ['import1 as alias1']);
  154. expect(mergeImportMaps([map1, map2])).toStrictEqual(
  155. new Map([['module-a', new Map([['alias1', parseImportInput('import1 as alias1')]])]]),
  156. );
  157. });
  158. test('it keeps both the non-type and type-only imports when the former is aliased', () => {
  159. const map1 = addToImportMap(createImportMap(), 'module-a', ['import1 as alias1']);
  160. const map2 = addToImportMap(createImportMap(), 'module-a', ['type import1']);
  161. expect(mergeImportMaps([map1, map2])).toStrictEqual(
  162. new Map([
  163. [
  164. 'module-a',
  165. new Map([
  166. ['alias1', parseImportInput('import1 as alias1')],
  167. ['import1', parseImportInput('type import1')],
  168. ]),
  169. ],
  170. ]),
  171. );
  172. });
  173. test('it keeps both the non-type and type-only imports when the latter is aliased', () => {
  174. const map1 = addToImportMap(createImportMap(), 'module-a', ['import1']);
  175. const map2 = addToImportMap(createImportMap(), 'module-a', ['type import1 as alias1']);
  176. expect(mergeImportMaps([map1, map2])).toStrictEqual(
  177. new Map([
  178. [
  179. 'module-a',
  180. new Map([
  181. ['import1', parseImportInput('import1')],
  182. ['alias1', parseImportInput('type import1 as alias1')],
  183. ]),
  184. ],
  185. ]),
  186. );
  187. });
  188. });
  189. describe('addToImportMap', () => {
  190. test('it adds imports to an empty import map', () => {
  191. expect(
  192. addToImportMap(createImportMap(), 'module-a', ['import1', 'type import2', 'import3 as alias3']),
  193. ).toStrictEqual(
  194. new Map([
  195. [
  196. 'module-a',
  197. new Map([
  198. ['import1', parseImportInput('import1')],
  199. ['import2', parseImportInput('type import2')],
  200. ['alias3', parseImportInput('import3 as alias3')],
  201. ]),
  202. ],
  203. ]),
  204. );
  205. });
  206. test('it adds imports to an existing import map', () => {
  207. expect(
  208. pipe(
  209. createImportMap(),
  210. m => addToImportMap(m, 'module-a', ['import1']),
  211. m => addToImportMap(m, 'module-b', ['import2']),
  212. ),
  213. ).toStrictEqual(
  214. new Map([
  215. ['module-a', new Map([['import1', parseImportInput('import1')]])],
  216. ['module-b', new Map([['import2', parseImportInput('import2')]])],
  217. ]),
  218. );
  219. });
  220. });
  221. describe('removeFromImportMap', () => {
  222. test('it removes type-only imports from an existing import map', () => {
  223. const importMap = addToImportMap(createImportMap(), 'module-a', ['import1', 'type import2']);
  224. expect(removeFromImportMap(importMap, 'module-a', ['import1'])).toStrictEqual(
  225. new Map([['module-a', new Map([['import2', parseImportInput('type import2')]])]]),
  226. );
  227. });
  228. test('it removes type-only imports from an existing import map', () => {
  229. const importMap = addToImportMap(createImportMap(), 'module-a', ['import1', 'type import2']);
  230. expect(removeFromImportMap(importMap, 'module-a', ['import2'])).toStrictEqual(
  231. new Map([['module-a', new Map([['import1', parseImportInput('import1')]])]]),
  232. );
  233. });
  234. test('it removes aliased imports from an existing import map using the aliased name', () => {
  235. const importMap = addToImportMap(createImportMap(), 'module-a', ['import1', 'import2 as alias2']);
  236. expect(removeFromImportMap(importMap, 'module-a', ['alias2'])).toStrictEqual(
  237. new Map([['module-a', new Map([['import1', parseImportInput('import1')]])]]),
  238. );
  239. });
  240. test('it removes multiple imports from the same module', () => {
  241. const importMap = addToImportMap(createImportMap(), 'module-a', [
  242. 'import1',
  243. 'import2 as alias2',
  244. 'type import3',
  245. ]);
  246. expect(removeFromImportMap(importMap, 'module-a', ['alias2', 'import3'])).toStrictEqual(
  247. new Map([['module-a', new Map([['import1', parseImportInput('import1')]])]]),
  248. );
  249. });
  250. test('it removes the module map when it is empty', () => {
  251. const importMap = addToImportMap(createImportMap(), 'module-a', ['import1', 'import2']);
  252. expect(removeFromImportMap(importMap, 'module-a', ['import1', 'import2'])).toStrictEqual(new Map());
  253. });
  254. });
  255. describe('importMapToString', () => {
  256. test('it converts an import map to a valid import statement', () => {
  257. const importMap = addToImportMap(createImportMap(), 'module-a', ['import1', 'import2', 'import3']);
  258. expect(importMapToString(importMap)).toBe("import { import1, import2, import3 } from 'module-a';");
  259. });
  260. test('it supports type-only import statements', () => {
  261. const importMap = addToImportMap(createImportMap(), 'module-a', ['type import1']);
  262. expect(importMapToString(importMap)).toBe("import { type import1 } from 'module-a';");
  263. });
  264. test('it supports aliased import statements', () => {
  265. const importMap = addToImportMap(createImportMap(), 'module-a', ['import1 as alias1']);
  266. expect(importMapToString(importMap)).toBe("import { import1 as alias1 } from 'module-a';");
  267. });
  268. test('it supports aliased type-only import statements', () => {
  269. const importMap = addToImportMap(createImportMap(), 'module-a', ['type import1 as alias1']);
  270. expect(importMapToString(importMap)).toBe("import { type import1 as alias1 } from 'module-a';");
  271. });
  272. test('it orders import items alphabetically', () => {
  273. const importMap = addToImportMap(createImportMap(), 'module-a', ['import3', 'import1', 'import2']);
  274. expect(importMapToString(importMap)).toBe("import { import1, import2, import3 } from 'module-a';");
  275. });
  276. test('it orders import statements alphabetically', () => {
  277. const importMap = pipe(
  278. createImportMap(),
  279. m => addToImportMap(m, 'module-b', ['import2']),
  280. m => addToImportMap(m, 'module-a', ['import1']),
  281. );
  282. expect(importMapToString(importMap)).toBe(
  283. "import { import1 } from 'module-a';\nimport { import2 } from 'module-b';",
  284. );
  285. });
  286. test('it places absolute imports before relative imports', () => {
  287. const importMap = pipe(
  288. createImportMap(),
  289. m => addToImportMap(m, './relative-module', ['import1']),
  290. m => addToImportMap(m, 'absolute-module', ['import2']),
  291. );
  292. expect(importMapToString(importMap)).toBe(
  293. "import { import2 } from 'absolute-module';\nimport { import1 } from './relative-module';",
  294. );
  295. });
  296. test('it replaces internal modules with their actual paths', () => {
  297. const importMap = addToImportMap(createImportMap(), 'generatedAccounts', ['myAccount']);
  298. expect(importMapToString(importMap)).toBe("import { myAccount } from '../accounts';");
  299. });
  300. test('it can override the paths of internal modules', () => {
  301. const importMap = addToImportMap(createImportMap(), 'generatedAccounts', ['myAccount']);
  302. expect(importMapToString(importMap, { generatedAccounts: '.' })).toBe("import { myAccount } from '.';");
  303. });
  304. test('it replaces placeholder kit packages with their @solana/kit', () => {
  305. const importMap = addToImportMap(createImportMap(), 'solanaAddresses', ['type Address']);
  306. expect(importMapToString(importMap)).toBe("import { type Address } from '@solana/kit';");
  307. });
  308. test('it can use granular packages when replacing placeholder kit packages', () => {
  309. const importMap = addToImportMap(createImportMap(), 'solanaAddresses', ['type Address']);
  310. expect(importMapToString(importMap, {}, true)).toBe("import { type Address } from '@solana/addresses';");
  311. });
  312. test('it can override the module of placeholder kit packages', () => {
  313. const importMap = addToImportMap(createImportMap(), 'solanaAddresses', ['type Address']);
  314. expect(importMapToString(importMap, { solanaAddresses: '@acme/solana-addresses' })).toBe(
  315. "import { type Address } from '@acme/solana-addresses';",
  316. );
  317. });
  318. });