@apollo/client-ai-apps 0.3.3 → 0.5.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 (404) hide show
  1. package/.github/CODEOWNERS +1 -0
  2. package/.github/workflows/pr.yaml +23 -1
  3. package/.github/workflows/release.yaml +1 -5
  4. package/.github/workflows/verify-changeset.yml +112 -21
  5. package/CHANGELOG.md +416 -0
  6. package/__mocks__/fs/promises.cjs +3 -0
  7. package/__mocks__/fs.cjs +3 -0
  8. package/dist/config/defineConfig.d.ts +26 -0
  9. package/dist/config/defineConfig.d.ts.map +1 -0
  10. package/dist/config/defineConfig.js +7 -0
  11. package/dist/config/defineConfig.js.map +1 -0
  12. package/dist/config/index.d.ts +3 -0
  13. package/dist/config/index.d.ts.map +1 -0
  14. package/dist/config/index.js +2 -0
  15. package/dist/config/index.js.map +1 -0
  16. package/dist/config/schema.d.ts +29 -0
  17. package/dist/config/schema.d.ts.map +1 -0
  18. package/dist/config/schema.js +51 -0
  19. package/dist/config/schema.js.map +1 -0
  20. package/dist/config/types.d.ts +7 -0
  21. package/dist/config/types.d.ts.map +1 -0
  22. package/dist/config/types.js +2 -0
  23. package/dist/config/types.js.map +1 -0
  24. package/dist/core/ApolloClient.d.ts +4 -3
  25. package/dist/core/ApolloClient.d.ts.map +1 -1
  26. package/dist/core/ApolloClient.js +6 -59
  27. package/dist/core/ApolloClient.js.map +1 -1
  28. package/dist/core/Platform.d.ts +8 -0
  29. package/dist/core/Platform.d.ts.map +1 -0
  30. package/dist/core/Platform.js +20 -0
  31. package/dist/core/Platform.js.map +1 -0
  32. package/dist/core/types.d.ts +13 -0
  33. package/dist/core/types.d.ts.map +1 -0
  34. package/dist/core/types.js +2 -0
  35. package/dist/core/types.js.map +1 -0
  36. package/dist/index.d.ts +2 -16
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -14
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mcp.d.ts +5 -0
  41. package/dist/index.mcp.d.ts.map +1 -0
  42. package/dist/index.mcp.js +5 -0
  43. package/dist/index.mcp.js.map +1 -0
  44. package/dist/index.openai.d.ts +5 -0
  45. package/dist/index.openai.d.ts.map +1 -0
  46. package/dist/index.openai.js +5 -0
  47. package/dist/index.openai.js.map +1 -0
  48. package/dist/link/ToolCallLink.d.ts +3 -3
  49. package/dist/link/ToolCallLink.d.ts.map +1 -1
  50. package/dist/link/ToolCallLink.js +5 -14
  51. package/dist/link/ToolCallLink.js.map +1 -1
  52. package/dist/mcp/core/ApolloClient.d.ts +21 -0
  53. package/dist/mcp/core/ApolloClient.d.ts.map +1 -0
  54. package/dist/mcp/core/ApolloClient.js +65 -0
  55. package/dist/mcp/core/ApolloClient.js.map +1 -0
  56. package/dist/mcp/core/McpAppManager.d.ts +34 -0
  57. package/dist/mcp/core/McpAppManager.d.ts.map +1 -0
  58. package/dist/mcp/core/McpAppManager.js +63 -0
  59. package/dist/mcp/core/McpAppManager.js.map +1 -0
  60. package/dist/mcp/core/Platform.d.ts +8 -0
  61. package/dist/mcp/core/Platform.d.ts.map +1 -0
  62. package/dist/mcp/core/Platform.js +8 -0
  63. package/dist/mcp/core/Platform.js.map +1 -0
  64. package/dist/mcp/index.d.ts +3 -0
  65. package/dist/mcp/index.d.ts.map +1 -0
  66. package/dist/mcp/index.js +3 -0
  67. package/dist/mcp/index.js.map +1 -0
  68. package/dist/mcp/link/ToolCallLink.d.ts +28 -0
  69. package/dist/mcp/link/ToolCallLink.d.ts.map +1 -0
  70. package/dist/mcp/link/ToolCallLink.js +35 -0
  71. package/dist/mcp/link/ToolCallLink.js.map +1 -0
  72. package/dist/mcp/react/hooks/useApolloClient.d.ts +3 -0
  73. package/dist/mcp/react/hooks/useApolloClient.d.ts.map +1 -0
  74. package/dist/mcp/react/hooks/useApolloClient.js +9 -0
  75. package/dist/mcp/react/hooks/useApolloClient.js.map +1 -0
  76. package/dist/mcp/react/hooks/useApp.d.ts +2 -0
  77. package/dist/mcp/react/hooks/useApp.d.ts.map +1 -0
  78. package/dist/mcp/react/hooks/useApp.js +5 -0
  79. package/dist/mcp/react/hooks/useApp.js.map +1 -0
  80. package/dist/mcp/react/hooks/useToolInput.d.ts +2 -0
  81. package/dist/mcp/react/hooks/useToolInput.d.ts.map +1 -0
  82. package/dist/mcp/react/hooks/useToolInput.js +5 -0
  83. package/dist/mcp/react/hooks/useToolInput.js.map +1 -0
  84. package/dist/mcp/react/hooks/useToolMetadata.d.ts +8 -0
  85. package/dist/mcp/react/hooks/useToolMetadata.d.ts.map +1 -0
  86. package/dist/mcp/react/hooks/useToolMetadata.js +5 -0
  87. package/dist/mcp/react/hooks/useToolMetadata.js.map +1 -0
  88. package/dist/mcp/react/hooks/useToolName.d.ts +2 -0
  89. package/dist/mcp/react/hooks/useToolName.d.ts.map +1 -0
  90. package/dist/mcp/react/hooks/useToolName.js +5 -0
  91. package/dist/mcp/react/hooks/useToolName.js.map +1 -0
  92. package/dist/mcp/react/index.d.ts +5 -0
  93. package/dist/mcp/react/index.d.ts.map +1 -0
  94. package/dist/mcp/react/index.js +5 -0
  95. package/dist/mcp/react/index.js.map +1 -0
  96. package/dist/openai/core/ApolloClient.d.ts +21 -0
  97. package/dist/openai/core/ApolloClient.d.ts.map +1 -0
  98. package/dist/openai/core/ApolloClient.js +66 -0
  99. package/dist/openai/core/ApolloClient.js.map +1 -0
  100. package/dist/openai/core/McpAppManager.d.ts +28 -0
  101. package/dist/openai/core/McpAppManager.d.ts.map +1 -0
  102. package/dist/openai/core/McpAppManager.js +73 -0
  103. package/dist/openai/core/McpAppManager.js.map +1 -0
  104. package/dist/openai/core/Platform.d.ts +8 -0
  105. package/dist/openai/core/Platform.d.ts.map +1 -0
  106. package/dist/openai/core/Platform.js +8 -0
  107. package/dist/openai/core/Platform.js.map +1 -0
  108. package/dist/openai/globals.d.ts +12 -0
  109. package/dist/openai/globals.d.ts.map +1 -0
  110. package/dist/openai/globals.js +2 -0
  111. package/dist/openai/globals.js.map +1 -0
  112. package/dist/openai/index.d.ts +3 -0
  113. package/dist/openai/index.d.ts.map +1 -0
  114. package/dist/openai/index.js +3 -0
  115. package/dist/openai/index.js.map +1 -0
  116. package/dist/openai/link/ToolCallLink.d.ts +28 -0
  117. package/dist/openai/link/ToolCallLink.d.ts.map +1 -0
  118. package/dist/openai/link/ToolCallLink.js +35 -0
  119. package/dist/openai/link/ToolCallLink.js.map +1 -0
  120. package/dist/openai/react/hooks/useApolloClient.d.ts +3 -0
  121. package/dist/openai/react/hooks/useApolloClient.d.ts.map +1 -0
  122. package/dist/openai/react/hooks/useApolloClient.js +9 -0
  123. package/dist/openai/react/hooks/useApolloClient.js.map +1 -0
  124. package/dist/openai/react/hooks/useApp.d.ts +2 -0
  125. package/dist/openai/react/hooks/useApp.d.ts.map +1 -0
  126. package/dist/openai/react/hooks/useApp.js +5 -0
  127. package/dist/openai/react/hooks/useApp.js.map +1 -0
  128. package/dist/openai/react/hooks/useOpenAiGlobal.d.ts +3 -0
  129. package/dist/openai/react/hooks/useOpenAiGlobal.d.ts.map +1 -0
  130. package/dist/{react → openai/react}/hooks/useOpenAiGlobal.js +1 -1
  131. package/dist/openai/react/hooks/useOpenAiGlobal.js.map +1 -0
  132. package/dist/openai/react/hooks/useToolInput.d.ts +2 -0
  133. package/dist/openai/react/hooks/useToolInput.d.ts.map +1 -0
  134. package/dist/openai/react/hooks/useToolInput.js +5 -0
  135. package/dist/openai/react/hooks/useToolInput.js.map +1 -0
  136. package/dist/openai/react/hooks/useToolMetadata.d.ts +2 -0
  137. package/dist/openai/react/hooks/useToolMetadata.d.ts.map +1 -0
  138. package/dist/openai/react/hooks/useToolMetadata.js +5 -0
  139. package/dist/openai/react/hooks/useToolMetadata.js.map +1 -0
  140. package/dist/openai/react/hooks/useToolName.d.ts.map +1 -0
  141. package/dist/openai/react/hooks/useToolName.js +5 -0
  142. package/dist/openai/react/hooks/useToolName.js.map +1 -0
  143. package/dist/{react → openai/react}/hooks/useWidgetState.d.ts +1 -1
  144. package/dist/openai/react/hooks/useWidgetState.d.ts.map +1 -0
  145. package/dist/openai/react/hooks/useWidgetState.js.map +1 -0
  146. package/dist/openai/react/index.d.ts +6 -0
  147. package/dist/openai/react/index.d.ts.map +1 -0
  148. package/dist/openai/react/index.js +6 -0
  149. package/dist/openai/react/index.js.map +1 -0
  150. package/dist/{types/openai.d.ts → openai/types.d.ts} +7 -11
  151. package/dist/openai/types.d.ts.map +1 -0
  152. package/dist/{types/openai.js → openai/types.js} +1 -1
  153. package/dist/openai/types.js.map +1 -0
  154. package/dist/react/ApolloProvider.d.ts +6 -2
  155. package/dist/react/ApolloProvider.d.ts.map +1 -1
  156. package/dist/react/ApolloProvider.js +12 -27
  157. package/dist/react/ApolloProvider.js.map +1 -1
  158. package/dist/react/index.d.ts +6 -0
  159. package/dist/react/index.d.ts.map +1 -0
  160. package/dist/react/index.js +9 -0
  161. package/dist/react/index.js.map +1 -0
  162. package/dist/react/index.mcp.d.ts +3 -0
  163. package/dist/react/index.mcp.d.ts.map +1 -0
  164. package/dist/react/index.mcp.js +3 -0
  165. package/dist/react/index.mcp.js.map +1 -0
  166. package/dist/react/index.openai.d.ts +3 -0
  167. package/dist/react/index.openai.d.ts.map +1 -0
  168. package/dist/react/index.openai.js +3 -0
  169. package/dist/react/index.openai.js.map +1 -0
  170. package/dist/react/missingHook.d.ts +2 -0
  171. package/dist/react/missingHook.d.ts.map +1 -0
  172. package/dist/react/missingHook.js +6 -0
  173. package/dist/react/missingHook.js.map +1 -0
  174. package/dist/tsconfig/core/tsconfig.json +5 -0
  175. package/dist/tsconfig/mcp/tsconfig.json +6 -0
  176. package/dist/tsconfig/openai/tsconfig.json +7 -0
  177. package/dist/types/application-manifest.d.ts +13 -1
  178. package/dist/types/application-manifest.d.ts.map +1 -1
  179. package/dist/types/application-manifest.js.map +1 -1
  180. package/dist/utilities/cacheAsync.d.ts +4 -0
  181. package/dist/utilities/cacheAsync.d.ts.map +1 -0
  182. package/dist/utilities/cacheAsync.js +19 -0
  183. package/dist/utilities/cacheAsync.js.map +1 -0
  184. package/dist/utilities/constants.d.ts +6 -0
  185. package/dist/utilities/constants.d.ts.map +1 -0
  186. package/dist/utilities/constants.js +6 -0
  187. package/dist/utilities/constants.js.map +1 -0
  188. package/dist/utilities/emptyModule.d.ts +2 -0
  189. package/dist/utilities/emptyModule.d.ts.map +1 -0
  190. package/dist/utilities/emptyModule.js +2 -0
  191. package/dist/utilities/emptyModule.js.map +1 -0
  192. package/dist/utilities/getVariablesForOperationFromToolInput.d.ts +5 -0
  193. package/dist/utilities/getVariablesForOperationFromToolInput.d.ts.map +1 -0
  194. package/dist/utilities/getVariablesForOperationFromToolInput.js +18 -0
  195. package/dist/utilities/getVariablesForOperationFromToolInput.js.map +1 -0
  196. package/dist/utilities/index.d.ts +6 -0
  197. package/dist/utilities/index.d.ts.map +1 -0
  198. package/dist/utilities/index.js +6 -0
  199. package/dist/utilities/index.js.map +1 -0
  200. package/dist/utilities/invariant.d.ts +2 -0
  201. package/dist/utilities/invariant.d.ts.map +1 -0
  202. package/dist/utilities/invariant.js +6 -0
  203. package/dist/utilities/invariant.js.map +1 -0
  204. package/dist/utilities/promiseWithResolvers.d.ts +7 -0
  205. package/dist/utilities/promiseWithResolvers.d.ts.map +1 -0
  206. package/dist/utilities/promiseWithResolvers.js +16 -0
  207. package/dist/utilities/promiseWithResolvers.js.map +1 -0
  208. package/dist/vite/__tests__/utilities/build.d.ts +41 -0
  209. package/dist/vite/__tests__/utilities/build.d.ts.map +1 -0
  210. package/dist/vite/__tests__/utilities/build.js +63 -0
  211. package/dist/vite/__tests__/utilities/build.js.map +1 -0
  212. package/dist/vite/apolloClientAiApps.d.ts +14 -0
  213. package/dist/vite/apolloClientAiApps.d.ts.map +1 -0
  214. package/dist/vite/apolloClientAiApps.js +350 -0
  215. package/dist/vite/apolloClientAiApps.js.map +1 -0
  216. package/dist/vite/index.d.ts +1 -2
  217. package/dist/vite/index.d.ts.map +1 -1
  218. package/dist/vite/index.js +1 -2
  219. package/dist/vite/index.js.map +1 -1
  220. package/dist/vite/utilities/config.d.ts +2 -0
  221. package/dist/vite/utilities/config.d.ts.map +1 -0
  222. package/dist/vite/utilities/config.js +11 -0
  223. package/dist/vite/utilities/config.js.map +1 -0
  224. package/dist/vite/utilities/graphql.d.ts +20 -0
  225. package/dist/vite/utilities/graphql.d.ts.map +1 -0
  226. package/dist/vite/utilities/graphql.js +42 -0
  227. package/dist/vite/utilities/graphql.js.map +1 -0
  228. package/knope.toml +1 -1
  229. package/package.json +109 -12
  230. package/src/config/defineConfig.ts +8 -0
  231. package/src/config/index.ts +2 -0
  232. package/src/config/schema.ts +73 -0
  233. package/src/config/types.ts +7 -0
  234. package/src/core/ApolloClient.ts +7 -95
  235. package/src/core/Platform.ts +27 -0
  236. package/src/core/types.ts +14 -0
  237. package/src/index.mcp.ts +5 -0
  238. package/src/index.openai.ts +5 -0
  239. package/src/index.ts +4 -28
  240. package/src/link/ToolCallLink.ts +6 -22
  241. package/src/mcp/core/ApolloClient.ts +102 -0
  242. package/src/mcp/core/McpAppManager.ts +98 -0
  243. package/src/mcp/core/Platform.ts +11 -0
  244. package/src/mcp/core/__tests__/ApolloClient.test.ts +464 -0
  245. package/src/mcp/index.ts +2 -0
  246. package/src/mcp/link/ToolCallLink.ts +40 -0
  247. package/src/mcp/link/__tests__/ToolCallLink.test.ts +53 -0
  248. package/src/mcp/react/hooks/__tests__/useApp.test.tsx +46 -0
  249. package/src/mcp/react/hooks/__tests__/useToolInput.test.tsx +50 -0
  250. package/src/mcp/react/hooks/__tests__/useToolMetadata.test.tsx +53 -0
  251. package/src/mcp/react/hooks/__tests__/useToolName.test.tsx +50 -0
  252. package/src/mcp/react/hooks/useApolloClient.ts +14 -0
  253. package/src/mcp/react/hooks/useApp.ts +5 -0
  254. package/src/mcp/react/hooks/useToolInput.ts +5 -0
  255. package/src/mcp/react/hooks/useToolMetadata.ts +5 -0
  256. package/src/mcp/react/hooks/useToolName.ts +5 -0
  257. package/src/mcp/react/index.ts +4 -0
  258. package/src/openai/core/ApolloClient.ts +100 -0
  259. package/src/openai/core/McpAppManager.ts +110 -0
  260. package/src/openai/core/Platform.ts +11 -0
  261. package/src/openai/core/__tests__/ApolloClient.test.ts +537 -0
  262. package/src/openai/globals.ts +14 -0
  263. package/src/openai/index.ts +2 -0
  264. package/src/openai/link/ToolCallLink.ts +40 -0
  265. package/src/{react → openai/react}/hooks/__tests__/useOpenAiGlobal.test.ts +1 -1
  266. package/src/openai/react/hooks/__tests__/useToolInput.test.tsx +85 -0
  267. package/src/openai/react/hooks/__tests__/useToolMetadata.test.tsx +86 -0
  268. package/src/openai/react/hooks/__tests__/useToolName.test.tsx +50 -0
  269. package/src/{react → openai/react}/hooks/__tests__/useWidgetState.test.tsx +1 -1
  270. package/src/openai/react/hooks/useApolloClient.ts +14 -0
  271. package/src/openai/react/hooks/useApp.ts +5 -0
  272. package/src/{react → openai/react}/hooks/useOpenAiGlobal.ts +2 -2
  273. package/src/openai/react/hooks/useToolInput.ts +5 -0
  274. package/src/openai/react/hooks/useToolMetadata.ts +5 -0
  275. package/src/openai/react/hooks/useToolName.ts +5 -0
  276. package/src/{react → openai/react}/hooks/useWidgetState.ts +1 -1
  277. package/src/openai/react/index.ts +5 -0
  278. package/src/{types/openai.ts → openai/types.ts} +9 -13
  279. package/src/react/ApolloProvider.tsx +23 -31
  280. package/src/react/__tests__/ApolloProvider/mcp.test.tsx +74 -0
  281. package/src/react/__tests__/ApolloProvider/openai.test.tsx +146 -0
  282. package/src/react/index.mcp.ts +7 -0
  283. package/src/react/index.openai.ts +7 -0
  284. package/src/react/index.ts +19 -0
  285. package/src/react/missingHook.ts +9 -0
  286. package/src/testing/internal/index.ts +8 -0
  287. package/src/testing/internal/matchers/index.ts +2 -0
  288. package/src/testing/internal/matchers/toComplete.ts +46 -0
  289. package/src/testing/internal/matchers/toEmitValue.ts +71 -0
  290. package/src/testing/internal/matchers/types.ts +3 -0
  291. package/src/testing/internal/mcp/graphqlToolResult.ts +31 -0
  292. package/src/testing/internal/mcp/minimalHostContextWithToolName.ts +9 -0
  293. package/src/testing/internal/mcp/mockMcpHost.ts +188 -0
  294. package/src/testing/internal/openai/dispatchStateChange.ts +1 -1
  295. package/src/testing/internal/openai/stubOpenAiGlobals.ts +22 -9
  296. package/src/testing/internal/react/renderAsync.ts +7 -0
  297. package/src/testing/internal/utilities/ObservableStream.ts +172 -0
  298. package/src/testing/internal/utilities/getSerializableProperties.ts +55 -0
  299. package/src/testing/internal/utilities/mockApplicationManifest.ts +39 -0
  300. package/src/testing/internal/utilities/spyOnConsole.ts +29 -0
  301. package/src/testing/internal/utilities/wait.ts +3 -0
  302. package/src/tsconfig/core/tsconfig.json +5 -0
  303. package/src/tsconfig/mcp/tsconfig.json +6 -0
  304. package/src/tsconfig/openai/tsconfig.json +7 -0
  305. package/src/types/application-manifest.ts +16 -1
  306. package/src/utilities/__tests__/cacheAsync.test.ts +64 -0
  307. package/src/utilities/cacheAsync.ts +25 -0
  308. package/src/utilities/constants.ts +5 -0
  309. package/src/utilities/emptyModule.ts +1 -0
  310. package/src/utilities/getVariablesForOperationFromToolInput.ts +27 -0
  311. package/src/utilities/index.ts +5 -0
  312. package/src/utilities/invariant.ts +5 -0
  313. package/src/utilities/promiseWithResolvers.ts +18 -0
  314. package/src/vite/__tests__/apolloClientAiApps.test.ts +1521 -0
  315. package/src/vite/__tests__/utilities/build.ts +72 -0
  316. package/src/vite/apolloClientAiApps.ts +515 -0
  317. package/src/vite/index.ts +1 -2
  318. package/src/vite/utilities/config.ts +13 -0
  319. package/src/vite/utilities/graphql.ts +123 -0
  320. package/tsconfig.base.json +2 -2
  321. package/tsconfig.vite.build.json +4 -1
  322. package/tsconfig.vite.json +8 -2
  323. package/vitest-setup.ts +28 -0
  324. package/dist/react/context/ToolUseContext.d.ts +0 -16
  325. package/dist/react/context/ToolUseContext.d.ts.map +0 -1
  326. package/dist/react/context/ToolUseContext.js +0 -11
  327. package/dist/react/context/ToolUseContext.js.map +0 -1
  328. package/dist/react/hooks/useCallTool.d.ts +0 -4
  329. package/dist/react/hooks/useCallTool.d.ts.map +0 -1
  330. package/dist/react/hooks/useCallTool.js +0 -5
  331. package/dist/react/hooks/useCallTool.js.map +0 -1
  332. package/dist/react/hooks/useOpenAiGlobal.d.ts +0 -3
  333. package/dist/react/hooks/useOpenAiGlobal.d.ts.map +0 -1
  334. package/dist/react/hooks/useOpenAiGlobal.js.map +0 -1
  335. package/dist/react/hooks/useOpenExternal.d.ts +0 -4
  336. package/dist/react/hooks/useOpenExternal.d.ts.map +0 -1
  337. package/dist/react/hooks/useOpenExternal.js +0 -5
  338. package/dist/react/hooks/useOpenExternal.js.map +0 -1
  339. package/dist/react/hooks/useRequestDisplayMode.d.ts +0 -7
  340. package/dist/react/hooks/useRequestDisplayMode.d.ts.map +0 -1
  341. package/dist/react/hooks/useRequestDisplayMode.js +0 -6
  342. package/dist/react/hooks/useRequestDisplayMode.js.map +0 -1
  343. package/dist/react/hooks/useSendFollowUpMessage.d.ts +0 -2
  344. package/dist/react/hooks/useSendFollowUpMessage.d.ts.map +0 -1
  345. package/dist/react/hooks/useSendFollowUpMessage.js +0 -8
  346. package/dist/react/hooks/useSendFollowUpMessage.js.map +0 -1
  347. package/dist/react/hooks/useToolEffect.d.ts +0 -3
  348. package/dist/react/hooks/useToolEffect.d.ts.map +0 -1
  349. package/dist/react/hooks/useToolEffect.js +0 -28
  350. package/dist/react/hooks/useToolEffect.js.map +0 -1
  351. package/dist/react/hooks/useToolInput.d.ts +0 -2
  352. package/dist/react/hooks/useToolInput.d.ts.map +0 -1
  353. package/dist/react/hooks/useToolInput.js +0 -6
  354. package/dist/react/hooks/useToolInput.js.map +0 -1
  355. package/dist/react/hooks/useToolName.d.ts.map +0 -1
  356. package/dist/react/hooks/useToolName.js +0 -6
  357. package/dist/react/hooks/useToolName.js.map +0 -1
  358. package/dist/react/hooks/useToolOutput.d.ts +0 -2
  359. package/dist/react/hooks/useToolOutput.d.ts.map +0 -1
  360. package/dist/react/hooks/useToolOutput.js +0 -5
  361. package/dist/react/hooks/useToolOutput.js.map +0 -1
  362. package/dist/react/hooks/useToolResponseMetadata.d.ts +0 -2
  363. package/dist/react/hooks/useToolResponseMetadata.d.ts.map +0 -1
  364. package/dist/react/hooks/useToolResponseMetadata.js +0 -5
  365. package/dist/react/hooks/useToolResponseMetadata.js.map +0 -1
  366. package/dist/react/hooks/useWidgetState.d.ts.map +0 -1
  367. package/dist/react/hooks/useWidgetState.js.map +0 -1
  368. package/dist/types/openai.d.ts.map +0 -1
  369. package/dist/types/openai.js.map +0 -1
  370. package/dist/vite/absolute_asset_imports_plugin.d.ts +0 -5
  371. package/dist/vite/absolute_asset_imports_plugin.d.ts.map +0 -1
  372. package/dist/vite/absolute_asset_imports_plugin.js +0 -17
  373. package/dist/vite/absolute_asset_imports_plugin.js.map +0 -1
  374. package/dist/vite/application_manifest_plugin.d.ts +0 -10
  375. package/dist/vite/application_manifest_plugin.d.ts.map +0 -1
  376. package/dist/vite/application_manifest_plugin.js +0 -274
  377. package/dist/vite/application_manifest_plugin.js.map +0 -1
  378. package/src/core/__tests__/ApolloClient.test.ts +0 -626
  379. package/src/react/__tests__/ApolloProvider.test.tsx +0 -41
  380. package/src/react/context/ToolUseContext.tsx +0 -31
  381. package/src/react/hooks/__tests__/useCallTool.test.ts +0 -46
  382. package/src/react/hooks/__tests__/useOpenExternal.test.tsx +0 -24
  383. package/src/react/hooks/__tests__/useRequestDisplayMode.test.ts +0 -17
  384. package/src/react/hooks/__tests__/useSendFollowUpMessage.test.ts +0 -15
  385. package/src/react/hooks/__tests__/useToolEffect.test.tsx +0 -67
  386. package/src/react/hooks/__tests__/useToolInput.test.ts +0 -13
  387. package/src/react/hooks/__tests__/useToolName.test.ts +0 -13
  388. package/src/react/hooks/__tests__/useToolOutput.test.tsx +0 -49
  389. package/src/react/hooks/__tests__/useToolResponseMetadata.test.tsx +0 -49
  390. package/src/react/hooks/useCallTool.ts +0 -13
  391. package/src/react/hooks/useOpenExternal.ts +0 -11
  392. package/src/react/hooks/useRequestDisplayMode.ts +0 -7
  393. package/src/react/hooks/useSendFollowUpMessage.ts +0 -7
  394. package/src/react/hooks/useToolEffect.tsx +0 -37
  395. package/src/react/hooks/useToolInput.ts +0 -7
  396. package/src/react/hooks/useToolName.ts +0 -7
  397. package/src/react/hooks/useToolOutput.ts +0 -5
  398. package/src/react/hooks/useToolResponseMetadata.ts +0 -5
  399. package/src/vite/__tests__/absolute_asset_imports_plugin.test.ts +0 -102
  400. package/src/vite/__tests__/application_manifest_plugin.test.ts +0 -1038
  401. package/src/vite/absolute_asset_imports_plugin.ts +0 -22
  402. package/src/vite/application_manifest_plugin.ts +0 -443
  403. /package/dist/{react → openai/react}/hooks/useToolName.d.ts +0 -0
  404. /package/dist/{react → openai/react}/hooks/useWidgetState.js +0 -0
