@apollo/client-ai-apps 0.5.3 → 0.6.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 (71) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/config/defineConfig.d.ts +1 -0
  3. package/dist/config/defineConfig.d.ts.map +1 -1
  4. package/dist/config/schema.d.ts +1 -0
  5. package/dist/config/schema.d.ts.map +1 -1
  6. package/dist/config/schema.js +1 -0
  7. package/dist/config/schema.js.map +1 -1
  8. package/dist/mcp/core/McpAppManager.d.ts +2 -1
  9. package/dist/mcp/core/McpAppManager.d.ts.map +1 -1
  10. package/dist/mcp/core/McpAppManager.js +11 -1
  11. package/dist/mcp/core/McpAppManager.js.map +1 -1
  12. package/dist/mcp/react/hooks/useHostContext.d.ts +2 -0
  13. package/dist/mcp/react/hooks/useHostContext.d.ts.map +1 -0
  14. package/dist/mcp/react/hooks/useHostContext.js +7 -0
  15. package/dist/mcp/react/hooks/useHostContext.js.map +1 -0
  16. package/dist/mcp/react/index.d.ts +1 -0
  17. package/dist/mcp/react/index.d.ts.map +1 -1
  18. package/dist/mcp/react/index.js +1 -0
  19. package/dist/mcp/react/index.js.map +1 -1
  20. package/dist/openai/core/McpAppManager.d.ts +2 -1
  21. package/dist/openai/core/McpAppManager.d.ts.map +1 -1
  22. package/dist/openai/core/McpAppManager.js +11 -1
  23. package/dist/openai/core/McpAppManager.js.map +1 -1
  24. package/dist/openai/react/hooks/useHostContext.d.ts +2 -0
  25. package/dist/openai/react/hooks/useHostContext.d.ts.map +1 -0
  26. package/dist/openai/react/hooks/useHostContext.js +7 -0
  27. package/dist/openai/react/hooks/useHostContext.js.map +1 -0
  28. package/dist/openai/react/index.d.ts +1 -0
  29. package/dist/openai/react/index.d.ts.map +1 -1
  30. package/dist/openai/react/index.js +1 -0
  31. package/dist/openai/react/index.js.map +1 -1
  32. package/dist/react/index.d.ts +1 -0
  33. package/dist/react/index.d.ts.map +1 -1
  34. package/dist/react/index.js +1 -0
  35. package/dist/react/index.js.map +1 -1
  36. package/dist/react/index.mcp.d.ts +1 -1
  37. package/dist/react/index.mcp.d.ts.map +1 -1
  38. package/dist/react/index.mcp.js +1 -1
  39. package/dist/react/index.mcp.js.map +1 -1
  40. package/dist/react/index.openai.d.ts +1 -1
  41. package/dist/react/index.openai.d.ts.map +1 -1
  42. package/dist/react/index.openai.js +1 -1
  43. package/dist/react/index.openai.js.map +1 -1
  44. package/dist/types/application-manifest.d.ts +1 -0
  45. package/dist/types/application-manifest.d.ts.map +1 -1
  46. package/dist/types/application-manifest.js.map +1 -1
  47. package/dist/vite/__tests__/utilities/build.d.ts.map +1 -1
  48. package/dist/vite/__tests__/utilities/build.js +0 -1
  49. package/dist/vite/__tests__/utilities/build.js.map +1 -1
  50. package/dist/vite/apolloClientAiApps.d.ts +1 -0
  51. package/dist/vite/apolloClientAiApps.d.ts.map +1 -1
  52. package/dist/vite/apolloClientAiApps.js +63 -46
  53. package/dist/vite/apolloClientAiApps.js.map +1 -1
  54. package/package.json +1 -4
  55. package/src/config/schema.ts +1 -0
  56. package/src/mcp/core/McpAppManager.ts +23 -1
  57. package/src/mcp/react/hooks/__tests__/useHostContext.test.tsx +95 -0
  58. package/src/mcp/react/hooks/useHostContext.ts +14 -0
  59. package/src/mcp/react/index.ts +1 -0
  60. package/src/openai/core/McpAppManager.ts +22 -1
  61. package/src/openai/react/hooks/useHostContext.ts +14 -0
  62. package/src/openai/react/index.ts +1 -0
  63. package/src/react/index.mcp.ts +1 -0
  64. package/src/react/index.openai.ts +1 -0
  65. package/src/react/index.ts +3 -0
  66. package/src/testing/internal/mcp/mockMcpHost.ts +12 -0
  67. package/src/testing/internal/utilities/mockApplicationManifest.ts +1 -0
  68. package/src/types/application-manifest.ts +1 -0
  69. package/src/vite/__tests__/apolloClientAiApps.test.ts +460 -61
  70. package/src/vite/__tests__/utilities/build.ts +0 -1
  71. package/src/vite/apolloClientAiApps.ts +100 -57
