@nifrajs/cli 4.0.0 → 5.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/README.md +3 -0
  2. package/dist/agent-files.d.ts +107 -0
  3. package/dist/agent-files.d.ts.map +1 -0
  4. package/dist/agent-files.js +162 -0
  5. package/dist/agent-files.js.map +1 -0
  6. package/dist/app-files.d.ts +6 -0
  7. package/dist/app-files.d.ts.map +1 -1
  8. package/dist/app-files.js +27 -2
  9. package/dist/app-files.js.map +1 -1
  10. package/dist/assure-hydration.d.ts.map +1 -1
  11. package/dist/assure-hydration.js +7 -2
  12. package/dist/assure-hydration.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts.map +1 -1
  14. package/dist/capabilities-tool.js +13 -3
  15. package/dist/capabilities-tool.js.map +1 -1
  16. package/dist/check-diagnostics.d.ts +20 -2
  17. package/dist/check-diagnostics.d.ts.map +1 -1
  18. package/dist/check-diagnostics.js +54 -7
  19. package/dist/check-diagnostics.js.map +1 -1
  20. package/dist/check-scan.d.ts +75 -22
  21. package/dist/check-scan.d.ts.map +1 -1
  22. package/dist/check-scan.js +1475 -310
  23. package/dist/check-scan.js.map +1 -1
  24. package/dist/check.d.ts +2 -2
  25. package/dist/check.d.ts.map +1 -1
  26. package/dist/check.js +192 -65
  27. package/dist/check.js.map +1 -1
  28. package/dist/cli.d.ts +7 -0
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +112 -5
  31. package/dist/cli.js.map +1 -1
  32. package/dist/client-module.d.ts +18 -0
  33. package/dist/client-module.d.ts.map +1 -0
  34. package/dist/client-module.js +49 -0
  35. package/dist/client-module.js.map +1 -0
  36. package/dist/command-catalog.d.ts +21 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +138 -38
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/component-check.d.ts +13 -0
  41. package/dist/component-check.d.ts.map +1 -0
  42. package/dist/component-check.js +59 -0
  43. package/dist/component-check.js.map +1 -0
  44. package/dist/contract-proof.d.ts +1 -1
  45. package/dist/contract-proof.d.ts.map +1 -1
  46. package/dist/contract-proof.js +2 -24
  47. package/dist/contract-proof.js.map +1 -1
  48. package/dist/contracts.d.ts +19 -0
  49. package/dist/contracts.d.ts.map +1 -1
  50. package/dist/contracts.js +50 -3
  51. package/dist/contracts.js.map +1 -1
  52. package/dist/db-tool.d.ts.map +1 -1
  53. package/dist/db-tool.js +4 -3
  54. package/dist/db-tool.js.map +1 -1
  55. package/dist/dev-bun-config.d.ts +5 -1
  56. package/dist/dev-bun-config.d.ts.map +1 -1
  57. package/dist/dev-bun-config.js +44 -11
  58. package/dist/dev-bun-config.js.map +1 -1
  59. package/dist/diagnostics.d.ts +3 -0
  60. package/dist/diagnostics.d.ts.map +1 -1
  61. package/dist/diagnostics.js.map +1 -1
  62. package/dist/diff-tool.d.ts +9 -8
  63. package/dist/diff-tool.d.ts.map +1 -1
  64. package/dist/diff-tool.js +28 -24
  65. package/dist/diff-tool.js.map +1 -1
  66. package/dist/doctor.d.ts +13 -0
  67. package/dist/doctor.d.ts.map +1 -1
  68. package/dist/doctor.js +23 -15
  69. package/dist/doctor.js.map +1 -1
  70. package/dist/env-file.d.ts +6 -0
  71. package/dist/env-file.d.ts.map +1 -1
  72. package/dist/env-file.js +6 -0
  73. package/dist/env-file.js.map +1 -1
  74. package/dist/fix-recipes.d.ts.map +1 -1
  75. package/dist/fix-recipes.js +28 -3
  76. package/dist/fix-recipes.js.map +1 -1
  77. package/dist/hand-built-manifest.d.ts +27 -0
  78. package/dist/hand-built-manifest.d.ts.map +1 -0
  79. package/dist/hand-built-manifest.js +124 -0
  80. package/dist/hand-built-manifest.js.map +1 -0
  81. package/dist/hooks-report.d.ts +23 -0
  82. package/dist/hooks-report.d.ts.map +1 -0
  83. package/dist/hooks-report.js +104 -0
  84. package/dist/hooks-report.js.map +1 -0
  85. package/dist/init-agents.d.ts +2 -2
  86. package/dist/init-agents.d.ts.map +1 -1
  87. package/dist/init-agents.js +17 -11
  88. package/dist/init-agents.js.map +1 -1
  89. package/dist/internal/nifra-project.d.ts +41 -0
  90. package/dist/internal/nifra-project.d.ts.map +1 -0
  91. package/dist/internal/nifra-project.js +183 -0
  92. package/dist/internal/nifra-project.js.map +1 -0
  93. package/dist/internal/project-specifiers.d.ts +16 -0
  94. package/dist/internal/project-specifiers.d.ts.map +1 -0
  95. package/dist/internal/project-specifiers.js +396 -0
  96. package/dist/internal/project-specifiers.js.map +1 -0
  97. package/dist/internal/script-kind.d.ts.map +1 -1
  98. package/dist/internal/script-kind.js +2 -1
  99. package/dist/internal/script-kind.js.map +1 -1
  100. package/dist/internal/source-facts.d.ts +2 -0
  101. package/dist/internal/source-facts.d.ts.map +1 -1
  102. package/dist/internal/source-facts.js +11 -0
  103. package/dist/internal/source-facts.js.map +1 -1
  104. package/dist/internal/typescript-import.d.ts +1 -1
  105. package/dist/internal/typescript-import.d.ts.map +1 -1
  106. package/dist/internal/typescript-import.js +5 -3
  107. package/dist/internal/typescript-import.js.map +1 -1
  108. package/dist/introspect.d.ts +6 -6
  109. package/dist/introspect.d.ts.map +1 -1
  110. package/dist/introspect.js +55 -24
  111. package/dist/introspect.js.map +1 -1
  112. package/dist/levels-tool.d.ts.map +1 -1
  113. package/dist/levels-tool.js +93 -15
  114. package/dist/levels-tool.js.map +1 -1
  115. package/dist/load.d.ts +31 -8
  116. package/dist/load.d.ts.map +1 -1
  117. package/dist/load.js +37 -7
  118. package/dist/load.js.map +1 -1
  119. package/dist/mcp-context.d.ts +5 -5
  120. package/dist/mcp-context.d.ts.map +1 -1
  121. package/dist/mcp-context.js +2 -2
  122. package/dist/mcp-context.js.map +1 -1
  123. package/dist/mcp-exec.d.ts +12 -4
  124. package/dist/mcp-exec.d.ts.map +1 -1
  125. package/dist/mcp-exec.js +50 -13
  126. package/dist/mcp-exec.js.map +1 -1
  127. package/dist/mcp-http.js +1 -1
  128. package/dist/mcp-io.d.ts +17 -0
  129. package/dist/mcp-io.d.ts.map +1 -1
  130. package/dist/mcp-io.js +40 -0
  131. package/dist/mcp-io.js.map +1 -1
  132. package/dist/mcp-isolate.d.ts.map +1 -1
  133. package/dist/mcp-isolate.js +3 -2
  134. package/dist/mcp-isolate.js.map +1 -1
  135. package/dist/mcp-project-child.js +1 -1
  136. package/dist/mcp-project-child.js.map +1 -1
  137. package/dist/mcp-root.d.ts.map +1 -1
  138. package/dist/mcp-root.js +50 -10
  139. package/dist/mcp-root.js.map +1 -1
  140. package/dist/mcp-ws.d.ts.map +1 -1
  141. package/dist/mcp-ws.js +3 -1
  142. package/dist/mcp-ws.js.map +1 -1
  143. package/dist/mcp.d.ts.map +1 -1
  144. package/dist/mcp.js +8 -2
  145. package/dist/mcp.js.map +1 -1
  146. package/dist/migrate-layout.d.ts +7 -0
  147. package/dist/migrate-layout.d.ts.map +1 -1
  148. package/dist/migrate-layout.js +958 -71
  149. package/dist/migrate-layout.js.map +1 -1
  150. package/dist/output-schemas.d.ts +94 -0
  151. package/dist/output-schemas.d.ts.map +1 -0
  152. package/dist/output-schemas.js +864 -0
  153. package/dist/output-schemas.js.map +1 -0
  154. package/dist/port.d.ts.map +1 -1
  155. package/dist/port.js +3 -2
  156. package/dist/port.js.map +1 -1
  157. package/dist/project-facts.d.ts +15 -4
  158. package/dist/project-facts.d.ts.map +1 -1
  159. package/dist/project-facts.js +1 -0
  160. package/dist/project-facts.js.map +1 -1
  161. package/dist/recipes/4.0.0.d.ts.map +1 -1
  162. package/dist/recipes/4.0.0.js +2 -0
  163. package/dist/recipes/4.0.0.js.map +1 -1
  164. package/dist/recipes/5.0.0.d.ts +11 -0
  165. package/dist/recipes/5.0.0.d.ts.map +1 -0
  166. package/dist/recipes/5.0.0.js +34 -0
  167. package/dist/recipes/5.0.0.js.map +1 -0
  168. package/dist/recipes/index.d.ts +7 -0
  169. package/dist/recipes/index.d.ts.map +1 -1
  170. package/dist/recipes/index.js +2 -0
  171. package/dist/recipes/index.js.map +1 -1
  172. package/dist/replay.d.ts.map +1 -1
  173. package/dist/replay.js +12 -3
  174. package/dist/replay.js.map +1 -1
  175. package/dist/review.d.ts +3 -1
  176. package/dist/review.d.ts.map +1 -1
  177. package/dist/review.js +38 -7
  178. package/dist/review.js.map +1 -1
  179. package/dist/rules/codes.d.ts +10 -0
  180. package/dist/rules/codes.d.ts.map +1 -1
  181. package/dist/rules/codes.js +10 -0
  182. package/dist/rules/codes.js.map +1 -1
  183. package/dist/rules/comment-markers.d.ts +16 -7
  184. package/dist/rules/comment-markers.d.ts.map +1 -1
  185. package/dist/rules/comment-markers.js +58 -19
  186. package/dist/rules/comment-markers.js.map +1 -1
  187. package/dist/rules/contract-visibility.d.ts +3 -0
  188. package/dist/rules/contract-visibility.d.ts.map +1 -0
  189. package/dist/rules/contract-visibility.js +337 -0
  190. package/dist/rules/contract-visibility.js.map +1 -0
  191. package/dist/rules/data-guard.d.ts +7 -0
  192. package/dist/rules/data-guard.d.ts.map +1 -1
  193. package/dist/rules/data-guard.js +319 -37
  194. package/dist/rules/data-guard.js.map +1 -1
  195. package/dist/rules/layout.d.ts +4 -0
  196. package/dist/rules/layout.d.ts.map +1 -0
  197. package/dist/rules/layout.js +19 -0
  198. package/dist/rules/layout.js.map +1 -0
  199. package/dist/rules/legacy.d.ts.map +1 -1
  200. package/dist/rules/legacy.js +158 -43
  201. package/dist/rules/legacy.js.map +1 -1
  202. package/dist/rules/pages.d.ts +24 -0
  203. package/dist/rules/pages.d.ts.map +1 -1
  204. package/dist/rules/pages.js +221 -3
  205. package/dist/rules/pages.js.map +1 -1
  206. package/dist/rules/routes.d.ts +6 -0
  207. package/dist/rules/routes.d.ts.map +1 -1
  208. package/dist/rules/routes.js +111 -16
  209. package/dist/rules/routes.js.map +1 -1
  210. package/dist/rules/secrets.js +2 -2
  211. package/dist/rules/secrets.js.map +1 -1
  212. package/dist/rules/security.d.ts +12 -1
  213. package/dist/rules/security.d.ts.map +1 -1
  214. package/dist/rules/security.js +281 -68
  215. package/dist/rules/security.js.map +1 -1
  216. package/dist/rules/zones.d.ts +13 -0
  217. package/dist/rules/zones.d.ts.map +1 -1
  218. package/dist/rules/zones.js +192 -40
  219. package/dist/rules/zones.js.map +1 -1
  220. package/dist/scaffold.d.ts.map +1 -1
  221. package/dist/scaffold.js +6 -4
  222. package/dist/scaffold.js.map +1 -1
  223. package/dist/sdk.d.ts.map +1 -1
  224. package/dist/sdk.js +14 -5
  225. package/dist/sdk.js.map +1 -1
  226. package/dist/stylex-migrate.d.ts.map +1 -1
  227. package/dist/stylex-migrate.js +163 -7
  228. package/dist/stylex-migrate.js.map +1 -1
  229. package/dist/sync-manifest.d.ts +10 -2
  230. package/dist/sync-manifest.d.ts.map +1 -1
  231. package/dist/sync-manifest.js +42 -18
  232. package/dist/sync-manifest.js.map +1 -1
  233. package/dist/test-tool.d.ts.map +1 -1
  234. package/dist/test-tool.js +12 -3
  235. package/dist/test-tool.js.map +1 -1
  236. package/dist/typecheck-coverage.d.ts +20 -0
  237. package/dist/typecheck-coverage.d.ts.map +1 -0
  238. package/dist/typecheck-coverage.js +129 -0
  239. package/dist/typecheck-coverage.js.map +1 -0
  240. package/dist/upgrade.d.ts +69 -0
  241. package/dist/upgrade.d.ts.map +1 -1
  242. package/dist/upgrade.js +420 -77
  243. package/dist/upgrade.js.map +1 -1
  244. package/dist/verification-plan.d.ts.map +1 -1
  245. package/dist/verification-plan.js +11 -0
  246. package/dist/verification-plan.js.map +1 -1
  247. package/dist/version-drift.d.ts +22 -0
  248. package/dist/version-drift.d.ts.map +1 -0
  249. package/dist/version-drift.js +42 -0
  250. package/dist/version-drift.js.map +1 -0
  251. package/dist/web-app-options.d.ts +1 -1
  252. package/dist/web-app-options.d.ts.map +1 -1
  253. package/dist/web-app-options.js +1 -1
  254. package/dist/web-app-options.js.map +1 -1
  255. package/dist/work-graph.d.ts.map +1 -1
  256. package/dist/work-graph.js +20 -8
  257. package/dist/work-graph.js.map +1 -1
  258. package/docs/examples.json +54 -6
  259. package/docs/llms-full.txt +1539 -138
  260. package/docs/types.json +1456 -206
  261. package/package.json +11 -12
  262. package/src/agent-files.ts +203 -0
  263. package/src/app-files.ts +34 -4
  264. package/src/assure-hydration.ts +15 -2
  265. package/src/capabilities-tool.ts +15 -3
  266. package/src/check-diagnostics.ts +73 -9
  267. package/src/check-scan.ts +1773 -381
  268. package/src/check.ts +237 -76
  269. package/src/cli.ts +141 -5
  270. package/src/client-module.ts +61 -0
  271. package/src/command-catalog.ts +181 -47
  272. package/src/component-check.ts +79 -0
  273. package/src/contract-proof.ts +3 -25
  274. package/src/contracts.ts +60 -3
  275. package/src/db-tool.ts +5 -2
  276. package/src/dev-bun-config.ts +51 -10
  277. package/src/diagnostics.ts +3 -0
  278. package/src/diff-tool.ts +33 -26
  279. package/src/doctor.ts +43 -21
  280. package/src/env-file.ts +7 -0
  281. package/src/fix-recipes.ts +34 -3
  282. package/src/hand-built-manifest.ts +144 -0
  283. package/src/hooks-report.ts +135 -0
  284. package/src/init-agents.ts +17 -11
  285. package/src/internal/nifra-project.ts +191 -0
  286. package/src/internal/project-specifiers.ts +424 -0
  287. package/src/internal/script-kind.ts +2 -1
  288. package/src/internal/source-facts.ts +25 -0
  289. package/src/internal/typescript-import.ts +10 -3
  290. package/src/introspect.ts +65 -27
  291. package/src/levels-tool.ts +117 -20
  292. package/src/load.ts +78 -14
  293. package/src/mcp-context.ts +7 -7
  294. package/src/mcp-exec.ts +60 -17
  295. package/src/mcp-http.ts +1 -1
  296. package/src/mcp-io.ts +41 -0
  297. package/src/mcp-isolate.ts +9 -2
  298. package/src/mcp-project-child.ts +1 -1
  299. package/src/mcp-root.ts +58 -8
  300. package/src/mcp-ws.ts +4 -2
  301. package/src/mcp.ts +8 -3
  302. package/src/migrate-layout.ts +1109 -70
  303. package/src/output-schemas.ts +1084 -0
  304. package/src/port.ts +3 -2
  305. package/src/project-facts.ts +17 -3
  306. package/src/recipes/4.0.0.ts +2 -0
  307. package/src/recipes/5.0.0.ts +35 -0
  308. package/src/recipes/index.ts +10 -0
  309. package/src/replay.ts +14 -3
  310. package/src/review.ts +39 -10
  311. package/src/rules/codes.ts +10 -0
  312. package/src/rules/comment-markers.ts +57 -21
  313. package/src/rules/contract-visibility.ts +383 -0
  314. package/src/rules/data-guard.ts +351 -42
  315. package/src/rules/layout.ts +22 -0
  316. package/src/rules/legacy.ts +184 -51
  317. package/src/rules/pages.ts +238 -3
  318. package/src/rules/routes.ts +124 -16
  319. package/src/rules/secrets.ts +2 -2
  320. package/src/rules/security.ts +352 -71
  321. package/src/rules/zones.ts +205 -38
  322. package/src/scaffold.ts +6 -4
  323. package/src/sdk.ts +14 -5
  324. package/src/stylex-migrate.ts +134 -7
  325. package/src/sync-manifest.ts +48 -18
  326. package/src/test-tool.ts +18 -3
  327. package/src/typecheck-coverage.ts +134 -0
  328. package/src/upgrade.ts +487 -71
  329. package/src/verification-plan.ts +24 -0
  330. package/src/version-drift.ts +67 -0
  331. package/src/web-app-options.ts +2 -2
  332. package/src/work-graph.ts +17 -12
