@animaapp/anima-sdk 0.3.0 → 0.3.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/example.env ADDED
@@ -0,0 +1,2 @@
1
+ # `FIGMA_AUTH_TOKEN` is used only for updating the design snapshot used by the tests
2
+ FIGMA_AUTH_TOKEN=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animaapp/anima-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "Anima's JavaScript utilities library",
6
6
  "author": "Anima App, Inc.",
@@ -22,7 +22,8 @@
22
22
  "build": "vite build",
23
23
  "dev": "vite build --watch",
24
24
  "test": "vitest --watch=false",
25
- "prepack": "yarn build"
25
+ "prepack": "yarn build",
26
+ "update-design-test-snapshot": "tsx tests/design/update.ts"
26
27
  },
27
28
  "dependencies": {
28
29
  "@animaapp/http-client-figma": "^1.0.2",
@@ -30,9 +31,11 @@
30
31
  "zod": "^3.24.1"
31
32
  },
32
33
  "devDependencies": {
34
+ "dotenv": "^16.4.7",
35
+ "tsx": "^4.19.3",
33
36
  "vite": "^6.0.11",
34
37
  "vite-plugin-dts": "^4.5.0",
35
38
  "vite-tsconfig-paths": "^5.1.4",
36
39
  "vitest": "^3.0.5"
37
40
  }
38
- }
41
+ }
package/tsconfig.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/tsconfig",
3
3
  "extends": "../tsconfig.json",
4
+ "include": ["./src/**/*", "./tests/**/*"],
4
5
  "exclude": ["../sdk-react"]
5
6
  }
