@apollo/client-ai-apps 0.3.1 → 0.3.3

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 (140) hide show
  1. package/.github/workflows/pr.yaml +52 -3
  2. package/.github/workflows/prep-release.yml +38 -0
  3. package/.github/workflows/release.yaml +8 -4
  4. package/.github/workflows/verify-changeset.yml +58 -0
  5. package/CHANGELOG.md +25 -0
  6. package/dist/core/ApolloClient.d.ts +3 -2
  7. package/dist/core/ApolloClient.d.ts.map +1 -0
  8. package/dist/core/ApolloClient.js +65 -0
  9. package/dist/core/ApolloClient.js.map +1 -0
  10. package/dist/index.d.ts +18 -17
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +16 -278
  13. package/dist/index.js.map +1 -0
  14. package/dist/link/ToolCallLink.d.ts +1 -0
  15. package/dist/link/ToolCallLink.d.ts.map +1 -0
  16. package/dist/link/ToolCallLink.js +39 -0
  17. package/dist/link/ToolCallLink.js.map +1 -0
  18. package/dist/react/ApolloProvider.d.ts +4 -3
  19. package/dist/react/ApolloProvider.d.ts.map +1 -0
  20. package/dist/react/ApolloProvider.js +30 -0
  21. package/dist/react/ApolloProvider.js.map +1 -0
  22. package/dist/react/context/ToolUseContext.d.ts +4 -3
  23. package/dist/react/context/ToolUseContext.d.ts.map +1 -0
  24. package/dist/react/context/ToolUseContext.js +11 -0
  25. package/dist/react/context/ToolUseContext.js.map +1 -0
  26. package/dist/react/hooks/useCallTool.d.ts +4 -0
  27. package/dist/react/hooks/useCallTool.d.ts.map +1 -0
  28. package/dist/react/hooks/useCallTool.js +5 -0
  29. package/dist/react/hooks/useCallTool.js.map +1 -0
  30. package/dist/react/hooks/useOpenAiGlobal.d.ts +2 -1
  31. package/dist/react/hooks/useOpenAiGlobal.d.ts.map +1 -0
  32. package/dist/react/hooks/useOpenAiGlobal.js +20 -0
  33. package/dist/react/hooks/useOpenAiGlobal.js.map +1 -0
  34. package/dist/react/hooks/useOpenExternal.d.ts +1 -0
  35. package/dist/react/hooks/useOpenExternal.d.ts.map +1 -0
  36. package/dist/react/hooks/useOpenExternal.js +5 -0
  37. package/dist/react/hooks/useOpenExternal.js.map +1 -0
  38. package/dist/react/hooks/useRequestDisplayMode.d.ts +2 -1
  39. package/dist/react/hooks/useRequestDisplayMode.d.ts.map +1 -0
  40. package/dist/react/hooks/useRequestDisplayMode.js +6 -0
  41. package/dist/react/hooks/useRequestDisplayMode.js.map +1 -0
  42. package/dist/react/hooks/useSendFollowUpMessage.d.ts +1 -0
  43. package/dist/react/hooks/useSendFollowUpMessage.d.ts.map +1 -0
  44. package/dist/react/hooks/useSendFollowUpMessage.js +8 -0
  45. package/dist/react/hooks/useSendFollowUpMessage.js.map +1 -0
  46. package/dist/react/hooks/useToolEffect.d.ts +1 -0
  47. package/dist/react/hooks/useToolEffect.d.ts.map +1 -0
  48. package/dist/react/hooks/useToolEffect.js +28 -0
  49. package/dist/react/hooks/useToolEffect.js.map +1 -0
  50. package/dist/react/hooks/useToolInput.d.ts +1 -0
  51. package/dist/react/hooks/useToolInput.d.ts.map +1 -0
  52. package/dist/react/hooks/useToolInput.js +6 -0
  53. package/dist/react/hooks/useToolInput.js.map +1 -0
  54. package/dist/react/hooks/useToolName.d.ts +1 -0
  55. package/dist/react/hooks/useToolName.d.ts.map +1 -0
  56. package/dist/react/hooks/useToolName.js +6 -0
  57. package/dist/react/hooks/useToolName.js.map +1 -0
  58. package/dist/react/hooks/useToolOutput.d.ts +2 -1
  59. package/dist/react/hooks/useToolOutput.d.ts.map +1 -0
  60. package/dist/react/hooks/useToolOutput.js +5 -0
  61. package/dist/react/hooks/useToolOutput.js.map +1 -0
  62. package/dist/react/hooks/useToolResponseMetadata.d.ts +2 -1
  63. package/dist/react/hooks/useToolResponseMetadata.d.ts.map +1 -0
  64. package/dist/react/hooks/useToolResponseMetadata.js +5 -0
  65. package/dist/react/hooks/useToolResponseMetadata.js.map +1 -0
  66. package/dist/react/hooks/useWidgetState.d.ts +3 -2
  67. package/dist/react/hooks/useWidgetState.d.ts.map +1 -0
  68. package/dist/react/hooks/useWidgetState.js +27 -0
  69. package/dist/react/hooks/useWidgetState.js.map +1 -0
  70. package/dist/types/application-manifest.d.ts +7 -0
  71. package/dist/types/application-manifest.d.ts.map +1 -0
  72. package/dist/types/application-manifest.js +2 -0
  73. package/dist/types/application-manifest.js.map +1 -0
  74. package/dist/types/openai.d.ts +1 -0
  75. package/dist/types/openai.d.ts.map +1 -0
  76. package/dist/types/openai.js +6 -0
  77. package/dist/types/openai.js.map +1 -0
  78. package/dist/vite/absolute_asset_imports_plugin.d.ts +1 -0
  79. package/dist/vite/absolute_asset_imports_plugin.d.ts.map +1 -0
  80. package/dist/vite/absolute_asset_imports_plugin.js +17 -0
  81. package/dist/vite/absolute_asset_imports_plugin.js.map +1 -0
  82. package/dist/vite/application_manifest_plugin.d.ts +2 -1
  83. package/dist/vite/application_manifest_plugin.d.ts.map +1 -0
  84. package/dist/vite/application_manifest_plugin.js +274 -0
  85. package/dist/vite/application_manifest_plugin.js.map +1 -0
  86. package/dist/vite/index.d.ts +3 -2
  87. package/dist/vite/index.d.ts.map +1 -0
  88. package/dist/vite/index.js +3 -302
  89. package/dist/vite/index.js.map +1 -0
  90. package/knope.toml +63 -0
  91. package/package.json +15 -8
  92. package/src/core/ApolloClient.ts +10 -5
  93. package/src/core/__tests__/ApolloClient.test.ts +12 -9
  94. package/src/index.ts +17 -17
  95. package/src/react/ApolloProvider.tsx +4 -3
  96. package/src/react/__tests__/ApolloProvider.test.tsx +3 -3
  97. package/src/react/context/ToolUseContext.tsx +2 -1
  98. package/src/react/hooks/__tests__/useCallTool.test.ts +1 -1
  99. package/src/react/hooks/__tests__/useOpenAiGlobal.test.ts +6 -6
  100. package/src/react/hooks/__tests__/useOpenExternal.test.tsx +2 -2
  101. package/src/react/hooks/__tests__/useRequestDisplayMode.test.ts +2 -2
  102. package/src/react/hooks/__tests__/useSendFollowUpMessage.test.ts +1 -1
  103. package/src/react/hooks/__tests__/useToolEffect.test.tsx +2 -2
  104. package/src/react/hooks/__tests__/useToolInput.test.ts +1 -1
  105. package/src/react/hooks/__tests__/useToolName.test.ts +1 -1
  106. package/src/react/hooks/__tests__/useToolOutput.test.tsx +2 -2
  107. package/src/react/hooks/__tests__/useToolResponseMetadata.test.tsx +2 -2
  108. package/src/react/hooks/__tests__/useWidgetState.test.tsx +2 -2
  109. package/src/react/hooks/useOpenAiGlobal.ts +2 -5
  110. package/src/react/hooks/useOpenExternal.ts +1 -1
  111. package/src/react/hooks/useRequestDisplayMode.ts +1 -1
  112. package/src/react/hooks/useToolEffect.tsx +3 -3
  113. package/src/react/hooks/useToolInput.ts +1 -1
  114. package/src/react/hooks/useToolName.ts +1 -1
  115. package/src/react/hooks/useToolOutput.ts +1 -1
  116. package/src/react/hooks/useToolResponseMetadata.ts +1 -1
  117. package/src/react/hooks/useWidgetState.ts +4 -3
  118. package/src/testing/internal/index.ts +2 -2
  119. package/src/testing/internal/matchers/index.ts +1 -1
  120. package/src/testing/internal/matchers/toRerender.ts +2 -2
  121. package/src/testing/internal/matchers/{index.d.ts → types.ts} +1 -1
  122. package/src/testing/internal/openai/dispatchStateChange.ts +1 -1
  123. package/src/testing/internal/openai/stubOpenAiGlobals.ts +6 -2
  124. package/src/types/application-manifest.ts +7 -0
  125. package/src/vite/__tests__/absolute_asset_imports_plugin.test.ts +2 -2
  126. package/src/vite/__tests__/application_manifest_plugin.test.ts +317 -232
  127. package/src/vite/application_manifest_plugin.ts +160 -90
  128. package/src/vite/index.ts +2 -2
  129. package/tsconfig.base.build.json +13 -0
  130. package/tsconfig.base.json +21 -0
  131. package/tsconfig.config.json +9 -0
  132. package/tsconfig.json +10 -0
  133. package/tsconfig.src.build.json +14 -0
  134. package/tsconfig.src.json +17 -0
  135. package/tsconfig.test.json +20 -0
  136. package/tsconfig.vite.build.json +6 -0
  137. package/tsconfig.vite.json +16 -0
  138. package/scripts/build-vite.mjs +0 -18
  139. package/scripts/build.mjs +0 -7
  140. package/scripts/shared.mjs +0 -9
