@animaapp/anima-sdk 0.2.7 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animaapp/anima-sdk",
3
- "version": "0.2.7",
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
- "tests": "vitest run --dir ./tests",
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
@@ -76,7 +76,7 @@ export class Anima {
76
76
  Accept: "text/event-stream",
77
77
  },
78
78
  body: JSON.stringify({
79
- tracking: params.tracking,
79
+ tracking,
80
80
  fileKey: params.fileKey,
81
81
  figmaToken: params.figmaToken,
82
82
  nodesId: params.nodesId,
@@ -96,17 +96,41 @@ export class Anima {
96
96
  });
97
97
 
98
98
  if (!response.ok) {
99
- let errorMessage;
100
- try {
101
- const errorData = await response.json();
102
- errorMessage =
103
- errorData.message || `HTTP error! status: ${response.status}`;
104
- } catch {
105
- errorMessage = `HTTP error! status: ${response.status}`;
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 Error",
109
- reason: errorMessage,
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 files found",
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
- | { type: "error"; payload: { message: CodegenErrorReason, status?: number } };
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
- console.log('NOT SURE IF THIS IS REACHABLE, ALL ERRORS ARE THROWING');
28
- controller.enqueue({
29
- type: "error",
30
- payload: { message: message.payload.reason },
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", payload: {
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" && firstMessage.value?.payload?.status
95
+ (firstMessage.value?.type === "error" &&
96
+ firstMessage.value?.payload?.status)
85
97
  ) {
86
98
  return new Response(JSON.stringify(firstMessage.value), {
87
- status: firstMessage.value?.type === "error" ? (firstMessage.value?.payload?.status ?? 500) : 500,
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
- "Connection": "keep-alive",
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
- | "The selected node is not a frame"
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
- | "No files found"
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({ name, reason, status }: { name: string; reason: CodegenErrorReason; status?: number }) {
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 = `[Codegen Error] ${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
+ });
@@ -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
  }