@@ -0,0 +1,424 @@
1
+ /**
2
+ * The first-party file an import specifier names, found the way a bundler finds it: a relative or
3
+ * absolute path, a tsconfig `paths` alias or `baseUrl` name (following `extends`), or a package.json
4
+ * `imports` entry (`#x`). A bare package specifier, a file under `node_modules` and a name that matches
5
+ * nothing resolve to `undefined`, so a check never mistakes a dependency for project code.
6
+ *
7
+ * Never `Bun.resolveSync`: it can install a missing package and remembers answers for the life of the
8
+ * process, and the MCP server is long-lived. A resolver caches what it reads for one check run only.
9
+ */
10
+ import { readFileSync, realpathSync, statSync } from "node:fs"
11
+ import { basename, dirname, extname, isAbsolute, join } from "node:path"
12
+
13
+ /** `(absolute importing file, specifier)` to the real path of the file it loads, or `undefined`. */
14
+ export type SpecifierResolver = (fromFile: string, specifier: string) => string | undefined
15
+
16
+ export interface ProjectResolver extends SpecifierResolver {
17
+ /**
18
+ * `specifier`, an alias as `fromFile` writes it, rewritten through the same alias to name `file`
19
+ * instead, in the same style (extension, index). `undefined` when `specifier` is not an alias or no
20
+ * target of its alias covers `file`. Paths are absolute and unresolved, as `fromFile` is spelled.
21
+ */
22
+ readonly respell: (fromFile: string, specifier: string, file: string) => string | undefined
23
+ /** The `paths` patterns of the nearest tsconfig at or above `dir`, after its `extends` chain. */
24
+ readonly pathPatterns: (dir: string) => readonly string[]
25
+ }
26
+
27
+ /** How an alias specifier reached its file: what a rewrite through the same alias has to fit. */
28
+ interface AliasRule {
29
+ /** The pattern the specifier matched; `undefined` for a `baseUrl` name, which matches none. */
30
+ readonly key: string | undefined
31
+ /** Every pattern in the same scope: a rewritten name must still match `key`, or none for `baseUrl`. */
32
+ readonly keys: readonly string[]
33
+ /** The directory targets resolve against. */
34
+ readonly base: string
35
+ /** The key's targets in try order, `STAR` where the matched text goes. */
36
+ readonly templates: readonly string[]
37
+ /** The path the specifier spells before probing, and the file probing found. */
38
+ readonly written: string
39
+ readonly found: string
40
+ }
41
+
42
+ const STAR = "\u0000"
43
+
44
+ const EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".json"]
45
+ // TypeScript's ESM convention: `./x.js` names `x.ts` when only the source exists.
46
+ const SOURCE_FOR: Readonly<Record<string, readonly string[]>> = {
47
+ ".js": [".ts", ".tsx"],
48
+ ".jsx": [".tsx"],
49
+ ".mjs": [".mts"],
50
+ ".cjs": [".cts"],
51
+ }
52
+ // The package.json `imports` conditions a server-side bundle of this project matches, in no order:
53
+ // the target object's own key order decides, as in Node.
54
+ const CONDITIONS: ReadonlySet<string> = new Set(["bun", "import", "default"])
55
+ const MAX_EXTENDS = 16
56
+
57
+ interface PathsConfig {
58
+ /** Directory `paths` targets resolve against: `baseUrl` when set, else the defining config's. */
59
+ readonly pathsBase: string
60
+ readonly paths: ReadonlyArray<readonly [string, readonly string[]]>
61
+ readonly baseUrl: string | undefined
62
+ }
63
+
64
+ /** JSON with comments and trailing commas, as tsconfig files are written. Throws on anything else. */
65
+ export function parseJsonc(text: string): unknown {
66
+ let code = ""
67
+ for (let i = 0; i < text.length; ) {
68
+ const ch = text[i]
69
+ if (ch === '"') {
70
+ let end = i + 1
71
+ while (end < text.length && text[end] !== '"') end += text[end] === "\\" ? 2 : 1
72
+ code += text.slice(i, end + 1)
73
+ i = end + 1
74
+ } else if (ch === "/" && text[i + 1] === "/") {
75
+ const newline = text.indexOf("\n", i)
76
+ i = newline === -1 ? text.length : newline
77
+ } else if (ch === "/" && text[i + 1] === "*") {
78
+ const close = text.indexOf("*/", i + 2)
79
+ i = close === -1 ? text.length : close + 2
80
+ } else {
81
+ code += ch
82
+ i += 1
83
+ }
84
+ }
85
+ let json = ""
86
+ for (let i = 0; i < code.length; i += 1) {
87
+ const ch = code[i]
88
+ if (ch === '"') {
89
+ let end = i + 1
90
+ while (end < code.length && code[end] !== '"') end += code[end] === "\\" ? 2 : 1
91
+ json += code.slice(i, end + 1)
92
+ i = end
93
+ } else if (ch === "," && /^\s*[}\]]/.test(code.slice(i + 1))) {
94
+ } else json += ch
95
+ }
96
+ return JSON.parse(json.replace(/^\uFEFF/, ""))
97
+ }
98
+
99
+ const record = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
100
+ typeof value === "object" && value !== null && !Array.isArray(value)
101
+ ? Object.fromEntries(Object.entries(value))
102
+ : undefined
103
+
104
+ const isFile = (path: string): boolean =>
105
+ statSync(path, { throwIfNoEntry: false })?.isFile() ?? false
106
+
107
+ const real = (path: string): string => {
108
+ try {
109
+ return realpathSync(path)
110
+ } catch {
111
+ return path
112
+ }
113
+ }
114
+
115
+ /** The file a path names: as written, its source for a `.js` name, with an extension, or an index. */
116
+ function probe(base: string): string | undefined {
117
+ if (isFile(base)) return base
118
+ const ext = extname(base)
119
+ for (const source of SOURCE_FOR[ext] ?? []) {
120
+ const candidate = `${base.slice(0, -ext.length)}${source}`
121
+ if (isFile(candidate)) return candidate
122
+ }
123
+ for (const extension of EXTENSIONS)
124
+ if (isFile(`${base}${extension}`)) return `${base}${extension}`
125
+ for (const extension of EXTENSIONS) {
126
+ const index = join(base, `index${extension}`)
127
+ if (isFile(index)) return index
128
+ }
129
+ return undefined
130
+ }
131
+
132
+ /**
133
+ * `file` written the way `written` names `found`: as is, without its extension, as its folder when
134
+ * `found` is an index, or with the `.js` name of a `.ts` source.
135
+ */
136
+ function sameStyle(written: string, found: string, file: string): string {
137
+ if (written === found) return file
138
+ const ext = extname(file)
139
+ const bare = file.slice(0, file.length - ext.length)
140
+ if (found.startsWith(`${written}/index.`) || found.startsWith(`${written}\\index.`))
141
+ return basename(file).startsWith("index.") ? dirname(file) : bare
142
+ if (found.startsWith(written)) return bare
143
+ const writtenExt = extname(written)
144
+ return SOURCE_FOR[writtenExt] === undefined ? file : `${bare}${writtenExt}`
145
+ }
146
+
147
+ /** Longest-prefix `*` match, TypeScript's rule for `paths` and Node's for `imports`. */
148
+ function matchPattern(
149
+ patterns: readonly string[],
150
+ specifier: string,
151
+ ): { readonly pattern: string; readonly star: string | undefined } | undefined {
152
+ if (patterns.includes(specifier)) return { pattern: specifier, star: undefined }
153
+ let best: { pattern: string; star: string } | undefined
154
+ for (const pattern of patterns) {
155
+ const at = pattern.indexOf("*")
156
+ if (at === -1 || pattern.indexOf("*", at + 1) !== -1) continue
157
+ const prefix = pattern.slice(0, at)
158
+ const suffix = pattern.slice(at + 1)
159
+ if (specifier.length < prefix.length + suffix.length) continue
160
+ if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) continue
161
+ if (best !== undefined && prefix.length <= best.pattern.indexOf("*")) continue
162
+ best = { pattern, star: specifier.slice(prefix.length, specifier.length - suffix.length) }
163
+ }
164
+ return best
165
+ }
166
+
167
+ export function createSpecifierResolver(): ProjectResolver {
168
+ const configs = new Map<string, PathsConfig | undefined>()
169
+ const tsconfigByDir = new Map<string, string | undefined>()
170
+ const manifests = new Map<string, Readonly<Record<string, unknown>> | undefined>()
171
+ const packageByDir = new Map<string, string | undefined>()
172
+
173
+ const readJsonc = (path: string): Readonly<Record<string, unknown>> | undefined => {
174
+ try {
175
+ return record(parseJsonc(readFileSync(path, "utf8")))
176
+ } catch {
177
+ return undefined
178
+ }
179
+ }
180
+
181
+ /** The nearest `tsconfig.json` (or `jsconfig.json`) at or above `dir`, as Bun's resolver picks it. */
182
+ const tsconfigFor = (dir: string): string | undefined => {
183
+ if (tsconfigByDir.has(dir)) return tsconfigByDir.get(dir)
184
+ const own = ["tsconfig.json", "jsconfig.json"].map((name) => join(dir, name)).find(isFile)
185
+ const parent = dirname(dir)
186
+ const found = own ?? (parent === dir ? undefined : tsconfigFor(parent))
187
+ tsconfigByDir.set(dir, found)
188
+ return found
189
+ }
190
+
191
+ /** An `extends` entry's file: a path relative to the config, or a package found in `node_modules`. */
192
+ const extendedConfig = (from: string, name: string): string | undefined => {
193
+ const withJson = (path: string): string | undefined =>
194
+ isFile(path) ? path : isFile(`${path}.json`) ? `${path}.json` : undefined
195
+ if (name.startsWith("./") || name.startsWith("../") || isAbsolute(name))
196
+ return withJson(join(dirname(from), name))
197
+ for (let dir = dirname(from); ; dir = dirname(dir)) {
198
+ const target = join(dir, "node_modules", name)
199
+ const direct = withJson(target)
200
+ if (direct !== undefined) return direct
201
+ const manifest = readJsonc(join(target, "package.json"))
202
+ const field = manifest?.tsconfig
203
+ const nested =
204
+ typeof field === "string"
205
+ ? withJson(join(target, field))
206
+ : withJson(join(target, "tsconfig"))
207
+ if (nested !== undefined) return nested
208
+ if (dirname(dir) === dir) return undefined
209
+ }
210
+ }
211
+
212
+ /** The `paths` and `baseUrl` a config ends up with after its `extends` chain. */
213
+ const pathsConfig = (
214
+ file: string,
215
+ seen: ReadonlySet<string> = new Set(),
216
+ ): PathsConfig | undefined => {
217
+ if (configs.has(file)) return configs.get(file)
218
+ if (seen.has(file) || seen.size >= MAX_EXTENDS) return undefined
219
+ const config = readJsonc(file)
220
+ let inherited: PathsConfig | undefined
221
+ const extended = config?.extends
222
+ for (const name of typeof extended === "string"
223
+ ? [extended]
224
+ : Array.isArray(extended)
225
+ ? extended
226
+ : []) {
227
+ if (typeof name !== "string") continue
228
+ const base = extendedConfig(file, name)
229
+ const parent = base === undefined ? undefined : pathsConfig(base, new Set([...seen, file]))
230
+ if (parent === undefined) continue
231
+ inherited = {
232
+ paths: parent.paths.length > 0 ? parent.paths : (inherited?.paths ?? []),
233
+ baseUrl: parent.baseUrl ?? inherited?.baseUrl,
234
+ pathsBase:
235
+ parent.paths.length > 0 ? parent.pathsBase : (inherited?.pathsBase ?? parent.pathsBase),
236
+ }
237
+ }
238
+ const options = record(config?.compilerOptions)
239
+ const baseUrl =
240
+ typeof options?.baseUrl === "string"
241
+ ? join(dirname(file), options.baseUrl)
242
+ : inherited?.baseUrl
243
+ const ownPaths = record(options?.paths)
244
+ const paths =
245
+ ownPaths === undefined
246
+ ? (inherited?.paths ?? [])
247
+ : Object.entries(ownPaths).map(
248
+ ([pattern, targets]) =>
249
+ [
250
+ pattern,
251
+ Array.isArray(targets)
252
+ ? targets.filter((t): t is string => typeof t === "string")
253
+ : [],
254
+ ] as const,
255
+ )
256
+ const pathsBase =
257
+ baseUrl ?? (ownPaths === undefined ? (inherited?.pathsBase ?? dirname(file)) : dirname(file))
258
+ const resolved = config === undefined ? undefined : { paths, baseUrl, pathsBase }
259
+ configs.set(file, resolved)
260
+ return resolved
261
+ }
262
+
263
+ // Keyed by config, not directory: every package name reaches here, from every folder of the project.
264
+ const aliases = new Map<string, string | undefined>()
265
+ const viaTsconfig = (fromFile: string, specifier: string): string | undefined => {
266
+ const tsconfig = tsconfigFor(dirname(fromFile))
267
+ if (tsconfig === undefined) return undefined
268
+ const key = `${tsconfig}\0${specifier}`
269
+ if (aliases.has(key)) return aliases.get(key)
270
+ const found = aliasTarget(tsconfig, specifier)
271
+ aliases.set(key, found)
272
+ return found
273
+ }
274
+ const aliasTarget = (tsconfig: string, specifier: string): string | undefined =>
275
+ pathsRule(tsconfig, specifier)?.found
276
+
277
+ /** The nearest package.json at or above `dir`: the package scope Node reads `imports` from. */
278
+ const packageFor = (dir: string): string | undefined => {
279
+ if (packageByDir.has(dir)) return packageByDir.get(dir)
280
+ const own = join(dir, "package.json")
281
+ const parent = dirname(dir)
282
+ const found = isFile(own) ? own : parent === dir ? undefined : packageFor(parent)
283
+ packageByDir.set(dir, found)
284
+ return found
285
+ }
286
+
287
+ const importTarget = (target: unknown, star: string | undefined): string | undefined => {
288
+ if (typeof target === "string")
289
+ return target.startsWith("./") ? target.replaceAll("*", star ?? "") : undefined
290
+ if (Array.isArray(target)) {
291
+ for (const entry of target) {
292
+ const found = importTarget(entry, star)
293
+ if (found !== undefined) return found
294
+ }
295
+ return undefined
296
+ }
297
+ const conditions = record(target)
298
+ if (conditions === undefined) return undefined
299
+ for (const [condition, value] of Object.entries(conditions)) {
300
+ if (CONDITIONS.has(condition)) return importTarget(value, star)
301
+ }
302
+ return undefined
303
+ }
304
+
305
+ const importsRule = (fromFile: string, specifier: string): AliasRule | undefined => {
306
+ const manifestPath = packageFor(dirname(fromFile))
307
+ if (manifestPath === undefined) return undefined
308
+ if (!manifests.has(manifestPath)) manifests.set(manifestPath, readJsonc(manifestPath))
309
+ const imports = record(manifests.get(manifestPath)?.imports)
310
+ if (imports === undefined) return undefined
311
+ const keys = Object.keys(imports)
312
+ const match = matchPattern(keys, specifier)
313
+ if (match === undefined) return undefined
314
+ const template = importTarget(imports[match.pattern], STAR)
315
+ if (template === undefined) return undefined
316
+ const base = dirname(manifestPath)
317
+ const written = join(base, template.replaceAll(STAR, match.star ?? ""))
318
+ const found = probe(written)
319
+ return found === undefined
320
+ ? undefined
321
+ : { key: match.pattern, keys, base, templates: [template], written, found }
322
+ }
323
+ const viaImports = (fromFile: string, specifier: string): string | undefined =>
324
+ importsRule(fromFile, specifier)?.found
325
+
326
+ const tsconfigRule = (fromFile: string, specifier: string): AliasRule | undefined => {
327
+ const tsconfig = tsconfigFor(dirname(fromFile))
328
+ return tsconfig === undefined ? undefined : pathsRule(tsconfig, specifier)
329
+ }
330
+ const pathsRule = (tsconfig: string, specifier: string): AliasRule | undefined => {
331
+ const config = pathsConfig(tsconfig)
332
+ if (config === undefined) return undefined
333
+ const keys = config.paths.map(([pattern]) => pattern)
334
+ const match = matchPattern(keys, specifier)
335
+ if (match === undefined) {
336
+ if (config.baseUrl === undefined) return undefined
337
+ const written = join(config.baseUrl, specifier)
338
+ const found = probe(written)
339
+ return found === undefined
340
+ ? undefined
341
+ : { key: undefined, keys, base: config.baseUrl, templates: [STAR], written, found }
342
+ }
343
+ // TypeScript permits at most one wildcard in each substitution, as in the alias key.
344
+ const targets = (config.paths.find(([pattern]) => pattern === match.pattern)?.[1] ?? []).filter(
345
+ (target) => target.indexOf("*") === target.lastIndexOf("*"),
346
+ )
347
+ for (const target of targets) {
348
+ const written = join(config.pathsBase, target.replaceAll("*", match.star ?? ""))
349
+ const found = probe(written)
350
+ if (found === undefined) continue
351
+ const templates = targets.map((t) => t.replaceAll("*", STAR))
352
+ return { key: match.pattern, keys, base: config.pathsBase, templates, written, found }
353
+ }
354
+ return undefined
355
+ }
356
+
357
+ const respell = (fromFile: string, specifier: string, file: string): string | undefined => {
358
+ if (/^\.\.?(?:\/|$)/.test(specifier) || isAbsolute(specifier)) return undefined
359
+ const rule =
360
+ (specifier.startsWith("#") ? importsRule(fromFile, specifier) : undefined) ??
361
+ tsconfigRule(fromFile, specifier)
362
+ if (rule === undefined) return undefined
363
+ const spelled = sameStyle(rule.written, rule.found, file)
364
+ for (const template of rule.templates) {
365
+ const at = template.indexOf(STAR)
366
+ if (at === -1) continue
367
+ // An empty head still ends at a folder boundary: `src2/x` is not under a `baseUrl` of `src`.
368
+ const prefix = join(rule.base, template.slice(0, at) || "./")
369
+ const suffix = template.slice(at + STAR.length)
370
+ if (!spelled.startsWith(prefix) || !spelled.endsWith(suffix)) continue
371
+ if (spelled.length < prefix.length + suffix.length) continue
372
+ const star = spelled
373
+ .slice(prefix.length, spelled.length - suffix.length)
374
+ .replaceAll("\\", "/")
375
+ if (star === "" || star.startsWith("/")) continue
376
+ const keyAt = rule.key?.indexOf("*") ?? 0
377
+ if (keyAt === -1) continue
378
+ const next =
379
+ rule.key === undefined
380
+ ? star
381
+ : `${rule.key.slice(0, keyAt)}${star}${rule.key.slice(keyAt + 1)}`
382
+ // The rewritten name must take the same route: another, longer pattern would claim it.
383
+ if (matchPattern(rule.keys, next)?.pattern !== rule.key) continue
384
+ return next
385
+ }
386
+ return undefined
387
+ }
388
+
389
+ const resolved = new Map<string, string | undefined>()
390
+ const resolve: SpecifierResolver = (fromFile, specifier) => {
391
+ // Every rule below reads only the importing file's directory, so the answer is the directory's.
392
+ const key = `${dirname(fromFile)}\0${specifier}`
393
+ if (resolved.has(key)) return resolved.get(key)
394
+ const path = resolveFrom(fromFile, specifier)
395
+ resolved.set(key, path)
396
+ return path
397
+ }
398
+ const pathPatterns = (dir: string): readonly string[] => {
399
+ const tsconfig = tsconfigFor(dir)
400
+ const config = tsconfig === undefined ? undefined : pathsConfig(tsconfig)
401
+ return (config?.paths ?? []).map(([pattern]) => pattern)
402
+ }
403
+
404
+ return Object.assign(resolve, { respell, pathPatterns })
405
+
406
+ function resolveFrom(fromFile: string, specifier: string): string | undefined {
407
+ let found: string | undefined
408
+ if (
409
+ specifier.startsWith("./") ||
410
+ specifier.startsWith("../") ||
411
+ specifier === "." ||
412
+ specifier === ".."
413
+ )
414
+ found = probe(join(dirname(fromFile), specifier))
415
+ else if (isAbsolute(specifier)) found = probe(specifier)
416
+ // A `#` name is a package.json import first; a tsconfig may still map it.
417
+ else if (specifier.startsWith("#"))
418
+ found = viaImports(fromFile, specifier) ?? viaTsconfig(fromFile, specifier)
419
+ else found = viaTsconfig(fromFile, specifier)
420
+ if (found === undefined) return undefined
421
+ const path = real(found)
422
+ return /[\\/]node_modules[\\/]/.test(path) ? undefined : path
423
+ }
424
+ }
@@ -6,6 +6,7 @@ import type * as TSApi from "typescript"
6
6
  */
