@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
package/src/cli.ts CHANGED
@@ -24,7 +24,7 @@ import { formatShadowedPages, shadowedPages } from "@nifrajs/web/route-manifest"
24
24
  import type { BunPlugin } from "bun"
25
25
  import { adapterFile, BACKEND_APP_FILE } from "./app-files.ts"
26
26
  import { bindCommandArgv, findCommandSpec, renderCommandCatalogHelp } from "./command-catalog.ts"
27
- import { applyEnvFiles, forgetAutoLoadedEnv, takeEnvFileFlags } from "./env-file.ts"
27
+ import { applyEnvFiles, forgetAutoLoadedEnv, REFLECT_ENV, takeEnvFileFlags } from "./env-file.ts"
28
28
  import { FRAMEWORK_WEB_OPTIONS, type LoadedApp, loadApp, type NifraFramework } from "./load.ts"
29
29
  import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.ts"
30
30
  import {
@@ -126,6 +126,9 @@ Usage:
126
126
  one left. Exits 1 while current-code errors are open.
127
127
  nifra logs [--since <n>] [--level <l>] [--grep <s>] The dev server's console, server and
128
128
  browser, each line tagged with its request.
129
+ nifra explain [<message>] [--stack <s>] [--name <n>] A structured diagnostic for an error: its
130
+ code, the frame in your source, and the cause and fix when
131
+ nifra knows them. No message: the dev server's latest error.
129
132
  nifra cdn-check <url> Check a deployed page behind a CDN: served from cache, no
130
133
  internal or CDN-only headers reaching the visitor, and soft
131
134
  navigations get page data, not the cached document. Exits 1
@@ -165,6 +168,10 @@ Usage:
165
168
  against it: a route the target can't honour (an ssr route on a
166
169
  static build, ISR where there's no revalidation) exits nonzero
167
170
  with the consequence - run in CI so it fails the build, not prod.
171
+ nifra routes --hooks [--json] List the hooks each backend route and each prefix's unmatched
172
+ [--since <file>] paths actually run: kind, plugin name, and where it was
173
+ registered (app, group, merge, mount). --since diffs against a
174
+ saved --json report.
168
175
  nifra init-agents [--force] [--json] Retrofit an EXISTING app with the agent files a new app ships:
169
176
  .mcp.json + .cursor/mcp.json (register this project's nifra MCP),
170
177
  each agent's pointer to AGENTS.md (CLAUDE.md, GEMINI.md,
@@ -201,6 +208,8 @@ Usage:
201
208
  backend code in browser code). Run as "done"; --json for agents;
202
209
  --sarif emits the same stable diagnostics as SARIF 2.1.0 for external
203
210
  review tools; --lints-only skips tsc for a near-instant inner-loop pass.
211
+ With a contracts.lock.json, both modes import backend/app.ts
212
+ (running its top-level code) to compare the routes it serves.
204
213
  nifra verify [--release] [--json] Run the shared repository verification gate. --release runs the
205
214
  full build, test, coverage, corpus, consumer, and cross-runtime gate.
206
215
  nifra fix [--code <NF-code>] Apply a registered diagnostic recipe, then print the remaining
@@ -262,20 +271,28 @@ Usage:
262
271
  [--list] [--json] apply exact imports, pin every matching dependency (keeping
263
272
  [--exact] ^/~, or exact with --exact), then verify with nifra check.
264
273
  [--allow-downgrade] Dry-run by default; --write applies then verifies (--no-verify
265
- to skip). --list shows available targets. A version newer than
274
+ [--no-hooks] to skip), then prints how hook scope changed (--no-hooks to
275
+ skip). --list shows available targets. A version newer than
266
276
  this CLI prints the command for that release's CLI. Fail-closed
267
277
  on an unknown version or a rollback (--allow-downgrade
268
278
  overrides). Deterministic + idempotent.
269
- nifra migrate layout [--write] [--json]
279
+ nifra migrate layout [--write] [--zones] [--json]
270
280
  Move an app onto the frontend/backend split: split each route
271
281
  into x.tsx + x.backend.ts, fold _middleware.ts into
272
282
  _layout.backend.ts, move backend.ts/framework.ts under backend/,
