@reallukemanning/folio 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/LICENSE +21 -0
  2. package/copy-components.js +31 -0
  3. package/coverage/base.css +224 -0
  4. package/coverage/block-navigation.js +87 -0
  5. package/coverage/core/copy-components.js.html +178 -0
  6. package/coverage/core/eslint.config.js.html +184 -0
  7. package/coverage/core/index.html +146 -0
  8. package/coverage/core/src/__tests__/benchmarks/ProjectCard.bench.tsx.html +364 -0
  9. package/coverage/core/src/__tests__/benchmarks/ProjectView.bench.tsx.html +484 -0
  10. package/coverage/core/src/__tests__/benchmarks/github.bench.ts.html +244 -0
  11. package/coverage/core/src/__tests__/benchmarks/index.html +191 -0
  12. package/coverage/core/src/__tests__/benchmarks/npm.bench.ts.html +271 -0
  13. package/coverage/core/src/__tests__/benchmarks/product-hunt.bench.ts.html +259 -0
  14. package/coverage/core/src/__tests__/benchmarks/utilities.bench.ts.html +478 -0
  15. package/coverage/core/src/components/FeaturedProject.test.tsx.html +697 -0
  16. package/coverage/core/src/components/FeaturedProject.tsx.html +163 -0
  17. package/coverage/core/src/components/ProjectCard/ProjectCard.test.tsx.html +928 -0
  18. package/coverage/core/src/components/ProjectCard/ProjectCard.tsx.html +379 -0
  19. package/coverage/core/src/components/ProjectCard/index.html +146 -0
  20. package/coverage/core/src/components/ProjectCard/index.ts.html +88 -0
  21. package/coverage/core/src/components/ProjectGrid/ProjectGrid.test.tsx.html +292 -0
  22. package/coverage/core/src/components/ProjectGrid/ProjectGrid.tsx.html +103 -0
  23. package/coverage/core/src/components/ProjectGrid/index.html +146 -0
  24. package/coverage/core/src/components/ProjectGrid/index.ts.html +88 -0
  25. package/coverage/core/src/components/ProjectList/ProjectList.test.tsx.html +292 -0
  26. package/coverage/core/src/components/ProjectList/ProjectList.tsx.html +103 -0
  27. package/coverage/core/src/components/ProjectList/index.html +146 -0
  28. package/coverage/core/src/components/ProjectList/index.ts.html +88 -0
  29. package/coverage/core/src/components/ProjectView/ProjectView.test.tsx.html +1108 -0
  30. package/coverage/core/src/components/ProjectView/ProjectView.tsx.html +589 -0
  31. package/coverage/core/src/components/ProjectView/index.html +146 -0
  32. package/coverage/core/src/components/ProjectView/index.ts.html +88 -0
  33. package/coverage/core/src/components/index.html +146 -0
  34. package/coverage/core/src/components/index.ts.html +97 -0
  35. package/coverage/core/src/index.html +116 -0
  36. package/coverage/core/src/index.ts.html +94 -0
  37. package/coverage/core/src/lib/__tests__/defineProjects.test.ts.html +421 -0
  38. package/coverage/core/src/lib/__tests__/filterByFeatured.test.ts.html +523 -0
  39. package/coverage/core/src/lib/__tests__/filterByStatus.test.ts.html +664 -0
  40. package/coverage/core/src/lib/__tests__/filterByType.test.ts.html +631 -0
  41. package/coverage/core/src/lib/__tests__/github.test.ts.html +1783 -0
  42. package/coverage/core/src/lib/__tests__/hybrid-config.test.ts.html +1345 -0
  43. package/coverage/core/src/lib/__tests__/index.html +311 -0
  44. package/coverage/core/src/lib/__tests__/normalise.test.ts.html +1585 -0
  45. package/coverage/core/src/lib/__tests__/npm-config.test.ts.html +385 -0
  46. package/coverage/core/src/lib/__tests__/npm.test.ts.html +1135 -0
  47. package/coverage/core/src/lib/__tests__/product-hunt-config.test.ts.html +397 -0
  48. package/coverage/core/src/lib/__tests__/product-hunt.test.ts.html +505 -0
  49. package/coverage/core/src/lib/__tests__/sortByDate.test.ts.html +751 -0
  50. package/coverage/core/src/lib/__tests__/sortByName.test.ts.html +832 -0
  51. package/coverage/core/src/lib/__tests__/sortByStars.test.ts.html +703 -0
  52. package/coverage/core/src/lib/defineProjects.ts.html +100 -0
  53. package/coverage/core/src/lib/filterByFeatured.ts.html +133 -0
  54. package/coverage/core/src/lib/filterByStatus.ts.html +145 -0
  55. package/coverage/core/src/lib/filterByType.ts.html +133 -0
  56. package/coverage/core/src/lib/github.ts.html +517 -0
  57. package/coverage/core/src/lib/index.html +281 -0
  58. package/coverage/core/src/lib/index.ts.html +130 -0
  59. package/coverage/core/src/lib/normalise.ts.html +868 -0
  60. package/coverage/core/src/lib/npm.ts.html +199 -0
  61. package/coverage/core/src/lib/product-hunt.ts.html +256 -0
  62. package/coverage/core/src/lib/sortByDate.ts.html +175 -0
  63. package/coverage/core/src/lib/sortByName.ts.html +172 -0
  64. package/coverage/core/src/lib/sortByStars.ts.html +172 -0
  65. package/coverage/core/src/types/index.html +116 -0
  66. package/coverage/core/src/types/index.ts.html +517 -0
  67. package/coverage/core/vitest.config.ts.html +178 -0
  68. package/coverage/coverage-final.json +53 -0
  69. package/coverage/coverage-summary.json +54 -0
  70. package/coverage/favicon.png +0 -0
  71. package/coverage/index.html +266 -0
  72. package/coverage/prettify.css +1 -0
  73. package/coverage/prettify.js +2 -0
  74. package/coverage/sort-arrow-sprite.png +0 -0
  75. package/coverage/sorter.js +210 -0
  76. package/dist/cli-components/FeaturedProject/FeaturedProject.d.ts +6 -0
  77. package/dist/cli-components/FeaturedProject/FeaturedProject.d.ts.map +1 -0
  78. package/dist/cli-components/FeaturedProject/FeaturedProject.js +9 -0
  79. package/dist/cli-components/FeaturedProject/FeaturedProject.js.map +1 -0
  80. package/dist/cli-components/FeaturedProject/FeaturedProject.tsx +54 -0
  81. package/dist/cli-components/FeaturedProject/index.d.ts +3 -0
  82. package/dist/cli-components/FeaturedProject/index.d.ts.map +1 -0
  83. package/dist/cli-components/FeaturedProject/index.js +2 -0
  84. package/dist/cli-components/FeaturedProject/index.js.map +1 -0
  85. package/dist/cli-components/FeaturedProject/index.ts +2 -0
  86. package/dist/cli-components/ProjectCard/ProjectCard.d.ts +26 -0
  87. package/dist/cli-components/ProjectCard/ProjectCard.d.ts.map +1 -0
  88. package/dist/cli-components/ProjectCard/ProjectCard.js +33 -0
  89. package/dist/cli-components/ProjectCard/ProjectCard.js.map +1 -0
  90. package/dist/cli-components/ProjectCard/ProjectCard.tsx +90 -0
  91. package/dist/cli-components/ProjectCard/index.d.ts +3 -0
  92. package/dist/cli-components/ProjectCard/index.d.ts.map +1 -0
  93. package/dist/cli-components/ProjectCard/index.js +2 -0
  94. package/dist/cli-components/ProjectCard/index.js.map +1 -0
  95. package/dist/cli-components/ProjectCard/index.ts +2 -0
  96. package/dist/cli-components/ProjectGrid/ProjectGrid.d.ts +5 -0
  97. package/dist/cli-components/ProjectGrid/ProjectGrid.d.ts.map +1 -0
  98. package/dist/cli-components/ProjectGrid/ProjectGrid.js +8 -0
  99. package/dist/cli-components/ProjectGrid/ProjectGrid.js.map +1 -0
  100. package/dist/cli-components/ProjectGrid/ProjectGrid.tsx +6 -0
  101. package/dist/cli-components/ProjectGrid/index.d.ts +3 -0
  102. package/dist/cli-components/ProjectGrid/index.d.ts.map +1 -0
  103. package/dist/cli-components/ProjectGrid/index.js +2 -0
  104. package/dist/cli-components/ProjectGrid/index.js.map +1 -0
  105. package/dist/cli-components/ProjectGrid/index.ts +2 -0
  106. package/dist/cli-components/ProjectList/ProjectList.d.ts +5 -0
  107. package/dist/cli-components/ProjectList/ProjectList.d.ts.map +1 -0
  108. package/dist/cli-components/ProjectList/ProjectList.js +8 -0
  109. package/dist/cli-components/ProjectList/ProjectList.js.map +1 -0
  110. package/dist/cli-components/ProjectList/ProjectList.tsx +6 -0
  111. package/dist/cli-components/ProjectList/index.d.ts +3 -0
  112. package/dist/cli-components/ProjectList/index.d.ts.map +1 -0
  113. package/dist/cli-components/ProjectList/index.js +2 -0
  114. package/dist/cli-components/ProjectList/index.js.map +1 -0
  115. package/dist/cli-components/ProjectList/index.ts +2 -0
  116. package/dist/cli-components/ProjectView/ProjectView.d.ts +17 -0
  117. package/dist/cli-components/ProjectView/ProjectView.d.ts.map +1 -0
  118. package/dist/cli-components/ProjectView/ProjectView.js +39 -0
  119. package/dist/cli-components/ProjectView/ProjectView.js.map +1 -0
  120. package/dist/cli-components/ProjectView/ProjectView.tsx +117 -0
  121. package/dist/cli-components/ProjectView/index.d.ts +3 -0
  122. package/dist/cli-components/ProjectView/index.d.ts.map +1 -0
  123. package/dist/cli-components/ProjectView/index.js +2 -0
  124. package/dist/cli-components/ProjectView/index.js.map +1 -0
  125. package/dist/cli-components/ProjectView/index.ts +2 -0
  126. package/dist/cli-components/types.d.ts +52 -0
  127. package/dist/cli-components/types.d.ts.map +1 -0
  128. package/dist/cli-components/types.js +2 -0
  129. package/dist/cli-components/types.js.map +1 -0
  130. package/dist/cli-components/types.ts +58 -0
  131. package/dist/cli-types.d.ts +52 -0
  132. package/dist/cli-types.d.ts.map +1 -0
  133. package/dist/cli-types.js +2 -0
  134. package/dist/cli-types.js.map +1 -0
  135. package/dist/cli.d.ts +3 -0
  136. package/dist/cli.d.ts.map +1 -0
  137. package/dist/cli.js +21 -0
  138. package/dist/cli.js.map +1 -0
  139. package/dist/commands/add.d.ts +2 -0
  140. package/dist/commands/add.d.ts.map +1 -0
  141. package/dist/commands/add.js +166 -0
  142. package/dist/commands/add.js.map +1 -0
  143. package/dist/commands/init.d.ts +6 -0
  144. package/dist/commands/init.d.ts.map +1 -0
  145. package/dist/commands/init.js +231 -0
  146. package/dist/commands/init.js.map +1 -0
  147. package/dist/components/FeaturedProject.d.ts +7 -0
  148. package/dist/components/FeaturedProject.d.ts.map +1 -0
  149. package/dist/components/FeaturedProject.js +10 -0
  150. package/dist/components/FeaturedProject.js.map +1 -0
  151. package/dist/components/FeaturedProject.test.tsx +204 -0
  152. package/dist/components/FeaturedProject.tsx +26 -0
  153. package/dist/components/ProjectCard/ProjectCard.d.ts +26 -0
  154. package/dist/components/ProjectCard/ProjectCard.d.ts.map +1 -0
  155. package/dist/components/ProjectCard/ProjectCard.js +39 -0
  156. package/dist/components/ProjectCard/ProjectCard.js.map +1 -0
  157. package/dist/components/ProjectCard/ProjectCard.test.tsx +281 -0
  158. package/dist/components/ProjectCard/ProjectCard.tsx +98 -0
  159. package/dist/components/ProjectCard/index.d.ts +2 -0
  160. package/dist/components/ProjectCard/index.d.ts.map +1 -0
  161. package/dist/components/ProjectCard/index.js +2 -0
  162. package/dist/components/ProjectCard/index.js.map +1 -0
  163. package/dist/components/ProjectCard/index.ts +1 -0
  164. package/dist/components/ProjectGrid/ProjectGrid.d.ts +5 -0
  165. package/dist/components/ProjectGrid/ProjectGrid.d.ts.map +1 -0
  166. package/dist/components/ProjectGrid/ProjectGrid.js +8 -0
  167. package/dist/components/ProjectGrid/ProjectGrid.js.map +1 -0
  168. package/dist/components/ProjectGrid/ProjectGrid.test.tsx +69 -0
  169. package/dist/components/ProjectGrid/ProjectGrid.tsx +6 -0
  170. package/dist/components/ProjectGrid/index.d.ts +2 -0
  171. package/dist/components/ProjectGrid/index.d.ts.map +1 -0
  172. package/dist/components/ProjectGrid/index.js +2 -0
  173. package/dist/components/ProjectGrid/index.js.map +1 -0
  174. package/dist/components/ProjectGrid/index.ts +1 -0
  175. package/dist/components/ProjectList/ProjectList.d.ts +5 -0
  176. package/dist/components/ProjectList/ProjectList.d.ts.map +1 -0
  177. package/dist/components/ProjectList/ProjectList.js +8 -0
  178. package/dist/components/ProjectList/ProjectList.js.map +1 -0
  179. package/dist/components/ProjectList/ProjectList.test.tsx +69 -0
  180. package/dist/components/ProjectList/ProjectList.tsx +6 -0
  181. package/dist/components/ProjectList/index.d.ts +2 -0
  182. package/dist/components/ProjectList/index.d.ts.map +1 -0
  183. package/dist/components/ProjectList/index.js +2 -0
  184. package/dist/components/ProjectList/index.js.map +1 -0
  185. package/dist/components/ProjectList/index.ts +1 -0
  186. package/dist/components/ProjectView/ProjectView.d.ts +20 -0
  187. package/dist/components/ProjectView/ProjectView.d.ts.map +1 -0
  188. package/dist/components/ProjectView/ProjectView.js +64 -0
  189. package/dist/components/ProjectView/ProjectView.js.map +1 -0
  190. package/dist/components/ProjectView/ProjectView.test.tsx +341 -0
  191. package/dist/components/ProjectView/ProjectView.tsx +168 -0
  192. package/dist/components/ProjectView/index.d.ts +2 -0
  193. package/dist/components/ProjectView/index.d.ts.map +1 -0
  194. package/dist/components/ProjectView/index.js +2 -0
  195. package/dist/components/ProjectView/index.js.map +1 -0
  196. package/dist/components/ProjectView/index.ts +1 -0
  197. package/dist/components/index.d.ts +5 -0
  198. package/dist/components/index.d.ts.map +1 -0
  199. package/dist/components/index.js +5 -0
  200. package/dist/components/index.js.map +1 -0
  201. package/dist/components/index.ts +4 -0
  202. package/dist/index.d.ts +4 -0
  203. package/dist/index.d.ts.map +1 -0
  204. package/dist/index.js +4 -0
  205. package/dist/index.js.map +1 -0
  206. package/dist/lib/defineProjects.d.ts +3 -0
  207. package/dist/lib/defineProjects.d.ts.map +1 -0
  208. package/dist/lib/defineProjects.js +4 -0
  209. package/dist/lib/defineProjects.js.map +1 -0
  210. package/dist/lib/filterByFeatured.d.ts +3 -0
  211. package/dist/lib/filterByFeatured.d.ts.map +1 -0
  212. package/dist/lib/filterByFeatured.js +10 -0
  213. package/dist/lib/filterByFeatured.js.map +1 -0
  214. package/dist/lib/filterByStatus.d.ts +3 -0
  215. package/dist/lib/filterByStatus.d.ts.map +1 -0
  216. package/dist/lib/filterByStatus.js +13 -0
  217. package/dist/lib/filterByStatus.js.map +1 -0
  218. package/dist/lib/filterByType.d.ts +3 -0
  219. package/dist/lib/filterByType.d.ts.map +1 -0
  220. package/dist/lib/filterByType.js +10 -0
  221. package/dist/lib/filterByType.js.map +1 -0
  222. package/dist/lib/github.d.ts +24 -0
  223. package/dist/lib/github.d.ts.map +1 -0
  224. package/dist/lib/github.js +107 -0
  225. package/dist/lib/github.js.map +1 -0
  226. package/dist/lib/index.d.ts +16 -0
  227. package/dist/lib/index.d.ts.map +1 -0
  228. package/dist/lib/index.js +12 -0
  229. package/dist/lib/index.js.map +1 -0
  230. package/dist/lib/normalise.d.ts +4 -0
  231. package/dist/lib/normalise.d.ts.map +1 -0
  232. package/dist/lib/normalise.js +221 -0
  233. package/dist/lib/normalise.js.map +1 -0
  234. package/dist/lib/npm.d.ts +7 -0
  235. package/dist/lib/npm.d.ts.map +1 -0
  236. package/dist/lib/npm.js +28 -0
  237. package/dist/lib/npm.js.map +1 -0
  238. package/dist/lib/product-hunt.d.ts +12 -0
  239. package/dist/lib/product-hunt.d.ts.map +1 -0
  240. package/dist/lib/product-hunt.js +40 -0
  241. package/dist/lib/product-hunt.js.map +1 -0
  242. package/dist/lib/sortByDate.d.ts +4 -0
  243. package/dist/lib/sortByDate.d.ts.map +1 -0
  244. package/dist/lib/sortByDate.js +21 -0
  245. package/dist/lib/sortByDate.js.map +1 -0
  246. package/dist/lib/sortByName.d.ts +4 -0
  247. package/dist/lib/sortByName.d.ts.map +1 -0
  248. package/dist/lib/sortByName.js +21 -0
  249. package/dist/lib/sortByName.js.map +1 -0
  250. package/dist/lib/sortByStars.d.ts +4 -0
  251. package/dist/lib/sortByStars.d.ts.map +1 -0
  252. package/dist/lib/sortByStars.js +21 -0
  253. package/dist/lib/sortByStars.js.map +1 -0
  254. package/dist/test-setup.d.ts +2 -0
  255. package/dist/test-setup.d.ts.map +1 -0
  256. package/dist/test-setup.js +2 -0
  257. package/dist/test-setup.js.map +1 -0
  258. package/dist/types/index.d.ts +121 -0
  259. package/dist/types/index.d.ts.map +1 -0
  260. package/dist/types/index.js +2 -0
  261. package/dist/types/index.js.map +1 -0
  262. package/eslint.config.js +33 -0
  263. package/package.json +47 -0
  264. package/playwright-report/index.html +85 -0
  265. package/src/__tests__/benchmarks/ProjectCard.bench.tsx +93 -0
  266. package/src/__tests__/benchmarks/ProjectView.bench.tsx +133 -0
  267. package/src/__tests__/benchmarks/github.bench.ts +53 -0
  268. package/src/__tests__/benchmarks/npm.bench.ts +62 -0
  269. package/src/__tests__/benchmarks/product-hunt.bench.ts +58 -0
  270. package/src/__tests__/benchmarks/utilities.bench.ts +131 -0
  271. package/src/cli-components/FeaturedProject/FeaturedProject.tsx +54 -0
  272. package/src/cli-components/FeaturedProject/index.ts +2 -0
  273. package/src/cli-components/ProjectCard/ProjectCard.tsx +90 -0
  274. package/src/cli-components/ProjectCard/index.ts +2 -0
  275. package/src/cli-components/ProjectGrid/ProjectGrid.tsx +6 -0
  276. package/src/cli-components/ProjectGrid/index.ts +2 -0
  277. package/src/cli-components/ProjectList/ProjectList.tsx +6 -0
  278. package/src/cli-components/ProjectList/index.ts +2 -0
  279. package/src/cli-components/ProjectView/ProjectView.tsx +117 -0
  280. package/src/cli-components/ProjectView/index.ts +2 -0
  281. package/src/cli-components/types.ts +58 -0
  282. package/src/cli-types.ts +58 -0
  283. package/src/cli.ts +26 -0
  284. package/src/commands/add.ts +191 -0
  285. package/src/commands/init.ts +260 -0
  286. package/src/components/FeaturedProject.test.tsx +204 -0
  287. package/src/components/FeaturedProject.tsx +26 -0
  288. package/src/components/ProjectCard/ProjectCard.test.tsx +281 -0
  289. package/src/components/ProjectCard/ProjectCard.tsx +98 -0
  290. package/src/components/ProjectCard/index.ts +1 -0
  291. package/src/components/ProjectGrid/ProjectGrid.test.tsx +69 -0
  292. package/src/components/ProjectGrid/ProjectGrid.tsx +6 -0
  293. package/src/components/ProjectGrid/index.ts +1 -0
  294. package/src/components/ProjectList/ProjectList.test.tsx +69 -0
  295. package/src/components/ProjectList/ProjectList.tsx +6 -0
  296. package/src/components/ProjectList/index.ts +1 -0
  297. package/src/components/ProjectView/ProjectView.test.tsx +341 -0
  298. package/src/components/ProjectView/ProjectView.tsx +168 -0
  299. package/src/components/ProjectView/index.ts +1 -0
  300. package/src/components/index.ts +4 -0
  301. package/src/index.ts +3 -0
  302. package/src/lib/__tests__/defineProjects.test.ts +112 -0
  303. package/src/lib/__tests__/filterByFeatured.test.ts +146 -0
  304. package/src/lib/__tests__/filterByStatus.test.ts +193 -0
  305. package/src/lib/__tests__/filterByType.test.ts +182 -0
  306. package/src/lib/__tests__/github.test.ts +566 -0
  307. package/src/lib/__tests__/hybrid-config.test.ts +420 -0
  308. package/src/lib/__tests__/normalise.test.ts +500 -0
  309. package/src/lib/__tests__/npm-config.test.ts +100 -0
  310. package/src/lib/__tests__/npm.test.ts +350 -0
  311. package/src/lib/__tests__/product-hunt-config.test.ts +104 -0
  312. package/src/lib/__tests__/product-hunt.test.ts +140 -0
  313. package/src/lib/__tests__/sortByDate.test.ts +222 -0
  314. package/src/lib/__tests__/sortByName.test.ts +249 -0
  315. package/src/lib/__tests__/sortByStars.test.ts +206 -0
  316. package/src/lib/defineProjects.ts +5 -0
  317. package/src/lib/filterByFeatured.ts +16 -0
  318. package/src/lib/filterByStatus.ts +20 -0
  319. package/src/lib/filterByType.ts +16 -0
  320. package/src/lib/github.ts +144 -0
  321. package/src/lib/index.ts +15 -0
  322. package/src/lib/normalise.ts +261 -0
  323. package/src/lib/npm.ts +38 -0
  324. package/src/lib/product-hunt.ts +57 -0
  325. package/src/lib/sortByDate.ts +30 -0
  326. package/src/lib/sortByName.ts +29 -0
  327. package/src/lib/sortByStars.ts +29 -0
  328. package/src/test-setup.ts +1 -0
  329. package/src/types/index.ts +144 -0
  330. package/test-results/.last-run.json +62 -0
  331. package/tsconfig.json +29 -0
  332. package/vitest.config.ts +31 -0
