@nifrajs/web 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 (426) hide show
  1. package/README.md +19 -0
  2. package/dist/build-vite.d.ts +19 -0
  3. package/dist/build-vite.d.ts.map +1 -1
  4. package/dist/build-vite.js +31 -10
  5. package/dist/build-vite.js.map +1 -1
  6. package/dist/build.d.ts +8 -10
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +90 -31
  9. package/dist/build.js.map +1 -1
  10. package/dist/cdn.d.ts +8 -0
  11. package/dist/cdn.d.ts.map +1 -1
  12. package/dist/cdn.js +14 -15
  13. package/dist/cdn.js.map +1 -1
  14. package/dist/client.d.ts +6 -6
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +46 -18
  17. package/dist/client.js.map +1 -1
  18. package/dist/deferred.d.ts +2 -2
  19. package/dist/deferred.d.ts.map +1 -1
  20. package/dist/deferred.js +28 -7
  21. package/dist/deferred.js.map +1 -1
  22. package/dist/dev-feed.d.ts.map +1 -1
  23. package/dist/dev-feed.js +24 -5
  24. package/dist/dev-feed.js.map +1 -1
  25. package/dist/dev-port.d.ts +3 -2
  26. package/dist/dev-port.d.ts.map +1 -1
  27. package/dist/dev-port.js +2 -6
  28. package/dist/dev-port.js.map +1 -1
  29. package/dist/dev-session.d.ts.map +1 -1
  30. package/dist/dev-session.js +4 -14
  31. package/dist/dev-session.js.map +1 -1
  32. package/dist/dev.d.ts +7 -17
  33. package/dist/dev.d.ts.map +1 -1
  34. package/dist/dev.js +13 -1
  35. package/dist/dev.js.map +1 -1
  36. package/dist/draft.d.ts +8 -5
  37. package/dist/draft.d.ts.map +1 -1
  38. package/dist/draft.js +30 -13
  39. package/dist/draft.js.map +1 -1
  40. package/dist/fonts-google.d.ts.map +1 -1
  41. package/dist/fonts-google.js +7 -0
  42. package/dist/fonts-google.js.map +1 -1
  43. package/dist/index.d.ts +5 -4
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +4 -3
  46. package/dist/index.js.map +1 -1
  47. package/dist/internal/codegen.d.ts +1 -0
  48. package/dist/internal/codegen.d.ts.map +1 -1
  49. package/dist/internal/codegen.js +56 -29
  50. package/dist/internal/codegen.js.map +1 -1
  51. package/dist/internal/disposable-dir.d.ts +8 -0
  52. package/dist/internal/disposable-dir.d.ts.map +1 -0
  53. package/dist/internal/disposable-dir.js +25 -0
  54. package/dist/internal/disposable-dir.js.map +1 -0
  55. package/dist/internal/entry-chunks.d.ts +9 -0
  56. package/dist/internal/entry-chunks.d.ts.map +1 -0
  57. package/dist/internal/entry-chunks.js +15 -0
  58. package/dist/internal/entry-chunks.js.map +1 -0
  59. package/dist/internal/evidence-traversal.d.ts +12 -0
  60. package/dist/internal/evidence-traversal.d.ts.map +1 -0
  61. package/dist/internal/evidence-traversal.js +48 -0
  62. package/dist/internal/evidence-traversal.js.map +1 -0
  63. package/dist/internal/head-merge.d.ts +6 -0
  64. package/dist/internal/head-merge.d.ts.map +1 -1
  65. package/dist/internal/head-merge.js +9 -0
  66. package/dist/internal/head-merge.js.map +1 -1
  67. package/dist/internal/js-string.d.ts +2 -0
  68. package/dist/internal/js-string.d.ts.map +1 -1
  69. package/dist/internal/js-string.js +5 -1
  70. package/dist/internal/js-string.js.map +1 -1
  71. package/dist/internal/mount-shadow.js +1 -1
  72. package/dist/internal/mount-shadow.js.map +1 -1
  73. package/dist/internal/output-guard.d.ts +7 -0
  74. package/dist/internal/output-guard.d.ts.map +1 -1
  75. package/dist/internal/output-guard.js +80 -8
  76. package/dist/internal/output-guard.js.map +1 -1
  77. package/dist/internal/package-entries.d.ts +34 -0
  78. package/dist/internal/package-entries.d.ts.map +1 -0
  79. package/dist/internal/package-entries.js +324 -0
  80. package/dist/internal/package-entries.js.map +1 -0
  81. package/dist/internal/page-execution.d.ts +6 -0
  82. package/dist/internal/page-execution.d.ts.map +1 -1
  83. package/dist/internal/page-execution.js +437 -114
  84. package/dist/internal/page-execution.js.map +1 -1
  85. package/dist/internal/parity.d.ts.map +1 -1
  86. package/dist/internal/parity.js +15 -2
  87. package/dist/internal/parity.js.map +1 -1
  88. package/dist/internal/private-env.d.ts +8 -0
  89. package/dist/internal/private-env.d.ts.map +1 -1
  90. package/dist/internal/private-env.js +25 -10
  91. package/dist/internal/private-env.js.map +1 -1
  92. package/dist/internal/progressive-cookies.d.ts +14 -0
  93. package/dist/internal/progressive-cookies.d.ts.map +1 -0
  94. package/dist/internal/progressive-cookies.js +43 -0
  95. package/dist/internal/progressive-cookies.js.map +1 -0
  96. package/dist/internal/public-env.d.ts +2 -0
  97. package/dist/internal/public-env.d.ts.map +1 -0
  98. package/dist/internal/public-env.js +13 -0
  99. package/dist/internal/public-env.js.map +1 -0
  100. package/dist/internal/render-document.d.ts +5 -44
  101. package/dist/internal/render-document.d.ts.map +1 -1
  102. package/dist/internal/render-document.js +51 -63
  103. package/dist/internal/render-document.js.map +1 -1
  104. package/dist/internal/response-controls.d.ts +2 -0
  105. package/dist/internal/response-controls.d.ts.map +1 -1
  106. package/dist/internal/response-controls.js +26 -16
  107. package/dist/internal/response-controls.js.map +1 -1
  108. package/dist/internal/revive-dates.d.ts +16 -0
  109. package/dist/internal/revive-dates.d.ts.map +1 -0
  110. package/dist/internal/revive-dates.js +32 -0
  111. package/dist/internal/revive-dates.js.map +1 -0
  112. package/dist/internal/route-path.d.ts +14 -0
  113. package/dist/internal/route-path.d.ts.map +1 -0
  114. package/dist/internal/route-path.js +19 -0
  115. package/dist/internal/route-path.js.map +1 -0
  116. package/dist/internal/secret-scan.d.ts.map +1 -1
  117. package/dist/internal/secret-scan.js +190 -20
  118. package/dist/internal/secret-scan.js.map +1 -1
  119. package/dist/internal/server-boundary.d.ts.map +1 -1
  120. package/dist/internal/server-boundary.js +33 -5
  121. package/dist/internal/server-boundary.js.map +1 -1
  122. package/dist/internal/web-app.d.ts +25 -18
  123. package/dist/internal/web-app.d.ts.map +1 -1
  124. package/dist/internal/web-app.js +81 -24
  125. package/dist/internal/web-app.js.map +1 -1
  126. package/dist/internal/wire-date.d.ts +3 -0
  127. package/dist/internal/wire-date.d.ts.map +1 -0
  128. package/dist/internal/wire-date.js +14 -0
  129. package/dist/internal/wire-date.js.map +1 -0
  130. package/dist/internal/without-query.d.ts +4 -0
  131. package/dist/internal/without-query.d.ts.map +1 -0
  132. package/dist/internal/without-query.js +9 -0
  133. package/dist/internal/without-query.js.map +1 -0
  134. package/dist/internal/zone-graph.d.ts +4 -0
  135. package/dist/internal/zone-graph.d.ts.map +1 -1
  136. package/dist/internal/zone-graph.js +71 -20
  137. package/dist/internal/zone-graph.js.map +1 -1
  138. package/dist/isr.d.ts +59 -3
  139. package/dist/isr.d.ts.map +1 -1
  140. package/dist/isr.js +311 -41
  141. package/dist/isr.js.map +1 -1
  142. package/dist/llms-txt.d.ts +2 -0
  143. package/dist/llms-txt.d.ts.map +1 -1
  144. package/dist/llms-txt.js +29 -3
  145. package/dist/llms-txt.js.map +1 -1
  146. package/dist/manifest.d.ts +37 -4
  147. package/dist/manifest.d.ts.map +1 -1
  148. package/dist/manifest.js +4 -0
  149. package/dist/manifest.js.map +1 -1
  150. package/dist/open-resolved.d.ts +18 -0
  151. package/dist/open-resolved.d.ts.map +1 -0
  152. package/dist/open-resolved.js +62 -0
  153. package/dist/open-resolved.js.map +1 -0
  154. package/dist/plugins/kit.d.ts +1 -0
  155. package/dist/plugins/kit.d.ts.map +1 -1
  156. package/dist/plugins/kit.js +26 -11
  157. package/dist/plugins/kit.js.map +1 -1
  158. package/dist/plugins/vite-leak-guard.d.ts +9 -1
  159. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  160. package/dist/plugins/vite-leak-guard.js +84 -7
  161. package/dist/plugins/vite-leak-guard.js.map +1 -1
  162. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  163. package/dist/plugins/vite-zone-guard.js +17 -21
  164. package/dist/plugins/vite-zone-guard.js.map +1 -1
  165. package/dist/plugins/zone-guard.d.ts.map +1 -1
  166. package/dist/plugins/zone-guard.js +44 -3
  167. package/dist/plugins/zone-guard.js.map +1 -1
  168. package/dist/prerender.d.ts +5 -1
  169. package/dist/prerender.d.ts.map +1 -1
  170. package/dist/prerender.js +41 -19
  171. package/dist/prerender.js.map +1 -1
  172. package/dist/public-dir.d.ts +8 -1
  173. package/dist/public-dir.d.ts.map +1 -1
  174. package/dist/public-dir.js +17 -6
  175. package/dist/public-dir.js.map +1 -1
  176. package/dist/query.d.ts +13 -4
  177. package/dist/query.d.ts.map +1 -1
  178. package/dist/query.js +89 -26
  179. package/dist/query.js.map +1 -1
  180. package/dist/render-seam.d.ts +25 -2
  181. package/dist/render-seam.d.ts.map +1 -1
  182. package/dist/render-seam.js +1 -0
  183. package/dist/render-seam.js.map +1 -1
  184. package/dist/route-types.d.ts.map +1 -1
  185. package/dist/route-types.js +20 -9
  186. package/dist/route-types.js.map +1 -1
  187. package/dist/router.d.ts +9 -2
  188. package/dist/router.d.ts.map +1 -1
  189. package/dist/router.js +47 -50
  190. package/dist/router.js.map +1 -1
  191. package/dist/seo.d.ts +49 -0
  192. package/dist/seo.d.ts.map +1 -0
  193. package/dist/seo.js +48 -0
  194. package/dist/seo.js.map +1 -0
  195. package/dist/server-components/build-info.d.ts +3 -0
  196. package/dist/server-components/build-info.d.ts.map +1 -0
  197. package/dist/server-components/build-info.js +7 -0
  198. package/dist/server-components/build-info.js.map +1 -0
  199. package/dist/server-components/build-target.d.ts +12 -0
  200. package/dist/server-components/build-target.d.ts.map +1 -0
  201. package/dist/server-components/build-target.js +153 -0
  202. package/dist/server-components/build-target.js.map +1 -0
  203. package/dist/server-components/build.d.ts +24 -0
  204. package/dist/server-components/build.d.ts.map +1 -0
  205. package/dist/server-components/build.js +366 -0
  206. package/dist/server-components/build.js.map +1 -0
  207. package/dist/server-components/bundle-proof.d.ts +3 -0
  208. package/dist/server-components/bundle-proof.d.ts.map +1 -0
  209. package/dist/server-components/bundle-proof.js +23 -0
  210. package/dist/server-components/bundle-proof.js.map +1 -0
  211. package/dist/server-components/client.d.ts +46 -0
  212. package/dist/server-components/client.d.ts.map +1 -0
  213. package/dist/server-components/client.js +594 -0
  214. package/dist/server-components/client.js.map +1 -0
  215. package/dist/server-components/definition.d.ts +11 -0
  216. package/dist/server-components/definition.d.ts.map +1 -0
  217. package/dist/server-components/definition.js +94 -0
  218. package/dist/server-components/definition.js.map +1 -0
  219. package/dist/server-components/dev-http.d.ts +5 -0
  220. package/dist/server-components/dev-http.d.ts.map +1 -0
  221. package/dist/server-components/dev-http.js +71 -0
  222. package/dist/server-components/dev-http.js.map +1 -0
  223. package/dist/server-components/error.d.ts +9 -0
  224. package/dist/server-components/error.d.ts.map +1 -0
  225. package/dist/server-components/error.js +24 -0
  226. package/dist/server-components/error.js.map +1 -0
  227. package/dist/server-components/frames.d.ts +7 -0
  228. package/dist/server-components/frames.d.ts.map +1 -0
  229. package/dist/server-components/frames.js +169 -0
  230. package/dist/server-components/frames.js.map +1 -0
  231. package/dist/server-components/html-policy.d.ts +6 -0
  232. package/dist/server-components/html-policy.d.ts.map +1 -0
  233. package/dist/server-components/html-policy.js +75 -0
  234. package/dist/server-components/html-policy.js.map +1 -0
  235. package/dist/server-components/html.d.ts +4 -0
  236. package/dist/server-components/html.d.ts.map +1 -0
  237. package/dist/server-components/html.js +67 -0
  238. package/dist/server-components/html.js.map +1 -0
  239. package/dist/server-components/islands.d.ts +44 -0
  240. package/dist/server-components/islands.d.ts.map +1 -0
  241. package/dist/server-components/islands.js +42 -0
  242. package/dist/server-components/islands.js.map +1 -0
  243. package/dist/server-components/native.d.ts +4 -0
  244. package/dist/server-components/native.d.ts.map +1 -0
  245. package/dist/server-components/native.js +81 -0
  246. package/dist/server-components/native.js.map +1 -0
  247. package/dist/server-components/portal.d.ts +6 -0
  248. package/dist/server-components/portal.d.ts.map +1 -0
  249. package/dist/server-components/portal.js +35 -0
  250. package/dist/server-components/portal.js.map +1 -0
  251. package/dist/server-components/props.d.ts +3 -0
  252. package/dist/server-components/props.d.ts.map +1 -0
  253. package/dist/server-components/props.js +14 -0
  254. package/dist/server-components/props.js.map +1 -0
  255. package/dist/server-components/protocol.d.ts +44 -0
  256. package/dist/server-components/protocol.d.ts.map +1 -0
  257. package/dist/server-components/protocol.js +139 -0
  258. package/dist/server-components/protocol.js.map +1 -0
  259. package/dist/server-components/range.d.ts +24 -0
  260. package/dist/server-components/range.d.ts.map +1 -0
  261. package/dist/server-components/range.js +263 -0
  262. package/dist/server-components/range.js.map +1 -0
  263. package/dist/server-components/registry.d.ts +6 -0
  264. package/dist/server-components/registry.d.ts.map +1 -0
  265. package/dist/server-components/registry.js +30 -0
  266. package/dist/server-components/registry.js.map +1 -0
  267. package/dist/server-components/response.d.ts +4 -0
  268. package/dist/server-components/response.d.ts.map +1 -0
  269. package/dist/server-components/response.js +32 -0
  270. package/dist/server-components/response.js.map +1 -0
  271. package/dist/server-components/router-integration.d.ts +9 -0
  272. package/dist/server-components/router-integration.d.ts.map +1 -0
  273. package/dist/server-components/router-integration.js +29 -0
  274. package/dist/server-components/router-integration.js.map +1 -0
  275. package/dist/server-components/runtime.d.ts +14 -0
  276. package/dist/server-components/runtime.d.ts.map +1 -0
  277. package/dist/server-components/runtime.js +276 -0
  278. package/dist/server-components/runtime.js.map +1 -0
  279. package/dist/server-components/scheduler.d.ts +14 -0
  280. package/dist/server-components/scheduler.d.ts.map +1 -0
  281. package/dist/server-components/scheduler.js +74 -0
  282. package/dist/server-components/scheduler.js.map +1 -0
  283. package/dist/server-components/schema.d.ts +4 -0
  284. package/dist/server-components/schema.d.ts.map +1 -0
  285. package/dist/server-components/schema.js +77 -0
  286. package/dist/server-components/schema.js.map +1 -0
  287. package/dist/server-components/server-entry.d.ts +12 -0
  288. package/dist/server-components/server-entry.d.ts.map +1 -0
  289. package/dist/server-components/server-entry.js +90 -0
  290. package/dist/server-components/server-entry.js.map +1 -0
  291. package/dist/server-components/source.d.ts +15 -0
  292. package/dist/server-components/source.d.ts.map +1 -0
  293. package/dist/server-components/source.js +269 -0
  294. package/dist/server-components/source.js.map +1 -0
  295. package/dist/server-components/state.d.ts +4 -0
  296. package/dist/server-components/state.d.ts.map +1 -0
  297. package/dist/server-components/state.js +123 -0
  298. package/dist/server-components/state.js.map +1 -0
  299. package/dist/server-components/stream.d.ts +6 -0
  300. package/dist/server-components/stream.d.ts.map +1 -0
  301. package/dist/server-components/stream.js +70 -0
  302. package/dist/server-components/stream.js.map +1 -0
  303. package/dist/server-components/trusted-html.d.ts +3 -0
  304. package/dist/server-components/trusted-html.d.ts.map +1 -0
  305. package/dist/server-components/trusted-html.js +14 -0
  306. package/dist/server-components/trusted-html.js.map +1 -0
  307. package/dist/server-components/types.d.ts +107 -0
  308. package/dist/server-components/types.d.ts.map +1 -0
  309. package/dist/server-components/types.js +2 -0
  310. package/dist/server-components/types.js.map +1 -0
  311. package/dist/server-components/typescript.d.ts +11 -0
  312. package/dist/server-components/typescript.d.ts.map +1 -0
  313. package/dist/server-components/typescript.js +28 -0
  314. package/dist/server-components/typescript.js.map +1 -0
  315. package/dist/server-components/value.d.ts +5 -0
  316. package/dist/server-components/value.d.ts.map +1 -0
  317. package/dist/server-components/value.js +77 -0
  318. package/dist/server-components/value.js.map +1 -0
  319. package/dist/server-components/vite.d.ts +12 -0
  320. package/dist/server-components/vite.d.ts.map +1 -0
  321. package/dist/server-components/vite.js +197 -0
  322. package/dist/server-components/vite.js.map +1 -0
  323. package/dist/server-components.d.ts +5 -0
  324. package/dist/server-components.d.ts.map +1 -0
  325. package/dist/server-components.js +4 -0
  326. package/dist/server-components.js.map +1 -0
  327. package/dist/service-worker.d.ts +8 -1
  328. package/dist/service-worker.d.ts.map +1 -1
  329. package/dist/service-worker.js +20 -6
  330. package/dist/service-worker.js.map +1 -1
  331. package/dist/vite.d.ts +12 -0
  332. package/dist/vite.d.ts.map +1 -1
  333. package/dist/vite.js +7 -1
  334. package/dist/vite.js.map +1 -1
  335. package/dist/zones.d.ts +64 -2
  336. package/dist/zones.d.ts.map +1 -1
  337. package/dist/zones.js +331 -28
  338. package/dist/zones.js.map +1 -1
  339. package/package.json +102 -4
  340. package/src/build-vite.ts +31 -9
  341. package/src/build.ts +109 -31
  342. package/src/cdn.ts +18 -12
  343. package/src/client.ts +48 -17
  344. package/src/deferred.ts +27 -9
  345. package/src/dev-feed.ts +24 -5
  346. package/src/dev-port.ts +4 -10
  347. package/src/dev-session.ts +4 -13
  348. package/src/dev.ts +29 -1
  349. package/src/draft.ts +35 -14
  350. package/src/fonts-google.ts +10 -0
  351. package/src/index.ts +5 -6
  352. package/src/internal/codegen.ts +60 -29
  353. package/src/internal/disposable-dir.ts +24 -0
  354. package/src/internal/entry-chunks.ts +18 -0
  355. package/src/internal/evidence-traversal.ts +57 -0
  356. package/src/internal/head-merge.ts +16 -0
  357. package/src/internal/js-string.ts +6 -1
  358. package/src/internal/mount-shadow.ts +1 -1
  359. package/src/internal/output-guard.ts +88 -7
  360. package/src/internal/package-entries.ts +331 -0
  361. package/src/internal/page-execution.ts +573 -114
  362. package/src/internal/parity.ts +15 -2
  363. package/src/internal/private-env.ts +27 -13
  364. package/src/internal/progressive-cookies.ts +60 -0
  365. package/src/internal/public-env.ts +14 -0
  366. package/src/internal/render-document.ts +66 -89
  367. package/src/internal/response-controls.ts +31 -14
  368. package/src/internal/revive-dates.ts +35 -0
  369. package/src/internal/route-path.ts +24 -0
  370. package/src/internal/secret-scan.ts +190 -18
  371. package/src/internal/server-boundary.ts +29 -5
  372. package/src/internal/web-app.ts +126 -42
  373. package/src/internal/wire-date.ts +16 -0
  374. package/src/internal/without-query.ts +8 -0
  375. package/src/internal/zone-graph.ts +78 -25
  376. package/src/isr.ts +364 -45
  377. package/src/llms-txt.ts +32 -3
  378. package/src/manifest.ts +38 -4
  379. package/src/open-resolved.ts +64 -0
  380. package/src/plugins/kit.ts +31 -11
  381. package/src/plugins/vite-leak-guard.ts +104 -8
  382. package/src/plugins/vite-zone-guard.ts +21 -19
  383. package/src/plugins/zone-guard.ts +41 -2
  384. package/src/prerender.ts +39 -18
  385. package/src/public-dir.ts +27 -8
  386. package/src/query.ts +107 -26
  387. package/src/render-seam.ts +30 -2
  388. package/src/route-types.ts +34 -7
  389. package/src/router.ts +66 -59
  390. package/src/seo.ts +68 -0
  391. package/src/server-components/build-info.ts +11 -0
  392. package/src/server-components/build-target.ts +194 -0
  393. package/src/server-components/build.ts +419 -0
  394. package/src/server-components/bundle-proof.ts +33 -0
  395. package/src/server-components/client.ts +663 -0
  396. package/src/server-components/definition.ts +125 -0
  397. package/src/server-components/dev-http.ts +75 -0
  398. package/src/server-components/error.ts +24 -0
  399. package/src/server-components/frames.ts +181 -0
  400. package/src/server-components/html-policy.ts +94 -0
  401. package/src/server-components/html.ts +81 -0
  402. package/src/server-components/islands.ts +111 -0
  403. package/src/server-components/native.ts +101 -0
  404. package/src/server-components/portal.ts +42 -0
  405. package/src/server-components/props.ts +19 -0
  406. package/src/server-components/protocol.ts +167 -0
  407. package/src/server-components/range.ts +313 -0
  408. package/src/server-components/registry.ts +34 -0
  409. package/src/server-components/response.ts +44 -0
  410. package/src/server-components/router-integration.ts +40 -0
  411. package/src/server-components/runtime.ts +376 -0
  412. package/src/server-components/scheduler.ts +84 -0
  413. package/src/server-components/schema.ts +69 -0
  414. package/src/server-components/server-entry.ts +98 -0
  415. package/src/server-components/source.ts +324 -0
  416. package/src/server-components/state.ts +143 -0
  417. package/src/server-components/stream.ts +71 -0
  418. package/src/server-components/trusted-html.ts +21 -0
  419. package/src/server-components/types.ts +138 -0
  420. package/src/server-components/typescript.ts +39 -0
  421. package/src/server-components/value.ts +82 -0
  422. package/src/server-components/vite.ts +242 -0
  423. package/src/server-components.ts +32 -0
  424. package/src/service-worker.ts +24 -7
  425. package/src/vite.ts +8 -1
  426. package/src/zones.ts +418 -32
