@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,260 @@
1
+ import { readFile, writeFile, access, constants } from 'node:fs/promises'
2
+ import { resolve } from 'node:path'
3
+ import inquirer from 'inquirer'
4
+ import chalk from 'chalk'
5
+ import { fetchGitHubRepos } from '../lib/github.js'
6
+
7
+ const CONFIG_FILE = 'folio.config.ts'
8
+ const PACKAGE_JSON = 'package.json'
9
+
10
+ interface InitOptions {
11
+ github?: boolean
12
+ }
13
+
14
+ export async function init(options: InitOptions = {}): Promise<void> {
15
+ const workingDir = process.cwd()
16
+
17
+ console.log(chalk.bold('🚀 Initializing Folio project...'))
18
+ console.log()
19
+
20
+ try {
21
+ await access(CONFIG_FILE, constants.F_OK)
22
+
23
+ const { overwrite } = await inquirer.prompt([
24
+ {
25
+ type: 'confirm',
26
+ name: 'overwrite',
27
+ message: `${CONFIG_FILE} already exists. Overwrite?`,
28
+ default: false,
29
+ },
30
+ ])
31
+
32
+ if (!overwrite) {
33
+ console.log(chalk.yellow('✖ Init cancelled.'))
34
+ return
35
+ }
36
+ } catch {
37
+ }
38
+
39
+ const isNextJs = await detectNextJs(workingDir)
40
+
41
+ if (isNextJs) {
42
+ console.log(chalk.green('✓ Next.js project detected'))
43
+ } else {
44
+ console.log(chalk.yellow('⚠ Next.js not detected. Folio is optimized for Next.js projects.'))
45
+ }
46
+
47
+ console.log()
48
+
49
+ let template: string
50
+
51
+ if (options.github) {
52
+ template = await generateGitHubConfig()
53
+ } else {
54
+ template = generateConfigTemplate()
55
+ }
56
+
57
+ try {
58
+ const configPath = resolve(workingDir, CONFIG_FILE)
59
+
60
+ await writeFile(configPath, template, 'utf-8')
61
+
62
+ console.log(chalk.green(`✓ ${CONFIG_FILE} created successfully`))
63
+ console.log()
64
+ console.log(chalk.bold('Next steps:'))
65
+ console.log(chalk.gray('1. Edit folio.config.ts to add your projects'))
66
+ console.log(chalk.gray('2. Import and use the components in your Next.js app'))
67
+ console.log(chalk.gray('3. See https://docs.folio.dev for usage examples'))
68
+ } catch (error) {
69
+ if (error instanceof Error && 'code' in error && error.code === 'EACCES') {
70
+ console.error(chalk.red(`✖ Permission denied. Cannot write to ${CONFIG_FILE}`))
71
+ console.error(chalk.gray(' Check file permissions and try again.'))
72
+ } else {
73
+ console.error(chalk.red('✖ Failed to create folio.config.ts'))
74
+ console.error(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`))
75
+ }
76
+ process.exit(1)
77
+ }
78
+ }
79
+
80
+ async function detectNextJs(workingDir: string): Promise<boolean> {
81
+ const packagePath = resolve(workingDir, PACKAGE_JSON)
82
+
83
+ try {
84
+ await access(packagePath, constants.F_OK)
85
+ } catch {
86
+ console.log(chalk.yellow(`⚠ ${PACKAGE_JSON} not found. Proceeding with basic scaffold.`))
87
+ return false
88
+ }
89
+
90
+ try {
91
+ const content = await readFile(packagePath, 'utf-8')
92
+ const pkg = JSON.parse(content)
93
+
94
+ if (!pkg || typeof pkg !== 'object') {
95
+ return false
96
+ }
97
+
98
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
99
+ return 'next' in deps
100
+ } catch (error) {
101
+ if (error instanceof SyntaxError) {
102
+ console.error(chalk.red(`✖ ${PACKAGE_JSON} is not valid JSON`))
103
+ console.error(chalk.gray(' Check syntax and try again.'))
104
+ } else {
105
+ console.error(chalk.red(`✖ Error reading ${PACKAGE_JSON}`))
106
+ console.error(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`))
107
+ }
108
+ return false
109
+ }
110
+ }
111
+
112
+ function generateConfigTemplate(): string {
113
+ return `import { defineProjects } from '@reallukemanning/folio'
114
+
115
+ export const projects = defineProjects([
116
+ {
117
+ id: 'my-project',
118
+ type: 'github',
119
+ repo: 'username/repo',
120
+ status: 'active',
121
+ featured: true,
122
+ background: 'A brief background of your project...',
123
+ why: 'Why you built this project...',
124
+ stack: ['TypeScript', 'React', 'Next.js'],
125
+ struggles: [],
126
+ timeline: [],
127
+ posts: [],
128
+ },
129
+ ])
130
+ `
131
+ }
132
+
133
+ // Optional fields to add context to your project:
134
+ // background: The story behind the project - what problem does it solve?
135
+ // why: Your motivation for building it - why does this matter to you?
136
+ // struggles: Current challenges or technical hurdles faced
137
+ // - type: 'warn' (minor issues) or 'error' (blockers)
138
+ // - text: Description of the challenge
139
+ // timeline: Milestones and important dates in the project's journey
140
+ // - date: Formatted date string (e.g., '2025-01-15')
141
+ // - note: What happened on this date
142
+ // posts: Related blog posts, announcements, or documentation
143
+ // - title: Post title
144
+ // - date: Publication date
145
+ // - url: Link to the post (optional)
146
+
147
+ async function generateGitHubConfig(): Promise<string> {
148
+ const { username } = await inquirer.prompt([
149
+ {
150
+ type: 'input',
151
+ name: 'username',
152
+ message: 'Enter your GitHub username:',
153
+ validate: (input: string) => input.trim().length > 0 || 'Username is required',
154
+ },
155
+ ])
156
+
157
+ console.log()
158
+ console.log(chalk.gray('Fetching repositories...'))
159
+
160
+ const result = await fetchGitHubRepos(username)
161
+
162
+ if (result.error) {
163
+ if (result.error === 'rate_limit') {
164
+ console.log(chalk.red('✖ GitHub API rate limit exceeded'))
165
+ if (!process.env.GITHUB_TOKEN) {
166
+ console.log(chalk.yellow(' Rate limit: 60 requests/hour (unauthenticated)'))
167
+ console.log(chalk.yellow(' Recommended: Set GITHUB_TOKEN environment variable for 5000 requests/hour'))
168
+ }
169
+ console.log(chalk.gray(' Run "npx folio init" for a basic scaffold instead.'))
170
+ } else if (result.error === 'network') {
171
+ console.log(chalk.red('✖ Failed to fetch repositories from GitHub'))
172
+ console.log(chalk.gray(' Check your network connection and try again.'))
173
+ console.log(chalk.gray(' Run "npx folio init" for a basic scaffold instead.'))
174
+ } else if (result.error === 'not_found') {
175
+ console.log(chalk.red(`✖ GitHub user "${username}" not found`))
176
+ console.log(chalk.gray(' Check the username and try again.'))
177
+ console.log(chalk.gray(' Run "npx folio init" for a basic scaffold instead.'))
178
+ } else {
179
+ console.log(chalk.red('✖ Failed to fetch repositories from GitHub'))
180
+ console.log(chalk.gray(' Run "npx folio init" for a basic scaffold instead.'))
181
+ }
182
+ process.exit(1)
183
+ }
184
+
185
+ if (!result.data || result.data.length === 0) {
186
+ console.log(chalk.yellow('✖ No repositories found'))
187
+ console.log(chalk.gray(' This could mean:'))
188
+ console.log(chalk.gray(' - You have no public repositories'))
189
+ console.log(chalk.gray(' - Your GITHUB_TOKEN is not set (private repos require auth)'))
190
+ console.log(chalk.gray(' - Rate limit exceeded (60/hr unauthenticated)'))
191
+ console.log()
192
+ console.log(chalk.gray('Suggestions:'))
193
+ console.log(chalk.gray(' 1. Set GITHUB_TOKEN environment variable for private repos'))
194
+ console.log(chalk.gray(' 2. Run "npx folio init" for a basic scaffold instead'))
195
+ console.log(chalk.gray(' 3. Check your GitHub username and try again'))
196
+ process.exit(1)
197
+ }
198
+
199
+ const { includeForks } = await inquirer.prompt([
200
+ {
201
+ type: 'confirm',
202
+ name: 'includeForks',
203
+ message: 'Include forked repositories?',
204
+ default: false,
205
+ },
206
+ ])
207
+
208
+ const filteredRepos = result.data.filter((repo) => {
209
+ if (repo.archived) {
210
+ return false
211
+ }
212
+
213
+ if (repo.name.endsWith('.template') || repo.name.endsWith('.github.io')) {
214
+ return false
215
+ }
216
+
217
+ if (repo.description === null) {
218
+ return false
219
+ }
220
+
221
+ return true
222
+ })
223
+
224
+ const finalRepos = includeForks
225
+ ? filteredRepos
226
+ : filteredRepos.filter((repo) => !repo.fork)
227
+
228
+ if (finalRepos.length === 0) {
229
+ console.log(chalk.yellow('✖ No repositories match your criteria'))
230
+ console.log(chalk.gray(' Try including forks or check your repository descriptions.'))
231
+ console.log(chalk.gray(' Run "npx folio init" for a basic scaffold instead.'))
232
+ process.exit(1)
233
+ }
234
+
235
+ const repoEntries = finalRepos
236
+ .map(
237
+ (repo) => ` {
238
+ id: '${repo.name}',
239
+ type: 'github',
240
+ repo: '${username}/${repo.name}',
241
+ status: 'active',
242
+ featured: false,
243
+ description: '${(repo.description || '').replace(/'/g, "\\'").replace(/\n/g, ' ')}',
244
+ background: null,
245
+ why: null,
246
+ stack: ${repo.language ? `['${repo.language}']` : '[]'},
247
+ struggles: [],
248
+ timeline: [],
249
+ posts: [],
250
+ },`
251
+ )
252
+ .join('\n')
253
+
254
+ return `import { defineProjects } from '@reallukemanning/folio'
255
+
256
+ export const projects = defineProjects([
257
+ ${repoEntries}
258
+ ])
259
+ `
260
+ }
@@ -0,0 +1,204 @@
1
+ import { describe, it, expect, afterEach } from 'vitest'
2
+ import { render, screen, cleanup } from '@testing-library/react'
3
+ import { FeaturedProject } from './FeaturedProject'
4
+ import type { FolioProject } from '../types'
5
+
6
+ afterEach(() => {
7
+ cleanup()
8
+ })
9
+
10
+ const createProject = (overrides: Partial<FolioProject> = {}): FolioProject => ({
11
+ id: 'test-project',
12
+ type: 'github',
13
+ status: 'active',
14
+ featured: true,
15
+ name: 'Featured Project',
16
+ tagline: 'A featured project',
17
+ description: 'This is a featured project description',
18
+ background: 'Background of the featured project.',
19
+ why: 'Why this project is featured.',
20
+ image: null,
21
+ struggles: [],
22
+ timeline: [],
23
+ posts: [],
24
+ stack: ['React', 'TypeScript'],
25
+ links: {
26
+ github: 'https://github.com/test/project',
27
+ live: 'https://test-project.com',
28
+ },
29
+ stats: {
30
+ stars: 1000,
31
+ forks: 200,
32
+ },
33
+ language: 'TypeScript',
34
+ languageColor: '#3178c6',
35
+ createdAt: '2024-01-01',
36
+ updatedAt: '2024-06-01',
37
+ ...overrides,
38
+ })
39
+
40
+ describe('FeaturedProject', () => {
41
+ it('displays project prominently with project data', () => {
42
+ const project = createProject()
43
+
44
+ const { container } = render(<FeaturedProject project={project} />)
45
+
46
+ expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('Featured Project')
47
+ expect(container.querySelector('[data-folio-view-section-name="background"]')).toHaveTextContent(/Background of the featured project/)
48
+ expect(screen.getByText('Why this project is featured.')).toBeInTheDocument()
49
+ })
50
+
51
+ it('renders nothing without project', () => {
52
+ const { container } = render(<FeaturedProject project={undefined} />)
53
+
54
+ expect(container.querySelector('[data-folio-featured]')).not.toBeInTheDocument()
55
+ expect(container.firstChild).toBeNull()
56
+ })
57
+
58
+ it('renders nothing with null project', () => {
59
+ const { container } = render(<FeaturedProject project={null} />)
60
+
61
+ expect(container.querySelector('[data-folio-featured]')).not.toBeInTheDocument()
62
+ })
63
+
64
+ it('displays image when project has image', () => {
65
+ const project = createProject({ image: 'https://example.com/image.png' })
66
+
67
+ const { container } = render(<FeaturedProject project={project} />)
68
+
69
+ const image = container.querySelector('[data-folio-featured-image]')
70
+ expect(image).toBeInTheDocument()
71
+ expect(image).toHaveAttribute('src', 'https://example.com/image.png')
72
+ expect(image).toHaveAttribute('alt', 'Featured Project')
73
+ })
74
+
75
+ it('does not display image when project has no image', () => {
76
+ const project = createProject({ image: null })
77
+
78
+ const { container } = render(<FeaturedProject project={project} />)
79
+
80
+ expect(container.querySelector('[data-folio-featured-image]')).not.toBeInTheDocument()
81
+ })
82
+
83
+ it('displays stats when available', () => {
84
+ const project = createProject()
85
+
86
+ render(<FeaturedProject project={project} />)
87
+
88
+ expect(screen.getByText('1000 stars')).toBeInTheDocument()
89
+ expect(screen.getByText('200 forks')).toBeInTheDocument()
90
+ })
91
+
92
+ it('displays links when available', () => {
93
+ const project = createProject()
94
+
95
+ const { container } = render(<FeaturedProject project={project} />)
96
+
97
+ expect(container.querySelector('[data-folio-link-type="github"]')).toBeInTheDocument()
98
+ expect(container.querySelector('[data-folio-link-type="live"]')).toBeInTheDocument()
99
+ })
100
+ })
101
+
102
+ describe('FeaturedProject data attributes', () => {
103
+ it('has correct data-folio-featured attribute on root', () => {
104
+ const project = createProject()
105
+
106
+ const { container } = render(<FeaturedProject project={project} />)
107
+
108
+ expect(container.querySelector('[data-folio-featured]')).toBeInTheDocument()
109
+ })
110
+
111
+ it('has correct data-folio-featured-image attribute', () => {
112
+ const project = createProject({ image: 'https://example.com/image.png' })
113
+
114
+ const { container } = render(<FeaturedProject project={project} />)
115
+
116
+ expect(container.querySelector('[data-folio-featured-image]')).toBeInTheDocument()
117
+ })
118
+
119
+ it('has correct data-folio-view attribute from ProjectView', () => {
120
+ const project = createProject()
121
+
122
+ const { container } = render(<FeaturedProject project={project} />)
123
+
124
+ expect(container.querySelector('[data-folio-view]')).toBeInTheDocument()
125
+ })
126
+
127
+ it('has correct data-folio-view-section attributes', () => {
128
+ const project = createProject()
129
+
130
+ const { container } = render(<FeaturedProject project={project} />)
131
+
132
+ expect(container.querySelector('[data-folio-view-section-name="background"]')).toBeInTheDocument()
133
+ expect(container.querySelector('[data-folio-view-section-name="why"]')).toBeInTheDocument()
134
+ })
135
+
136
+ it('has correct data-folio-view-stats attribute', () => {
137
+ const project = createProject()
138
+
139
+ const { container } = render(<FeaturedProject project={project} />)
140
+
141
+ expect(container.querySelector('[data-folio-view-stats]')).toBeInTheDocument()
142
+ })
143
+
144
+ it('has correct data-folio-view-links attribute', () => {
145
+ const project = createProject()
146
+
147
+ const { container } = render(<FeaturedProject project={project} />)
148
+
149
+ expect(container.querySelector('[data-folio-view-links]')).toBeInTheDocument()
150
+ })
151
+ })
152
+
153
+ describe('FeaturedProject composition', () => {
154
+ it('uses ProjectView internally for project display', () => {
155
+ const project = createProject()
156
+
157
+ const { container } = render(<FeaturedProject project={project} />)
158
+
159
+ expect(container.querySelector('[data-folio-view]')).toBeInTheDocument()
160
+ })
161
+
162
+ it('renders background section via ProjectView.Section', () => {
163
+ const project = createProject({ background: 'Custom background' })
164
+
165
+ const { container } = render(<FeaturedProject project={project} />)
166
+
167
+ expect(container.querySelector('[data-folio-view-section-name="background"]')).toHaveTextContent('Custom background')
168
+ })
169
+
170
+ it('renders why section via ProjectView.Section', () => {
171
+ const project = createProject({ why: 'Custom why' })
172
+
173
+ const { container } = render(<FeaturedProject project={project} />)
174
+
175
+ expect(container.querySelector('[data-folio-view-section-name="why"]')).toHaveTextContent('Custom why')
176
+ })
177
+
178
+ it('handles project with no background gracefully', () => {
179
+ const project = createProject({ background: null })
180
+
181
+ const { container } = render(<FeaturedProject project={project} />)
182
+
183
+ expect(container.querySelector('[data-folio-featured]')).toBeInTheDocument()
184
+ expect(container.querySelector('[data-folio-view-section-name="background"]')).not.toBeInTheDocument()
185
+ })
186
+
187
+ it('handles project with no why gracefully', () => {
188
+ const project = createProject({ why: null })
189
+
190
+ const { container } = render(<FeaturedProject project={project} />)
191
+
192
+ expect(container.querySelector('[data-folio-featured]')).toBeInTheDocument()
193
+ expect(container.querySelector('[data-folio-view-section-name="why"]')).not.toBeInTheDocument()
194
+ })
195
+
196
+ it('handles project with no stats gracefully', () => {
197
+ const project = createProject({ stats: null })
198
+
199
+ const { container } = render(<FeaturedProject project={project} />)
200
+
201
+ expect(container.querySelector('[data-folio-featured]')).toBeInTheDocument()
202
+ expect(container.querySelector('[data-folio-view-stats]')).not.toBeInTheDocument()
203
+ })
204
+ })
@@ -0,0 +1,26 @@
1
+ import type { FolioProject } from '../types'
2
+ import { ProjectView } from './ProjectView'
3
+
4
+ interface FeaturedProjectProps {
5
+ project: FolioProject | null | undefined
6
+ }
7
+
8
+ function FeaturedProject({ project }: FeaturedProjectProps) {
9
+ if (!project) {
10
+ return null
11
+ }
12
+
13
+ return (
14
+ <div data-folio-featured>
15
+ {project.image && <img data-folio-featured-image src={project.image} alt={project.name} />}
16
+ <ProjectView project={project}>
17
+ <ProjectView.Section name="background" project={project} />
18
+ <ProjectView.Section name="why" project={project} />
19
+ <ProjectView.Stats project={project} />
20
+ <ProjectView.Links project={project} />
21
+ </ProjectView>
22
+ </div>
23
+ )
24
+ }
25
+
26
+ export { FeaturedProject }