@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
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 {
@@ -165,6 +165,10 @@ Usage:
165
165
  against it: a route the target can't honour (an ssr route on a
166
166
  static build, ISR where there's no revalidation) exits nonzero
167
167
  with the consequence - run in CI so it fails the build, not prod.
168
+ nifra routes --hooks [--json] List the hooks each backend route and each prefix's unmatched
169
+ [--since <file>] paths actually run: kind, plugin name, and where it was
170
+ registered (app, group, merge, mount). --since diffs against a
171
+ saved --json report.
168
172
  nifra init-agents [--force] [--json] Retrofit an EXISTING app with the agent files a new app ships:
169
173
  .mcp.json + .cursor/mcp.json (register this project's nifra MCP),
170
174
  each agent's pointer to AGENTS.md (CLAUDE.md, GEMINI.md,
@@ -201,6 +205,8 @@ Usage:
201
205
  backend code in browser code). Run as "done"; --json for agents;
202
206
  --sarif emits the same stable diagnostics as SARIF 2.1.0 for external
203
207
  review tools; --lints-only skips tsc for a near-instant inner-loop pass.
208
+ With a contracts.lock.json, both modes import backend/app.ts
209
+ (running its top-level code) to compare the routes it serves.
204
210
  nifra verify [--release] [--json] Run the shared repository verification gate. --release runs the
205
211
  full build, test, coverage, corpus, consumer, and cross-runtime gate.
206
212
  nifra fix [--code <NF-code>] Apply a registered diagnostic recipe, then print the remaining
@@ -262,7 +268,8 @@ Usage:
262
268
  [--list] [--json] apply exact imports, pin every matching dependency (keeping
263
269
  [--exact] ^/~, or exact with --exact), then verify with nifra check.
264
270
  [--allow-downgrade] Dry-run by default; --write applies then verifies (--no-verify
265
- to skip). --list shows available targets. A version newer than
271
+ [--no-hooks] to skip), then prints how hook scope changed (--no-hooks to
272
+ skip). --list shows available targets. A version newer than
266
273
  this CLI prints the command for that release's CLI. Fail-closed
267
274
  on an unknown version or a rollback (--allow-downgrade
268
275
  overrides). Deterministic + idempotent.
@@ -276,6 +283,10 @@ Usage:
276
283
  params, the data its output schemas let through, and api typed
277
284
  by backend/app.ts. dev, build and check refresh them too;
278
285
  --check fails when one is stale.
286
+ nifra schemas [--write] [--json] Draft loaderOutput/actionOutput from each loader's and
287
+ action's return type for review; --write adds the missing
288
+ ones (marked nifra:generated) and refreshes generated ones.
289
+ Lists the returned fields a hand-written schema drops.
279
290
  nifra port [--target <t>] [--json] Portability linter: print a feature × deploy-target capability
280
291
  [--ci] [--strict] matrix (in-memory stores, in-process cron/WebSocket, Bun/Deno
281
292
  globals, node: builtins) with file:line evidence. --target auto-
