@animaapp/anima-sdk 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-dev.log +0 -0
- package/.turbo/turbo-test.log +21 -0
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +734 -718
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/anima.ts +35 -13
- package/src/dataStream.ts +26 -11
- package/src/errors.ts +31 -5
- package/src/utils/figma.spec.ts +31 -0
- package/src/utils/figma.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animaapp/anima-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Anima's JavaScript utilities library",
|
|
6
6
|
"author": "Anima App, Inc.",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "vite build",
|
|
23
|
-
"
|
|
23
|
+
"dev": "vite build --watch",
|
|
24
|
+
"test": "vitest --watch=false",
|
|
24
25
|
"prepack": "yarn build"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
package/src/anima.ts
CHANGED
|
@@ -96,17 +96,41 @@ export class Anima {
|
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
if (!response.ok) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
const errorData = await response
|
|
100
|
+
.json()
|
|
101
|
+
.catch(() => "HTTP error from Anima API");
|
|
102
|
+
|
|
103
|
+
if (typeof errorData === "string") {
|
|
104
|
+
throw new CodegenError({
|
|
105
|
+
name: errorData,
|
|
106
|
+
reason: "Unknown",
|
|
107
|
+
detail: { status: response.status },
|
|
108
|
+
status: response.status,
|
|
109
|
+
});
|
|
106
110
|
}
|
|
111
|
+
|
|
112
|
+
if (typeof errorData !== "object") {
|
|
113
|
+
throw new CodegenError({
|
|
114
|
+
name: `Error "${errorData}"`,
|
|
115
|
+
reason: "Unknown",
|
|
116
|
+
detail: { status: response.status },
|
|
117
|
+
status: response.status,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (errorData.error?.name === "ZodError") {
|
|
122
|
+
throw new CodegenError({
|
|
123
|
+
name: "HTTP error from Anima API",
|
|
124
|
+
reason: "Invalid body payload",
|
|
125
|
+
detail: errorData.error.issues,
|
|
126
|
+
status: response.status,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
107
130
|
throw new CodegenError({
|
|
108
|
-
name: "HTTP
|
|
109
|
-
reason:
|
|
131
|
+
name: errorData.error?.name || "HTTP error from Anima API",
|
|
132
|
+
reason: "Unknown",
|
|
133
|
+
detail: { status: response.status },
|
|
110
134
|
status: response.status,
|
|
111
135
|
});
|
|
112
136
|
}
|
|
@@ -223,7 +247,6 @@ export class Anima {
|
|
|
223
247
|
}
|
|
224
248
|
|
|
225
249
|
case "error": {
|
|
226
|
-
// not sure if we want to throw on "stream" errors
|
|
227
250
|
throw new CodegenError({
|
|
228
251
|
name: data.payload.errorName,
|
|
229
252
|
reason: data.payload.reason,
|
|
@@ -232,13 +255,12 @@ export class Anima {
|
|
|
232
255
|
|
|
233
256
|
case "done": {
|
|
234
257
|
if (!result.files) {
|
|
235
|
-
// not sure if we want to throw on "logical" errors
|
|
236
|
-
// I think we should throw only on "HTTP" errors
|
|
237
258
|
throw new CodegenError({
|
|
238
259
|
name: "Invalid response",
|
|
239
|
-
reason: "No
|
|
260
|
+
reason: "No code generated",
|
|
240
261
|
});
|
|
241
262
|
}
|
|
263
|
+
|
|
242
264
|
result.tokenUsage = data.payload.tokenUsage;
|
|
243
265
|
return result as AnimaSDKResult;
|
|
244
266
|
}
|
package/src/dataStream.ts
CHANGED
|
@@ -4,7 +4,15 @@ import type { GetCodeParams, SSECodgenMessage } from "./types";
|
|
|
4
4
|
|
|
5
5
|
export type StreamCodgenMessage =
|
|
6
6
|
| Exclude<SSECodgenMessage, { type: "error" }>
|
|
7
|
-
| {
|
|
7
|
+
| {
|
|
8
|
+
type: "error";
|
|
9
|
+
payload: {
|
|
10
|
+
name: string;
|
|
11
|
+
message: CodegenErrorReason;
|
|
12
|
+
status?: number;
|
|
13
|
+
detail?: unknown;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* Start the code generation and creates a ReadableStream to output its result.
|
|
@@ -24,11 +32,11 @@ export const createCodegenStream = (
|
|
|
24
32
|
anima
|
|
25
33
|
.generateCode(params, (message) => {
|
|
26
34
|
if (message.type === "error") {
|
|
27
|
-
|
|
28
|
-
controller.enqueue({
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
35
|
+
// TODO: It's a dead code. It's never reached, since all errors are thrown.
|
|
36
|
+
// controller.enqueue({
|
|
37
|
+
// type: "error",
|
|
38
|
+
// payload: { message: message.payload.reason },
|
|
39
|
+
// });
|
|
32
40
|
} else {
|
|
33
41
|
controller.enqueue(message);
|
|
34
42
|
}
|
|
@@ -39,10 +47,11 @@ export const createCodegenStream = (
|
|
|
39
47
|
})
|
|
40
48
|
.then((_result) => {
|
|
41
49
|
controller.enqueue({
|
|
42
|
-
type: "done",
|
|
50
|
+
type: "done",
|
|
51
|
+
payload: {
|
|
43
52
|
tokenUsage: _result.tokenUsage,
|
|
44
53
|
sessionId: _result.sessionId,
|
|
45
|
-
}
|
|
54
|
+
},
|
|
46
55
|
});
|
|
47
56
|
controller.close();
|
|
48
57
|
})
|
|
@@ -50,8 +59,10 @@ export const createCodegenStream = (
|
|
|
50
59
|
controller.enqueue({
|
|
51
60
|
type: "error",
|
|
52
61
|
payload: {
|
|
62
|
+
name: "name" in error ? error.name : "Unknown error",
|
|
53
63
|
message: "message" in error ? error.message : "Unknown",
|
|
54
64
|
status: "status" in error ? error.status : undefined,
|
|
65
|
+
detail: "detail" in error ? error.detail : undefined,
|
|
55
66
|
},
|
|
56
67
|
});
|
|
57
68
|
controller.close();
|
|
@@ -81,10 +92,14 @@ export const createCodegenResponseEventStream = async (
|
|
|
81
92
|
if (
|
|
82
93
|
firstMessage.done ||
|
|
83
94
|
!firstMessage.value ||
|
|
84
|
-
firstMessage.value?.type === "error" &&
|
|
95
|
+
(firstMessage.value?.type === "error" &&
|
|
96
|
+
firstMessage.value?.payload?.status)
|
|
85
97
|
) {
|
|
86
98
|
return new Response(JSON.stringify(firstMessage.value), {
|
|
87
|
-
status:
|
|
99
|
+
status:
|
|
100
|
+
firstMessage.value?.type === "error"
|
|
101
|
+
? (firstMessage.value?.payload?.status ?? 500)
|
|
102
|
+
: 500,
|
|
88
103
|
headers: {
|
|
89
104
|
"Content-Type": "application/json",
|
|
90
105
|
},
|
|
@@ -107,7 +122,7 @@ export const createCodegenResponseEventStream = async (
|
|
|
107
122
|
status: 200,
|
|
108
123
|
headers: {
|
|
109
124
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
110
|
-
|
|
125
|
+
Connection: "keep-alive",
|
|
111
126
|
"Cache-Control": "no-cache",
|
|
112
127
|
},
|
|
113
128
|
});
|
package/src/errors.ts
CHANGED
|
@@ -1,21 +1,47 @@
|
|
|
1
1
|
// TODO: `CodegenErrorReason` should be imported from `anima-public-api`
|
|
2
|
+
/**
|
|
3
|
+
* Errors from Public API
|
|
4
|
+
*/
|
|
2
5
|
export type CodegenErrorReason =
|
|
3
|
-
| "
|
|
6
|
+
| "Selected node type is not supported"
|
|
7
|
+
| "Selected node is a page with multiple children"
|
|
4
8
|
| "There is no node with the given id"
|
|
5
9
|
| "Invalid Figma token"
|
|
10
|
+
| "Anima API connection error"
|
|
6
11
|
| "Figma token expired"
|
|
7
|
-
| "
|
|
12
|
+
| "Invalid user token"
|
|
13
|
+
| "Figma file not found"
|
|
14
|
+
| "Figma rate limit exceeded"
|
|
15
|
+
| "Unknown";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Errors from the SDK
|
|
19
|
+
*/
|
|
20
|
+
export type SDKErrorReason =
|
|
21
|
+
| "Invalid body payload"
|
|
22
|
+
| "No code generated"
|
|
8
23
|
| "Connection closed before the 'done' message"
|
|
9
|
-
| "Unknown"
|
|
10
24
|
| "Response body is null";
|
|
11
25
|
|
|
12
26
|
export class CodegenError extends Error {
|
|
13
27
|
status?: number;
|
|
28
|
+
detail?: unknown;
|
|
14
29
|
|
|
15
|
-
constructor({
|
|
30
|
+
constructor({
|
|
31
|
+
name,
|
|
32
|
+
reason,
|
|
33
|
+
status,
|
|
34
|
+
detail,
|
|
35
|
+
}: {
|
|
36
|
+
name: string;
|
|
37
|
+
reason: CodegenErrorReason | SDKErrorReason;
|
|
38
|
+
status?: number;
|
|
39
|
+
detail?: unknown;
|
|
40
|
+
}) {
|
|
16
41
|
super();
|
|
17
|
-
this.name =
|
|
42
|
+
this.name = name;
|
|
18
43
|
this.message = reason;
|
|
44
|
+
this.detail = detail;
|
|
19
45
|
this.status = status;
|
|
20
46
|
}
|
|
21
47
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatToFigmaLink } from "./figma";
|
|
3
|
+
|
|
4
|
+
describe("# figma", () => {
|
|
5
|
+
describe(".formatToFigmaLink", () => {
|
|
6
|
+
it("generates a link with file key and node id", () => {
|
|
7
|
+
const url = formatToFigmaLink({
|
|
8
|
+
fileKey: "file-key",
|
|
9
|
+
nodeId: "1:2",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(url.href).toBe(
|
|
13
|
+
"https://www.figma.com/design/file-key?node-id=1-2"
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('when the "duplicate" flag is enabled', () => {
|
|
18
|
+
it("generates a link including the '/duplicated' path", () => {
|
|
19
|
+
const url = formatToFigmaLink({
|
|
20
|
+
fileKey: "file-key",
|
|
21
|
+
nodeId: "1:2",
|
|
22
|
+
duplicate: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(url.href).toBe(
|
|
26
|
+
"https://www.figma.com/design/file-key/duplicate?node-id=1-2"
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
package/src/utils/figma.ts
CHANGED
|
@@ -29,13 +29,19 @@ export const isValidFigmaUrl = (
|
|
|
29
29
|
export const formatToFigmaLink = ({
|
|
30
30
|
fileKey,
|
|
31
31
|
nodeId,
|
|
32
|
+
duplicate,
|
|
32
33
|
}: {
|
|
33
34
|
fileKey: string;
|
|
34
35
|
nodeId: string;
|
|
36
|
+
duplicate?: boolean;
|
|
35
37
|
}) => {
|
|
36
38
|
const url = new URL("https://www.figma.com");
|
|
37
39
|
url.pathname = `design/${fileKey}`;
|
|
38
40
|
|
|
41
|
+
if (duplicate) {
|
|
42
|
+
url.pathname = `${url.pathname}/duplicate`;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
if (nodeId) {
|
|
40
46
|
url.searchParams.set("node-id", nodeId.replace(":", "-"));
|
|
41
47
|
}
|