@@ -1,16 +1,17 @@
1
1
  import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { spyOnConsole } from "../../testing/internal/index.js";
2
3
  import fs from "node:fs";
3
4
  import { gql, type DocumentNode } from "@apollo/client";
4
- import { print } from "@apollo/client/utilities";
5
+ import { getMainDefinition, print } from "@apollo/client/utilities";
5
6
  import { getOperationName } from "@apollo/client/utilities/internal";
6
7
  import { vol } from "memfs";
7
8
  import { apolloClientAiApps } from "../apolloClientAiApps.js";
8
9
  import { buildApp, setupServer } from "./utilities/build.js";
9
- import type {
10
- ApplicationManifest,
11
- ManifestWidgetSettings,
12
- } from "../../types/application-manifest.js";
10
+ import type { ApplicationManifest } from "../../types/application-manifest.js";
13
11
  import { explorer } from "../utilities/config.js";
12
+ import { invariant } from "@apollo/client/utilities/invariant";
13
+ import { Kind } from "graphql";
14
+ import type { ApolloClientAiAppsConfig } from "../../config/types.js";
14
15
 
15
16
  beforeEach(() => {
16
17
  explorer.clearCaches();
@@ -27,12 +28,19 @@ describe("operations", () => {
27
28
  invoked: "Tested global!",
28
29
  },
29
30
  },
31
+ csp: {
32
+ baseUriDomains: ["https://base.example.com"],
33
+ connectDomains: ["https://connect.example.com"],
34
+ frameDomains: ["https://frame.example.com"],
35
+ redirectDomains: ["https://redirect.example.com"],
36
+ resourceDomains: ["https://resource.example.com"],
37
+ },
30
38
  widgetSettings: {
31
39
  description: "Test",
32
40
  domain: "https://example.com",
33
41
  prefersBorder: true,
34
- } satisfies ManifestWidgetSettings,
35
- },
42
+ },
43
+ } satisfies ApolloClientAiAppsConfig.Config,
36
44
  }),
37
45
  "src/my-component.tsx": declareOperation(gql`
38
46
  query HelloWorldQuery($name: string!)
@@ -59,7 +67,9 @@ describe("operations", () => {
59
67
  });
60
68
 
61
69
  await using server = await setupServer({
62
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
70
+ plugins: [
71
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
72
+ ],
63
73
  });
64
74
  await server.listen();
65
75
 
@@ -68,10 +78,21 @@ describe("operations", () => {
68
78
  {
69
79
  "appVersion": "1.0.0",
70
80
  "csp": {
71
- "connectDomains": [],
72
- "frameDomains": [],
73
- "redirectDomains": [],
74
- "resourceDomains": [],
81
+ "baseUriDomains": [
82
+ "https://base.example.com",
83
+ ],
84
+ "connectDomains": [
85
+ "https://connect.example.com",
86
+ ],
87
+ "frameDomains": [
88
+ "https://frame.example.com",
89
+ ],
90
+ "redirectDomains": [
91
+ "https://redirect.example.com",
92
+ ],
93
+ "resourceDomains": [
94
+ "https://resource.example.com",
95
+ ],
75
96
  },
76
97
  "format": "apollo-ai-app-manifest",
77
98
  "hash": "abc",
@@ -79,6 +100,7 @@ describe("operations", () => {
79
100
  "toolInvocation/invoked": "Tested global!",
80
101
  "toolInvocation/invoking": "Testing global...",
81
102
  },
103
+ "name": "my-app",
82
104
  "operations": [
83
105
  {
84
106
  "body": "query HelloWorldQuery($name: string!) {
@@ -121,6 +143,181 @@ describe("operations", () => {
121
143
  `);
122
144
  });
123
145
 