@@ -304,12 +315,23 @@ set in the process environment is never overwritten by a file.
304
315
  ${renderCommandCatalogHelp()}`
305
316
 
306
317
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
307
- const CLI_VERSION = "4.0.0"
318
+ const CLI_VERSION = "5.0.0"
308
319
 
309
320
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
310
321
  const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
311
322
  const asBunPlugins = (v: readonly unknown[]): BunPlugin[] => v as BunPlugin[]
312
323
 
324
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Trusted application configuration supplies native Vite plugins; the pinned pipeline validates their hooks.
325
+ const asComponentPlugins = (
326
+ value: readonly unknown[],
327
+ ): NonNullable<
328
+ import("@nifrajs/web/server-components/build").ComponentPipelineOptions["plugins"]
329
+ > =>
330
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Trusted config forwards the pinned Vite plugin ABI; this does not accept request data.
331
+ value as NonNullable<
332
+ import("@nifrajs/web/server-components/build").ComponentPipelineOptions["plugins"]
333
+ >
334
+
313
335
  /**
314
336
  * Render an error for the CLI, unwrapping the detail a bare `.message` drops.
315
337
  *
@@ -345,6 +367,27 @@ export function formatCliError(err: unknown): string {
345
367
  }
346
368
 
347
369
  async function dev(app: LoadedApp, flags: Flags): Promise<void> {
370
+ if (app.framework.componentPipeline !== undefined) {
371
+ if (flags.bun)
372
+ throw new Error(
373
+ "[nifra/components] Components require the Vite 8 pipeline; Bun remains a server runtime",
374
+ )
375
+ const { devServerComponents } = await import("@nifrajs/web/server-components/build")
376
+ if (
377
+ app.resolvedPlugins.clientPlugins.length > 0 ||
378
+ app.resolvedPlugins.serverPlugins.length > 0
379
+ )
380
+ throw new Error(
381
+ "[nifra/components] Use Vite equivalents for client/server Bun plugins in the component pipeline",
382
+ )
383
+ await devServerComponents({
384
+ ...app.framework.componentPipeline,
385
+ root: app.cwd,
386
+ port: flags.port,
387
+ plugins: asComponentPlugins(app.resolvedPlugins.vitePlugins),
388
+ })
389
+ return
390
+ }
348
391
  if (flags.vite && flags.bun) {
349
392
  throw new Error("[nifra] `nifra dev` takes `--vite` or `--bun`, not both.")
350
393
  }
@@ -511,6 +554,7 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
511
554
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
512
555
  ...(fw.define ? { define: fw.define } : {}),
513
556
  indicator: showsIndicator(flags, fw),
557
+ allowedHosts: fw.dev?.allowedHosts ?? [],
514
558
  createApp: (clientEntry, importQuery, dev) =>
515
559
  createWebApp({
516
560
  adapter: asAdapter(fw.adapter),
@@ -693,7 +737,7 @@ async function assertFreshWorkspaceDists(cwd: string): Promise<void> {
693
737
  let rootPackage: Record<string, unknown>
694
738
  try {
695
739
  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
740
+ // nifra-expect NF-S001: not a security gate. An unreadable/non-JSON package.json is a separate
697
741
  // build problem; returning here skips the freshness check rather than masking a denial.
698
742
  } catch {
699
743
  return // an unreadable/!JSON package.json is a separate problem; don't mask it as a stale-dist failure
@@ -727,6 +771,40 @@ async function buildForTarget(
727
771
  throw new Error("[nifra] `nifra build` takes `--vite` or `--bun`, not both.")
728
772
  }
729
773
  const { framework: fw } = app
774
+ if (fw.componentPipeline !== undefined) {
775
+ if (flags.bun)
776
+ throw new Error(
777
+ "[nifra/components] Components require the Vite 8 pipeline; Bun remains a server runtime",
778
+ )
779
+ if (target === "static")
780
+ throw new Error(
781
+ "[nifra/components] Component owners require private per-request execution; choose bun, node, deno, cloudflare, or vercel",
782
+ )
783
+ if (
784
+ app.resolvedPlugins.clientPlugins.length > 0 ||
785
+ app.resolvedPlugins.serverPlugins.length > 0
786
+ )
787
+ throw new Error(
788
+ "[nifra/components] Use Vite equivalents for client/server Bun plugins in the component pipeline",
789
+ )
790
+ const { buildServerComponents } = await import("@nifrajs/web/server-components/build")
791
+ const outDir =
792
+ target === "vercel" && flags.out === undefined
793
+ ? resolve(app.cwd, ".vercel/output")
794
+ : app.outDir
795
+ const result = await buildServerComponents({
796
+ ...fw.componentPipeline,
797
+ target,
798
+ root: app.cwd,
799
+ outDir,
800
+ plugins: asComponentPlugins(app.resolvedPlugins.vitePlugins),
801
+ })
802
+ await assertFreshWorkspaceDists(app.cwd)
803
+ console.log(
804
+ `[nifra/components] Built ${fw.componentPipeline.adapter} for ${target} (${result.build}) in ${outDir}`,
805
+ )
806
+ return
807
+ }
730
808
  const cssCodeSplitRequested = fw.cssCodeSplit === false
731
809
  const hasBunTransforms =
732
810
  app.resolvedPlugins.clientPlugins.length > 0 || app.resolvedPlugins.serverPlugins.length > 0
@@ -945,6 +1023,42 @@ function installReflectionExitHint(): (command: string | undefined) => void {
945
1023
  }
946
1024
  }
947
1025
 
1026
+ /** Commands that import the app only to read what it declares, never sending it a request, so the
1027
+ * app may skip connecting to its services. `assure` qualifies without its bundle lane, which renders. */
1028
+ const READ_ONLY_COMMANDS: ReadonlySet<string> = new Set([
1029
+ "capabilities",
1030
+ "check",
1031
+ "context",
1032
+ "contracts",
1033
+ "diff",
1034
+ "doctor",
1035
+ "manifest",
1036
+ "openapi",
1037
+ "routes",
1038
+ "sync-manifest",
1039
+ "sync-routes",
1040
+ ])
1041
+
1042
+ const HELD_OPEN_GRACE_MS = 2_000
1043
+
1044
+ /**
1045
+ * A command that imports the app for its answer has already answered when the app's own work - a
1046
+ * database or Redis client reconnecting, an interval - would keep the process alive indefinitely.
1047
+ * A process that exits on its own never reaches this unref'd timer; a held one is ended with a note.
1048
+ */
1049
+ function exitIfHeldOpen(command: string): void {
1050
+ setTimeout(() => {
1051
+ process.stderr.write(
1052
+ `[nifra] \`nifra ${command}\` has finished, but something the app started when it was imported ` +
1053
+ `(an open connection or a timer) is keeping the process alive, so nifra is exiting.` +
1054
+ (process.env[REFLECT_ENV] === command
1055
+ ? ` To skip that work while nifra only reads the app, check process.env.${REFLECT_ENV}, which is set to "${command}".\n`
1056
+ : "\n"),
1057
+ )
1058
+ process.exit(typeof process.exitCode === "number" ? process.exitCode : 0)
1059
+ }, HELD_OPEN_GRACE_MS).unref()
1060
+ }
1061
+
948
1062
  /** Set on the copy of `nifra mcp` that serves without `.env` files, so it does not restart again. */