7
7
  export function scriptKindOf(ts: typeof TSApi, file: string): TSApi.ScriptKind {
8
8
  if (file.endsWith(".tsx")) return ts.ScriptKind.TSX
9
- if (/\.[cm]?ts$/.test(file)) return ts.ScriptKind.TS
9
+ // A `.svelte`/`.vue` file is scanned as its script blocks, which may be TypeScript.
10
+ if (/\.(?:[cm]?ts|svelte|vue)$/.test(file)) return ts.ScriptKind.TS
10
11
  return file.endsWith(".jsx") ? ts.ScriptKind.JSX : ts.ScriptKind.JS
11
12
  }
@@ -20,6 +20,13 @@ export interface SourceFacts {
20
20
  method: string,
21
21
  path: string,
22
22
  ): boolean | undefined
23
+ /** A `.method(argument, …)` member call whose first argument is written exactly as `argument`. */
24
+ isRouteArgumentAt(
25
+ source: TSApi.SourceFile,
26
+ position: number,
27
+ method: string,
28
+ argument: string,
29
+ ): boolean | undefined
23
30
  /** A call to the free function `callee` whose argument at `pathIndex` is the literal `path`. */
24
31
  isFunctionRouteCallAt(
25
32
  source: TSApi.SourceFile,
@@ -142,6 +149,23 @@ export function createSourceFacts(ts: TypeScriptApi): SourceFacts {
142
149
  )
143
150
  }