146
+ test("handles operations with fragments in the same file", async () => {
147
+ vol.fromJSON({
148
+ "package.json": mockPackageJson(),
149
+ "src/my-component.tsx": declareOperation(gql`
150
+ query HelloWorldQuery($name: string!)
151
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
152
+ greeting {
153
+ message
154
+ recipient {
155
+ ...RecipientFragment
156
+ }
157
+ }
158
+ }
159
+
160
+ fragment RecipientFragment on Recipient {
161
+ id
162
+ name
163
+ }
164
+ `),
165
+ });
166
+
167
+ await using server = await setupServer({
168
+ plugins: [
169
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
170
+ ],
171
+ });
172
+ await server.listen();
173
+
174
+ const manifest = readManifestFile();
175
+ expect(manifest).toMatchInlineSnapshot(`
176
+ {
177
+ "appVersion": "1.0.0",
178
+ "csp": {
179
+ "baseUriDomains": [],
180
+ "connectDomains": [],
181
+ "frameDomains": [],
182
+ "redirectDomains": [],
183
+ "resourceDomains": [],
184
+ },
185
+ "format": "apollo-ai-app-manifest",
186
+ "hash": "abc",
187
+ "name": "my-app",
188
+ "operations": [
189
+ {
190
+ "body": "query HelloWorldQuery {
191
+ greeting {
192
+ message
193
+ recipient {
194
+ ...RecipientFragment
195
+ __typename
196
+ }
197
+ __typename
198
+ }
199
+ }
200
+
201
+ fragment RecipientFragment on Recipient {
202
+ id
203
+ name
204
+ __typename
205
+ }",
206
+ "id": "1646a86ae2ff5ad75457161be5cff80f3ba5172da573a0fc796b268870119020",
207
+ "name": "HelloWorldQuery",
208
+ "prefetch": false,
209
+ "tools": [
210
+ {
211
+ "description": "This is an awesome tool!",
212
+ "name": "hello-world",
213
+ },
214
+ ],
215
+ "type": "query",
216
+ "variables": {
217
+ "name": "string",
218
+ },
219
+ },
220
+ ],
221
+ "resource": "http://localhost:3333",
222
+ "version": "1",
223
+ }
224
+ `);
225
+ });
226
+
227
+ test("handles operations with fragments in other files", async () => {
228
+ vol.fromJSON({
229
+ "package.json": mockPackageJson(),
230
+ "src/first-recipient.tsx": declareFragment(gql`
231
+ fragment OtherRecipientFragment on Recipient {
232
+ name
233
+ }
234
+ `),
235
+ "src/my-component.tsx": declareOperation(gql`
236
+ query HelloWorldQuery($name: string!)
237
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
238
+ greeting {
239
+ message
240
+ recipient {
241
+ ...RecipientFragment
242
+ ...OtherRecipientFragment
243
+ }
244
+ }
245
+ }
246
+ `),
247
+ "src/my-fragment.tsx": declareFragment(gql`
248
+ fragment RecipientFragment on Recipient {
249
+ id
250
+ name
251
+ }
252
+ `),
253
+ });
254
+
255
+ await using server = await setupServer({
256
+ plugins: [
257
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
258
+ ],
259
+ });
260
+ await server.listen();
261
+
262
+ const manifest = readManifestFile();
263
+ expect(manifest).toMatchInlineSnapshot(`
264
+ {
265
+ "appVersion": "1.0.0",
266
+ "csp": {
267
+ "baseUriDomains": [],
268
+ "connectDomains": [],
269
+ "frameDomains": [],
270
+ "redirectDomains": [],
271
+ "resourceDomains": [],
272
+ },
273
+ "format": "apollo-ai-app-manifest",
274
+ "hash": "abc",
275
+ "name": "my-app",
276
+ "operations": [
277
+ {
278
+ "body": "query HelloWorldQuery {
279
+ greeting {
280
+ message
281
+ recipient {
282
+ ...RecipientFragment
283
+ ...OtherRecipientFragment
284
+ __typename
285
+ }
286
+ __typename
287
+ }
288
+ }
289
+
290
+ fragment OtherRecipientFragment on Recipient {
291
+ name
292
+ __typename
293
+ }
294
+
295
+ fragment RecipientFragment on Recipient {
296
+ id
297
+ name
298
+ __typename
299
+ }",
300
+ "id": "c65cb5ec2dc76bbfa992cd2a98c05ab3f909349f3f1478e608a7f16ae29bdd4a",
301
+ "name": "HelloWorldQuery",
302
+ "prefetch": false,
303
+ "tools": [
304
+ {
305
+ "description": "This is an awesome tool!",
306
+ "name": "hello-world",
307
+ },
308
+ ],
309
+ "type": "query",
310
+ "variables": {
311
+ "name": "string",
312
+ },
313
+ },
314
+ ],
315
+ "resource": "http://localhost:3333",
316
+ "version": "1",
317
+ }
318
+ `);
319
+ });
320
+
124
321
  test("does not write to dev application manifest file when using a build command", async () => {
125
322
  vol.fromJSON({
126
323
  "package.json": mockPackageJson(),
@@ -134,7 +331,9 @@ describe("operations", () => {
134
331
 
135
332
  await buildApp({
136
333
  mode: "production",
137
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
334
+ plugins: [
335
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
336
+ ],
138
337
  });
139
338
 
140
339
  const manifest = readManifestFile();
@@ -151,7 +350,9 @@ describe("operations", () => {
151
350
  });
152
351
 
153
352
  await using server = await setupServer({
154
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
353
+ plugins: [
354
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
355
+ ],
155
356
  });
156
357
  await server.listen();
157
358
 
@@ -160,6 +361,7 @@ describe("operations", () => {
160
361
  {
161
362
  "appVersion": "1.0.0",
162
363
  "csp": {
364
+ "baseUriDomains": [],
163
365
  "connectDomains": [],
164
366
  "frameDomains": [],
165
367
  "redirectDomains": [],
@@ -167,6 +369,7 @@ describe("operations", () => {
167
369
  },
168
370
  "format": "apollo-ai-app-manifest",
169
371
  "hash": "abc",
372
+ "name": "my-app",
170
373
  "operations": [],
171
374
  "resource": "http://localhost:3333",
172
375
  "version": "1",
@@ -185,7 +388,9 @@ describe("operations", () => {
185
388
  });
186
389
 
187
390
  await using server = await setupServer({
188
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
391
+ plugins: [
392
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
393
+ ],
189
394
  });
190
395
  await server.listen();
191
396
 
@@ -194,6 +399,7 @@ describe("operations", () => {
194
399
  {
195
400
  "appVersion": "1.0.0",
196
401
  "csp": {
402
+ "baseUriDomains": [],
197
403
  "connectDomains": [],
198
404
  "frameDomains": [],
199
405
  "redirectDomains": [],
@@ -201,6 +407,7 @@ describe("operations", () => {
201
407
  },
202
408
  "format": "apollo-ai-app-manifest",
203
409
  "hash": "abc",
410
+ "name": "my-app",
204
411
  "operations": [
205
412
  {
206
413
  "body": "query HelloWorldQuery {
@@ -232,7 +439,9 @@ describe("operations", () => {
232
439
  });
233
440
 
234
441
  await using server = await setupServer({
235
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
442
+ plugins: [
443
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
444
+ ],
236
445
  });
237
446
  await server.listen();
238
447
 
@@ -241,6 +450,7 @@ describe("operations", () => {
241
450
  {
242
451
  "appVersion": "1.0.0",
243
452
  "csp": {
453
+ "baseUriDomains": [],
244
454
  "connectDomains": [],
245
455
  "frameDomains": [],
246
456
  "redirectDomains": [],
@@ -248,6 +458,7 @@ describe("operations", () => {
248
458
  },
249
459
  "format": "apollo-ai-app-manifest",
250
460
  "hash": "abc",
461
+ "name": "my-app",
251
462
  "operations": [
252
463
  {
253
464
  "body": "mutation HelloWorldQuery {
@@ -285,11 +496,13 @@ describe("operations", () => {
285
496
 
286
497
  await expect(async () => {
287
498
  await using server = await setupServer({
288
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
499
+ plugins: [
500
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
501
+ ],
289
502
  });
290
503
  await server.listen();
291
504
  }).rejects.toThrowErrorMatchingInlineSnapshot(
292
- `[Error: Found an unsupported operation type. Only Query and Mutation are supported.]`
505
+ `[Error: Found unsupported operation type 'subscription'. Only queries and mutations are supported.]`
293
506
  );
294
507
  });
295
508
 
@@ -313,7 +526,9 @@ describe("operations", () => {
313
526
  });
314
527
 
315
528
  await using server = await setupServer({
316
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
529
+ plugins: [
530
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
531
+ ],
317
532
  });
318
533
  await server.listen();
319
534
 
@@ -322,6 +537,7 @@ describe("operations", () => {
322
537
  {
323
538
  "appVersion": "1.0.0",
324
539
  "csp": {
540
+ "baseUriDomains": [],
325
541
  "connectDomains": [],
326
542
  "frameDomains": [],
327
543
  "redirectDomains": [],
@@ -329,6 +545,7 @@ describe("operations", () => {
329
545
  },
330
546
  "format": "apollo-ai-app-manifest",
331
547
  "hash": "abc",
548
+ "name": "my-app",
332
549
  "operations": [
333
550
  {
334
551
  "body": "query HelloWorldQuery {
@@ -388,7 +605,9 @@ describe("@prefetch", () => {
388
605
  });
389
606
 
390
607
  await using server = await setupServer({
391
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
608
+ plugins: [
609
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
610
+ ],
392
611
  });
393
612
  await server.listen();
394
613
 
@@ -397,6 +616,7 @@ describe("@prefetch", () => {
397
616
  {
398
617
  "appVersion": "1.0.0",
399
618
  "csp": {
619
+ "baseUriDomains": [],
400
620
  "connectDomains": [],
401
621
  "frameDomains": [],
402
622
  "redirectDomains": [],
@@ -404,6 +624,7 @@ describe("@prefetch", () => {
404
624
  },
405
625
  "format": "apollo-ai-app-manifest",
406
626
  "hash": "abc",
627
+ "name": "my-app",
407
628
  "operations": [
408
629
  {
409
630
  "body": "query HelloWorldQuery {
@@ -443,7 +664,9 @@ describe("@prefetch", () => {
443
664
 
444
665
  await expect(async () => {
445
666
  await using server = await setupServer({
446
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
667
+ plugins: [
668
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
669
+ ],
447
670
  });
448
671
  await server.listen();
449
672
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -465,7 +688,9 @@ describe("@tool validation", () => {
465
688
 
466
689
  await expect(async () => {
467
690
  await using server = await setupServer({
468
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
691
+ plugins: [
692
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
693
+ ],
469
694
  });
470
695
  await server.listen();
471
696
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -485,7 +710,9 @@ describe("@tool validation", () => {
485
710
 
486
711
  await expect(async () => {
487
712
  await using server = await setupServer({
488
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
713
+ plugins: [
714
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
715
+ ],
489
716
  });
490
717
  await server.listen();
491
718
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -506,7 +733,9 @@ describe("@tool validation", () => {
506
733
 
507
734
  await expect(async () => {
508
735
  await using server = await setupServer({
509
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
736
+ plugins: [
737
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
738
+ ],
510
739
  });
511
740
  await server.listen();
512
741
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -529,7 +758,9 @@ describe("@tool validation", () => {
529
758
 
530
759
  await expect(async () => {
531
760
  await using server = await setupServer({
532
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
761
+ plugins: [
762
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
763
+ ],
533
764
  });
534
765
  await server.listen();
535
766
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -549,7 +780,9 @@ describe("@tool validation", () => {
549
780
 
550
781
  await expect(async () => {
551
782
  await using server = await setupServer({
552
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
783
+ plugins: [
784
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
785
+ ],
553
786
  });
554
787
  await server.listen();
555
788
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -570,7 +803,9 @@ describe("@tool validation", () => {
570
803
 
571
804
  await expect(async () => {
572
805
  await using server = await setupServer({
573
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
806
+ plugins: [
807
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
808
+ ],
574
809
  });
575
810
  await server.listen();
576
811
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -595,7 +830,9 @@ describe("@tool validation", () => {
595
830
 
596
831
  await expect(async () => {
597
832
  await using server = await setupServer({
598
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
833
+ plugins: [
834
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
835
+ ],
599
836
  });
600
837
  await server.listen();
601
838
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -623,7 +860,9 @@ describe("config validation", () => {
623
860
 
624
861
  await expect(async () => {
625
862
  await using server = await setupServer({
626
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
863
+ plugins: [
864
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
865
+ ],
627
866
  });
628
867
  await server.listen();
629
868
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -652,7 +891,9 @@ describe("config validation", () => {
652
891
 
653
892
  await expect(async () => {
654
893
  await using server = await setupServer({
655
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
894
+ plugins: [
895
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
896
+ ],
656
897
  });
657
898
  await server.listen();
658
899
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -681,7 +922,9 @@ describe("config validation", () => {
681
922
 
682
923
  await expect(async () => {
683
924
  await using server = await setupServer({
684
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
925
+ plugins: [
926
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
927
+ ],
685
928
  });
686
929
  await server.listen();
687
930
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -705,7 +948,9 @@ describe("config validation", () => {
705
948
  });
706
949
 
707
950
  await using server = await setupServer({
708
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
951
+ plugins: [
952
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
953
+ ],
709
954
  });
710
955
  await server.listen();
711
956
 
@@ -733,7 +978,9 @@ describe("config validation", () => {
733
978
 
734
979
  await expect(async () => {
735
980
  await using server = await setupServer({
736
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
981
+ plugins: [
982
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
983
+ ],
737
984
  });
738
985
  await server.listen();
739
986
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -761,7 +1008,9 @@ describe("config validation", () => {
761
1008
 
762
1009
  await expect(async () => {
763
1010
  await using server = await setupServer({
764
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1011
+ plugins: [
1012
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1013
+ ],
765
1014
  });
766
1015
  await server.listen();
767
1016
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -792,7 +1041,9 @@ describe("config validation", () => {
792
1041
 
793
1042
  await expect(async () => {
794
1043
  await using server = await setupServer({
795
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1044
+ plugins: [
1045
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1046
+ ],
796
1047
  });
797
1048
  await server.listen();
798
1049
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -820,7 +1071,9 @@ describe("config validation", () => {
820
1071
 
821
1072
  await expect(async () => {
822
1073
  await using server = await setupServer({
823
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1074
+ plugins: [
1075
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1076
+ ],
824
1077
  });
825
1078
  await server.listen();
826
1079
  }).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -843,7 +1096,9 @@ describe("config validation", () => {
843
1096
  });
844
1097
 
845
1098
  await using server = await setupServer({
846
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1099
+ plugins: [
1100
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1101
+ ],
847
1102
  });
848
1103
  await server.listen();
849
1104
 
@@ -872,7 +1127,9 @@ describe("entry points", () => {
872
1127
 
873
1128
  await using server = await setupServer({
874
1129
  mode: "staging",
875
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1130
+ plugins: [
1131
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1132
+ ],
876
1133
  });
877
1134
  await server.listen();
878
1135
 
@@ -901,7 +1158,13 @@ describe("entry points", () => {
901
1158
 
902
1159
  await using server = await setupServer({
903
1160
  mode: "staging",
904
- plugins: [apolloClientAiApps({ targets: ["mcp"], devTarget: "mcp" })],
1161
+ plugins: [
1162
+ apolloClientAiApps({
1163
+ targets: ["mcp"],
1164
+ devTarget: "mcp",
1165
+ appsOutDir: "dist/apps",
1166
+ }),
1167
+ ],
905
1168
  });
906
1169
  await server.listen();
907
1170
 
@@ -922,7 +1185,9 @@ describe("entry points", () => {
922
1185
 
923
1186
  await using server = await setupServer({
924
1187
  server: { https: {}, port: 5678 },
925
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1188
+ plugins: [
1189
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1190
+ ],
926
1191
  });
927
1192
  await server.listen();
928
1193
 
@@ -943,7 +1208,9 @@ describe("entry points", () => {
943
1208
 
944
1209
  await using server = await setupServer({
945
1210
  server: { port: 5678, host: "0.0.0.0" },
946
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1211
+ plugins: [
1212
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1213
+ ],
947
1214
  });
948
1215
  await server.listen();
949
1216
 
@@ -970,7 +1237,9 @@ describe("entry points", () => {
970
1237
 
971
1238
  await buildApp({
972
1239
  mode: "staging",
973
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1240
+ plugins: [
1241
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1242
+ ],
974
1243
  });
975
1244
 
976
1245
  const manifest = readManifestFile();
@@ -999,7 +1268,12 @@ describe("entry points", () => {
999
1268
 
1000
1269
  await buildApp({
1001
1270
  mode: "staging",
1002
- plugins: [apolloClientAiApps({ targets: ["mcp", "openai"] })],
1271
+ plugins: [
1272
+ apolloClientAiApps({
1273
+ targets: ["mcp", "openai"],
1274
+ appsOutDir: "dist/apps",
1275
+ }),
1276
+ ],
1003
1277
  });
1004
1278
 
1005
1279
  const manifest = readManifestFile();
@@ -1028,7 +1302,12 @@ describe("entry points", () => {
1028
1302
 
1029
1303
  await buildApp({
1030
1304
  mode: "staging",
1031
- plugins: [apolloClientAiApps({ targets: ["mcp", "openai"] })],
1305
+ plugins: [
1306
+ apolloClientAiApps({
1307
+ targets: ["mcp", "openai"],
1308
+ appsOutDir: "dist/apps",
1309
+ }),
1310
+ ],
1032
1311
  });
1033
1312
 
1034
1313
  const manifest = readManifestFile();
@@ -1059,7 +1338,9 @@ describe("entry points", () => {
1059
1338
 
1060
1339
  await buildApp({
1061
1340
  mode: "staging",
1062
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1341
+ plugins: [
1342
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1343
+ ],
1063
1344
  });
1064
1345
 
1065
1346
  const manifest = readManifestFile();
@@ -1081,7 +1362,9 @@ describe("entry points", () => {
1081
1362
 
1082
1363
  await buildApp({
1083
1364
  mode: "production",
1084
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1365
+ plugins: [
1366
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1367
+ ],
1085
1368
  });
1086
1369
 
1087
1370
  const manifest = readManifestFile();
@@ -1101,7 +1384,12 @@ describe("entry points", () => {
1101
1384
 
1102
1385
  await buildApp({
1103
1386
  mode: "production",
1104
- plugins: [apolloClientAiApps({ targets: ["mcp", "openai"] })],
1387
+ plugins: [
1388
+ apolloClientAiApps({
1389
+ targets: ["mcp", "openai"],
1390
+ appsOutDir: "dist/apps",
1391
+ }),
1392
+ ],
1105
1393
  });
1106
1394
 
1107
1395
  const manifest = readManifestFile();
@@ -1126,7 +1414,9 @@ describe("entry points", () => {
1126
1414
  async () =>
1127
1415
  await buildApp({
1128
1416
  mode: "staging",
1129
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1417
+ plugins: [
1418
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1419
+ ],
1130
1420
  })
1131
1421
  ).rejects.toThrowError(
1132
1422
  `[@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.`
@@ -1146,11 +1436,15 @@ describe("entry points", () => {
1146
1436
 
1147
1437
  await buildApp({
1148
1438
  mode: "production",
1149
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1439
+ plugins: [
1440
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1441
+ ],
1150
1442
  });
1151
1443
 
1152
1444
  expect(vol.existsSync(".application-manifest.json")).toBe(true);
1153
- expect(vol.existsSync("dist/.application-manifest.json")).toBe(true);
1445
+ expect(vol.existsSync("dist/apps/my-app/.application-manifest.json")).toBe(
1446
+ true
1447
+ );
1154
1448
  });
1155
1449
 
1156
1450
  test("writes to both locations when running in build mode with multiple targets", async () => {
@@ -1166,11 +1460,90 @@ describe("entry points", () => {
1166
1460
 
1167
1461
  await buildApp({
1168
1462
  mode: "production",
1169
- plugins: [apolloClientAiApps({ targets: ["mcp", "openai"] })],
1463
+ plugins: [
1464
+ apolloClientAiApps({
1465
+ targets: ["mcp", "openai"],
1466
+ appsOutDir: "dist/apps",
1467
+ }),
1468
+ ],
1170
1469
  });
1171
1470
 
1172
1471
  expect(vol.existsSync(".application-manifest.json")).toBe(true);
1173
- expect(vol.existsSync("dist/.application-manifest.json")).toBe(true);
1472
+ expect(vol.existsSync("dist/apps/my-app/.application-manifest.json")).toBe(
1473
+ true
1474
+ );
1475
+ });
1476
+ });
1477
+
1478
+ describe("appsOutDir", () => {
1479
+ test("errors when last segment is not `apps`", () => {
1480
+ expect(() =>
1481
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/output" })
1482
+ ).toThrowError(
1483
+ "`appsOutDir` must end with `apps` as the final path segment (e.g. `path/to/apps`)."
1484
+ );
1485
+ });
1486
+
1487
+ test("accepts trailing slash", async () => {
1488
+ vol.fromJSON({
1489
+ "package.json": mockPackageJson(),
1490
+ });
1491
+
1492
+ await expect(
1493
+ buildApp({
1494
+ mode: "production",
1495
+ build: { write: false },
1496
+ plugins: [
1497
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps/" }),
1498
+ ],
1499
+ })
1500
+ ).resolves.not.toThrowError();
1501
+ });
1502
+
1503
+ test("warns when `build.outDir` is set", async () => {
1504
+ vol.fromJSON({
1505
+ "package.json": mockPackageJson(),
1506
+ });
1507
+
1508
+ using _ = spyOnConsole("warn");
1509
+
1510
+ await buildApp({
1511
+ mode: "production",
1512
+ build: { outDir: "custom-out", write: false },
1513
+ plugins: [
1514
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1515
+ ],
1516
+ });
1517
+
1518
+ expect(console.warn).toHaveBeenCalledWith(
1519
+ expect.stringContaining(
1520
+ "`build.outDir` is set in your Vite config but will be ignored"
1521
+ )
1522
+ );
1523
+ });
1524
+
1525
+ test("places output under `appsOutDir`", async () => {
1526
+ vol.fromJSON({
1527
+ "package.json": mockPackageJson(),
1528
+ "src/my-component.tsx": declareOperation(gql`
1529
+ query HelloWorldQuery
1530
+ @tool(name: "hello-world", description: "This is an awesome tool!") {
1531
+ helloWorld
1532
+ }
1533
+ `),
1534
+ });
1535
+
1536
+ await buildApp({
1537
+ mode: "production",
1538
+ plugins: [
1539
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1540
+ ],
1541
+ });
1542
+
1543
+ expect(vol.existsSync("dist/apps/my-app/.application-manifest.json")).toBe(
1544
+ true
1545
+ );
1546
+ expect(vol.existsSync(".application-manifest.json")).toBe(true);
1174
1547
  });
1175
1548
  });
1176
1549
 
@@ -1218,7 +1591,9 @@ export default config;
1218
1591
 
1219
1592
  await buildApp({
1220
1593
  mode: "production",
1221
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1594
+ plugins: [
1595
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1596
+ ],
1222
1597
  });
1223
1598
 
1224
1599
  const manifest = readManifestFile();
@@ -1254,7 +1629,9 @@ export default config;
1254
1629
 
1255
1630
  await buildApp({
1256
1631
  mode: "production",
1257
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1632
+ plugins: [
1633
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1634
+ ],
1258
1635
  });
1259
1636
 
1260
1637
  const manifest = readManifestFile();
@@ -1288,7 +1665,9 @@ describe("file watching", () => {
1288
1665
  });
1289
1666
 
1290
1667
  await using server = await setupServer({
1291
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1668
+ plugins: [
1669
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1670
+ ],
1292
1671
  });
1293
1672
  await server.listen();
1294
1673
 
@@ -1326,7 +1705,9 @@ describe("html transforms", () => {
1326
1705
  server: {
1327
1706
  origin: "http://localhost:3000",
1328
1707
  },
1329
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1708
+ plugins: [
1709
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1710
+ ],
1330
1711
  });
1331
1712
 
1332
1713
  const html = `<html><head><script type="module" src="/@vite/client"></script></head><body><script module src="/assets/main.ts?t=12345"></script></body></html>`;
@@ -1350,7 +1731,9 @@ describe("html transforms", () => {
1350
1731
  server: {
1351
1732
  port: 3000,
1352
1733
  },
1353
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1734
+ plugins: [
1735
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1736
+ ],
1354
1737
  });
1355
1738
 
1356
1739
  await server.listen();
@@ -1378,7 +1761,9 @@ window.$RefreshSig$ = () => (type) => type;</script></head></html>`;
1378
1761
  server: {
1379
1762
  origin: "http://localhost:3000",
1380
1763
  },
1381
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1764
+ plugins: [
1765
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1766
+ ],
1382
1767
  });
1383
1768
 
1384
1769
  const html = `<html><head> <script type="module">import { injectIntoGlobalHook } from "/@react-refresh";
@@ -1404,7 +1789,9 @@ window.$RefreshSig$ = () => (type) => type;</script></head></html>`;
1404
1789
  server: {
1405
1790
  port: 3000,
1406
1791
  },
1407
- plugins: [apolloClientAiApps({ targets: ["mcp"] })],
1792
+ plugins: [
1793
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1794
+ ],
1408
1795
  });
1409
1796
 
1410
1797
  await server.listen();
@@ -1435,7 +1822,7 @@ window.$RefreshSig$ = () => (type) => type;</script></head></html>`;
1435
1822
  origin: "http://localhost:3000",
1436
1823
  },
1437
1824
  plugins: [
1438
- apolloClientAiApps({ targets: ["mcp"] }),
1825
+ apolloClientAiApps({ targets: ["mcp"], appsOutDir: "dist/apps" }),
1439
1826
  {
1440
1827
  name: "capture-html",
1441
1828
  transformIndexHtml: {
@@ -1461,8 +1848,20 @@ function declareOperation(operation: DocumentNode) {
1461
1848
  return `const ${varName} = gql\`\n${print(operation)}\n\``;
1462
1849
  }
1463
1850
 
1851
+ function declareFragment(fragment: DocumentNode) {
1852
+ const definition = getMainDefinition(fragment);
1853
+ invariant(
1854
+ definition.kind === Kind.FRAGMENT_DEFINITION,
1855
+ "declareFragment must receive a fragment definition"
1856
+ );
1857
+
1858
+ const name = definition.name.value;
1859
+ const varName = name.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
1860
+ return `const ${varName} = gql\`\n${print(fragment)}\n\``;
1861
+ }
1862
+
1464
1863
  function mockPackageJson(config?: Record<string, unknown>) {
1465
- return JSON.stringify({ version: "1.0.0", ...config });
1864
+ return JSON.stringify({ version: "1.0.0", name: "my-app", ...config });
1466
1865
  }
1467
1866
 
1468
1867
  function readManifestFile(