273
- and zone the other modules into frontend/, backend/ or shared/.
274
- Dry-run by default; --write applies.
283
+ and zone the other modules into frontend/, backend/ or shared/
284
+ (--zones keeps them in place and lists them in package.json
285
+ "nifra.zones"). Dry-run by default; --write applies.
275
286
  nifra types [--check] [--json] Generate each route's ./+types module under .nifra/types: its
276
287
  params, the data its output schemas let through, and api typed
277
288
  by backend/app.ts. dev, build and check refresh them too;
278
289
  --check fails when one is stale.
290
+ nifra schemas [--write] [--check] [--json]
291
+ Draft loaderOutput/actionOutput from each loader's and
292
+ action's return type for review; --write adds the missing
293
+ ones (marked nifra:generated) and refreshes generated ones.
294
+ Lists the returned fields a hand-written schema drops;
295
+ --check fails on a missing, stale or any-typed one.
279
296
  nifra port [--target <t>] [--json] Portability linter: print a feature × deploy-target capability
280
297
  [--ci] [--strict] matrix (in-memory stores, in-process cron/WebSocket, Bun/Deno
281
298
  globals, node: builtins) with file:line evidence. --target auto-
@@ -304,12 +321,23 @@ set in the process environment is never overwritten by a file.
304
321
  ${renderCommandCatalogHelp()}`
305
322
 
306
323
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
307
- const CLI_VERSION = "4.0.2"
324
+ const CLI_VERSION = "5.1.0"
308
325
 
309
326
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
310
327
  const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
311
328
  const asBunPlugins = (v: readonly unknown[]): BunPlugin[] => v as BunPlugin[]
312
329
 
330
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Trusted application configuration supplies native Vite plugins; the pinned pipeline validates their hooks.
331
+ const asComponentPlugins = (
332
+ value: readonly unknown[],
333
+ ): NonNullable<
334
+ import("@nifrajs/web/server-components/build").ComponentPipelineOptions["plugins"]
335
+ > =>
336
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Trusted config forwards the pinned Vite plugin ABI; this does not accept request data.
337
+ value as NonNullable<
338
+ import("@nifrajs/web/server-components/build").ComponentPipelineOptions["plugins"]
339
+ >
340
+
313
341
  /**
314
342
  * Render an error for the CLI, unwrapping the detail a bare `.message` drops.
315
343
  *
@@ -344,7 +372,36 @@ export function formatCliError(err: unknown): string {
344
372
  return String(err)
345
373
  }
346
374
 
375
+ /** A failed command's message on stderr and, for a `--json` caller parsing stdout, as JSON there. */
376
+ function reportFailure(err: unknown, argv: readonly string[]): void {
377
+ const message = formatCliError(err)
378
+ console.error(message)
379
+ if (argv.includes("--json") || argv.includes("--json=true"))
380
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2))
381
+ }
382
+
347
383
  async function dev(app: LoadedApp, flags: Flags): Promise<void> {
384
+ if (app.framework.componentPipeline !== undefined) {
385
+ if (flags.bun)
386
+ throw new Error(
387
+ "[nifra/components] Components require the Vite 8 pipeline; Bun remains a server runtime",
388
+ )
389
+ const { devServerComponents } = await import("@nifrajs/web/server-components/build")
390
+ if (
391
+ app.resolvedPlugins.clientPlugins.length > 0 ||
392
+ app.resolvedPlugins.serverPlugins.length > 0
393
+ )
394
+ throw new Error(
395
+ "[nifra/components] Use Vite equivalents for client/server Bun plugins in the component pipeline",
396
+ )
397
+ await devServerComponents({
398
+ ...app.framework.componentPipeline,
399
+ root: app.cwd,
400
+ port: flags.port,
401
+ plugins: asComponentPlugins(app.resolvedPlugins.vitePlugins),
402
+ })
403
+ return
404
+ }
348
405
  if (flags.vite && flags.bun) {
349
406
  throw new Error("[nifra] `nifra dev` takes `--vite` or `--bun`, not both.")
350
407
  }
@@ -511,6 +568,7 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
511
568
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
512
569
  ...(fw.define ? { define: fw.define } : {}),
513
570
  indicator: showsIndicator(flags, fw),
571
+ allowedHosts: fw.dev?.allowedHosts ?? [],
514
572
  createApp: (clientEntry, importQuery, dev) =>
515
573
  createWebApp({
516
574
  adapter: asAdapter(fw.adapter),
@@ -693,7 +751,7 @@ async function assertFreshWorkspaceDists(cwd: string): Promise<void> {
693
751
  let rootPackage: Record<string, unknown>
694
752
  try {
695
753
  rootPackage = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>
696
- // @nifra-gate-reviewed: not a security gate. An unreadable/non-JSON package.json is a separate
754
+ // nifra-expect NF-S001: not a security gate. An unreadable/non-JSON package.json is a separate
697
755
  // build problem; returning here skips the freshness check rather than masking a denial.
698
756
  } catch {
699
757
  return // an unreadable/!JSON package.json is a separate problem; don't mask it as a stale-dist failure
@@ -727,6 +785,40 @@ async function buildForTarget(
727
785
  throw new Error("[nifra] `nifra build` takes `--vite` or `--bun`, not both.")
728
786
  }
729
787
  const { framework: fw } = app
788
+ if (fw.componentPipeline !== undefined) {
789
+ if (flags.bun)
790
+ throw new Error(
791
+ "[nifra/components] Components require the Vite 8 pipeline; Bun remains a server runtime",
792
+ )
793
+ if (target === "static")
794
+ throw new Error(
795
+ "[nifra/components] Component owners require private per-request execution; choose bun, node, deno, cloudflare, or vercel",
796
+ )
797
+ if (
798
+ app.resolvedPlugins.clientPlugins.length > 0 ||
799
+ app.resolvedPlugins.serverPlugins.length > 0
800
+ )
801
+ throw new Error(
802
+ "[nifra/components] Use Vite equivalents for client/server Bun plugins in the component pipeline",
803
+ )
804
+ const { buildServerComponents } = await import("@nifrajs/web/server-components/build")
805
+ const outDir =
806
+ target === "vercel" && flags.out === undefined
807
+ ? resolve(app.cwd, ".vercel/output")
808
+ : app.outDir
809
+ const result = await buildServerComponents({
810
+ ...fw.componentPipeline,
811
+ target,
812
+ root: app.cwd,
813
+ outDir,
814
+ plugins: asComponentPlugins(app.resolvedPlugins.vitePlugins),
815
+ })
816
+ await assertFreshWorkspaceDists(app.cwd)
817
+ console.log(
818
+ `[nifra/components] Built ${fw.componentPipeline.adapter} for ${target} (${result.build}) in ${outDir}`,
819
+ )
820
+ return
821
+ }
730
822
  const cssCodeSplitRequested = fw.cssCodeSplit === false
731
823
  const hasBunTransforms =
732
824
  app.resolvedPlugins.clientPlugins.length > 0 || app.resolvedPlugins.serverPlugins.length > 0
@@ -876,35 +968,73 @@ async function start(app: LoadedApp, flags: Flags): Promise<void> {
876
968
  await import(serverFile)
877
969
  }
878
970
 
971
+ const VALUE_FLAGS: Readonly<Record<string, "port" | "out" | "target">> = {
972
+ "--port": "port",
973
+ "-p": "port",
974
+ "--out": "out",
975
+ "--target": "target",
976
+ }
977
+
978
+ type SwitchField = "poll" | "report" | "vite" | "bun" | "allowDuplicateIdentity" | "noIndicator"
979
+
980
+ const SWITCH_FLAGS: Readonly<Record<string, SwitchField>> = {
981
+ "--poll": "poll",
982
+ "--report": "report",
983
+ "--vite": "vite",
984
+ "--bun": "bun",
985
+ "--allow-duplicate-identity": "allowDuplicateIdentity",
986
+ "--no-indicator": "noIndicator",
987
+ }
988
+
989
+ /** The flags of `nifra dev`, `build` and `start`, as `--flag value` or `--flag=value`. Anything else is
990
+ * refused: an ignored `--target=vercel` would build the default target and exit 0. */
879
991
  export function parseFlags(args: readonly string[]): Flags {
880
992
  // Port precedence: `--port`/`-p` (most specific) > `PORT` env > the framework default. The default is
881
993
  // the SAME uncommon port for `nifra dev` and `nifra start` (DEFAULT_DEV_PORT) so a project's URL is
882
994
  // stable across commands and doesn't collide with the usual 3000/5173/8080 crowd.
883
995
  let port = Number(Bun.env.PORT ?? DEFAULT_DEV_PORT)
884
996
  let out: string | undefined
885
- let poll = Bun.env.CHOKIDAR_USEPOLLING === "1"
886
997
  let target: string | undefined
887
- let report = false
888
- let vite = false
889
- let bun = false
890
- let allowDuplicateIdentity = false
891
- let noIndicator = false
998
+ const switches: Record<SwitchField, boolean> = {
999
+ poll: Bun.env.CHOKIDAR_USEPOLLING === "1",
1000
+ report: false,
1001
+ vite: false,
1002
+ bun: false,
1003
+ allowDuplicateIdentity: false,
1004
+ noIndicator: false,
1005
+ }
892
1006
  for (let i = 0; i < args.length; i++) {
893
- const a = args[i]
894
- if ((a === "--port" || a === "-p") && args[i + 1]) port = Number(args[++i])
895
- else if (a === "--out" && args[i + 1]) out = args[++i] as string
896
- else if (a === "--poll") poll = true
897
- else if (a === "--target" && args[i + 1]) target = args[++i] as string
898
- else if (a === "--report") report = true
899
- else if (a === "--vite") vite = true
900
- else if (a === "--bun") bun = true
901
- else if (a === "--allow-duplicate-identity") allowDuplicateIdentity = true
902
- else if (a === "--no-indicator") noIndicator = true
1007
+ const arg = args[i] as string
1008
+ const equals = arg.startsWith("--") ? arg.indexOf("=") : -1
1009
+ const name = equals === -1 ? arg : arg.slice(0, equals)
1010
+ const valueField = Object.hasOwn(VALUE_FLAGS, name) ? VALUE_FLAGS[name] : undefined
1011
+ if (valueField !== undefined) {
1012
+ const value = equals === -1 ? args[++i] : arg.slice(equals + 1)
1013
+ if (value === undefined || value === "" || (equals === -1 && value.startsWith("-")))
1014
+ throw new Error(`[nifra] ${name} needs a value`)
1015
+ if (valueField === "port") port = Number(value)
1016
+ else if (valueField === "out") out = value
1017
+ else target = value
1018
+ continue
1019
+ }
1020
+ const switchField = Object.hasOwn(SWITCH_FLAGS, name) ? SWITCH_FLAGS[name] : undefined
1021
+ if (switchField !== undefined) {
1022
+ const value = equals === -1 ? "true" : arg.slice(equals + 1)
1023
+ if (value !== "true" && value !== "false")
1024
+ throw new Error(`[nifra] ${name} takes no value (or =true / =false)`)
1025
+ switches[switchField] = value === "true"
1026
+ continue
1027
+ }
1028
+ throw new Error(
1029
+ arg.startsWith("-")
1030
+ ? `[nifra] unknown option: ${arg} - see \`nifra help\``
1031
+ : `[nifra] unexpected argument: ${arg} - see \`nifra help\``,
1032
+ )
903
1033
  }