144
151
 
152
+ const isRouteArgumentAt = (
153
+ source: TSApi.SourceFile,
154
+ position: number,
155
+ method: string,
156
+ argument: string,
157
+ ): boolean | undefined => {
158
+ const call = ancestorAt(ts, source, position, ts.isCallExpression)
159
+ if (call === undefined || !ts.isPropertyAccessExpression(call.expression)) return false
160
+ const first = call.arguments[0]
161
+ return (
162
+ call.expression.name.text.toUpperCase() === method &&
163
+ call.arguments.length > 1 &&
164
+ first !== undefined &&
165
+ first.getText(source) === argument
166
+ )
167
+ }
168
+
145
169
  const isFunctionRouteCallAt = (
146
170
  source: TSApi.SourceFile,
147
171
  position: number,
@@ -200,6 +224,7 @@ export function createSourceFacts(ts: TypeScriptApi): SourceFacts {
200
224
  parse,
201
225
  isValueImportAt,
202
226
  isRouteRegistrationAt,
227
+ isRouteArgumentAt,
203
228
  isFunctionRouteCallAt,
204
229
  isResponseSyntaxAt,
205
230
  isDynamicImportAt,
@@ -48,11 +48,14 @@ export function unsupportedTypeScriptMessage(version: string): string {
48
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
49
  }
50
50
 
51
- export function unsupportedTypeScriptDiagnostic(version: string): Diagnostic {
51
+ export function unsupportedTypeScriptDiagnostic(
52
+ version: string,
53
+ message = unsupportedTypeScriptMessage(version),
54
+ ): Diagnostic {
52
55
  return diagnostic({
53
56
  code: NIFRA_TS_UNSUPPORTED,
54
57
  severity: "error",
55
- message: unsupportedTypeScriptMessage(version),
58
+ message,
56
59
  fix: {
57
60
  recipe: "toolchain.install-typescript",
58
61
  command: "bun add -d typescript@^6",
@@ -412,7 +415,11 @@ function sourceEntries(
412
415
  const content = sourceIndex.read(file)
413
416
  if (content === undefined) continue
414
417
  const absolute = isAbsolute(file) ? file : resolve(root, file)
415
- const normalized = normalizedPath(absolute)
418
+ // The session opens script extensions only; a component's script blocks are opened as TypeScript
419
+ // beside it, and `createSourceFile` finds them by their text.
420
+ const normalized = normalizedPath(
421
+ /\.(?:svelte|vue)$/.test(absolute) ? `${absolute}.ts` : absolute,
422
+ )
416
423
  paths.push(normalized)
417
424
  for (const variant of pathVariants(normalized)) files.set(variant, content)
418
425
  }