package/src/zones.ts CHANGED
@@ -11,7 +11,15 @@
11
11
  */
12
12
  import { closeSync, existsSync, openSync, readFileSync, readSync, realpathSync } from "node:fs"
13
13
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
14
+ import {
15
+ type EntryOwnership,
16
+ type EnvironmentDeclaration,
17
+ entryKey,
18
+ entryOwnership,
19
+ parseEnvironment,
20
+ } from "./internal/package-entries.ts"
14
21
  import { SERVER_FN_MODULE } from "./internal/server-boundary.ts"
22
+ import { withoutQuery } from "./internal/without-query.ts"
15
23
 
16
24
  export type Zone =
17
25
  /** A page or special file under `routes/` - the frontend half of a route. */
@@ -23,6 +31,8 @@ export type Zone =
23
31
  | "shared"
24
32
  /** A server-function module (`*.fn.ts`); the browser only ever receives its generated RPC stub. */
25
33
  | "fn"
34
+ /** A compiler-owned server UI module; only opaque reference stubs may reach a browser. */
35
+ | "component"
26
36
  /** Framework output (the client entry, the server composition, generated types). */
27
37
  | "generated"
28
38
  /** A third-party package, or a workspace package declaring `"nifra": { "environment": "library" }`. */
@@ -37,8 +47,17 @@ export type Classification =
37
47
  readonly declared?: PackageEnvironment
