enpilink 1.0.2

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 (477) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +289 -0
  3. package/bin/run.js +5 -0
  4. package/dist/cli/build-helpers.d.ts +8 -0
  5. package/dist/cli/build-helpers.js +105 -0
  6. package/dist/cli/build-helpers.js.map +1 -0
  7. package/dist/cli/build-helpers.test.d.ts +1 -0
  8. package/dist/cli/build-helpers.test.js +100 -0
  9. package/dist/cli/build-helpers.test.js.map +1 -0
  10. package/dist/cli/detect-port.d.ts +18 -0
  11. package/dist/cli/detect-port.js +50 -0
  12. package/dist/cli/detect-port.js.map +1 -0
  13. package/dist/cli/ensure-ssh-key.d.ts +17 -0
  14. package/dist/cli/ensure-ssh-key.js +45 -0
  15. package/dist/cli/ensure-ssh-key.js.map +1 -0
  16. package/dist/cli/ensure-ssh-key.test.d.ts +1 -0
  17. package/dist/cli/ensure-ssh-key.test.js +68 -0
  18. package/dist/cli/ensure-ssh-key.test.js.map +1 -0
  19. package/dist/cli/header.d.ts +4 -0
  20. package/dist/cli/header.js +6 -0
  21. package/dist/cli/header.js.map +1 -0
  22. package/dist/cli/resolve-views-dir.d.ts +1 -0
  23. package/dist/cli/resolve-views-dir.js +17 -0
  24. package/dist/cli/resolve-views-dir.js.map +1 -0
  25. package/dist/cli/run-command.d.ts +2 -0
  26. package/dist/cli/run-command.js +43 -0
  27. package/dist/cli/run-command.js.map +1 -0
  28. package/dist/cli/telemetry.d.ts +14 -0
  29. package/dist/cli/telemetry.js +24 -0
  30. package/dist/cli/telemetry.js.map +1 -0
  31. package/dist/cli/tunnel-control-server.d.ts +11 -0
  32. package/dist/cli/tunnel-control-server.js +35 -0
  33. package/dist/cli/tunnel-control-server.js.map +1 -0
  34. package/dist/cli/tunnel-control-server.test.d.ts +1 -0
  35. package/dist/cli/tunnel-control-server.test.js +39 -0
  36. package/dist/cli/tunnel-control-server.test.js.map +1 -0
  37. package/dist/cli/tunnel-handler.d.ts +3 -0
  38. package/dist/cli/tunnel-handler.js +48 -0
  39. package/dist/cli/tunnel-handler.js.map +1 -0
  40. package/dist/cli/tunnel-handler.test.d.ts +1 -0
  41. package/dist/cli/tunnel-handler.test.js +107 -0
  42. package/dist/cli/tunnel-handler.test.js.map +1 -0
  43. package/dist/cli/tunnel-providers/index.d.ts +5 -0
  44. package/dist/cli/tunnel-providers/index.js +5 -0
  45. package/dist/cli/tunnel-providers/index.js.map +1 -0
  46. package/dist/cli/tunnel-providers/srv-us.d.ts +18 -0
  47. package/dist/cli/tunnel-providers/srv-us.js +66 -0
  48. package/dist/cli/tunnel-providers/srv-us.js.map +1 -0
  49. package/dist/cli/tunnel-providers/srv-us.test.d.ts +1 -0
  50. package/dist/cli/tunnel-providers/srv-us.test.js +74 -0
  51. package/dist/cli/tunnel-providers/srv-us.test.js.map +1 -0
  52. package/dist/cli/tunnel-providers/types.d.ts +49 -0
  53. package/dist/cli/tunnel-providers/types.js +2 -0
  54. package/dist/cli/tunnel-providers/types.js.map +1 -0
  55. package/dist/cli/tunnel.d.ts +75 -0
  56. package/dist/cli/tunnel.js +254 -0
  57. package/dist/cli/tunnel.js.map +1 -0
  58. package/dist/cli/tunnel.test.d.ts +1 -0
  59. package/dist/cli/tunnel.test.js +255 -0
  60. package/dist/cli/tunnel.test.js.map +1 -0
  61. package/dist/cli/types.d.ts +5 -0
  62. package/dist/cli/types.js +2 -0
  63. package/dist/cli/types.js.map +1 -0
  64. package/dist/cli/use-execute-steps.d.ts +11 -0
  65. package/dist/cli/use-execute-steps.js +36 -0
  66. package/dist/cli/use-execute-steps.js.map +1 -0
  67. package/dist/cli/use-messages.d.ts +3 -0
  68. package/dist/cli/use-messages.js +11 -0
  69. package/dist/cli/use-messages.js.map +1 -0
  70. package/dist/cli/use-nodemon.d.ts +2 -0
  71. package/dist/cli/use-nodemon.js +73 -0
  72. package/dist/cli/use-nodemon.js.map +1 -0
  73. package/dist/cli/use-open-browser.d.ts +1 -0
  74. package/dist/cli/use-open-browser.js +44 -0
  75. package/dist/cli/use-open-browser.js.map +1 -0
  76. package/dist/cli/use-open-tunnel-browser.d.ts +6 -0
  77. package/dist/cli/use-open-tunnel-browser.js +19 -0
  78. package/dist/cli/use-open-tunnel-browser.js.map +1 -0
  79. package/dist/cli/use-tunnel.d.ts +17 -0
  80. package/dist/cli/use-tunnel.js +131 -0
  81. package/dist/cli/use-tunnel.js.map +1 -0
  82. package/dist/cli/use-typescript-check.d.ts +9 -0
  83. package/dist/cli/use-typescript-check.js +94 -0
  84. package/dist/cli/use-typescript-check.js.map +1 -0
  85. package/dist/commands/build.d.ts +8 -0
  86. package/dist/commands/build.js +97 -0
  87. package/dist/commands/build.js.map +1 -0
  88. package/dist/commands/create.d.ts +9 -0
  89. package/dist/commands/create.js +30 -0
  90. package/dist/commands/create.js.map +1 -0
  91. package/dist/commands/dev.d.ts +13 -0
  92. package/dist/commands/dev.js +112 -0
  93. package/dist/commands/dev.js.map +1 -0
  94. package/dist/commands/start.d.ts +10 -0
  95. package/dist/commands/start.js +76 -0
  96. package/dist/commands/start.js.map +1 -0
  97. package/dist/commands/telemetry/disable.d.ts +5 -0
  98. package/dist/commands/telemetry/disable.js +12 -0
  99. package/dist/commands/telemetry/disable.js.map +1 -0
  100. package/dist/commands/telemetry/enable.d.ts +5 -0
  101. package/dist/commands/telemetry/enable.js +12 -0
  102. package/dist/commands/telemetry/enable.js.map +1 -0
  103. package/dist/commands/telemetry/status.d.ts +5 -0
  104. package/dist/commands/telemetry/status.js +12 -0
  105. package/dist/commands/telemetry/status.js.map +1 -0
  106. package/dist/server/admin.d.ts +79 -0
  107. package/dist/server/admin.js +239 -0
  108. package/dist/server/admin.js.map +1 -0
  109. package/dist/server/admin.test.d.ts +1 -0
  110. package/dist/server/admin.test.js +226 -0
  111. package/dist/server/admin.test.js.map +1 -0
  112. package/dist/server/analytics.d.ts +60 -0
  113. package/dist/server/analytics.js +168 -0
  114. package/dist/server/analytics.js.map +1 -0
  115. package/dist/server/analytics.test.d.ts +1 -0
  116. package/dist/server/analytics.test.js +179 -0
  117. package/dist/server/analytics.test.js.map +1 -0
  118. package/dist/server/asset-base-url-transform-plugin.d.ts +11 -0
  119. package/dist/server/asset-base-url-transform-plugin.js +48 -0
  120. package/dist/server/asset-base-url-transform-plugin.js.map +1 -0
  121. package/dist/server/asset-base-url-transform-plugin.test.d.ts +1 -0
  122. package/dist/server/asset-base-url-transform-plugin.test.js +134 -0
  123. package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -0
  124. package/dist/server/auth.d.ts +20 -0
  125. package/dist/server/auth.js +28 -0
  126. package/dist/server/auth.js.map +1 -0
  127. package/dist/server/build-manifest.test.d.ts +1 -0
  128. package/dist/server/build-manifest.test.js +27 -0
  129. package/dist/server/build-manifest.test.js.map +1 -0
  130. package/dist/server/config/config.test.d.ts +1 -0
  131. package/dist/server/config/config.test.js +214 -0
  132. package/dist/server/config/config.test.js.map +1 -0
  133. package/dist/server/config/index.d.ts +3 -0
  134. package/dist/server/config/index.js +4 -0
  135. package/dist/server/config/index.js.map +1 -0
  136. package/dist/server/config/resolve.d.ts +73 -0
  137. package/dist/server/config/resolve.js +167 -0
  138. package/dist/server/config/resolve.js.map +1 -0
  139. package/dist/server/config/router.d.ts +23 -0
  140. package/dist/server/config/router.js +119 -0
  141. package/dist/server/config/router.js.map +1 -0
  142. package/dist/server/config/schema.d.ts +78 -0
  143. package/dist/server/config/schema.js +158 -0
  144. package/dist/server/config/schema.js.map +1 -0
  145. package/dist/server/content-helpers.d.ts +67 -0
  146. package/dist/server/content-helpers.js +79 -0
  147. package/dist/server/content-helpers.js.map +1 -0
  148. package/dist/server/content-helpers.test.d.ts +1 -0
  149. package/dist/server/content-helpers.test.js +70 -0
  150. package/dist/server/content-helpers.test.js.map +1 -0
  151. package/dist/server/express.d.ts +11 -0
  152. package/dist/server/express.js +129 -0
  153. package/dist/server/express.js.map +1 -0
  154. package/dist/server/express.test.d.ts +1 -0
  155. package/dist/server/express.test.js +464 -0
  156. package/dist/server/express.test.js.map +1 -0
  157. package/dist/server/file-ref.d.ts +28 -0
  158. package/dist/server/file-ref.js +27 -0
  159. package/dist/server/file-ref.js.map +1 -0
  160. package/dist/server/index.d.ts +17 -0
  161. package/dist/server/index.js +14 -0
  162. package/dist/server/index.js.map +1 -0
  163. package/dist/server/inferUtilityTypes.d.ts +64 -0
  164. package/dist/server/inferUtilityTypes.js +2 -0
  165. package/dist/server/inferUtilityTypes.js.map +1 -0
  166. package/dist/server/log-sink.d.ts +16 -0
  167. package/dist/server/log-sink.js +66 -0
  168. package/dist/server/log-sink.js.map +1 -0
  169. package/dist/server/metric.d.ts +12 -0
  170. package/dist/server/metric.js +13 -0
  171. package/dist/server/metric.js.map +1 -0
  172. package/dist/server/middleware.d.ts +137 -0
  173. package/dist/server/middleware.js +93 -0
  174. package/dist/server/middleware.js.map +1 -0
  175. package/dist/server/middleware.test-d.d.ts +1 -0
  176. package/dist/server/middleware.test-d.js +75 -0
  177. package/dist/server/middleware.test-d.js.map +1 -0
  178. package/dist/server/middleware.test.d.ts +1 -0
  179. package/dist/server/middleware.test.js +493 -0
  180. package/dist/server/middleware.test.js.map +1 -0
  181. package/dist/server/mock-seed.d.ts +62 -0
  182. package/dist/server/mock-seed.js +251 -0
  183. package/dist/server/mock-seed.js.map +1 -0
  184. package/dist/server/mock-seed.test.d.ts +1 -0
  185. package/dist/server/mock-seed.test.js +122 -0
  186. package/dist/server/mock-seed.test.js.map +1 -0
  187. package/dist/server/observability.d.ts +149 -0
  188. package/dist/server/observability.js +340 -0
  189. package/dist/server/observability.js.map +1 -0
  190. package/dist/server/observability.test.d.ts +1 -0
  191. package/dist/server/observability.test.js +251 -0
  192. package/dist/server/observability.test.js.map +1 -0
  193. package/dist/server/otel.d.ts +45 -0
  194. package/dist/server/otel.js +117 -0
  195. package/dist/server/otel.js.map +1 -0
  196. package/dist/server/otel.test.d.ts +1 -0
  197. package/dist/server/otel.test.js +122 -0
  198. package/dist/server/otel.test.js.map +1 -0
  199. package/dist/server/server.d.ts +422 -0
  200. package/dist/server/server.js +684 -0
  201. package/dist/server/server.js.map +1 -0
  202. package/dist/server/storage/index.d.ts +23 -0
  203. package/dist/server/storage/index.js +46 -0
  204. package/dist/server/storage/index.js.map +1 -0
  205. package/dist/server/storage/memory.d.ts +30 -0
  206. package/dist/server/storage/memory.js +98 -0
  207. package/dist/server/storage/memory.js.map +1 -0
  208. package/dist/server/storage/memory.test.d.ts +1 -0
  209. package/dist/server/storage/memory.test.js +81 -0
  210. package/dist/server/storage/memory.test.js.map +1 -0
  211. package/dist/server/storage/postgres.d.ts +65 -0
  212. package/dist/server/storage/postgres.js +242 -0
  213. package/dist/server/storage/postgres.js.map +1 -0
  214. package/dist/server/storage/postgres.test.d.ts +1 -0
  215. package/dist/server/storage/postgres.test.js +182 -0
  216. package/dist/server/storage/postgres.test.js.map +1 -0
  217. package/dist/server/storage/sqlite.d.ts +33 -0
  218. package/dist/server/storage/sqlite.js +250 -0
  219. package/dist/server/storage/sqlite.js.map +1 -0
  220. package/dist/server/storage/sqlite.test.d.ts +1 -0
  221. package/dist/server/storage/sqlite.test.js +133 -0
  222. package/dist/server/storage/sqlite.test.js.map +1 -0
  223. package/dist/server/storage/types.d.ts +119 -0
  224. package/dist/server/storage/types.js +11 -0
  225. package/dist/server/storage/types.js.map +1 -0
  226. package/dist/server/templateHelper.d.ts +16 -0
  227. package/dist/server/templateHelper.js +11 -0
  228. package/dist/server/templateHelper.js.map +1 -0
  229. package/dist/server/templates.generated.d.ts +4 -0
  230. package/dist/server/templates.generated.js +47 -0
  231. package/dist/server/templates.generated.js.map +1 -0
  232. package/dist/server/tunnel-proxy-router.d.ts +7 -0
  233. package/dist/server/tunnel-proxy-router.js +110 -0
  234. package/dist/server/tunnel-proxy-router.js.map +1 -0
  235. package/dist/server/tunnel-proxy-router.test.d.ts +1 -0
  236. package/dist/server/tunnel-proxy-router.test.js +229 -0
  237. package/dist/server/tunnel-proxy-router.test.js.map +1 -0
  238. package/dist/server/viewsDevServer.d.ts +14 -0
  239. package/dist/server/viewsDevServer.js +45 -0
  240. package/dist/server/viewsDevServer.js.map +1 -0
  241. package/dist/test/utils.d.ts +127 -0
  242. package/dist/test/utils.js +247 -0
  243. package/dist/test/utils.js.map +1 -0
  244. package/dist/test/view.test.d.ts +1 -0
  245. package/dist/test/view.test.js +568 -0
  246. package/dist/test/view.test.js.map +1 -0
  247. package/dist/version.d.ts +1 -0
  248. package/dist/version.js +3 -0
  249. package/dist/version.js.map +1 -0
  250. package/dist/web/bridges/apps-sdk/adaptor.d.ts +54 -0
  251. package/dist/web/bridges/apps-sdk/adaptor.js +164 -0
  252. package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -0
  253. package/dist/web/bridges/apps-sdk/bridge.d.ts +11 -0
  254. package/dist/web/bridges/apps-sdk/bridge.js +47 -0
  255. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -0
  256. package/dist/web/bridges/apps-sdk/index.d.ts +5 -0
  257. package/dist/web/bridges/apps-sdk/index.js +5 -0
  258. package/dist/web/bridges/apps-sdk/index.js.map +1 -0
  259. package/dist/web/bridges/apps-sdk/types.d.ts +147 -0
  260. package/dist/web/bridges/apps-sdk/types.js +10 -0
  261. package/dist/web/bridges/apps-sdk/types.js.map +1 -0
  262. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +13 -0
  263. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +18 -0
  264. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -0
  265. package/dist/web/bridges/get-adaptor.d.ts +9 -0
  266. package/dist/web/bridges/get-adaptor.js +15 -0
  267. package/dist/web/bridges/get-adaptor.js.map +1 -0
  268. package/dist/web/bridges/index.d.ts +5 -0
  269. package/dist/web/bridges/index.js +6 -0
  270. package/dist/web/bridges/index.js.map +1 -0
  271. package/dist/web/bridges/mcp-app/adaptor.d.ts +81 -0
  272. package/dist/web/bridges/mcp-app/adaptor.js +346 -0
  273. package/dist/web/bridges/mcp-app/adaptor.js.map +1 -0
  274. package/dist/web/bridges/mcp-app/bridge.d.ts +28 -0
  275. package/dist/web/bridges/mcp-app/bridge.js +124 -0
  276. package/dist/web/bridges/mcp-app/bridge.js.map +1 -0
  277. package/dist/web/bridges/mcp-app/index.d.ts +4 -0
  278. package/dist/web/bridges/mcp-app/index.js +4 -0
  279. package/dist/web/bridges/mcp-app/index.js.map +1 -0
  280. package/dist/web/bridges/mcp-app/types.d.ts +8 -0
  281. package/dist/web/bridges/mcp-app/types.js +2 -0
  282. package/dist/web/bridges/mcp-app/types.js.map +1 -0
  283. package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +19 -0
  284. package/dist/web/bridges/mcp-app/use-mcp-app-context.js +19 -0
  285. package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -0
  286. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.d.ts +1 -0
  287. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +26 -0
  288. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -0
  289. package/dist/web/bridges/mcp-app/view-tools.test.d.ts +1 -0
  290. package/dist/web/bridges/mcp-app/view-tools.test.js +144 -0
  291. package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -0
  292. package/dist/web/bridges/types.d.ts +243 -0
  293. package/dist/web/bridges/types.js +2 -0
  294. package/dist/web/bridges/types.js.map +1 -0
  295. package/dist/web/bridges/use-host-context.d.ts +7 -0
  296. package/dist/web/bridges/use-host-context.js +13 -0
  297. package/dist/web/bridges/use-host-context.js.map +1 -0
  298. package/dist/web/components/modal-provider.d.ts +4 -0
  299. package/dist/web/components/modal-provider.js +45 -0
  300. package/dist/web/components/modal-provider.js.map +1 -0
  301. package/dist/web/create-store.d.ts +29 -0
  302. package/dist/web/create-store.js +64 -0
  303. package/dist/web/create-store.js.map +1 -0
  304. package/dist/web/create-store.test.d.ts +1 -0
  305. package/dist/web/create-store.test.js +129 -0
  306. package/dist/web/create-store.test.js.map +1 -0
  307. package/dist/web/data-llm.d.ts +47 -0
  308. package/dist/web/data-llm.js +100 -0
  309. package/dist/web/data-llm.js.map +1 -0
  310. package/dist/web/data-llm.test.d.ts +1 -0
  311. package/dist/web/data-llm.test.js +142 -0
  312. package/dist/web/data-llm.test.js.map +1 -0
  313. package/dist/web/generate-helpers.d.ts +120 -0
  314. package/dist/web/generate-helpers.js +115 -0
  315. package/dist/web/generate-helpers.js.map +1 -0
  316. package/dist/web/generate-helpers.test-d.d.ts +1 -0
  317. package/dist/web/generate-helpers.test-d.js +211 -0
  318. package/dist/web/generate-helpers.test-d.js.map +1 -0
  319. package/dist/web/generate-helpers.test.d.ts +1 -0
  320. package/dist/web/generate-helpers.test.js +17 -0
  321. package/dist/web/generate-helpers.test.js.map +1 -0
  322. package/dist/web/helpers/state.d.ts +7 -0
  323. package/dist/web/helpers/state.js +45 -0
  324. package/dist/web/helpers/state.js.map +1 -0
  325. package/dist/web/helpers/state.test.d.ts +1 -0
  326. package/dist/web/helpers/state.test.js +53 -0
  327. package/dist/web/helpers/state.test.js.map +1 -0
  328. package/dist/web/hooks/index.d.ts +17 -0
  329. package/dist/web/hooks/index.js +18 -0
  330. package/dist/web/hooks/index.js.map +1 -0
  331. package/dist/web/hooks/test/utils.d.ts +20 -0
  332. package/dist/web/hooks/test/utils.js +75 -0
  333. package/dist/web/hooks/test/utils.js.map +1 -0
  334. package/dist/web/hooks/use-call-tool.d.ts +146 -0
  335. package/dist/web/hooks/use-call-tool.js +96 -0
  336. package/dist/web/hooks/use-call-tool.js.map +1 -0
  337. package/dist/web/hooks/use-call-tool.test-d.d.ts +1 -0
  338. package/dist/web/hooks/use-call-tool.test-d.js +104 -0
  339. package/dist/web/hooks/use-call-tool.test-d.js.map +1 -0
  340. package/dist/web/hooks/use-call-tool.test.d.ts +1 -0
  341. package/dist/web/hooks/use-call-tool.test.js +211 -0
  342. package/dist/web/hooks/use-call-tool.test.js.map +1 -0
  343. package/dist/web/hooks/use-display-mode.d.ts +24 -0
  344. package/dist/web/hooks/use-display-mode.js +29 -0
  345. package/dist/web/hooks/use-display-mode.js.map +1 -0
  346. package/dist/web/hooks/use-display-mode.test-d.d.ts +1 -0
  347. package/dist/web/hooks/use-display-mode.test-d.js +8 -0
  348. package/dist/web/hooks/use-display-mode.test-d.js.map +1 -0
  349. package/dist/web/hooks/use-display-mode.test.d.ts +1 -0
  350. package/dist/web/hooks/use-display-mode.test.js +41 -0
  351. package/dist/web/hooks/use-display-mode.test.js.map +1 -0
  352. package/dist/web/hooks/use-download.d.ts +5 -0
  353. package/dist/web/hooks/use-download.js +8 -0
  354. package/dist/web/hooks/use-download.js.map +1 -0
  355. package/dist/web/hooks/use-download.test.d.ts +1 -0
  356. package/dist/web/hooks/use-download.test.js +95 -0
  357. package/dist/web/hooks/use-download.test.js.map +1 -0
  358. package/dist/web/hooks/use-files.d.ts +39 -0
  359. package/dist/web/hooks/use-files.js +42 -0
  360. package/dist/web/hooks/use-files.js.map +1 -0
  361. package/dist/web/hooks/use-files.test.d.ts +1 -0
  362. package/dist/web/hooks/use-files.test.js +54 -0
  363. package/dist/web/hooks/use-files.test.js.map +1 -0
  364. package/dist/web/hooks/use-intent.d.ts +30 -0
  365. package/dist/web/hooks/use-intent.js +34 -0
  366. package/dist/web/hooks/use-intent.js.map +1 -0
  367. package/dist/web/hooks/use-intent.test.d.ts +1 -0
  368. package/dist/web/hooks/use-intent.test.js +85 -0
  369. package/dist/web/hooks/use-intent.test.js.map +1 -0
  370. package/dist/web/hooks/use-layout.d.ts +24 -0
  371. package/dist/web/hooks/use-layout.js +25 -0
  372. package/dist/web/hooks/use-layout.js.map +1 -0
  373. package/dist/web/hooks/use-layout.test.d.ts +1 -0
  374. package/dist/web/hooks/use-layout.test.js +96 -0
  375. package/dist/web/hooks/use-layout.test.js.map +1 -0
  376. package/dist/web/hooks/use-notify.d.ts +29 -0
  377. package/dist/web/hooks/use-notify.js +33 -0
  378. package/dist/web/hooks/use-notify.js.map +1 -0
  379. package/dist/web/hooks/use-notify.test.d.ts +1 -0
  380. package/dist/web/hooks/use-notify.test.js +105 -0
  381. package/dist/web/hooks/use-notify.test.js.map +1 -0
  382. package/dist/web/hooks/use-open-external.d.ts +20 -0
  383. package/dist/web/hooks/use-open-external.js +24 -0
  384. package/dist/web/hooks/use-open-external.js.map +1 -0
  385. package/dist/web/hooks/use-open-external.test.d.ts +1 -0
  386. package/dist/web/hooks/use-open-external.test.js +65 -0
  387. package/dist/web/hooks/use-open-external.test.js.map +1 -0
  388. package/dist/web/hooks/use-register-view-tool.d.ts +38 -0
  389. package/dist/web/hooks/use-register-view-tool.js +50 -0
  390. package/dist/web/hooks/use-register-view-tool.js.map +1 -0
  391. package/dist/web/hooks/use-request-close.d.ts +16 -0
  392. package/dist/web/hooks/use-request-close.js +21 -0
  393. package/dist/web/hooks/use-request-close.js.map +1 -0
  394. package/dist/web/hooks/use-request-close.test.d.ts +1 -0
  395. package/dist/web/hooks/use-request-close.test.js +52 -0
  396. package/dist/web/hooks/use-request-close.test.js.map +1 -0
  397. package/dist/web/hooks/use-request-modal.d.ts +24 -0
  398. package/dist/web/hooks/use-request-modal.js +31 -0
  399. package/dist/web/hooks/use-request-modal.js.map +1 -0
  400. package/dist/web/hooks/use-request-modal.test.d.ts +1 -0
  401. package/dist/web/hooks/use-request-modal.test.js +61 -0
  402. package/dist/web/hooks/use-request-modal.test.js.map +1 -0
  403. package/dist/web/hooks/use-request-size.d.ts +20 -0
  404. package/dist/web/hooks/use-request-size.js +24 -0
  405. package/dist/web/hooks/use-request-size.js.map +1 -0
  406. package/dist/web/hooks/use-request-size.test.d.ts +1 -0
  407. package/dist/web/hooks/use-request-size.test.js +65 -0
  408. package/dist/web/hooks/use-request-size.test.js.map +1 -0
  409. package/dist/web/hooks/use-send-follow-up-message.d.ts +19 -0
  410. package/dist/web/hooks/use-send-follow-up-message.js +25 -0
  411. package/dist/web/hooks/use-send-follow-up-message.js.map +1 -0
  412. package/dist/web/hooks/use-set-open-in-app-url.d.ts +18 -0
  413. package/dist/web/hooks/use-set-open-in-app-url.js +25 -0
  414. package/dist/web/hooks/use-set-open-in-app-url.js.map +1 -0
  415. package/dist/web/hooks/use-set-open-in-app-url.test.d.ts +1 -0
  416. package/dist/web/hooks/use-set-open-in-app-url.test.js +43 -0
  417. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -0
  418. package/dist/web/hooks/use-tool-info.d.ts +87 -0
  419. package/dist/web/hooks/use-tool-info.js +49 -0
  420. package/dist/web/hooks/use-tool-info.js.map +1 -0
  421. package/dist/web/hooks/use-tool-info.test-d.d.ts +1 -0
  422. package/dist/web/hooks/use-tool-info.test-d.js +91 -0
  423. package/dist/web/hooks/use-tool-info.test-d.js.map +1 -0
  424. package/dist/web/hooks/use-tool-info.test.d.ts +1 -0
  425. package/dist/web/hooks/use-tool-info.test.js +130 -0
  426. package/dist/web/hooks/use-tool-info.test.js.map +1 -0
  427. package/dist/web/hooks/use-user.d.ts +20 -0
  428. package/dist/web/hooks/use-user.js +37 -0
  429. package/dist/web/hooks/use-user.js.map +1 -0
  430. package/dist/web/hooks/use-user.test.d.ts +1 -0
  431. package/dist/web/hooks/use-user.test.js +122 -0
  432. package/dist/web/hooks/use-user.test.js.map +1 -0
  433. package/dist/web/hooks/use-view-state.d.ts +25 -0
  434. package/dist/web/hooks/use-view-state.js +32 -0
  435. package/dist/web/hooks/use-view-state.js.map +1 -0
  436. package/dist/web/hooks/use-view-state.test.d.ts +1 -0
  437. package/dist/web/hooks/use-view-state.test.js +177 -0
  438. package/dist/web/hooks/use-view-state.test.js.map +1 -0
  439. package/dist/web/index.d.ts +7 -0
  440. package/dist/web/index.js +8 -0
  441. package/dist/web/index.js.map +1 -0
  442. package/dist/web/mount-view.d.ts +20 -0
  443. package/dist/web/mount-view.js +46 -0
  444. package/dist/web/mount-view.js.map +1 -0
  445. package/dist/web/plugin/data-llm.test.d.ts +1 -0
  446. package/dist/web/plugin/data-llm.test.js +81 -0
  447. package/dist/web/plugin/data-llm.test.js.map +1 -0
  448. package/dist/web/plugin/plugin.d.ts +33 -0
  449. package/dist/web/plugin/plugin.js +189 -0
  450. package/dist/web/plugin/plugin.js.map +1 -0
  451. package/dist/web/plugin/scan-views.d.ts +16 -0
  452. package/dist/web/plugin/scan-views.js +88 -0
  453. package/dist/web/plugin/scan-views.js.map +1 -0
  454. package/dist/web/plugin/scan-views.test.d.ts +1 -0
  455. package/dist/web/plugin/scan-views.test.js +99 -0
  456. package/dist/web/plugin/scan-views.test.js.map +1 -0
  457. package/dist/web/plugin/transform-data-llm.d.ts +12 -0
  458. package/dist/web/plugin/transform-data-llm.js +96 -0
  459. package/dist/web/plugin/transform-data-llm.js.map +1 -0
  460. package/dist/web/plugin/transform-data-llm.test.d.ts +1 -0
  461. package/dist/web/plugin/transform-data-llm.test.js +81 -0
  462. package/dist/web/plugin/transform-data-llm.test.js.map +1 -0
  463. package/dist/web/plugin/validate-view.d.ts +1 -0
  464. package/dist/web/plugin/validate-view.js +9 -0
  465. package/dist/web/plugin/validate-view.js.map +1 -0
  466. package/dist/web/plugin/validate-view.test.d.ts +1 -0
  467. package/dist/web/plugin/validate-view.test.js +24 -0
  468. package/dist/web/plugin/validate-view.test.js.map +1 -0
  469. package/dist/web/proxy.d.ts +1 -0
  470. package/dist/web/proxy.js +52 -0
  471. package/dist/web/proxy.js.map +1 -0
  472. package/dist/web/types.d.ts +20 -0
  473. package/dist/web/types.js +2 -0
  474. package/dist/web/types.js.map +1 -0
  475. package/package.json +125 -0
  476. package/scripts/postinstall.mjs +45 -0
  477. package/tsconfig.base.json +36 -0