@@ -1,8 +1,10 @@
1
- import { expect, test, vi, describe, beforeEach, Mock } from "vitest";
2
- import { ApplicationManifestPlugin } from "../application_manifest_plugin";
1
+ import type { Mock } from "vitest";
2
+ import { expect, test, vi, describe, beforeEach } from "vitest";
3
+ import { ApplicationManifestPlugin } from "../application_manifest_plugin.js";
3
4
  import fs from "fs";
4
5
  import * as glob from "glob";
5
6
  import path from "path";
7
+ import type { ManifestWidgetSettings } from "../../types/application-manifest.js";
6
8
 
7
9
  const root = process.cwd();
8
10
 
@@ -31,12 +33,7 @@ vi.mock(import("path"), async (importOriginal) => {
31
33
  };
32
34
  });
33
35
 
34
- vi.mock(import("glob"), async (importOriginal) => {
35
- const actual = await importOriginal();
36
- return {
37
- glob: vi.fn(),
38
- };
39
- });
36
+ vi.mock(import("glob"));
40
37
 
41
38
  beforeEach(() => {
42
39
  vi.clearAllMocks();
@@ -44,19 +41,23 @@ beforeEach(() => {
44
41
 
45
42
  describe("buildStart", () => {
46
43
  test("Should write to dev application manifest file when using a serve command", async () => {
47
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
48
- if (path === "package.json") {
49
- return JSON.stringify({});
50
- } else if (path === root + "/my-component.tsx") {
51
- return `
52
- const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-world", description: "This is an awesome tool!", extraInputs: [{
53
- name: "doStuff",
54
- type: "boolean",
55
- description: "Should we do stuff?"
56
- }]) { helloWorld(name: $name) }\`;
57
- `;
58
- }
44
+ mockReadFile({
45
+ "package.json": JSON.stringify({
46
+ widgetSettings: {
47
+ description: "Test",
48
+ domain: "https://example.com",
49
+ prefersBorder: true,
50
+ } satisfies ManifestWidgetSettings,
51
+ }),
52
+ [`${root}/my-component.tsx`]: `
53
+ const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-world", description: "This is an awesome tool!", extraInputs: [{
54
+ name: "doStuff",
55
+ type: "boolean",
56
+ description: "Should we do stuff?"
57
+ }]) { helloWorld(name: $name) }\`;
58
+ `,
59
59
  });
60
+
60
61
  vi.spyOn(glob, "glob").mockImplementation(() =>
61
62
  Promise.resolve(["my-component.tsx"])
62
63
  );
@@ -114,19 +115,21 @@ describe("buildStart", () => {
114
115
  ],
115
116
  "resource": "http://localhost:undefined",
116
117
  "version": "1",
118
+ "widgetSettings": {
119
+ "description": "Test",
120
+ "domain": "https://example.com",
121
+ "prefersBorder": true,
122
+ },
117
123
  }
118
124
  `);
119
125
  });
120
126
 
121
127
  test("Should NOT write to dev application manifest file when using a build command", async () => {
122
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
123
- if (path === "package.json") {
124
- return JSON.stringify({});
125
- } else if (path === "my-component.tsx") {
126
- return `
127
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
128
- `;
129
- }
128
+ mockReadFile({
129
+ "package.json": JSON.stringify({}),
130
+ "my-component.tsx": `
131
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
132
+ `,
130
133
  });
131
134
  vi.spyOn(glob, "glob").mockImplementation(() =>
132
135
  Promise.resolve(["my-component.tsx"])
@@ -142,14 +145,11 @@ describe("buildStart", () => {
142
145
  });
143
146
 
144
147
  test("Should not process files that do not contain gql tags", async () => {
145
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
146
- if (path === "package.json") {
147
- return JSON.stringify({});
148
- } else if (path === root + "/my-component.tsx") {
149
- return `
150
- const MY_QUERY = \`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
151
- `;
152
- }
148
+ mockReadFile({
149
+ "package.json": JSON.stringify({}),
150
+ [`${root}/my-component.tsx`]: `
151
+ const MY_QUERY = \`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
152
+ `,
153
153
  });
154
154
  vi.spyOn(glob, "glob").mockImplementation(() =>
155
155
  Promise.resolve(["my-component.tsx"])
@@ -187,14 +187,11 @@ describe("buildStart", () => {
187
187
  });
188
188
 
189
189
  test("Should capture queries when writing to manifest file", async () => {
190
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
191
- if (path === "package.json") {
192
- return JSON.stringify({});
193
- } else if (path === root + "/my-component.tsx") {
194
- return `
195
- const MY_QUERY = gql\`query HelloWorldQuery { helloWorld }\`;
196
- `;
197
- }
190
+ mockReadFile({
191
+ "package.json": JSON.stringify({}),
192
+ [`${root}/my-component.tsx`]: `
193
+ const MY_QUERY = gql\`query HelloWorldQuery { helloWorld }\`;
194
+ `,
198
195
  });
199
196
  vi.spyOn(glob, "glob").mockImplementation(() =>
200
197
  Promise.resolve(["my-component.tsx"])
@@ -244,14 +241,11 @@ describe("buildStart", () => {
244
241
  });
245
242
 
246
243
  test("Should capture queries as prefetch when query is marked with @prefetch directive", async () => {
247
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
248
- if (path === "package.json") {
249
- return JSON.stringify({});
250
- } else if (path === root + "/my-component.tsx") {
251
- return `
252
- const MY_QUERY = gql\`query HelloWorldQuery @prefetch { helloWorld }\`;
253
- `;
254
- }
244
+ mockReadFile({
245
+ "package.json": JSON.stringify({}),
246
+ [`${root}/my-component.tsx`]: `
247
+ const MY_QUERY = gql\`query HelloWorldQuery @prefetch { helloWorld }\`;
248
+ `,
255
249
  });
256
250
  vi.spyOn(glob, "glob").mockImplementation(() =>
257
251
  Promise.resolve(["my-component.tsx"])
@@ -302,15 +296,12 @@ describe("buildStart", () => {
302
296
  });
303
297
 
304
298
  test("Should error when multiple operations are marked with @prefetch", async () => {
305
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
306
- if (path === "package.json") {
307
- return JSON.stringify({});
308
- } else if (path === "my-component.tsx") {
309
- return `
310
- const MY_QUERY = gql\`query HelloWorldQuery @prefetch { helloWorld }\`;
311
- const MY_QUERY2 = gql\`query HelloWorldQuery2 @prefetch { helloWorld }\`;
312
- `;
313
- }
299
+ mockReadFile({
300
+ "package.json": JSON.stringify({}),
301
+ "my-component.tsx": `
302
+ const MY_QUERY = gql\`query HelloWorldQuery @prefetch { helloWorld }\`;
303
+ const MY_QUERY2 = gql\`query HelloWorldQuery2 @prefetch { helloWorld }\`;
304
+ `,
314
305
  });
315
306
  vi.spyOn(glob, "glob").mockImplementation(() =>
316
307
  Promise.resolve(["my-component.tsx"])
@@ -328,14 +319,11 @@ describe("buildStart", () => {
328
319
  });
329
320
 
330
321
  test("Should capture mutations when writing to manifest file", async () => {
331
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
332
- if (path === "package.json") {
333
- return JSON.stringify({});
334
- } else if (path === root + "/my-component.tsx") {
335
- return `
336
- const MY_QUERY = gql\`mutation HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
337
- `;
338
- }
322
+ mockReadFile({
323
+ "package.json": JSON.stringify({}),
324
+ [`${root}/my-component.tsx`]: `
325
+ const MY_QUERY = gql\`mutation HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
326
+ `,
339
327
  });
340
328
  vi.spyOn(glob, "glob").mockImplementation(() =>
341
329
  Promise.resolve(["my-component.tsx"])
@@ -390,14 +378,11 @@ describe("buildStart", () => {
390
378
  });
391
379
 
392
380
  test("Should throw error when a subscription operation type is discovered", async () => {
393
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
394
- if (path === "package.json") {
395
- return JSON.stringify({});
396
- } else if (path === "my-component.tsx") {
397
- return `
398
- const MY_QUERY = gql\`subscription HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
399
- `;
400
- }
381
+ mockReadFile({
382
+ "package.json": JSON.stringify({}),
383
+ "my-component.tsx": `
384
+ const MY_QUERY = gql\`subscription HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
385
+ `,
401
386
  });
402
387
  vi.spyOn(glob, "glob").mockImplementation(() =>
403
388
  Promise.resolve(["my-component.tsx"])
@@ -416,18 +401,15 @@ describe("buildStart", () => {
416
401
  });
417
402
 
418
403
  test("Should use custom entry point when in serve mode and provided in package.json", async () => {
419
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
420
- if (path === "package.json") {
421
- return JSON.stringify({
422
- entry: {
423
- staging: "http://staging.awesome.com",
424
- },
425
- });
426
- } else if (path === "my-component.tsx") {
427
- return `
428
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
429
- `;
430
- }
404
+ mockReadFile({
405
+ "package.json": JSON.stringify({
406
+ entry: {
407
+ staging: "http://staging.awesome.com",
408
+ },
409
+ }),
410
+ "my-component.tsx": `
411
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
412
+ `,
431
413
  });
432
414
  vi.spyOn(glob, "glob").mockImplementation(() =>
433
415
  Promise.resolve(["my-component.tsx"])
@@ -444,21 +426,18 @@ describe("buildStart", () => {
444
426
  });
445
427
  await plugin.buildStart();
446
428
 
447
- let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
429
+ let [, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
448
430
  let contentObj = JSON.parse(content);
449
431
 
450
432
  expect(contentObj.resource).toBe("http://staging.awesome.com");
451
433
  });
452
434
 
453
435
  test("Should use https when enabled in server config", async () => {
454
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
455
- if (path === "package.json") {
456
- return JSON.stringify({});
457
- } else if (path === "my-component.tsx") {
458
- return `
459
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
460
- `;
461
- }
436
+ mockReadFile({
437
+ "package.json": JSON.stringify({}),
438
+ "my-component.tsx": `
439
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
440
+ `,
462
441
  });
463
442
  vi.spyOn(glob, "glob").mockImplementation(() =>
464
443
  Promise.resolve(["my-component.tsx"])
@@ -474,21 +453,18 @@ describe("buildStart", () => {
474
453
  });
475
454
  await plugin.buildStart();
476
455
 
477
- let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
456
+ let [, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
478
457
  let contentObj = JSON.parse(content);
479
458
 
480
459
  expect(contentObj.resource).toBe("https://localhost:5678");
481
460
  });
482
461
 
483
462
  test("Should use custom host when specified in server config", async () => {
484
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
485
- if (path === "package.json") {
486
- return JSON.stringify({});
487
- } else if (path === "my-component.tsx") {
488
- return `
489
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
490
- `;
491
- }
463
+ mockReadFile({
464
+ "package.json": JSON.stringify({}),
465
+ "my-component.tsx": `
466
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
467
+ `,
492
468
  });
493
469
  vi.spyOn(glob, "glob").mockImplementation(() =>
494
470
  Promise.resolve(["my-component.tsx"])
@@ -504,21 +480,18 @@ describe("buildStart", () => {
504
480
  });
505
481
  await plugin.buildStart();
506
482
 
507
- let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
483
+ let [, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
508
484
  let contentObj = JSON.parse(content);
509
485
 
510
486
  expect(contentObj.resource).toBe("http://awesome.com:5678");
511
487
  });
512
488
 
513
489
  test("Should error when tool name is not provided", async () => {
514
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
515
- if (path === "package.json") {
516
- return JSON.stringify({});
517
- } else if (path === "my-component.tsx") {
518
- return `
519
- const MY_QUERY = gql\`query HelloWorldQuery @tool { helloWorld }\`;
520
- `;
521
- }
490
+ mockReadFile({
491
+ "package.json": JSON.stringify({}),
492
+ "my-component.tsx": `
493
+ const MY_QUERY = gql\`query HelloWorldQuery @tool { helloWorld }\`;
494
+ `,
522
495
  });
523
496
  vi.spyOn(glob, "glob").mockImplementation(() =>
524
497
  Promise.resolve(["my-component.tsx"])
@@ -537,14 +510,11 @@ describe("buildStart", () => {
537
510
  });
538
511
 
539
512
  test("Should error when tool description is not provided", async () => {
540
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
541
- if (path === "package.json") {
542
- return JSON.stringify({});
543
- } else if (path === "my-component.tsx") {
544
- return `
545
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world") { helloWorld }\`;
546
- `;
547
- }
513
+ mockReadFile({
514
+ "package.json": JSON.stringify({}),
515
+ "my-component.tsx": `
516
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world") { helloWorld }\`;
517
+ `,
548
518
  });
549
519
  vi.spyOn(glob, "glob").mockImplementation(() =>
550
520
  Promise.resolve(["my-component.tsx"])
@@ -562,15 +532,35 @@ describe("buildStart", () => {
562
532
  );
563
533
  });
564
534
 
535
+ test("Should error when tool name contains spaces", async () => {
536
+ mockReadFile({
537
+ "package.json": JSON.stringify({}),
538
+ "my-component.tsx": `
539
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello world", description: "A tool") { helloWorld }\`;
540
+ `,
541
+ });
542
+ vi.spyOn(glob, "glob").mockImplementation(() =>
543
+ Promise.resolve(["my-component.tsx"])
544
+ );
545
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
546
+ vi.spyOn(fs, "writeFileSync");
547
+
548
+ const plugin = ApplicationManifestPlugin();
549
+ plugin.configResolved({ command: "serve", server: {} });
550
+
551
+ await expect(
552
+ async () => await plugin.buildStart()
553
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
554
+ `[Error: Tool with name "hello world" contains spaces which is not allowed.]`
555
+ );
556
+ });
557
+
565
558
  test("Should error when tool name is not a string", async () => {
566
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
567
- if (path === "package.json") {
568
- return JSON.stringify({});
569
- } else if (path === "my-component.tsx") {
570
- return `
571
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: true) { helloWorld }\`;
572
- `;
573
- }
559
+ mockReadFile({
560
+ "package.json": JSON.stringify({}),
561
+ "my-component.tsx": `
562
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: true) { helloWorld }\`;
563
+ `,
574
564
  });
575
565
  vi.spyOn(glob, "glob").mockImplementation(() =>
576
566
  Promise.resolve(["my-component.tsx"])
@@ -589,14 +579,11 @@ describe("buildStart", () => {
589
579
  });
590
580
 
591
581
  test("Should error when tool description is not a string", async () => {
592
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
593
- if (path === "package.json") {
594
- return JSON.stringify({});
595
- } else if (path === "my-component.tsx") {
596
- return `
597
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: false) { helloWorld }\`;
598
- `;
599
- }
582
+ mockReadFile({
583
+ "package.json": JSON.stringify({}),
584
+ "my-component.tsx": `
585
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: false) { helloWorld }\`;
586
+ `,
600
587
  });
601
588
  vi.spyOn(glob, "glob").mockImplementation(() =>
602
589
  Promise.resolve(["my-component.tsx"])
@@ -615,14 +602,11 @@ describe("buildStart", () => {
615
602
  });
616
603
 
617
604
  test("Should error when extraInputs is not an array", async () => {
618
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
619
- if (path === "package.json") {
620
- return JSON.stringify({});
621
- } else if (path === "my-component.tsx") {
622
- return `
623
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "hello", extraInputs: false ) { helloWorld }\`;
624
- `;
625
- }
605
+ mockReadFile({
606
+ "package.json": JSON.stringify({}),
607
+ "my-component.tsx": `
608
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "hello", extraInputs: false ) { helloWorld }\`;
609
+ `,
626
610
  });
627
611
  vi.spyOn(glob, "glob").mockImplementation(() =>
628
612
  Promise.resolve(["my-component.tsx"])
@@ -640,17 +624,116 @@ describe("buildStart", () => {
640
624
  );
641
625
  });
642
626
 
627
+ test("Should error when widgetSettings.prefersBorder is not a boolean", async () => {
628
+ mockReadFile({
629
+ "package.json": JSON.stringify({
630
+ widgetSettings: {
631
+ prefersBorder: "test",
632
+ },
633
+ }),
634
+ "my-component.tsx": `
635
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test") { helloWorld }\`;
636
+ `,
637
+ });
638
+ vi.spyOn(glob, "glob").mockImplementation(() =>
639
+ Promise.resolve(["my-component.tsx"])
640
+ );
641
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
642
+ vi.spyOn(fs, "writeFileSync");
643
+
644
+ const plugin = ApplicationManifestPlugin();
645
+ plugin.configResolved({ command: "serve", server: {} });
646
+
647
+ await expect(
648
+ async () => await plugin.buildStart()
649
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
650
+ `[Error: Expected 'widgetSettings.prefersBorder' to be of type 'boolean' but found 'string' instead.]`
651
+ );
652
+ });
653
+
654
+ test("Should error when widgetSettings.description is not a string", async () => {
655
+ mockReadFile({
656
+ "package.json": JSON.stringify({
657
+ widgetSettings: {
658
+ description: true,
659
+ },
660
+ }),
661
+ "my-component.tsx": `
662
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test") { helloWorld }\`;
663
+ `,
664
+ });
665
+ vi.spyOn(glob, "glob").mockImplementation(() =>
666
+ Promise.resolve(["my-component.tsx"])
667
+ );
668
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
669
+ vi.spyOn(fs, "writeFileSync");
670
+
671
+ const plugin = ApplicationManifestPlugin();
672
+ plugin.configResolved({ command: "serve", server: {} });
673
+
674
+ await expect(
675
+ async () => await plugin.buildStart()
676
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
677
+ `[Error: Expected 'widgetSettings.description' to be of type 'string' but found 'boolean' instead.]`
678
+ );
679
+ });
680
+
681
+ test("Should error when widgetSettings.domain is not a string", async () => {
682
+ mockReadFile({
683
+ "package.json": JSON.stringify({
684
+ widgetSettings: {
685
+ domain: true,
686
+ },
687
+ }),
688
+ "my-component.tsx": `
689
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test") { helloWorld }\`;
690
+ `,
691
+ });
692
+ vi.spyOn(glob, "glob").mockImplementation(() =>
693
+ Promise.resolve(["my-component.tsx"])
694
+ );
695
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
696
+ vi.spyOn(fs, "writeFileSync");
697
+
698
+ const plugin = ApplicationManifestPlugin();
699
+ plugin.configResolved({ command: "serve", server: {} });
700
+
701
+ await expect(
702
+ async () => await plugin.buildStart()
703
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
704
+ `[Error: Expected 'widgetSettings.domain' to be of type 'string' but found 'boolean' instead.]`
705
+ );
706
+ });
707
+
708
+ test("Should allow empty widgetSettings value", async () => {
709
+ mockReadFile({
710
+ "package.json": JSON.stringify({
711
+ widgetSettings: {},
712
+ }),
713
+ "my-component.tsx": `
714
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test") { helloWorld }\`;
715
+ `,
716
+ });
717
+ vi.spyOn(glob, "glob").mockImplementation(() =>
718
+ Promise.resolve(["my-component.tsx"])
719
+ );
720
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
721
+ vi.spyOn(fs, "writeFileSync");
722
+
723
+ const plugin = ApplicationManifestPlugin();
724
+ plugin.configResolved({ command: "serve", server: {}, build: {} });
725
+
726
+ await expect(plugin.buildStart()).resolves.toBeUndefined();
727
+ });
728
+
643
729
  test("Should error when an unknown type is discovered", async () => {
644
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
645
- if (path === "package.json") {
646
- return JSON.stringify({});
647
- } else if (path === "my-component.tsx") {
648
- return `
649
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "hello", extraInputs: [{
650
- name: 3.1
651
- }] ) { helloWorld }\`;
652
- `;
653
- }
730
+ mockReadFile({
731
+ "package.json": JSON.stringify({}),
732
+ "my-component.tsx": `
733
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "hello", extraInputs: [{
734
+ name: 3.1
735
+ }] ) { helloWorld }\`;
736
+ `,
654
737
  });
655
738
  vi.spyOn(glob, "glob").mockImplementation(() =>
656
739
  Promise.resolve(["my-component.tsx"])
@@ -669,25 +752,22 @@ describe("buildStart", () => {
669
752
  });
670
753
 
671
754
  test("Should order operations and fragments when generating normalized operation", async () => {
672
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
673
- if (path === "package.json") {
674
- return JSON.stringify({});
675
- } else if (path === root + "/my-component.tsx") {
676
- return `
677
- const MY_QUERY = gql\`
678
- fragment A on User { firstName }
679
- fragment B on User { lastName }
680
- query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") {
681
- helloWorld {
682
- ...B
683
- ...A
684
- ...C
685
- }
686
-
687
- fragment C on User { middleName }
688
- }\`;
689
- `;
690
- }
755
+ mockReadFile({
756
+ "package.json": JSON.stringify({}),
757
+ [`${root}/my-component.tsx`]: `
758
+ const MY_QUERY = gql\`
759
+ fragment A on User { firstName }
760
+ fragment B on User { lastName }
761
+ query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") {
762
+ helloWorld {
763
+ ...B
764
+ ...A
765
+ ...C
766
+ }
767
+
768
+ fragment C on User { middleName }
769
+ }\`;
770
+ `,
691
771
  });
692
772
  vi.spyOn(glob, "glob").mockImplementation(() =>
693
773
  Promise.resolve(["my-component.tsx"])
@@ -766,18 +846,15 @@ describe("buildStart", () => {
766
846
 
767
847
  describe("writeBundle", () => {
768
848
  test("Should use custom entry point when in build mode and provided in package.json", async () => {
769
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
770
- if (path === "package.json") {
771
- return JSON.stringify({
772
- entry: {
773
- staging: "http://staging.awesome.com",
774
- },
775
- });
776
- } else if (path === "my-component.tsx") {
777
- return `
778
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
779
- `;
780
- }
849
+ mockReadFile({
850
+ "package.json": JSON.stringify({
851
+ entry: {
852
+ staging: "http://staging.awesome.com",
853
+ },
854
+ }),
855
+ "my-component.tsx": `
856
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
857
+ `,
781
858
  });
782
859
  vi.spyOn(glob, "glob").mockImplementation(() =>
783
860
  Promise.resolve(["my-component.tsx"])
@@ -796,21 +873,18 @@ describe("writeBundle", () => {
796
873
  await plugin.buildStart();
797
874
  await plugin.writeBundle();
798
875
 
799
- let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
876
+ let [, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
800
877
  let contentObj = JSON.parse(content);
801
878
 
802
879
  expect(contentObj.resource).toBe("http://staging.awesome.com");
803
880
  });
804
881
 
805
882
  test("Should use index.html when in build production and not provided in package.json", async () => {
806
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
807
- if (path === "package.json") {
808
- return JSON.stringify({});
809
- } else if (path === "my-component.tsx") {
810
- return `
811
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
812
- `;
813
- }
883
+ mockReadFile({
884
+ "package.json": JSON.stringify({}),
885
+ "my-component.tsx": `
886
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
887
+ `,
814
888
  });
815
889
  vi.spyOn(glob, "glob").mockImplementation(() =>
816
890
  Promise.resolve(["my-component.tsx"])
@@ -829,21 +903,18 @@ describe("writeBundle", () => {
829
903
  await plugin.buildStart();
830
904
  await plugin.writeBundle();
831
905
 
832
- let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
906
+ let [, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
833
907
  let contentObj = JSON.parse(content);
834
908
 
835
909
  expect(contentObj.resource).toBe("index.html");
836
910
  });
837
911
 
838
912
  test("Should throw an error when in build mode and using a mode that is not production and not provided in package.json", async () => {
839
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
840
- if (path === "package.json") {
841
- return JSON.stringify({});
842
- } else if (path === "my-component.tsx") {
843
- return `
844
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
845
- `;
846
- }
913
+ mockReadFile({
914
+ "package.json": JSON.stringify({}),
915
+ "my-component.tsx": `
916
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
917
+ `,
847
918
  });
848
919
  vi.spyOn(glob, "glob").mockImplementation(() =>
849
920
  Promise.resolve(["my-component.tsx"])
@@ -869,14 +940,11 @@ describe("writeBundle", () => {
869
940
  });
870
941
 
871
942
  test("Should always write to both locations when running in build mode", async () => {
872
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
873
- if (path === "package.json") {
874
- return JSON.stringify({});
875
- } else if (path === "my-component.tsx") {
876
- return `
877
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
878
- `;
879
- }
943
+ mockReadFile({
944
+ "package.json": JSON.stringify({}),
945
+ "my-component.tsx": `
946
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
947
+ `,
880
948
  });
881
949
  vi.spyOn(glob, "glob").mockImplementation(() =>
882
950
  Promise.resolve(["my-component.tsx"])
@@ -901,18 +969,15 @@ describe("writeBundle", () => {
901
969
 
902
970
  describe("configureServer", () => {
903
971
  test("Should write to manifest file when package.json or file is updated", async () => {
904
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
905
- if (path === "package.json") {
906
- return JSON.stringify({});
907
- } else if (path === "my-component.tsx") {
908
- return `
909
- const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-world", description: "This is an awesome tool!", extraInputs: [{
910
- name: "doStuff",
911
- type: "boolean",
912
- description: "Should we do stuff?"
913
- }]) { helloWorld(name: $name) }\`;
914
- `;
915
- }
972
+ mockReadFile({
973
+ "package.json": JSON.stringify({}),
974
+ "my-component.tsx": `
975
+ const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-world", description: "This is an awesome tool!", extraInputs: [{
976
+ name: "doStuff",
977
+ type: "boolean",
978
+ description: "Should we do stuff?"
979
+ }]) { helloWorld(name: $name) }\`;
980
+ `,
916
981
  });
917
982
  vi.spyOn(glob, "glob").mockImplementation(() =>
918
983
  Promise.resolve(["my-component.tsx"])
@@ -920,16 +985,18 @@ describe("configureServer", () => {
920
985
  vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
921
986
  vi.spyOn(fs, "writeFileSync");
922
987
 
988
+ let _callbacks: Function[] = [];
989
+
923
990
  const server = {
924
991
  watcher: {
925
992
  init: () => {
926
- this._callbacks = [];
993
+ _callbacks = [];
927
994
  },
928
995
  on: (_event: string, callback: Function) => {
929
- this._callbacks.push(callback);
996
+ _callbacks.push(callback);
930
997
  },
931
- trigger: async (file) => {
932
- for (const callback of this._callbacks) {
998
+ trigger: async (file: string) => {
999
+ for (const callback of _callbacks) {
933
1000
  await callback(file);
934
1001
  }
935
1002
  },
@@ -944,10 +1011,28 @@ describe("configureServer", () => {
944
1011
  build: { outDir: "/dist" },
945
1012
  });
946
1013
  await plugin.buildStart();
947
- await plugin.configureServer(server);
1014
+ plugin.configureServer(server);
948
1015
  await server.watcher.trigger("package.json");
949
1016
  await server.watcher.trigger("my-component.tsx");
950
1017
 
951
1018
  expect(fs.writeFileSync).toBeCalledTimes(6);
952
1019
  });
953
1020
  });
1021
+
1022
+ type FilePath = string;
1023
+
1024
+ function mockReadFile(mocks: Record<FilePath, string | (() => string)>) {
1025
+ vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
1026
+ const mock = mocks[path.toString()];
1027
+
1028
+ if (!mock) {
1029
+ throw new Error(`No matched mock for path '${path}'`);
1030
+ }
1031
+
1032
+ if (typeof mock === "function") {
1033
+ return mock();
1034
+ }
1035
+
1036
+ return mock;
1037
+ });
1038
+ }