@@ -0,0 +1,1521 @@
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import fs from "node:fs";
3
+ import { gql, type DocumentNode } from "@apollo/client";
4
+ import { print } from "@apollo/client/utilities";
5
+ import { getOperationName } from "@apollo/client/utilities/internal";
6
+ import { vol } from "memfs";
7
+ import { apolloClientAiApps } from "../apolloClientAiApps.js";
8
+ import { buildApp, setupServer } from "./utilities/build.js";
9
+ import type {
10
+ ApplicationManifest,
11
+ ManifestWidgetSettings,
12
+ } from "../../types/application-manifest.js";
13
+ import { explorer } from "../utilities/config.js";
14
+
15
+ beforeEach(() => {
16
+ explorer.clearCaches();
17
+ });
18
+
19
+ describe("operations", () => {
20
+ test("writes to dev application manifest file when using a serve command", async () => {
21
+ vol.fromJSON({
22
+ "package.json": mockPackageJson({
23
+ "apollo-client-ai-apps": {
24
+ labels: {
25
+ toolInvocation: {
26
+ invoking: "Testing global...",
27
+ invoked: "Tested global!",
28
+ },
29
+ },
30
+ widgetSettings: {
31
+ description: "Test",
32
+ domain: "https://example.com",
33
+ prefersBorder: true,
34
+ } satisfies ManifestWidgetSettings,
35
+ },
36
+ }),
37
+ "src/my-component.tsx": declareOperation(gql`
38
+ query HelloWorldQuery($name: string!)
39
+ @tool(
40
+ name: "hello-world"
41
+ description: "This is an awesome tool!"
42
+ extraInputs: [
43
+ {
44
+ name: "doStuff"
45
+ type: "boolean"
46
+ description: "Should we do stuff?"
47
+ }
48
+ ]
49
+ labels: {
50
+ toolInvocation: {
51
+ invoking: "Testing tool..."
52
+ invoked: "Tested tool!"
53
+ }
54
+ }
55
+ ) {
56
+ helloWorld(name: $name)
57
+ }
58
+ `),
59
+ });
60
+
61
+ await using server = await setupServer({
62
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
63
+ });
64
+ await server.listen();
65
+
66
+ const manifest = readManifestFile();
67
+ expect(manifest).toMatchInlineSnapshot(`
68
+ {
69
+ "appVersion": "1.0.0",
70
+ "csp": {
71
+ "connectDomains": [],
72
+ "frameDomains": [],
73
+ "redirectDomains": [],
74
+ "resourceDomains": [],
75
+ },
76
+ "format": "apollo-ai-app-manifest",
77
+ "hash": "abc",
78
+ "labels": {
79
+ "toolInvocation/invoked": "Tested global!",
80
+ "toolInvocation/invoking": "Testing global...",
81
+ },
82
+ "operations": [
83
+ {
84
+ "body": "query HelloWorldQuery($name: string!) {
85
+ helloWorld(name: $name)
86
+ }",
87
+ "id": "c2ceb00338549909d9a8cd5cc601bda78d8c27654294dfe408a6c3735beb26a6",
88
+ "name": "HelloWorldQuery",
89
+ "prefetch": false,
90
+ "tools": [
91
+ {
92
+ "description": "This is an awesome tool!",
93
+ "extraInputs": [
94
+ {
95
+ "description": "Should we do stuff?",
96
+ "name": "doStuff",
97
+ "type": "boolean",
98
+ },
99
+ ],
100
+ "labels": {
101
+ "toolInvocation/invoked": "Tested tool!",
102
+ "toolInvocation/invoking": "Testing tool...",
103
+ },
104
+ "name": "hello-world",
105
+ },
106
+ ],
107
+ "type": "query",
108
+ "variables": {
109
+ "name": "string",
110
+ },
111
+ },
112
+ ],
113
+ "resource": "http://localhost:3333",
114
+ "version": "1",
115
+ "widgetSettings": {
116
+ "description": "Test",
117
+ "domain": "https://example.com",
118
+ "prefersBorder": true,
119
+ },
120
+ }
121
+ `);
122
+ });
123
+
124
+ test("does not write to dev application manifest file when using a build command", async () => {
125
+ vol.fromJSON({
126
+ "package.json": mockPackageJson(),
127
+ "src/my-component.tsx": declareOperation(gql`
128
+ query HelloWorldQuery
129
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
130
+ helloWorld
131
+ }
132
+ `),
133
+ });
134
+
135
+ await buildApp({
136
+ mode: "production",
137
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
138
+ });
139
+
140
+ const manifest = readManifestFile();
141
+ expect(manifest.operations).toHaveLength(1);
142
+ expect(manifest.operations[0].name).toBe("HelloWorldQuery");
143
+ });
144
+
145
+ test("does not process files that do not contain gql tags", async () => {
146
+ vol.fromJSON({
147
+ "package.json": mockPackageJson(),
148
+ "src/my-component.tsx": `
149
+ const MY_QUERY = \`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
150
+ `,
151
+ });
152
+
153
+ await using server = await setupServer({
154
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
155
+ });
156
+ await server.listen();
157
+
158
+ const manifest = readManifestFile();
159
+ expect(manifest).toMatchInlineSnapshot(`
160
+ {
161
+ "appVersion": "1.0.0",
162
+ "csp": {
163
+ "connectDomains": [],
164
+ "frameDomains": [],
165
+ "redirectDomains": [],
166
+ "resourceDomains": [],
167
+ },
168
+ "format": "apollo-ai-app-manifest",
169
+ "hash": "abc",
170
+ "operations": [],
171
+ "resource": "http://localhost:3333",
172
+ "version": "1",
173
+ }
174
+ `);
175
+ });
176
+
177
+ test("captures queries in manifest file", async () => {
178
+ vol.fromJSON({
179
+ "package.json": mockPackageJson(),
180
+ "src/my-component.tsx": declareOperation(gql`
181
+ query HelloWorldQuery {
182
+ helloWorld
183
+ }
184
+ `),
185
+ });
186
+
187
+ await using server = await setupServer({
188
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
189
+ });
190
+ await server.listen();
191
+
192
+ const manifest = readManifestFile();
193
+ expect(manifest).toMatchInlineSnapshot(`
194
+ {
195
+ "appVersion": "1.0.0",
196
+ "csp": {
197
+ "connectDomains": [],
198
+ "frameDomains": [],
199
+ "redirectDomains": [],
200
+ "resourceDomains": [],
201
+ },
202
+ "format": "apollo-ai-app-manifest",
203
+ "hash": "abc",
204
+ "operations": [
205
+ {
206
+ "body": "query HelloWorldQuery {
207
+ helloWorld
208
+ }",
209
+ "id": "f8604bba13e2f589608c0eb36c3039c5ef3a4c5747bc1596f9dbcbe924dc90f9",
210
+ "name": "HelloWorldQuery",
211
+ "prefetch": false,
212
+ "tools": [],
213
+ "type": "query",
214
+ "variables": {},
215
+ },
216
+ ],
217
+ "resource": "http://localhost:3333",
218
+ "version": "1",
219
+ }
220
+ `);
221
+ });
222
+
223
+ test("captures mutations in manifest file", async () => {
224
+ vol.fromJSON({
225
+ "package.json": mockPackageJson(),
226
+ "src/my-component.tsx": declareOperation(gql`
227
+ mutation HelloWorldQuery
228
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
229
+ helloWorld
230
+ }
231
+ `),
232
+ });
233
+
234
+ await using server = await setupServer({
235
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
236
+ });
237
+ await server.listen();
238
+
239
+ const manifest = readManifestFile();
240
+ expect(manifest).toMatchInlineSnapshot(`
241
+ {
242
+ "appVersion": "1.0.0",
243
+ "csp": {
244
+ "connectDomains": [],
245
+ "frameDomains": [],
246
+ "redirectDomains": [],
247
+ "resourceDomains": [],
248
+ },
249
+ "format": "apollo-ai-app-manifest",
250
+ "hash": "abc",
251
+ "operations": [
252
+ {
253
+ "body": "mutation HelloWorldQuery {
254
+ helloWorld
255
+ }",
256
+ "id": "0c98e15f08542215c9c268192aaff732800bc33b79dddea7dc6fdf69c21b61a7",
257
+ "name": "HelloWorldQuery",
258
+ "prefetch": false,
259
+ "tools": [
260
+ {
261
+ "description": "This is an awesome tool!",
262
+ "name": "hello-world",
263
+ },
264
+ ],
265
+ "type": "mutation",
266
+ "variables": {},
267
+ },
268
+ ],
269
+ "resource": "http://localhost:3333",
270
+ "version": "1",
271
+ }
272
+ `);
273
+ });
274
+
275
+ test("errors when a subscription operation type is discovered", async () => {
276
+ vol.fromJSON({
277
+ "package.json": mockPackageJson(),
278
+ "src/my-component.tsx": declareOperation(gql`
279
+ subscription HelloWorldQuery
280
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
281
+ helloWorld
282
+ }
283
+ `),
284
+ });
285
+
286
+ await expect(async () => {
287
+ await using server = await setupServer({
288
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
289
+ });
290
+ await server.listen();
291
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
292
+ `[Error: Found an unsupported operation type. Only Query and Mutation are supported.]`
293
+ );
294
+ });
295
+
296
+ test("orders operations and fragments when generating normalized operation", async () => {
297
+ vol.fromJSON({
298
+ "package.json": mockPackageJson(),
299
+ "src/my-component.tsx": `
300
+ const MY_QUERY = gql\`
301
+ fragment A on User { firstName }
302
+ fragment B on User { lastName }
303
+ query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") {
304
+ helloWorld {
305
+ ...B
306
+ ...A
307
+ ...C
308
+ }
309
+
310
+ fragment C on User { middleName }
311
+ }\`;
312
+ `,
313
+ });
314
+
315
+ await using server = await setupServer({
316
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
317
+ });
318
+ await server.listen();
319
+
320
+ const manifest = readManifestFile();
321
+ expect(manifest).toMatchInlineSnapshot(`
322
+ {
323
+ "appVersion": "1.0.0",
324
+ "csp": {
325
+ "connectDomains": [],
326
+ "frameDomains": [],
327
+ "redirectDomains": [],
328
+ "resourceDomains": [],
329
+ },
330
+ "format": "apollo-ai-app-manifest",
331
+ "hash": "abc",
332
+ "operations": [
333
+ {
334
+ "body": "query HelloWorldQuery {
335
+ helloWorld {
336
+ ...B
337
+ ...A
338
+ ...C
339
+ __typename
340
+ }
341
+ fragment
342
+ C
343
+ on
344
+ User {
345
+ middleName
346
+ __typename
347
+ }
348
+ }
349
+
350
+ fragment A on User {
351
+ firstName
352
+ __typename
353
+ }
354
+
355
+ fragment B on User {
356
+ lastName
357
+ __typename
358
+ }",
359
+ "id": "58359ad006a8e1a6cdabe4b49c0322e8a41d71c5194a796e6432be055220b9ec",
360
+ "name": "HelloWorldQuery",
361
+ "prefetch": false,
362
+ "tools": [
363
+ {
364
+ "description": "This is an awesome tool!",
365
+ "name": "hello-world",
366
+ },
367
+ ],
368
+ "type": "query",
369
+ "variables": {},
370
+ },
371
+ ],
372
+ "resource": "http://localhost:3333",
373
+ "version": "1",
374
+ }
375
+ `);
376
+ });
377
+ });
378
+
379
+ describe("@prefetch", () => {
380
+ test("captures queries as prefetch when marked with @prefetch directive", async () => {
381
+ vol.fromJSON({
382
+ "package.json": mockPackageJson(),
383
+ "src/my-component.tsx": declareOperation(gql`
384
+ query HelloWorldQuery @prefetch {
385
+ helloWorld
386
+ }
387
+ `),
388
+ });
389
+
390
+ await using server = await setupServer({
391
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
392
+ });
393
+ await server.listen();
394
+
395
+ const manifest = readManifestFile();
396
+ expect(manifest).toMatchInlineSnapshot(`
397
+ {
398
+ "appVersion": "1.0.0",
399
+ "csp": {
400
+ "connectDomains": [],
401
+ "frameDomains": [],
402
+ "redirectDomains": [],
403
+ "resourceDomains": [],
404
+ },
405
+ "format": "apollo-ai-app-manifest",
406
+ "hash": "abc",
407
+ "operations": [
408
+ {
409
+ "body": "query HelloWorldQuery {
410
+ helloWorld
411
+ }",
412
+ "id": "f8604bba13e2f589608c0eb36c3039c5ef3a4c5747bc1596f9dbcbe924dc90f9",
413
+ "name": "HelloWorldQuery",
414
+ "prefetch": true,
415
+ "prefetchID": "__anonymous",
416
+ "tools": [],
417
+ "type": "query",
418
+ "variables": {},
419
+ },
420
+ ],
421
+ "resource": "http://localhost:3333",
422
+ "version": "1",
423
+ }
424
+ `);
425
+ });
426
+
427
+ test("errors when multiple operations are marked with @prefetch", async () => {
428
+ vol.fromJSON({
429
+ "package.json": mockPackageJson(),
430
+ "src/my-component.tsx": [
431
+ declareOperation(gql`
432
+ query HelloWorldQuery @prefetch {
433
+ helloWorld
434
+ }
435
+ `),
436
+ declareOperation(gql`
437
+ query HelloWorldQuery2 @prefetch {
438
+ helloWorld
439
+ }
440
+ `),
441
+ ].join("\n"),
442
+ });
443
+
444
+ await expect(async () => {
445
+ await using server = await setupServer({
446
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
447
+ });
448
+ await server.listen();
449
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
450
+ `[Error: Found multiple operations marked as \`@prefetch\`. You can only mark 1 operation with \`@prefetch\`.]`
451
+ );
452
+ });
453
+ });
454
+
455
+ describe("@tool validation", () => {
456
+ test("errors when tool name is not provided", async () => {
457
+ vol.fromJSON({
458
+ "package.json": mockPackageJson(),
459
+ "src/my-component.tsx": declareOperation(gql`
460
+ query HelloWorldQuery @tool {
461
+ helloWorld
462
+ }
463
+ `),
464
+ });
465
+
466
+ await expect(async () => {
467
+ await using server = await setupServer({
468
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
469
+ });
470
+ await server.listen();
471
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
472
+ `[Error: 'name' argument must be supplied for @tool]`
473
+ );
474
+ });
475
+
476
+ test("errors when tool description is not provided", async () => {
477
+ vol.fromJSON({
478
+ "package.json": mockPackageJson(),
479
+ "src/my-component.tsx": declareOperation(gql`
480
+ query HelloWorldQuery @tool(name: "hello-world") {
481
+ helloWorld
482
+ }
483
+ `),
484
+ });
485
+
486
+ await expect(async () => {
487
+ await using server = await setupServer({
488
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
489
+ });
490
+ await server.listen();
491
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
492
+ `[Error: 'description' argument must be supplied for @tool]`
493
+ );
494
+ });
495
+
496
+ test("errors when tool name contains spaces", async () => {
497
+ vol.fromJSON({
498
+ "package.json": mockPackageJson(),
499
+ "src/my-component.tsx": declareOperation(gql`
500
+ query HelloWorldQuery
501
+ @tool(name: "hello world", description: "A tool") {
502
+ helloWorld
503
+ }
504
+ `),
505
+ });
506
+
507
+ await expect(async () => {
508
+ await using server = await setupServer({
509
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
510
+ });
511
+ await server.listen();
512
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
513
+ `
514
+ [Error: ✖ Tool with name "hello world" must not contain spaces
515
+ → at name]
516
+ `
517
+ );
518
+ });
519
+
520
+ test("errors when tool name is not a string", async () => {
521
+ vol.fromJSON({
522
+ "package.json": mockPackageJson(),
523
+ "src/my-component.tsx": declareOperation(gql`
524
+ query HelloWorldQuery @tool(name: true) {
525
+ helloWorld
526
+ }
527
+ `),
528
+ });
529
+
530
+ await expect(async () => {
531
+ await using server = await setupServer({
532
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
533
+ });
534
+ await server.listen();
535
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
536
+ `[Error: Expected argument 'name' to be of type 'StringValue' but found 'BooleanValue' instead.]`
537
+ );
538
+ });
539
+
540
+ test("errors when tool description is not a string", async () => {
541
+ vol.fromJSON({
542
+ "package.json": mockPackageJson(),
543
+ "src/my-component.tsx": declareOperation(gql`
544
+ query HelloWorldQuery @tool(name: "hello-world", description: false) {
545
+ helloWorld
546
+ }
547
+ `),
548
+ });
549
+
550
+ await expect(async () => {
551
+ await using server = await setupServer({
552
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
553
+ });
554
+ await server.listen();
555
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
556
+ `[Error: Expected argument 'description' to be of type 'StringValue' but found 'BooleanValue' instead.]`
557
+ );
558
+ });
559
+
560
+ test("errors when extraInputs is not an array", async () => {
561
+ vol.fromJSON({
562
+ "package.json": mockPackageJson(),
563
+ "src/my-component.tsx": declareOperation(gql`
564
+ query HelloWorldQuery
565
+ @tool(name: "hello-world", description: "hello", extraInputs: false) {
566
+ helloWorld
567
+ }
568
+ `),
569
+ });
570
+
571
+ await expect(async () => {
572
+ await using server = await setupServer({
573
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
574
+ });
575
+ await server.listen();
576
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
577
+ `[Error: Expected argument 'extraInputs' to be of type 'ListValue' but found 'BooleanValue' instead.]`
578
+ );
579
+ });
580
+
581
+ test("errors when an unknown type is discovered", async () => {
582
+ vol.fromJSON({
583
+ "package.json": mockPackageJson(),
584
+ "src/my-component.tsx": declareOperation(gql`
585
+ query HelloWorldQuery
586
+ @tool(
587
+ name: "hello-world"
588
+ description: "hello"
589
+ extraInputs: [{ name: 3.1 }]
590
+ ) {
591
+ helloWorld
592
+ }
593
+ `),
594
+ });
595
+
596
+ await expect(async () => {
597
+ await using server = await setupServer({
598
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
599
+ });
600
+ await server.listen();
601
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
602
+ `[Error: Error when parsing directive values: unexpected type 'FloatValue']`
603
+ );
604
+ });
605
+ });
606
+
607
+ describe("config validation", () => {
608
+ test("errors when widgetSettings.prefersBorder is not a boolean", async () => {
609
+ vol.fromJSON({
610
+ "package.json": mockPackageJson({
611
+ "apollo-client-ai-apps": {
612
+ widgetSettings: {
613
+ prefersBorder: "test",
614
+ },
615
+ },
616
+ }),
617
+ "src/my-component.tsx": declareOperation(gql`
618
+ query HelloWorldQuery @tool(name: "test", description: "Test") {
619
+ helloWorld
620
+ }
621
+ `),
622
+ });
623
+
624
+ await expect(async () => {
625
+ await using server = await setupServer({
626
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
627
+ });
628
+ await server.listen();
629
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
630
+ `
631
+ "✖ Invalid input: expected boolean, received string
632
+ → at widgetSettings.prefersBorder"
633
+ `
634
+ );
635
+ });
636
+
637
+ test("errors when widgetSettings.description is not a string", async () => {
638
+ vol.fromJSON({
639
+ "package.json": mockPackageJson({
640
+ "apollo-client-ai-apps": {
641
+ widgetSettings: {
642
+ description: true,
643
+ },
644
+ },
645
+ }),
646
+ "src/my-component.tsx": declareOperation(gql`
647
+ query HelloWorldQuery @tool(name: "test", description: "Test") {
648
+ helloWorld
649
+ }
650
+ `),
651
+ });
652
+
653
+ await expect(async () => {
654
+ await using server = await setupServer({
655
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
656
+ });
657
+ await server.listen();
658
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
659
+ `
660
+ "✖ Invalid input: expected string, received boolean
661
+ → at widgetSettings.description"
662
+ `
663
+ );
664
+ });
665
+
666
+ test("errors when widgetSettings.domain is not a string", async () => {
667
+ vol.fromJSON({
668
+ "package.json": mockPackageJson({
669
+ "apollo-client-ai-apps": {
670
+ widgetSettings: {
671
+ domain: true,
672
+ },
673
+ },
674
+ }),
675
+ "src/my-component.tsx": declareOperation(gql`
676
+ query HelloWorldQuery @tool(name: "test", description: "Test") {
677
+ helloWorld
678
+ }
679
+ `),
680
+ });
681
+
682
+ await expect(async () => {
683
+ await using server = await setupServer({
684
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
685
+ });
686
+ await server.listen();
687
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
688
+ `
689
+ "✖ Invalid input: expected string, received boolean
690
+ → at widgetSettings.domain"
691
+ `
692
+ );
693
+ });
694
+
695
+ test("allows empty widgetSettings value", async () => {
696
+ vol.fromJSON({
697
+ "package.json": mockPackageJson({
698
+ widgetSettings: {},
699
+ }),
700
+ "src/my-component.tsx": declareOperation(gql`
701
+ query HelloWorldQuery @tool(name: "test", description: "Test") {
702
+ helloWorld
703
+ }
704
+ `),
705
+ });
706
+
707
+ await using server = await setupServer({
708
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
709
+ });
710
+ await server.listen();
711
+
712
+ const manifest = readManifestFile();
713
+ expect(manifest.operations).toHaveLength(1);
714
+ });
715
+
716
+ test("errors when labels.toolInvocation.invoking in package.json is not a string", async () => {
717
+ vol.fromJSON({
718
+ "package.json": mockPackageJson({
719
+ "apollo-client-ai-apps": {
720
+ labels: {
721
+ toolInvocation: {
722
+ invoking: true,
723
+ },
724
+ },
725
+ },
726
+ }),
727
+ "src/my-component.tsx": declareOperation(gql`
728
+ query HelloWorldQuery @tool(name: "test", description: "Test") {
729
+ helloWorld
730
+ }
731
+ `),
732
+ });
733
+
734
+ await expect(async () => {
735
+ await using server = await setupServer({
736
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
737
+ });
738
+ await server.listen();
739
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
740
+ `
741
+ "✖ Invalid input: expected string, received boolean
742
+ → at labels.toolInvocation.invoking"
743
+ `
744
+ );
745
+ });
746
+
747
+ test("errors when labels.toolInvocation.invoking in @tool is not a string", async () => {
748
+ vol.fromJSON({
749
+ "package.json": mockPackageJson(),
750
+ "src/my-component.tsx": declareOperation(gql`
751
+ query HelloWorldQuery
752
+ @tool(
753
+ name: "test"
754
+ description: "Test"
755
+ labels: { toolInvocation: { invoking: true } }
756
+ ) {
757
+ helloWorld
758
+ }
759
+ `),
760
+ });
761
+
762
+ await expect(async () => {
763
+ await using server = await setupServer({
764
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
765
+ });
766
+ await server.listen();
767
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
768
+ `
769
+ [Error: ✖ Invalid input: expected string, received boolean
770
+ → at labels.toolInvocation.invoking]
771
+ `
772
+ );
773
+ });
774
+
775
+ test("errors when labels.toolInvocation.invoked in package.json is not a string", async () => {
776
+ vol.fromJSON({
777
+ "package.json": mockPackageJson({
778
+ "apollo-client-ai-apps": {
779
+ labels: {
780
+ toolInvocation: {
781
+ invoked: true,
782
+ },
783
+ },
784
+ },
785
+ }),
786
+ "src/my-component.tsx": declareOperation(gql`
787
+ query HelloWorldQuery @tool(name: "test", description: "Test") {
788
+ helloWorld
789
+ }
790
+ `),
791
+ });
792
+
793
+ await expect(async () => {
794
+ await using server = await setupServer({
795
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
796
+ });
797
+ await server.listen();
798
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
799
+ `
800
+ "✖ Invalid input: expected string, received boolean
801
+ → at labels.toolInvocation.invoked"
802
+ `
803
+ );
804
+ });
805
+
806
+ test("errors when labels.toolInvocation.invoked in @tool is not a string", async () => {
807
+ vol.fromJSON({
808
+ "package.json": mockPackageJson(),
809
+ "src/my-component.tsx": declareOperation(gql`
810
+ query HelloWorldQuery
811
+ @tool(
812
+ name: "test"
813
+ description: "Test"
814
+ labels: { toolInvocation: { invoked: true } }
815
+ ) {
816
+ helloWorld
817
+ }
818
+ `),
819
+ });
820
+
821
+ await expect(async () => {
822
+ await using server = await setupServer({
823
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
824
+ });
825
+ await server.listen();
826
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
827
+ `
828
+ [Error: ✖ Invalid input: expected string, received boolean
829
+ → at labels.toolInvocation.invoked]
830
+ `
831
+ );
832
+ });
833
+
834
+ test("allows empty labels value", async () => {
835
+ vol.fromJSON({
836
+ "package.json": mockPackageJson({ labels: {} }),
837
+ "src/my-component.tsx": declareOperation(gql`
838
+ query HelloWorldQuery
839
+ @tool(name: "test", description: "Test", labels: {}) {
840
+ helloWorld
841
+ }
842
+ `),
843
+ });
844
+
845
+ await using server = await setupServer({
846
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
847
+ });
848
+ await server.listen();
849
+
850
+ const manifest = readManifestFile();
851
+ expect(manifest.operations).toHaveLength(1);
852
+ });
853
+ });
854
+
855
+ describe("entry points", () => {
856
+ test("uses custom entry point when in serve mode and provided in package.json", async () => {
857
+ vol.fromJSON({
858
+ "package.json": mockPackageJson({
859
+ "apollo-client-ai-apps": {
860
+ entry: {
861
+ staging: "http://staging.awesome.com",
862
+ },
863
+ },
864
+ }),
865
+ "src/my-component.tsx": declareOperation(gql`
866
+ query HelloWorldQuery
867
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
868
+ helloWorld
869
+ }
870
+ `),
871
+ });
872
+
873
+ await using server = await setupServer({
874
+ mode: "staging",
875
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
876
+ });
877
+ await server.listen();
878
+
879
+ const manifest = readManifestFile();
880
+ expect(manifest.resource).toBe("http://staging.awesome.com");
881
+ });
882
+
883
+ test("uses custom entry point for devTarget when in serve mode and provided in package.json", async () => {
884
+ vol.fromJSON({
885
+ "package.json": mockPackageJson({
886
+ "apollo-client-ai-apps": {
887
+ entry: {
888
+ staging: {
889
+ mcp: "http://staging.awesome.com",
890
+ },
891
+ },
892
+ },
893
+ }),
894
+ "src/my-component.tsx": declareOperation(gql`
895
+ query HelloWorldQuery
896
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
897
+ helloWorld
898
+ }
899
+ `),
900
+ });
901
+
902
+ await using server = await setupServer({
903
+ mode: "staging",
904
+ plugins: [apolloClientAiApps({ targets: ["mcp"], devTarget: "mcp" })],
905
+ });
906
+ await server.listen();
907
+
908
+ const manifest = readManifestFile();
909
+ expect(manifest.resource).toBe("http://staging.awesome.com");
910
+ });
911
+
912
+ test("uses https when enabled in server config", async () => {
913
+ vol.fromJSON({
914
+ "package.json": mockPackageJson(),
915
+ "src/my-component.tsx": declareOperation(gql`
916
+ query HelloWorldQuery
917
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
918
+ helloWorld
919
+ }
920
+ `),
921
+ });
922
+
923
+ await using server = await setupServer({
924
+ server: { https: {}, port: 5678 },
925
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
926
+ });
927
+ await server.listen();
928
+
929
+ const manifest = readManifestFile();
930
+ expect(manifest.resource).toBe("https://localhost:5678");
931
+ });
932
+
933
+ test("uses custom host when specified in server config", async () => {
934
+ vol.fromJSON({
935
+ "package.json": mockPackageJson(),
936
+ "src/my-component.tsx": declareOperation(gql`
937
+ query HelloWorldQuery
938
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
939
+ helloWorld
940
+ }
941
+ `),
942
+ });
943
+
944
+ await using server = await setupServer({
945
+ server: { port: 5678, host: "0.0.0.0" },
946
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
947
+ });
948
+ await server.listen();
949
+
950
+ const manifest = readManifestFile();
951
+ expect(manifest.resource).toBe("http://0.0.0.0:5678");
952
+ });
953
+
954
+ test("uses custom entry point when in build mode and provided in package.json", async () => {
955
+ vol.fromJSON({
956
+ "package.json": mockPackageJson({
957
+ "apollo-client-ai-apps": {
958
+ entry: {
959
+ staging: "http://staging.awesome.com",
960
+ },
961
+ },
962
+ }),
963
+ "src/my-component.tsx": declareOperation(gql`
964
+ query HelloWorldQuery
965
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
966
+ helloWorld
967
+ }
968
+ `),
969
+ });
970
+
971
+ await buildApp({
972
+ mode: "staging",
973
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
974
+ });
975
+
976
+ const manifest = readManifestFile();
977
+ expect(manifest.resource).toEqual({ mcp: "http://staging.awesome.com" });
978
+ });
979
+
980
+ test("uses custom entry point for target when in build mode with multiple targets", async () => {
981
+ vol.fromJSON({
982
+ "package.json": mockPackageJson({
983
+ "apollo-client-ai-apps": {
984
+ entry: {
985
+ staging: {
986
+ mcp: "http://staging-mcp.awesome.com",
987
+ openai: "http://staging-openai.awesome.com",
988
+ },
989
+ },
990
+ },
991
+ }),
992
+ "src/my-component.tsx": declareOperation(gql`
993
+ query HelloWorldQuery
994
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
995
+ helloWorld
996
+ }
997
+ `),
998
+ });
999
+
1000
+ await buildApp({
1001
+ mode: "staging",
1002
+ plugins: [apolloClientAiApps({ targets: ["mcp", "openai"] })],
1003
+ });
1004
+
1005
+ const manifest = readManifestFile();
1006
+ expect(manifest.resource).toEqual({
1007
+ mcp: "http://staging-mcp.awesome.com",
1008
+ openai: "http://staging-openai.awesome.com",
1009
+ });
1010
+ });
1011
+
1012
+ test("uses custom entry point for all targets when in build mode with multiple targets", async () => {
1013
+ vol.fromJSON({
1014
+ "package.json": mockPackageJson({
1015
+ "apollo-client-ai-apps": {
1016
+ entry: {
1017
+ staging: "http://staging.awesome.com",
1018
+ },
1019
+ },
1020
+ }),
1021
+ "src/my-component.tsx": declareOperation(gql`
1022
+ query HelloWorldQuery
1023
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1024
+ helloWorld
1025
+ }
1026
+ `),
1027
+ });
1028
+
1029
+ await buildApp({
1030
+ mode: "staging",
1031
+ plugins: [apolloClientAiApps({ targets: ["mcp", "openai"] })],
1032
+ });
1033
+
1034
+ const manifest = readManifestFile();
1035
+ expect(manifest.resource).toEqual({
1036
+ mcp: "http://staging.awesome.com",
1037
+ openai: "http://staging.awesome.com",
1038
+ });
1039
+ });
1040
+
1041
+ test("uses custom entry point for target when in build mode with single target", async () => {
1042
+ vol.fromJSON({
1043
+ "package.json": mockPackageJson({
1044
+ "apollo-client-ai-apps": {
1045
+ entry: {
1046
+ staging: {
1047
+ mcp: "http://staging-mcp.awesome.com",
1048
+ },
1049
+ },
1050
+ },
1051
+ }),
1052
+ "src/my-component.tsx": declareOperation(gql`
1053
+ query HelloWorldQuery
1054
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1055
+ helloWorld
1056
+ }
1057
+ `),
1058
+ });
1059
+
1060
+ await buildApp({
1061
+ mode: "staging",
1062
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1063
+ });
1064
+
1065
+ const manifest = readManifestFile();
1066
+ expect(manifest.resource).toEqual({
1067
+ mcp: "http://staging-mcp.awesome.com",
1068
+ });
1069
+ });
1070
+
1071
+ test("uses [target]/index.html when in build production and not provided in package.json with single target", async () => {
1072
+ vol.fromJSON({
1073
+ "package.json": mockPackageJson(),
1074
+ "src/my-component.tsx": declareOperation(gql`
1075
+ query HelloWorldQuery
1076
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1077
+ helloWorld
1078
+ }
1079
+ `),
1080
+ });
1081
+
1082
+ await buildApp({
1083
+ mode: "production",
1084
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1085
+ });
1086
+
1087
+ const manifest = readManifestFile();
1088
+ expect(manifest.resource).toEqual({ mcp: "mcp/index.html" });
1089
+ });
1090
+
1091
+ test("uses [target]/index.html when in build production with multiple targets", async () => {
1092
+ vol.fromJSON({
1093
+ "package.json": mockPackageJson(),
1094
+ "src/my-component.tsx": declareOperation(gql`
1095
+ query HelloWorldQuery
1096
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1097
+ helloWorld
1098
+ }
1099
+ `),
1100
+ });
1101
+
1102
+ await buildApp({
1103
+ mode: "production",
1104
+ plugins: [apolloClientAiApps({ targets: ["mcp", "openai"] })],
1105
+ });
1106
+
1107
+ const manifest = readManifestFile();
1108
+ expect(manifest.resource).toEqual({
1109
+ mcp: "mcp/index.html",
1110
+ openai: "openai/index.html",
1111
+ });
1112
+ });
1113
+
1114
+ test("errors when in build mode and using a mode that is not production and not provided in package.json", async () => {
1115
+ vol.fromJSON({
1116
+ "package.json": mockPackageJson(),
1117
+ "src/my-component.tsx": declareOperation(gql`
1118
+ query HelloWorldQuery
1119
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1120
+ helloWorld
1121
+ }
1122
+ `),
1123
+ });
1124
+
1125
+ await expect(
1126
+ async () =>
1127
+ await buildApp({
1128
+ mode: "staging",
1129
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1130
+ })
1131
+ ).rejects.toThrowError(
1132
+ `[@apollo/client-ai-apps/vite] No entry point found for mode "staging". Entry points other than "development" and "production" must be defined in package.json file.`
1133
+ );
1134
+ });
1135
+
1136
+ test("writes to both locations when running in build mode", async () => {
1137
+ vol.fromJSON({
1138
+ "package.json": mockPackageJson(),
1139
+ "src/my-component.tsx": declareOperation(gql`
1140
+ query HelloWorldQuery
1141
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1142
+ helloWorld
1143
+ }
1144
+ `),
1145
+ });
1146
+
1147
+ await buildApp({
1148
+ mode: "production",
1149
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1150
+ });
1151
+
1152
+ expect(vol.existsSync(".application-manifest.json")).toBe(true);
1153
+ expect(vol.existsSync("dist/.application-manifest.json")).toBe(true);
1154
+ });
1155
+
1156
+ test("writes to both locations when running in build mode with multiple targets", async () => {
1157
+ vol.fromJSON({
1158
+ "package.json": mockPackageJson(),
1159
+ "src/my-component.tsx": declareOperation(gql`
1160
+ query HelloWorldQuery
1161
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1162
+ helloWorld
1163
+ }
1164
+ `),
1165
+ });
1166
+
1167
+ await buildApp({
1168
+ mode: "production",
1169
+ plugins: [apolloClientAiApps({ targets: ["mcp", "openai"] })],
1170
+ });
1171
+
1172
+ expect(vol.existsSync(".application-manifest.json")).toBe(true);
1173
+ expect(vol.existsSync("dist/.application-manifest.json")).toBe(true);
1174
+ });
1175
+ });
1176
+
1177
+ describe("config files", () => {
1178
+ const appConfigName = "test-app";
1179
+ const appConfigDescription = "test description";
1180
+
1181
+ const json = JSON.stringify({
1182
+ name: appConfigName,
1183
+ description: appConfigDescription,
1184
+ });
1185
+ const yaml = `
1186
+ name: "${appConfigName}"
1187
+ description: "${appConfigDescription}"
1188
+ `;
1189
+ const cjs = `
1190
+ module.exports = {
1191
+ name: "${appConfigName}",
1192
+ description: "${appConfigDescription}",
1193
+ }
1194
+ `;
1195
+ const mjs = `
1196
+ export default {
1197
+ name: "${appConfigName}",
1198
+ description: "${appConfigDescription}"
1199
+ }
1200
+ `;
1201
+ const ts = `
1202
+ import type { ApolloAiAppsConfig } from "@apollo/client-ai-apps/config";
1203
+
1204
+ const config: ApolloAiAppsConfig.Config = {
1205
+ name: "${appConfigName}",
1206
+ description: "${appConfigDescription}",
1207
+ }
1208
+
1209
+ export default config;
1210
+ `;
1211
+
1212
+ test("reads config from package.json", async () => {
1213
+ vol.fromJSON({
1214
+ "package.json": mockPackageJson({
1215
+ "apollo-client-ai-apps": JSON.parse(json),
1216
+ }),
1217
+ });
1218
+
1219
+ await buildApp({
1220
+ mode: "production",
1221
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1222
+ });
1223
+
1224
+ const manifest = readManifestFile();
1225
+ expect(manifest).toMatchObject({
1226
+ name: appConfigName,
1227
+ description: appConfigDescription,
1228
+ });
1229
+ });
1230
+
1231
+ test.each([
1232
+ [".apollo-client-ai-apps.config.json", json],
1233
+ ["apollo-client-ai-apps.config.json", json],
1234
+ [".apollo-client-ai-apps.config.yml", yaml],
1235
+ ["apollo-client-ai-apps.config.yml", yaml],
1236
+ [".apollo-client-ai-apps.config.yaml", yaml],
1237
+ ["apollo-client-ai-apps.config.yaml", yaml],
1238
+ [".apollo-client-ai-apps.config.js", cjs],
1239
+ ["apollo-client-ai-apps.config.js", cjs],
1240
+ [".apollo-client-ai-apps.config.ts", ts],
1241
+ ["apollo-client-ai-apps.config.ts", ts],
1242
+ [".apollo-client-ai-apps.config.cjs", cjs],
1243
+ ["apollo-client-ai-apps.config.cjs", cjs],
1244
+ [".apollo-client-ai-apps.config.mjs", mjs],
1245
+ ["apollo-client-ai-apps.config.mjs", mjs],
1246
+ ])("reads config from %s", async (filepath, contents) => {
1247
+ using _ = interceptWriteESMtoCJS();
1248
+ using __ = await tmpWriteRealFile(filepath, contents);
1249
+
1250
+ vol.fromJSON({
1251
+ "package.json": mockPackageJson(),
1252
+ [filepath]: contents.trimStart(),
1253
+ });
1254
+
1255
+ await buildApp({
1256
+ mode: "production",
1257
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1258
+ });
1259
+
1260
+ const manifest = readManifestFile();
1261
+ expect(manifest).toMatchObject({
1262
+ name: appConfigName,
1263
+ description: appConfigDescription,
1264
+ });
1265
+ });
1266
+ });
1267
+
1268
+ describe("file watching", () => {
1269
+ test("updates manifest file when a source file is changed", async () => {
1270
+ vol.fromJSON({
1271
+ "package.json": mockPackageJson(),
1272
+ "src/my-component.tsx": declareOperation(gql`
1273
+ query HelloWorldQuery($name: string!)
1274
+ @tool(
1275
+ name: "hello-world"
1276
+ description: "This is an awesome tool!"
1277
+ extraInputs: [
1278
+ {
1279
+ name: "doStuff"
1280
+ type: "boolean"
1281
+ description: "Should we do stuff?"
1282
+ }
1283
+ ]
1284
+ ) {
1285
+ helloWorld(name: $name)
1286
+ }
1287
+ `),
1288
+ });
1289
+
1290
+ await using server = await setupServer({
1291
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1292
+ });
1293
+ await server.listen();
1294
+
1295
+ const manifestBefore = readManifestFile();
1296
+ expect(manifestBefore.operations).toHaveLength(1);
1297
+
1298
+ vol.writeFileSync(
1299
+ "src/my-component.tsx",
1300
+ declareOperation(gql`
1301
+ query UpdatedQuery($name: string!)
1302
+ @tool(name: "updated-tool", description: "Updated tool!") {
1303
+ updatedWorld(name: $name)
1304
+ }
1305
+ `)
1306
+ );
1307
+
1308
+ server.watcher.emit("change", process.cwd() + "/src/my-component.tsx");
1309
+
1310
+ // Allow async handlers to complete
1311
+ await new Promise((resolve) => setTimeout(resolve, 100));
1312
+
1313
+ const manifestAfter = readManifestFile();
1314
+ expect(manifestAfter.operations).toHaveLength(1);
1315
+ expect(manifestAfter.operations[0].name).toBe("UpdatedQuery");
1316
+ });
1317
+ });
1318
+
1319
+ describe("html transforms", () => {
1320
+ test("replaces root relative scripts with full url when origin is provided", async () => {
1321
+ vol.fromJSON({
1322
+ "package.json": mockPackageJson(),
1323
+ });
1324
+
1325
+ await using server = await setupServer({
1326
+ server: {
1327
+ origin: "http://localhost:3000",
1328
+ },
1329
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1330
+ });
1331
+
1332
+ const html = `<html><head><script type="module" src="/@vite/client"></script></head><body><script module src="/assets/main.ts?t=12345"></script></body></html>`;
1333
+ const result = await server.transformIndexHtml("index.html", html);
1334
+
1335
+ expect(result).toMatchInlineSnapshot(
1336
+ `
1337
+ "<html><head>
1338
+ <script type="module" src="http://localhost:3000/@vite/client"></script>
1339
+ <script type="module" src="http://localhost:3000/@vite/client"></script></head><body><script module src="http://localhost:3000/assets/main.ts?t=12345"></script></body></html>"
1340
+ `
1341
+ );
1342
+ });
1343
+
1344
+ test("replaces root relative scripts with full url when origin is not provided", async () => {
1345
+ vol.fromJSON({
1346
+ "package.json": mockPackageJson(),
1347
+ });
1348
+
1349
+ await using server = await setupServer({
1350
+ server: {
1351
+ port: 3000,
1352
+ },
1353
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1354
+ });
1355
+
1356
+ await server.listen();
1357
+
1358
+ const html = `<html><head> <script type="module">import { injectIntoGlobalHook } from "/@react-refresh";
1359
+ injectIntoGlobalHook(window);
1360
+ window.$RefreshReg$ = () => {};
1361
+ window.$RefreshSig$ = () => (type) => type;</script></head></html>`;
1362
+
1363
+ const result = await server.transformIndexHtml("index.html", html);
1364
+
1365
+ expect(result).toMatchInlineSnapshot(`
1366
+ "<html><head>
1367
+ <script type="module" src="http://localhost:3000/@vite/client"></script>
1368
+ <script type="module" src="http://localhost:3000/@id/__x00__index.html?html-proxy&index=0.js"></script></head></html>"
1369
+ `);
1370
+ });
1371
+
1372
+ test("replaces root relative imports with full url when origin is provided", async () => {
1373
+ vol.fromJSON({
1374
+ "package.json": mockPackageJson(),
1375
+ });
1376
+
1377
+ await using server = await setupServer({
1378
+ server: {
1379
+ origin: "http://localhost:3000",
1380
+ },
1381
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1382
+ });
1383
+
1384
+ const html = `<html><head> <script type="module">import { injectIntoGlobalHook } from "/@react-refresh";
1385
+ injectIntoGlobalHook(window);
1386
+ window.$RefreshReg$ = () => {};
1387
+ window.$RefreshSig$ = () => (type) => type;</script></head></html>`;
1388
+
1389
+ const result = await server.transformIndexHtml("index.html", html);
1390
+
1391
+ expect(result).toMatchInlineSnapshot(`
1392
+ "<html><head>
1393
+ <script type="module" src="http://localhost:3000/@vite/client"></script>
1394
+ <script type="module" src="http://localhost:3000/@id/__x00__index.html?html-proxy&index=0.js"></script></head></html>"
1395
+ `);
1396
+ });
1397
+
1398
+ test("replaces root relative imports with full url when origin is not provided", async () => {
1399
+ vol.fromJSON({
1400
+ "package.json": mockPackageJson(),
1401
+ });
1402
+
1403
+ await using server = await setupServer({
1404
+ server: {
1405
+ port: 3000,
1406
+ },
1407
+ plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1408
+ });
1409
+
1410
+ await server.listen();
1411
+
1412
+ const html = `<html><body><script module src="/assets/main.ts?t=12345"></script></body></html>`;
1413
+
1414
+ const result = await server.transformIndexHtml("index.html", html);
1415
+
1416
+ expect(result).toMatchInlineSnapshot(`
1417
+ "<html>
1418
+ <script type="module" src="http://localhost:3000/@vite/client"></script>
1419
+ <body><script module src="http://localhost:3000/assets/main.ts?t=12345"></script></body></html>"
1420
+ `);
1421
+ });
1422
+
1423
+ test("does not prepend absolute urls when running a build instead of a local server", async () => {
1424
+ vol.fromJSON({
1425
+ "package.json": mockPackageJson(),
1426
+ "index.html": `<html><head></head><body><script type="module" src="/src/main.ts"></script></body></html>`,
1427
+ "src/main.ts": "export default {};",
1428
+ });
1429
+
1430
+ let transformedHtml: string | undefined;
1431
+
1432
+ await buildApp({
1433
+ build: { rollupOptions: { input: "index.html" } },
1434
+ server: {
1435
+ origin: "http://localhost:3000",
1436
+ },
1437
+ plugins: [
1438
+ apolloClientAiApps({ targets: ["mcp"] }),
1439
+ {
1440
+ name: "capture-html",
1441
+ transformIndexHtml: {
1442
+ order: "post",
1443
+ handler(html) {
1444
+ transformedHtml = html;
1445
+ },
1446
+ },
1447
+ },
1448
+ ],
1449
+ });
1450
+
1451
+ expect(transformedHtml).toMatchInlineSnapshot(`
1452
+ "<html><head> <script type="module" crossorigin src="/assets/index-B5Qt9EMX.js"></script>
1453
+ </head><body></body></html>"
1454
+ `);
1455
+ });
1456
+ });
1457
+
1458
+ function declareOperation(operation: DocumentNode) {
1459
+ const name = getOperationName(operation, "MY_OPERATION");
1460
+ const varName = name.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
1461
+ return `const ${varName} = gql\`\n${print(operation)}\n\``;
1462
+ }
1463
+
1464
+ function mockPackageJson(config?: Record<string, unknown>) {
1465
+ return JSON.stringify({ version: "1.0.0", ...config });
1466
+ }
1467
+
1468
+ function readManifestFile(
1469
+ path = ".application-manifest.json"
1470
+ ): ApplicationManifest {
1471
+ const manifest = JSON.parse(fs.readFileSync(path, "utf-8"));
1472
+
1473
+ // Use a deterministic hash for tests to ensure snapshots maintain a
1474
+ // consistent output
1475
+ manifest.hash = "abc";
1476
+
1477
+ return manifest;
1478
+ }
1479
+
1480
+ async function tmpWriteRealFile(filepath: string, contents: string) {
1481
+ vi.doUnmock("node:fs");
1482
+ const fs = await import("node:fs");
1483
+
1484
+ fs.writeFileSync(filepath, contents);
1485
+
1486
+ return {
1487
+ [Symbol.dispose]() {
1488
+ fs.rmSync(filepath);
1489
+ },
1490
+ } satisfies Disposable;
1491
+ }
1492
+
1493
+ function interceptWriteESMtoCJS() {
1494
+ // Cosmiconfig's async loadTs transpiles .ts configs to ES2022 module syntax
1495
+ // and writes a .mjs file. In vitest, the subsequent import() transforms ESM to
1496
+ // CJS differently than Node's native loader, causing the default export to be
1497
+ // double-wrapped as { __esModule: true, default: actualConfig }. Converting to
1498
+ // CJS module.exports before the file is written ensures correct behavior.
1499
+ const origWriteFile = fs.promises.writeFile.bind(fs.promises);
1500
+ (fs.promises as any).writeFile = async function (
1501
+ filepath: any,
1502
+ content: any,
1503
+ ...args: any[]
1504
+ ) {
1505
+ if (
1506
+ typeof filepath === "string" &&
1507
+ filepath.endsWith(".mjs") &&
1508
+ typeof content === "string"
1509
+ ) {
1510
+ content = content.replace(/\bexport\s+default\s+/g, "module.exports = ");
1511
+ }
1512
+ return origWriteFile(filepath, content, ...args);
1513
+ };
1514
+
1515
+ return {
1516
+ ...fs.promises,
1517
+ [Symbol.dispose]() {
1518
+ fs.promises.writeFile = origWriteFile;
1519
+ },
1520
+ };
1521
+ }