@@ -0,0 +1,168 @@
1
+ import { setActiveStorage } from "./log-sink.js";
2
+ import { seedMockData } from "./mock-seed.js";
3
+ import { initOtel } from "./otel.js";
4
+ import { resolveStorageAdapter } from "./storage/index.js";
5
+ import { MemoryStorageAdapter } from "./storage/memory.js";
6
+ /**
7
+ * Resolve a storage adapter for the session. In `--mock` mode we force an
8
+ * in-memory adapter (the demo seed must never touch disk and must vanish on
9
+ * exit); otherwise the configured `ENPILINK_STORAGE` adapter is used.
10
+ */
11
+ function resolveSessionStorage(mock) {
12
+ return mock ? new MemoryStorageAdapter() : resolveStorageAdapter();
13
+ }
14
+ /** Truthy values that enable analytics via {@link analyticsEnabled}. */
15
+ const TRUTHY = new Set(["1", "true", "yes", "on"]);
16
+ /**
17
+ * Whether analytics is enabled. OFF by default; enable with
18
+ * `ENPILINK_ANALYTICS=1` (also accepts `true`/`yes`/`on`, case-insensitive).
19
+ */
20
+ export function analyticsEnabled() {
21
+ const raw = process.env.ENPILINK_ANALYTICS;
22
+ return raw !== undefined && TRUTHY.has(raw.trim().toLowerCase());
23
+ }
24
+ /**
25
+ * Whether the `--mock` demo seed is enabled (`ENPILINK_MOCK`=1/true/yes/on).
26
+ * Mock mode is opt-in only and IMPLIES analytics-on + in-memory storage for the
27
+ * session, so the Dashboard renders full demo data with NO real traffic. It
28
+ * NEVER touches disk and is never on by default.
29
+ */
30
+ export function mockEnabled() {
31
+ // The demo seed is DEV-ONLY: it must never seed a real deployment. In
32
+ // production `ENPILINK_MOCK` is ignored entirely (read the literal so the
33
+ // guard survives DCE and is unambiguous).
34
+ if (process.env.NODE_ENV === "production") {
35
+ return false;
36
+ }
37
+ const raw = process.env.ENPILINK_MOCK;
38
+ return raw !== undefined && TRUTHY.has(raw.trim().toLowerCase());
39
+ }
40
+ /**
41
+ * Extract the tool name from a `tools/call` request's params. Returns
42
+ * `undefined` for non-`tools/call` methods or malformed params, never throws.
43
+ */
44
+ function toolNameOf(method, params) {
45
+ if (method !== "tools/call") {
46
+ return undefined;
47
+ }
48
+ const name = params?.name;
49
+ return typeof name === "string" ? name : undefined;
50
+ }
51
+ /**
52
+ * Build the analytics middleware entry. Times each request around `next()`,
53
+ * records a `tool_call`-typed event (capturing the tool name for `tools/call`),
54
+ * and ALWAYS swallows storage errors so a storage failure can never break or
55
+ * slow a tool call. Recording is fire-and-forget (non-blocking).
56
+ */
57
+ export function createAnalyticsMiddleware(storage, now = Date.now, otel = null) {
58
+ return async (request, _extra, next) => {
59
+ const start = now();
60
+ const tool = toolNameOf(request.method, request.params);
61
+ let ok = true;
62
+ let error;
63
+ try {
64
+ const result = await next();
65
+ // A tool result with `isError: true` is a soft (handled) failure.
66
+ if (result &&
67
+ typeof result === "object" &&
68
+ result.isError === true) {
69
+ ok = false;
70
+ }
71
+ return result;
72
+ }
73
+ catch (err) {
74
+ // Record the failure, then rethrow so behavior is unchanged.
75
+ ok = false;
76
+ error = err instanceof Error ? err.message : String(err);
77
+ throw err;
78
+ }
79
+ finally {
80
+ const ms = now() - start;
81
+ const event = {
82
+ ts: start,
83
+ type: "tool_call",
84
+ tool,
85
+ method: request.method,
86
+ ms,
87
+ ok,
88
+ error,
89
+ };
90
+ // Fire-and-forget; never block or throw into the request path.
91
+ void recordSafely(storage, event);
92
+ // Optional OTel export — guarded, synchronous, error-swallowing.
93
+ if (otel) {
94
+ try {
95
+ otel.record(event);
96
+ }
97
+ catch {
98
+ // OTel export must never break or slow a tool call.
99
+ }
100
+ }
101
+ }
102
+ };
103
+ }
104
+ /** Record an event, swallowing any storage error. */
105
+ async function recordSafely(storage, event) {
106
+ try {
107
+ await storage.recordEvent(event);
108
+ }
109
+ catch {
110
+ // A storage failure must never break or slow a tool call.
111
+ }
112
+ }
113
+ /**
114
+ * Install analytics on a server, ONLY when enabled (`ENPILINK_ANALYTICS`).
115
+ *
116
+ * When enabled: resolves a {@link StorageAdapter} via `resolveStorageAdapter()`
117
+ * (`ENPILINK_STORAGE` / `ENPILINK_DB_PATH`), `init()`s it, registers it as the
118
+ * active storage for the log sink + `getActiveStorage()`, and returns the
119
+ * built analytics middleware entry to splice into the chain.
120
+ *
121
+ * When disabled: resolves/initializes NOTHING and returns `null` — zero
122
+ * overhead, zero network, no `enpilink.db` created.
123
+ *
124
+ * @returns the active storage + middleware entry, or `null` when disabled.
125
+ */
126
+ export async function installAnalytics(opts = {}) {
127
+ // `--mock` (ENPILINK_MOCK) force-enables analytics for the session even when
128
+ // the env-based gating is off, so demos work without setting both flags.
129
+ const mock = mockEnabled();
130
+ if (!mock && !analyticsEnabled()) {
131
+ return null;
132
+ }
133
+ let storage;
134
+ try {
135
+ storage = resolveSessionStorage(mock);
136
+ await storage.init();
137
+ }
138
+ catch (err) {
139
+ // Never let an analytics/storage failure break server startup.
140
+ console.error("[enpilink] analytics disabled: storage init failed:", err instanceof Error ? err.message : err);
141
+ return null;
142
+ }
143
+ setActiveStorage(storage);
144
+ // In `--mock` mode, seed the in-memory storage with a deterministic demo
145
+ // dataset so the Dashboard renders full immediately (no real traffic).
146
+ // Determinism: a fixed seed + a base timestamp captured once here.
147
+ if (mock) {
148
+ const now = (opts.now ?? Date.now)();
149
+ try {
150
+ await seedMockData(storage, { now });
151
+ }
152
+ catch {
153
+ // A seeding failure must never break server startup.
154
+ }
155
+ }
156
+ // Optional OTel export (M6): off by default, zero network/imports when unset.
157
+ // Initialized once here and fed by the middleware alongside storage. Mock mode
158
+ // never exports (it's dev-only demo data); requires the explicit env opt-in.
159
+ const otel = mock ? null : await initOtel();
160
+ const entry = {
161
+ // "request" matches every (non-notification) request so non-tool methods
162
+ // are counted too; the handler captures the tool name for tools/call.
163
+ filter: "request",
164
+ handler: createAnalyticsMiddleware(storage, opts.now, otel),
165
+ };
166
+ return { storage, entry, otel };
167
+ }
168
+ //# sourceMappingURL=analytics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/server/analytics.ts"],"names":[],"mappings":"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","sourcesContent":["import { setActiveStorage } from \"./log-sink.js\";\nimport type { McpMiddlewareEntry, McpMiddlewareFn } from \"./middleware.js\";\nimport { seedMockData } from \"./mock-seed.js\";\nimport { initOtel, type OtelSink } from \"./otel.js\";\nimport { resolveStorageAdapter } from \"./storage/index.js\";\nimport { MemoryStorageAdapter } from \"./storage/memory.js\";\nimport type { StorageAdapter } from \"./storage/types.js\";\n\n/**\n * Analytics + log capture (M2). Opt-in, env-gated, zero overhead when off.\n *\n * When enabled, a single {@link StorageAdapter} is resolved + `init()`ed at\n * server startup and shared in-process: the analytics middleware writes\n * `tool_call` events to it, the log sink mirrors server logs to it, and (M3)\n * the observability API reads from the very same instance via the\n * `server.storage` getter or {@link getActiveStorage}.\n *\n * Gating: OFF unless `ENPILINK_ANALYTICS` is `1` or `true` (case-insensitive).\n * When OFF, no adapter is resolved or initialized (so no `enpilink.db` is\n * created), no middleware is registered, and there is zero network activity.\n */\n\n/** Options for {@link installAnalytics}. */\nexport interface InstallAnalyticsOptions {\n /**\n * Inject a clock for deterministic tests. Defaults to `Date.now`.\n */\n now?: () => number;\n}\n\n/**\n * Resolve a storage adapter for the session. In `--mock` mode we force an\n * in-memory adapter (the demo seed must never touch disk and must vanish on\n * exit); otherwise the configured `ENPILINK_STORAGE` adapter is used.\n */\nfunction resolveSessionStorage(mock: boolean): StorageAdapter {\n return mock ? new MemoryStorageAdapter() : resolveStorageAdapter();\n}\n\n/** Truthy values that enable analytics via {@link analyticsEnabled}. */\nconst TRUTHY = new Set([\"1\", \"true\", \"yes\", \"on\"]);\n\n/**\n * Whether analytics is enabled. OFF by default; enable with\n * `ENPILINK_ANALYTICS=1` (also accepts `true`/`yes`/`on`, case-insensitive).\n */\nexport function analyticsEnabled(): boolean {\n const raw = process.env.ENPILINK_ANALYTICS;\n return raw !== undefined && TRUTHY.has(raw.trim().toLowerCase());\n}\n\n/**\n * Whether the `--mock` demo seed is enabled (`ENPILINK_MOCK`=1/true/yes/on).\n * Mock mode is opt-in only and IMPLIES analytics-on + in-memory storage for the\n * session, so the Dashboard renders full demo data with NO real traffic. It\n * NEVER touches disk and is never on by default.\n */\nexport function mockEnabled(): boolean {\n // The demo seed is DEV-ONLY: it must never seed a real deployment. In\n // production `ENPILINK_MOCK` is ignored entirely (read the literal so the\n // guard survives DCE and is unambiguous).\n if (process.env.NODE_ENV === \"production\") {\n return false;\n }\n const raw = process.env.ENPILINK_MOCK;\n return raw !== undefined && TRUTHY.has(raw.trim().toLowerCase());\n}\n\n/**\n * Extract the tool name from a `tools/call` request's params. Returns\n * `undefined` for non-`tools/call` methods or malformed params, never throws.\n */\nfunction toolNameOf(\n method: string,\n params: Record<string, unknown>,\n): string | undefined {\n if (method !== \"tools/call\") {\n return undefined;\n }\n const name = params?.name;\n return typeof name === \"string\" ? name : undefined;\n}\n\n/**\n * Build the analytics middleware entry. Times each request around `next()`,\n * records a `tool_call`-typed event (capturing the tool name for `tools/call`),\n * and ALWAYS swallows storage errors so a storage failure can never break or\n * slow a tool call. Recording is fire-and-forget (non-blocking).\n */\nexport function createAnalyticsMiddleware(\n storage: StorageAdapter,\n now: () => number = Date.now,\n otel: OtelSink | null = null,\n): McpMiddlewareFn {\n return async (request, _extra, next) => {\n const start = now();\n const tool = toolNameOf(request.method, request.params);\n let ok = true;\n let error: string | undefined;\n\n try {\n const result = await next();\n // A tool result with `isError: true` is a soft (handled) failure.\n if (\n result &&\n typeof result === \"object\" &&\n (result as { isError?: unknown }).isError === true\n ) {\n ok = false;\n }\n return result;\n } catch (err) {\n // Record the failure, then rethrow so behavior is unchanged.\n ok = false;\n error = err instanceof Error ? err.message : String(err);\n throw err;\n } finally {\n const ms = now() - start;\n const event = {\n ts: start,\n type: \"tool_call\",\n tool,\n method: request.method,\n ms,\n ok,\n error,\n } as const;\n // Fire-and-forget; never block or throw into the request path.\n void recordSafely(storage, event);\n // Optional OTel export — guarded, synchronous, error-swallowing.\n if (otel) {\n try {\n otel.record(event);\n } catch {\n // OTel export must never break or slow a tool call.\n }\n }\n }\n };\n}\n\n/** Record an event, swallowing any storage error. */\nasync function recordSafely(\n storage: StorageAdapter,\n event: Parameters<StorageAdapter[\"recordEvent\"]>[0],\n): Promise<void> {\n try {\n await storage.recordEvent(event);\n } catch {\n // A storage failure must never break or slow a tool call.\n }\n}\n\n/**\n * Install analytics on a server, ONLY when enabled (`ENPILINK_ANALYTICS`).\n *\n * When enabled: resolves a {@link StorageAdapter} via `resolveStorageAdapter()`\n * (`ENPILINK_STORAGE` / `ENPILINK_DB_PATH`), `init()`s it, registers it as the\n * active storage for the log sink + `getActiveStorage()`, and returns the\n * built analytics middleware entry to splice into the chain.\n *\n * When disabled: resolves/initializes NOTHING and returns `null` — zero\n * overhead, zero network, no `enpilink.db` created.\n *\n * @returns the active storage + middleware entry, or `null` when disabled.\n */\nexport async function installAnalytics(\n opts: InstallAnalyticsOptions = {},\n): Promise<{\n storage: StorageAdapter;\n entry: McpMiddlewareEntry;\n otel: OtelSink | null;\n} | null> {\n // `--mock` (ENPILINK_MOCK) force-enables analytics for the session even when\n // the env-based gating is off, so demos work without setting both flags.\n const mock = mockEnabled();\n if (!mock && !analyticsEnabled()) {\n return null;\n }\n\n let storage: StorageAdapter;\n try {\n storage = resolveSessionStorage(mock);\n await storage.init();\n } catch (err) {\n // Never let an analytics/storage failure break server startup.\n console.error(\n \"[enpilink] analytics disabled: storage init failed:\",\n err instanceof Error ? err.message : err,\n );\n return null;\n }\n\n setActiveStorage(storage);\n\n // In `--mock` mode, seed the in-memory storage with a deterministic demo\n // dataset so the Dashboard renders full immediately (no real traffic).\n // Determinism: a fixed seed + a base timestamp captured once here.\n if (mock) {\n const now = (opts.now ?? Date.now)();\n try {\n await seedMockData(storage, { now });\n } catch {\n // A seeding failure must never break server startup.\n }\n }\n\n // Optional OTel export (M6): off by default, zero network/imports when unset.\n // Initialized once here and fed by the middleware alongside storage. Mock mode\n // never exports (it's dev-only demo data); requires the explicit env opt-in.\n const otel = mock ? null : await initOtel();\n\n const entry: McpMiddlewareEntry = {\n // \"request\" matches every (non-notification) request so non-tool methods\n // are counted too; the handler captures the tool name for tools/call.\n filter: \"request\",\n handler: createAnalyticsMiddleware(storage, opts.now, otel),\n };\n\n return { storage, entry, otel };\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,179 @@
1
+ import { existsSync } from "node:fs";
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import { analyticsEnabled, createAnalyticsMiddleware, installAnalytics, } from "./analytics.js";
4
+ import { getActiveStorage, serverLog, setActiveStorage } from "./log-sink.js";
5
+ import { MemoryStorageAdapter } from "./storage/memory.js";
6
+ /** Flush the fire-and-forget recordEvent/appendLog microtasks. */
7
+ const flush = () => new Promise((r) => setTimeout(r, 0));
8
+ /** A fixed clock: 1000 at start, +5ms on the second read. */
9
+ function fixedClock() {
10
+ const ticks = [1000, 1005];
11
+ let i = 0;
12
+ return () => ticks[Math.min(i++, ticks.length - 1)];
13
+ }
14
+ describe("analyticsEnabled", () => {
15
+ const original = process.env.ENPILINK_ANALYTICS;
16
+ afterEach(() => {
17
+ if (original === undefined) {
18
+ delete process.env.ENPILINK_ANALYTICS;
19
+ }
20
+ else {
21
+ process.env.ENPILINK_ANALYTICS = original;
22
+ }
23
+ });
24
+ it("is off by default", () => {
25
+ delete process.env.ENPILINK_ANALYTICS;
26
+ expect(analyticsEnabled()).toBe(false);
27
+ });
28
+ it("accepts 1/true/yes/on (case-insensitive)", () => {
29
+ for (const v of ["1", "true", "TRUE", "yes", "on", " True "]) {
30
+ process.env.ENPILINK_ANALYTICS = v;
31
+ expect(analyticsEnabled()).toBe(true);
32
+ }
33
+ });
34
+ it("rejects other values", () => {
35
+ for (const v of ["0", "false", "", "off", "no"]) {
36
+ process.env.ENPILINK_ANALYTICS = v;
37
+ expect(analyticsEnabled()).toBe(false);
38
+ }
39
+ });
40
+ });
41
+ describe("createAnalyticsMiddleware", () => {
42
+ let store;
43
+ beforeEach(async () => {
44
+ store = new MemoryStorageAdapter({ cap: 100 });
45
+ await store.init();
46
+ });
47
+ it("records a tool_call event on success with tool, ms, ok", async () => {
48
+ const mw = createAnalyticsMiddleware(store, fixedClock());
49
+ const result = { content: [{ type: "text", text: "hi" }] };
50
+ const ret = await mw({ method: "tools/call", params: { name: "greet" } }, undefined, async () => result);
51
+ await flush();
52
+ expect(ret).toBe(result);
53
+ const [e] = await store.queryEvents({});
54
+ expect(e).toMatchObject({
55
+ type: "tool_call",
56
+ tool: "greet",
57
+ method: "tools/call",
58
+ ms: 5,
59
+ ok: true,
60
+ });
61
+ expect(e?.ts).toBe(1000);
62
+ expect(e?.error).toBeUndefined();
63
+ });
64
+ it("records ok=false when the result has isError", async () => {
65
+ const mw = createAnalyticsMiddleware(store, fixedClock());
66
+ await mw({ method: "tools/call", params: { name: "boom" } }, undefined, async () => ({ isError: true, content: [] }));
67
+ await flush();
68
+ const [e] = await store.queryEvents({});
69
+ expect(e).toMatchObject({ tool: "boom", ok: false });
70
+ });
71
+ it("records the event AND rethrows on a thrown error", async () => {
72
+ const mw = createAnalyticsMiddleware(store, fixedClock());
73
+ await expect(mw({ method: "tools/call", params: { name: "kaboom" } }, undefined, async () => {
74
+ throw new Error("nope");
75
+ })).rejects.toThrow("nope");
76
+ await flush();
77
+ const [e] = await store.queryEvents({});
78
+ expect(e).toMatchObject({ tool: "kaboom", ok: false, error: "nope" });
79
+ });
80
+ it("captures the method but no tool for non-tools/call requests", async () => {
81
+ const mw = createAnalyticsMiddleware(store, fixedClock());
82
+ await mw({ method: "tools/list", params: {} }, undefined, async () => ({
83
+ tools: [],
84
+ }));
85
+ await flush();
86
+ const [e] = await store.queryEvents({});
87
+ expect(e?.method).toBe("tools/list");
88
+ expect(e?.tool).toBeUndefined();
89
+ });
90
+ it("never breaks the call when storage.recordEvent throws", async () => {
91
+ const broken = new MemoryStorageAdapter();
92
+ await broken.init();
93
+ broken.recordEvent = async () => {
94
+ throw new Error("storage down");
95
+ };
96
+ const mw = createAnalyticsMiddleware(broken, fixedClock());
97
+ const result = { content: [] };
98
+ await expect(mw({ method: "tools/call", params: { name: "x" } }, undefined, async () => result)).resolves.toBe(result);
99
+ await flush();
100
+ });
101
+ });
102
+ describe("installAnalytics gating", () => {
103
+ const originalAnalytics = process.env.ENPILINK_ANALYTICS;
104
+ const originalStorage = process.env.ENPILINK_STORAGE;
105
+ beforeEach(() => {
106
+ setActiveStorage(null);
107
+ });
108
+ afterEach(() => {
109
+ setActiveStorage(null);
110
+ if (originalAnalytics === undefined) {
111
+ delete process.env.ENPILINK_ANALYTICS;
112
+ }
113
+ else {
114
+ process.env.ENPILINK_ANALYTICS = originalAnalytics;
115
+ }
116
+ if (originalStorage === undefined) {
117
+ delete process.env.ENPILINK_STORAGE;
118
+ }
119
+ else {
120
+ process.env.ENPILINK_STORAGE = originalStorage;
121
+ }
122
+ });
123
+ it("returns null and creates no adapter when disabled", async () => {
124
+ delete process.env.ENPILINK_ANALYTICS;
125
+ const result = await installAnalytics();
126
+ expect(result).toBeNull();
127
+ expect(getActiveStorage()).toBeNull();
128
+ });
129
+ it("does NOT create a sqlite db file when disabled", async () => {
130
+ delete process.env.ENPILINK_ANALYTICS;
131
+ process.env.ENPILINK_STORAGE = "sqlite";
132
+ const dbPath = `${process.cwd()}/enpilink.db`;
133
+ const preexisting = existsSync(dbPath);
134
+ await installAnalytics();
135
+ if (!preexisting) {
136
+ expect(existsSync(dbPath)).toBe(false);
137
+ }
138
+ });
139
+ it("resolves + inits + registers the active storage when enabled", async () => {
140
+ process.env.ENPILINK_ANALYTICS = "1";
141
+ process.env.ENPILINK_STORAGE = "memory";
142
+ const result = await installAnalytics();
143
+ expect(result).not.toBeNull();
144
+ expect(result?.entry.filter).toBe("request");
145
+ expect(typeof result?.entry.handler).toBe("function");
146
+ expect(getActiveStorage()).toBe(result?.storage);
147
+ await result?.storage.close();
148
+ });
149
+ });
150
+ describe("serverLog capture", () => {
151
+ beforeEach(() => setActiveStorage(null));
152
+ afterEach(() => setActiveStorage(null));
153
+ it("is a no-op sink (no storage) when no active storage", () => {
154
+ expect(() => serverLog("info", "hello")).not.toThrow();
155
+ });
156
+ it("mirrors logs to the active storage when set", async () => {
157
+ const store = new MemoryStorageAdapter();
158
+ await store.init();
159
+ setActiveStorage(store);
160
+ serverLog("error", "boom", { code: 1 });
161
+ await flush();
162
+ const [l] = await store.queryLogs({});
163
+ expect(l).toMatchObject({ level: "error", msg: "boom" });
164
+ expect(l?.data).toEqual({ code: 1 });
165
+ await store.close();
166
+ });
167
+ it("swallows storage errors", async () => {
168
+ const store = new MemoryStorageAdapter();
169
+ await store.init();
170
+ store.appendLog = async () => {
171
+ throw new Error("down");
172
+ };
173
+ setActiveStorage(store);
174
+ expect(() => serverLog("info", "x")).not.toThrow();
175
+ await flush();
176
+ await store.close();
177
+ });
178
+ });
179
+ //# sourceMappingURL=analytics.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.test.js","sourceRoot":"","sources":["../../src/server/analytics.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\nimport {\n analyticsEnabled,\n createAnalyticsMiddleware,\n installAnalytics,\n} from \"./analytics.js\";\nimport { getActiveStorage, serverLog, setActiveStorage } from \"./log-sink.js\";\nimport { MemoryStorageAdapter } from \"./storage/memory.js\";\nimport type { AnalyticsEvent } from \"./storage/types.js\";\n\n/** Flush the fire-and-forget recordEvent/appendLog microtasks. */\nconst flush = () => new Promise((r) => setTimeout(r, 0));\n\n/** A fixed clock: 1000 at start, +5ms on the second read. */\nfunction fixedClock(): () => number {\n const ticks = [1000, 1005];\n let i = 0;\n return () => ticks[Math.min(i++, ticks.length - 1)] as number;\n}\n\ndescribe(\"analyticsEnabled\", () => {\n const original = process.env.ENPILINK_ANALYTICS;\n afterEach(() => {\n if (original === undefined) {\n delete process.env.ENPILINK_ANALYTICS;\n } else {\n process.env.ENPILINK_ANALYTICS = original;\n }\n });\n\n it(\"is off by default\", () => {\n delete process.env.ENPILINK_ANALYTICS;\n expect(analyticsEnabled()).toBe(false);\n });\n\n it(\"accepts 1/true/yes/on (case-insensitive)\", () => {\n for (const v of [\"1\", \"true\", \"TRUE\", \"yes\", \"on\", \" True \"]) {\n process.env.ENPILINK_ANALYTICS = v;\n expect(analyticsEnabled()).toBe(true);\n }\n });\n\n it(\"rejects other values\", () => {\n for (const v of [\"0\", \"false\", \"\", \"off\", \"no\"]) {\n process.env.ENPILINK_ANALYTICS = v;\n expect(analyticsEnabled()).toBe(false);\n }\n });\n});\n\ndescribe(\"createAnalyticsMiddleware\", () => {\n let store: MemoryStorageAdapter;\n beforeEach(async () => {\n store = new MemoryStorageAdapter({ cap: 100 });\n await store.init();\n });\n\n it(\"records a tool_call event on success with tool, ms, ok\", async () => {\n const mw = createAnalyticsMiddleware(store, fixedClock());\n const result = { content: [{ type: \"text\", text: \"hi\" }] };\n const ret = await mw(\n { method: \"tools/call\", params: { name: \"greet\" } },\n undefined,\n async () => result,\n );\n await flush();\n\n expect(ret).toBe(result);\n const [e] = await store.queryEvents({});\n expect(e).toMatchObject<Partial<AnalyticsEvent>>({\n type: \"tool_call\",\n tool: \"greet\",\n method: \"tools/call\",\n ms: 5,\n ok: true,\n });\n expect(e?.ts).toBe(1000);\n expect(e?.error).toBeUndefined();\n });\n\n it(\"records ok=false when the result has isError\", async () => {\n const mw = createAnalyticsMiddleware(store, fixedClock());\n await mw(\n { method: \"tools/call\", params: { name: \"boom\" } },\n undefined,\n async () => ({ isError: true, content: [] }),\n );\n await flush();\n const [e] = await store.queryEvents({});\n expect(e).toMatchObject({ tool: \"boom\", ok: false });\n });\n\n it(\"records the event AND rethrows on a thrown error\", async () => {\n const mw = createAnalyticsMiddleware(store, fixedClock());\n await expect(\n mw(\n { method: \"tools/call\", params: { name: \"kaboom\" } },\n undefined,\n async () => {\n throw new Error(\"nope\");\n },\n ),\n ).rejects.toThrow(\"nope\");\n await flush();\n const [e] = await store.queryEvents({});\n expect(e).toMatchObject({ tool: \"kaboom\", ok: false, error: \"nope\" });\n });\n\n it(\"captures the method but no tool for non-tools/call requests\", async () => {\n const mw = createAnalyticsMiddleware(store, fixedClock());\n await mw({ method: \"tools/list\", params: {} }, undefined, async () => ({\n tools: [],\n }));\n await flush();\n const [e] = await store.queryEvents({});\n expect(e?.method).toBe(\"tools/list\");\n expect(e?.tool).toBeUndefined();\n });\n\n it(\"never breaks the call when storage.recordEvent throws\", async () => {\n const broken = new MemoryStorageAdapter();\n await broken.init();\n broken.recordEvent = async () => {\n throw new Error(\"storage down\");\n };\n const mw = createAnalyticsMiddleware(broken, fixedClock());\n const result = { content: [] };\n await expect(\n mw(\n { method: \"tools/call\", params: { name: \"x\" } },\n undefined,\n async () => result,\n ),\n ).resolves.toBe(result);\n await flush();\n });\n});\n\ndescribe(\"installAnalytics gating\", () => {\n const originalAnalytics = process.env.ENPILINK_ANALYTICS;\n const originalStorage = process.env.ENPILINK_STORAGE;\n\n beforeEach(() => {\n setActiveStorage(null);\n });\n afterEach(() => {\n setActiveStorage(null);\n if (originalAnalytics === undefined) {\n delete process.env.ENPILINK_ANALYTICS;\n } else {\n process.env.ENPILINK_ANALYTICS = originalAnalytics;\n }\n if (originalStorage === undefined) {\n delete process.env.ENPILINK_STORAGE;\n } else {\n process.env.ENPILINK_STORAGE = originalStorage;\n }\n });\n\n it(\"returns null and creates no adapter when disabled\", async () => {\n delete process.env.ENPILINK_ANALYTICS;\n const result = await installAnalytics();\n expect(result).toBeNull();\n expect(getActiveStorage()).toBeNull();\n });\n\n it(\"does NOT create a sqlite db file when disabled\", async () => {\n delete process.env.ENPILINK_ANALYTICS;\n process.env.ENPILINK_STORAGE = \"sqlite\";\n const dbPath = `${process.cwd()}/enpilink.db`;\n const preexisting = existsSync(dbPath);\n await installAnalytics();\n if (!preexisting) {\n expect(existsSync(dbPath)).toBe(false);\n }\n });\n\n it(\"resolves + inits + registers the active storage when enabled\", async () => {\n process.env.ENPILINK_ANALYTICS = \"1\";\n process.env.ENPILINK_STORAGE = \"memory\";\n const result = await installAnalytics();\n expect(result).not.toBeNull();\n expect(result?.entry.filter).toBe(\"request\");\n expect(typeof result?.entry.handler).toBe(\"function\");\n expect(getActiveStorage()).toBe(result?.storage);\n await result?.storage.close();\n });\n});\n\ndescribe(\"serverLog capture\", () => {\n beforeEach(() => setActiveStorage(null));\n afterEach(() => setActiveStorage(null));\n\n it(\"is a no-op sink (no storage) when no active storage\", () => {\n expect(() => serverLog(\"info\", \"hello\")).not.toThrow();\n });\n\n it(\"mirrors logs to the active storage when set\", async () => {\n const store = new MemoryStorageAdapter();\n await store.init();\n setActiveStorage(store);\n serverLog(\"error\", \"boom\", { code: 1 });\n await flush();\n const [l] = await store.queryLogs({});\n expect(l).toMatchObject({ level: \"error\", msg: \"boom\" });\n expect(l?.data).toEqual({ code: 1 });\n await store.close();\n });\n\n it(\"swallows storage errors\", async () => {\n const store = new MemoryStorageAdapter();\n await store.init();\n store.appendLog = async () => {\n throw new Error(\"down\");\n };\n setActiveStorage(store);\n expect(() => serverLog(\"info\", \"x\")).not.toThrow();\n await flush();\n await store.close();\n });\n});\n"]}
@@ -0,0 +1,11 @@
1
+ import type { Plugin } from "vite";
2
+ export declare function isCssRequest(id: string): boolean;
3
+ /**
4
+ * Transforms asset import paths to resolve at runtime via `window.enpilink.serverUrl`,
5
+ * so they work both locally and behind tunnels.
6
+ */
7
+ export declare function assetBaseUrlTransform(code: string): string;
8
+ /**
9
+ * Vite plugin that transforms asset import paths to resolve at runtime via `window.enpilink.serverUrl`.
10
+ */
11
+ export declare function assetBaseUrlTransformPlugin(): Plugin;
@@ -0,0 +1,48 @@
1
+ // Mirrors Vite's own `isCSSRequest`: matches the css family of extensions
2
+ // either at the end of the id or right before the query string. Catches both
3
+ // plain `.css` requests and SFC style blocks (e.g. `Foo.vue?vue&type=style&lang.css`).
4
+ const CSS_LANGS_RE = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
5
+ export function isCssRequest(id) {
6
+ return CSS_LANGS_RE.test(id);
7
+ }
8
+ /**
9
+ * Transforms asset import paths to resolve at runtime via `window.enpilink.serverUrl`,
10
+ * so they work both locally and behind tunnels.
11
+ */
12
+ export function assetBaseUrlTransform(code) {
13
+ const assetStringPattern = /(?<!\bfrom\s)(?<!https?:\/\/)(["'`])(\/[^"'`]+\.(svg|png|jpeg|jpg|gif|webp|mp3|mp4|woff|woff2|ttf|eot))\1/g;
14
+ code = code.replace(assetStringPattern, (_match, _quote, assetPath) => {
15
+ return `(window.enpilink?.serverUrl ?? "") + "${assetPath}"`;
16
+ });
17
+ return code;
18
+ }
19
+ /**
20
+ * Vite plugin that transforms asset import paths to resolve at runtime via `window.enpilink.serverUrl`.
21
+ */
22
+ export function assetBaseUrlTransformPlugin() {
23
+ return {
24
+ name: "asset-base-url-transform",
25
+ transform(code, id) {
26
+ if (!code) {
27
+ return null;
28
+ }
29
+ // Vite serves CSS modules as JS that embeds the stylesheet as a string
30
+ // literal. Rewriting `url("/foo.woff2")` inside that string to a JS
31
+ // concatenation expression produces invalid CSS once it lands in a
32
+ // <style> tag. CSS asset URLs are already handled at build time by
33
+ // `experimental.renderBuiltUrl`, so skip CSS requests here.
34
+ if (isCssRequest(id)) {
35
+ return null;
36
+ }
37
+ const transformedCode = assetBaseUrlTransform(code);
38
+ if (transformedCode === code) {
39
+ return null;
40
+ }
41
+ return {
42
+ code: transformedCode,
43
+ map: null,
44
+ };
45
+ },
46
+ };
47
+ }
48
+ //# sourceMappingURL=asset-base-url-transform-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-base-url-transform-plugin.js","sourceRoot":"","sources":["../../src/server/asset-base-url-transform-plugin.ts"],"names":[],"mappings":"AAEA,0EAA0E;AAC1E,6EAA6E;AAC7E,uFAAuF;AACvF,MAAM,YAAY,GAChB,6DAA6D,CAAC;AAEhE,MAAM,UAAU,YAAY,CAAC,EAAU;IACrC,OAAO,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC/B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,MAAM,kBAAkB,GACtB,4GAA4G,CAAC;IAE/G,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE;QACpE,OAAO,yCAAyC,SAAS,GAAG,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO;QACL,IAAI,EAAE,0BAA0B;QAChC,SAAS,CAAC,IAAI,EAAE,EAAE;YAChB,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,IAAI,CAAC;YACd,CAAC;YAED,uEAAuE;YACvE,oEAAoE;YACpE,mEAAmE;YACnE,mEAAmE;YACnE,4DAA4D;YAC5D,IAAI,YAAY,CAAC,EAAE,CAAC,EAAE,CAAC;gBACrB,OAAO,IAAI,CAAC;YACd,CAAC;YAED,MAAM,eAAe,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;YAEpD,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;gBAC7B,OAAO,IAAI,CAAC;YACd,CAAC;YAED,OAAO;gBACL,IAAI,EAAE,eAAe;gBACrB,GAAG,EAAE,IAAI;aACV,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type { Plugin } from \"vite\";\n\n// Mirrors Vite's own `isCSSRequest`: matches the css family of extensions\n// either at the end of the id or right before the query string. Catches both\n// plain `.css` requests and SFC style blocks (e.g. `Foo.vue?vue&type=style&lang.css`).\nconst CSS_LANGS_RE =\n /\\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\\?)/;\n\nexport function isCssRequest(id: string): boolean {\n return CSS_LANGS_RE.test(id);\n}\n\n/**\n * Transforms asset import paths to resolve at runtime via `window.enpilink.serverUrl`,\n * so they work both locally and behind tunnels.\n */\nexport function assetBaseUrlTransform(code: string): string {\n const assetStringPattern =\n /(?<!\\bfrom\\s)(?<!https?:\\/\\/)([\"'`])(\\/[^\"'`]+\\.(svg|png|jpeg|jpg|gif|webp|mp3|mp4|woff|woff2|ttf|eot))\\1/g;\n\n code = code.replace(assetStringPattern, (_match, _quote, assetPath) => {\n return `(window.enpilink?.serverUrl ?? \"\") + \"${assetPath}\"`;\n });\n\n return code;\n}\n\n/**\n * Vite plugin that transforms asset import paths to resolve at runtime via `window.enpilink.serverUrl`.\n */\nexport function assetBaseUrlTransformPlugin(): Plugin {\n return {\n name: \"asset-base-url-transform\",\n transform(code, id) {\n if (!code) {\n return null;\n }\n\n // Vite serves CSS modules as JS that embeds the stylesheet as a string\n // literal. Rewriting `url(\"/foo.woff2\")` inside that string to a JS\n // concatenation expression produces invalid CSS once it lands in a\n // <style> tag. CSS asset URLs are already handled at build time by\n // `experimental.renderBuiltUrl`, so skip CSS requests here.\n if (isCssRequest(id)) {\n return null;\n }\n\n const transformedCode = assetBaseUrlTransform(code);\n\n if (transformedCode === code) {\n return null;\n }\n\n return {\n code: transformedCode,\n map: null,\n };\n },\n };\n}\n"]}
@@ -0,0 +1,134 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { assetBaseUrlTransform, assetBaseUrlTransformPlugin, isCssRequest, } from "./asset-base-url-transform-plugin.js";
3
+ describe("assetBaseUrlTransform", () => {
4
+ it("should transform asset paths to use window.enpilink.serverUrl", () => {
5
+ const cases = [
6
+ {
7
+ desc: "single-quoted",
8
+ code: `const image = '/assets/logo.png';`,
9
+ expected: `const image = (window.enpilink?.serverUrl ?? "") + "/assets/logo.png";`,
10
+ },
11
+ {
12
+ desc: "double-quoted",
13
+ code: `const image = "/assets/logo.png";`,
14
+ expected: `const image = (window.enpilink?.serverUrl ?? "") + "/assets/logo.png";`,
15
+ },
16
+ {
17
+ desc: "backtick-quoted",
18
+ code: "const image = `/assets/logo.png`;",
19
+ expected: `const image = (window.enpilink?.serverUrl ?? "") + "/assets/logo.png";`,
20
+ },
21
+ ];
22
+ for (const { code, expected } of cases) {
23
+ const result = assetBaseUrlTransform(code);
24
+ expect(result).toBe(expected);
25
+ }
26
+ });
27
+ it("should transform multiple asset paths", () => {
28
+ const code = `
29
+ const logo = '/assets/logo.png';
30
+ const icon = '/assets/icon.svg';
31
+ const font = '/assets/font.woff2';
32
+ `;
33
+ const result = assetBaseUrlTransform(code);
34
+ expect(result).toContain(`(window.enpilink?.serverUrl ?? "") + "/assets/logo.png"`);
35
+ expect(result).toContain(`(window.enpilink?.serverUrl ?? "") + "/assets/icon.svg"`);
36
+ expect(result).toContain(`(window.enpilink?.serverUrl ?? "") + "/assets/font.woff2"`);
37
+ });
38
+ it("should not transform already absolute URLs", () => {
39
+ const code = `
40
+ const local = '/assets/logo.png';
41
+ const http = 'http://example.com/image.png';
42
+ const https = 'https://example.com/image.png';
43
+ `;
44
+ const result = assetBaseUrlTransform(code);
45
+ expect(result).toContain(`(window.enpilink?.serverUrl ?? "") + "/assets/logo.png"`);
46
+ expect(result).toContain("http://example.com/image.png");
47
+ expect(result).toContain("https://example.com/image.png");
48
+ });
49
+ it("should not transform code without asset paths", () => {
50
+ const code = `const text = "Hello World";`;
51
+ const result = assetBaseUrlTransform(code);
52
+ expect(result).toBe(code);
53
+ });
54
+ it("should not transform asset paths inside static `import ... from` clauses", () => {
55
+ // Reproducer for #713: a dep does `import * as sprite from './icons.svg'`,
56
+ // Vite resolves the relative path to absolute, then this transform used
57
+ // to rewrite the resolved string — producing invalid JS like
58
+ // `import * as sprite from (expr) + "..."` that crashes vite:import-analysis.
59
+ const cases = [
60
+ `import * as sprite from "/Users/me/proj/node_modules/pkg/icons.svg";`,
61
+ `import sprite from '/assets/icons.svg';`,
62
+ `import sprite from "/assets/icons.svg";`,
63
+ `export { default } from "/assets/icons.svg";`,
64
+ `export * from '/assets/sprites.svg';`,
65
+ ];
66
+ for (const code of cases) {
67
+ expect(assetBaseUrlTransform(code)).toBe(code);
68
+ }
69
+ });
70
+ it("should still transform value-position asset paths in files that also have unrelated imports", () => {
71
+ const code = [
72
+ `import { foo } from "./foo.js";`,
73
+ `import * as sprite from "/assets/sprite.svg";`,
74
+ `const logo = "/assets/logo.png";`,
75
+ ].join("\n");
76
+ const result = assetBaseUrlTransform(code);
77
+ // Imports untouched
78
+ expect(result).toContain(`from "./foo.js"`);
79
+ expect(result).toContain(`from "/assets/sprite.svg"`);
80
+ // Value-position rewritten
81
+ expect(result).toContain(`const logo = (window.enpilink?.serverUrl ?? "") + "/assets/logo.png";`);
82
+ });
83
+ });
84
+ describe("isCssRequest", () => {
85
+ it("returns true for CSS-family extensions", () => {
86
+ expect(isCssRequest("/src/styles.css")).toBe(true);
87
+ expect(isCssRequest("/src/styles.module.css")).toBe(true);
88
+ expect(isCssRequest("/src/styles.scss")).toBe(true);
89
+ expect(isCssRequest("/src/styles.sass")).toBe(true);
90
+ expect(isCssRequest("/src/styles.less")).toBe(true);
91
+ expect(isCssRequest("/src/styles.styl")).toBe(true);
92
+ });
93
+ it("returns true for CSS modules with Vite query strings", () => {
94
+ expect(isCssRequest("/src/styles.css?direct")).toBe(true);
95
+ expect(isCssRequest("/src/styles.css?inline")).toBe(true);
96
+ expect(isCssRequest("/src/styles.css?used")).toBe(true);
97
+ expect(isCssRequest("/src/Foo.vue?vue&type=style&lang.css")).toBe(true);
98
+ });
99
+ it("returns false for non-CSS modules", () => {
100
+ expect(isCssRequest("/src/index.tsx")).toBe(false);
101
+ expect(isCssRequest("/src/utils.ts")).toBe(false);
102
+ expect(isCssRequest("/src/Logo.svg")).toBe(false);
103
+ expect(isCssRequest("/src/notes.cssx")).toBe(false);
104
+ });
105
+ });
106
+ describe("assetBaseUrlTransformPlugin", () => {
107
+ function runTransform(id, code) {
108
+ const plugin = assetBaseUrlTransformPlugin();
109
+ const hook = plugin.transform;
110
+ if (!hook) {
111
+ throw new Error("plugin.transform is not defined");
112
+ }
113
+ const handler = typeof hook === "function" ? hook : hook.handler;
114
+ return handler.call(
115
+ // biome-ignore lint/suspicious/noExplicitAny: vitest harness for plugin hook
116
+ {}, code, id, { moduleType: "js" });
117
+ }
118
+ it("rewrites asset paths in JS modules", () => {
119
+ const result = runTransform("/src/widget.tsx", `const logo = "/assets/logo.png";`);
120
+ expect(result?.code).toContain(`(window.enpilink?.serverUrl ?? "") + "/assets/logo.png"`);
121
+ });
122
+ // Reproducer for #697: CSS imports are served as JS modules with the
123
+ // stylesheet embedded as a string. Rewriting url("/foo.woff2") inside that
124
+ // string would produce invalid CSS once the styles are injected.
125
+ it("does not rewrite asset paths when transforming a CSS module", () => {
126
+ const cssCode = `__vite__updateStyle("style-id", "@font-face { src: url(\\"/fonts/Brand.woff2\\") format(\\"woff2\\"); }");`;
127
+ expect(runTransform("/src/fonts.css", cssCode)).toBeNull();
128
+ expect(runTransform("/src/fonts.css?direct", cssCode)).toBeNull();
129
+ expect(runTransform("/src/fonts.css?inline", cssCode)).toBeNull();
130
+ expect(runTransform("/src/styles.scss", cssCode)).toBeNull();
131
+ expect(runTransform("/src/Foo.vue?vue&type=style&lang.css", cssCode)).toBeNull();
132
+ });
133
+ });
134
+ //# sourceMappingURL=asset-base-url-transform-plugin.test.js.map