904
1034
  if (!Number.isFinite(port) || port < 0 || port > 65535) {
905
1035
  throw new Error(`[nifra] invalid --port: ${port}`)
906
1036
  }
907
- return { port, out, poll, target, report, vite, bun, allowDuplicateIdentity, noIndicator }
1037
+ return { port, out, target, ...switches }
908
1038
  }
909
1039
 
910
1040
  /**
@@ -945,6 +1075,42 @@ function installReflectionExitHint(): (command: string | undefined) => void {
945
1075
  }
946
1076
  }
947
1077
 
1078
+ /** Commands that import the app only to read what it declares, never sending it a request, so the
1079
+ * app may skip connecting to its services. `assure` qualifies without its bundle lane, which renders. */
1080
+ const READ_ONLY_COMMANDS: ReadonlySet<string> = new Set([
1081
+ "capabilities",
1082
+ "check",
1083
+ "context",
1084
+ "contracts",
1085
+ "diff",
1086
+ "doctor",
1087
+ "manifest",
1088
+ "openapi",
1089
+ "routes",
1090
+ "sync-manifest",
1091
+ "sync-routes",
1092
+ ])
1093
+
1094
+ const HELD_OPEN_GRACE_MS = 2_000
1095
+
1096
+ /**
1097
+ * A command that imports the app for its answer has already answered when the app's own work - a
1098
+ * database or Redis client reconnecting, an interval - would keep the process alive indefinitely.
1099
+ * A process that exits on its own never reaches this unref'd timer; a held one is ended with a note.
1100
+ */
1101
+ function exitIfHeldOpen(command: string): void {
1102
+ setTimeout(() => {
1103
+ process.stderr.write(
1104
+ `[nifra] \`nifra ${command}\` has finished, but something the app started when it was imported ` +
1105
+ `(an open connection or a timer) is keeping the process alive, so nifra is exiting.` +
1106
+ (process.env[REFLECT_ENV] === command
1107
+ ? ` To skip that work while nifra only reads the app, check process.env.${REFLECT_ENV}, which is set to "${command}".\n`
1108
+ : "\n"),
1109
+ )
1110
+ process.exit(typeof process.exitCode === "number" ? process.exitCode : 0)
1111
+ }, HELD_OPEN_GRACE_MS).unref()
1112
+ }
1113
+
948
1114
  /** Set on the copy of `nifra mcp` that serves without `.env` files, so it does not restart again. */
