@nifrajs/cli 4.0.2 → 5.1.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 (364) hide show
  1. package/README.md +10 -2
  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 +9 -2
  12. package/dist/assure-hydration.js.map +1 -1
  13. package/dist/assure.d.ts +12 -2
  14. package/dist/assure.d.ts.map +1 -1
  15. package/dist/assure.js +59 -8
  16. package/dist/assure.js.map +1 -1
  17. package/dist/capabilities-tool.d.ts.map +1 -1
  18. package/dist/capabilities-tool.js +36 -9
  19. package/dist/capabilities-tool.js.map +1 -1
  20. package/dist/check-diagnostics.d.ts +22 -2
  21. package/dist/check-diagnostics.d.ts.map +1 -1
  22. package/dist/check-diagnostics.js +54 -7
  23. package/dist/check-diagnostics.js.map +1 -1
  24. package/dist/check-scan.d.ts +83 -24
  25. package/dist/check-scan.d.ts.map +1 -1
  26. package/dist/check-scan.js +1516 -318
  27. package/dist/check-scan.js.map +1 -1
  28. package/dist/check.d.ts +2 -2
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +203 -70
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +9 -0
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +215 -50
  35. package/dist/cli.js.map +1 -1
  36. package/dist/client-module.d.ts +20 -0
  37. package/dist/client-module.d.ts.map +1 -0
  38. package/dist/client-module.js +73 -0
  39. package/dist/client-module.js.map +1 -0
  40. package/dist/command-catalog.d.ts +24 -5
  41. package/dist/command-catalog.d.ts.map +1 -1
  42. package/dist/command-catalog.js +194 -45
  43. package/dist/command-catalog.js.map +1 -1
  44. package/dist/component-check.d.ts +13 -0
  45. package/dist/component-check.d.ts.map +1 -0
  46. package/dist/component-check.js +59 -0
  47. package/dist/component-check.js.map +1 -0
  48. package/dist/contract-proof.d.ts +1 -1
  49. package/dist/contract-proof.d.ts.map +1 -1
  50. package/dist/contract-proof.js +2 -24
  51. package/dist/contract-proof.js.map +1 -1
  52. package/dist/contracts.d.ts +19 -0
  53. package/dist/contracts.d.ts.map +1 -1
  54. package/dist/contracts.js +50 -3
  55. package/dist/contracts.js.map +1 -1
  56. package/dist/db-tool.d.ts.map +1 -1
  57. package/dist/db-tool.js +4 -3
  58. package/dist/db-tool.js.map +1 -1
  59. package/dist/dev-bun-config.d.ts +5 -1
  60. package/dist/dev-bun-config.d.ts.map +1 -1
  61. package/dist/dev-bun-config.js +44 -11
  62. package/dist/dev-bun-config.js.map +1 -1
  63. package/dist/dev-feed-tool.d.ts +18 -1
  64. package/dist/dev-feed-tool.d.ts.map +1 -1
  65. package/dist/dev-feed-tool.js +93 -0
  66. package/dist/dev-feed-tool.js.map +1 -1
  67. package/dist/diagnostics.d.ts +3 -0
  68. package/dist/diagnostics.d.ts.map +1 -1
  69. package/dist/diagnostics.js.map +1 -1
  70. package/dist/diff-tool.d.ts +9 -8
  71. package/dist/diff-tool.d.ts.map +1 -1
  72. package/dist/diff-tool.js +28 -24
  73. package/dist/diff-tool.js.map +1 -1
  74. package/dist/doctor.d.ts +13 -0
  75. package/dist/doctor.d.ts.map +1 -1
  76. package/dist/doctor.js +23 -15
  77. package/dist/doctor.js.map +1 -1
  78. package/dist/env-file.d.ts +6 -0
  79. package/dist/env-file.d.ts.map +1 -1
  80. package/dist/env-file.js +6 -0
  81. package/dist/env-file.js.map +1 -1
  82. package/dist/fix-recipes.d.ts +2 -0
  83. package/dist/fix-recipes.d.ts.map +1 -1
  84. package/dist/fix-recipes.js +102 -2
  85. package/dist/fix-recipes.js.map +1 -1
  86. package/dist/hand-built-manifest.d.ts +27 -0
  87. package/dist/hand-built-manifest.d.ts.map +1 -0
  88. package/dist/hand-built-manifest.js +124 -0
  89. package/dist/hand-built-manifest.js.map +1 -0
  90. package/dist/hooks-report.d.ts +23 -0
  91. package/dist/hooks-report.d.ts.map +1 -0
  92. package/dist/hooks-report.js +104 -0
  93. package/dist/hooks-report.js.map +1 -0
  94. package/dist/init-agents.d.ts +18 -2
  95. package/dist/init-agents.d.ts.map +1 -1
  96. package/dist/init-agents.js +20 -13
  97. package/dist/init-agents.js.map +1 -1
  98. package/dist/internal/config-paths.d.ts +16 -0
  99. package/dist/internal/config-paths.d.ts.map +1 -0
  100. package/dist/internal/config-paths.js +124 -0
  101. package/dist/internal/config-paths.js.map +1 -0
  102. package/dist/internal/installed-package.d.ts +4 -0
  103. package/dist/internal/installed-package.d.ts.map +1 -0
  104. package/dist/internal/installed-package.js +17 -0
  105. package/dist/internal/installed-package.js.map +1 -0
  106. package/dist/internal/nifra-project.d.ts +41 -0
  107. package/dist/internal/nifra-project.d.ts.map +1 -0
  108. package/dist/internal/nifra-project.js +183 -0
  109. package/dist/internal/nifra-project.js.map +1 -0
  110. package/dist/internal/project-specifiers.d.ts +29 -0
  111. package/dist/internal/project-specifiers.d.ts.map +1 -0
  112. package/dist/internal/project-specifiers.js +435 -0
  113. package/dist/internal/project-specifiers.js.map +1 -0
  114. package/dist/internal/script-kind.d.ts.map +1 -1
  115. package/dist/internal/script-kind.js +2 -1
  116. package/dist/internal/script-kind.js.map +1 -1
  117. package/dist/internal/source-facts.d.ts +8 -1
  118. package/dist/internal/source-facts.d.ts.map +1 -1
  119. package/dist/internal/source-facts.js +22 -5
  120. package/dist/internal/source-facts.js.map +1 -1
  121. package/dist/internal/typescript-import.d.ts +1 -1
  122. package/dist/internal/typescript-import.d.ts.map +1 -1
  123. package/dist/internal/typescript-import.js +10 -6
  124. package/dist/internal/typescript-import.js.map +1 -1
  125. package/dist/introspect.d.ts +15 -6
  126. package/dist/introspect.d.ts.map +1 -1
  127. package/dist/introspect.js +87 -25
  128. package/dist/introspect.js.map +1 -1
  129. package/dist/levels-tool.d.ts.map +1 -1
  130. package/dist/levels-tool.js +98 -23
  131. package/dist/levels-tool.js.map +1 -1
  132. package/dist/load.d.ts +33 -10
  133. package/dist/load.d.ts.map +1 -1
  134. package/dist/load.js +60 -10
  135. package/dist/load.js.map +1 -1
  136. package/dist/mcp-context.d.ts +5 -5
  137. package/dist/mcp-context.d.ts.map +1 -1
  138. package/dist/mcp-context.js +8 -23
  139. package/dist/mcp-context.js.map +1 -1
  140. package/dist/mcp-delegate.d.ts +2 -2
  141. package/dist/mcp-delegate.d.ts.map +1 -1
  142. package/dist/mcp-delegate.js +19 -7
  143. package/dist/mcp-delegate.js.map +1 -1
  144. package/dist/mcp-exec.d.ts +13 -4
  145. package/dist/mcp-exec.d.ts.map +1 -1
  146. package/dist/mcp-exec.js +56 -14
  147. package/dist/mcp-exec.js.map +1 -1
  148. package/dist/mcp-http.js +1 -1
  149. package/dist/mcp-io.d.ts +30 -0
  150. package/dist/mcp-io.d.ts.map +1 -1
  151. package/dist/mcp-io.js +63 -0
  152. package/dist/mcp-io.js.map +1 -1
  153. package/dist/mcp-isolate.d.ts.map +1 -1
  154. package/dist/mcp-isolate.js +3 -2
  155. package/dist/mcp-isolate.js.map +1 -1
  156. package/dist/mcp-path.d.ts +2 -1
  157. package/dist/mcp-path.d.ts.map +1 -1
  158. package/dist/mcp-path.js +9 -24
  159. package/dist/mcp-path.js.map +1 -1
  160. package/dist/mcp-project-child.js +1 -1
  161. package/dist/mcp-project-child.js.map +1 -1
  162. package/dist/mcp-root.d.ts.map +1 -1
  163. package/dist/mcp-root.js +50 -10
  164. package/dist/mcp-root.js.map +1 -1
  165. package/dist/mcp-ws.d.ts.map +1 -1
  166. package/dist/mcp-ws.js +3 -1
  167. package/dist/mcp-ws.js.map +1 -1
  168. package/dist/mcp.d.ts.map +1 -1
  169. package/dist/mcp.js +48 -19
  170. package/dist/mcp.js.map +1 -1
  171. package/dist/migrate-layout.d.ts +47 -1
  172. package/dist/migrate-layout.d.ts.map +1 -1
  173. package/dist/migrate-layout.js +1972 -170
  174. package/dist/migrate-layout.js.map +1 -1
  175. package/dist/output-schemas.d.ts +128 -0
  176. package/dist/output-schemas.d.ts.map +1 -0
  177. package/dist/output-schemas.js +975 -0
  178. package/dist/output-schemas.js.map +1 -0
  179. package/dist/port.d.ts.map +1 -1
  180. package/dist/port.js +3 -2
  181. package/dist/port.js.map +1 -1
  182. package/dist/project-facts.d.ts +15 -4
  183. package/dist/project-facts.d.ts.map +1 -1
  184. package/dist/project-facts.js +1 -0
  185. package/dist/project-facts.js.map +1 -1
  186. package/dist/project-path.d.ts +6 -0
  187. package/dist/project-path.d.ts.map +1 -1
  188. package/dist/project-path.js +48 -14
  189. package/dist/project-path.js.map +1 -1
  190. package/dist/recipes/5.0.0.d.ts +11 -0
  191. package/dist/recipes/5.0.0.d.ts.map +1 -0
  192. package/dist/recipes/5.0.0.js +34 -0
  193. package/dist/recipes/5.0.0.js.map +1 -0
  194. package/dist/recipes/index.d.ts +7 -0
  195. package/dist/recipes/index.d.ts.map +1 -1
  196. package/dist/recipes/index.js +2 -0
  197. package/dist/recipes/index.js.map +1 -1
  198. package/dist/release-verification.d.ts +5 -0
  199. package/dist/release-verification.d.ts.map +1 -1
  200. package/dist/release-verification.js +14 -0
  201. package/dist/release-verification.js.map +1 -1
  202. package/dist/replay.d.ts.map +1 -1
  203. package/dist/replay.js +12 -3
  204. package/dist/replay.js.map +1 -1
  205. package/dist/review.d.ts +3 -1
  206. package/dist/review.d.ts.map +1 -1
  207. package/dist/review.js +43 -7
  208. package/dist/review.js.map +1 -1
  209. package/dist/rules/codes.d.ts +12 -0
  210. package/dist/rules/codes.d.ts.map +1 -1
  211. package/dist/rules/codes.js +12 -0
  212. package/dist/rules/codes.js.map +1 -1
  213. package/dist/rules/comment-markers.d.ts +16 -7
  214. package/dist/rules/comment-markers.d.ts.map +1 -1
  215. package/dist/rules/comment-markers.js +58 -19
  216. package/dist/rules/comment-markers.js.map +1 -1
  217. package/dist/rules/contract-visibility.d.ts +3 -0
  218. package/dist/rules/contract-visibility.d.ts.map +1 -0
  219. package/dist/rules/contract-visibility.js +337 -0
  220. package/dist/rules/contract-visibility.js.map +1 -0
  221. package/dist/rules/data-guard.d.ts +13 -0
  222. package/dist/rules/data-guard.d.ts.map +1 -1
  223. package/dist/rules/data-guard.js +356 -37
  224. package/dist/rules/data-guard.js.map +1 -1
  225. package/dist/rules/layout.d.ts +4 -0
  226. package/dist/rules/layout.d.ts.map +1 -0
  227. package/dist/rules/layout.js +19 -0
  228. package/dist/rules/layout.js.map +1 -0
  229. package/dist/rules/legacy.d.ts.map +1 -1
  230. package/dist/rules/legacy.js +178 -44
  231. package/dist/rules/legacy.js.map +1 -1
  232. package/dist/rules/pages.d.ts +50 -0
  233. package/dist/rules/pages.d.ts.map +1 -1
  234. package/dist/rules/pages.js +436 -4
  235. package/dist/rules/pages.js.map +1 -1
  236. package/dist/rules/routes.d.ts +6 -0
  237. package/dist/rules/routes.d.ts.map +1 -1
  238. package/dist/rules/routes.js +111 -16
  239. package/dist/rules/routes.js.map +1 -1
  240. package/dist/rules/secrets.js +2 -2
  241. package/dist/rules/secrets.js.map +1 -1
  242. package/dist/rules/security.d.ts +13 -1
  243. package/dist/rules/security.d.ts.map +1 -1
  244. package/dist/rules/security.js +313 -69
  245. package/dist/rules/security.js.map +1 -1
  246. package/dist/rules/zones.d.ts +13 -0
  247. package/dist/rules/zones.d.ts.map +1 -1
  248. package/dist/rules/zones.js +218 -44
  249. package/dist/rules/zones.js.map +1 -1
  250. package/dist/scaffold.d.ts.map +1 -1
  251. package/dist/scaffold.js +6 -4
  252. package/dist/scaffold.js.map +1 -1
  253. package/dist/sdk.d.ts.map +1 -1
  254. package/dist/sdk.js +14 -5
  255. package/dist/sdk.js.map +1 -1
  256. package/dist/stylex-migrate.d.ts.map +1 -1
  257. package/dist/stylex-migrate.js +290 -24
  258. package/dist/stylex-migrate.js.map +1 -1
  259. package/dist/test-tool.d.ts.map +1 -1
  260. package/dist/test-tool.js +6 -3
  261. package/dist/test-tool.js.map +1 -1
  262. package/dist/typecheck-coverage.d.ts +20 -0
  263. package/dist/typecheck-coverage.d.ts.map +1 -0
  264. package/dist/typecheck-coverage.js +129 -0
  265. package/dist/typecheck-coverage.js.map +1 -0
  266. package/dist/upgrade.d.ts +84 -0
  267. package/dist/upgrade.d.ts.map +1 -1
  268. package/dist/upgrade.js +498 -78
  269. package/dist/upgrade.js.map +1 -1
  270. package/dist/verification-plan.d.ts.map +1 -1
  271. package/dist/verification-plan.js +16 -2
  272. package/dist/verification-plan.js.map +1 -1
  273. package/dist/version-drift.d.ts +22 -0
  274. package/dist/version-drift.d.ts.map +1 -0
  275. package/dist/version-drift.js +94 -0
  276. package/dist/version-drift.js.map +1 -0
  277. package/dist/web-app-options.d.ts +1 -1
  278. package/dist/web-app-options.d.ts.map +1 -1
  279. package/dist/web-app-options.js +1 -1
  280. package/dist/web-app-options.js.map +1 -1
  281. package/dist/work-graph.d.ts.map +1 -1
  282. package/dist/work-graph.js +20 -8
  283. package/dist/work-graph.js.map +1 -1
  284. package/docs/examples.json +55 -7
  285. package/docs/llms-full.txt +2043 -222
  286. package/docs/types.json +1573 -246
  287. package/package.json +11 -12
  288. package/src/agent-files.ts +203 -0
  289. package/src/app-files.ts +34 -4
  290. package/src/assure-hydration.ts +18 -2
  291. package/src/assure.ts +88 -10
  292. package/src/capabilities-tool.ts +47 -9
  293. package/src/check-diagnostics.ts +75 -9
  294. package/src/check-scan.ts +1816 -377
  295. package/src/check.ts +258 -81
  296. package/src/cli.ts +242 -37
  297. package/src/client-module.ts +97 -0
  298. package/src/command-catalog.ts +237 -52
  299. package/src/component-check.ts +79 -0
  300. package/src/contract-proof.ts +3 -25
  301. package/src/contracts.ts +60 -3
  302. package/src/db-tool.ts +5 -2
  303. package/src/dev-bun-config.ts +51 -10
  304. package/src/dev-feed-tool.ts +112 -1
  305. package/src/diagnostics.ts +3 -0
  306. package/src/diff-tool.ts +33 -26
  307. package/src/doctor.ts +43 -21
  308. package/src/env-file.ts +7 -0
  309. package/src/fix-recipes.ts +116 -2
  310. package/src/hand-built-manifest.ts +144 -0
  311. package/src/hooks-report.ts +135 -0
  312. package/src/init-agents.ts +21 -14
  313. package/src/internal/config-paths.ts +154 -0
  314. package/src/internal/installed-package.ts +15 -0
  315. package/src/internal/nifra-project.ts +191 -0
  316. package/src/internal/project-specifiers.ts +477 -0
  317. package/src/internal/script-kind.ts +2 -1
  318. package/src/internal/source-facts.ts +48 -5
  319. package/src/internal/typescript-import.ts +15 -6
  320. package/src/introspect.ts +104 -28
  321. package/src/levels-tool.ts +124 -29
  322. package/src/load.ts +107 -17
  323. package/src/mcp-context.ts +12 -25
  324. package/src/mcp-delegate.ts +22 -10
  325. package/src/mcp-exec.ts +68 -18
  326. package/src/mcp-http.ts +1 -1
  327. package/src/mcp-io.ts +74 -0
  328. package/src/mcp-isolate.ts +9 -2
  329. package/src/mcp-path.ts +8 -22
  330. package/src/mcp-project-child.ts +1 -1
  331. package/src/mcp-root.ts +58 -8
  332. package/src/mcp-ws.ts +4 -2
  333. package/src/mcp.ts +56 -22
  334. package/src/migrate-layout.ts +2257 -176
  335. package/src/output-schemas.ts +1240 -0
  336. package/src/port.ts +3 -2
  337. package/src/project-facts.ts +17 -3
  338. package/src/project-path.ts +44 -13
  339. package/src/recipes/5.0.0.ts +35 -0
  340. package/src/recipes/index.ts +10 -0
  341. package/src/release-verification.ts +20 -0
  342. package/src/replay.ts +14 -3
  343. package/src/review.ts +44 -10
  344. package/src/rules/codes.ts +12 -0
  345. package/src/rules/comment-markers.ts +57 -21
  346. package/src/rules/contract-visibility.ts +383 -0
  347. package/src/rules/data-guard.ts +391 -42
  348. package/src/rules/layout.ts +22 -0
  349. package/src/rules/legacy.ts +206 -52
  350. package/src/rules/pages.ts +481 -4
  351. package/src/rules/routes.ts +124 -16
  352. package/src/rules/secrets.ts +2 -2
  353. package/src/rules/security.ts +391 -72
  354. package/src/rules/zones.ts +232 -42
  355. package/src/scaffold.ts +6 -4
  356. package/src/sdk.ts +14 -5
  357. package/src/stylex-migrate.ts +251 -24
  358. package/src/test-tool.ts +12 -3
  359. package/src/typecheck-coverage.ts +134 -0
  360. package/src/upgrade.ts +576 -72
  361. package/src/verification-plan.ts +33 -2
  362. package/src/version-drift.ts +118 -0
  363. package/src/web-app-options.ts +2 -2
  364. package/src/work-graph.ts +17 -12
