@nifrajs/cli 3.5.0 → 4.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 (403) hide show
  1. package/README.md +42 -3
  2. package/dist/app-files.d.ts +11 -0
  3. package/dist/app-files.d.ts.map +1 -0
  4. package/dist/app-files.js +34 -0
  5. package/dist/app-files.js.map +1 -0
  6. package/dist/assure-hydration.d.ts +7 -1
  7. package/dist/assure-hydration.d.ts.map +1 -1
  8. package/dist/assure-hydration.js +128 -43
  9. package/dist/assure-hydration.js.map +1 -1
  10. package/dist/assure.d.ts.map +1 -1
  11. package/dist/assure.js +16 -2
  12. package/dist/assure.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts +5 -1
  14. package/dist/capabilities-tool.d.ts.map +1 -1
  15. package/dist/capabilities-tool.js +32 -10
  16. package/dist/capabilities-tool.js.map +1 -1
  17. package/dist/cdn-check.d.ts +41 -0
  18. package/dist/cdn-check.d.ts.map +1 -0
  19. package/dist/cdn-check.js +207 -0
  20. package/dist/cdn-check.js.map +1 -0
  21. package/dist/check-diagnostics.d.ts +4 -0
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +13 -0
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +49 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +291 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +99 -63
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +42 -16
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +262 -82
  35. package/dist/cli.js.map +1 -1
  36. package/dist/command-catalog.d.ts +57 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +270 -26
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/config-target.d.ts +8 -0
  41. package/dist/config-target.d.ts.map +1 -0
  42. package/dist/config-target.js +45 -0
  43. package/dist/config-target.js.map +1 -0
  44. package/dist/contracts.d.ts.map +1 -1
  45. package/dist/contracts.js +52 -13
  46. package/dist/contracts.js.map +1 -1
  47. package/dist/db-audit.d.ts +37 -0
  48. package/dist/db-audit.d.ts.map +1 -0
  49. package/dist/db-audit.js +99 -0
  50. package/dist/db-audit.js.map +1 -0
  51. package/dist/db-child.d.ts +64 -0
  52. package/dist/db-child.d.ts.map +1 -0
  53. package/dist/db-child.js +213 -0
  54. package/dist/db-child.js.map +1 -0
  55. package/dist/db-config.d.ts +60 -0
  56. package/dist/db-config.d.ts.map +1 -0
  57. package/dist/db-config.js +176 -0
  58. package/dist/db-config.js.map +1 -0
  59. package/dist/db-tool.d.ts +84 -0
  60. package/dist/db-tool.d.ts.map +1 -0
  61. package/dist/db-tool.js +537 -0
  62. package/dist/db-tool.js.map +1 -0
  63. package/dist/dev-bun-config.d.ts +20 -13
  64. package/dist/dev-bun-config.d.ts.map +1 -1
  65. package/dist/dev-bun-config.js +98 -41
  66. package/dist/dev-bun-config.js.map +1 -1
  67. package/dist/dev-feed-tool.d.ts +106 -0
  68. package/dist/dev-feed-tool.d.ts.map +1 -0
  69. package/dist/dev-feed-tool.js +713 -0
  70. package/dist/dev-feed-tool.js.map +1 -0
  71. package/dist/dev-server-client.d.ts +67 -0
  72. package/dist/dev-server-client.d.ts.map +1 -0
  73. package/dist/dev-server-client.js +195 -0
  74. package/dist/dev-server-client.js.map +1 -0
  75. package/dist/diff-tool.d.ts +3 -3
  76. package/dist/diff-tool.d.ts.map +1 -1
  77. package/dist/diff-tool.js +6 -5
  78. package/dist/diff-tool.js.map +1 -1
  79. package/dist/docs-search.d.ts.map +1 -1
  80. package/dist/docs-search.js +12 -4
  81. package/dist/docs-search.js.map +1 -1
  82. package/dist/doctor.d.ts +17 -0
  83. package/dist/doctor.d.ts.map +1 -1
  84. package/dist/doctor.js +76 -15
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/env-file.d.ts +15 -0
  87. package/dist/env-file.d.ts.map +1 -1
  88. package/dist/env-file.js +73 -1
  89. package/dist/env-file.js.map +1 -1
  90. package/dist/fix-recipes.d.ts.map +1 -1
  91. package/dist/fix-recipes.js +41 -21
  92. package/dist/fix-recipes.js.map +1 -1
  93. package/dist/frontend-guidance.js +5 -5
  94. package/dist/frontend-guidance.js.map +1 -1
  95. package/dist/i18n-check.d.ts +16 -0
  96. package/dist/i18n-check.d.ts.map +1 -0
  97. package/dist/i18n-check.js +154 -0
  98. package/dist/i18n-check.js.map +1 -0
  99. package/dist/init-agents.d.ts +49 -13
  100. package/dist/init-agents.d.ts.map +1 -1
  101. package/dist/init-agents.js +239 -34
  102. package/dist/init-agents.js.map +1 -1
  103. package/dist/internal/code-unit-order.d.ts +4 -0
  104. package/dist/internal/code-unit-order.d.ts.map +1 -0
  105. package/dist/internal/code-unit-order.js +6 -0
  106. package/dist/internal/code-unit-order.js.map +1 -0
  107. package/dist/internal/script-kind.d.ts +7 -0
  108. package/dist/internal/script-kind.d.ts.map +1 -0
  109. package/dist/internal/script-kind.js +12 -0
  110. package/dist/internal/script-kind.js.map +1 -0
  111. package/dist/internal/source-facts.d.ts +5 -0
  112. package/dist/internal/source-facts.d.ts.map +1 -1
  113. package/dist/internal/source-facts.js +61 -5
  114. package/dist/internal/source-facts.js.map +1 -1
  115. package/dist/internal/typescript-import.d.ts +49 -24
  116. package/dist/internal/typescript-import.d.ts.map +1 -1
  117. package/dist/internal/typescript-import.js +507 -67
  118. package/dist/internal/typescript-import.js.map +1 -1
  119. package/dist/introspect.d.ts +5 -1
  120. package/dist/introspect.d.ts.map +1 -1
  121. package/dist/introspect.js +92 -27
  122. package/dist/introspect.js.map +1 -1
  123. package/dist/learn.js +3 -3
  124. package/dist/learn.js.map +1 -1
  125. package/dist/levels-tool.d.ts +10 -0
  126. package/dist/levels-tool.d.ts.map +1 -1
  127. package/dist/levels-tool.js +33 -3
  128. package/dist/levels-tool.js.map +1 -1
  129. package/dist/load.d.ts +67 -17
  130. package/dist/load.d.ts.map +1 -1
  131. package/dist/load.js +78 -18
  132. package/dist/load.js.map +1 -1
  133. package/dist/manifest-tool.d.ts.map +1 -1
  134. package/dist/manifest-tool.js +3 -2
  135. package/dist/manifest-tool.js.map +1 -1
  136. package/dist/mcp-app-tool.d.ts.map +1 -1
  137. package/dist/mcp-app-tool.js +2 -0
  138. package/dist/mcp-app-tool.js.map +1 -1
  139. package/dist/mcp-context.js +1 -1
  140. package/dist/mcp-context.js.map +1 -1
  141. package/dist/mcp-delegate.d.ts +57 -0
  142. package/dist/mcp-delegate.d.ts.map +1 -0
  143. package/dist/mcp-delegate.js +130 -0
  144. package/dist/mcp-delegate.js.map +1 -0
  145. package/dist/mcp-docs-tools.d.ts.map +1 -1
  146. package/dist/mcp-docs-tools.js +23 -5
  147. package/dist/mcp-docs-tools.js.map +1 -1
  148. package/dist/mcp-exec.d.ts +2 -0
  149. package/dist/mcp-exec.d.ts.map +1 -1
  150. package/dist/mcp-exec.js +132 -108
  151. package/dist/mcp-exec.js.map +1 -1
  152. package/dist/mcp-http.d.ts.map +1 -1
  153. package/dist/mcp-http.js +17 -5
  154. package/dist/mcp-http.js.map +1 -1
  155. package/dist/mcp-io.d.ts +14 -0
  156. package/dist/mcp-io.d.ts.map +1 -1
  157. package/dist/mcp-io.js +45 -0
  158. package/dist/mcp-io.js.map +1 -1
  159. package/dist/mcp-isolate.d.ts +48 -0
  160. package/dist/mcp-isolate.d.ts.map +1 -0
  161. package/dist/mcp-isolate.js +222 -0
  162. package/dist/mcp-isolate.js.map +1 -0
  163. package/dist/mcp-project-child.d.ts +74 -0
  164. package/dist/mcp-project-child.d.ts.map +1 -0
  165. package/dist/mcp-project-child.js +130 -0
  166. package/dist/mcp-project-child.js.map +1 -0
  167. package/dist/mcp-render.js +2 -2
  168. package/dist/mcp-render.js.map +1 -1
  169. package/dist/mcp-root.d.ts +37 -9
  170. package/dist/mcp-root.d.ts.map +1 -1
  171. package/dist/mcp-root.js +110 -23
  172. package/dist/mcp-root.js.map +1 -1
  173. package/dist/mcp-run.d.ts +18 -1
  174. package/dist/mcp-run.d.ts.map +1 -1
  175. package/dist/mcp-run.js +82 -9
  176. package/dist/mcp-run.js.map +1 -1
  177. package/dist/mcp-ws.js +4 -1
  178. package/dist/mcp-ws.js.map +1 -1
  179. package/dist/mcp.d.ts +16 -3
  180. package/dist/mcp.d.ts.map +1 -1
  181. package/dist/mcp.js +81 -53
  182. package/dist/mcp.js.map +1 -1
  183. package/dist/migrate-layout.d.ts +44 -0
  184. package/dist/migrate-layout.d.ts.map +1 -0
  185. package/dist/migrate-layout.js +982 -0
  186. package/dist/migrate-layout.js.map +1 -0
  187. package/dist/openapi-tool.d.ts +2 -2
  188. package/dist/openapi-tool.d.ts.map +1 -1
  189. package/dist/openapi-tool.js.map +1 -1
  190. package/dist/openapi-types.d.ts +2 -1
  191. package/dist/openapi-types.d.ts.map +1 -1
  192. package/dist/openapi-types.js +240 -12
  193. package/dist/openapi-types.js.map +1 -1
  194. package/dist/pipeline-report.d.ts +3 -2
  195. package/dist/pipeline-report.d.ts.map +1 -1
  196. package/dist/pipeline-report.js +19 -16
  197. package/dist/pipeline-report.js.map +1 -1
  198. package/dist/port.d.ts +5 -4
  199. package/dist/port.d.ts.map +1 -1
  200. package/dist/port.js +46 -29
  201. package/dist/port.js.map +1 -1
  202. package/dist/project-facts.d.ts +5 -0
  203. package/dist/project-facts.d.ts.map +1 -1
  204. package/dist/project-facts.js.map +1 -1
  205. package/dist/recipes/4.0.0.d.ts +9 -0
  206. package/dist/recipes/4.0.0.d.ts.map +1 -0
  207. package/dist/recipes/4.0.0.js +34 -0
  208. package/dist/recipes/4.0.0.js.map +1 -0
  209. package/dist/recipes/index.d.ts.map +1 -1
  210. package/dist/recipes/index.js +2 -0
  211. package/dist/recipes/index.js.map +1 -1
  212. package/dist/release-verification.d.ts +11 -1
  213. package/dist/release-verification.d.ts.map +1 -1
  214. package/dist/release-verification.js +47 -4
  215. package/dist/release-verification.js.map +1 -1
  216. package/dist/review.d.ts.map +1 -1
  217. package/dist/review.js +12 -8
  218. package/dist/review.js.map +1 -1
  219. package/dist/route-types.d.ts +28 -0
  220. package/dist/route-types.d.ts.map +1 -0
  221. package/dist/route-types.js +106 -0
  222. package/dist/route-types.js.map +1 -0
  223. package/dist/rules/codes.d.ts +9 -0
  224. package/dist/rules/codes.d.ts.map +1 -1
  225. package/dist/rules/codes.js +9 -0
  226. package/dist/rules/codes.js.map +1 -1
  227. package/dist/rules/data-guard.d.ts +12 -0
  228. package/dist/rules/data-guard.d.ts.map +1 -0
  229. package/dist/rules/data-guard.js +213 -0
  230. package/dist/rules/data-guard.js.map +1 -0
  231. package/dist/rules/index.d.ts +3 -0
  232. package/dist/rules/index.d.ts.map +1 -1
  233. package/dist/rules/index.js +17 -5
  234. package/dist/rules/index.js.map +1 -1
  235. package/dist/rules/islands.d.ts.map +1 -1
  236. package/dist/rules/islands.js +7 -4
  237. package/dist/rules/islands.js.map +1 -1
  238. package/dist/rules/legacy.d.ts.map +1 -1
  239. package/dist/rules/legacy.js +71 -38
  240. package/dist/rules/legacy.js.map +1 -1
  241. package/dist/rules/nano.d.ts.map +1 -1
  242. package/dist/rules/nano.js +15 -6
  243. package/dist/rules/nano.js.map +1 -1
  244. package/dist/rules/pages.d.ts +15 -0
  245. package/dist/rules/pages.d.ts.map +1 -0
  246. package/dist/rules/pages.js +88 -0
  247. package/dist/rules/pages.js.map +1 -0
  248. package/dist/rules/routes.d.ts +11 -0
  249. package/dist/rules/routes.d.ts.map +1 -1
  250. package/dist/rules/routes.js +180 -2
  251. package/dist/rules/routes.js.map +1 -1
  252. package/dist/rules/secrets.d.ts +10 -0
  253. package/dist/rules/secrets.d.ts.map +1 -0
  254. package/dist/rules/secrets.js +169 -0
  255. package/dist/rules/secrets.js.map +1 -0
  256. package/dist/rules/security.d.ts +7 -0
  257. package/dist/rules/security.d.ts.map +1 -1
  258. package/dist/rules/security.js +171 -65
  259. package/dist/rules/security.js.map +1 -1
  260. package/dist/rules/typescript.d.ts +16 -0
  261. package/dist/rules/typescript.d.ts.map +1 -0
  262. package/dist/rules/typescript.js +59 -0
  263. package/dist/rules/typescript.js.map +1 -0
  264. package/dist/rules/zones.d.ts +17 -0
  265. package/dist/rules/zones.d.ts.map +1 -0
  266. package/dist/rules/zones.js +174 -0
  267. package/dist/rules/zones.js.map +1 -0
  268. package/dist/scaffold.d.ts +25 -12
  269. package/dist/scaffold.d.ts.map +1 -1
  270. package/dist/scaffold.js +132 -87
  271. package/dist/scaffold.js.map +1 -1
  272. package/dist/sdk.d.ts +17 -3
  273. package/dist/sdk.d.ts.map +1 -1
  274. package/dist/sdk.js +941 -153
  275. package/dist/sdk.js.map +1 -1
  276. package/dist/search-terms.d.ts +2 -0
  277. package/dist/search-terms.d.ts.map +1 -1
  278. package/dist/search-terms.js +23 -8
  279. package/dist/search-terms.js.map +1 -1
  280. package/dist/smoke.d.ts +79 -0
  281. package/dist/smoke.d.ts.map +1 -0
  282. package/dist/smoke.js +494 -0
  283. package/dist/smoke.js.map +1 -0
  284. package/dist/sync-manifest.d.ts.map +1 -1
  285. package/dist/sync-manifest.js +2 -1
  286. package/dist/sync-manifest.js.map +1 -1
  287. package/dist/sync-routes.d.ts +4 -3
  288. package/dist/sync-routes.d.ts.map +1 -1
  289. package/dist/sync-routes.js +4 -3
  290. package/dist/sync-routes.js.map +1 -1
  291. package/dist/test-tool.d.ts.map +1 -1
  292. package/dist/test-tool.js +30 -11
  293. package/dist/test-tool.js.map +1 -1
  294. package/dist/types-search.d.ts.map +1 -1
  295. package/dist/types-search.js +11 -5
  296. package/dist/types-search.js.map +1 -1
  297. package/dist/upgrade.d.ts +36 -6
  298. package/dist/upgrade.d.ts.map +1 -1
  299. package/dist/upgrade.js +167 -29
  300. package/dist/upgrade.js.map +1 -1
  301. package/dist/verification-plan.d.ts.map +1 -1
  302. package/dist/verification-plan.js +4 -0
  303. package/dist/verification-plan.js.map +1 -1
  304. package/dist/verification.d.ts.map +1 -1
  305. package/dist/verification.js +45 -12
  306. package/dist/verification.js.map +1 -1
  307. package/dist/web-app-options.d.ts +19 -0
  308. package/dist/web-app-options.d.ts.map +1 -0
  309. package/dist/web-app-options.js +49 -0
  310. package/dist/web-app-options.js.map +1 -0
  311. package/dist/work-graph.d.ts.map +1 -1
  312. package/dist/work-graph.js +29 -13
  313. package/dist/work-graph.js.map +1 -1
  314. package/docs/examples.json +185 -29
  315. package/docs/llms-full.txt +3807 -580
  316. package/docs/types.json +14229 -10939
  317. package/package.json +19 -12
  318. package/src/app-files.ts +40 -0
  319. package/src/assure-hydration.ts +139 -46
  320. package/src/assure.ts +17 -2
  321. package/src/capabilities-tool.ts +35 -9
  322. package/src/cdn-check.ts +268 -0
  323. package/src/check-diagnostics.ts +17 -0
  324. package/src/check-scan.ts +358 -63
  325. package/src/check.ts +97 -62
  326. package/src/cli.ts +296 -87
  327. package/src/command-catalog.ts +374 -38
  328. package/src/config-target.ts +47 -0
  329. package/src/contracts.ts +54 -12
  330. package/src/db-audit.ts +128 -0
  331. package/src/db-child.ts +285 -0
  332. package/src/db-config.ts +224 -0
  333. package/src/db-tool.ts +690 -0
  334. package/src/dev-bun-config.ts +116 -40
  335. package/src/dev-feed-tool.ts +912 -0
  336. package/src/dev-server-client.ts +267 -0
  337. package/src/diff-tool.ts +6 -5
  338. package/src/docs-search.ts +10 -3
  339. package/src/doctor.ts +98 -15
  340. package/src/env-file.ts +78 -1
  341. package/src/fix-recipes.ts +44 -31
  342. package/src/frontend-guidance.ts +5 -5
  343. package/src/i18n-check.ts +179 -0
  344. package/src/init-agents.ts +301 -38
  345. package/src/internal/code-unit-order.ts +5 -0
  346. package/src/internal/script-kind.ts +11 -0
  347. package/src/internal/source-facts.ts +96 -22
  348. package/src/internal/typescript-import.ts +663 -68
  349. package/src/introspect.ts +90 -27
  350. package/src/learn.ts +3 -3
  351. package/src/levels-tool.ts +47 -2
  352. package/src/load.ts +140 -33
  353. package/src/manifest-tool.ts +3 -2
  354. package/src/mcp-app-tool.ts +2 -0
  355. package/src/mcp-context.ts +1 -1
  356. package/src/mcp-delegate.ts +162 -0
  357. package/src/mcp-docs-tools.ts +23 -5
  358. package/src/mcp-exec.ts +160 -132
  359. package/src/mcp-http.ts +19 -5
  360. package/src/mcp-io.ts +57 -0
  361. package/src/mcp-isolate.ts +287 -0
  362. package/src/mcp-project-child.ts +182 -0
  363. package/src/mcp-render.ts +2 -2
  364. package/src/mcp-root.ts +117 -22
  365. package/src/mcp-run.ts +104 -10
  366. package/src/mcp-ws.ts +4 -1
  367. package/src/mcp.ts +106 -64
  368. package/src/migrate-layout.ts +1121 -0
  369. package/src/openapi-tool.ts +7 -3
  370. package/src/openapi-types.ts +315 -13
  371. package/src/pipeline-report.ts +22 -18
  372. package/src/port.ts +53 -30
  373. package/src/project-facts.ts +5 -0
  374. package/src/recipes/4.0.0.ts +35 -0
  375. package/src/recipes/index.ts +2 -0
  376. package/src/release-verification.ts +74 -5
  377. package/src/review.ts +13 -8
  378. package/src/route-types.ts +133 -0
  379. package/src/rules/codes.ts +9 -0
  380. package/src/rules/data-guard.ts +230 -0
  381. package/src/rules/index.ts +18 -5
  382. package/src/rules/islands.ts +12 -4
  383. package/src/rules/legacy.ts +71 -39
  384. package/src/rules/nano.ts +18 -6
  385. package/src/rules/pages.ts +99 -0
  386. package/src/rules/routes.ts +181 -2
  387. package/src/rules/secrets.ts +174 -0
  388. package/src/rules/security.ts +198 -70
  389. package/src/rules/typescript.ts +77 -0
  390. package/src/rules/zones.ts +195 -0
  391. package/src/scaffold.ts +147 -95
  392. package/src/sdk.ts +1215 -158
  393. package/src/search-terms.ts +24 -7
  394. package/src/smoke.ts +607 -0
  395. package/src/sync-manifest.ts +2 -1
  396. package/src/sync-routes.ts +4 -3
  397. package/src/test-tool.ts +32 -11
  398. package/src/types-search.ts +11 -4
  399. package/src/upgrade.ts +205 -25
  400. package/src/verification-plan.ts +13 -0
  401. package/src/verification.ts +50 -12
  402. package/src/web-app-options.ts +57 -0
  403. package/src/work-graph.ts +27 -13