949
1115
  const MCP_WITHOUT_ENV_FILES = "NIFRA_MCP_WITHOUT_ENV_FILES"
950
1116
 
@@ -1051,6 +1217,9 @@ async function main(): Promise<void> {
1051
1217
  const catalogSpec = findCommandSpec(dbSub === undefined ? command : `db-${dbSub}`)
1052
1218
  if (catalogSpec?.transports.includes("cli")) {
1053
1219
  const markReflecting = installReflectionExitHint()
1220
+ if (READ_ONLY_COMMANDS.has(command) || (command === "assure" && !assureBundleRequested(argv))) {
1221
+ process.env[REFLECT_ENV] = command
1222
+ }
1054
1223
  try {
1055
1224
  const input = bindCommandArgv(catalogSpec, argv.slice(dbSub === undefined ? 1 : 2))
1056
1225
  markReflecting(command)
@@ -1073,7 +1242,7 @@ async function main(): Promise<void> {
1073
1242
  // A thrown error is nifra's own reporting path, not a silent abort: clear the sentinel so the
1074
1243
  // hint stays reserved for the case where the process died inside the app import.
1075
1244
  markReflecting(undefined)
1076
- console.error(formatCliError(err))
1245
+ reportFailure(err, argv)
1077
1246
  const candidate =
1078
1247
  typeof err === "object" && err !== null && "exitCode" in err
1079
1248
  ? (err as { readonly exitCode?: unknown }).exitCode
@@ -1086,6 +1255,7 @@ async function main(): Promise<void> {
1086
1255
  ? candidate
1087
1256
  : 1
1088
1257
  }
1258
+ exitIfHeldOpen(command)
1089
1259
  return
1090
1260
  }
1091
1261
  if (command === "verify") {
@@ -1135,6 +1305,7 @@ async function main(): Promise<void> {
1135
1305
  console.error(formatCliError(err))
1136
1306
  process.exitCode = 1
1137
1307
  }
1308
+ exitIfHeldOpen(command)
1138
1309
  return
1139
1310
  }
1140
1311
  // `upgrade` is a pure cwd file-transformer (package.json pins + import moves) driven by a per-release
@@ -1150,15 +1321,17 @@ async function main(): Promise<void> {
1150
1321
  json: argv.includes("--json"),
1151
1322
  list: argv.includes("--list"),
1152
1323
  verify: !argv.includes("--no-verify"),
1324
+ hooks: !argv.includes("--no-hooks"),
1153
1325
  allowDowngrade: argv.includes("--allow-downgrade"),
1154
1326
  exact: argv.includes("--exact"),
1155
1327
  cliVersion: CLI_VERSION,
1156
1328
  })
1157
1329
  if (!ok) process.exitCode = 1
1158
1330
  } catch (err) {
1159
- console.error(formatCliError(err))
1331
+ reportFailure(err, argv)
1160
1332
  process.exitCode = 1
1161
1333
  }
1334
+ exitIfHeldOpen(command)
1162
1335
  return
1163
1336
  }