949
1063
  const MCP_WITHOUT_ENV_FILES = "NIFRA_MCP_WITHOUT_ENV_FILES"
950
1064
 
@@ -1051,6 +1165,9 @@ async function main(): Promise<void> {
1051
1165
  const catalogSpec = findCommandSpec(dbSub === undefined ? command : `db-${dbSub}`)
1052
1166
  if (catalogSpec?.transports.includes("cli")) {
1053
1167
  const markReflecting = installReflectionExitHint()
1168
+ if (READ_ONLY_COMMANDS.has(command) || (command === "assure" && !assureBundleRequested(argv))) {
1169
+ process.env[REFLECT_ENV] = command
1170
+ }
1054
1171
  try {
1055
1172
  const input = bindCommandArgv(catalogSpec, argv.slice(dbSub === undefined ? 1 : 2))
1056
1173
  markReflecting(command)
@@ -1086,6 +1203,7 @@ async function main(): Promise<void> {
1086
1203
  ? candidate
1087
1204
  : 1
1088
1205
  }
1206
+ exitIfHeldOpen(command)
1089
1207
  return
1090
1208
  }
1091
1209
  if (command === "verify") {
@@ -1150,6 +1268,7 @@ async function main(): Promise<void> {
1150
1268
  json: argv.includes("--json"),
1151
1269
  list: argv.includes("--list"),
1152
1270
  verify: !argv.includes("--no-verify"),
1271
+ hooks: !argv.includes("--no-hooks"),
1153
1272
  allowDowngrade: argv.includes("--allow-downgrade"),
1154
1273
  exact: argv.includes("--exact"),
1155
1274
  cliVersion: CLI_VERSION,
@@ -1278,12 +1397,29 @@ function findCliCommand(values: readonly string[]): number {
1278
1397
  return values.findIndex((value) => isCliCommandToken(value))
1279
1398
  }
1280
1399
 
1400
+ /**
1401
+ * The command vector from one runtime argv: the command, then its arguments. Arguments typed before
1402
+ * the command (`nifra --json check`) follow the command's own, ahead of any `--`, rather than being
1403
+ * dropped. Only when index 1 is the script: a runtime flag there means the leading entries are the
1404
+ * runtime's own, not the caller's.
1405
+ */
1406
+ export function commandArgv(values: readonly string[], commandIndex: number): readonly string[] {
1407
+ const command = values.slice(commandIndex)
1408
+ const script = values[1]
1409
+ if (commandIndex <= 2 || script === undefined || script.startsWith("-")) return command
1410
+ const leading = values.slice(2, commandIndex)
1411
+ const separator = command.indexOf("--")
1412
+ return separator === -1
1413
+ ? [...command, ...leading]
1414
+ : [...command.slice(0, separator), ...leading, ...command.slice(separator)]
1415
+ }
1416
+
1281
1417
  function cliArgv(): readonly string[] {
1282
1418
  const reexec = reexecCliArgv()
1283
1419
  if (reexec !== undefined) return reexec
1284
1420
  for (const values of [Bun.argv, process.argv]) {
1285
1421
  const commandIndex = findCliCommand(values)
1286
- if (commandIndex >= 0) return values.slice(commandIndex)
1422
+ if (commandIndex >= 0) return commandArgv(values, commandIndex)
1287
1423
  }
1288
1424
  return Bun.argv.slice(2)
1289
1425
  }
@@ -0,0 +1,61 @@
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 { CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
8
+ import { stripComments } from "./check-scan.ts"
9
+ import type { SpecifierResolver } from "./internal/project-specifiers.ts"
10
+
11
+ const CLIENT_MODULE_EXPORT =
12
+ /\bexport\s+const\s+clientModule\s*(?::\s*string\s*)?=\s*(?:"([^"\\\r\n]*)"|'([^'\\\r\n]*)')/
13
+
14
+ export interface ClientModuleRoot {
15
+ /** The project-relative config file that names it. */
16
+ readonly config: string
17
+ readonly line: number
18
+ readonly specifier: string
19
+ /** The real path of the file it names. */
20
+ readonly file: string
21
+ }
22
+
23
+ /** The project-relative directory of every app among `files`: one holding a nifra config. */
24
+ export function appDirs(files: readonly string[]): string[] {
25
+ const apps = new Set<string>()
26
+ for (const file of files) {
27
+ for (const config of [CONFIG_FILE, FRAMEWORK_FILE]) {
28
+ if (file === config) apps.add("")
29
+ else if (file.endsWith(`/${config}`)) apps.add(file.slice(0, -config.length - 1))
30
+ }
31
+ }
32
+ return [...apps]
33
+ }
34
+
35
+ /**
36
+ * The file `clientModule` names in `app`, read the way the loader reads it: from `nifra.config.ts`
37
+ * when it exists, else `backend/framework.ts`, and a relative path against the app root. `undefined`
38
+ * for a package specifier, a value that is not a string literal, or a path to no file.
39
+ */
40
+ export function clientModuleRoot(
41
+ root: string,
42
+ app: string,
43
+ resolve: SpecifierResolver,
44
+ ): ClientModuleRoot | undefined {
45
+ const config = existsSync(join(root, app, CONFIG_FILE)) ? CONFIG_FILE : FRAMEWORK_FILE
46
+ const path = join(root, app, config)
47
+ if (!existsSync(path)) return undefined
48
+ const text = readFileSync(path, "utf8")
49
+ // `stripComments` keeps offsets, so the match index is a position in `text`.
50
+ const match = CLIENT_MODULE_EXPORT.exec(stripComments(text))
51
+ if (match === null) return undefined
52
+ const specifier = match[1] ?? match[2] ?? ""
53
+ const file = resolve(join(root, app, CONFIG_FILE), specifier)
54
+ if (file === undefined) return undefined
55
+ return {
56
+ config: app === "" ? config : `${app}/${config}`,
57
+ line: text.slice(0, match.index).split("\n").length,
58
+ specifier,
59
+ file,
60
+ }
61
+ }