@@ -1,56 +1,146 @@
1
1
  /**
2
2
  * The one place the CLI imports TypeScript from.
3
3
  *
4
- * The interpolated-SQL rule parses source with the TypeScript compiler, because the regex it replaced
5
- * could not tell `sql\`… ${id} …\`` (bound) from a plain template (injectable) without guessing at
6
- * string and comment boundaries. That is the right tool, but it is a ~25 MB dependency, and the CLI's
7
- * own typecheck step already treats `tsc` as something the PROJECT provides rather than something the
8
- * CLI ships. Making it a hard dependency would have contradicted that for every install.
9
- *
10
- * So it is an optional peer, resolved at the moment the rule runs. Every Nifra project has TypeScript
11
- * - the templates all ship a `typecheck` script - so this resolves in practice; the point is that an
12
- * install does not pay for it.
4
+ * The CLI supports the legacy compiler API shipped by TypeScript 5 and 6 and the
5
+ * split API shipped by TypeScript 7. The scanners only need a small AST surface,
6
+ * so TypeScript 7 is presented through a compatibility facade rather than leaking
7
+ * either compiler's concrete types through the rule registry.
13
8
  */
14
9
 
15
- import { existsSync } from "node:fs"
16
- import { dirname, join } from "node:path"
10
+ import { existsSync, readFileSync, realpathSync } from "node:fs"
11
+ import { dirname, isAbsolute, join, resolve } from "node:path"
12
+ import { pathToFileURL } from "node:url"
13
+ import { type Diagnostic, diagnostic } from "../diagnostics.ts"
17
14
 