@@ -0,0 +1,350 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { fetchNpmPackage, NpmPackageData } from '../npm'
3
+
4
+ const mockFetch = vi.fn()
5
+ vi.stubGlobal('fetch', mockFetch)
6
+
7
+ describe('fetchNpmPackage', () => {
8
+ beforeEach(() => {
9
+ vi.clearAllMocks()
10
+ })
11
+
12
+ afterEach(() => {
13
+ vi.restoreAllMocks()
14
+ })
15
+
16
+ describe('valid package', () => {
17
+ it('should return correct package data for valid package', async () => {
18
+ const mockDownloadsData = {
19
+ package: 'react',
20
+ downloads: 10000000,
21
+ }
22
+
23
+ const mockRegistryData = {
24
+ 'dist-tags': {
25
+ latest: '18.2.0',
26
+ },
27
+ }
28
+
29
+ mockFetch
30
+ .mockResolvedValueOnce({
31
+ ok: true,
32
+ json: () => Promise.resolve(mockDownloadsData),
33
+ })
34
+ .mockResolvedValueOnce({
35
+ ok: true,
36
+ json: () => Promise.resolve(mockRegistryData),
37
+ })
38
+
39
+ const result = await fetchNpmPackage('react')
40
+
41
+ expect(result).toEqual({
42
+ name: 'react',
43
+ version: '18.2.0',
44
+ downloads: 10000000,
45
+ })
46
+ expect(mockFetch).toHaveBeenCalledTimes(2)
47
+ })
48
+
49
+ it('should fetch from correct npm endpoints', async () => {
50
+ const mockDownloadsData = { package: 'vue', downloads: 5000000 }
51
+ const mockRegistryData = { 'dist-tags': { latest: '3.4.0' } }
52
+
53
+ mockFetch
54
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
55
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
56
+
57
+ await fetchNpmPackage('vue')
58
+
59
+ expect(mockFetch).toHaveBeenCalledWith(
60
+ 'https://api.npmjs.org/downloads/point/last-month/vue',
61
+ { cache: 'force-cache' }
62
+ )
63
+ expect(mockFetch).toHaveBeenCalledWith(
64
+ 'https://registry.npmjs.org/vue',
65
+ { cache: 'force-cache' }
66
+ )
67
+ })
68
+ })
69
+
70
+ describe('invalid package', () => {
71
+ it('should return null for 404 not found on downloads endpoint', async () => {
72
+ mockFetch.mockResolvedValueOnce({
73
+ ok: false,
74
+ status: 404,
75
+ })
76
+
77
+ const result = await fetchNpmPackage('nonexistent-package-xyz')
78
+
79
+ expect(result).toBeNull()
80
+ })
81
+
82
+ it('should return null for 404 not found on registry endpoint', async () => {
83
+ mockFetch
84
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ package: 'test', downloads: 0 }) })
85
+ .mockResolvedValueOnce({ ok: false, status: 404 })
86
+
87
+ const result = await fetchNpmPackage('nonexistent-registry-package')
88
+
89
+ expect(result).toBeNull()
90
+ })
91
+
92
+ it('should return null for 500 server error', async () => {
93
+ mockFetch.mockResolvedValueOnce({
94
+ ok: false,
95
+ status: 500,
96
+ })
97
+
98
+ const result = await fetchNpmPackage('server-error-package')
99
+
100
+ expect(result).toBeNull()
101
+ })
102
+ })
103
+
104
+ describe('network error', () => {
105
+ it('should return null for network failure', async () => {
106
+ mockFetch.mockRejectedValueOnce(new Error('Network error'))
107
+
108
+ const result = await fetchNpmPackage('network-error-package')
109
+
110
+ expect(result).toBeNull()
111
+ })
112
+
113
+ it('should return null for timeout error', async () => {
114
+ mockFetch.mockRejectedValueOnce(new Error('Timeout'))
115
+
116
+ const result = await fetchNpmPackage('timeout-package')
117
+
118
+ expect(result).toBeNull()
119
+ })
120
+
121
+ it('should return null for DNS resolution failure', async () => {
122
+ mockFetch.mockRejectedValueOnce(new Error('ENOTFOUND'))
123
+
124
+ const result = await fetchNpmPackage('dns-error-package')
125
+
126
+ expect(result).toBeNull()
127
+ })
128
+ })
129
+
130
+ describe('caching strategy', () => {
131
+ it('should use force-cache for build-time caching on downloads endpoint', async () => {
132
+ const mockDownloadsData = { package: 'cached-pkg', downloads: 1000 }
133
+ const mockRegistryData = { 'dist-tags': { latest: '1.0.0' } }
134
+
135
+ mockFetch
136
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
137
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
138
+
139
+ await fetchNpmPackage('cached-pkg')
140
+
141
+ expect(mockFetch).toHaveBeenNthCalledWith(
142
+ 1,
143
+ expect.any(String),
144
+ expect.objectContaining({ cache: 'force-cache' })
145
+ )
146
+ })
147
+
148
+ it('should use force-cache for build-time caching on registry endpoint', async () => {
149
+ const mockDownloadsData = { package: 'cached-pkg-2', downloads: 2000 }
150
+ const mockRegistryData = { 'dist-tags': { latest: '2.0.0' } }
151
+
152
+ mockFetch
153
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
154
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
155
+
156
+ await fetchNpmPackage('cached-pkg-2')
157
+
158
+ expect(mockFetch).toHaveBeenNthCalledWith(
159
+ 2,
160
+ expect.any(String),
161
+ expect.objectContaining({ cache: 'force-cache' })
162
+ )
163
+ })
164
+ })
165
+
166
+ describe('response handling', () => {
167
+ it('should handle package name from downloads response', async () => {
168
+ const mockDownloadsData = { package: 'exact-package-name', downloads: 500 }
169
+ const mockRegistryData = { 'dist-tags': { latest: '0.1.0' } }
170
+
171
+ mockFetch
172
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
173
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
174
+
175
+ const result = await fetchNpmPackage('exact-package-name')
176
+
177
+ expect(result?.name).toBe('exact-package-name')
178
+ })
179
+
180
+ it('should use package name from parameter when not in downloads response', async () => {
181
+ const mockDownloadsData = { downloads: 300 }
182
+ const mockRegistryData = { 'dist-tags': { latest: '1.0.0' } }
183
+
184
+ mockFetch
185
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
186
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
187
+
188
+ const result = await fetchNpmPackage('param-package-name')
189
+
190
+ expect(result?.name).toBe('param-package-name')
191
+ })
192
+
193
+ it('should handle zero downloads', async () => {
194
+ const mockDownloadsData = { package: 'zero-downloads', downloads: 0 }
195
+ const mockRegistryData = { 'dist-tags': { latest: '0.0.1' } }
196
+
197
+ mockFetch
198
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
199
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
200
+
201
+ const result = await fetchNpmPackage('zero-downloads')
202
+
203
+ expect(result?.downloads).toBe(0)
204
+ })
205
+
206
+ it('should handle missing downloads field gracefully', async () => {
207
+ const mockDownloadsData = { package: 'no-downloads-field' }
208
+ const mockRegistryData = { 'dist-tags': { latest: '1.0.0' } }
209
+
210
+ mockFetch
211
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
212
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
213
+
214
+ const result = await fetchNpmPackage('no-downloads-field')
215
+
216
+ expect(result?.downloads).toBe(0)
217
+ })
218
+ })
219
+
220
+ describe('version handling', () => {
221
+ it('should return null when dist-tags latest is missing', async () => {
222
+ const mockDownloadsData = { package: 'no-version', downloads: 100 }
223
+ const mockRegistryData = { 'dist-tags': {} }
224
+
225
+ mockFetch
226
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
227
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
228
+
229
+ const result = await fetchNpmPackage('no-version')
230
+
231
+ expect(result).toBeNull()
232
+ })
233
+
234
+ it('should return null when dist-tags is undefined', async () => {
235
+ const mockDownloadsData = { package: 'no-dist-tags', downloads: 100 }
236
+ const mockRegistryData = {}
237
+
238
+ mockFetch
239
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
240
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
241
+
242
+ const result = await fetchNpmPackage('no-dist-tags')
243
+
244
+ expect(result).toBeNull()
245
+ })
246
+
247
+ it('should extract latest version from dist-tags', async () => {
248
+ const mockDownloadsData = { package: 'versioned-pkg', downloads: 1000 }
249
+ const mockRegistryData = {
250
+ 'dist-tags': {
251
+ latest: '5.0.0',
252
+ next: '6.0.0-beta.1',
253
+ canary: '6.0.0-canary.5',
254
+ },
255
+ }
256
+
257
+ mockFetch
258
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
259
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
260
+
261
+ const result = await fetchNpmPackage('versioned-pkg')
262
+
263
+ expect(result?.version).toBe('5.0.0')
264
+ })
265
+ })
266
+
267
+ describe('no real API calls', () => {
268
+ it('should use mocked fetch and not hit real npm API', async () => {
269
+ const mockDownloadsData = { package: 'mocked-pkg', downloads: 999 }
270
+ const mockRegistryData = { 'dist-tags': { latest: '1.0.0' } }
271
+
272
+ mockFetch
273
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
274
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
275
+
276
+ await fetchNpmPackage('mocked-pkg')
277
+
278
+ expect(mockFetch).toHaveBeenCalledTimes(2)
279
+ expect(mockFetch).not.toHaveBeenCalledWith(
280
+ expect.stringContaining('https://api.npmjs.org/downloads/point/last-month/react'),
281
+ expect.any(Object)
282
+ )
283
+ })
284
+ })
285
+
286
+ describe('API contract verification', () => {
287
+ it('should map all required fields from npm API response', async () => {
288
+ const mockDownloadsData = {
289
+ package: 'full-api-pkg',
290
+ downloads: 12345678,
291
+ start: '2024-01-01',
292
+ end: '2024-01-31',
293
+ }
294
+
295
+ const mockRegistryData = {
296
+ name: 'full-api-pkg',
297
+ 'dist-tags': { latest: '10.20.30' },
298
+ versions: { '10.20.30': {} },
299
+ }
300
+
301
+ mockFetch
302
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
303
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
304
+
305
+ const result = await fetchNpmPackage('full-api-pkg')
306
+
307
+ expect(result).toEqual<NpmPackageData>({
308
+ name: 'full-api-pkg',
309
+ version: '10.20.30',
310
+ downloads: 12345678,
311
+ })
312
+ })
313
+
314
+ it('should handle scoped packages', async () => {
315
+ const mockDownloadsData = { package: '@types/node', downloads: 5000000 }
316
+ const mockRegistryData = { 'dist-tags': { latest: '20.10.0' } }
317
+
318
+ mockFetch
319
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
320
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
321
+
322
+ const result = await fetchNpmPackage('@types/node')
323
+
324
+ expect(result).toEqual({
325
+ name: '@types/node',
326
+ version: '20.10.0',
327
+ downloads: 5000000,
328
+ })
329
+ expect(mockFetch).toHaveBeenCalledWith(
330
+ 'https://api.npmjs.org/downloads/point/last-month/@types/node',
331
+ { cache: 'force-cache' }
332
+ )
333
+ })
334
+ })
335
+
336
+ describe('parallel requests', () => {
337
+ it('should make downloads and registry requests in parallel', async () => {
338
+ const mockDownloadsData = { package: 'parallel-pkg', downloads: 5000 }
339
+ const mockRegistryData = { 'dist-tags': { latest: '2.0.0' } }
340
+
341
+ mockFetch
342
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockDownloadsData) })
343
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockRegistryData) })
344
+
345
+ await fetchNpmPackage('parallel-pkg')
346
+
347
+ expect(mockFetch).toHaveBeenCalledTimes(2)
348
+ })
349
+ })
350
+ })
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { normalise } from '../normalise'
3
+ import type { ProductHuntProjectInput } from '../../types'
4
+
5
+ describe('product-hunt config recognition', () => {
6
+ it('should recognize a Product Hunt project when type is "product-hunt"', async () => {
7
+ const input: ProductHuntProjectInput = {
8
+ id: 'test-ph',
9
+ type: 'product-hunt',
10
+ slug: 'test-product',
11
+ status: 'active',
12
+ name: 'Test Product',
13
+ tagline: 'A test product',
14
+ description: 'This is a test description',
15
+ }
16
+
17
+ const result = await normalise(input)
18
+
19
+ expect(result.type).toBe('product-hunt')
20
+ expect(result.id).toBe('test-ph')
21
+ expect(result.slug).toBe('test-product')
22
+ })
23
+
24
+ it('should match the slug property to the Product Hunt post slug', async () => {
25
+ const input: ProductHuntProjectInput = {
26
+ id: 'test-ph',
27
+ type: 'product-hunt',
28
+ slug: 'my-awesome-product',
29
+ status: 'active',
30
+ name: 'My Awesome Product',
31
+ }
32
+
33
+ const result = await normalise(input)
34
+
35
+ expect(result.slug).toBe('my-awesome-product')
36
+ })
37
+
38
+ it('should handle Product Hunt project with all optional fields', async () => {
39
+ const input: ProductHuntProjectInput = {
40
+ id: 'full-ph',
41
+ type: 'product-hunt',
42
+ slug: 'full-product',
43
+ status: 'shipped',
44
+ featured: true,
45
+ name: 'Full Product',
46
+ tagline: 'A complete Product Hunt product',
47
+ description: 'Description of the product',
48
+ background: 'Background info',
49
+ why: 'Why I built it',
50
+ struggles: [],
51
+ timeline: [],
52
+ posts: [],
53
+ stack: ['TypeScript', 'React'],
54
+ links: {
55
+ productHunt: 'https://www.producthunt.com/products/full-product',
56
+ github: 'https://github.com/user/full-product',
57
+ live: 'https://full-product.com',
58
+ },
59
+ stats: {
60
+ upvotes: 100,
61
+ comments: 25,
62
+ launchDate: '2024-01-15',
63
+ },
64
+ }
65
+
66
+ const result = await normalise(input)
67
+
68
+ expect(result.type).toBe('product-hunt')
69
+ expect(result.slug).toBe('full-product')
70
+ expect(result.name).toBe('Full Product')
71
+ expect(result.tagline).toBe('A complete Product Hunt product')
72
+ expect(result.description).toBe('Description of the product')
73
+ expect(result.background).toBe('Background info')
74
+ expect(result.why).toBe('Why I built it')
75
+ expect(result.stack).toEqual(['TypeScript', 'React'])
76
+ expect(result.links).toEqual({
77
+ productHunt: 'https://www.producthunt.com/products/full-product',
78
+ github: 'https://github.com/user/full-product',
79
+ live: 'https://full-product.com',
80
+ })
81
+ expect(result.stats).toEqual({
82
+ upvotes: 100,
83
+ comments: 25,
84
+ launchDate: '2024-01-15',
85
+ })
86
+ })
87
+
88
+ it('should handle Product Hunt project with minimal config', async () => {
89
+ const input: ProductHuntProjectInput = {
90
+ id: 'minimal-ph',
91
+ type: 'product-hunt',
92
+ slug: 'minimal-product',
93
+ status: 'active',
94
+ }
95
+
96
+ const result = await normalise(input)
97
+
98
+ expect(result.type).toBe('product-hunt')
99
+ expect(result.slug).toBe('minimal-product')
100
+ expect(result.name).toBe('')
101
+ expect(result.tagline).toBe('')
102
+ expect(result.description).toBe('')
103
+ })
104
+ })
@@ -0,0 +1,140 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { fetchProductHuntPost } from '../product-hunt'
3
+
4
+ describe('fetchProductHuntPost', () => {
5
+ const originalEnv = process.env
6
+
7
+ beforeEach(() => {
8
+ vi.resetAllMocks()
9
+ process.env = { ...originalEnv }
10
+ global.fetch = vi.fn()
11
+ })
12
+
13
+ afterEach(() => {
14
+ process.env = originalEnv
15
+ vi.restoreAllMocks()
16
+ })
17
+
18
+ it('should fetch Product Hunt post data successfully', async () => {
19
+ const mockData = {
20
+ post: {
21
+ name: 'Test Product',
22
+ tagline: 'A test product',
23
+ description: 'This is a test description',
24
+ votes_count: 100,
25
+ comments_count: 25,
26
+ featured_at: '2024-01-15T10:00:00Z',
27
+ website: 'https://example.com',
28
+ url: 'https://www.producthunt.com/products/test-product',
29
+ },
30
+ }
31
+
32
+ process.env.PRODUCT_HUNT_TOKEN = 'test-token'
33
+ vi.mocked(fetch).mockResolvedValueOnce({
34
+ ok: true,
35
+ json: async () => mockData,
36
+ } as Response)
37
+
38
+ const result = await fetchProductHuntPost('test-product')
39
+
40
+ expect(result).toEqual({
41
+ name: 'Test Product',
42
+ tagline: 'A test product',
43
+ description: 'This is a test description',
44
+ votes_count: 100,
45
+ comments_count: 25,
46
+ featured_at: '2024-01-15T10:00:00Z',
47
+ website: 'https://example.com',
48
+ url: 'https://www.producthunt.com/products/test-product',
49
+ })
50
+ expect(fetch).toHaveBeenCalledWith(
51
+ 'https://api.producthunt.com/v2/posts/test-product',
52
+ {
53
+ headers: {
54
+ Authorization: 'Bearer test-token',
55
+ Accept: 'application/json',
56
+ },
57
+ cache: 'force-cache',
58
+ }
59
+ )
60
+ })
61
+
62
+ it('should return null when PRODUCT_HUNT_TOKEN is not set', async () => {
63
+ process.env.PRODUCT_HUNT_TOKEN = undefined
64
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
65
+
66
+ const result = await fetchProductHuntPost('test-product')
67
+
68
+ expect(result).toBeNull()
69
+ expect(consoleWarnSpy).toHaveBeenCalledWith('PRODUCT_HUNT_TOKEN not set - cannot fetch Product Hunt data')
70
+ expect(fetch).not.toHaveBeenCalled()
71
+ consoleWarnSpy.mockRestore()
72
+ })
73
+
74
+ it('should return null when API returns 404', async () => {
75
+ process.env.PRODUCT_HUNT_TOKEN = 'test-token'
76
+ vi.mocked(fetch).mockResolvedValueOnce({
77
+ ok: false,
78
+ status: 404,
79
+ } as Response)
80
+
81
+ const result = await fetchProductHuntPost('non-existent-product')
82
+
83
+ expect(result).toBeNull()
84
+ })
85
+
86
+ it('should return null when fetch fails with network error', async () => {
87
+ process.env.PRODUCT_HUNT_TOKEN = 'test-token'
88
+ vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'))
89
+
90
+ const result = await fetchProductHuntPost('test-product')
91
+
92
+ expect(result).toBeNull()
93
+ })
94
+
95
+ it('should handle missing post data in response', async () => {
96
+ process.env.PRODUCT_HUNT_TOKEN = 'test-token'
97
+ vi.mocked(fetch).mockResolvedValueOnce({
98
+ ok: true,
99
+ json: async () => ({}),
100
+ } as Response)
101
+
102
+ const result = await fetchProductHuntPost('test-product')
103
+
104
+ expect(result).toBeNull()
105
+ })
106
+
107
+ it('should handle missing optional fields gracefully', async () => {
108
+ const mockData = {
109
+ post: {
110
+ name: 'Test Product',
111
+ tagline: 'A test product',
112
+ description: '',
113
+ votes_count: 0,
114
+ comments_count: 0,
115
+ featured_at: null,
116
+ website: '',
117
+ url: '',
118
+ },
119
+ }
120
+
121
+ process.env.PRODUCT_HUNT_TOKEN = 'test-token'
122
+ vi.mocked(fetch).mockResolvedValueOnce({
123
+ ok: true,
124
+ json: async () => mockData,
125
+ } as Response)
126
+
127
+ const result = await fetchProductHuntPost('test-product')
128
+
129
+ expect(result).toEqual({
130
+ name: 'Test Product',
131
+ tagline: 'A test product',
132
+ description: '',
133
+ votes_count: 0,
134
+ comments_count: 0,
135
+ featured_at: null,
136
+ website: '',
137
+ url: '',
138
+ })
139
+ })
140
+ })