1164
1337
  // `port` is a pure cwd-based portability linter (scans source, doesn't run the app) - like `check`/
@@ -1169,17 +1342,32 @@ async function main(): Promise<void> {
1169
1342
  process.exitCode = 1
1170
1343
  return
1171
1344
  }
1345
+ if (argv.includes("--help") || argv.includes("-h")) {
1346
+ console.log(HELP)
1347
+ return
1348
+ }
1172
1349
  const flags = parseFlags(argv.slice(1))
1173
- const app = await loadApp(process.cwd(), flags.out)
1174
- if (command !== "start") {
1175
- const { refreshRouteTypes, watchRouteTypes } = await import("./route-types.ts")
1176
- refreshRouteTypes(app.cwd)
1177
- // A relaunched `nifra dev --bun` child runs beside its parent, which already watches.
1178
- if (command === "dev" && process.env.NIFRA_BUN_DEV_TOKEN === undefined) watchRouteTypes(app.cwd)
1350
+ try {
1351
+ const app = await loadApp(process.cwd(), flags.out)
1352
+ if (command !== "start" && process.env.NIFRA_BUN_DEV_TOKEN === undefined) {
1353
+ const { unservedRoutesWarning } = await import("./introspect.ts")
1354
+ const unserved = unservedRoutesWarning(app)
1355
+ if (unserved !== undefined) console.warn(unserved)
1356
+ }
1357
+ if (command !== "start") {
1358
+ const { refreshRouteTypes, watchRouteTypes } = await import("./route-types.ts")
1359
+ refreshRouteTypes(app.cwd)
1360
+ // A relaunched `nifra dev --bun` child runs beside its parent, which already watches.
1361
+ if (command === "dev" && process.env.NIFRA_BUN_DEV_TOKEN === undefined)
1362
+ watchRouteTypes(app.cwd)
1363
+ }
1364
+ if (command === "dev") await dev(app, flags)
1365
+ else if (command === "build") await buildForTarget(app, flags.target, flags)
1366
+ else await start(app, flags)
1367
+ } finally {
1368
+ // `dev` and `start` keep serving after they return; a build is over.
1369
+ if (command === "build") exitIfHeldOpen(command)
1179
1370
  }
1180
- if (command === "dev") await dev(app, flags)
1181
- else if (command === "build") await buildForTarget(app, flags.target, flags)
1182
- else await start(app, flags)
1183
1371
  }