18
- /** The slice of the compiler the SQL scanner uses. Structural, so the peer stays optional. */
15
+ /** The legacy compiler API used by the syntax-only scanners and the compatibility facade. */
19
16
  export type TypeScriptApi = typeof import("typescript")
20
17
 
18
+ export const NIFRA_TS_UNSUPPORTED = "NIFRA_TS_UNSUPPORTED" as const
19
+
20
+ /** A project compiler was found, but its API is outside the scanner adapter's supported range. */
21
+ export class UnsupportedTypeScriptError extends Error {
22
+ readonly code = NIFRA_TS_UNSUPPORTED
23
+ readonly version: string
24
+
25
+ constructor(version: string) {
26
+ super(unsupportedTypeScriptMessage(version))
27
+ this.name = "UnsupportedTypeScriptError"
28
+ this.version = version
29
+ }
30
+ }
31
+
32
+ export function isUnsupportedTypeScriptError(error: unknown): error is UnsupportedTypeScriptError {
33
+ if (error instanceof UnsupportedTypeScriptError) return true
34
+ if (typeof error !== "object" || error === null) return false
35
+ const candidate = error as {
36
+ readonly code?: unknown
37
+ readonly version?: unknown
38
+ readonly message?: unknown
39
+ }
40
+ return (
41
+ candidate.code === NIFRA_TS_UNSUPPORTED &&
42
+ typeof candidate.version === "string" &&
43
+ typeof candidate.message === "string"
44
+ )
45
+ }
46
+
47
+ export function unsupportedTypeScriptMessage(version: string): string {
48
+ return `${NIFRA_TS_UNSUPPORTED}: TypeScript ${version} is outside the compiler API versions supported by Nifra scanners. Use TypeScript 5, 6, or 7, or pin the project to TypeScript 6 (for example, \`bun add -d typescript@^6\`).`
49
+ }
50
+
51
+ export function unsupportedTypeScriptDiagnostic(version: string): Diagnostic {
52
+ return diagnostic({
53
+ code: NIFRA_TS_UNSUPPORTED,
54
+ severity: "error",
55
+ message: unsupportedTypeScriptMessage(version),
56
+ fix: {
57
+ recipe: "toolchain.install-typescript",
58
+ command: "bun add -d typescript@^6",
59
+ },
60
+ verify: "nifra check --lints-only",
61
+ })
62
+ }
63
+
64
+ /** Source files supplied to the TypeScript 7 virtual file system. */
65
+ export interface TypeScriptSourceIndex {
66
+ readonly files: readonly string[]
67
+ read(file: string): string | undefined
68
+ /** Optional temporary project config used by semantic adapters (for example OpenAPI probes). */
69
+ readonly projectConfig?: string
70
+ }
71
+
72
+ /** A compiler plus the process/session that owns it. */
73
+ export interface TypeScriptSession {
74
+ readonly compiler: TypeScriptApi
75
+ /** TypeScript 7's asynchronous semantic API, present when a project program was opened. */
76
+ readonly semantic?: TypeScriptSemanticSession
77
+ close(): Promise<void>
78
+ }
79
+
80
+ export interface TypeScriptSemanticSession {
81
+ readonly checker: unknown
82
+ readonly getCheckerForFile: (file: string) => Promise<unknown | undefined>
83
+ readonly getSourceFile: (file: string) => Promise<unknown | undefined>
84
+ readonly typeFlags: Readonly<Record<string, unknown>>
85
+ readonly symbolFlags: Readonly<Record<string, unknown>>
86
+ readonly signatureKind: Readonly<Record<string, unknown>>
87
+ }
88
+
21
89
  /**
22
- * Is this module actually the compiler, rather than something else published under the same name?
23
- *
24
- * The `typescript` package name also carries small non-compiler entry points (a version stub, for
25
- * one), and a resolver that falls back to a global download cache can hand one of those back. They
26
- * import cleanly and then explode on first use - `undefined is not an object (evaluating
27
- * 'ts.ScriptKind.TSX')` deep inside a scan - which reads as a Nifra bug rather than as "the compiler
28
- * we loaded is not a compiler". Checking the two members every caller needs turns that into a
29
- * resolution miss, which the fallbacks below already know how to handle.
90
+ * Is this module actually the legacy compiler, rather than TypeScript 7's version
91
+ * entry point or another package published under the same name?
30
92
  */