@@ -1,12 +0,0 @@
1
-
2
- vite v6.0.11 building for production...
3
- transforming (1) src/index.ts✓ 14 modules transformed.
4
- rendering chunks (1)...
5
- [vite:dts] Start generate declaration files...
6
- computing gzip size (0)...computing gzip size (1)...dist/index.cjs 88.33 kB │ gzip: 23.70 kB │ map: 326.42 kB
7
- [vite:dts] Start rollup declaration files...
8
- Analysis will use the bundled TypeScript version 5.7.2
9
- [vite:dts] Declaration files built in 2117ms.
10
- 
11
- rendering chunks (1)...computing gzip size (1)...dist/index.js 125.80 kB │ gzip: 28.43 kB │ map: 340.53 kB
12
- ✓ built in 2.40s
File without changes
@@ -1,21 +0,0 @@
1
-
2
- [?25l
3
-  RUN  v3.0.5 /Users/macabeus/ApenasMeu/anima/anima-sdk/sdk
4
-
5
- [?2026h
6
-  ❯ src/utils/figma.spec.ts [queued]
7
-
8
-  Test Files 0 passed (1)
9
-  Tests 0 passed (0)
10
-  Start at 21:43:55
11
-  Duration 102ms
12
- [?2026l ✓ src/utils/figma.spec.ts (2 tests) 2ms
13
- ✓ # figma > .formatToFigmaLink > generates a link with file key and node id
14
- ✓ # figma > .formatToFigmaLink > when the "duplicate" flag is enabled > generates a link including the '/duplicated' path
15
-
16
-  Test Files  1 passed (1)
17
-  Tests  2 passed (2)
18
-  Start at  21:43:55
19
-  Duration  234ms (transform 24ms, setup 0ms, collect 20ms, tests 2ms, environment 0ms, prepare 44ms)
20
-
21
- [?25h
package/src/anima.ts DELETED
@@ -1,281 +0,0 @@
1
- import { CodegenError } from "./errors";
2
- import { validateSettings } from "./settings";
3
- import {
4
- AnimaSDKResult,
5
- GetCodeHandler,
6
- GetCodeParams,
7
- SSECodgenMessage,
8
- } from "./types";
9
-
10
- export type Auth =
11
- | { token: string; teamId: string } // for Anima user, it's mandatory to have an associated team
12
- | { token: string; userId?: string }; // for users from a 3rd-party integrations, they may have optionally a user id
13
-
14
- export class Anima {
15
- #auth?: Auth;
16
- #apiBaseAddress: string;
17
-
18
- constructor({
19
- auth,
20
- apiBaseAddress = "https://public-api.animaapp.com",
21
- }: {
22
- auth?: Auth;
23
- apiBaseAddress?: string;
24
- path?: string;
25
- } = {}) {
26
- this.#apiBaseAddress = apiBaseAddress;
27
-
28
- if (auth) {
29
- this.auth = auth;
30
- }
31
- }
32
-
33
- protected hasAuth() {
34
- return !!this.#auth;
35
- }
36
-
37
- set auth(auth: Auth) {
38
- this.#auth = auth;
39
- }
40
-
41
- protected get headers() {
42
- const headers: Record<string, string> = {
43
- "Content-Type": "application/json",
44
- };
45
-
46
- if (this.#auth) {
47
- headers["Authorization"] = `Bearer ${this.#auth.token}`;
48
-
49
- if ("teamId" in this.#auth) {
50
- headers["X-Team-Id"] = this.#auth.teamId;
51
- }
52
- }
53
-
54
- return headers;
55
- }
56
-
57
- async generateCode(params: GetCodeParams, handler: GetCodeHandler = {}) {
58
- if (this.hasAuth() === false) {
59
- throw new Error('It needs to set "auth" before calling this method.');
60
- }
61
-
62
- const result: Partial<AnimaSDKResult> = {};
63
- const settings = validateSettings(params.settings);
64
-
65
- let tracking = params.tracking;
66
- if (this.#auth && "userId" in this.#auth && this.#auth.userId) {
67
- if (!tracking?.externalId) {
68
- tracking = { externalId: this.#auth.userId };
69
- }
70
- }
71
-
72
- const response = await fetch(`${this.#apiBaseAddress}/v1/codegen`, {
73
- method: "POST",
74
- headers: {
75
- ...this.headers,
76
- Accept: "text/event-stream",
77
- },
78
- body: JSON.stringify({
79
- tracking,
80
- fileKey: params.fileKey,
81
- figmaToken: params.figmaToken,
82
- nodesId: params.nodesId,
83
- assetsStorage: params.assetsStorage,
84
- language: settings.language,
85
- model: settings.model,
86
- framework: settings.framework,
87
- styling: settings.styling,
88
- uiLibrary: settings.uiLibrary,
89
- enableTranslation: settings.enableTranslation,
90
- enableUILibraryTheming: settings.enableUILibraryTheming,
91
- enableCompactStructure: settings.enableCompactStructure,
92
- enableAutoSplit: settings.enableAutoSplit,
93
- autoSplitThreshold: settings.autoSplitThreshold,
94
- disableMarkedForExport: settings.disableMarkedForExport,
95
- }),
96
- });
97
-
98
- if (!response.ok) {
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
- });
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
-
130
- throw new CodegenError({
131
- name: errorData.error?.name || "HTTP error from Anima API",
132
- reason: "Unknown",
133
- detail: { status: response.status },
134
- status: response.status,
135
- });
136
- }
137
-
138
- if (!response.body) {
139
- throw new CodegenError({
140
- name: "Stream Error",
141
- reason: "Response body is null",
142
- status: response.status,
143
- });
144
- }
145
-
146
- const reader = response.body.getReader();
147
- const decoder = new TextDecoder();
148
- let buffer = "";
149
-
150
- try {
151
- while (true) {
152
- const { done, value } = await reader.read();
153
- if (done) {
154
- break;
155
- }
156
-
157
- buffer += decoder.decode(value, { stream: true });
158
-
159
- const lines = buffer.split("\n");
160
-
161
- // Process all complete lines
162
- buffer = lines.pop() || ""; // Keep the last incomplete line in the buffer
163
-
164
- for (const line of lines) {
165
- if (!line.trim() || line.startsWith(":")) continue;
166
-
167
- if (line.startsWith("data: ")) {
168
- let data: SSECodgenMessage;
169
- try {
170
- data = JSON.parse(line.slice(6));
171
- } catch {
172
- // ignore malformed JSON
173
- continue;
174
- }
175
-
176
- switch (data.type) {
177
- case "queueing": {
178
- typeof handler === "function"
179
- ? handler(data)
180
- : handler.onQueueing?.();
181
- break;
182
- }
183
- case "start": {
184
- result.sessionId = data.sessionId;
185
- typeof handler === "function"
186
- ? handler(data)
187
- : handler.onStart?.({ sessionId: data.sessionId });
188
- break;
189
- }
190
-
191
- case "pre_codegen": {
192
- typeof handler === "function"
193
- ? handler(data)
194
- : handler.onPreCodegen?.({ message: data.message });
195
- break;
196
- }
197
-
198
- case "assets_uploaded": {
199
- typeof handler === "function"
200
- ? handler(data)
201
- : handler.onAssetsUploaded?.();
202
- break;
203
- }
204
-
205
- case "assets_list": {
206
- result.assets = data.payload.assets;
207
-
208
- typeof handler === "function"
209
- ? handler(data)
210
- : handler.onAssetsList?.(data.payload);
211
- break;
212
- }
213
-
214
- case "figma_metadata": {
215
- result.figmaFileName = data.figmaFileName;
216
- result.figmaSelectedFrameName = data.figmaSelectedFrameName;
217
-
218
- typeof handler === "function"
219
- ? handler(data)
220
- : handler.onFigmaMetadata?.({
221
- figmaFileName: data.figmaFileName,
222
- figmaSelectedFrameName: data.figmaSelectedFrameName,
223
- });
224
- break;
225
- }
226
-
227
- case "generating_code": {
228
- if (data.payload.status === "success") {
229
- result.files = data.payload.files;
230
- }
231
-
232
- typeof handler === "function"
233
- ? handler(data)
234
- : handler.onGeneratingCode?.({
235
- status: data.payload.status,
236
- progress: data.payload.progress,
237
- files: data.payload.files,
238
- });
239
- break;
240
- }
241
-
242
- case "codegen_completed": {
243
- typeof handler === "function"
244
- ? handler(data)
245
- : handler.onCodegenCompleted?.();
246
- break;
247
- }
248
-
249
- case "error": {
250
- throw new CodegenError({
251
- name: data.payload.errorName,
252
- reason: data.payload.reason,
253
- });
254
- }
255
-
256
- case "done": {
257
- if (!result.files) {
258
- throw new CodegenError({
259
- name: "Invalid response",
260
- reason: "No code generated",
261
- });
262
- }
263
-
264
- result.tokenUsage = data.payload.tokenUsage;
265
- return result as AnimaSDKResult;
266
- }
267
- }
268
- }
269
- }
270
- }
271
- } finally {
272
- reader.cancel();
273
- }
274
-
275
- throw new CodegenError({
276
- name: "Connection",
277
- reason: "Connection closed before the 'done' message",
278
- status: 500,
279
- });
280
- }
281
- }
package/src/dataStream.ts DELETED
@@ -1,129 +0,0 @@
1
- import type { Anima } from "./anima";
2
- import type { CodegenErrorReason } from "./errors";
3
- import type { GetCodeParams, SSECodgenMessage } from "./types";
4
-
5
- export type StreamCodgenMessage =
6
- | Exclude<SSECodgenMessage, { type: "error" }>
7
- | {
8
- type: "error";
9
- payload: {
10
- name: string;
11
- message: CodegenErrorReason;
12
- status?: number;
13
- detail?: unknown;
14
- };
15
- };
16
-
17
- /**
18
- * Start the code generation and creates a ReadableStream to output its result.
19
- *
20
- * The stream is closed when the codegen ends.
21
- *
22
- * @param {Anima} anima - An Anima service instance to generate the code from.
23
- * @param {GetCodeParams} params - Parameters required for the code generation process.
24
- * @returns {ReadableStream<StreamCodgenMessage>} - A ReadableStream that emits messages related to the code generation process.
25
- */
26
- export const createCodegenStream = (
27
- anima: Anima,
28
- params: GetCodeParams
29
- ): ReadableStream<StreamCodgenMessage> => {
30
- return new ReadableStream({
31
- start(controller) {
32
- anima
33
- .generateCode(params, (message) => {
34
- if (message.type === "error") {
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
- // });
40
- } else {
41
- controller.enqueue(message);
42
- }
43
-
44
- if (message.type === "aborted" || message.type === "error") {
45
- controller.close();
46
- }
47
- })
48
- .then((_result) => {
49
- controller.enqueue({
50
- type: "done",
51
- payload: {
52
- tokenUsage: _result.tokenUsage,
53
- sessionId: _result.sessionId,
54
- },
55
- });
56
- controller.close();
57
- })
58
- .catch((error) => {
59
- controller.enqueue({
60
- type: "error",
61
- payload: {
62
- name: "name" in error ? error.name : "Unknown error",
63
- message: "message" in error ? error.message : "Unknown",
64
- status: "status" in error ? error.status : undefined,
65
- detail: "detail" in error ? error.detail : undefined,
66
- },
67
- });
68
- controller.close();
69
- });
70
- },
71
- });
72
- };
73
-
74
- /**
75
- * Creates a Server-Sent Events (SSE) `Response` that forwards all messages from the code generation stream.
76
- *
77
- * But, if the first message indicates an error (e.g., connection failed), the function returns a 500 response with the error message.
78
- *
79
- * @param {Anima} anima - The Anima instance to use for creating the data stream.
80
- * @param {GetCodeParams} params - The parameters for the code generation request.
81
- * @returns {Promise<Response>} - A promise that resolves to an HTTP response.
82
- */
83
- export const createCodegenResponseEventStream = async (
84
- anima: Anima,
85
- params: GetCodeParams
86
- ): Promise<Response> => {
87
- const stream = createCodegenStream(anima, params);
88
-
89
- const [verifyStream, consumerStream] = stream.tee();
90
- const firstMessage = await verifyStream.getReader().read();
91
-
92
- if (
93
- firstMessage.done ||
94
- !firstMessage.value ||
95
- (firstMessage.value?.type === "error" &&
96
- firstMessage.value?.payload?.status)
97
- ) {
98
- return new Response(JSON.stringify(firstMessage.value), {
99
- status:
100
- firstMessage.value?.type === "error"
101
- ? (firstMessage.value?.payload?.status ?? 500)
102
- : 500,
103
- headers: {
104
- "Content-Type": "application/json",
105
- },
106
- });
107
- }
108
-
109
- const encoder = new TextEncoder();
110
- const seeStream = consumerStream.pipeThrough(
111
- new TransformStream({
112
- transform(chunk, controller) {
113
- const sseString = `event: ${chunk.type}\ndata: ${JSON.stringify(
114
- chunk
115
- )}\n\n`;
116
- controller.enqueue(encoder.encode(sseString));
117
- },
118
- })
119
- );
120
-
121
- return new Response(seeStream, {
122
- status: 200,
123
- headers: {
124
- "Content-Type": "text/event-stream; charset=utf-8",
125
- Connection: "keep-alive",
126
- "Cache-Control": "no-cache",
127
- },
128
- });
129
- };
package/src/errors.ts DELETED
@@ -1,47 +0,0 @@
1
- // TODO: `CodegenErrorReason` should be imported from `anima-public-api`
2
- /**
3
- * Errors from Public API
4
- */
5
- export type CodegenErrorReason =
6
- | "Selected node type is not supported"
7
- | "Selected node is a page with multiple children"
8
- | "There is no node with the given id"
9
- | "Invalid Figma token"
10
- | "Anima API connection error"
11
- | "Figma token expired"
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"
23
- | "Connection closed before the 'done' message"
24
- | "Response body is null";
25
-
26
- export class CodegenError extends Error {
27
- status?: number;
28
- detail?: unknown;
29
-
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
- }) {
41
- super();
42
- this.name = name;
43
- this.message = reason;
44
- this.detail = detail;
45
- this.status = status;
46
- }
47
- }
@@ -1,120 +0,0 @@
1
- import type { CodegenErrorReason } from "../errors";
2
-
3
- const figmaTokenIssueErrorMessage = "Figma Token Issue";
4
- export class FigmaTokenIssue extends Error {
5
- fileKey: string;
6
- reason: string;
7
-
8
- constructor({ fileKey, reason }: { fileKey: string; reason: string }) {
9
- super(figmaTokenIssueErrorMessage);
10
-
11
- this.fileKey = fileKey;
12
- this.reason = reason;
13
- }
14
- }
15
-
16
- const rateLimitExceededErrorMessage = "Rate Limit Exceeded";
17
- export class RateLimitExceeded extends Error {
18
- fileKey: string;
19
-
20
- constructor({ fileKey }: { fileKey: string }) {
21
- super(rateLimitExceededErrorMessage);
22
-
23
- this.fileKey = fileKey;
24
- }
25
- }
26
-
27
- // Not Found
28
- const notFoundErrorMessage = "Not Found";
29
- export class NotFound extends Error {
30
- fileKey: string;
31
-
32
- constructor({ fileKey }: { fileKey: string }) {
33
- super(notFoundErrorMessage);
34
-
35
- this.fileKey = fileKey;
36
- }
37
- }
38
- export const isNotFound = (error: Error) => {
39
- return error.message === notFoundErrorMessage;
40
- };
41
-
42
- // Unknown Exception
43
- const unknownFigmaApiExceptionMessage = "Unknown Figma API Exception";
44
- export class UnknownFigmaApiException extends Error {
45
- fileKey: string;
46
-
47
- constructor({ fileKey, cause }: { fileKey: string; cause: unknown }) {
48
- super(unknownFigmaApiExceptionMessage);
49
-
50
- this.name = "UnknownFigmaApiException";
51
- this.fileKey = fileKey;
52
- this.cause = cause;
53
- }
54
- }
55
- export const isUnknownFigmaApiException = (error: Error) => {
56
- return error.message === unknownFigmaApiExceptionMessage;
57
- };
58
-
59
- export const isRateLimitExceeded = (error: Error) => {
60
- return error.message === rateLimitExceededErrorMessage;
61
- };
62
-
63
- export const isFigmaTokenIssue = (error: Error) => {
64
- const figmaTokenCodegenErrors: CodegenErrorReason[] = [
65
- "Invalid Figma token",
66
- "Figma token expired",
67
- ];
68
-
69
- return [figmaTokenIssueErrorMessage, ...figmaTokenCodegenErrors].includes(
70
- error.message
71
- );
72
- };
73
-
74
- export type FigmaApiError = {
75
- cause?: { body?: { status?: number; reason?: string } };
76
- body?: { status?: number; reason?: string };
77
- };
78
-
79
- export const handleFigmaApiError = (error: FigmaApiError, fileKey: string) => {
80
- const err = error?.cause?.body || error.body;
81
-
82
- if (err?.status === 403) {
83
- throw new FigmaTokenIssue({
84
- fileKey,
85
- reason: (error?.cause?.body?.reason || error.body?.reason || "Access denied").toString(),
86
- });
87
- }
88
-
89
- if (err?.status === 429) {
90
- throw new RateLimitExceeded({ fileKey });
91
- }
92
-
93
- if (err?.status === 404) {
94
- throw new NotFound({ fileKey });
95
- }
96
-
97
- throw new UnknownFigmaApiException({ fileKey, cause: error });
98
- };
99
-
100
- export type FigmaApiErrorType =
101
- | "FigmaTokenIssue"
102
- | "RateLimitExceeded"
103
- | "NotFound"
104
- | "UnknownFigmaApiException";
105
-
106
- export const getFigmaApiErrorType = (error: Error): FigmaApiErrorType => {
107
- if (isNotFound(error)) {
108
- return "NotFound";
109
- }
110
-
111
- if (isRateLimitExceeded(error)) {
112
- return "RateLimitExceeded";
113
- }
114
-
115
- if (isFigmaTokenIssue(error)) {
116
- return "FigmaTokenIssue";
117
- }
118
-
119
- return "UnknownFigmaApiException";
120
- };
@@ -1,2 +0,0 @@
1
- export * from "./figmaError";
2
- export * from "./utils";