1184
1372
 
1185
1373
  /**
@@ -1278,12 +1466,29 @@ function findCliCommand(values: readonly string[]): number {
1278
1466
  return values.findIndex((value) => isCliCommandToken(value))
1279
1467
  }
1280
1468
 
1469
+ /**
1470
+ * The command vector from one runtime argv: the command, then its arguments. Arguments typed before
1471
+ * the command (`nifra --json check`) follow the command's own, ahead of any `--`, rather than being
1472
+ * dropped. Only when index 1 is the script: a runtime flag there means the leading entries are the
1473
+ * runtime's own, not the caller's.
1474
+ */
1475
+ export function commandArgv(values: readonly string[], commandIndex: number): readonly string[] {
1476
+ const command = values.slice(commandIndex)
1477
+ const script = values[1]
1478
+ if (commandIndex <= 2 || script === undefined || script.startsWith("-")) return command
1479
+ const leading = values.slice(2, commandIndex)
1480
+ const separator = command.indexOf("--")
1481
+ return separator === -1
1482
+ ? [...command, ...leading]
1483
+ : [...command.slice(0, separator), ...leading, ...command.slice(separator)]
1484
+ }
1485
+
1281
1486
  function cliArgv(): readonly string[] {
1282
1487
  const reexec = reexecCliArgv()
1283
1488
  if (reexec !== undefined) return reexec
1284
1489
  for (const values of [Bun.argv, process.argv]) {
1285
1490
  const commandIndex = findCliCommand(values)
1286
- if (commandIndex >= 0) return values.slice(commandIndex)
1491
+ if (commandIndex >= 0) return commandArgv(values, commandIndex)
1287
1492
  }
1288
1493
  return Bun.argv.slice(2)
1289
1494
  }