38
48
  /** A third-party file's path inside `node_modules`: `drizzle-orm/node-postgres/index.js`. */
39
49
  readonly modulePath?: string
50
+ /** The export subpath (`"./react"`) whose own declared side decided this import. */
51
+ readonly entry?: string
52
+ }
53
+ | {
54
+ readonly zone: "error"
55
+ readonly reason: string
56
+ /** The undeclared package this error is about: every file in it shares the one fix. */
57
+ readonly package?: string
58
+ /** The export subpath the import named, when the package's map gives it no side. */
59
+ readonly entry?: string
40
60
  }
41
- | { readonly zone: "error"; readonly reason: string }
42
61
 
43
62
  export type PackageEnvironment = "frontend" | "backend" | "shared" | "library"
44
63
  const PACKAGE_ENVIRONMENTS: ReadonlySet<string> = new Set([
@@ -113,6 +132,7 @@ const SOURCE = "(?:[cm]?[jt]sx?|svelte|vue|mdx)"
113
132
  export const ROUTE_BACKEND_FILE = /\.backend\.(?:[cm]?[jt]s)$/
114
133
  const ZONE_SUFFIX = new RegExp(`\\.(frontend|backend|shared|fn)\\.${SOURCE}$`)
115
134
  const LEGACY_SERVER_SUFFIX = new RegExp(`\\.server\\.${SOURCE}$`)
135
+ export const SERVER_COMPONENT_FILE = /\.component\.server\.[cm]?[jt]sx?$/
116
136
 
117
137
  const ZONE_FOLDERS: Readonly<Record<string, Zone>> = {
118
138
  frontend: "frontend",
@@ -128,12 +148,182 @@ export interface ZoneClassifierOptions {
128
148
  readonly routesDir?: string
129
149
  /** Extra absolute files treated as `generated` - a build's own entry modules. */
130
150
  readonly generatedFiles?: readonly string[]
151
+ /** The app's zone settings. Default: `"nifra": { "zones": ... }` in the app's package.json. */
152
+ readonly zones?: ZoneConfig
153
+ }
154
+
155
+ /**
156
+ * App-side zone settings, read from `"nifra": { "zones": ... }` in the app's package.json so every
157
+ * build, dev server, `nifra check` and the editor plugin see the same ones without running app code.
158
+ */
159
+ export interface ZoneConfig {
160
+ /**
161
+ * The side of a package that does not declare one in its own package.json, by name: exact
162
+ * (`"@shop/db"`) or a trailing-`*` prefix (`"@shop/*"`). The exact name wins over a prefix, the
163
+ * longest prefix over a shorter one, and a package's own declaration over all of them.
164
+ */
165
+ readonly packages?: Readonly<Record<string, PackageEnvironment>>
166
+ /**
167
+ * App-relative folders or files holding backend code, beside `backend/` (`"migrations"`). A path
168
+ * listed inside another one's folder overrides it: the most specific entry wins.
169
+ */
170
+ readonly backend?: readonly string[]
171
+ /** App-relative folders or files holding frontend code, beside `frontend/`. */
172
+ readonly frontend?: readonly string[]
173
+ /** App-relative folders or files holding code both sides import, beside `shared/`. */
174
+ readonly shared?: readonly string[]
175
+ }
176
+
177
+ /** A `"nifra": { "zones": ... }` setting that cannot be applied. Thrown when a classifier is created. */
178
+ export class ZoneConfigError extends Error {
179
+ override readonly name = "ZoneConfigError"
180
+ /** The file holding the setting, as the caller named it. */
181
+ readonly source: string
182
+ constructor(source: string, problem: string) {
183
+ super(`[nifra] ${source} "nifra.zones": ${problem}`)
184
+ this.source = source
185
+ }
186
+ }
187
+
188
+ const FOLDER_SIDES = ["backend", "frontend", "shared"] as const
189
+ const FOLDER_SIDE_NAMES: ReadonlySet<string> = new Set(FOLDER_SIDES)
190
+ const isPackageEnvironment = (value: unknown): value is PackageEnvironment =>
191
+ typeof value === "string" && PACKAGE_ENVIRONMENTS.has(value)
192
+ const RESERVED_FOLDERS: ReadonlySet<string> = new Set([
193
+ "routes",
194
+ "frontend",
195
+ "backend",
196
+ "shared",
197
+ "public",
198
+ "node_modules",
199
+ ])
200
+
201
+ const recordOf = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
202
+ typeof value === "object" && value !== null && !Array.isArray(value)
203
+ ? Object.fromEntries(Object.entries(value))
204
+ : undefined
205
+
206
+ /**
207
+ * Validate a `zones` setting. `source` names where it came from in the error. Folders and files are
208
+ * plain app-relative paths (a trailing `/**` is accepted and dropped); any other glob is refused, so
209
+ * a pattern can never match less than its author meant without saying so. One path under two sides
210
+ * is refused; a path nested in another's folder is not, since the most specific one decides.
211
+ */
212
+ export function parseZoneConfig(value: unknown, source: string): ZoneConfig {
213
+ const fail = (message: string): never => {
214
+ throw new ZoneConfigError(source, message)
215
+ }
216
+ const record = recordOf(value) ?? fail("must be an object")
217
+ const config: {
218
+ packages?: Record<string, PackageEnvironment>
219
+ backend?: string[]
220
+ frontend?: string[]
221
+ shared?: string[]
222
+ } = {}
223
+ for (const key of Object.keys(record)) {
224
+ if (key !== "packages" && !FOLDER_SIDE_NAMES.has(key))
225
+ fail(`unknown key "${key}" (expected packages, backend, frontend or shared)`)
226
+ }
227
+ if (record.packages !== undefined) {
228
+ const packages = recordOf(record.packages) ?? fail('"packages" must be an object')
229
+ const parsed: Record<string, PackageEnvironment> = {}
230
+ for (const [pattern, side] of Object.entries(packages)) {
231
+ if (pattern === "" || pattern.indexOf("*") !== pattern.lastIndexOf("*"))
232
+ fail(`package pattern "${pattern}" must be a name, or a name prefix ending in one "*"`)
233
+ if (pattern.includes("*") && !pattern.endsWith("*"))
234
+ fail(`package pattern "${pattern}" may use "*" only at its end`)
235
+ parsed[pattern] = isPackageEnvironment(side)
236
+ ? side
237
+ : fail(`packages["${pattern}"] must be one of frontend, backend, shared, library`)
238
+ }
239
+ config.packages = parsed
240
+ }
241
+ const owner = new Map<string, string>()
242
+ for (const side of FOLDER_SIDES) {
243
+ const raw = record[side]
244
+ if (raw === undefined) continue
245
+ const entries: readonly unknown[] = Array.isArray(raw)
246
+ ? raw
247
+ : fail(`"${side}" must be an array of app-relative folders or files`)
248
+ const folders: string[] = []
249
+ for (const entry of entries) {
250
+ const folder = (typeof entry === "string" ? entry : fail(`"${side}" must hold strings`))
251
+ .replaceAll("\\", "/")
252
+ .replace(/^\.\//, "")
253
+ .replace(/\/\*\*$/, "")
254
+ .replace(/\/+$/, "")
255
+ const top = folder.split("/")[0] ?? ""
256
+ if (folder === "" || folder.startsWith("/") || /^[a-z]:/i.test(folder))
257
+ fail(`"${side}" entry "${entry}" must be a folder or file relative to the app`)
258
+ if (folder.split("/").includes("..")) fail(`"${side}" entry "${entry}" may not contain ".."`)
259
+ if (/[*?[\]{}!]/.test(folder))
260
+ fail(`"${side}" entry "${entry}" must be a plain path (a trailing "/**" is allowed)`)
261
+ if (RESERVED_FOLDERS.has(top) || top.startsWith(".nifra"))
262
+ fail(`"${side}" entry "${entry}" is inside ${top}/, which already has its own rules`)
263
+ const other = owner.get(folder)
264
+ if (other !== undefined && other !== side)
265
+ fail(`"${folder}" is listed as both ${other} and ${side}; a path has one side`)
266
+ owner.set(folder, side)
267
+ folders.push(folder)
268
+ }
269
+ config[side] = folders
270
+ }
271
+ return config
272
+ }
273
+
274
+ /** The `nifra.zones` of the nearest package.json at or above `appRoot`, validated. */
275
+ function appZoneConfig(appRoot: string): ZoneConfig {
276
+ let dir = appRoot
277
+ for (;;) {
278
+ const manifest = join(dir, "package.json")
279
+ if (existsSync(manifest)) {
280
+ let parsed: unknown
281
+ try {
282
+ parsed = JSON.parse(readFileSync(manifest, "utf8"))
283
+ } catch {
284
+ return {}
285
+ }
286
+ const zones = recordOf(recordOf(parsed)?.nifra)?.zones
287
+ return zones === undefined ? {} : parseZoneConfig(zones, toPosix(manifest))
288
+ }
289
+ const parent = dirname(dir)
290
+ if (parent === dir) return {}
291
+ dir = parent
292
+ }
131
293
  }
132
294
 
295
+ /** The side `packages` gives `name`: the exact entry, else the longest matching prefix. */
296
+ function mappedSide(
297
+ packages: Readonly<Record<string, PackageEnvironment>> | undefined,
298
+ name: string | undefined,
299
+ ): PackageEnvironment | undefined {
300
+ if (packages === undefined || name === undefined) return undefined
301
+ const exact = packages[name]
302
+ if (exact !== undefined && Object.hasOwn(packages, name)) return exact
303
+ let best: string | undefined
304
+ for (const pattern of Object.keys(packages)) {
305
+ if (!pattern.endsWith("*") || !name.startsWith(pattern.slice(0, -1))) continue
306
+ if (best === undefined || pattern.length > best.length) best = pattern
307
+ }
308
+ return best === undefined ? undefined : packages[best]
309
+ }
310
+
311
+ /** How the `packages` setting would name `name`: its scope wildcard when it has a scope. */
312
+ const suggestedPattern = (name: string): string =>
313
+ name.startsWith("@") && name.includes("/") ? `${name.slice(0, name.indexOf("/"))}/*` : name
314
+
133
315
  export interface ZoneClassifier {
134
316
  readonly appRoot: string
135
- /** Classify a file by its absolute (or appRoot-relative) path. Results are cached per path. */
136
- classify(file: string): Classification
317
+ /**
318
+ * Classify a file by its absolute (or appRoot-relative) path. Results are cached per path.
319
+ *
320
+ * With `specifier`, the import as written that resolved to `file`: when it names a package that
321
+ * gives each export entry its own side (`"nifra": { "environment": { ".": "backend", "./react":
322
+ * "frontend" } }`), the entry it names decides, since two entries may share one file. A specifier
323
+ * naming an entry the map leaves out is an error. The resolved file must belong to that entry, as
324
+ * the package's own imports reach it; one only other entries reach is judged by its path.
325
+ */
326
+ classify(file: string, specifier?: string): Classification
137
327
  }
138
328
 
139
329
  const toPosix = (path: string): string => path.replaceAll("\\", "/")
@@ -187,7 +377,19 @@ function nodeModulesPackage(path: string): string | undefined {
187
377
  interface PackageInfo {
188
378
  readonly dir: string
189
379
  readonly name: string | undefined
190
- readonly environment: string | undefined
380
+ readonly declaration: EnvironmentDeclaration
381
+ readonly manifest: Readonly<Record<string, unknown>>
382
+ }
383
+
384
+ const DECLARATION_FILE = /\.d\.[cm]?ts$/
385
+
386
+ /** The one side a file reached by several mapped entries takes: what each entry's importers may import. */
387
+ function ownersSide(owners: ReadonlySet<PackageEnvironment>): PackageEnvironment {
388
+ const [only] = owners
389
+ if (owners.size === 1 && only !== undefined) return only
390
+ return owners.has("library") || (owners.has("frontend") && owners.has("backend"))
391
+ ? "library"
392
+ : "shared"
191
393
  }
192
394
 
193
395
  /**
@@ -198,6 +400,19 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
198
400
  const appRoot = realFile(resolve(options.appRoot))
199
401
  const routesDir = realFile(resolve(options.routesDir ?? join(options.appRoot, "routes")))
200
402
  const generated = new Set((options.generatedFiles ?? []).map((file) => realFile(resolve(file))))
403
+ const zones =
404
+ options.zones === undefined
405
+ ? appZoneConfig(appRoot)
406
+ : parseZoneConfig(options.zones, "ZoneClassifierOptions")
407
+ const configuredFolders: Array<readonly [string, "backend" | "frontend" | "shared"]> = []
408
+ for (const side of FOLDER_SIDES)
409
+ for (const folder of zones[side] ?? []) configuredFolders.push([folder, side])
410
+ // Longest first, so the most specific entry covering a file decides its side.
411
+ configuredFolders.sort(([a], [b]) => b.length - a.length)
412
+ const configuredPath = (
413
+ rel: string,
414
+ ): readonly [string, "backend" | "frontend" | "shared"] | undefined =>
415
+ configuredFolders.find(([folder]) => rel === folder || rel.startsWith(`${folder}/`))
201
416
  const cache = new Map<string, Classification>()
202
417
  const packages = new Map<string, PackageInfo | null>()
203
418
 
@@ -214,17 +429,17 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
214
429
  seen.push(current)
215
430
  const manifest = join(current, "package.json")
216
431
  if (existsSync(manifest)) {
217
- let parsed: { name?: unknown; nifra?: { environment?: unknown } } = {}
432
+ let parsed: Readonly<Record<string, unknown>> = {}
218
433
  try {
219
- parsed = JSON.parse(readFileSync(manifest, "utf8")) as typeof parsed
434
+ parsed = recordOf(JSON.parse(readFileSync(manifest, "utf8"))) ?? {}
220
435
  } catch {
221
436
  // An unreadable manifest declares nothing; the caller reports the missing declaration.
222
437
  }
223
438
  const info: PackageInfo = {
224
439
  dir: current,
225
440
  name: typeof parsed.name === "string" ? parsed.name : undefined,
226
- environment:
227
- typeof parsed.nifra?.environment === "string" ? parsed.nifra.environment : undefined,
441
+ declaration: parseEnvironment(recordOf(parsed.nifra)?.environment),
442
+ manifest: parsed,
228
443
  }
229
444
  for (const visited of seen) packages.set(visited, info)
230
445
  return info
@@ -238,15 +453,96 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
238
453
  }
239
454
  }
240
455
 
456
+ const ownerships = new Map<string, EntryOwnership>()
457
+ const ownershipOf = (
458
+ info: PackageInfo,
459
+ entries: Readonly<Record<string, PackageEnvironment>>,
460
+ ): EntryOwnership => {
461
+ let found = ownerships.get(info.dir)
462
+ if (found === undefined) {
463
+ found = entryOwnership(info.dir, info.manifest, entries)
464
+ ownerships.set(info.dir, found)
465
+ }
466
+ return found
467
+ }
468
+
469
+ /** The package `file` is part of, when that package is not the app: installed, or a workspace sibling. */
470
+ const packageOf = (file: string): PackageInfo | undefined => {
471
+ const thirdParty = nodeModulesPackage(file)
472
+ if (thirdParty !== undefined) {
473
+ const at = file.lastIndexOf("/node_modules/") + "/node_modules/".length
474
+ return packageFor(file.slice(0, at + thirdParty.length))
475
+ }
476
+ return isInside(file, appRoot) ? undefined : packageFor(toPosix(dirname(file)))
477
+ }
478
+
479
+ /** What a side declared for `file` (by its package, an entry of it, or the app) makes it. */
480
+ const declaredAs = (
481
+ file: string,
482
+ info: PackageInfo | undefined,
483
+ side: PackageEnvironment,
484
+ entry?: string,
485
+ ): Classification => {
486
+ const thirdParty = nodeModulesPackage(file)
487
+ const named = entry === undefined || info?.name === undefined ? {} : { packageName: info.name }
488
+ const decided = entry === undefined ? {} : { entry }
489
+ if (thirdParty !== undefined) {
490
+ return {
491
+ zone: "library",
492
+ packageName: thirdParty,
493
+ modulePath: file.slice(file.lastIndexOf("/node_modules/") + "/node_modules/".length),
494
+ declared: side,
495
+ ...decided,
496
+ }
497
+ }
498
+ if (side !== "library") return { zone: side, ...named, ...decided }
499
+ return {
500
+ zone: "library",
501
+ ...(info?.name ? { packageName: info.name } : {}),
502
+ declared: "library",
503
+ ...decided,
504
+ }
505
+ }
506
+
507
+ /** A file of a package that declares a side per entry, judged by the entries that reach it. */
508
+ const byOwners = (
509
+ file: string,
510
+ info: PackageInfo,
511
+ entries: Readonly<Record<string, PackageEnvironment>>,
512
+ ): Classification => {
513
+ const ownership = ownershipOf(info, entries)
514
+ const owners = ownership.owners.get(file)
515
+ if (owners !== undefined) return declaredAs(file, info, ownersSide(owners))
516
+ const shown = nodeModulesPackage(file) === undefined ? file : file.slice(info.dir.length + 1)
517
+ const left =
518
+ ownership.unmapped.length === 0
519
+ ? ""
520
+ : ` (the map leaves out ${ownership.unmapped.map((key) => `"${key}"`).join(", ")})`
521
+ return {
522
+ zone: "error",
523
+ reason: `"${shown}" is in package "${info.name ?? info.dir}", whose "nifra.environment" gives each entry its own side, and no mapped entry reaches this file${left}. Import the package through a mapped entry`,
524
+ }
525
+ }
526
+
527
+ const invalidDeclaration = (info: PackageInfo, problem: string): Classification => {
528
+ const reason = `${info.dir}/package.json "nifra.environment": ${problem}`
529
+ return info.name === undefined
530
+ ? { zone: "error", reason }
531
+ : { zone: "error", reason, package: info.name }
532
+ }
533
+
241
534
  const classifyUncached = (file: string): Classification => {
242
535
  if (generated.has(file)) return { zone: "generated" }
243
536
  const thirdParty = nodeModulesPackage(file)
244
537
  if (thirdParty !== undefined) {
245
- const info = packageFor(toPosix(dirname(file)))
538
+ const info = packageOf(file)
539
+ const declaration = info?.declaration ?? { kind: "none" }
540
+ if (info !== undefined && declaration.kind === "invalid")
541
+ return invalidDeclaration(info, declaration.problem)
542
+ if (info !== undefined && declaration.kind === "entries")
543
+ return byOwners(file, info, declaration.entries)
246
544
  const declared =
247
- info?.environment !== undefined && PACKAGE_ENVIRONMENTS.has(info.environment)
248
- ? (info.environment as PackageEnvironment)
249
- : undefined
545
+ declaration.kind === "package" ? declaration.side : mappedSide(zones.packages, thirdParty)
250
546
  return {
251
547
  zone: "library",
252
548
  packageName: thirdParty,
@@ -259,6 +555,8 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
259
555
  if (SERVER_MANIFEST_FILE.test(base) && isGeneratedServerManifest(file)) {
260
556
  return { zone: "generated" }
261
557
  }
558
+ if (SERVER_COMPONENT_FILE.test(base)) return { zone: "component" }
559
+ if (/\.island\.[cm]?[jt]sx?$/.test(base)) return { zone: "frontend" }
262
560
  if (LEGACY_SERVER_SUFFIX.test(base)) {
263
561
  return {
264
562
  zone: "error",
@@ -289,13 +587,20 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
289
587
  if (suffix === "shared") return { zone: "shared" }
290
588
  return { zone: "route-frontend" }
291
589
  }
292
- const folder = ZONE_FOLDERS[top]
590
+ const configured = ZONE_FOLDERS[top] === undefined ? configuredPath(rel) : undefined
591
+ const folder = ZONE_FOLDERS[top] ?? configured?.[1]
293
592
  if (folder !== undefined) {
294
593
  if (suffix === undefined || suffix === folder) return { zone: folder }
295
594
  if (suffix === "fn" && folder === "backend") return { zone: "fn" }
595
+ const where =
596
+ configured === undefined
597
+ ? `sits in ${top}/`
598
+ : configured[0] === rel
599
+ ? `is listed as ${folder} in "nifra.zones"`
600
+ : `sits in ${configured[0]}/, which "nifra.zones" lists as ${folder},`
296
601
  return {
297
602
  zone: "error",
298
- reason: `"${rel}" sits in ${top}/ but its ".${suffix}" suffix says otherwise. A file belongs to one side: move it, or drop the suffix`,
603
+ reason: `"${rel}" ${where} but its ".${suffix}" suffix says otherwise. A file belongs to one side: move it, or drop the suffix`,
299
604
  }
300
605
  }
301
606
  if (suffix !== undefined) return { zone: suffix === "fn" ? "fn" : suffix }
@@ -303,33 +608,80 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
303
608
  }
304
609
  // First-party code outside the app: a workspace package, or a relative import out of the app.
305
610
  const info = packageFor(toPosix(dirname(file)))
306
- const environment = info?.environment
307
- if (environment !== undefined && PACKAGE_ENVIRONMENTS.has(environment)) {
308
- return environment === "library"
309
- ? {
310
- zone: "library",
311
- ...(info?.name ? { packageName: info.name } : {}),
312
- declared: "library",
313
- }
314
- : { zone: environment as Zone }
611
+ const declaration = info?.declaration ?? { kind: "none" }
612
+ if (info !== undefined && declaration.kind === "invalid")
613
+ return invalidDeclaration(info, declaration.problem)
614
+ if (info !== undefined && declaration.kind === "entries")
615
+ return byOwners(file, info, declaration.entries)
616
+ const environment =
617
+ declaration.kind === "package" ? declaration.side : mappedSide(zones.packages, info?.name)
618
+ if (environment !== undefined) return declaredAs(file, info, environment)
619
+ const declare = `"nifra": { "environment": "frontend" | "backend" | "shared" | "library" }`
620
+ if (info?.name === undefined) {
621
+ return {
622
+ zone: "error",
623
+ reason: `"${file}" is first-party code outside the app and declares no side. Add ${declare} to ${info === undefined ? "a package.json above it" : `${info.dir}/package.json`}`,
624
+ }
315
625
  }
316
- const where = info?.name ?? info?.dir ?? dirname(file)
626
+ // Named per package, not per file, so a build that reaches 50 files of it reports one fact once.
317
627
  return {
318
628
  zone: "error",
319
- reason: `"${file}" is first-party code outside the app (${where}) and declares no side. Add "nifra": { "environment": "frontend" | "backend" | "shared" | "library" } to ${info === undefined ? "its package.json" : `${info.dir}/package.json`}`,
629
+ package: info.name,
630
+ reason: `package "${info.name}" is first-party code outside the app and declares no side. Add ${declare} to ${info.dir}/package.json, or give it a side from the app's package.json: "nifra": { "zones": { "packages": { "${suggestedPattern(info.name)}": "backend" } } } (or "library" if the browser may load it)`,
631
+ }
632
+ }
633
+
634
+ const byPath = (file: string): Classification => {
635
+ const cached = cache.get(file)
636
+ if (cached !== undefined) return cached
637
+ const result = classifyUncached(file)
638
+ cache.set(file, result)
639
+ return result
640
+ }
641
+
642
+ // Keyed by file and subpath: two entries may share one file and declare different sides.
643
+ const imports = new Map<string, Classification>()
644
+ const byImport = (file: string, specifier: string): Classification => {
645
+ const name = specifierPackage(specifier)
646
+ const info = name === undefined ? undefined : packageOf(file)
647
+ if (name === undefined || info?.name !== name || info.declaration.kind !== "entries")
648
+ return byPath(file)
649
+ const subpath = entrySubpath(specifier, name)
650
+ const cacheKey = `${file}\0${subpath}`
651
+ const cached = imports.get(cacheKey)
652
+ if (cached !== undefined) return cached
653
+ const { entries } = info.declaration
654
+ const key = entryKey(entries, subpath)
655
+ const side = key === undefined ? undefined : entries[key]
656
+ let result: Classification
657
+ if (side === undefined) {
658
+ const mapped = Object.keys(entries)
659
+ .map((entry) => `"${entry}"`)
660
+ .join(", ")
661
+ result = {
662
+ zone: "error",
663
+ reason: `"${specifier}" names the "${subpath}" entry of package "${name}", whose "nifra.environment" map gives it no side (it maps ${mapped}). Map the entry, or import one that is mapped`,
664
+ entry: subpath,
665
+ }
666
+ } else {
667
+ // The resolver's pick must belong to the named entry; a file only other entries reach (an alias
668
+ // pointed at it) is judged by its path. A declaration file is types only and erased.
669
+ const owners = ownershipOf(info, entries).owners.get(file)
670
+ result =
671
+ owners !== undefined && !owners.has(side) && !DECLARATION_FILE.test(file)
672
+ ? byPath(file)
673
+ : declaredAs(file, info, side, subpath)
320
674
  }
675
+ imports.set(cacheKey, result)
676
+ return result
321
677
  }
322
678
 
323
679
  return {
324
680
  appRoot,
325
- classify(input: string): Classification {
681
+ classify(input: string, specifier?: string): Classification {
326
682
  const absolute = isAbsolute(input) ? input : resolve(appRoot, input)
327
683
  const file = realFile(absolute)
328
- const cached = cache.get(file)
329
- if (cached !== undefined) return cached
330
- const result = classifyUncached(file)
331
- cache.set(file, result)
332
- return result
684
+ return specifier === undefined ? byPath(file) : byImport(file, specifier)
333
685
  },
334
686
  }
335
687
  }
@@ -360,6 +712,27 @@ export function specifierPackage(specifier: string): string | undefined {
360
712
  return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]
361
713
  }
362
714
 
715
+ /** The export subpath (`"."`, `"./react"`) a bare specifier of package `name` names. */
716
+ const entrySubpath = (specifier: string, name: string): string =>
717
+ `.${withoutQuery(specifier.slice(name.length))}`
718
+
719
+ /**
720
+ * The side a package's `"nifra": { "environment": ... }` value gives the bare import `specifier` of it:
721
+ * the package-wide string, or the side of the entry it names - the map's exact subpath, else its most
722
+ * specific `*` pattern. `undefined` when it gives none, as a map that leaves the entry out does.
723
+ */
724
+ export function declaredSide(
725
+ environment: unknown,
726
+ specifier: string,
727
+ ): PackageEnvironment | undefined {
728
+ const declaration = parseEnvironment(environment)
729
+ if (declaration.kind === "package") return declaration.side
730
+ const name = specifierPackage(specifier)
731
+ if (declaration.kind !== "entries" || name === undefined) return undefined
732
+ const key = entryKey(declaration.entries, entrySubpath(specifier, name))
733
+ return key === undefined ? undefined : declaration.entries[key]
734
+ }
735
+
363
736
  /**
364
737
  * Why a classified file may not ship to a browser, or `undefined` when it may. A `drizzle-orm` driver
365
738
  * entry is recognized by the import as written (`specifier`) or by the file's path in the package.
@@ -370,13 +743,19 @@ export function browserDenial(
370
743
  ): string | undefined {
371
744
  if (classification.zone === "error") return classification.reason
372
745
  if (!BROWSER_ZONES.has(classification.zone)) {
746
+ if (classification.entry !== undefined)
747
+ return `package "${classification.packageName}" declares its "${classification.entry}" entry ${classification.zone}-only`
373
748
  return classification.zone === "route-backend"
374
749
  ? "it is a route's backend half, which runs on the server only"
375
750
  : "it is backend code"
376
751
  }
377
752
  if (classification.zone !== "library") return undefined
378
753
  const name = classification.packageName
379
- if (classification.declared === "backend") return `package "${name}" declares itself backend-only`
754
+ if (classification.declared === "backend") {
755
+ return classification.entry === undefined
756
+ ? `package "${name}" declares itself backend-only`
757
+ : `package "${name}" declares its "${classification.entry}" entry backend-only`
758
+ }
380
759
  if (name !== undefined && SERVER_PACKAGES.includes(name)) {
381
760
  return `package "${name}" is server code (a driver, server SDK or credential helper)`
382
761
  }
@@ -388,8 +767,15 @@ export function browserDenial(
388
767
  return undefined
389
768
  }
390
769
 
770
+ /** Whether a bare specifier names one of {@link SERVER_PACKAGES} or a database driver entry. */
771
+ export function isServerPackageSpecifier(specifier: string): boolean {
772
+ const name = specifierPackage(specifier)
773
+ return (name !== undefined && SERVER_PACKAGES.includes(name)) || SERVER_SUBPATHS.test(specifier)
774
+ }
775
+
391
776
  /** Whether a module in `from` may import (with a value import) a module in `to`. */
392
777
  export function importAllowed(from: Zone, to: Zone): boolean {
778
+ if (from === "component") return true
393
779
  if (from === "generated" || from === "library") return true
394
780
  if (to === "generated" || to === "library") return true
395
781
  if (FRONTEND_ZONES.has(from)) return FRONTEND_ZONES.has(to) || to === "shared" || to === "fn"