@nifrajs/web 3.5.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (390) hide show
  1. package/dist/backend-only.d.ts +15 -0
  2. package/dist/backend-only.d.ts.map +1 -0
  3. package/dist/backend-only.js +16 -0
  4. package/dist/backend-only.js.map +1 -0
  5. package/dist/boundary.d.ts +5 -0
  6. package/dist/boundary.d.ts.map +1 -1
  7. package/dist/boundary.js +11 -2
  8. package/dist/boundary.js.map +1 -1
  9. package/dist/build-plan.d.ts +18 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +82 -34
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +4 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +68 -13
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +57 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +356 -85
  20. package/dist/build.js.map +1 -1
  21. package/dist/bun-dev-entry.d.ts +1 -1
  22. package/dist/bun-dev-entry.js +1 -1
  23. package/dist/cdn.d.ts +74 -0
  24. package/dist/cdn.d.ts.map +1 -0
  25. package/dist/cdn.js +132 -0
  26. package/dist/cdn.js.map +1 -0
  27. package/dist/client.d.ts +3 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +111 -15
  30. package/dist/client.js.map +1 -1
  31. package/dist/conformance.d.ts +4 -3
  32. package/dist/conformance.d.ts.map +1 -1
  33. package/dist/conformance.js +12 -3
  34. package/dist/conformance.js.map +1 -1
  35. package/dist/csp.d.ts +93 -0
  36. package/dist/csp.d.ts.map +1 -0
  37. package/dist/csp.js +122 -0
  38. package/dist/csp.js.map +1 -0
  39. package/dist/dev-client.d.ts +76 -0
  40. package/dist/dev-client.d.ts.map +1 -0
  41. package/dist/dev-client.js +280 -0
  42. package/dist/dev-client.js.map +1 -0
  43. package/dist/dev-diagnostics.d.ts +26 -2
  44. package/dist/dev-diagnostics.d.ts.map +1 -1
  45. package/dist/dev-diagnostics.js +38 -7
  46. package/dist/dev-diagnostics.js.map +1 -1
  47. package/dist/dev-error.d.ts +4 -13
  48. package/dist/dev-error.d.ts.map +1 -1
  49. package/dist/dev-error.js +46 -4
  50. package/dist/dev-error.js.map +1 -1
  51. package/dist/dev-feed.d.ts +298 -0
  52. package/dist/dev-feed.d.ts.map +1 -0
  53. package/dist/dev-feed.js +797 -0
  54. package/dist/dev-feed.js.map +1 -0
  55. package/dist/dev-indicator.d.ts +38 -0
  56. package/dist/dev-indicator.d.ts.map +1 -0
  57. package/dist/dev-indicator.js +93 -0
  58. package/dist/dev-indicator.js.map +1 -0
  59. package/dist/dev-session.d.ts +77 -0
  60. package/dist/dev-session.d.ts.map +1 -0
  61. package/dist/dev-session.js +580 -0
  62. package/dist/dev-session.js.map +1 -0
  63. package/dist/dev-sourcemap.d.ts +27 -0
  64. package/dist/dev-sourcemap.d.ts.map +1 -0
  65. package/dist/dev-sourcemap.js +235 -0
  66. package/dist/dev-sourcemap.js.map +1 -0
  67. package/dist/dev-ssr-graph.d.ts.map +1 -1
  68. package/dist/dev-ssr-graph.js +13 -5
  69. package/dist/dev-ssr-graph.js.map +1 -1
  70. package/dist/dev.d.ts +15 -2
  71. package/dist/dev.d.ts.map +1 -1
  72. package/dist/dev.js +69 -44
  73. package/dist/dev.js.map +1 -1
  74. package/dist/diagnostic-catalog.d.ts +35 -0
  75. package/dist/diagnostic-catalog.d.ts.map +1 -0
  76. package/dist/diagnostic-catalog.js +203 -0
  77. package/dist/diagnostic-catalog.js.map +1 -0
  78. package/dist/diagnostic-prompt.d.ts +49 -0
  79. package/dist/diagnostic-prompt.d.ts.map +1 -0
  80. package/dist/diagnostic-prompt.js +303 -0
  81. package/dist/diagnostic-prompt.js.map +1 -0
  82. package/dist/diagnostic.d.ts +17 -23
  83. package/dist/diagnostic.d.ts.map +1 -1
  84. package/dist/diagnostic.js +31 -39
  85. package/dist/diagnostic.js.map +1 -1
  86. package/dist/draft.d.ts.map +1 -1
  87. package/dist/draft.js +4 -1
  88. package/dist/draft.js.map +1 -1
  89. package/dist/fn.d.ts +5 -0
  90. package/dist/fn.d.ts.map +1 -1
  91. package/dist/fn.js +13 -3
  92. package/dist/fn.js.map +1 -1
  93. package/dist/fs.d.ts.map +1 -1
  94. package/dist/fs.js +4 -2
  95. package/dist/fs.js.map +1 -1
  96. package/dist/index.d.ts +13 -15
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +11 -12
  99. package/dist/index.js.map +1 -1
  100. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  101. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  102. package/dist/internal/cdn/cloudflare.js +94 -0
  103. package/dist/internal/cdn/cloudflare.js.map +1 -0
  104. package/dist/internal/cdn/core.d.ts +91 -0
  105. package/dist/internal/cdn/core.d.ts.map +1 -0
  106. package/dist/internal/cdn/core.js +220 -0
  107. package/dist/internal/cdn/core.js.map +1 -0
  108. package/dist/internal/cdn/fastly.d.ts +17 -0
  109. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  110. package/dist/internal/cdn/fastly.js +43 -0
  111. package/dist/internal/cdn/fastly.js.map +1 -0
  112. package/dist/internal/cdn/vercel.d.ts +31 -0
  113. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  114. package/dist/internal/cdn/vercel.js +56 -0
  115. package/dist/internal/cdn/vercel.js.map +1 -0
  116. package/dist/internal/code-unit-order.d.ts +4 -0
  117. package/dist/internal/code-unit-order.d.ts.map +1 -0
  118. package/dist/internal/code-unit-order.js +6 -0
  119. package/dist/internal/code-unit-order.js.map +1 -0
  120. package/dist/internal/codegen.d.ts.map +1 -1
  121. package/dist/internal/codegen.js +141 -25
  122. package/dist/internal/codegen.js.map +1 -1
  123. package/dist/internal/dev-csp.d.ts +24 -0
  124. package/dist/internal/dev-csp.d.ts.map +1 -0
  125. package/dist/internal/dev-csp.js +160 -0
  126. package/dist/internal/dev-csp.js.map +1 -0
  127. package/dist/internal/dev-reserved.d.ts +25 -0
  128. package/dist/internal/dev-reserved.d.ts.map +1 -0
  129. package/dist/internal/dev-reserved.js +39 -0
  130. package/dist/internal/dev-reserved.js.map +1 -0
  131. package/dist/internal/html-spans.d.ts +10 -0
  132. package/dist/internal/html-spans.d.ts.map +1 -0
  133. package/dist/internal/html-spans.js +46 -0
  134. package/dist/internal/html-spans.js.map +1 -0
  135. package/dist/internal/loading-runtime.d.ts +44 -0
  136. package/dist/internal/loading-runtime.d.ts.map +1 -0
  137. package/dist/internal/loading-runtime.js +206 -0
  138. package/dist/internal/loading-runtime.js.map +1 -0
  139. package/dist/internal/matches-runtime.d.ts +8 -0
  140. package/dist/internal/matches-runtime.d.ts.map +1 -0
  141. package/dist/internal/matches-runtime.js +61 -0
  142. package/dist/internal/matches-runtime.js.map +1 -0
  143. package/dist/internal/mount-shadow.d.ts +36 -0
  144. package/dist/internal/mount-shadow.d.ts.map +1 -0
  145. package/dist/internal/mount-shadow.js +65 -0
  146. package/dist/internal/mount-shadow.js.map +1 -0
  147. package/dist/internal/node-builtins.d.ts +3 -0
  148. package/dist/internal/node-builtins.d.ts.map +1 -0
  149. package/dist/internal/node-builtins.js +79 -0
  150. package/dist/internal/node-builtins.js.map +1 -0
  151. package/dist/internal/output-guard.d.ts +65 -0
  152. package/dist/internal/output-guard.d.ts.map +1 -0
  153. package/dist/internal/output-guard.js +630 -0
  154. package/dist/internal/output-guard.js.map +1 -0
  155. package/dist/internal/page-execution.d.ts +20 -8
  156. package/dist/internal/page-execution.d.ts.map +1 -1
  157. package/dist/internal/page-execution.js +571 -105
  158. package/dist/internal/page-execution.js.map +1 -1
  159. package/dist/internal/parity.d.ts +21 -0
  160. package/dist/internal/parity.d.ts.map +1 -1
  161. package/dist/internal/parity.js +104 -22
  162. package/dist/internal/parity.js.map +1 -1
  163. package/dist/internal/private-env.d.ts +12 -0
  164. package/dist/internal/private-env.d.ts.map +1 -0
  165. package/dist/internal/private-env.js +335 -0
  166. package/dist/internal/private-env.js.map +1 -0
  167. package/dist/internal/public-url.d.ts +3 -0
  168. package/dist/internal/public-url.d.ts.map +1 -0
  169. package/dist/internal/public-url.js +18 -0
  170. package/dist/internal/public-url.js.map +1 -0
  171. package/dist/internal/render-document.d.ts +25 -19
  172. package/dist/internal/render-document.d.ts.map +1 -1
  173. package/dist/internal/render-document.js +77 -61
  174. package/dist/internal/render-document.js.map +1 -1
  175. package/dist/internal/response-controls.d.ts +75 -0
  176. package/dist/internal/response-controls.d.ts.map +1 -0
  177. package/dist/internal/response-controls.js +256 -0
  178. package/dist/internal/response-controls.js.map +1 -0
  179. package/dist/internal/runtime-contract.d.ts +11 -16
  180. package/dist/internal/runtime-contract.d.ts.map +1 -1
  181. package/dist/internal/runtime-contract.js.map +1 -1
  182. package/dist/internal/secret-scan.d.ts +104 -0
  183. package/dist/internal/secret-scan.d.ts.map +1 -0
  184. package/dist/internal/secret-scan.js +485 -0
  185. package/dist/internal/secret-scan.js.map +1 -0
  186. package/dist/internal/server-boundary.d.ts +6 -29
  187. package/dist/internal/server-boundary.d.ts.map +1 -1
  188. package/dist/internal/server-boundary.js +6 -66
  189. package/dist/internal/server-boundary.js.map +1 -1
  190. package/dist/internal/source-map.d.ts +47 -0
  191. package/dist/internal/source-map.d.ts.map +1 -0
  192. package/dist/internal/source-map.js +225 -0
  193. package/dist/internal/source-map.js.map +1 -0
  194. package/dist/internal/target-compat.d.ts +27 -0
  195. package/dist/internal/target-compat.d.ts.map +1 -0
  196. package/dist/internal/target-compat.js +77 -0
  197. package/dist/internal/target-compat.js.map +1 -0
  198. package/dist/internal/timing-safe-equal.d.ts +6 -0
  199. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  200. package/dist/internal/timing-safe-equal.js +10 -0
  201. package/dist/internal/timing-safe-equal.js.map +1 -1
  202. package/dist/internal/vitals-runtime.d.ts +18 -0
  203. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  204. package/dist/internal/vitals-runtime.js +38 -0
  205. package/dist/internal/vitals-runtime.js.map +1 -0
  206. package/dist/internal/web-app.d.ts +38 -9
  207. package/dist/internal/web-app.d.ts.map +1 -1
  208. package/dist/internal/web-app.js +199 -59
  209. package/dist/internal/web-app.js.map +1 -1
  210. package/dist/internal/zone-graph.d.ts +89 -0
  211. package/dist/internal/zone-graph.d.ts.map +1 -0
  212. package/dist/internal/zone-graph.js +492 -0
  213. package/dist/internal/zone-graph.js.map +1 -0
  214. package/dist/isr.d.ts +83 -6
  215. package/dist/isr.d.ts.map +1 -1
  216. package/dist/isr.js +263 -36
  217. package/dist/isr.js.map +1 -1
  218. package/dist/llms-txt.d.ts.map +1 -1
  219. package/dist/llms-txt.js +31 -6
  220. package/dist/llms-txt.js.map +1 -1
  221. package/dist/manifest.d.ts +220 -13
  222. package/dist/manifest.d.ts.map +1 -1
  223. package/dist/manifest.js +386 -9
  224. package/dist/manifest.js.map +1 -1
  225. package/dist/module-graph.d.ts +26 -5
  226. package/dist/module-graph.d.ts.map +1 -1
  227. package/dist/module-graph.js +17 -7
  228. package/dist/module-graph.js.map +1 -1
  229. package/dist/navigation.d.ts +17 -3
  230. package/dist/navigation.d.ts.map +1 -1
  231. package/dist/navigation.js +11 -5
  232. package/dist/navigation.js.map +1 -1
  233. package/dist/nonce.d.ts +47 -0
  234. package/dist/nonce.d.ts.map +1 -0
  235. package/dist/nonce.js +63 -0
  236. package/dist/nonce.js.map +1 -0
  237. package/dist/plugins/css-modules.d.ts.map +1 -1
  238. package/dist/plugins/css-modules.js +4 -3
  239. package/dist/plugins/css-modules.js.map +1 -1
  240. package/dist/plugins/kit.d.ts +9 -0
  241. package/dist/plugins/kit.d.ts.map +1 -1
  242. package/dist/plugins/kit.js +18 -0
  243. package/dist/plugins/kit.js.map +1 -1
  244. package/dist/plugins/svg.d.ts +22 -3
  245. package/dist/plugins/svg.d.ts.map +1 -1
  246. package/dist/plugins/svg.js +112 -1
  247. package/dist/plugins/svg.js.map +1 -1
  248. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  249. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  250. package/dist/plugins/vite-leak-guard.js +378 -17
  251. package/dist/plugins/vite-leak-guard.js.map +1 -1
  252. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  253. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  254. package/dist/plugins/vite-zone-guard.js +208 -0
  255. package/dist/plugins/vite-zone-guard.js.map +1 -0
  256. package/dist/plugins/zone-guard.d.ts +27 -0
  257. package/dist/plugins/zone-guard.d.ts.map +1 -0
  258. package/dist/plugins/zone-guard.js +72 -0
  259. package/dist/plugins/zone-guard.js.map +1 -0
  260. package/dist/prerender.d.ts +10 -0
  261. package/dist/prerender.d.ts.map +1 -1
  262. package/dist/prerender.js +35 -3
  263. package/dist/prerender.js.map +1 -1
  264. package/dist/public-dir.d.ts +9 -0
  265. package/dist/public-dir.d.ts.map +1 -1
  266. package/dist/public-dir.js +67 -9
  267. package/dist/public-dir.js.map +1 -1
  268. package/dist/pwa-manifest.d.ts +68 -0
  269. package/dist/pwa-manifest.d.ts.map +1 -0
  270. package/dist/pwa-manifest.js +178 -0
  271. package/dist/pwa-manifest.js.map +1 -0
  272. package/dist/query.js +4 -4
  273. package/dist/query.js.map +1 -1
  274. package/dist/render-seam.d.ts +42 -2
  275. package/dist/render-seam.d.ts.map +1 -1
  276. package/dist/render-seam.js +6 -0
  277. package/dist/render-seam.js.map +1 -1
  278. package/dist/route-manifest.d.ts +3 -0
  279. package/dist/route-manifest.d.ts.map +1 -1
  280. package/dist/route-manifest.js +26 -3
  281. package/dist/route-manifest.js.map +1 -1
  282. package/dist/route-types.d.ts +24 -0
  283. package/dist/route-types.d.ts.map +1 -0
  284. package/dist/route-types.js +195 -0
  285. package/dist/route-types.js.map +1 -0
  286. package/dist/router.d.ts +28 -6
  287. package/dist/router.d.ts.map +1 -1
  288. package/dist/router.js +129 -40
  289. package/dist/router.js.map +1 -1
  290. package/dist/vitals.d.ts +33 -0
  291. package/dist/vitals.d.ts.map +1 -0
  292. package/dist/vitals.js +16 -0
  293. package/dist/vitals.js.map +1 -0
  294. package/dist/vite.d.ts +16 -1
  295. package/dist/vite.d.ts.map +1 -1
  296. package/dist/vite.js +187 -36
  297. package/dist/vite.js.map +1 -1
  298. package/dist/zones.d.ts +70 -0
  299. package/dist/zones.d.ts.map +1 -0
  300. package/dist/zones.js +365 -0
  301. package/dist/zones.js.map +1 -0
  302. package/package.json +67 -14
  303. package/src/backend-only.ts +16 -0
  304. package/src/boundary.ts +18 -5
  305. package/src/build-plan.ts +90 -35
  306. package/src/build-vite.ts +77 -11
  307. package/src/build.ts +466 -96
  308. package/src/bun-dev-entry.ts +1 -1
  309. package/src/cdn.ts +216 -0
  310. package/src/client.ts +111 -18
  311. package/src/conformance.ts +16 -3
  312. package/src/csp.ts +194 -0
  313. package/src/dev-client.ts +349 -0
  314. package/src/dev-diagnostics.ts +60 -7
  315. package/src/dev-error.ts +49 -5
  316. package/src/dev-feed.ts +1140 -0
  317. package/src/dev-indicator.ts +113 -0
  318. package/src/dev-session.ts +729 -0
  319. package/src/dev-sourcemap.ts +281 -0
  320. package/src/dev-ssr-graph.ts +16 -5
  321. package/src/dev.ts +86 -43
  322. package/src/diagnostic-catalog.ts +243 -0
  323. package/src/diagnostic-prompt.ts +355 -0
  324. package/src/diagnostic.ts +63 -66
  325. package/src/draft.ts +4 -1
  326. package/src/fn.ts +19 -3
  327. package/src/fs.ts +13 -2
  328. package/src/index.ts +42 -10
  329. package/src/internal/cdn/cloudflare.ts +146 -0
  330. package/src/internal/cdn/core.ts +316 -0
  331. package/src/internal/cdn/fastly.ts +62 -0
  332. package/src/internal/cdn/vercel.ts +95 -0
  333. package/src/internal/code-unit-order.ts +5 -0
  334. package/src/internal/codegen.ts +161 -24
  335. package/src/internal/dev-csp.ts +176 -0
  336. package/src/internal/dev-reserved.ts +52 -0
  337. package/src/internal/html-spans.ts +50 -0
  338. package/src/internal/loading-runtime.ts +244 -0
  339. package/src/internal/matches-runtime.ts +62 -0
  340. package/src/internal/mount-shadow.ts +88 -0
  341. package/src/internal/node-builtins.ts +79 -0
  342. package/src/internal/output-guard.ts +701 -0
  343. package/src/internal/page-execution.ts +795 -185
  344. package/src/internal/parity.ts +150 -36
  345. package/src/internal/private-env.ts +334 -0
  346. package/src/internal/public-url.ts +22 -0
  347. package/src/internal/render-document.ts +114 -67
  348. package/src/internal/response-controls.ts +284 -0
  349. package/src/internal/runtime-contract.ts +10 -15
  350. package/src/internal/secret-scan.ts +623 -0
  351. package/src/internal/server-boundary.ts +6 -69
  352. package/src/internal/source-map.ts +256 -0
  353. package/src/internal/target-compat.ts +92 -0
  354. package/src/internal/timing-safe-equal.ts +13 -0
  355. package/src/internal/vitals-runtime.ts +53 -0
  356. package/src/internal/web-app.ts +262 -84
  357. package/src/internal/zone-graph.ts +593 -0
  358. package/src/isr.ts +325 -37
  359. package/src/llms-txt.ts +28 -4
  360. package/src/manifest.ts +670 -16
  361. package/src/module-graph.ts +53 -9
  362. package/src/navigation.ts +29 -6
  363. package/src/nonce.ts +104 -0
  364. package/src/plugins/css-modules.ts +5 -3
  365. package/src/plugins/kit.ts +24 -0
  366. package/src/plugins/svg.ts +135 -4
  367. package/src/plugins/vite-leak-guard.ts +529 -16
  368. package/src/plugins/vite-zone-guard.ts +263 -0
  369. package/src/plugins/zone-guard.ts +110 -0
  370. package/src/prerender.ts +49 -3
  371. package/src/public-dir.ts +69 -8
  372. package/src/pwa-manifest.ts +226 -0
  373. package/src/query.ts +4 -4
  374. package/src/render-seam.ts +46 -1
  375. package/src/route-manifest.ts +15 -3
  376. package/src/route-types.ts +264 -0
  377. package/src/router.ts +163 -63
  378. package/src/vitals.ts +44 -0
  379. package/src/vite.ts +207 -36
  380. package/src/zones.ts +430 -0
  381. package/dist/plugins/vite-server-only.d.ts +0 -49
  382. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  383. package/dist/plugins/vite-server-only.js +0 -57
  384. package/dist/plugins/vite-server-only.js.map +0 -1
  385. package/dist/server-only.d.ts +0 -21
  386. package/dist/server-only.d.ts.map +0 -1
  387. package/dist/server-only.js +0 -25
  388. package/dist/server-only.js.map +0 -1
  389. package/src/plugins/vite-server-only.ts +0 -75
  390. package/src/server-only.ts +0 -25