@@ -0,0 +1,191 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { dirname, join, relative } from "node:path"
3
+ import { CONFIG_FILE, FRAMEWORK_FILE } from "../app-files.ts"
4
+
5
+ const NIFRA_PACKAGE = /^(?:@nifrajs\/|nifra$)/
6
+ const DEPENDENCY_FIELDS = [
7
+ "dependencies",
8
+ "devDependencies",
9
+ "peerDependencies",
10
+ "optionalDependencies",
11
+ ]
12
+
13
+ /** Whether a package.json text names `nifra` or a `@nifrajs/` package, or carries a `nifra` setting. */
14
+ function declaresNifra(manifest: string): boolean {
15
+ let parsed: unknown
16
+ try {
17
+ parsed = JSON.parse(manifest)
18
+ } catch {
19
+ // A manifest nobody can read proves nothing either way, so the directory stays checked.
20
+ return true
21
+ }
22
+ if (typeof parsed !== "object" || parsed === null) return true
23
+ if ("nifra" in parsed) return true
24
+ return DEPENDENCY_FIELDS.some((field) => {
25
+ const deps: unknown = Reflect.get(parsed, field)
26
+ return (
27
+ typeof deps === "object" &&
28
+ deps !== null &&
29
+ Object.keys(deps).some((name) => NIFRA_PACKAGE.test(name))
30
+ )
31
+ })
32
+ }
33
+
34
+ /**
35
+ * Whether a project-relative directory ("" for the root) belongs to a nifra project, read statically.
36
+ * The checked root always does: running the check there says so. Below it, walking up, the first
37
+ * directory with a nifra config decides yes, and the first package.json decides by naming `nifra` or
38
+ * a `@nifrajs/` package, or carrying a `nifra` setting; a nested package that does none of that is
39
+ * another project, such as another framework's app kept beside this one.
40
+ */
41
+ export function createNifraProjectProbe(root: string): (dir: string) => boolean {
42
+ const cache = new Map<string, boolean>()
43
+ const probe = (dir: string): boolean => {
44
+ const cached = cache.get(dir)
45
+ if (cached !== undefined) return cached
46
+ const at = join(root, dir)
47
+ let nifra: boolean
48
+ if (dir === "" || existsSync(join(at, CONFIG_FILE)) || existsSync(join(at, FRAMEWORK_FILE)))
49
+ nifra = true
50
+ else if (existsSync(join(at, "package.json"))) {
51
+ let manifest: string
52
+ try {
53
+ manifest = readFileSync(join(at, "package.json"), "utf8")
54
+ } catch {
55
+ manifest = ""
56
+ }
57
+ nifra = declaresNifra(manifest)
58
+ } else nifra = probe(dir.slice(0, Math.max(dir.lastIndexOf("/"), 0)))
59
+ cache.set(dir, nifra)
60
+ return nifra
61
+ }
62
+ return probe
63
+ }
64
+
65
+ export type ProjectKind = "api" | "web"
66
+
67
+ export interface KindDeclaration {
68
+ /** `"api"` or `"web"` from `"nifra": { "kind" }`, or `undefined` when the package declares none. */
69
+ readonly kind: ProjectKind | undefined
70
+ /** The package.json that decides, relative to the checked root (`../package.json` above it). */
71
+ readonly manifest: string | undefined
72
+ /** The problem with a declared value that is neither `"api"` nor `"web"`. */
73
+ readonly invalid?: string
74
+ }
75
+
76
+ function readJson(path: string): unknown {
77
+ try {
78
+ return JSON.parse(readFileSync(path, "utf8"))
79
+ } catch {
80
+ return undefined
81
+ }
82
+ }
83
+
84
+ const recordOf = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
85
+ typeof value === "object" && value !== null && !Array.isArray(value)
86
+ ? Object.fromEntries(Object.entries(value))
87
+ : undefined
88
+
89
+ /**
90
+ * The `"nifra": { "kind" }` that governs a project-relative directory: read from the nearest
91
+ * package.json at or above it, the way the zone setting is read, with no app code run.
92
+ */
93
+ export function createKindProbe(root: string): (dir: string) => KindDeclaration {
94
+ const cache = new Map<string, KindDeclaration>()
95
+ const probe = (absolute: string): KindDeclaration => {
96
+ const cached = cache.get(absolute)
97
+ if (cached !== undefined) return cached
98
+ let declaration: KindDeclaration
99
+ const manifestPath = join(absolute, "package.json")
100
+ if (existsSync(manifestPath)) {
101
+ const manifest = relative(root, manifestPath).replaceAll("\\", "/")
102
+ const kind = recordOf(recordOf(readJson(manifestPath))?.nifra)?.kind
103
+ if (kind === undefined) declaration = { kind: undefined, manifest }
104
+ else if (kind === "api" || kind === "web") declaration = { kind, manifest }
105
+ else
106
+ declaration = {
107
+ kind: undefined,
108
+ manifest,
109
+ invalid: `[nifra] ${manifest} "nifra.kind" must be "api" or "web", not ${JSON.stringify(kind)}`,
110
+ }
111
+ } else {
112
+ const parent = dirname(absolute)
113
+ declaration = parent === absolute ? { kind: undefined, manifest: undefined } : probe(parent)
114
+ }
115
+ cache.set(absolute, declaration)
116
+ return declaration
117
+ }
118
+ return (dir) => probe(join(root, dir))
119
+ }
120
+
121
+ /** Whether a project-relative directory holds web config: a `nifra.config.ts` exporting `adapter`, or
122
+ * `backend/framework.ts`. */
123
+ export function hasWebConfig(root: string, dir: string): boolean {
124
+ if (existsSync(join(root, dir, FRAMEWORK_FILE))) return true
125
+ const config = join(root, dir, CONFIG_FILE)
126
+ if (!existsSync(config)) return false
127
+ let text: string
128
+ try {
129
+ text = readFileSync(config, "utf8")
130
+ } catch {
131
+ return true
132
+ }
133
+ return /\bexport\s+(?:const|let|var|function)\s+adapter\b|\bexport\s*\{[^}]*\badapter\b/.test(
134
+ text,
135
+ )
136
+ }
137
+
138
+ const WEB_PACKAGE = /^@nifrajs\/web(?:-[a-z0-9-]+)?$/
139
+
140
+ /**
141
+ * Whether a project-relative directory ("" for the root) is inside a nifra web app. Walking up, the
142
+ * first directory with `nifra.config.ts` or `backend/framework.ts` makes it one; the first package.json
143
+ * decides instead: `"nifra": { "kind": "api" }` says no, otherwise naming `@nifrajs/web` or an adapter
144
+ * says yes. Nothing up to the root means no.
145
+ */
146
+ export function createWebAppProbe(root: string): (dir: string) => boolean {
147
+ const cache = new Map<string, boolean>()
148
+ const probe = (dir: string): boolean => {
149
+ const cached = cache.get(dir)
150
+ if (cached !== undefined) return cached
151
+ const at = join(root, dir)
152
+ let web: boolean
153
+ if (existsSync(join(at, CONFIG_FILE)) || existsSync(join(at, FRAMEWORK_FILE))) web = true
154
+ else if (existsSync(join(at, "package.json"))) {
155
+ const manifest = recordOf(readJson(join(at, "package.json")))
156
+ web =
157
+ recordOf(manifest?.nifra)?.kind !== "api" &&
158
+ DEPENDENCY_FIELDS.some((field) => {
159
+ const deps = recordOf(manifest?.[field])
160
+ return deps !== undefined && Object.keys(deps).some((name) => WEB_PACKAGE.test(name))
161
+ })
162
+ } else web = dir !== "" && probe(dir.slice(0, Math.max(dir.lastIndexOf("/"), 0)))
163
+ cache.set(dir, web)
164
+ return web
165
+ }
166
+ return probe
167
+ }
168
+
169
+ /** A zone or server-only finding outside any web app, with the API-only setting named. */
170
+ export const withApiKindHint = (message: string): string =>
171
+ `${message}${message.endsWith(".") ? "" : "."} If this project serves no pages, declare it in package.json: "nifra": { "kind": "api" }`
172
+
173
+ /**
174
+ * Whether `"nifra": { "kind": "api" }` takes a project-relative directory out of the zone checks. It
175
+ * does only where no web config sits at or above the directory, up to the checked root: an app that
176
+ * serves pages keeps its zone checks whatever its package.json says (NF-C037 reports the clash).
177
+ */
178
+ export function createApiOnlyProbe(root: string): (dir: string) => boolean {
179
+ const kindOf = createKindProbe(root)
180
+ const web = new Map<string, boolean>()
181
+ const webConfigUp = (dir: string): boolean => {
182
+ const cached = web.get(dir)
183
+ if (cached !== undefined) return cached
184
+ const found =
185
+ hasWebConfig(root, dir) ||
186
+ (dir !== "" && webConfigUp(dir.slice(0, Math.max(dir.lastIndexOf("/"), 0))))
187
+ web.set(dir, found)
188
+ return found
189
+ }
190
+ return (dir) => kindOf(dir).kind === "api" && !webConfigUp(dir)
191
+ }
@@ -0,0 +1,477 @@
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
+ * Whether the compiler settings for `file` keep an import whose named specifiers are all
27
+ * type-only, as `import {} from "x"` (`verbatimModuleSyntax`). `true` when its tsconfig chain
28
+ * cannot be read, so a scan that trusts the answer keeps the edge.
29
+ */
30
+ readonly keepsTypeOnlyImports: (file: string) => boolean
31
+ }
32
+
33
+ /** How an alias specifier reached its file: what a rewrite through the same alias has to fit. */
34
+ interface AliasRule {
35
+ /** The pattern the specifier matched; `undefined` for a `baseUrl` name, which matches none. */
36
+ readonly key: string | undefined
37
+ /** Every pattern in the same scope: a rewritten name must still match `key`, or none for `baseUrl`. */
38
+ readonly keys: readonly string[]
39
+ /** The directory targets resolve against. */
40
+ readonly base: string
41
+ /** The key's targets in try order, `STAR` where the matched text goes. */
42
+ readonly templates: readonly string[]
43
+ /** The path the specifier spells before probing, and the file probing found. */
44
+ readonly written: string
45
+ readonly found: string
46
+ }
47
+
48
+ const STAR = "\u0000"
49
+
50
+ const EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".json"]
51
+ // TypeScript's ESM convention: `./x.js` names `x.ts` when only the source exists.
52
+ const SOURCE_FOR: Readonly<Record<string, readonly string[]>> = {
53
+ ".js": [".ts", ".tsx"],
54
+ ".jsx": [".tsx"],
55
+ ".mjs": [".mts"],
56
+ ".cjs": [".cts"],
57
+ }
58
+ // The package.json `imports` conditions a server-side bundle of this project matches, in no order:
59
+ // the target object's own key order decides, as in Node.
60
+ const CONDITIONS: ReadonlySet<string> = new Set(["bun", "import", "default"])
61
+ const MAX_EXTENDS = 16
62
+
63
+ interface PathsConfig {
64
+ /** Directory `paths` targets resolve against: `baseUrl` when set, else the defining config's. */
65
+ readonly pathsBase: string
66
+ readonly paths: ReadonlyArray<readonly [string, readonly string[]]>
67
+ readonly baseUrl: string | undefined
68
+ /** The {@link IMPORT_EMIT_OPTIONS} the chain sets, later configs winning; `undefined` when part of
69
+ * the chain could not be read. */
70
+ readonly importEmit: Readonly<Record<string, unknown>> | undefined
71
+ }
72
+
73
+ // The options that decide whether an import left with only type specifiers is emitted. The last two
74
+ // are removed in TypeScript 6 but still read by older compilers and bundlers.
75
+ const IMPORT_EMIT_OPTIONS = [
76
+ "verbatimModuleSyntax",
77
+ "importsNotUsedAsValues",
78
+ "preserveValueImports",
79
+ ]
80
+
81
+ /** JSON with comments and trailing commas, as tsconfig files are written. Throws on anything else. */
82
+ export function parseJsonc(text: string): unknown {
83
+ let code = ""
84
+ for (let i = 0; i < text.length; ) {
85
+ const ch = text[i]
86
+ if (ch === '"') {
87
+ let end = i + 1
88
+ while (end < text.length && text[end] !== '"') end += text[end] === "\\" ? 2 : 1
89
+ code += text.slice(i, end + 1)
90
+ i = end + 1
91
+ } else if (ch === "/" && text[i + 1] === "/") {
92
+ const newline = text.indexOf("\n", i)
93
+ i = newline === -1 ? text.length : newline
94
+ } else if (ch === "/" && text[i + 1] === "*") {
95
+ const close = text.indexOf("*/", i + 2)
96
+ i = close === -1 ? text.length : close + 2
97
+ } else {
98
+ code += ch
99
+ i += 1
100
+ }
101
+ }
102
+ let json = ""
103
+ for (let i = 0; i < code.length; i += 1) {
104
+ const ch = code[i]
105
+ if (ch === '"') {
106
+ let end = i + 1
107
+ while (end < code.length && code[end] !== '"') end += code[end] === "\\" ? 2 : 1
108
+ json += code.slice(i, end + 1)
109
+ i = end
110
+ } else if (ch === "," && /^\s*[}\]]/.test(code.slice(i + 1))) {
111
+ } else json += ch
112
+ }
113
+ return JSON.parse(json.replace(/^\uFEFF/, ""))
114
+ }
115
+
116
+ const record = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
117
+ typeof value === "object" && value !== null && !Array.isArray(value)
118
+ ? Object.fromEntries(Object.entries(value))
119
+ : undefined
120
+
121
+ /** Whether `path` is a file. A path through a file (`a.ts/index.ts`) is none, not an error. */
122
+ export const isFile = (path: string): boolean => {
123
+ try {
124
+ return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false
125
+ } catch {
126
+ return false
127
+ }
128
+ }
129
+
130
+ const real = (path: string): string => {
131
+ try {
132
+ return realpathSync(path)
133
+ } catch {
134
+ return path
135
+ }
136
+ }
137
+
138
+ /** The file a path names: as written, its source for a `.js` name, with an extension, or an index. */
139
+ function probe(base: string, exists: (path: string) => boolean): string | undefined {
140
+ if (exists(base)) return base
141
+ const ext = extname(base)
142
+ for (const source of SOURCE_FOR[ext] ?? []) {
143
+ const candidate = `${base.slice(0, -ext.length)}${source}`
144
+ if (exists(candidate)) return candidate
145
+ }
146
+ for (const extension of EXTENSIONS)
147
+ if (exists(`${base}${extension}`)) return `${base}${extension}`
148
+ for (const extension of EXTENSIONS) {
149
+ const index = join(base, `index${extension}`)
150
+ if (exists(index)) return index
151
+ }
152
+ return undefined
153
+ }
154
+
155
+ /**
156
+ * `file` written the way `written` names `found`: as is, without its extension, as its folder when
157
+ * `found` is an index, or with the `.js` name of a `.ts` source.
158
+ */
159
+ function sameStyle(written: string, found: string, file: string): string {
160
+ if (written === found) return file
161
+ const ext = extname(file)
162
+ const bare = file.slice(0, file.length - ext.length)
163
+ if (found.startsWith(`${written}/index.`) || found.startsWith(`${written}\\index.`))
164
+ return basename(file).startsWith("index.") ? dirname(file) : bare
165
+ if (found.startsWith(written)) return bare
166
+ const writtenExt = extname(written)
167
+ return SOURCE_FOR[writtenExt] === undefined ? file : `${bare}${writtenExt}`
168
+ }
169
+
170
+ /** Longest-prefix `*` match, TypeScript's rule for `paths` and Node's for `imports`. */
171
+ function matchPattern(
172
+ patterns: readonly string[],
173
+ specifier: string,
174
+ ): { readonly pattern: string; readonly star: string | undefined } | undefined {
175
+ if (patterns.includes(specifier)) return { pattern: specifier, star: undefined }
176
+ let best: { pattern: string; star: string } | undefined
177
+ for (const pattern of patterns) {
178
+ const at = pattern.indexOf("*")
179
+ if (at === -1 || pattern.indexOf("*", at + 1) !== -1) continue
180
+ const prefix = pattern.slice(0, at)
181
+ const suffix = pattern.slice(at + 1)
182
+ if (specifier.length < prefix.length + suffix.length) continue
183
+ if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) continue
184
+ if (best !== undefined && prefix.length <= best.pattern.indexOf("*")) continue
185
+ best = { pattern, star: specifier.slice(prefix.length, specifier.length - suffix.length) }
186
+ }
187
+ return best
188
+ }
189
+
190
+ export interface SpecifierResolverOptions {
191
+ /** Whether a module file exists, for resolving against a planned layout rather than the disk.
192
+ * Config files (tsconfig, package.json) are still read from the disk. */
193
+ readonly moduleExists?: (path: string) => boolean
194
+ }
195
+
196
+ export function createSpecifierResolver(options: SpecifierResolverOptions = {}): ProjectResolver {
197
+ const exists = options.moduleExists ?? isFile
198
+ const configs = new Map<string, PathsConfig | undefined>()
199
+ const tsconfigByDir = new Map<string, string | undefined>()
200
+ const manifests = new Map<string, Readonly<Record<string, unknown>> | undefined>()
201
+ const packageByDir = new Map<string, string | undefined>()
202
+
203
+ const readJsonc = (path: string): Readonly<Record<string, unknown>> | undefined => {
204
+ try {
205
+ return record(parseJsonc(readFileSync(path, "utf8")))
206
+ } catch {
207
+ return undefined
208
+ }
209
+ }
210
+
211
+ /** The nearest `tsconfig.json` (or `jsconfig.json`) at or above `dir`, as Bun's resolver picks it. */
212
+ const tsconfigFor = (dir: string): string | undefined => {
213
+ if (tsconfigByDir.has(dir)) return tsconfigByDir.get(dir)
214
+ const own = ["tsconfig.json", "jsconfig.json"].map((name) => join(dir, name)).find(isFile)
215
+ const parent = dirname(dir)
216
+ const found = own ?? (parent === dir ? undefined : tsconfigFor(parent))
217
+ tsconfigByDir.set(dir, found)
218
+ return found
219
+ }
220
+
221
+ /** An `extends` entry's file: a path relative to the config, or a package found in `node_modules`. */
222
+ const extendedConfig = (from: string, name: string): string | undefined => {
223
+ const withJson = (path: string): string | undefined =>
224
+ isFile(path) ? path : isFile(`${path}.json`) ? `${path}.json` : undefined
225
+ if (name.startsWith("./") || name.startsWith("../") || isAbsolute(name))
226
+ return withJson(join(dirname(from), name))
227
+ for (let dir = dirname(from); ; dir = dirname(dir)) {
228
+ const target = join(dir, "node_modules", name)
229
+ const direct = withJson(target)
230
+ if (direct !== undefined) return direct
231
+ const manifest = readJsonc(join(target, "package.json"))
232
+ const field = manifest?.tsconfig
233
+ const nested =
234
+ typeof field === "string"
235
+ ? withJson(join(target, field))
236
+ : withJson(join(target, "tsconfig"))
237
+ if (nested !== undefined) return nested
238
+ if (dirname(dir) === dir) return undefined
239
+ }
240
+ }
241
+
242
+ /** The `paths`, `baseUrl` and import-emit options a config ends up with after its `extends` chain. */
243
+ const pathsConfig = (
244
+ file: string,
245
+ seen: ReadonlySet<string> = new Set(),
246
+ ): PathsConfig | undefined => {
247
+ if (configs.has(file)) return configs.get(file)
248
+ if (seen.has(file) || seen.size >= MAX_EXTENDS) return undefined
249
+ const config = readJsonc(file)
250
+ let inherited: PathsConfig | undefined
251
+ let importEmit: Record<string, unknown> | undefined = {}
252
+ const extended = config?.extends
253
+ for (const name of typeof extended === "string"
254
+ ? [extended]
255
+ : Array.isArray(extended)
256
+ ? extended
257
+ : []) {
258
+ if (typeof name !== "string") continue
259
+ const base = extendedConfig(file, name)
260
+ const parent = base === undefined ? undefined : pathsConfig(base, new Set([...seen, file]))
261
+ importEmit =
262
+ importEmit === undefined || parent?.importEmit === undefined
263
+ ? undefined
264
+ : { ...importEmit, ...parent.importEmit }
265
+ if (parent === undefined) continue
266
+ inherited = {
267
+ paths: parent.paths.length > 0 ? parent.paths : (inherited?.paths ?? []),
268
+ baseUrl: parent.baseUrl ?? inherited?.baseUrl,
269
+ pathsBase:
270
+ parent.paths.length > 0 ? parent.pathsBase : (inherited?.pathsBase ?? parent.pathsBase),
271
+ importEmit,
272
+ }
273
+ }
274
+ const options = record(config?.compilerOptions)
275
+ if (importEmit !== undefined) {
276
+ for (const key of IMPORT_EMIT_OPTIONS)
277
+ if (options?.[key] !== undefined) importEmit = { ...importEmit, [key]: options[key] }
278
+ }
279
+ const baseUrl =
280
+ typeof options?.baseUrl === "string"
281
+ ? join(dirname(file), options.baseUrl)
282
+ : inherited?.baseUrl
283
+ const ownPaths = record(options?.paths)
284
+ const paths =
285
+ ownPaths === undefined
286
+ ? (inherited?.paths ?? [])
287
+ : Object.entries(ownPaths).map(
288
+ ([pattern, targets]) =>
289
+ [
290
+ pattern,
291
+ Array.isArray(targets)
292
+ ? targets.filter((t): t is string => typeof t === "string")
293
+ : [],
294
+ ] as const,
295
+ )
296
+ const pathsBase =
297
+ baseUrl ?? (ownPaths === undefined ? (inherited?.pathsBase ?? dirname(file)) : dirname(file))
298
+ const resolved = config === undefined ? undefined : { paths, baseUrl, pathsBase, importEmit }
299
+ configs.set(file, resolved)
300
+ return resolved
301
+ }
302
+
303
+ // Keyed by config, not directory: every package name reaches here, from every folder of the project.
304
+ const aliases = new Map<string, string | undefined>()
305
+ const viaTsconfig = (fromFile: string, specifier: string): string | undefined => {
306
+ const tsconfig = tsconfigFor(dirname(fromFile))
307
+ if (tsconfig === undefined) return undefined
308
+ const key = `${tsconfig}\0${specifier}`
309
+ if (aliases.has(key)) return aliases.get(key)
310
+ const found = aliasTarget(tsconfig, specifier)
311
+ aliases.set(key, found)
312
+ return found
313
+ }
314
+ const aliasTarget = (tsconfig: string, specifier: string): string | undefined =>
315
+ pathsRule(tsconfig, specifier)?.found
316
+
317
+ /** The nearest package.json at or above `dir`: the package scope Node reads `imports` from. */
318
+ const packageFor = (dir: string): string | undefined => {
319
+ if (packageByDir.has(dir)) return packageByDir.get(dir)
320
+ const own = join(dir, "package.json")
321
+ const parent = dirname(dir)
322
+ const found = isFile(own) ? own : parent === dir ? undefined : packageFor(parent)
323
+ packageByDir.set(dir, found)
324
+ return found
325
+ }
326
+
327
+ const importTarget = (target: unknown, star: string | undefined): string | undefined => {
328
+ if (typeof target === "string")
329
+ return target.startsWith("./") ? target.replaceAll("*", star ?? "") : undefined
330
+ if (Array.isArray(target)) {
331
+ for (const entry of target) {
332
+ const found = importTarget(entry, star)
333
+ if (found !== undefined) return found
334
+ }
335
+ return undefined
336
+ }
337
+ const conditions = record(target)
338
+ if (conditions === undefined) return undefined
339
+ for (const [condition, value] of Object.entries(conditions)) {
340
+ if (CONDITIONS.has(condition)) return importTarget(value, star)
341
+ }
342
+ return undefined
343
+ }
344
+
345
+ const importsRule = (fromFile: string, specifier: string): AliasRule | undefined => {
346
+ const manifestPath = packageFor(dirname(fromFile))
347
+ if (manifestPath === undefined) return undefined
348
+ if (!manifests.has(manifestPath)) manifests.set(manifestPath, readJsonc(manifestPath))
349
+ const imports = record(manifests.get(manifestPath)?.imports)
350
+ if (imports === undefined) return undefined
351
+ const keys = Object.keys(imports)
352
+ const match = matchPattern(keys, specifier)
353
+ if (match === undefined) return undefined
354
+ const template = importTarget(imports[match.pattern], STAR)
355
+ if (template === undefined) return undefined
356
+ const base = dirname(manifestPath)
357
+ const written = join(base, template.replaceAll(STAR, match.star ?? ""))
358
+ const found = probe(written, exists)
359
+ return found === undefined
360
+ ? undefined
361
+ : { key: match.pattern, keys, base, templates: [template], written, found }
362
+ }
363
+ const viaImports = (fromFile: string, specifier: string): string | undefined =>
364
+ importsRule(fromFile, specifier)?.found
365
+
366
+ const tsconfigRule = (fromFile: string, specifier: string): AliasRule | undefined => {
367
+ const tsconfig = tsconfigFor(dirname(fromFile))
368
+ return tsconfig === undefined ? undefined : pathsRule(tsconfig, specifier)
369
+ }
370
+ const pathsRule = (tsconfig: string, specifier: string): AliasRule | undefined => {
371
+ const config = pathsConfig(tsconfig)
372
+ if (config === undefined) return undefined
373
+ const keys = config.paths.map(([pattern]) => pattern)
374
+ const match = matchPattern(keys, specifier)
375
+ if (match === undefined) {
376
+ if (config.baseUrl === undefined) return undefined
377
+ const written = join(config.baseUrl, specifier)
378
+ const found = probe(written, exists)
379
+ return found === undefined
380
+ ? undefined
381
+ : { key: undefined, keys, base: config.baseUrl, templates: [STAR], written, found }
382
+ }
383
+ // TypeScript permits at most one wildcard in each substitution, as in the alias key.
384
+ const targets = (config.paths.find(([pattern]) => pattern === match.pattern)?.[1] ?? []).filter(
385
+ (target) => target.indexOf("*") === target.lastIndexOf("*"),
386
+ )
387
+ for (const target of targets) {
388
+ const written = join(config.pathsBase, target.replaceAll("*", match.star ?? ""))
389
+ const found = probe(written, exists)
390
+ if (found === undefined) continue
391
+ const templates = targets.map((t) => t.replaceAll("*", STAR))
392
+ return { key: match.pattern, keys, base: config.pathsBase, templates, written, found }
393
+ }
394
+ return undefined
395
+ }
396
+
397
+ const respell = (fromFile: string, specifier: string, file: string): string | undefined => {
398
+ if (/^\.\.?(?:\/|$)/.test(specifier) || isAbsolute(specifier)) return undefined
399
+ const rule =
400
+ (specifier.startsWith("#") ? importsRule(fromFile, specifier) : undefined) ??
401
+ tsconfigRule(fromFile, specifier)
402
+ if (rule === undefined) return undefined
403
+ const spelled = sameStyle(rule.written, rule.found, file)
404
+ for (const template of rule.templates) {
405
+ const at = template.indexOf(STAR)
406
+ if (at === -1) continue
407
+ // An empty head still ends at a folder boundary: `src2/x` is not under a `baseUrl` of `src`.
408
+ const prefix = join(rule.base, template.slice(0, at) || "./")
409
+ const suffix = template.slice(at + STAR.length)
410
+ if (!spelled.startsWith(prefix) || !spelled.endsWith(suffix)) continue
411
+ if (spelled.length < prefix.length + suffix.length) continue
412
+ const star = spelled
413
+ .slice(prefix.length, spelled.length - suffix.length)
414
+ .replaceAll("\\", "/")
415
+ if (star === "" || star.startsWith("/")) continue
416
+ const keyAt = rule.key?.indexOf("*") ?? 0
417
+ if (keyAt === -1) continue
418
+ const next =
419
+ rule.key === undefined
420
+ ? star
421
+ : `${rule.key.slice(0, keyAt)}${star}${rule.key.slice(keyAt + 1)}`
422
+ // The rewritten name must take the same route: another, longer pattern would claim it.
423
+ if (matchPattern(rule.keys, next)?.pattern !== rule.key) continue
424
+ return next
425
+ }
426
+ return undefined
427
+ }
428
+
429
+ const resolved = new Map<string, string | undefined>()
430
+ const resolve: SpecifierResolver = (fromFile, specifier) => {
431
+ // Every rule below reads only the importing file's directory, so the answer is the directory's.
432
+ const key = `${dirname(fromFile)}\0${specifier}`
433
+ if (resolved.has(key)) return resolved.get(key)
434
+ const path = resolveFrom(fromFile, specifier)
435
+ resolved.set(key, path)
436
+ return path
437
+ }
438
+ const pathPatterns = (dir: string): readonly string[] => {
439
+ const tsconfig = tsconfigFor(dir)
440
+ const config = tsconfig === undefined ? undefined : pathsConfig(tsconfig)
441
+ return (config?.paths ?? []).map(([pattern]) => pattern)
442
+ }
443
+
444
+ const keepsTypeOnlyImports = (file: string): boolean => {
445
+ const tsconfig = tsconfigFor(dirname(file))
446
+ if (tsconfig === undefined) return false
447
+ const emit = pathsConfig(tsconfig)?.importEmit
448
+ if (emit === undefined) return true
449
+ return (
450
+ emit.verbatimModuleSyntax === true ||
451
+ emit.preserveValueImports === true ||
452
+ emit.importsNotUsedAsValues === "preserve" ||
453
+ emit.importsNotUsedAsValues === "error"
454
+ )
455
+ }
456
+
457
+ return Object.assign(resolve, { respell, pathPatterns, keepsTypeOnlyImports })
458
+
459
+ function resolveFrom(fromFile: string, specifier: string): string | undefined {
460
+ let found: string | undefined
461
+ if (
462
+ specifier.startsWith("./") ||
463
+ specifier.startsWith("../") ||
464
+ specifier === "." ||
465
+ specifier === ".."
466
+ )
467
+ found = probe(join(dirname(fromFile), specifier), exists)
468
+ else if (isAbsolute(specifier)) found = probe(specifier, exists)
469
+ // A `#` name is a package.json import first; a tsconfig may still map it.
470
+ else if (specifier.startsWith("#"))
471
+ found = viaImports(fromFile, specifier) ?? viaTsconfig(fromFile, specifier)
472
+ else found = viaTsconfig(fromFile, specifier)
473
+ if (found === undefined) return undefined
474
+ const path = real(found)
475
+ return /[\\/]node_modules[\\/]/.test(path) ? undefined : path
476
+ }
477
+ }
@@ -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
  }