31
93
  function isCompiler(module: unknown): module is TypeScriptApi {
32
94
  if (typeof module !== "object" || module === null) return false
33
95
  const api = module as Partial<TypeScriptApi>
34
- return typeof api.createSourceFile === "function" && api.ScriptKind !== undefined
96
+ return (
97
+ typeof api.createSourceFile === "function" &&
98
+ api.ScriptKind !== undefined &&
99
+ typeof api.forEachChild === "function"
100
+ )
35
101
  }
36
102
 
37
103
  /** Unwrap the CJS default interop wrapper, so both `import * as ts` shapes look the same. */
38
104
  function compilerOf(module: unknown): TypeScriptApi | undefined {
39
105
  if (isCompiler(module)) return module
40
- const wrapped = (module as { default?: unknown } | undefined)?.default
106
+ const wrapped = (module as { readonly default?: unknown } | undefined)?.default
41
107
  return isCompiler(wrapped) ? wrapped : undefined
42
108
  }
43
109
 
44
- /**
45
- * Import TypeScript from the CLI's own dependency tree, or `undefined` when it is not installed.
46
- *
47
- * Only a RESOLUTION failure is absence. A compiler that resolves and then fails while evaluating is a
48
- * broken install, not a missing one, and telling that user to install what they already have sends
49
- * them the wrong way - so that error is rethrown for the caller to report.
50
- */
51
- export async function importTypeScript(): Promise<TypeScriptApi | undefined> {
110
+ function versionOf(module: unknown): string | undefined {
111
+ if (typeof module !== "object" || module === null) return undefined
112
+ const version = (module as { readonly version?: unknown }).version
113
+ if (typeof version === "string") return version
114
+ const wrapped = (module as { readonly default?: unknown }).default
115
+ if (typeof wrapped !== "object" || wrapped === null) return undefined
116
+ const wrappedVersion = (wrapped as { readonly version?: unknown }).version
117
+ return typeof wrappedVersion === "string" ? wrappedVersion : undefined
118
+ }
119
+
120
+ function majorVersion(version: string): number | undefined {
121
+ const match = /^(\d+)(?:\.|$)/.exec(version.trim())
122
+ if (match === null) return undefined
123
+ const major = Number.parseInt(match[1]!, 10)
124
+ return Number.isSafeInteger(major) ? major : undefined
125
+ }
126
+
127
+ /** Keep unknown fixture versions usable, but reject known compiler majors outside 5/6/7. */
128
+ function isUnsupportedVersion(version: string): boolean {
129
+ const major = majorVersion(version)
130
+ return major !== undefined && major > 0 && major !== 5 && major !== 6 && major !== 7
131
+ }
132
+
133
+ function packageVersion(packageRoot: string): string | undefined {
134
+ const parsed: unknown = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"))
135
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined
136
+ const version = (parsed as { readonly version?: unknown }).version
137
+ return typeof version === "string" ? version : undefined
138
+ }
139
+
140
+ /** Import TypeScript from the CLI's own dependency tree, or `undefined` when it is not installed. */
141
+ async function importTypeScriptModule(): Promise<unknown | undefined> {
52
142
  try {
53
- return compilerOf(await import("typescript"))
143
+ return await import("typescript")
54
144
  } catch (cause) {
55
145
  const message = cause instanceof Error ? cause.message : String(cause)
56
146
  if (/cannot find (?:package|module)|ERR_MODULE_NOT_FOUND/i.test(message)) return undefined
@@ -58,55 +148,560 @@ export async function importTypeScript(): Promise<TypeScriptApi | undefined> {
58
148
  }
59
149
  }
60
150
 
151
+ /** Import the CLI's own TypeScript only for legacy callers. Project loads use the adapter below. */
152
+ export async function importTypeScript(): Promise<TypeScriptApi | undefined> {
153
+ const module = await importTypeScriptModule()
154
+ if (module === undefined) return undefined
155
+ const version = versionOf(module)
156
+ if (version !== undefined && isUnsupportedVersion(version))
157
+ throw new UnsupportedTypeScriptError(version)
158
+ return compilerOf(module)
159
+ }
160
+
161
+ function packageRootForResolvedEntry(entry: string): string {
162
+ return dirname(dirname(entry))
163
+ }
164
+
165
+ interface ResolvedProjectTypeScript {
166
+ readonly packageRoot: string
167
+ readonly entry: string
168
+ readonly version: string | undefined
169
+ }
170
+
61
171
  /**
62
- * Find the project's compiler the way module resolution would: `node_modules/typescript` in `root`,
63
- * then each parent directory up to the filesystem root - the same walk the typecheck gate uses to
64
- * find `tsc`, so both gates agree on which compiler the project has.
65
- *
66
- * The walk is deliberate, not a fallback for a missing resolver. `Bun.resolveSync` memoizes a
67
- * specifier for the life of the process AND auto-installs from the global download cache when the
68
- * project has none, so a long-lived process (the MCP server) that first resolved `typescript` before
69
- * `bun install` kept handing back that cache entry afterwards - the typecheck went phantom until the
70
- * server was restarted. A filesystem probe is re-answered every call, so an install that lands
71
- * mid-session is picked up on the next scan.
172
+ * Find the project's compiler the way module resolution would: `node_modules/typescript` in
173
+ * `root`, then each parent directory up to the filesystem root. This filesystem probe is
174
+ * deliberate: Bun's resolver can memoize a missing package for a long-lived MCP process.
72
175
  */
73
- function resolveProjectTypeScript(root: string): string | undefined {
176
+ function resolveProjectTypeScript(root: string): ResolvedProjectTypeScript | undefined {
74
177
  let dir = root
75
178
  while (true) {
76
- const pkg = join(dir, "node_modules", "typescript")
77
- // `lib/typescript.js` is the compiler entry across every published major; probing it directly
78
- // (rather than importing the directory) keeps a package whose main entry is a stub out of play.
79
- const entry = join(pkg, "lib", "typescript.js")
80
- if (existsSync(entry)) return entry
81
- if (existsSync(join(pkg, "package.json"))) return pkg
179
+ const packageRoot = join(dir, "node_modules", "typescript")
180
+ const entry = join(packageRoot, "lib", "typescript.js")
181
+ if (existsSync(entry)) return { packageRoot, entry, version: packageVersion(packageRoot) }
182
+ if (existsSync(join(packageRoot, "package.json"))) {
183
+ return { packageRoot, entry: packageRoot, version: packageVersion(packageRoot) }
184
+ }
82
185
  const parent = dirname(dir)
83
186
  if (parent === dir) return undefined
84
187
  dir = parent
85
188
  }
86
189
  }
87
190
 
191
+ function normalizedPath(path: string): string {
192
+ return resolve(path).replaceAll("\\", "/")
193
+ }
194
+
195
+ // TypeScript 7 canonicalizes Windows paths case-insensitively before virtual-FS callbacks.
196
+ function pathIdentity(path: string): string {
197
+ const normalized = normalizedPath(path)
198
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized
199
+ }
200
+
201
+ function realPath(path: string): string {
202
+ try {
203
+ // Bun's portable realpath can preserve an 8.3 spelling on Windows; the native
204
+ // implementation resolves the same spelling used by the TypeScript 7 service.
205
+ return normalizedPath(realpathSync.native(path))
206
+ } catch {
207
+ return normalizedPath(realpathSync(path))
208
+ }
209
+ }
210
+
211
+ function canonicalRoot(root: string): string {
212
+ try {
213
+ return realPath(root)
214
+ } catch {
215
+ // Virtual project roots may not exist yet; the resolved spelling is still the best identity.
216
+ return normalizedPath(root)
217
+ }
218
+ }
219
+
220
+ function pathVariants(path: string): readonly string[] {
221
+ const variants = new Set<string>()
222
+ const add = (candidate: string): void => {
223
+ const normalized = normalizedPath(candidate)
224
+ variants.add(normalized)
225
+ if (process.platform === "win32") variants.add(normalized.toLowerCase())
226
+ }
227
+ add(path)
228
+ try {
229
+ add(realPath(path))
230
+ } catch {
231
+ // Virtual fixture files do not exist on disk; their normalized path is sufficient.
232
+ }
233
+ return [...variants]
234
+ }
235
+
236
+ interface TypeScript7Node {
237
+ readonly kind: number
238
+ readonly pos: number
239
+ readonly end: number
240
+ readonly text?: string
241
+ forEachChild<T>(
242
+ visit: (node: TypeScript7Node) => T,
243
+ visitNodes?: (nodes: unknown) => T,
244
+ ): T | undefined
245
+ getStart(sourceFile?: TypeScript7SourceFile): number
246
+ getText(sourceFile?: TypeScript7SourceFile): string
247
+ }
248
+
249
+ interface TypeScript7SourceFile extends TypeScript7Node {
250
+ readonly fileName: string
251
+ readonly text: string
252
+ readonly statements: readonly TypeScript7Node[]
253
+ getLineAndCharacterOfPosition(position: number): {
254
+ readonly line: number
255
+ readonly character: number
256
+ }
257
+ }
258
+
259
+ interface TypeScript7Program {
260
+ getSourceFile(file: string): Promise<TypeScript7SourceFile | undefined>
261
+ getSyntacticDiagnostics(file?: string): Promise<readonly unknown[]>
262
+ }
263
+
264
+ interface TypeScript7Project {
265
+ readonly configFileName: string
266
+ readonly rootFiles: readonly string[]
267
+ readonly program: TypeScript7Program
268
+ readonly checker: unknown
269
+ }
270
+
271
+ interface TypeScript7Snapshot {
272
+ getProject(configFileName: string): TypeScript7Project | undefined
273
+ getDefaultProjectForFile(file: string): Promise<TypeScript7Project | undefined>
274
+ dispose(): Promise<void> | void
275
+ }
276
+
277
+ interface TypeScript7Api {
278
+ updateSnapshot(params: {
279
+ readonly openFiles: readonly string[]
280
+ readonly openProject?: string
281
+ }): Promise<TypeScript7Snapshot>
282
+ close(): Promise<void>
283
+ }
284
+
285
+ interface TypeScript7ApiModule {
286
+ readonly API: new (options?: Record<string, unknown>) => TypeScript7Api
287
+ }
288
+
289
+ type UnknownRecord = Record<string, unknown>
290
+
291
+ function requiredRecord(value: unknown, label: string): UnknownRecord {
292
+ if (typeof value !== "object" || value === null)
293
+ throw new Error(`TypeScript 7 ${label} export is invalid`)
294
+ return value as UnknownRecord
295
+ }
296
+
297
+ function requiredConstructor(value: unknown, label: string): TypeScript7ApiModule["API"] {
298
+ if (typeof value !== "function") throw new Error(`TypeScript 7 ${label} export is invalid`)
299
+ return value as TypeScript7ApiModule["API"]
300
+ }
301
+
302
+ function requiredNumberMap(module: UnknownRecord, name: string): UnknownRecord {
303
+ return requiredRecord(module[name], name)
304
+ }
305
+
306
+ function hasNodeKind(node: unknown, kinds: ReadonlySet<number>): boolean {
307
+ if (typeof node !== "object" || node === null) return false
308
+ const kind = (node as { readonly kind?: unknown }).kind
309
+ return typeof kind === "number" && kinds.has(kind)
310
+ }
311
+
312
+ function syntaxKinds(module: UnknownRecord, names: readonly string[]): ReadonlySet<number> {
313
+ const syntaxKind = requiredNumberMap(module, "SyntaxKind")
314
+ const values = names.flatMap((name) => {
315
+ const value = syntaxKind[name]
316
+ return typeof value === "number" ? [value] : []
317
+ })
318
+ return new Set(values)
319
+ }
320
+
321
+ function createVirtualFileSystem(
322
+ sourceFiles: ReadonlyMap<string, string>,
323
+ root: string,
324
+ ): UnknownRecord {
325
+ const rootPaths = pathVariants(root)
326
+ const virtualFiles: string[] = []
327
+ const seenFiles = new Set<string>()
328
+ for (const file of sourceFiles.keys()) {
329
+ const identity = pathIdentity(file)
330
+ if (seenFiles.has(identity)) continue
331
+ seenFiles.add(identity)
332
+ virtualFiles.push(normalizedPath(file))
333
+ }
334
+ const virtualDirectories = new Set<string>()
335
+ let hasVirtualFileInRoot = false
336
+ for (const file of virtualFiles) {
337
+ const normalized = normalizedPath(file)
338
+ const rootPath = rootPaths.find(
339
+ (candidate) => normalized === candidate || normalized.startsWith(`${candidate}/`),
340
+ )
341
+ if (rootPath === undefined) continue
342
+ if (!hasVirtualFileInRoot) {
343
+ // Leave an empty overlay transparent so real project files remain discoverable.
344
+ for (const candidate of rootPaths) virtualDirectories.add(candidate)
345
+ hasVirtualFileInRoot = true
346
+ }
347
+ let directory = normalizedPath(dirname(normalized))
348
+ while (directory === rootPath || directory.startsWith(`${rootPath}/`)) {
349
+ for (const variant of pathVariants(directory)) virtualDirectories.add(variant)
350
+ if (directory === rootPath) break
351
+ directory = normalizedPath(dirname(directory))
352
+ }
353
+ }
354
+ const lookup = (fileName: string): string | undefined => {
355
+ for (const variant of pathVariants(fileName)) {
356
+ const content = sourceFiles.get(variant)
357
+ if (content !== undefined) return content
358
+ }
359
+ return undefined
360
+ }
361
+ return {
362
+ readFile: (fileName: string): string | undefined => lookup(fileName),
363
+ fileExists: (fileName: string): boolean | undefined =>
364
+ lookup(fileName) !== undefined || undefined,
365
+ directoryExists: (directoryName: string): boolean | undefined => {
366
+ const normalized = pathVariants(directoryName).find((variant) =>
367
+ virtualDirectories.has(variant),
368
+ )
369
+ return normalized === undefined ? undefined : true
370
+ },
371
+ getAccessibleEntries: (
372
+ directoryName: string,
373
+ ):
374
+ | { readonly files: readonly string[]; readonly directories: readonly string[] }
375
+ | undefined => {
376
+ const directory = pathVariants(directoryName).find((variant) =>
377
+ virtualDirectories.has(variant),
378
+ )
379
+ if (directory === undefined) return undefined
380
+ const directoryIdentity = pathIdentity(directory)
381
+ const prefix = directoryIdentity.endsWith("/") ? directoryIdentity : `${directoryIdentity}/`
382
+ const files = new Set<string>()
383
+ const directories = new Set<string>()
384
+ for (const file of virtualFiles) {
385
+ const normalized = normalizedPath(file)
386
+ const identity = pathIdentity(normalized)
387
+ if (!identity.startsWith(prefix)) continue
388
+ const remainder = normalized.slice(prefix.length)
389
+ const slash = remainder.indexOf("/")
390
+ if (slash === -1) files.add(remainder)
391
+ else directories.add(remainder.slice(0, slash))
392
+ }
393
+ return { files: [...files], directories: [...directories] }
394
+ },
395
+ realpath: (path: string): string | undefined => {
396
+ const normalized = pathVariants(path).find(
397
+ (variant) => sourceFiles.has(variant) || virtualDirectories.has(variant),
398
+ )
399
+ return normalized
400
+ },
401
+ }
402
+ }
403
+
404
+ function sourceEntries(
405
+ root: string,
406
+ sourceIndex: TypeScriptSourceIndex | undefined,
407
+ ): { readonly files: Map<string, string>; readonly paths: string[] } {
408
+ const files = new Map<string, string>()
409
+ if (sourceIndex === undefined) return { files, paths: [] }
410
+ const paths: string[] = []
411
+ for (const file of sourceIndex.files) {
412
+ const content = sourceIndex.read(file)
413
+ if (content === undefined) continue
414
+ const absolute = isAbsolute(file) ? file : resolve(root, file)
415
+ const normalized = normalizedPath(absolute)
416
+ paths.push(normalized)
417
+ for (const variant of pathVariants(normalized)) files.set(variant, content)
418
+ }
419
+ return { files, paths: [...new Set(paths)] }
420
+ }
421
+
422
+ /** Build a TypeScript 7 facade over unstable/ast and the async project API. */
423
+ async function createTypeScript7Session(
424
+ root: string,
425
+ packageRoot: string,
426
+ version: string,
427
+ sourceIndex: TypeScriptSourceIndex | undefined,
428
+ ): Promise<TypeScriptSession> {
429
+ // Resolve subpaths from the exact package installation selected above. Resolving from `root`
430
+ // alone can select a different hoisted TypeScript copy when this loader is used from a CLI that
431
+ // has its own peer installation.
432
+ const packageResolveRoot = dirname(packageRoot)
433
+ const astPath = Bun.resolveSync("typescript/unstable/ast", packageResolveRoot)
434
+ const asyncPath = Bun.resolveSync("typescript/unstable/async", packageResolveRoot)
435
+ const astModule = requiredRecord(await import(pathToFileURL(astPath).href), "ast")
436
+ const asyncModule = requiredRecord(await import(pathToFileURL(asyncPath).href), "async")
437
+ const API = requiredConstructor(asyncModule.API, "async.API")
438
+ // Windows can return a DOS 8.3 spelling from os.tmpdir() (for example RUNNER~1), while the
439
+ // TypeScript 7 project service resolves the same directory to its long spelling. Keep cwd,
440
+ // virtual files, and open files on one canonical path identity so project discovery is stable.
441
+ const sessionRoot = canonicalRoot(root)
442
+ const { files, paths } = sourceEntries(sessionRoot, sourceIndex)
443
+ const projectConfig = sourceIndex?.projectConfig ?? join(sessionRoot, "tsconfig.json")
444
+ const sourceCache = new Map<string, TypeScript7SourceFile>()
445
+ const sourceByContent = new Map<string, TypeScript7SourceFile>()
446
+ const projects = new Map<string, TypeScript7Project>()
447
+
448
+ const api = new API({
449
+ cwd: sessionRoot,
450
+ fs: createVirtualFileSystem(files, sessionRoot),
451
+ })
452
+ let snapshot: TypeScript7Snapshot | undefined
453
+ let configuredProject: TypeScript7Project | undefined
454
+ let closed = false
455
+ try {
456
+ snapshot = await api.updateSnapshot({
457
+ openFiles: paths,
458
+ ...(existsSync(projectConfig) ? { openProject: projectConfig } : {}),
459
+ })
460
+ configuredProject =
461
+ sourceIndex?.projectConfig === undefined ? undefined : snapshot.getProject(projectConfig)
462
+ if (sourceIndex?.projectConfig !== undefined && configuredProject === undefined) {
463
+ throw new Error(`TypeScript 7 could not open project config ${projectConfig}`)
464
+ }
465
+ for (const path of paths) {
466
+ const project = configuredProject ?? (await snapshot.getDefaultProjectForFile(path))
467
+ if (project === undefined) {
468
+ throw new Error(`TypeScript 7 could not create a project for source file ${path}`)
469
+ }
470
+ projects.set(pathIdentity(path), project)
471
+ const source = await project.program.getSourceFile(path)
472
+ if (source === undefined) {
473
+ throw new Error(`TypeScript 7 could not load project source file ${path}`)
474
+ }
475
+ for (const variant of pathVariants(path)) sourceCache.set(variant, source)
476
+ for (const variant of pathVariants(source.fileName)) sourceCache.set(variant, source)
477
+ sourceByContent.set(source.text, source)
478
+ const diagnostics = await project.program.getSyntacticDiagnostics(path)
479
+ // Legacy scanners use parseDiagnostics to avoid interpreting recovery nodes. The remote
480
+ // TypeScript 7 source object is extensible in current releases; define the same optional
481
+ // property without copying source text or changing node identities.
482
+ try {
483
+ Object.defineProperty(source, "parseDiagnostics", {
484
+ configurable: true,
485
+ enumerable: false,
486
+ value: diagnostics,
487
+ })
488
+ } catch {
489
+ // A future remote source implementation may be sealed. The scanner remains conservative
490
+ // for files whose syntax diagnostics are not observable through the compatibility object.
491
+ }
492
+ }
493
+ } catch (error) {
494
+ await snapshot?.dispose()
495
+ await api.close()
496
+ throw error
497
+ }
498
+
499
+ const functionLikeKinds = syntaxKinds(astModule, [
500
+ "FunctionDeclaration",
501
+ "FunctionExpression",
502
+ "ArrowFunction",
503
+ "MethodDeclaration",
504
+ "GetAccessor",
505
+ "SetAccessor",
506
+ "Constructor",
507
+ "MethodSignature",
508
+ "CallSignature",
509
+ "ConstructSignature",
510
+ "IndexSignature",
511
+ "FunctionType",
512
+ "ConstructorType",
513
+ "JSDocFunctionType",
514
+ "JSDocSignature",
515
+ ])
516
+ const parameterKinds = syntaxKinds(astModule, ["Parameter"])
517
+ const stringLiteralKinds = syntaxKinds(astModule, [
518
+ "StringLiteral",
519
+ "NoSubstitutionTemplateLiteral",
520
+ ])
521
+
522
+ const createSourceFile = (_fileName: unknown, sourceText: unknown): unknown => {
523
+ if (typeof _fileName !== "string" || typeof sourceText !== "string") {
524
+ throw new TypeError("TypeScript 7 createSourceFile received invalid arguments")
525
+ }
526
+ const absolute = isAbsolute(_fileName) ? _fileName : resolve(sessionRoot, _fileName)
527
+ const cached = pathVariants(absolute)
528
+ .map((variant) => sourceCache.get(variant))
529
+ .find((source): source is TypeScript7SourceFile => source !== undefined)
530
+ if (cached !== undefined && cached.text === sourceText) return cached
531
+ const byContent = sourceByContent.get(sourceText)
532
+ if (byContent !== undefined) return byContent
533
+ throw new Error(`TypeScript 7 source file was not preloaded: ${_fileName}`)
534
+ }
535
+ const forEachChild = (node: unknown, visit: unknown): unknown => {
536
+ if (typeof visit !== "function")
537
+ throw new TypeError("TypeScript 7 forEachChild visitor is invalid")
538
+ if (typeof node !== "object" || node === null)
539
+ throw new TypeError("TypeScript 7 node is invalid")
540
+ const childVisitor = visit as (child: TypeScript7Node) => unknown
541
+ return (node as TypeScript7Node).forEachChild(childVisitor)
542
+ }
543
+ const isFunctionLike = (node: unknown): boolean => hasNodeKind(node, functionLikeKinds)
544
+ const isParameter = (node: unknown): boolean => hasNodeKind(node, parameterKinds)
545
+ const isStringLiteralLike = (node: unknown): boolean => hasNodeKind(node, stringLiteralKinds)
546
+
547
+ const facade: UnknownRecord = {
548
+ ...astModule,
549
+ version,
550
+ createSourceFile,
551
+ forEachChild,
552
+ isFunctionLike,
553
+ isParameter,
554
+ isStringLiteralLike,
555
+ }
556
+ const compiler = Object.freeze(facade) as unknown as TypeScriptApi
557
+
558
+ const firstProject = projects.values().next().value as TypeScript7Project | undefined
559
+ const semantic =
560
+ firstProject === undefined
561
+ ? undefined
562
+ : ({
563
+ checker: firstProject.checker,
564
+ getCheckerForFile: async (file: string): Promise<unknown | undefined> => {
565
+ const project =
566
+ projects.get(pathIdentity(file)) ??
567
+ configuredProject ??
568
+ (await snapshot!.getDefaultProjectForFile(file))
569
+ return project?.checker
570
+ },
571
+ getSourceFile: async (file: string): Promise<unknown | undefined> => {
572
+ const project =
573
+ projects.get(pathIdentity(file)) ??
574
+ configuredProject ??
575
+ (await snapshot!.getDefaultProjectForFile(file))
576
+ return project?.program.getSourceFile(file)
577
+ },
578
+ typeFlags: requiredNumberMap(asyncModule, "TypeFlags"),
579
+ symbolFlags: requiredNumberMap(asyncModule, "SymbolFlags"),
580
+ signatureKind: requiredNumberMap(asyncModule, "SignatureKind"),
581
+ } satisfies TypeScriptSemanticSession)
582
+
583
+ const close = async (): Promise<void> => {
584
+ if (closed) return
585
+ closed = true
586
+ await snapshot?.dispose()
587
+ await api.close()
588
+ }
589
+ return {
590
+ compiler,
591
+ ...(semantic === undefined ? {} : { semantic }),
592
+ close,
593
+ }
594
+ }
595
+
596
+ function legacySession(compiler: TypeScriptApi): TypeScriptSession {
597
+ return { compiler, close: async () => {} }
598
+ }
599
+
600
+ interface ResolvedLoad {
601
+ readonly session: TypeScriptSession | undefined
602
+ readonly unsupported: UnsupportedTypeScriptError | undefined
603
+ }
604
+
605
+ async function resolveProjectSession(
606
+ root: string,
607
+ sourceIndex: TypeScriptSourceIndex | undefined,
608
+ ): Promise<ResolvedLoad> {
609
+ const resolved = resolveProjectTypeScript(root)
610
+ if (resolved === undefined) {
611
+ const module = await importTypeScriptModule()
612
+ if (module === undefined) return { session: undefined, unsupported: undefined }
613
+ const version = versionOf(module)
614
+ if (version !== undefined && isUnsupportedVersion(version))
615
+ throw new UnsupportedTypeScriptError(version)
616
+ if (majorVersion(version ?? "") === 7) {
617
+ const entry = Bun.resolveSync("typescript", import.meta.dir)
618
+ return {
619
+ session: await createTypeScript7Session(
620
+ root,
621
+ packageRootForResolvedEntry(entry),
622
+ version ?? "7",
623
+ sourceIndex,
624
+ ),
625
+ unsupported: undefined,
626
+ }
627
+ }
628
+ const compiler = compilerOf(module)
629
+ return {
630
+ session: compiler === undefined ? undefined : legacySession(compiler),
631
+ unsupported: undefined,
632
+ }
633
+ }
634
+ if (resolved.version !== undefined && isUnsupportedVersion(resolved.version)) {
635
+ throw new UnsupportedTypeScriptError(resolved.version)
636
+ }
637
+ const major = resolved.version === undefined ? undefined : majorVersion(resolved.version)
638
+ if (major === 7) {
639
+ return {
640
+ session: await createTypeScript7Session(
641
+ root,
642
+ resolved.packageRoot,
643
+ resolved.version ?? "7",
644
+ sourceIndex,
645
+ ),
646
+ unsupported: undefined,
647
+ }
648
+ }
649
+ const module = await import(pathToFileURL(resolved.entry).href)
650
+ const version = resolved.version ?? versionOf(module)
651
+ if (version !== undefined && isUnsupportedVersion(version))
652
+ throw new UnsupportedTypeScriptError(version)
653
+ const compiler = compilerOf(module)
654
+ if (compiler === undefined) {
655
+ throw new Error(
656
+ `TypeScript package at ${resolved.packageRoot} does not expose the legacy compiler API`,
657
+ )
658
+ }
659
+ return { session: legacySession(compiler), unsupported: undefined }
660
+ }
661
+
88
662
  /**
89
- * Import TypeScript resolved from the PROJECT root first, falling back to the CLI's own dependency
90
- * tree. A bare `import("typescript")` resolves relative to this file - the CLI's install - so a
91
- * globally-installed or bunx-run CLI could miss the project's compiler (or load a different major)
92
- * depending on where the CLI happened to live, while the project's own `typescript` sat unused in
93
- * its node_modules. Resolving from `root` makes the verdicts a function of the project, not of how
94
- * the CLI was installed - the same cwd-invariance the typecheck gate's tsc resolution has.
663
+ * Import TypeScript resolved from the PROJECT root first, with TypeScript 7's package exports
664
+ * selected explicitly. A project TypeScript 7 install never falls back to the CLI's compiler.
95
665
  */
96
- export async function importProjectTypeScript(root: string): Promise<TypeScriptApi | undefined> {
97
- const resolved = resolveProjectTypeScript(root)
98
- // Not installed in the project (or installed without a compiler entry): fall back to the CLI's own
99
- // tree, which keeps the previous behavior as the floor.
100
- if (resolved === undefined) return importTypeScript()
666
+ export async function importProjectTypeScript(
667
+ root: string,
668
+ sourceIndex?: TypeScriptSourceIndex,
669
+ ): Promise<TypeScriptApi | undefined> {
670
+ const loaded = await resolveProjectSession(root, sourceIndex)
671
+ return loaded.session?.compiler
672
+ }
673
+
674
+ export interface ProjectTypeScriptLoad {
675
+ readonly compiler: TypeScriptApi | undefined
676
+ readonly session: TypeScriptSession | undefined
677
+ readonly unsupported: UnsupportedTypeScriptError | undefined
678
+ }
679
+
680
+ /** Load a project compiler for scanners while preserving a structured unsupported-toolchain result. */
681
+ export async function loadProjectTypeScript(
682
+ root: string,
683
+ load?: () => Promise<TypeScriptApi | undefined>,
684
+ sourceIndex?: TypeScriptSourceIndex,
685
+ ): Promise<ProjectTypeScriptLoad> {
101
686
  try {
102
- const compiler = compilerOf(await import(resolved))
103
- return compiler ?? (await importTypeScript())
104
- } catch (cause) {
105
- // Same contract as importTypeScript: a compiler that RESOLVES and then fails evaluating is a
106
- // broken install, not a missing one - rethrow rather than silently checking with another copy.
107
- const message = cause instanceof Error ? cause.message : String(cause)
108
- if (/cannot find (?:package|module)|ERR_MODULE_NOT_FOUND/i.test(message))
109
- return importTypeScript()
110
- throw cause
687
+ if (load !== undefined) {
688
+ const compiler = await load()
689
+ return {
690
+ compiler,
691
+ session: compiler === undefined ? undefined : legacySession(compiler),
692
+ unsupported: undefined,
693
+ }
694
+ }
695
+ const resolved = await resolveProjectSession(root, sourceIndex)
696
+ return {
697
+ compiler: resolved.session?.compiler,
698
+ session: resolved.session,
699
+ unsupported: resolved.unsupported,
700
+ }
701
+ } catch (error) {
702
+ if (isUnsupportedTypeScriptError(error)) {
703
+ return { compiler: undefined, session: undefined, unsupported: error }
704
+ }
705
+ throw error
111
706
  }
112
707
  }