@@ -0,0 +1,226 @@
1
+ /**
2
+ * `@nifrajs/web/pwa-manifest` - a Web App Manifest builder, the declarative half of PWA next to
3
+ * `@nifrajs/web/service-worker`'s generated worker. Pure + runtime-agnostic: describe the app and
4
+ * get the `manifest.json` bytes to serve (typically `app.get("/manifest.webmanifest", ...)` with
5
+ * `content-type: application/manifest+json`), plus the `<link rel="manifest">` tag for the
6
+ * document head. Follows the [Web App Manifest spec](https://www.w3.org/TR/appmanifest/) field
7
+ * names exactly, so MDN is the reference for anything not documented here.
8
+ *
9
+ * Invalid input throws loudly at build/startup time (a missing install name, a malformed icon
10
+ * size, an unknown display mode) - a manifest the browser silently ignores is worse than no
11
+ * manifest, and these are all developer mistakes, never request input.
12
+ */
13
+
14
+ export type ManifestDisplay = "fullscreen" | "standalone" | "minimal-ui" | "browser"
15
+ export type ManifestDir = "ltr" | "rtl" | "auto"
16
+ export type ManifestPurpose = "any" | "maskable" | "monochrome"
17
+
18
+ export interface ManifestIcon {
19
+ /** Icon URL, resolved relative to `start_url` by the browser when relative. */
20
+ readonly src: string
21
+ /** Space-separated sizes, e.g. `"192x192 512x512"`, or `"any"`. Default `"any"`. */
22
+ readonly sizes?: string
23
+ /** MIME type, e.g. `"image/png"`. */
24
+ readonly type?: string
25
+ /** Space-separated purposes. Default `"any"`. */
26
+ readonly purpose?: string
27
+ }
28
+
29
+ export interface ManifestScreenshot {
30
+ readonly src: string
31
+ readonly sizes?: string
32
+ readonly type?: string
33
+ }
34
+
35
+ export interface ManifestShortcut {
36
+ readonly name: string
37
+ readonly url: string
38
+ readonly short_name?: string
39
+ readonly description?: string
40
+ readonly icons?: readonly ManifestIcon[]
41
+ }
42
+
43
+ export interface PwaManifestOptions {
44
+ /** Full install name (`"Nifra News"`). One of `name` / `short_name` is required. */
45
+ readonly name?: string
46
+ /** Short install name for launchers (`"News"`). One of `name` / `short_name` is required. */
47
+ readonly short_name?: string
48
+ readonly description?: string
49
+ /** Navigation scope root. Default `"/"`. */
50
+ readonly start_url?: string
51
+ /** URL scope the manifest (and its service worker) controls. Default `start_url`'s directory. */
52
+ readonly scope?: string
53
+ /** Preferred display mode. Default `"standalone"` (the installable PWA shape). */
54
+ readonly display?: ManifestDisplay
55
+ readonly orientation?: string
56
+ readonly background_color?: string
57
+ readonly theme_color?: string
58
+ readonly lang?: string
59
+ readonly dir?: ManifestDir
60
+ /** Opaque id distinguishing this install (defaults to `start_url` per spec). */
61
+ readonly id?: string
62
+ readonly categories?: readonly string[]
63
+ readonly icons?: readonly ManifestIcon[]
64
+ readonly screenshots?: readonly ManifestScreenshot[]
65
+ readonly shortcuts?: readonly ManifestShortcut[]
66
+ }
67
+
68
+ const SIZE_LIST = /^(?:any|\d+x\d+)(?:\s+(?:any|\d+x\d+))*$/
69
+ const PURPOSE_TOKENS: ReadonlySet<string> = new Set(["any", "maskable", "monochrome"])
70
+
71
+ /** The start URL's directory, origin-preserving for absolute URLs. Query strings and hashes
72
+ * never belong to a scope - it is a URL prefix the browser matches against. */
73
+ function defaultScope(start_url: string): string {
74
+ if (start_url.startsWith("/")) {
75
+ const bare = start_url.split(/[?#]/)[0] as string
76
+ return bare.endsWith("/") ? bare : bare.slice(0, bare.lastIndexOf("/") + 1)
77
+ }
78
+ let url: URL
79
+ try {
80
+ url = new URL(start_url)
81
+ } catch {
82
+ throw new Error(`pwaManifest: start_url ${JSON.stringify(start_url)} is not a path or URL`)
83
+ }
84
+ if (url.origin === "null" || (url.protocol !== "http:" && url.protocol !== "https:")) {
85
+ throw new Error(`pwaManifest: start_url ${JSON.stringify(start_url)} is not a path or URL`)
86
+ }
87
+ const dir = url.pathname.endsWith("/")
88
+ ? url.pathname
89
+ : url.pathname.slice(0, url.pathname.lastIndexOf("/") + 1)
90
+ return `${url.origin}${dir}`
91
+ }
92
+
93
+ function purposeOf(purpose: string | undefined, what: string): string | undefined {
94
+ if (purpose === undefined) return undefined
95
+ const tokens = purpose.split(/\s+/).filter((t) => t !== "")
96
+ if (tokens.length === 0) throw new Error(`pwaManifest: ${what} purpose must not be empty`)
97
+ for (const token of tokens) {
98
+ if (!PURPOSE_TOKENS.has(token)) {
99
+ throw new Error(
100
+ `pwaManifest: ${what} purpose ${JSON.stringify(token)} must be any/maskable/monochrome`,
101
+ )
102
+ }
103
+ }
104
+ return tokens.join(" ")
105
+ }
106
+
107
+ /** Build the manifest document object (pass through `JSON.stringify` to serve it). */
108
+ export function pwaManifest(options: PwaManifestOptions): Record<string, unknown> {
109
+ const { name, short_name } = options
110
+ if (name === undefined && short_name === undefined) {
111
+ throw new Error("pwaManifest: one of name / short_name is required")
112
+ }
113
+ if (name !== undefined && name.trim() === "") {
114
+ throw new Error("pwaManifest: name must not be empty")
115
+ }
116
+ if (short_name !== undefined && short_name.trim() === "") {
117
+ throw new Error("pwaManifest: short_name must not be empty")
118
+ }
119
+ const display = options.display ?? "standalone"
120
+ if (
121
+ display !== "fullscreen" &&
122
+ display !== "minimal-ui" &&
123
+ display !== "browser" &&
124
+ display !== "standalone"
125
+ ) {
126
+ throw new Error(`pwaManifest: unknown display ${JSON.stringify(display)}`)
127
+ }
128
+ if (
129
+ options.dir !== undefined &&
130
+ options.dir !== "ltr" &&
131
+ options.dir !== "rtl" &&
132
+ options.dir !== "auto"
133
+ ) {
134
+ throw new Error(`pwaManifest: unknown dir ${JSON.stringify(options.dir)}`)
135
+ }
136
+ const start_url = options.start_url ?? "/"
137
+ if (start_url === "" || start_url.startsWith("//")) {
138
+ throw new Error("pwaManifest: start_url must be a relative path or absolute URL")
139
+ }
140
+ const manifest: Record<string, unknown> = {}
141
+ if (name !== undefined) manifest.name = name
142
+ if (short_name !== undefined) manifest.short_name = short_name
143
+ if (options.description !== undefined) manifest.description = options.description
144
+ manifest.start_url = start_url
145
+ // The default scope is the start URL's directory ("https://x.com/app/home" → "…/app/"),
146
+ // query/hash excluded; a same-origin absolute URL keeps its origin, a relative one stays
147
+ // relative. Anything unparseable is a developer mistake, and fails loud.
148
+ manifest.scope = options.scope ?? defaultScope(start_url)
149
+ manifest.display = display
150
+ if (options.orientation !== undefined) manifest.orientation = options.orientation
151
+ if (options.background_color !== undefined) manifest.background_color = options.background_color
152
+ if (options.theme_color !== undefined) manifest.theme_color = options.theme_color
153
+ if (options.lang !== undefined) manifest.lang = options.lang
154
+ if (options.dir !== undefined) manifest.dir = options.dir
155
+ if (options.id !== undefined) manifest.id = options.id
156
+ if (options.categories !== undefined) manifest.categories = [...options.categories]
157
+ if (options.icons !== undefined) {
158
+ manifest.icons = options.icons.map((icon, i) => {
159
+ if (icon.src === "") throw new Error(`pwaManifest: icons[${i}].src must not be empty`)
160
+ const sizes = icon.sizes ?? "any"
161
+ if (!SIZE_LIST.test(sizes)) {
162
+ throw new Error(
163
+ `pwaManifest: icons[${i}].sizes ${JSON.stringify(icon.sizes)} must be "any" or space-separated WxH`,
164
+ )
165
+ }
166
+ const entry: Record<string, unknown> = { src: icon.src, sizes }
167
+ if (icon.type !== undefined) entry.type = icon.type
168
+ const purpose = purposeOf(icon.purpose, `icons[${i}]`)
169
+ if (purpose !== undefined) entry.purpose = purpose
170
+ return entry
171
+ })
172
+ }
173
+ if (options.screenshots !== undefined) {
174
+ manifest.screenshots = options.screenshots.map((shot, i) => {
175
+ if (shot.src === "") throw new Error(`pwaManifest: screenshots[${i}].src must not be empty`)
176
+ const entry: Record<string, unknown> = { src: shot.src }
177
+ if (shot.sizes !== undefined) {
178
+ if (!SIZE_LIST.test(shot.sizes)) {
179
+ throw new Error(
180
+ `pwaManifest: screenshots[${i}].sizes ${JSON.stringify(shot.sizes)} must be "any" or space-separated WxH`,
181
+ )
182
+ }
183
+ entry.sizes = shot.sizes
184
+ }
185
+ if (shot.type !== undefined) entry.type = shot.type
186
+ return entry
187
+ })
188
+ }
189
+ if (options.shortcuts !== undefined) {
190
+ manifest.shortcuts = options.shortcuts.map((cut, i) => {
191
+ if (cut.name === "") throw new Error(`pwaManifest: shortcuts[${i}].name must not be empty`)
192
+ if (cut.url === "") throw new Error(`pwaManifest: shortcuts[${i}].url must not be empty`)
193
+ const entry: Record<string, unknown> = { name: cut.name, url: cut.url }
194
+ if (cut.short_name !== undefined) entry.short_name = cut.short_name
195
+ if (cut.description !== undefined) entry.description = cut.description
196
+ if (cut.icons !== undefined) {
197
+ entry.icons = cut.icons.map((icon, j) => {
198
+ if (icon.src === "") {
199
+ throw new Error(`pwaManifest: shortcuts[${i}].icons[${j}].src must not be empty`)
200
+ }
201
+ const sizes = icon.sizes ?? "any"
202
+ if (!SIZE_LIST.test(sizes)) {
203
+ throw new Error(
204
+ `pwaManifest: shortcuts[${i}].icons[${j}].sizes must be "any" or space-separated WxH`,
205
+ )
206
+ }
207
+ return { src: icon.src, sizes }
208
+ })
209
+ }
210
+ return entry
211
+ })
212
+ }
213
+ return manifest
214
+ }
215
+
216
+ /** Serialize a manifest document to servable JSON (stable key order, no whitespace). */
217
+ export function serializeManifest(manifest: Record<string, unknown>): string {
218
+ return JSON.stringify(manifest)
219
+ }
220
+
221
+ /** The `<link rel="manifest">` tag for the document head. */
222
+ export function manifestLink(href = "/manifest.webmanifest"): string {
223
+ if (href === "") throw new Error("manifestLink: href must not be empty")
224
+ const quoted = href.replace(/"/g, "&quot;")
225
+ return `<link rel="manifest" href="${quoted}">`
226
+ }
package/src/query.ts CHANGED
@@ -642,7 +642,7 @@ export function createMutation<TData, TVariables>(
642
642
  let mutationFn = fn
643
643
  let cbs = callbacks
644
644
  let state = IDLE_MUTATION as MutationState<TData, TVariables>
645
- let token = 0
645
+ let latest = 0
646
646
  const listeners = new Set<() => void>()
647
647
  const emit = (): void => {
648
648
  for (const l of listeners) l()
@@ -665,19 +665,19 @@ export function createMutation<TData, TVariables>(
665
665
  },
666
666
  reset: () => setState(IDLE_MUTATION as MutationState<TData, TVariables>),
667
667
  mutate: async (variables) => {
668
- const mine = ++token
668
+ const mine = ++latest
669
669
  setState({ status: "pending", data: undefined, error: undefined, variables })
670
670
  // Only await a callback when one is actually supplied - an `await undefined` would insert a
671
671
  // needless microtask before the mutationFn runs (and complicates single-flight timing).
672
672
  if (cbs.onMutate !== undefined) await cbs.onMutate(variables)
673
673
  try {
674
674
  const data = await mutationFn(variables)
675
- if (mine === token) setState({ status: "success", data, error: undefined, variables })
675
+ if (mine === latest) setState({ status: "success", data, error: undefined, variables })
676
676
  if (cbs.onSuccess !== undefined) await cbs.onSuccess(data, variables)
677
677
  if (cbs.onSettled !== undefined) await cbs.onSettled(data, undefined, variables)
678
678
  return data
679
679
  } catch (error) {
680
- if (mine === token) setState({ status: "error", data: undefined, error, variables })
680
+ if (mine === latest) setState({ status: "error", data: undefined, error, variables })
681
681
  if (cbs.onError !== undefined) await cbs.onError(error, variables)
682
682
  if (cbs.onSettled !== undefined) await cbs.onSettled(undefined, error, variables)
683
683
  throw error
@@ -30,6 +30,41 @@ export interface RenderProps {
30
30
  readonly search?: Record<string, unknown>
31
31
  /** Neutral named-boundary states; adapters choose how each boundary's `render` UI is mounted. */
32
32
  readonly boundaries?: BoundaryStates
33
+ /** The rendered chain's module ids and `handle` exports, for the adapter's `useMatches`. */
34
+ readonly matchChain?: MatchChain
35
+ }
36
+
37
+ /**
38
+ * The modules a render stacks, as `useMatches` needs them: each one's id and `handle` export,
39
+ * outermost layout first and the page (or status page) last. Built by the server for SSR and by the
40
+ * generated client entry per route, from the same manifest, so both sides report the same matches.
41
+ */
42
+ export interface MatchChain {
43
+ /** Layout ids (`orgs/[org]/_layout`), then the page's route id. */
44
+ readonly ids: readonly string[]
45
+ /** Each module's `handle` export, index-aligned with {@link ids}. */
46
+ readonly handles: readonly unknown[]
47
+ }
48
+
49
+ /** One module of the rendered chain, as `useMatches` returns it. */
50
+ export interface UIMatch {
51
+ /** The module's id: a layout's (`orgs/[org]/_layout`), or the page's route id last. */
52
+ readonly id: string
53
+ /** The part of the URL path this module wraps: a layout's directory prefix, the whole path for the
54
+ * page. Raw, as it appears in the URL. */
55
+ readonly pathname: string
56
+ /** The params this module's loader receives: a layout's own, every route param for the page. */
57
+ readonly params: Readonly<Record<string, string>>
58
+ /** This module's loader data: `null` for a layout without a loader. */
59
+ readonly data: unknown
60
+ /** The module's `handle` export - any value, commonly a breadcrumb label or a render function. */
61
+ readonly handle: unknown
62
+ }
63
+
64
+ /** Per-document options for {@link RenderAdapter.renderToStream}. */
65
+ export interface RenderStreamOptions {
66
+ /** The document's CSP nonce, for every executable `<script>` the framework streams. */
67
+ readonly nonce?: string
33
68
  }
34
69
 
35
70
  /**
@@ -39,11 +74,14 @@ export interface RenderProps {
39
74
  export interface RenderAdapter {
40
75
  /**
41
76
  * Render a route's layout `chain` (outermost layout → page) to a Web stream of HTML bytes,
42
- * including the framework's hydration markers.
77
+ * including the framework's hydration markers. When `options.nonce` is set, every executable
78
+ * `<script>` the framework streams (out-of-order Suspense runtimes, serialized resources) must
79
+ * carry it, or a nonce-based Content-Security-Policy blocks it and the boundary never resolves.
43
80
  */
44
81
  renderToStream(
45
82
  chain: readonly unknown[],
46
83
  props: RenderProps,
84
+ options?: RenderStreamOptions,
47
85
  ): ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>
48
86
  /** Render the chain to a complete HTML string when no content is deferred. */
49
87
  renderToString?(chain: readonly unknown[], props: RenderProps): string | Promise<string>
@@ -67,6 +105,13 @@ export function ssrModuleLoader(): SsrModuleLoader | undefined {
67
105
  return loaderSlot[SSR_MODULE_LOADER_SLOT]
68
106
  }
69
107
 
108
+ /**
109
+ * `id` of the inert `<script type="application/json">` a hydrating document hands its page state over
110
+ * in: one JSON object keyed by the `*_GLOBAL` names below, which the client entry assigns onto
111
+ * `window` before anything reads them. Data the browser never executes needs no CSP nonce or hash.
112
+ */
113
+ export const HANDOVER_ID = "__nifra-handover"
114
+
70
115
  /** Global the server serializes loader data into; the client reads it to hydrate. */
71
116
  export const DATA_GLOBAL = "__NIFRA_DATA__"
72
117
  /** Per-layout loader data for hydration. */
@@ -16,8 +16,19 @@
16
16
  * target, and reports what the target cannot honour. Pure and fs-free: it takes an already-built
17
17
  * `Manifest` so it runs at build time, in `nifra routes`, and in a test with a hand-built manifest.
18
18
  */
19
+
20
+ import { codeUnitOrder } from "./internal/code-unit-order.ts"
19
21
  import type { Manifest, RouteModule } from "./manifest.ts"
20
22
 
23
+ export {
24
+ formatShadowedPages,
25
+ normalizeMountPath,
26
+ type ShadowedPage,
27
+ shadowedPages,
28
+ } from "./internal/mount-shadow.ts"
29
+ /** Which half of a route each export belongs in: the frontend file or its `x.backend.ts`. */
30
+ export { BACKEND_ROUTE_EXPORTS, FRONTEND_ROUTE_EXPORTS } from "./manifest.ts"
31
+
21
32
  /**
22
33
  * How a route produces its HTML.
23
34
  *
@@ -115,7 +126,8 @@ export function deriveRouteEntry(
115
126
  requires: readonly RouteCapability[]
116
127
  } = { id, pattern, mode, hydrate: module.hydrate !== false, requires }
117
128
  if (mode === "isr" && module.revalidate !== undefined) entry.revalidate = module.revalidate
118
- if (mode === "isr" && module.revalidateTags !== undefined) {
129
+ // A function's tags exist per request; the manifest records only a fixed list.
130
+ if (mode === "isr" && Array.isArray(module.revalidateTags)) {
119
131
  entry.revalidateTags = module.revalidateTags
120
132
  }
121
133
  if (prerenderedPaths !== undefined) entry.prerenderedPaths = prerenderedPaths
@@ -134,7 +146,7 @@ const TARGET_CAPABILITIES: Readonly<Record<string, readonly RouteCapability[]>>
134
146
  bun: ["server"],
135
147
  node: ["server"],
136
148
  deno: ["server"],
137
- "cf-pages": ["server"],
149
+ cloudflare: ["server"],
138
150
  vercel: ["server"],
139
151
  static: [],
140
152
  }
@@ -169,7 +181,7 @@ export async function buildRouteManifest(
169
181
  const module = (await route.load()) as RouteModule
170
182
  entries.push(deriveRouteEntry(route.id, route.pattern, module, options.prerendered?.[route.id]))
171
183
  }
172
- entries.sort((a, b) => a.pattern.localeCompare(b.pattern))
184
+ entries.sort((a, b) => codeUnitOrder(a.pattern, b.pattern))
173
185
 
174
186
  const conflicts: RouteManifestConflict[] = []
175
187
  const capabilities =
@@ -0,0 +1,264 @@
1
+ /**
2
+ * `@nifrajs/web/route-types` - the generated `./+types/<route>` modules.
3
+ *
4
+ * Every route file gets `.nifra/types/<routes>/<dir>/+types/<name>.d.ts`, declaring a `Route`
5
+ * namespace: the route's `Params`, the `LoaderData` and `ActionData` its output schemas let through,
6
+ * the `LoaderArgs` its backend half receives, and its component's props. With
7
+ * `"rootDirs": [".", "./.nifra/types"]` in tsconfig, both halves of a route import it as
8
+ * `./+types/<name>`:
9
+ *
10
+ * // routes/blog/[slug].tsx
11
+ * import type { Route } from "./+types/[slug]"
12
+ * export default function Post({ data }: Route.ComponentProps) { ... }
13
+ *
14
+ * // routes/blog/[slug].backend.ts
15
+ * export async function loader({ api, params }: Route.LoaderArgs) { ... }
16
+ *
17
+ * `LoaderData` is the output type of `loaderOutput`, not the loader's return type: the data guard
18
+ * projects the loader's value through that schema, so the schema is what the component receives.
19
+ * `.nifra/types/register.d.ts` registers `backend/app.ts`'s `backend` with `@nifrajs/client`, so `api`
20
+ * is typed without a route importing the backend.
21
+ */
22
+ import {
23
+ type Dirent,
24
+ existsSync,
25
+ mkdirSync,
26
+ readdirSync,
27
+ readFileSync,
28
+ rmSync,
29
+ writeFileSync,
30
+ } from "node:fs"
31
+ import { dirname, join, relative } from "node:path"
32
+ import { codeUnitOrder } from "./internal/code-unit-order.ts"
33
+ import {
34
+ buildManifest,
35
+ isMiddlewareFile,
36
+ isRouteBackendFile,
37
+ type LayoutEntry,
38
+ type Manifest,
39
+ } from "./manifest.ts"
40
+
41
+ /** Where the generated types live, relative to the app root. */
42
+ export const ROUTE_TYPES_DIR = ".nifra/types"
43
+
44
+ const BACKEND_APP = "backend/app"
45
+ const SCRIPT_EXTS = [".ts", ".tsx", ".js", ".jsx"] as const
46
+
47
+ export interface RouteTypesOptions {
48
+ /** The app root: where `.nifra/` and `backend/app.ts` live. */
49
+ readonly appRoot: string
50
+ /** The routes directory. Defaults to `<appRoot>/routes`. */
51
+ readonly routesDir?: string
52
+ }
53
+
54
+ type Kind = "page" | "layout"
55
+
56
+ interface RouteFile {
57
+ readonly file: string
58
+ readonly backend: string | undefined
59
+ readonly kind: Kind
60
+ }
61
+
62
+ const posix = (path: string): string => path.replaceAll("\\", "/")
63
+
64
+ /** Every route file the router serves, once each: pages, layouts and the special pages. */
65
+ function routeFiles(manifest: Manifest): readonly RouteFile[] {
66
+ const files = new Map<string, RouteFile>()
67
+ const add = (entry: Pick<LayoutEntry, "file" | "backend">, kind: Kind): void => {
68
+ if (!files.has(entry.file))
69
+ files.set(entry.file, { file: entry.file, backend: entry.backend, kind })
70
+ }
71
+ for (const route of manifest.routes) add(route, "page")
72
+ for (const layout of Object.values(manifest.layouts)) add(layout, "layout")
73
+ for (const group of [
74
+ manifest.errors,
75
+ manifest.notFounds,
76
+ manifest.statusPages,
77
+ manifest.loadings,
78
+ ]) {
79
+ for (const entry of Object.values(group ?? {})) add(entry, "page")
80
+ }
81
+ if (manifest.notFound !== undefined) add(manifest.notFound, "page")
82
+ return [...files.values()].sort((a, b) => codeUnitOrder(a.file, b.file))
83
+ }
84
+
85
+ const PARAM = /^\[([A-Za-z_][A-Za-z0-9_]*)\]$/
86
+ const OPTIONAL = /^\[\[([A-Za-z_][A-Za-z0-9_]*)\]\]$/
87
+ const CATCH_ALL = /^\[\.\.\.([A-Za-z_][A-Za-z0-9_]*)\]$/
88
+
89
+ /** The params a route file's URL carries: its directories', plus its own name's for a page. */
90
+ function paramsType(file: string, kind: Kind): string {
91
+ const segments = file.replace(/\.[^./]+$/, "").split("/")
92
+ const own =
93
+ kind === "page" && !segments.at(-1)?.startsWith("_") ? segments : segments.slice(0, -1)
94
+ const fields: string[] = []
95
+ for (const segment of own) {
96
+ const optional = OPTIONAL.exec(segment)
97
+ const name = optional?.[1] ?? PARAM.exec(segment)?.[1] ?? CATCH_ALL.exec(segment)?.[1]
98
+ if (name !== undefined) fields.push(`readonly ${name}${optional === null ? "" : "?"}: string`)
99
+ }
100
+ return fields.length === 0 ? "Record<string, never>" : `{ ${fields.join("; ")} }`
101
+ }
102
+
103
+ /** A module specifier for `target` from a file in `fromDir`, without a script extension. */
104
+ function specifier(fromDir: string, target: string): string {
105
+ let path = posix(relative(fromDir, target))
106
+ for (const ext of SCRIPT_EXTS) if (path.endsWith(ext)) path = path.slice(0, -ext.length)
107
+ return path.startsWith(".") ? path : `./${path}`
108
+ }
109
+
110
+ const backendAppOf = (appRoot: string): string | undefined =>
111
+ SCRIPT_EXTS.map((ext) => join(appRoot, `${BACKEND_APP}${ext}`)).find((path) => existsSync(path))
112
+
113
+ function routeTypesModule(
114
+ route: RouteFile,
115
+ out: string,
116
+ routesDir: string,
117
+ label: string,
118
+ register: string | undefined,
119
+ ): string {
120
+ const dir = dirname(out)
121
+ const backend = route.backend === undefined ? undefined : join(routesDir, route.backend)
122
+ // Only a script module's exports are visible to TypeScript; a .svelte/.vue/.mdx one is an ambient shim.
123
+ const frontend = /\.[jt]sx?$/.test(route.file) ? join(routesDir, route.file) : undefined
124
+ const lines = [`// Generated by nifra from ${label}. Do not edit: \`nifra types\` rewrites it.`]
125
+ if (register !== undefined)
126
+ lines.push(`/// <reference path="${posix(relative(dir, register))}" />`)
127
+ lines.push('import type { OutputOf, RouteLoaderArgs, SearchSchemaOf } from "@nifrajs/client"')
128
+ if (route.kind === "page") lines.push('import type { RenderProps } from "@nifrajs/web"')
129
+ lines.push("")
130
+ if (backend !== undefined)
131
+ lines.push(`type Backend = typeof import("${specifier(dir, backend)}")`)
132
+ if (frontend !== undefined)
133
+ lines.push(`type Frontend = typeof import("${specifier(dir, frontend)}")`)
134
+ if (backend !== undefined || frontend !== undefined) lines.push("")
135
+ const output = (name: string) => (backend === undefined ? "null" : `OutputOf<Backend, "${name}">`)
136
+ lines.push(
137
+ "export namespace Route {",
138
+ " /** The URL params this route's path carries. */",
139
+ ` export type Params = ${paramsType(route.file, route.kind)}`,
140
+ " /** What the loader sends to the browser: the output type of `loaderOutput`. */",
141
+ ` export type LoaderData = ${output("loaderOutput")}`,
142
+ " /** What the action sends back: the output type of `actionOutput`. */",
143
+ ` export type ActionData = ${output("actionOutput")}`,
144
+ " /** The context the backend half's loader receives. */",
145
+ ` export type LoaderArgs = RouteLoaderArgs<Params${frontend === undefined ? "" : ", SearchSchemaOf<Frontend>"}>`,
146
+ " /** The context the backend half's action receives. */",
147
+ " export type ActionArgs = LoaderArgs",
148
+ )
149
+ if (route.kind === "page") {
150
+ lines.push(
151
+ " /** The page component's props. */",
152
+ ' export type ComponentProps = Omit<RenderProps, "data" | "actionData" | "params"> & {',
153
+ " readonly data: LoaderData",
154
+ " readonly actionData?: ActionData",
155
+ " readonly params: Params",
156
+ " }",
157
+ )
158
+ } else {
159
+ lines.push(
160
+ " /** The layout component's props; `Children` is the adapter's children type. */",
161
+ " export type ComponentProps<Children = unknown> = {",
162
+ " readonly data: LoaderData",
163
+ " readonly children?: Children",
164
+ " }",
165
+ )
166
+ }
167
+ lines.push("}", "")
168
+ return lines.join("\n")
169
+ }
170
+
171
+ function registerModule(out: string, backendApp: string, label: string): string {
172
+ return [
173
+ `// Generated by nifra from ${label}. Do not edit: \`nifra types\` rewrites it.`,
174
+ `import type { backend } from "${specifier(dirname(out), backendApp)}"`,
175
+ "",
176
+ 'declare module "@nifrajs/client" {',
177
+ " interface Register {",
178
+ " readonly backend: typeof backend",
179
+ " }",
180
+ "}",
181
+ "",
182
+ ].join("\n")
183
+ }
184
+
185
+ /** Every generated types file for the app, by absolute path. */
186
+ export function routeTypeFiles(options: RouteTypesOptions): ReadonlyMap<string, string> {
187
+ const appRoot = options.appRoot
188
+ const routesDir = options.routesDir ?? join(appRoot, "routes")
189
+ const typesRoot = join(appRoot, ROUTE_TYPES_DIR)
190
+ const files = new Map<string, string>()
191
+ const backendApp = backendAppOf(appRoot)
192
+ const register = backendApp === undefined ? undefined : join(typesRoot, "register.d.ts")
193
+ if (backendApp !== undefined && register !== undefined) {
194
+ files.set(register, registerModule(register, backendApp, posix(relative(appRoot, backendApp))))
195
+ }
196
+ if (!existsSync(routesDir)) return files
197
+ const listed = (readdirSync(routesDir, { recursive: true }) as string[])
198
+ .map(posix)
199
+ .filter(
200
+ (file) =>
201
+ /\.(?:tsx|jsx|svelte|vue|mdx)$/.test(file) ||
202
+ isRouteBackendFile(file) ||
203
+ isMiddlewareFile(file),
204
+ )
205
+ // Discovery only: the importer is never called.
206
+ const manifest = buildManifest(listed, () => () => Promise.reject(new Error("not loaded")))
207
+ const routesRel = posix(relative(appRoot, routesDir))
208
+ for (const route of routeFiles(manifest)) {
209
+ const dir = dirname(route.file)
210
+ const name = route.file.slice(dir === "." ? 0 : dir.length + 1).replace(/\.[^.]+$/, "")
211
+ const out = join(typesRoot, routesRel, dir === "." ? "" : dir, "+types", `${name}.d.ts`)
212
+ files.set(out, routeTypesModule(route, out, routesDir, `${routesRel}/${route.file}`, register))
213
+ }
214
+ return files
215
+ }
216
+
217
+ /** Generated files missing or different on disk, and files on disk the app no longer generates. */
218
+ export function staleRouteTypes(options: RouteTypesOptions): readonly string[] {
219
+ const files = routeTypeFiles(options)
220
+ const stale: string[] = []
221
+ for (const [path, content] of files) {
222
+ if (!existsSync(path) || readFileSync(path, "utf8") !== content) stale.push(path)
223
+ }
224
+ for (const path of onDisk(join(options.appRoot, ROUTE_TYPES_DIR))) {
225
+ if (!files.has(path)) stale.push(path)
226
+ }
227
+ return stale.sort()
228
+ }
229
+
230
+ function onDisk(dir: string): readonly string[] {
231
+ if (!existsSync(dir)) return []
232
+ return (readdirSync(dir, { recursive: true, withFileTypes: true }) as Dirent[])
233
+ .filter((entry) => entry.isFile())
234
+ .map((entry) => join(entry.parentPath, entry.name))
235
+ }
236
+
237
+ export interface RouteTypesResult {
238
+ /** Files written because they were missing or out of date. */
239
+ readonly written: readonly string[]
240
+ /** Files removed because no route generates them any more. */
241
+ readonly removed: readonly string[]
242
+ }
243
+
244
+ /**
245
+ * Bring `.nifra/types` up to date with the app's routes. Unchanged files are left alone, so an
246
+ * editor watching them sees only real changes. Owns the directory: anything else in it is removed.
247
+ */
248
+ export function writeRouteTypes(options: RouteTypesOptions): RouteTypesResult {
249
+ const files = routeTypeFiles(options)
250
+ const written: string[] = []
251
+ const removed: string[] = []
252
+ for (const [path, content] of files) {
253
+ if (existsSync(path) && readFileSync(path, "utf8") === content) continue
254
+ mkdirSync(dirname(path), { recursive: true })
255
+ writeFileSync(path, content)
256
+ written.push(path)
257
+ }
258
+ for (const path of onDisk(join(options.appRoot, ROUTE_TYPES_DIR))) {
259
+ if (files.has(path)) continue
260
+ rmSync(path)
261
+ removed.push(path)
262
+ }
263
+ return { written: written.sort(), removed: removed.sort() }
264
+ }