@@ -1349,7 +1554,7 @@ if (isMainModule() || isBunDevReexecChild() || hasCliCommandToken()) {
1349
1554
  try {
1350
1555
  await main()
1351
1556
  } catch (err) {
1352
- console.error(formatCliError(err))
1557
+ reportFailure(err, cliArgv())
1353
1558
  process.exitCode = 1
1354
1559
  }
1355
1560
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * An app's `clientModule` when its config names a first-party file. The browser bundle's generated
3
+ * entry imports it, so the build classifies that file, and everything it imports, as browser code.
4
+ */
5
+ import { existsSync, readFileSync } from "node:fs"
6
+ import { join } from "node:path"
7
+ import type * as TS from "typescript"
8
+ import { CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
9
+ import { stripComments } from "./check-scan.ts"
10
+ import { configPathReader, exportedInitializer } from "./internal/config-paths.ts"
11
+ import type { SpecifierResolver } from "./internal/project-specifiers.ts"
12
+
13
+ const CLIENT_MODULE_EXPORT =
14
+ /\bexport\s+const\s+clientModule\s*(?::\s*string\s*)?=\s*(?:"([^"\\\r\n]*)"|'([^'\\\r\n]*)')/
15
+
16
+ export interface ClientModuleRoot {
17
+ /** The project-relative config file that names it. */
18
+ readonly config: string
19
+ readonly line: number
20
+ readonly specifier: string
21
+ /** The real path of the file it names. */
22
+ readonly file: string
23
+ }
24
+
25
+ /** The project-relative directory of every app among `files`: one holding a nifra config. */
26
+ export function appDirs(files: readonly string[]): string[] {
27
+ const apps = new Set<string>()
28
+ for (const file of files) {
29
+ for (const config of [CONFIG_FILE, FRAMEWORK_FILE]) {
30
+ if (file === config) apps.add("")
31
+ else if (file.endsWith(`/${config}`)) apps.add(file.slice(0, -config.length - 1))
32
+ }
33
+ }
34
+ return [...apps]
35
+ }
36
+
37
+ /**
38
+ * The file `clientModule` names in `app`, read the way the loader reads it: from `nifra.config.ts`
39
+ * when it exists, else `backend/framework.ts`; a string against the app root, and, with `ts`, a
40
+ * `resolve`/`join` of the config's own folder against that folder. `undefined` for a package
41
+ * specifier, a value built any other way, or a path to no file.
42
+ */
43
+ export function clientModuleRoot(
44
+ root: string,
45
+ app: string,
46
+ resolve: SpecifierResolver,
47
+ ts?: typeof TS,
48
+ ): ClientModuleRoot | undefined {
49
+ const config = existsSync(join(root, app, CONFIG_FILE)) ? CONFIG_FILE : FRAMEWORK_FILE
50
+ const path = join(root, app, config)
51
+ if (!existsSync(path)) return undefined
52
+ const text = readFileSync(path, "utf8")
53
+ const found = ts === undefined ? literalClientModule(text) : staticClientModule(ts, path, text)
54
+ if (found === undefined) return undefined
55
+ const file =
56
+ found.base === "app"
57
+ ? resolve(join(root, app, CONFIG_FILE), found.specifier)
58
+ : resolve(path, found.specifier.startsWith("../") ? found.specifier : `./${found.specifier}`)
59
+ if (file === undefined) return undefined
60
+ return {
61
+ config: app === "" ? config : `${app}/${config}`,
62
+ line: text.slice(0, found.index).split("\n").length,
63
+ specifier: found.specifier,
64
+ file,
65
+ }
66
+ }
67
+
68
+ interface FoundClientModule {
69
+ readonly base: "app" | "config"
70
+ readonly specifier: string
71
+ readonly index: number
72
+ }
73
+
74
+ function literalClientModule(text: string): FoundClientModule | undefined {
75
+ // `stripComments` keeps offsets, so the match index is a position in `text`.
76
+ const match = CLIENT_MODULE_EXPORT.exec(stripComments(text))
77
+ if (match === null) return undefined
78
+ return { base: "app", specifier: match[1] ?? match[2] ?? "", index: match.index }
79
+ }
80
+
81
+ function staticClientModule(
82
+ ts: typeof TS,
83
+ path: string,
84
+ text: string,
85
+ ): FoundClientModule | undefined {
86
+ let sf: TS.SourceFile
87
+ try {
88
+ sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
89
+ } catch {
90
+ // TypeScript 7 parses only what its session preloaded; the literal form still reads.
91
+ return literalClientModule(text)
92
+ }
93
+ const initializer = exportedInitializer(ts, sf, "clientModule")
94
+ const found = configPathReader(ts, sf)(initializer)
95
+ if (initializer === undefined || found === undefined) return undefined
96
+ return { base: found.base, specifier: found.path, index: initializer.getStart(sf) }
97
+ }