@apollo/client-ai-apps 0.2.1 → 0.2.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.
- package/dist/hooks/useToolName.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/vite/application_manifest_plugin.d.ts +1 -2
- package/dist/vite/index.js +31 -8
- package/package.json +1 -1
- package/src/apollo_client/client.ts +1 -1
- package/src/hooks/useToolInput.test.ts +13 -0
- 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/application_manifest_plugin.test.ts +830 -0
- package/src/vite/application_manifest_plugin.ts +45 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const useToolName: () =>
|
|
1
|
+
export declare const useToolName: () => string | undefined;
|
package/dist/index.js
CHANGED
|
@@ -116,7 +116,7 @@ var ExtendedApolloClient = class extends ApolloClient {
|
|
|
116
116
|
}
|
|
117
117
|
async prefetchData() {
|
|
118
118
|
this.manifest.operations.forEach((operation) => {
|
|
119
|
-
if (operation.prefetch && operation.prefetchID && window.openai.toolOutput.prefetch[operation.prefetchID]) {
|
|
119
|
+
if (operation.prefetch && operation.prefetchID && window.openai.toolOutput.prefetch?.[operation.prefetchID]) {
|
|
120
120
|
this.writeQuery({
|
|
121
121
|
query: parse(operation.body),
|
|
122
122
|
data: window.openai.toolOutput.prefetch[operation.prefetchID].data
|
|
@@ -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.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 } });
|
|
@@ -135,7 +159,7 @@ var ApplicationManifestPlugin = () => {
|
|
|
135
159
|
};
|
|
136
160
|
if (config.command === "build") {
|
|
137
161
|
const dest = path.resolve(root, config.build.outDir, ".application-manifest.json");
|
|
138
|
-
|
|
162
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
139
163
|
writeFileSync(dest, JSON.stringify(manifest));
|
|
140
164
|
}
|
|
141
165
|
writeFileSync(".application-manifest.json", JSON.stringify(manifest));
|
|
@@ -209,6 +233,5 @@ function removeClientDirective(doc) {
|
|
|
209
233
|
}
|
|
210
234
|
export {
|
|
211
235
|
ApplicationManifestPlugin,
|
|
212
|
-
getTypeName,
|
|
213
236
|
sortTopLevelDefinitions
|
|
214
237
|
};
|
package/package.json
CHANGED
|
@@ -59,7 +59,7 @@ export class ExtendedApolloClient extends ApolloClient {
|
|
|
59
59
|
async prefetchData() {
|
|
60
60
|
// Write prefetched data to the cache
|
|
61
61
|
this.manifest.operations.forEach((operation) => {
|
|
62
|
-
if (operation.prefetch && operation.prefetchID && window.openai.toolOutput.prefetch[operation.prefetchID]) {
|
|
62
|
+
if (operation.prefetch && operation.prefetchID && window.openai.toolOutput.prefetch?.[operation.prefetchID]) {
|
|
63
63
|
this.writeQuery({
|
|
64
64
|
query: parse(operation.body),
|
|
65
65
|
data: window.openai.toolOutput.prefetch[operation.prefetchID].data,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expect, test, vi } from "vitest";
|
|
2
|
+
import { useToolInput } from "./useToolInput";
|
|
3
|
+
import { renderHook } from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
test("Should return tool input when called", async () => {
|
|
6
|
+
vi.stubGlobal("openai", {
|
|
7
|
+
toolInput: { name: "John" },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const { result } = renderHook(() => useToolInput());
|
|
11
|
+
|
|
12
|
+
expect(result.current).toEqual({ name: "John" });
|
|
13
|
+
});
|
|
@@ -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,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
|
+
vi.mock(import("fs"), async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal();
|
|
9
|
+
return {
|
|
10
|
+
default: {
|
|
11
|
+
...actual.default,
|
|
12
|
+
readFileSync: vi.fn(),
|
|
13
|
+
writeFileSync: vi.fn(),
|
|
14
|
+
mkdirSync: vi.fn(),
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
vi.mock(import("path"), async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal();
|
|
21
|
+
return {
|
|
22
|
+
default: {
|
|
23
|
+
...actual.default,
|
|
24
|
+
resolve: vi.fn(),
|
|
25
|
+
dirname: vi.fn(),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
vi.mock(import("glob"), async (importOriginal) => {
|
|
31
|
+
const actual = await importOriginal();
|
|
32
|
+
return {
|
|
33
|
+
glob: vi.fn(),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("buildStart", () => {
|
|
42
|
+
test("Should write to dev application manifest file when using a serve command", async () => {
|
|
43
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
44
|
+
if (path === "package.json") {
|
|
45
|
+
return JSON.stringify({});
|
|
46
|
+
} else if (path === "my-component.tsx") {
|
|
47
|
+
return `
|
|
48
|
+
const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-world", description: "This is an awesome tool!", extraInputs: [{
|
|
49
|
+
name: "doStuff",
|
|
50
|
+
type: "boolean",
|
|
51
|
+
description: "Should we do stuff?"
|
|
52
|
+
}]) { helloWorld(name: $name) }\`;
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
57
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
58
|
+
vi.spyOn(fs, "writeFileSync");
|
|
59
|
+
|
|
60
|
+
const plugin = ApplicationManifestPlugin();
|
|
61
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
62
|
+
await plugin.buildStart();
|
|
63
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
64
|
+
|
|
65
|
+
// Ignore the hash so we can do a snapshot that doesn't constantly change
|
|
66
|
+
let contentObj = JSON.parse(content);
|
|
67
|
+
contentObj.hash = "abc";
|
|
68
|
+
|
|
69
|
+
expect(fs.writeFileSync).toHaveBeenCalledOnce();
|
|
70
|
+
expect(file).toBe(".application-manifest.json");
|
|
71
|
+
expect(contentObj).toMatchInlineSnapshot(`
|
|
72
|
+
{
|
|
73
|
+
"csp": {
|
|
74
|
+
"connectDomains": [],
|
|
75
|
+
"resourceDomains": [],
|
|
76
|
+
},
|
|
77
|
+
"format": "apollo-ai-app-manifest",
|
|
78
|
+
"hash": "abc",
|
|
79
|
+
"operations": [
|
|
80
|
+
{
|
|
81
|
+
"body": "query HelloWorldQuery($name: string!) {
|
|
82
|
+
helloWorld(name: $name)
|
|
83
|
+
}",
|
|
84
|
+
"id": "c2ceb00338549909d9a8cd5cc601bda78d8c27654294dfe408a6c3735beb26a6",
|
|
85
|
+
"name": "HelloWorldQuery",
|
|
86
|
+
"prefetch": false,
|
|
87
|
+
"tools": [
|
|
88
|
+
{
|
|
89
|
+
"description": "This is an awesome tool!",
|
|
90
|
+
"extraInputs": [
|
|
91
|
+
{
|
|
92
|
+
"description": "Should we do stuff?",
|
|
93
|
+
"name": "doStuff",
|
|
94
|
+
"type": "boolean",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
"name": "hello-world",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
"type": "query",
|
|
101
|
+
"variables": {
|
|
102
|
+
"name": "string",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
"resource": "http://localhost:undefined",
|
|
107
|
+
"version": "1",
|
|
108
|
+
}
|
|
109
|
+
`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("Should NOT write to dev application manifest file when using a build command", async () => {
|
|
113
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
114
|
+
if (path === "package.json") {
|
|
115
|
+
return JSON.stringify({});
|
|
116
|
+
} else if (path === "my-component.tsx") {
|
|
117
|
+
return `
|
|
118
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
123
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
124
|
+
vi.spyOn(fs, "writeFileSync");
|
|
125
|
+
|
|
126
|
+
const plugin = ApplicationManifestPlugin();
|
|
127
|
+
plugin.configResolved({ command: "build", server: {} });
|
|
128
|
+
await plugin.buildStart();
|
|
129
|
+
|
|
130
|
+
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("Should not process files that do not contain gql tags", async () => {
|
|
134
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
135
|
+
if (path === "package.json") {
|
|
136
|
+
return JSON.stringify({});
|
|
137
|
+
} else if (path === "my-component.tsx") {
|
|
138
|
+
return `
|
|
139
|
+
const MY_QUERY = \`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
144
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
145
|
+
vi.spyOn(fs, "writeFileSync");
|
|
146
|
+
|
|
147
|
+
const plugin = ApplicationManifestPlugin();
|
|
148
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
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).toHaveBeenCalledOnce();
|
|
157
|
+
expect(file).toBe(".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 === "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(path, "resolve").mockImplementation((_, file) => file);
|
|
185
|
+
vi.spyOn(fs, "writeFileSync");
|
|
186
|
+
|
|
187
|
+
const plugin = ApplicationManifestPlugin();
|
|
188
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
189
|
+
await plugin.buildStart();
|
|
190
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
191
|
+
|
|
192
|
+
// Ignore the hash so we can do a snapshot that doesn't constantly change
|
|
193
|
+
let contentObj = JSON.parse(content);
|
|
194
|
+
contentObj.hash = "abc";
|
|
195
|
+
|
|
196
|
+
expect(fs.writeFileSync).toHaveBeenCalledOnce();
|
|
197
|
+
expect(file).toBe(".application-manifest.json");
|
|
198
|
+
expect(contentObj).toMatchInlineSnapshot(`
|
|
199
|
+
{
|
|
200
|
+
"csp": {
|
|
201
|
+
"connectDomains": [],
|
|
202
|
+
"resourceDomains": [],
|
|
203
|
+
},
|
|
204
|
+
"format": "apollo-ai-app-manifest",
|
|
205
|
+
"hash": "abc",
|
|
206
|
+
"operations": [
|
|
207
|
+
{
|
|
208
|
+
"body": "query HelloWorldQuery {
|
|
209
|
+
helloWorld
|
|
210
|
+
}",
|
|
211
|
+
"id": "f8604bba13e2f589608c0eb36c3039c5ef3a4c5747bc1596f9dbcbe924dc90f9",
|
|
212
|
+
"name": "HelloWorldQuery",
|
|
213
|
+
"prefetch": false,
|
|
214
|
+
"tools": [],
|
|
215
|
+
"type": "query",
|
|
216
|
+
"variables": {},
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
"resource": "http://localhost:undefined",
|
|
220
|
+
"version": "1",
|
|
221
|
+
}
|
|
222
|
+
`);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("Should capture queries as prefetch when query is marked with @prefetch directive", async () => {
|
|
226
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
227
|
+
if (path === "package.json") {
|
|
228
|
+
return JSON.stringify({});
|
|
229
|
+
} else if (path === "my-component.tsx") {
|
|
230
|
+
return `
|
|
231
|
+
const MY_QUERY = gql\`query HelloWorldQuery @prefetch { helloWorld }\`;
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
236
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
237
|
+
vi.spyOn(fs, "writeFileSync");
|
|
238
|
+
|
|
239
|
+
const plugin = ApplicationManifestPlugin();
|
|
240
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
241
|
+
await plugin.buildStart();
|
|
242
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
243
|
+
|
|
244
|
+
// Ignore the hash so we can do a snapshot that doesn't constantly change
|
|
245
|
+
let contentObj = JSON.parse(content);
|
|
246
|
+
contentObj.hash = "abc";
|
|
247
|
+
|
|
248
|
+
expect(fs.writeFileSync).toHaveBeenCalledOnce();
|
|
249
|
+
expect(file).toBe(".application-manifest.json");
|
|
250
|
+
expect(contentObj).toMatchInlineSnapshot(`
|
|
251
|
+
{
|
|
252
|
+
"csp": {
|
|
253
|
+
"connectDomains": [],
|
|
254
|
+
"resourceDomains": [],
|
|
255
|
+
},
|
|
256
|
+
"format": "apollo-ai-app-manifest",
|
|
257
|
+
"hash": "abc",
|
|
258
|
+
"operations": [
|
|
259
|
+
{
|
|
260
|
+
"body": "query HelloWorldQuery {
|
|
261
|
+
helloWorld
|
|
262
|
+
}",
|
|
263
|
+
"id": "f8604bba13e2f589608c0eb36c3039c5ef3a4c5747bc1596f9dbcbe924dc90f9",
|
|
264
|
+
"name": "HelloWorldQuery",
|
|
265
|
+
"prefetch": true,
|
|
266
|
+
"prefetchID": "__anonymous",
|
|
267
|
+
"tools": [],
|
|
268
|
+
"type": "query",
|
|
269
|
+
"variables": {},
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
"resource": "http://localhost:undefined",
|
|
273
|
+
"version": "1",
|
|
274
|
+
}
|
|
275
|
+
`);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("Should error when multiple operations are marked with @prefetch", async () => {
|
|
279
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
280
|
+
if (path === "package.json") {
|
|
281
|
+
return JSON.stringify({});
|
|
282
|
+
} else if (path === "my-component.tsx") {
|
|
283
|
+
return `
|
|
284
|
+
const MY_QUERY = gql\`query HelloWorldQuery @prefetch { helloWorld }\`;
|
|
285
|
+
const MY_QUERY2 = gql\`query HelloWorldQuery2 @prefetch { helloWorld }\`;
|
|
286
|
+
`;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
290
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
291
|
+
vi.spyOn(fs, "writeFileSync");
|
|
292
|
+
|
|
293
|
+
const plugin = ApplicationManifestPlugin();
|
|
294
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
295
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
296
|
+
`[Error: Found multiple operations marked as \`@prefetch\`. You can only mark 1 operation with \`@prefetch\`.]`
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("Should capture mutations when writing to manifest file", async () => {
|
|
301
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
302
|
+
if (path === "package.json") {
|
|
303
|
+
return JSON.stringify({});
|
|
304
|
+
} else if (path === "my-component.tsx") {
|
|
305
|
+
return `
|
|
306
|
+
const MY_QUERY = gql\`mutation HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
311
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
312
|
+
vi.spyOn(fs, "writeFileSync");
|
|
313
|
+
|
|
314
|
+
const plugin = ApplicationManifestPlugin();
|
|
315
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
316
|
+
await plugin.buildStart();
|
|
317
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
318
|
+
|
|
319
|
+
// Ignore the hash so we can do a snapshot that doesn't constantly change
|
|
320
|
+
let contentObj = JSON.parse(content);
|
|
321
|
+
contentObj.hash = "abc";
|
|
322
|
+
|
|
323
|
+
expect(fs.writeFileSync).toHaveBeenCalledOnce();
|
|
324
|
+
expect(file).toBe(".application-manifest.json");
|
|
325
|
+
expect(contentObj).toMatchInlineSnapshot(`
|
|
326
|
+
{
|
|
327
|
+
"csp": {
|
|
328
|
+
"connectDomains": [],
|
|
329
|
+
"resourceDomains": [],
|
|
330
|
+
},
|
|
331
|
+
"format": "apollo-ai-app-manifest",
|
|
332
|
+
"hash": "abc",
|
|
333
|
+
"operations": [
|
|
334
|
+
{
|
|
335
|
+
"body": "mutation HelloWorldQuery {
|
|
336
|
+
helloWorld
|
|
337
|
+
}",
|
|
338
|
+
"id": "0c98e15f08542215c9c268192aaff732800bc33b79dddea7dc6fdf69c21b61a7",
|
|
339
|
+
"name": "HelloWorldQuery",
|
|
340
|
+
"prefetch": false,
|
|
341
|
+
"tools": [
|
|
342
|
+
{
|
|
343
|
+
"description": "This is an awesome tool!",
|
|
344
|
+
"name": "hello-world",
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
"type": "mutation",
|
|
348
|
+
"variables": {},
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
"resource": "http://localhost:undefined",
|
|
352
|
+
"version": "1",
|
|
353
|
+
}
|
|
354
|
+
`);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("Should throw error when a subscription operation type is discovered", async () => {
|
|
358
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
359
|
+
if (path === "package.json") {
|
|
360
|
+
return JSON.stringify({});
|
|
361
|
+
} else if (path === "my-component.tsx") {
|
|
362
|
+
return `
|
|
363
|
+
const MY_QUERY = gql\`subscription HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
364
|
+
`;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
368
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
369
|
+
vi.spyOn(fs, "writeFileSync");
|
|
370
|
+
|
|
371
|
+
const plugin = ApplicationManifestPlugin();
|
|
372
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
373
|
+
|
|
374
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
375
|
+
`[Error: Found an unsupported operation type. Only Query and Mutation are supported.]`
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("Should use custom entry point when in serve mode and provided in package.json", async () => {
|
|
380
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
381
|
+
if (path === "package.json") {
|
|
382
|
+
return JSON.stringify({
|
|
383
|
+
entry: {
|
|
384
|
+
staging: "http://staging.awesome.com",
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
} else if (path === "my-component.tsx") {
|
|
388
|
+
return `
|
|
389
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
390
|
+
`;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
394
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
395
|
+
vi.spyOn(fs, "writeFileSync");
|
|
396
|
+
|
|
397
|
+
const plugin = ApplicationManifestPlugin();
|
|
398
|
+
plugin.configResolved({ command: "serve", mode: "staging", server: {} });
|
|
399
|
+
await plugin.buildStart();
|
|
400
|
+
|
|
401
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
402
|
+
let contentObj = JSON.parse(content);
|
|
403
|
+
|
|
404
|
+
expect(contentObj.resource).toBe("http://staging.awesome.com");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("Should use https when enabled in server config", async () => {
|
|
408
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
409
|
+
if (path === "package.json") {
|
|
410
|
+
return JSON.stringify({});
|
|
411
|
+
} else if (path === "my-component.tsx") {
|
|
412
|
+
return `
|
|
413
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
414
|
+
`;
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
418
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
419
|
+
vi.spyOn(fs, "writeFileSync");
|
|
420
|
+
|
|
421
|
+
const plugin = ApplicationManifestPlugin();
|
|
422
|
+
plugin.configResolved({ command: "serve", server: { https: {}, port: "5678" } });
|
|
423
|
+
await plugin.buildStart();
|
|
424
|
+
|
|
425
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
426
|
+
let contentObj = JSON.parse(content);
|
|
427
|
+
|
|
428
|
+
expect(contentObj.resource).toBe("https://localhost:5678");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("Should use custom host when specified in server config", async () => {
|
|
432
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
433
|
+
if (path === "package.json") {
|
|
434
|
+
return JSON.stringify({});
|
|
435
|
+
} else if (path === "my-component.tsx") {
|
|
436
|
+
return `
|
|
437
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
|
|
438
|
+
`;
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
442
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
443
|
+
vi.spyOn(fs, "writeFileSync");
|
|
444
|
+
|
|
445
|
+
const plugin = ApplicationManifestPlugin();
|
|
446
|
+
plugin.configResolved({ command: "serve", server: { port: "5678", host: "awesome.com" } });
|
|
447
|
+
await plugin.buildStart();
|
|
448
|
+
|
|
449
|
+
let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
|
|
450
|
+
let contentObj = JSON.parse(content);
|
|
451
|
+
|
|
452
|
+
expect(contentObj.resource).toBe("http://awesome.com:5678");
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("Should error when tool name is not provided", async () => {
|
|
456
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
457
|
+
if (path === "package.json") {
|
|
458
|
+
return JSON.stringify({});
|
|
459
|
+
} else if (path === "my-component.tsx") {
|
|
460
|
+
return `
|
|
461
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool { helloWorld }\`;
|
|
462
|
+
`;
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
466
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
467
|
+
vi.spyOn(fs, "writeFileSync");
|
|
468
|
+
|
|
469
|
+
const plugin = ApplicationManifestPlugin();
|
|
470
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
471
|
+
|
|
472
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
473
|
+
`[Error: 'name' argument must be supplied for @tool]`
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("Should error when tool description is not provided", async () => {
|
|
478
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
479
|
+
if (path === "package.json") {
|
|
480
|
+
return JSON.stringify({});
|
|
481
|
+
} else if (path === "my-component.tsx") {
|
|
482
|
+
return `
|
|
483
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world") { helloWorld }\`;
|
|
484
|
+
`;
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
488
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
489
|
+
vi.spyOn(fs, "writeFileSync");
|
|
490
|
+
|
|
491
|
+
const plugin = ApplicationManifestPlugin();
|
|
492
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
493
|
+
|
|
494
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
495
|
+
`[Error: 'description' argument must be supplied for @tool]`
|
|
496
|
+
);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("Should error when tool name is not a string", async () => {
|
|
500
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
501
|
+
if (path === "package.json") {
|
|
502
|
+
return JSON.stringify({});
|
|
503
|
+
} else if (path === "my-component.tsx") {
|
|
504
|
+
return `
|
|
505
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: true) { helloWorld }\`;
|
|
506
|
+
`;
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
510
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
511
|
+
vi.spyOn(fs, "writeFileSync");
|
|
512
|
+
|
|
513
|
+
const plugin = ApplicationManifestPlugin();
|
|
514
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
515
|
+
|
|
516
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
517
|
+
`[Error: Expected argument 'name' to be of type 'StringValue' but found 'BooleanValue' instead.]`
|
|
518
|
+
);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("Should error when tool description is not a string", async () => {
|
|
522
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
523
|
+
if (path === "package.json") {
|
|
524
|
+
return JSON.stringify({});
|
|
525
|
+
} else if (path === "my-component.tsx") {
|
|
526
|
+
return `
|
|
527
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: false) { helloWorld }\`;
|
|
528
|
+
`;
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
532
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
533
|
+
vi.spyOn(fs, "writeFileSync");
|
|
534
|
+
|
|
535
|
+
const plugin = ApplicationManifestPlugin();
|
|
536
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
537
|
+
|
|
538
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
539
|
+
`[Error: Expected argument 'description' to be of type 'StringValue' but found 'BooleanValue' instead.]`
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("Should error when extraInputs is not an array", async () => {
|
|
544
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
545
|
+
if (path === "package.json") {
|
|
546
|
+
return JSON.stringify({});
|
|
547
|
+
} else if (path === "my-component.tsx") {
|
|
548
|
+
return `
|
|
549
|
+
const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "hello", extraInputs: false ) { helloWorld }\`;
|
|
550
|
+
`;
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
554
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
555
|
+
vi.spyOn(fs, "writeFileSync");
|
|
556
|
+
|
|
557
|
+
const plugin = ApplicationManifestPlugin();
|
|
558
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
559
|
+
|
|
560
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
561
|
+
`[Error: Expected argument 'extraInputs' to be of type 'ListValue' but found 'BooleanValue' instead.]`
|
|
562
|
+
);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("Should error when an unknown type is discovered", 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: "hello-world", description: "hello", extraInputs: [{
|
|
572
|
+
name: 3.1
|
|
573
|
+
}] ) { helloWorld }\`;
|
|
574
|
+
`;
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
578
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
579
|
+
vi.spyOn(fs, "writeFileSync");
|
|
580
|
+
|
|
581
|
+
const plugin = ApplicationManifestPlugin();
|
|
582
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
583
|
+
|
|
584
|
+
await expect(async () => await plugin.buildStart()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
585
|
+
`[Error: Error when parsing directive values: unexpected type 'FloatValue']`
|
|
586
|
+
);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("Should order operations and fragments when generating normalized operation", async () => {
|
|
590
|
+
vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
|
|
591
|
+
if (path === "package.json") {
|
|
592
|
+
return JSON.stringify({});
|
|
593
|
+
} else if (path === "my-component.tsx") {
|
|
594
|
+
return `
|
|
595
|
+
const MY_QUERY = gql\`
|
|
596
|
+
fragment A on User { firstName }
|
|
597
|
+
fragment B on User { lastName }
|
|
598
|
+
query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") {
|
|
599
|
+
helloWorld {
|
|
600
|
+
...B
|
|
601
|
+
...A
|
|
602
|
+
...C
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
fragment C on User { middleName }
|
|
606
|
+
}\`;
|
|
607
|
+
`;
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
vi.spyOn(glob, "glob").mockImplementation(() => Promise.resolve(["my-component.tsx"]));
|
|
611
|
+
vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
|
|
612
|
+
vi.spyOn(fs, "writeFileSync");
|
|
613
|
+
|
|
614
|
+
const plugin = ApplicationManifestPlugin();
|
|
615
|
+
plugin.configResolved({ command: "serve", server: {} });
|
|
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).toHaveBeenCalledOnce();
|
|
624
|
+
expect(file).toBe(".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: {} });
|
|
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(3);
|
|
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
|
|
|
@@ -174,7 +209,7 @@ export const ApplicationManifestPlugin = () => {
|
|
|
174
209
|
|
|
175
210
|
if (config.command === "build") {
|
|
176
211
|
const dest = path.resolve(root, config.build.outDir, ".application-manifest.json");
|
|
177
|
-
|
|
212
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
178
213
|
writeFileSync(dest, JSON.stringify(manifest));
|
|
179
214
|
}
|
|
180
215
|
// Always write to the dev location so that the app can bundle the manifest content
|