@apollo/client-ai-apps 0.2.2 → 0.2.4
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.
- package/dist/hooks/useToolName.d.ts +1 -1
- package/dist/vite/absolute_asset_imports_plugin.d.ts +4 -0
- package/dist/vite/application_manifest_plugin.d.ts +1 -2
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.js +47 -12
- package/package.json +1 -1
- package/src/hooks/useToolInput.test.ts +1 -1
- package/src/hooks/useToolInput.ts +1 -1
- package/src/hooks/useToolName.test.ts +13 -0
- package/src/hooks/useToolName.ts +1 -1
- package/src/vite/absolute_asset_imports_plugin.test.ts +100 -0
- package/src/vite/absolute_asset_imports_plugin.ts +20 -0
- package/src/vite/application_manifest_plugin.test.ts +830 -0
- package/src/vite/application_manifest_plugin.ts +49 -14
- package/src/vite/index.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const useToolName: () =>
|
|
1
|
+
export declare const useToolName: () => string | undefined;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function getTypeName(type: TypeNode): string;
|
|
1
|
+
import { type DocumentNode } from "graphql";
|
|
3
2
|
export declare const ApplicationManifestPlugin: () => {
|
|
4
3
|
name: string;
|
|
5
4
|
configResolved(resolvedConfig: any): Promise<void>;
|
package/dist/vite/index.d.ts
CHANGED
package/dist/vite/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/vite/application_manifest_plugin.ts
|
|
2
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
3
|
import { glob } from "glob";
|
|
4
4
|
import { gqlPluckFromCodeStringSync } from "@graphql-tools/graphql-tag-pluck";
|
|
5
5
|
import { createHash } from "crypto";
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
import { ApolloClient, ApolloLink, InMemoryCache } from "@apollo/client";
|
|
13
13
|
import Observable from "rxjs";
|
|
14
14
|
import path from "path";
|
|
15
|
-
import fs from "fs";
|
|
16
15
|
var root = process.cwd();
|
|
17
16
|
var getRawValue = (node) => {
|
|
18
17
|
switch (node.kind) {
|
|
@@ -26,8 +25,25 @@ var getRawValue = (node) => {
|
|
|
26
25
|
acc[field.name.value] = getRawValue(field.value);
|
|
27
26
|
return acc;
|
|
28
27
|
}, {});
|
|
28
|
+
default:
|
|
29
|
+
throw new Error(`Error when parsing directive values: unexpected type '${node.kind}'`);
|
|
29
30
|
}
|
|
30
31
|
};
|
|
32
|
+
var getTypedDirectiveArgument = (argumentName, expectedType, directiveArguments) => {
|
|
33
|
+
if (!directiveArguments || directiveArguments.length === 0) {
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
let argument = directiveArguments.find((directiveArgument) => directiveArgument.name.value === argumentName);
|
|
37
|
+
if (!argument) {
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
if (argument.value.kind != expectedType) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Expected argument '${argumentName}' to be of type '${expectedType}' but found '${argument.value.kind}' instead.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return getRawValue(argument.value);
|
|
46
|
+
};
|
|
31
47
|
function getTypeName(type) {
|
|
32
48
|
let t = type;
|
|
33
49
|
while (t.kind === "NonNullType" || t.kind === "ListType") {
|
|
@@ -54,11 +70,19 @@ var ApplicationManifestPlugin = () => {
|
|
|
54
70
|
const id = createHash("sha256").update(body).digest("hex");
|
|
55
71
|
const prefetchID = prefetch ? "__anonymous" : void 0;
|
|
56
72
|
const tools = operation.query.definitions.find((d) => d.kind === "OperationDefinition").directives?.filter((d) => d.name.value === "tool").map((directive) => {
|
|
57
|
-
const
|
|
73
|
+
const name2 = getTypedDirectiveArgument("name", Kind.STRING, directive.arguments);
|
|
74
|
+
const description = getTypedDirectiveArgument("description", Kind.STRING, directive.arguments);
|
|
75
|
+
const extraInputs = getTypedDirectiveArgument("extraInputs", Kind.LIST, directive.arguments);
|
|
76
|
+
if (!name2) {
|
|
77
|
+
throw new Error("'name' argument must be supplied for @tool");
|
|
78
|
+
}
|
|
79
|
+
if (!description) {
|
|
80
|
+
throw new Error("'description' argument must be supplied for @tool");
|
|
81
|
+
}
|
|
58
82
|
return {
|
|
59
|
-
name:
|
|
60
|
-
description
|
|
61
|
-
extraInputs
|
|
83
|
+
name: name2,
|
|
84
|
+
description,
|
|
85
|
+
extraInputs
|
|
62
86
|
};
|
|
63
87
|
});
|
|
64
88
|
return Observable.of({ data: { id, name, type, body, variables, prefetch, prefetchID, tools } });
|
|
@@ -133,11 +157,9 @@ var ApplicationManifestPlugin = () => {
|
|
|
133
157
|
resourceDomains: packageJson.csp?.resourceDomains ?? []
|
|
134
158
|
}
|
|
135
159
|
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
writeFileSync(dest, JSON.stringify(manifest));
|
|
140
|
-
}
|
|
160
|
+
const dest = path.resolve(root, config.build.outDir, ".application-manifest.json");
|
|
161
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
162
|
+
writeFileSync(dest, JSON.stringify(manifest));
|
|
141
163
|
writeFileSync(".application-manifest.json", JSON.stringify(manifest));
|
|
142
164
|
};
|
|
143
165
|
return {
|
|
@@ -207,8 +229,21 @@ function removeClientDirective(doc) {
|
|
|
207
229
|
}
|
|
208
230
|
});
|
|
209
231
|
}
|
|
232
|
+
|
|
233
|
+
// src/vite/absolute_asset_imports_plugin.ts
|
|
234
|
+
var AbsoluteAssetImportsPlugin = () => {
|
|
235
|
+
return {
|
|
236
|
+
name: "absolute-asset-imports",
|
|
237
|
+
transformIndexHtml(html, ctx) {
|
|
238
|
+
if (!ctx.server) return html;
|
|
239
|
+
let baseUrl = (ctx.server.config?.server?.origin ?? ctx.server.resolvedUrls?.local[0]).replace(/\/$/, "");
|
|
240
|
+
baseUrl = baseUrl.replace(/\/$/, "");
|
|
241
|
+
return html.replace(/(from\s+["'])\/([^"']+)/g, `$1${baseUrl}/$2`).replace(/(src=["'])\/([^"']+)/gi, `$1${baseUrl}/$2`);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
};
|
|
210
245
|
export {
|
|
246
|
+
AbsoluteAssetImportsPlugin,
|
|
211
247
|
ApplicationManifestPlugin,
|
|
212
|
-
getTypeName,
|
|
213
248
|
sortTopLevelDefinitions
|
|
214
249
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test, vi } from "vitest";
|
|
2
2
|
import { useToolInput } from "./useToolInput";
|
|
3
|
-
import { renderHook
|
|
3
|
+
import { renderHook } from "@testing-library/react";
|
|
4
4
|
|
|
5
5
|
test("Should return tool input when called", async () => {
|
|
6
6
|
vi.stubGlobal("openai", {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expect, test, vi } from "vitest";
|
|
2
|
+
import { useToolName } from "./useToolName";
|
|
3
|
+
import { renderHook } from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
test("Should return tool input when called", async () => {
|
|
6
|
+
vi.stubGlobal("openai", {
|
|
7
|
+
toolResponseMetadata: { toolName: "get-products" },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const { result } = renderHook(() => useToolName());
|
|
11
|
+
|
|
12
|
+
expect(result.current).toBe("get-products");
|
|
13
|
+
});
|
package/src/hooks/useToolName.ts
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { expect, test, vi, describe, beforeEach, Mock } from "vitest";
|
|
2
|
+
import { AbsoluteAssetImportsPlugin } from "./absolute_asset_imports_plugin";
|
|
3
|
+
|
|
4
|
+
test("Should replace root relative scripts with full url when origin is provided", () => {
|
|
5
|
+
const ctx = {
|
|
6
|
+
server: {
|
|
7
|
+
config: {
|
|
8
|
+
server: {
|
|
9
|
+
origin: "http://localhost:3000/",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
const html = `<html><head><script type="module" src="/@vite/client"></script></head><body><script module src="/assets/main.ts?t=12345"></script></body></html>`;
|
|
15
|
+
const plugin = AbsoluteAssetImportsPlugin();
|
|
16
|
+
|
|
17
|
+
let result = plugin.transformIndexHtml(html, ctx);
|
|
18
|
+
|
|
19
|
+
expect(result).toMatchInlineSnapshot(
|
|
20
|
+
`"<html><head><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>"`
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("Should replace root relative scripts with full url when origin is not provided", () => {
|
|
25
|
+
const ctx = {
|
|
26
|
+
server: {
|
|
27
|
+
resolvedUrls: {
|
|
28
|
+
local: ["http://localhost:3000/"],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const html = `<html><head> <script type="module">import { injectIntoGlobalHook } from "/@react-refresh";
|
|
33
|
+
injectIntoGlobalHook(window);
|
|
34
|
+
window.$RefreshReg$ = () => {};
|
|
35
|
+
window.$RefreshSig$ = () => (type) => type;</script></head></html>`;
|
|
36
|
+
const plugin = AbsoluteAssetImportsPlugin();
|
|
37
|
+
|
|
38
|
+
let result = plugin.transformIndexHtml(html, ctx);
|
|
39
|
+
|
|
40
|
+
expect(result).toMatchInlineSnapshot(`
|
|
41
|
+
"<html><head> <script type="module">import { injectIntoGlobalHook } from "http://localhost:3000/@react-refresh";
|
|
42
|
+
injectIntoGlobalHook(window);
|
|
43
|
+
window.$RefreshReg$ = () => {};
|
|
44
|
+
window.$RefreshSig$ = () => (type) => type;</script></head></html>"
|
|
45
|
+
`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("Should replace root relative imports with full url when origin is provided", () => {
|
|
49
|
+
const ctx = {
|
|
50
|
+
server: {
|
|
51
|
+
config: {
|
|
52
|
+
server: {
|
|
53
|
+
origin: "http://localhost:3000/",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const html = `<html><head> <script type="module">import { injectIntoGlobalHook } from "/@react-refresh";
|
|
59
|
+
injectIntoGlobalHook(window);
|
|
60
|
+
window.$RefreshReg$ = () => {};
|
|
61
|
+
window.$RefreshSig$ = () => (type) => type;</script></head></html>`;
|
|
62
|
+
const plugin = AbsoluteAssetImportsPlugin();
|
|
63
|
+
|
|
64
|
+
let result = plugin.transformIndexHtml(html, ctx);
|
|
65
|
+
|
|
66
|
+
expect(result).toMatchInlineSnapshot(`
|
|
67
|
+
"<html><head> <script type="module">import { injectIntoGlobalHook } from "http://localhost:3000/@react-refresh";
|
|
68
|
+
injectIntoGlobalHook(window);
|
|
69
|
+
window.$RefreshReg$ = () => {};
|
|
70
|
+
window.$RefreshSig$ = () => (type) => type;</script></head></html>"
|
|
71
|
+
`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("Should replace root relative imports with full url when origin is not provided", () => {
|
|
75
|
+
const ctx = {
|
|
76
|
+
server: {
|
|
77
|
+
resolvedUrls: {
|
|
78
|
+
local: ["http://localhost:3000/"],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const html = `<html><body><script module src="/assets/main.ts?t=12345"></script></body></html>`;
|
|
83
|
+
const plugin = AbsoluteAssetImportsPlugin();
|
|
84
|
+
|
|
85
|
+
let result = plugin.transformIndexHtml(html, ctx);
|
|
86
|
+
|
|
87
|
+
expect(result).toMatchInlineSnapshot(
|
|
88
|
+
`"<html><body><script module src="http://localhost:3000/assets/main.ts?t=12345"></script></body></html>"`
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("Should not modify html when not running a local server", () => {
|
|
93
|
+
const ctx = {};
|
|
94
|
+
const html = `<html><head><script type="module" src="/@vite/client"></script></head><body><script module src="/assets/main.ts?t=12345"></script></body></html>`;
|
|
95
|
+
const plugin = AbsoluteAssetImportsPlugin();
|
|
96
|
+
|
|
97
|
+
let result = plugin.transformIndexHtml(html, ctx);
|
|
98
|
+
|
|
99
|
+
expect(result).toMatchInlineSnapshot(`"<html><head><script type="module" src="/@vite/client"></script></head><body><script module src="/assets/main.ts?t=12345"></script></body></html>"`);
|
|
100
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const AbsoluteAssetImportsPlugin = () => {
|
|
2
|
+
return {
|
|
3
|
+
name: "absolute-asset-imports",
|
|
4
|
+
|
|
5
|
+
transformIndexHtml(html: string, ctx: any) {
|
|
6
|
+
if (!ctx.server) return html;
|
|
7
|
+
|
|
8
|
+
let baseUrl = (ctx.server.config?.server?.origin ?? ctx.server.resolvedUrls?.local[0]).replace(/\/$/, "");
|
|
9
|
+
baseUrl = baseUrl.replace(/\/$/, "");
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
html
|
|
13
|
+
// import "/@vite/..." or "/@react-refresh"
|
|
14
|
+
.replace(/(from\s+["'])\/([^"']+)/g, `$1${baseUrl}/$2`)
|
|
15
|
+
// src="/src/..."
|
|
16
|
+
.replace(/(src=["'])\/([^"']+)/gi, `$1${baseUrl}/$2`)
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
import { expect, test, vi, describe, beforeEach, Mock } from "vitest";
|
|
2
|
+
import { ApplicationManifestPlugin } from "./application_manifest_plugin";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import * as glob from "glob";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
const root = process.cwd();
|
|
8
|
+
|
|
9
|
+
vi.mock(import("fs"), async (importOriginal) => {
|
|
10
|
+
const actual = await importOriginal();
|
|
11
|
+
return {
|
|
12
|
+
default: {
|
|
13
|
+
...actual.default,
|
|
14
|
+
readFileSync: vi.fn(),
|
|
15
|
+
writeFileSync: vi.fn(),
|
|
16
|
+
mkdirSync: vi.fn(),
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
vi.mock(import("path"), async (importOriginal) => {
|
|
22
|
+
const actual = await importOriginal();
|
|
23
|
+
return {
|
|
24
|
+
default: {
|
|
25
|
+
...actual.default,
|
|
26
|
+
resolve: vi.fn((...args) => args.map((a, i) => (i === 0 ? a : a.replace(/^\//, ""))).join("/")),
|
|
27
|
+
dirname: vi.fn(),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
vi.mock(import("glob"), async (importOriginal) => {
|
|
33
|
+
const actual = await importOriginal();
|
|
34
|
+
return {
|
|
35
|
+
glob: vi.fn(),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.clearAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("buildStart", () => {
|
|
44
|
+
test("Should write to dev application manifest file when using a serve command", async () => {
|
|
45
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
46
|
+
if (path === "package.json") {
|
|
47
|
+
return JSON.stringify({});
|
|
48
|
+
} else if (path === root + "/my-component.tsx") {
|
|
49
|
+
return `
|
|
50
|
+
const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-world", description: "This is an awesome tool!", extraInputs: [{
|
|
51
|
+
name: "doStuff",
|
|
52
|
+
type: "boolean",
|
|
53
|
+
description: "Should we do stuff?"
|
|
54
|
+
}]) { helloWorld(name: $name) }\`;
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
59
|
+
vi.spyOn(fs, "writeFileSync");
|
|
60
|
+
|
|
61
|
+
const plugin = ApplicationManifestPlugin();
|
|
62
|
+
plugin.configResolved({ command: "serve", server: {}, build: { outDir: "/dist" } });
|
|
63
|
+
await plugin.buildStart();
|
|
64
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
65
|
+
|
|
66
|
+
// Ignore the hash so we can do a snapshot that doesn't constantly change
|
|
67
|
+
let contentObj = JSON.parse(content);
|
|
68
|
+
contentObj.hash = "abc";
|
|
69
|
+
|
|
70
|
+
expect(fs.writeFileSync).toHaveBeenCalledTimes(2);
|
|
71
|
+
expect(file).toBe(root + "/dist/.application-manifest.json");
|
|
72
|
+
expect(contentObj).toMatchInlineSnapshot(`
|
|
73
|
+
{
|
|
74
|
+
"csp": {
|
|
75
|
+
"connectDomains": [],
|
|
76
|
+
"resourceDomains": [],
|
|
77
|
+
},
|
|
78
|
+
"format": "apollo-ai-app-manifest",
|
|
79
|
+
"hash": "abc",
|
|
80
|
+
"operations": [
|
|
81
|
+
{
|
|
82
|
+
"body": "query HelloWorldQuery($name: string!) {
|
|
83
|
+
helloWorld(name: $name)
|
|
84
|
+
}",
|
|
85
|
+
"id": "c2ceb00338549909d9a8cd5cc601bda78d8c27654294dfe408a6c3735beb26a6",
|
|
86
|
+
"name": "HelloWorldQuery",
|
|
87
|
+
"prefetch": false,
|
|
88
|
+
"tools": [
|
|
89
|
+
{
|
|
90
|
+
"description": "This is an awesome tool!",
|
|
91
|
+
"extraInputs": [
|
|
92
|
+
{
|
|
93
|
+
"description": "Should we do stuff?",
|
|
94
|
+
"name": "doStuff",
|
|
95
|
+
"type": "boolean",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
"name": "hello-world",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
"type": "query",
|
|
102
|
+
"variables": {
|
|
103
|
+
"name": "string",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
"resource": "http://localhost:undefined",
|
|
108
|
+
"version": "1",
|
|
109
|
+
}
|
|
110
|
+
`);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("Should NOT write to dev application manifest file when using a build command", async () => {
|
|
114
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
115
|
+
if (path === "package.json") {
|
|
116
|
+
return JSON.stringify({});
|
|
117
|
+
} else if (path === "my-component.tsx") {
|
|
118
|
+
return `
|
|
119
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
124
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
125
|
+
vi.spyOn(fs, "writeFileSync");
|
|
126
|
+
|
|
127
|
+
const plugin = ApplicationManifestPlugin();
|
|
128
|
+
plugin.configResolved({ command: "build", server: {} });
|
|
129
|
+
await plugin.buildStart();
|
|
130
|
+
|
|
131
|
+
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("Should not process files that do not contain gql tags", async () => {
|
|
135
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
136
|
+
if (path === "package.json") {
|
|
137
|
+
return JSON.stringify({});
|
|
138
|
+
} else if (path === root + "/my-component.tsx") {
|
|
139
|
+
return `
|
|
140
|
+
const MY_QUERY = \`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
145
|
+
vi.spyOn(fs, "writeFileSync");
|
|
146
|
+
|
|
147
|
+
const plugin = ApplicationManifestPlugin();
|
|
148
|
+
plugin.configResolved({ command: "serve", server: {}, build: { outDir: "/dist" } });
|
|
149
|
+
await plugin.buildStart();
|
|
150
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
151
|
+
|
|
152
|
+
// Ignore the hash so we can do a snapshot that doesn't constantly change
|
|
153
|
+
let contentObj = JSON.parse(content);
|
|
154
|
+
contentObj.hash = "abc";
|
|
155
|
+
|
|
156
|
+
expect(fs.writeFileSync).toHaveBeenCalledTimes(2);
|
|
157
|
+
expect(file).toBe(root + "/dist/.application-manifest.json");
|
|
158
|
+
expect(contentObj).toMatchInlineSnapshot(`
|
|
159
|
+
{
|
|
160
|
+
"csp": {
|
|
161
|
+
"connectDomains": [],
|
|
162
|
+
"resourceDomains": [],
|
|
163
|
+
},
|
|
164
|
+
"format": "apollo-ai-app-manifest",
|
|
165
|
+
"hash": "abc",
|
|
166
|
+
"operations": [],
|
|
167
|
+
"resource": "http://localhost:undefined",
|
|
168
|
+
"version": "1",
|
|
169
|
+
}
|
|
170
|
+
`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("Should capture queries when writing to manifest file", async () => {
|
|
174
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
175
|
+
if (path === "package.json") {
|
|
176
|
+
return JSON.stringify({});
|
|
177
|
+
} else if (path === root + "/my-component.tsx") {
|
|
178
|
+
return `
|
|
179
|
+
const MY_QUERY = gql\`query HelloWorldQuery { helloWorld }\`;
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
184
|
+
vi.spyOn(fs, "writeFileSync");
|
|
185
|
+
|
|
186
|
+
const plugin = ApplicationManifestPlugin();
|
|
187
|
+
plugin.configResolved({ command: "serve", server: {}, build: { outDir: "/dist" } });
|
|
188
|
+
await plugin.buildStart();
|
|
189
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
190
|
+
|
|
191
|
+
// Ignore the hash so we can do a snapshot that doesn't constantly change
|
|
192
|
+
let contentObj = JSON.parse(content);
|
|
193
|
+
contentObj.hash = "abc";
|
|
194
|
+
|
|
195
|
+
expect(fs.writeFileSync).toHaveBeenCalledTimes(2);
|
|
196
|
+
expect(file).toBe(root + "/dist/.application-manifest.json");
|
|
197
|
+
expect(contentObj).toMatchInlineSnapshot(`
|
|
198
|
+
{
|
|
199
|
+
"csp": {
|
|
200
|
+
"connectDomains": [],
|
|
201
|
+
"resourceDomains": [],
|
|
202
|
+
},
|
|
203
|
+
"format": "apollo-ai-app-manifest",
|
|
204
|
+
"hash": "abc",
|
|
205
|
+
"operations": [
|
|
206
|
+
{
|
|
207
|
+
"body": "query HelloWorldQuery {
|
|
208
|
+
helloWorld
|
|
209
|
+
}",
|
|
210
|
+
"id": "f8604bba13e2f589608c0eb36c3039c5ef3a4c5747bc1596f9dbcbe924dc90f9",
|
|
211
|
+
"name": "HelloWorldQuery",
|
|
212
|
+
"prefetch": false,
|
|
213
|
+
"tools": [],
|
|
214
|
+
"type": "query",
|
|
215
|
+
"variables": {},
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
"resource": "http://localhost:undefined",
|
|
219
|
+
"version": "1",
|
|
220
|
+
}
|
|
221
|
+
`);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("Should capture queries as prefetch when query is marked with @prefetch directive", async () => {
|
|
225
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
226
|
+
if (path === "package.json") {
|
|
227
|
+
return JSON.stringify({});
|
|
228
|
+
} else if (path === root + "/my-component.tsx") {
|
|
229
|
+
return `
|
|
230
|
+
const MY_QUERY = gql\`query HelloWorldQuery @prefetch { helloWorld }\`;
|
|
231
|
+
`;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
235
|
+
vi.spyOn(fs, "writeFileSync");
|
|
236
|
+
|
|
237
|
+
const plugin = ApplicationManifestPlugin();
|
|
238
|
+
plugin.configResolved({ command: "serve", server: {}, build: { outDir: "/dist" } });
|
|
239
|
+
await plugin.buildStart();
|
|
240
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
241
|
+
|
|
242
|
+
// Ignore the hash so we can do a snapshot that doesn't constantly change
|
|
243
|
+
let contentObj = JSON.parse(content);
|
|
244
|
+
contentObj.hash = "abc";
|
|
245
|
+
|
|
246
|
+
expect(fs.writeFileSync).toHaveBeenCalledTimes(2);
|
|
247
|
+
expect(file).toBe(root + "/dist/.application-manifest.json");
|
|
248
|
+
expect(contentObj).toMatchInlineSnapshot(`
|
|
249
|
+
{
|
|
250
|
+
"csp": {
|
|
251
|
+
"connectDomains": [],
|
|
252
|
+
"resourceDomains": [],
|
|
253
|
+
},
|
|
254
|
+
"format": "apollo-ai-app-manifest",
|
|
255
|
+
"hash": "abc",
|
|
256
|
+
"operations": [
|
|
257
|
+
{
|
|
258
|
+
"body": "query HelloWorldQuery {
|
|
259
|
+
helloWorld
|
|
260
|
+
}",
|
|
261
|
+
"id": "f8604bba13e2f589608c0eb36c3039c5ef3a4c5747bc1596f9dbcbe924dc90f9",
|
|
262
|
+
"name": "HelloWorldQuery",
|
|
263
|
+
"prefetch": true,
|
|
264
|
+
"prefetchID": "__anonymous",
|
|
265
|
+
"tools": [],
|
|
266
|
+
"type": "query",
|
|
267
|
+
"variables": {},
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
"resource": "http://localhost:undefined",
|
|
271
|
+
"version": "1",
|
|
272
|
+
}
|
|
273
|
+
`);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("Should error when multiple operations are marked with @prefetch", async () => {
|
|
277
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
278
|
+
if (path === "package.json") {
|
|
279
|
+
return JSON.stringify({});
|
|
280
|
+
} else if (path === "my-component.tsx") {
|
|
281
|
+
return `
|
|
282
|
+
const MY_QUERY = gql\`query HelloWorldQuery @prefetch { helloWorld }\`;
|
|
283
|
+
const MY_QUERY2 = gql\`query HelloWorldQuery2 @prefetch { helloWorld }\`;
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
288
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
289
|
+
vi.spyOn(fs, "writeFileSync");
|
|
290
|
+
|
|
291
|
+
const plugin = ApplicationManifestPlugin();
|
|
292
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
293
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
294
|
+
`[Error: Found multiple operations marked as \`@prefetch\`. You can only mark 1 operation with \`@prefetch\`.]`
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("Should capture mutations when writing to manifest file", async () => {
|
|
299
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
300
|
+
if (path === "package.json") {
|
|
301
|
+
return JSON.stringify({});
|
|
302
|
+
} else if (path === root + "/my-component.tsx") {
|
|
303
|
+
return `
|
|
304
|
+
const MY_QUERY = gql\`mutation HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
305
|
+
`;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
309
|
+
vi.spyOn(fs, "writeFileSync");
|
|
310
|
+
|
|
311
|
+
const plugin = ApplicationManifestPlugin();
|
|
312
|
+
plugin.configResolved({ command: "serve", server: {}, build: { outDir: "/dist" } });
|
|
313
|
+
await plugin.buildStart();
|
|
314
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
315
|
+
|
|
316
|
+
// Ignore the hash so we can do a snapshot that doesn't constantly change
|
|
317
|
+
let contentObj = JSON.parse(content);
|
|
318
|
+
contentObj.hash = "abc";
|
|
319
|
+
|
|
320
|
+
expect(fs.writeFileSync).toHaveBeenCalledTimes(2);
|
|
321
|
+
expect(file).toBe(root + "/dist/.application-manifest.json");
|
|
322
|
+
expect(contentObj).toMatchInlineSnapshot(`
|
|
323
|
+
{
|
|
324
|
+
"csp": {
|
|
325
|
+
"connectDomains": [],
|
|
326
|
+
"resourceDomains": [],
|
|
327
|
+
},
|
|
328
|
+
"format": "apollo-ai-app-manifest",
|
|
329
|
+
"hash": "abc",
|
|
330
|
+
"operations": [
|
|
331
|
+
{
|
|
332
|
+
"body": "mutation HelloWorldQuery {
|
|
333
|
+
helloWorld
|
|
334
|
+
}",
|
|
335
|
+
"id": "0c98e15f08542215c9c268192aaff732800bc33b79dddea7dc6fdf69c21b61a7",
|
|
336
|
+
"name": "HelloWorldQuery",
|
|
337
|
+
"prefetch": false,
|
|
338
|
+
"tools": [
|
|
339
|
+
{
|
|
340
|
+
"description": "This is an awesome tool!",
|
|
341
|
+
"name": "hello-world",
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
"type": "mutation",
|
|
345
|
+
"variables": {},
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
"resource": "http://localhost:undefined",
|
|
349
|
+
"version": "1",
|
|
350
|
+
}
|
|
351
|
+
`);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("Should throw error when a subscription operation type is discovered", async () => {
|
|
355
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
356
|
+
if (path === "package.json") {
|
|
357
|
+
return JSON.stringify({});
|
|
358
|
+
} else if (path === "my-component.tsx") {
|
|
359
|
+
return `
|
|
360
|
+
const MY_QUERY = gql\`subscription HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
361
|
+
`;
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
365
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
366
|
+
vi.spyOn(fs, "writeFileSync");
|
|
367
|
+
|
|
368
|
+
const plugin = ApplicationManifestPlugin();
|
|
369
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
370
|
+
|
|
371
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
372
|
+
`[Error: Found an unsupported operation type. Only Query and Mutation are supported.]`
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("Should use custom entry point when in serve mode and provided in package.json", async () => {
|
|
377
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
378
|
+
if (path === "package.json") {
|
|
379
|
+
return JSON.stringify({
|
|
380
|
+
entry: {
|
|
381
|
+
staging: "http://staging.awesome.com",
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
} else if (path === "my-component.tsx") {
|
|
385
|
+
return `
|
|
386
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
387
|
+
`;
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
391
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
392
|
+
vi.spyOn(fs, "writeFileSync");
|
|
393
|
+
|
|
394
|
+
const plugin = ApplicationManifestPlugin();
|
|
395
|
+
plugin.configResolved({ command: "serve", mode: "staging", server: {}, build: { outDir: "/dist" } });
|
|
396
|
+
await plugin.buildStart();
|
|
397
|
+
|
|
398
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
399
|
+
let contentObj = JSON.parse(content);
|
|
400
|
+
|
|
401
|
+
expect(contentObj.resource).toBe("http://staging.awesome.com");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("Should use https when enabled in server config", async () => {
|
|
405
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
406
|
+
if (path === "package.json") {
|
|
407
|
+
return JSON.stringify({});
|
|
408
|
+
} else if (path === "my-component.tsx") {
|
|
409
|
+
return `
|
|
410
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
411
|
+
`;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
415
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
416
|
+
vi.spyOn(fs, "writeFileSync");
|
|
417
|
+
|
|
418
|
+
const plugin = ApplicationManifestPlugin();
|
|
419
|
+
plugin.configResolved({ command: "serve", server: { https: {}, port: "5678" }, build: { outDir: "/dist" } });
|
|
420
|
+
await plugin.buildStart();
|
|
421
|
+
|
|
422
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
423
|
+
let contentObj = JSON.parse(content);
|
|
424
|
+
|
|
425
|
+
expect(contentObj.resource).toBe("https://localhost:5678");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("Should use custom host when specified in server config", async () => {
|
|
429
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
430
|
+
if (path === "package.json") {
|
|
431
|
+
return JSON.stringify({});
|
|
432
|
+
} else if (path === "my-component.tsx") {
|
|
433
|
+
return `
|
|
434
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
435
|
+
`;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
439
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
440
|
+
vi.spyOn(fs, "writeFileSync");
|
|
441
|
+
|
|
442
|
+
const plugin = ApplicationManifestPlugin();
|
|
443
|
+
plugin.configResolved({
|
|
444
|
+
command: "serve",
|
|
445
|
+
server: { port: "5678", host: "awesome.com" },
|
|
446
|
+
build: { outDir: "/dist" },
|
|
447
|
+
});
|
|
448
|
+
await plugin.buildStart();
|
|
449
|
+
|
|
450
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
451
|
+
let contentObj = JSON.parse(content);
|
|
452
|
+
|
|
453
|
+
expect(contentObj.resource).toBe("http://awesome.com:5678");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("Should error when tool name is not provided", async () => {
|
|
457
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
458
|
+
if (path === "package.json") {
|
|
459
|
+
return JSON.stringify({});
|
|
460
|
+
} else if (path === "my-component.tsx") {
|
|
461
|
+
return `
|
|
462
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool { helloWorld }\`;
|
|
463
|
+
`;
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
467
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
468
|
+
vi.spyOn(fs, "writeFileSync");
|
|
469
|
+
|
|
470
|
+
const plugin = ApplicationManifestPlugin();
|
|
471
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
472
|
+
|
|
473
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
474
|
+
`[Error: 'name' argument must be supplied for @tool]`
|
|
475
|
+
);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test("Should error when tool description is not provided", async () => {
|
|
479
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
480
|
+
if (path === "package.json") {
|
|
481
|
+
return JSON.stringify({});
|
|
482
|
+
} else if (path === "my-component.tsx") {
|
|
483
|
+
return `
|
|
484
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world") { helloWorld }\`;
|
|
485
|
+
`;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
489
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
490
|
+
vi.spyOn(fs, "writeFileSync");
|
|
491
|
+
|
|
492
|
+
const plugin = ApplicationManifestPlugin();
|
|
493
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
494
|
+
|
|
495
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
496
|
+
`[Error: 'description' argument must be supplied for @tool]`
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("Should error when tool name is not a string", async () => {
|
|
501
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
502
|
+
if (path === "package.json") {
|
|
503
|
+
return JSON.stringify({});
|
|
504
|
+
} else if (path === "my-component.tsx") {
|
|
505
|
+
return `
|
|
506
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: true) { helloWorld }\`;
|
|
507
|
+
`;
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
511
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
512
|
+
vi.spyOn(fs, "writeFileSync");
|
|
513
|
+
|
|
514
|
+
const plugin = ApplicationManifestPlugin();
|
|
515
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
516
|
+
|
|
517
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
518
|
+
`[Error: Expected argument 'name' to be of type 'StringValue' but found 'BooleanValue' instead.]`
|
|
519
|
+
);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("Should error when tool description is not a string", async () => {
|
|
523
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
524
|
+
if (path === "package.json") {
|
|
525
|
+
return JSON.stringify({});
|
|
526
|
+
} else if (path === "my-component.tsx") {
|
|
527
|
+
return `
|
|
528
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: false) { helloWorld }\`;
|
|
529
|
+
`;
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
533
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
534
|
+
vi.spyOn(fs, "writeFileSync");
|
|
535
|
+
|
|
536
|
+
const plugin = ApplicationManifestPlugin();
|
|
537
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
538
|
+
|
|
539
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
540
|
+
`[Error: Expected argument 'description' to be of type 'StringValue' but found 'BooleanValue' instead.]`
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("Should error when extraInputs is not an array", async () => {
|
|
545
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
546
|
+
if (path === "package.json") {
|
|
547
|
+
return JSON.stringify({});
|
|
548
|
+
} else if (path === "my-component.tsx") {
|
|
549
|
+
return `
|
|
550
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "hello", extraInputs: false ) { helloWorld }\`;
|
|
551
|
+
`;
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
555
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
556
|
+
vi.spyOn(fs, "writeFileSync");
|
|
557
|
+
|
|
558
|
+
const plugin = ApplicationManifestPlugin();
|
|
559
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
560
|
+
|
|
561
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
562
|
+
`[Error: Expected argument 'extraInputs' to be of type 'ListValue' but found 'BooleanValue' instead.]`
|
|
563
|
+
);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("Should error when an unknown type is discovered", async () => {
|
|
567
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
568
|
+
if (path === "package.json") {
|
|
569
|
+
return JSON.stringify({});
|
|
570
|
+
} else if (path === "my-component.tsx") {
|
|
571
|
+
return `
|
|
572
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "hello", extraInputs: [{
|
|
573
|
+
name: 3.1
|
|
574
|
+
}] ) { helloWorld }\`;
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
579
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
580
|
+
vi.spyOn(fs, "writeFileSync");
|
|
581
|
+
|
|
582
|
+
const plugin = ApplicationManifestPlugin();
|
|
583
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
584
|
+
|
|
585
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
586
|
+
`[Error: Error when parsing directive values: unexpected type 'FloatValue']`
|
|
587
|
+
);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("Should order operations and fragments when generating normalized operation", async () => {
|
|
591
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
592
|
+
if (path === "package.json") {
|
|
593
|
+
return JSON.stringify({});
|
|
594
|
+
} else if (path === root + "/my-component.tsx") {
|
|
595
|
+
return `
|
|
596
|
+
const MY_QUERY = gql\`
|
|
597
|
+
fragment A on User { firstName }
|
|
598
|
+
fragment B on User { lastName }
|
|
599
|
+
query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") {
|
|
600
|
+
helloWorld {
|
|
601
|
+
...B
|
|
602
|
+
...A
|
|
603
|
+
...C
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
fragment C on User { middleName }
|
|
607
|
+
}\`;
|
|
608
|
+
`;
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
612
|
+
vi.spyOn(fs, "writeFileSync");
|
|
613
|
+
|
|
614
|
+
const plugin = ApplicationManifestPlugin();
|
|
615
|
+
plugin.configResolved({ command: "serve", server: {}, build: { outDir: "/dist" } });
|
|
616
|
+
await plugin.buildStart();
|
|
617
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
618
|
+
|
|
619
|
+
// Ignore the hash so we can do a snapshot that doesn't constantly change
|
|
620
|
+
let contentObj = JSON.parse(content);
|
|
621
|
+
contentObj.hash = "abc";
|
|
622
|
+
|
|
623
|
+
expect(fs.writeFileSync).toHaveBeenCalledTimes(2);
|
|
624
|
+
expect(file).toBe(`${root}/dist/.application-manifest.json`);
|
|
625
|
+
expect(contentObj).toMatchInlineSnapshot(`
|
|
626
|
+
{
|
|
627
|
+
"csp": {
|
|
628
|
+
"connectDomains": [],
|
|
629
|
+
"resourceDomains": [],
|
|
630
|
+
},
|
|
631
|
+
"format": "apollo-ai-app-manifest",
|
|
632
|
+
"hash": "abc",
|
|
633
|
+
"operations": [
|
|
634
|
+
{
|
|
635
|
+
"body": "query HelloWorldQuery {
|
|
636
|
+
helloWorld {
|
|
637
|
+
...B
|
|
638
|
+
...A
|
|
639
|
+
...C
|
|
640
|
+
__typename
|
|
641
|
+
}
|
|
642
|
+
fragment
|
|
643
|
+
C
|
|
644
|
+
on
|
|
645
|
+
User {
|
|
646
|
+
middleName
|
|
647
|
+
__typename
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
fragment A on User {
|
|
652
|
+
firstName
|
|
653
|
+
__typename
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
fragment B on User {
|
|
657
|
+
lastName
|
|
658
|
+
__typename
|
|
659
|
+
}",
|
|
660
|
+
"id": "58359ad006a8e1a6cdabe4b49c0322e8a41d71c5194a796e6432be055220b9ec",
|
|
661
|
+
"name": "HelloWorldQuery",
|
|
662
|
+
"prefetch": false,
|
|
663
|
+
"tools": [
|
|
664
|
+
{
|
|
665
|
+
"description": "This is an awesome tool!",
|
|
666
|
+
"name": "hello-world",
|
|
667
|
+
},
|
|
668
|
+
],
|
|
669
|
+
"type": "query",
|
|
670
|
+
"variables": {},
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
"resource": "http://localhost:undefined",
|
|
674
|
+
"version": "1",
|
|
675
|
+
}
|
|
676
|
+
`);
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
describe("writeBundle", () => {
|
|
681
|
+
test("Should use custom entry point when in build mode and provided in package.json", async () => {
|
|
682
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
683
|
+
if (path === "package.json") {
|
|
684
|
+
return JSON.stringify({
|
|
685
|
+
entry: {
|
|
686
|
+
staging: "http://staging.awesome.com",
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
} else if (path === "my-component.tsx") {
|
|
690
|
+
return `
|
|
691
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
692
|
+
`;
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
696
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
697
|
+
vi.spyOn(path, "dirname").mockImplementation(() => "/dist");
|
|
698
|
+
vi.spyOn(fs, "writeFileSync");
|
|
699
|
+
|
|
700
|
+
const plugin = ApplicationManifestPlugin();
|
|
701
|
+
plugin.configResolved({ command: "build", mode: "staging", server: {}, build: { outDir: "/dist/" } });
|
|
702
|
+
await plugin.buildStart();
|
|
703
|
+
await plugin.writeBundle();
|
|
704
|
+
|
|
705
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
706
|
+
let contentObj = JSON.parse(content);
|
|
707
|
+
|
|
708
|
+
expect(contentObj.resource).toBe("http://staging.awesome.com");
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test("Should use index.html when in build production and not provided in package.json", async () => {
|
|
712
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
713
|
+
if (path === "package.json") {
|
|
714
|
+
return JSON.stringify({});
|
|
715
|
+
} else if (path === "my-component.tsx") {
|
|
716
|
+
return `
|
|
717
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
718
|
+
`;
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
722
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
723
|
+
vi.spyOn(path, "dirname").mockImplementation(() => "/dist");
|
|
724
|
+
vi.spyOn(fs, "writeFileSync");
|
|
725
|
+
|
|
726
|
+
const plugin = ApplicationManifestPlugin();
|
|
727
|
+
plugin.configResolved({ command: "build", mode: "production", server: {}, build: { outDir: "/dist/" } });
|
|
728
|
+
await plugin.buildStart();
|
|
729
|
+
await plugin.writeBundle();
|
|
730
|
+
|
|
731
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
732
|
+
let contentObj = JSON.parse(content);
|
|
733
|
+
|
|
734
|
+
expect(contentObj.resource).toBe("index.html");
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test("Should throw an error when in build mode and using a mode that is not production and not provided in package.json", async () => {
|
|
738
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
739
|
+
if (path === "package.json") {
|
|
740
|
+
return JSON.stringify({});
|
|
741
|
+
} else if (path === "my-component.tsx") {
|
|
742
|
+
return `
|
|
743
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
744
|
+
`;
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
748
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
749
|
+
vi.spyOn(path, "dirname").mockImplementation(() => "/dist");
|
|
750
|
+
vi.spyOn(fs, "writeFileSync");
|
|
751
|
+
|
|
752
|
+
const plugin = ApplicationManifestPlugin();
|
|
753
|
+
plugin.configResolved({ command: "build", mode: "staging", server: {}, build: { outDir: "/dist/" } });
|
|
754
|
+
await plugin.buildStart();
|
|
755
|
+
|
|
756
|
+
await expect(async () => await plugin.writeBundle()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
757
|
+
`[Error: No entry point found for mode "staging". Entry points other than "development" and "production" must be defined in package.json file.]`
|
|
758
|
+
);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
test("Should always write to both locations when running in build mode", async () => {
|
|
762
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
763
|
+
if (path === "package.json") {
|
|
764
|
+
return JSON.stringify({});
|
|
765
|
+
} else if (path === "my-component.tsx") {
|
|
766
|
+
return `
|
|
767
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
768
|
+
`;
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
772
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
773
|
+
vi.spyOn(path, "dirname").mockImplementation(() => "/dist");
|
|
774
|
+
vi.spyOn(fs, "writeFileSync");
|
|
775
|
+
|
|
776
|
+
const plugin = ApplicationManifestPlugin();
|
|
777
|
+
plugin.configResolved({ command: "build", mode: "production", server: {}, build: { outDir: "/dist/" } });
|
|
778
|
+
await plugin.buildStart();
|
|
779
|
+
await plugin.writeBundle();
|
|
780
|
+
|
|
781
|
+
expect(fs.writeFileSync).toBeCalledTimes(2);
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
describe("configureServer", () => {
|
|
786
|
+
test("Should write to manifest file when package.json or file is updated", async () => {
|
|
787
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
788
|
+
if (path === "package.json") {
|
|
789
|
+
return JSON.stringify({});
|
|
790
|
+
} else if (path === "my-component.tsx") {
|
|
791
|
+
return `
|
|
792
|
+
const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-world", description: "This is an awesome tool!", extraInputs: [{
|
|
793
|
+
name: "doStuff",
|
|
794
|
+
type: "boolean",
|
|
795
|
+
description: "Should we do stuff?"
|
|
796
|
+
}]) { helloWorld(name: $name) }\`;
|
|
797
|
+
`;
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
801
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
802
|
+
vi.spyOn(fs, "writeFileSync");
|
|
803
|
+
|
|
804
|
+
const server = {
|
|
805
|
+
watcher: {
|
|
806
|
+
init: () => {
|
|
807
|
+
this._callbacks = [];
|
|
808
|
+
},
|
|
809
|
+
on: (_event: string, callback: Function) => {
|
|
810
|
+
this._callbacks.push(callback);
|
|
811
|
+
},
|
|
812
|
+
trigger: async (file) => {
|
|
813
|
+
for (const callback of this._callbacks) {
|
|
814
|
+
await callback(file);
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
server.watcher.init();
|
|
820
|
+
|
|
821
|
+
const plugin = ApplicationManifestPlugin();
|
|
822
|
+
plugin.configResolved({ command: "serve", server: {}, build: { outDir: "/dist" } });
|
|
823
|
+
await plugin.buildStart();
|
|
824
|
+
await plugin.configureServer(server);
|
|
825
|
+
await server.watcher.trigger("package.json");
|
|
826
|
+
await server.watcher.trigger("my-component.tsx");
|
|
827
|
+
|
|
828
|
+
expect(fs.writeFileSync).toBeCalledTimes(6);
|
|
829
|
+
});
|
|
830
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
2
|
import { glob } from "glob";
|
|
3
3
|
import { gqlPluckFromCodeStringSync } from "@graphql-tools/graphql-tag-pluck";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
5
|
import {
|
|
6
|
+
ArgumentNode,
|
|
6
7
|
Kind,
|
|
7
8
|
ListTypeNode,
|
|
8
9
|
NamedTypeNode,
|
|
@@ -18,11 +19,9 @@ import {
|
|
|
18
19
|
import { ApolloClient, ApolloLink, InMemoryCache } from "@apollo/client";
|
|
19
20
|
import Observable from "rxjs";
|
|
20
21
|
import path from "path";
|
|
21
|
-
import fs from "fs";
|
|
22
22
|
|
|
23
23
|
const root = process.cwd();
|
|
24
24
|
|
|
25
|
-
// TODO: Do we need "validation" of the types for the different properties? Probably?
|
|
26
25
|
const getRawValue = (node: ValueNode): any => {
|
|
27
26
|
switch (node.kind) {
|
|
28
27
|
case Kind.STRING:
|
|
@@ -35,10 +34,36 @@ const getRawValue = (node: ValueNode): any => {
|
|
|
35
34
|
acc[field.name.value] = getRawValue(field.value);
|
|
36
35
|
return acc;
|
|
37
36
|
}, {});
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(`Error when parsing directive values: unexpected type '${node.kind}'`);
|
|
38
39
|
}
|
|
39
40
|
};
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
const getTypedDirectiveArgument = (
|
|
43
|
+
argumentName: string,
|
|
44
|
+
expectedType: Kind,
|
|
45
|
+
directiveArguments: readonly ArgumentNode[] | undefined
|
|
46
|
+
) => {
|
|
47
|
+
if (!directiveArguments || directiveArguments.length === 0) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let argument = directiveArguments.find((directiveArgument) => directiveArgument.name.value === argumentName);
|
|
52
|
+
|
|
53
|
+
if (!argument) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (argument.value.kind != expectedType) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Expected argument '${argumentName}' to be of type '${expectedType}' but found '${argument.value.kind}' instead.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return getRawValue(argument.value);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function getTypeName(type: TypeNode): string {
|
|
42
67
|
let t = type;
|
|
43
68
|
while (t.kind === "NonNullType" || t.kind === "ListType") {
|
|
44
69
|
t = (t as NonNullTypeNode | ListTypeNode).type;
|
|
@@ -78,12 +103,22 @@ export const ApplicationManifestPlugin = () => {
|
|
|
78
103
|
).directives
|
|
79
104
|
?.filter((d) => d.name.value === "tool")
|
|
80
105
|
.map((directive) => {
|
|
81
|
-
const
|
|
82
|
-
|
|
106
|
+
const name = getTypedDirectiveArgument("name", Kind.STRING, directive.arguments);
|
|
107
|
+
const description = getTypedDirectiveArgument("description", Kind.STRING, directive.arguments);
|
|
108
|
+
const extraInputs = getTypedDirectiveArgument("extraInputs", Kind.LIST, directive.arguments);
|
|
109
|
+
|
|
110
|
+
if (!name) {
|
|
111
|
+
throw new Error("'name' argument must be supplied for @tool");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!description) {
|
|
115
|
+
throw new Error("'description' argument must be supplied for @tool");
|
|
116
|
+
}
|
|
117
|
+
|
|
83
118
|
return {
|
|
84
|
-
name
|
|
85
|
-
description
|
|
86
|
-
extraInputs
|
|
119
|
+
name,
|
|
120
|
+
description,
|
|
121
|
+
extraInputs,
|
|
87
122
|
};
|
|
88
123
|
});
|
|
89
124
|
|
|
@@ -172,11 +207,11 @@ export const ApplicationManifestPlugin = () => {
|
|
|
172
207
|
},
|
|
173
208
|
};
|
|
174
209
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
// Always write to build directory so the MCP server picks it up
|
|
211
|
+
const dest = path.resolve(root, config.build.outDir, ".application-manifest.json");
|
|
212
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
213
|
+
writeFileSync(dest, JSON.stringify(manifest));
|
|
214
|
+
|
|
180
215
|
// Always write to the dev location so that the app can bundle the manifest content
|
|
181
216
|
writeFileSync(".application-manifest.json", JSON.stringify(manifest));
|
|
182
217
|
};
|
package/src/vite/index.ts
CHANGED