@animaapp/anima-sdk-react 0.2.8 → 0.3.1
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/.storybook/main.ts +25 -0
- package/.storybook/preview.ts +13 -0
- package/.turbo/turbo-build.log +5 -5
- package/.turbo/turbo-dev.log +48 -0
- package/.turbo/turbo-test.log +161 -0
- package/dist/index.cjs +6 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4377 -4366
- package/dist/index.js.map +1 -1
- package/package.json +19 -6
- package/src/stories/useAnimaCodegen.stories.tsx +222 -0
- package/src/useAnimaCodegen.ts +44 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animaapp/anima-sdk-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Anima's JavaScript utilities library",
|
|
6
6
|
"author": "Anima App, Inc.",
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "vite build",
|
|
23
|
-
"
|
|
24
|
-
"prepack": "yarn build"
|
|
23
|
+
"test": "test-storybook",
|
|
24
|
+
"prepack": "yarn build",
|
|
25
|
+
"dev": "storybook dev -p 6006"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"eventsource": "^3.0.5",
|
|
@@ -35,9 +36,21 @@
|
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@animaapp/anima-sdk": "workspace:*",
|
|
39
|
+
"@chromatic-com/storybook": "^3",
|
|
40
|
+
"@storybook/addon-essentials": "^8.6.6",
|
|
41
|
+
"@storybook/addon-interactions": "^8.6.6",
|
|
42
|
+
"@storybook/addon-onboarding": "^8.6.6",
|
|
43
|
+
"@storybook/blocks": "^8.6.6",
|
|
44
|
+
"@storybook/react": "^8.6.6",
|
|
45
|
+
"@storybook/react-vite": "^8.6.6",
|
|
46
|
+
"@storybook/test": "^8.6.6",
|
|
47
|
+
"@storybook/test-runner": "^0.22.0",
|
|
48
|
+
"@types/prop-types": "^15",
|
|
49
|
+
"@types/react": "^19.0.10",
|
|
50
|
+
"prop-types": "^15.8.1",
|
|
51
|
+
"storybook": "^8.6.6",
|
|
38
52
|
"vite": "^6.0.11",
|
|
39
53
|
"vite-plugin-dts": "^4.5.0",
|
|
40
|
-
"vite-tsconfig-paths": "^5.1.4"
|
|
41
|
-
"vitest": "^3.0.5"
|
|
54
|
+
"vite-tsconfig-paths": "^5.1.4"
|
|
42
55
|
}
|
|
43
|
-
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { expect, userEvent, waitFor, within } from "@storybook/test";
|
|
3
|
+
import { AnimaSDKResult, CodegenError } from "@animaapp/anima-sdk";
|
|
4
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
5
|
+
import { useAnimaCodegen, UseAnimaParams } from "..";
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
payload: UseAnimaParams;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const DummyComponent = ({ payload }: Props) => {
|
|
12
|
+
const [result, setResult] = useState<AnimaSDKResult | null>(null);
|
|
13
|
+
const [error, setError] = useState<CodegenError | null>(null);
|
|
14
|
+
|
|
15
|
+
const animaCodegen = useAnimaCodegen({
|
|
16
|
+
url: "http://localhost:3000",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const handleClick = async () => {
|
|
20
|
+
const { result, error } = await animaCodegen.getCode(payload);
|
|
21
|
+
|
|
22
|
+
setResult(result);
|
|
23
|
+
setError(error);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div>
|
|
28
|
+
<button data-testid="run-button" onClick={handleClick}>
|
|
29
|
+
Run request
|
|
30
|
+
</button>
|
|
31
|
+
|
|
32
|
+
{result && (
|
|
33
|
+
<div>
|
|
34
|
+
<h3>Success result</h3>
|
|
35
|
+
|
|
36
|
+
<pre data-testid="request-result" data-status="success">
|
|
37
|
+
{JSON.stringify(result, null, 2)}
|
|
38
|
+
</pre>
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
{error && (
|
|
43
|
+
<div>
|
|
44
|
+
<h3>Error result</h3>
|
|
45
|
+
|
|
46
|
+
<pre data-testid="request-result" data-status="error">
|
|
47
|
+
{JSON.stringify(
|
|
48
|
+
{
|
|
49
|
+
name: error.name,
|
|
50
|
+
message: error.message,
|
|
51
|
+
status: error.status,
|
|
52
|
+
detail: error.detail,
|
|
53
|
+
},
|
|
54
|
+
null,
|
|
55
|
+
2
|
|
56
|
+
)}
|
|
57
|
+
</pre>
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const meta = {
|
|
65
|
+
title: "Hooks/useAnimaCodegen",
|
|
66
|
+
component: DummyComponent,
|
|
67
|
+
} satisfies Meta<typeof DummyComponent>;
|
|
68
|
+
export default meta;
|
|
69
|
+
|
|
70
|
+
type Story = StoryObj<typeof meta>;
|
|
71
|
+
|
|
72
|
+
const run = async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
73
|
+
const result: { result: AnimaSDKResult | null; error: CodegenError | null } =
|
|
74
|
+
{ result: null, error: null };
|
|
75
|
+
|
|
76
|
+
const canvas = within(canvasElement);
|
|
77
|
+
|
|
78
|
+
const runButton = canvas.getByTestId("run-button");
|
|
79
|
+
await userEvent.click(runButton);
|
|
80
|
+
|
|
81
|
+
const requestResult = await waitFor(
|
|
82
|
+
() => canvas.findByTestId("request-result"),
|
|
83
|
+
{ timeout: 10_000 }
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (!requestResult.textContent) {
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const resultStatus = requestResult.dataset.status;
|
|
91
|
+
|
|
92
|
+
if (resultStatus === "success") {
|
|
93
|
+
result.result = JSON.parse(requestResult.textContent);
|
|
94
|
+
} else {
|
|
95
|
+
result.error = JSON.parse(requestResult.textContent);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const Success: Story = {
|
|
102
|
+
play: async (context) => {
|
|
103
|
+
const { result } = await run(context);
|
|
104
|
+
|
|
105
|
+
expect(result).toMatchObject({
|
|
106
|
+
sessionId: expect.anything(),
|
|
107
|
+
figmaFileName: "Anima SDK - Test File",
|
|
108
|
+
figmaSelectedFrameName: "MyFrame",
|
|
109
|
+
files: expect.anything(),
|
|
110
|
+
tokenUsage: expect.anything(),
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
args: {
|
|
115
|
+
payload: {
|
|
116
|
+
fileKey: "5d0u9PmD4GtB5fdX57pTtK",
|
|
117
|
+
nodesId: ["1:2"],
|
|
118
|
+
settings: {
|
|
119
|
+
framework: "react",
|
|
120
|
+
styling: "plain_css",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const InvalidBody: Story = {
|
|
127
|
+
play: async (context) => {
|
|
128
|
+
const { error } = await run(context);
|
|
129
|
+
|
|
130
|
+
expect(error).toMatchObject({
|
|
131
|
+
name: "HTTP error from Anima API",
|
|
132
|
+
message: "Invalid body payload",
|
|
133
|
+
status: 400,
|
|
134
|
+
detail: [
|
|
135
|
+
{
|
|
136
|
+
code: "invalid_type",
|
|
137
|
+
expected: "string",
|
|
138
|
+
received: "null",
|
|
139
|
+
path: ["fileKey"],
|
|
140
|
+
message: "Expected string, received null",
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
args: {
|
|
147
|
+
payload: {
|
|
148
|
+
// @ts-expect-error: Testing invalid body payload
|
|
149
|
+
fileKey: null,
|
|
150
|
+
nodesId: ["1:2"],
|
|
151
|
+
settings: {
|
|
152
|
+
framework: "react",
|
|
153
|
+
styling: "plain_css",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const NotFoundFile: Story = {
|
|
160
|
+
play: async (context) => {
|
|
161
|
+
const { error } = await run(context);
|
|
162
|
+
|
|
163
|
+
expect(error).toMatchObject({
|
|
164
|
+
name: "Task Crashed",
|
|
165
|
+
message: "Figma file not found",
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
args: {
|
|
170
|
+
payload: {
|
|
171
|
+
fileKey: "invalid-file-key",
|
|
172
|
+
nodesId: ["1:2"],
|
|
173
|
+
settings: {
|
|
174
|
+
framework: "react",
|
|
175
|
+
styling: "plain_css",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const NotSupportedNode: Story = {
|
|
182
|
+
play: async (context) => {
|
|
183
|
+
const { error } = await run(context);
|
|
184
|
+
|
|
185
|
+
expect(error).toMatchObject({
|
|
186
|
+
name: "Task Crashed",
|
|
187
|
+
message: "Selected node type is not supported",
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
args: {
|
|
192
|
+
payload: {
|
|
193
|
+
fileKey: "5d0u9PmD4GtB5fdX57pTtK",
|
|
194
|
+
nodesId: ["14:5"],
|
|
195
|
+
settings: {
|
|
196
|
+
framework: "react",
|
|
197
|
+
styling: "plain_css",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const InvisibleGroupNode: Story = {
|
|
204
|
+
play: async (context) => {
|
|
205
|
+
const { error } = await run(context);
|
|
206
|
+
|
|
207
|
+
expect(error).toMatchObject({
|
|
208
|
+
name: "Task Crashed",
|
|
209
|
+
message: "Invisible group nodes are unsupported",
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
args: {
|
|
213
|
+
payload: {
|
|
214
|
+
fileKey: "5d0u9PmD4GtB5fdX57pTtK",
|
|
215
|
+
nodesId: ["134:14"],
|
|
216
|
+
settings: {
|
|
217
|
+
framework: "react",
|
|
218
|
+
styling: "plain_css",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
};
|
package/src/useAnimaCodegen.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
GetCodeParams,
|
|
5
5
|
StreamCodgenMessage,
|
|
6
6
|
} from "@animaapp/anima-sdk";
|
|
7
|
+
import { CodegenError } from "@animaapp/anima-sdk";
|
|
7
8
|
import { EventSource } from "eventsource";
|
|
8
9
|
import { useImmer } from "use-immer";
|
|
9
10
|
|
|
@@ -21,7 +22,7 @@ type TaskStatus = "pending" | "running" | "finished";
|
|
|
21
22
|
|
|
22
23
|
type CodegenStatus = {
|
|
23
24
|
status: Status;
|
|
24
|
-
error:
|
|
25
|
+
error: CodegenError | null;
|
|
25
26
|
result: AnimaSDKResult | null;
|
|
26
27
|
tasks: {
|
|
27
28
|
fetchDesign: { status: TaskStatus };
|
|
@@ -98,18 +99,26 @@ export const useAnimaCodegen = ({
|
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
// TODO: We have two workarounds here because of limitations on the `eventsource` package:
|
|
103
|
+
// 1. We need to use the `fetch` function from the `EventSource` constructor to send the request with the correct method and body (https://github.com/EventSource/eventsource/issues/316#issuecomment-2525315835).
|
|
104
|
+
// 2. We need to store the last fetch response to handle errors to read its body response, since it isn't expoted by the package (https://github.com/EventSource/eventsource/blob/8aa7057bccd7fb819372a3b2c1292e7b53424d52/src/EventSource.ts#L348-L376)
|
|
105
|
+
// We might need to use other library, or do it from our self, to improve the code quality.
|
|
106
|
+
let lastFetchResponse: ReturnType<typeof fetch>;
|
|
101
107
|
const es = new EventSource(url, {
|
|
102
|
-
fetch: (url, init) =>
|
|
103
|
-
fetch(url, {
|
|
108
|
+
fetch: (url, init) => {
|
|
109
|
+
lastFetchResponse = fetch(url, {
|
|
104
110
|
...init,
|
|
105
111
|
method,
|
|
106
112
|
body: JSON.stringify(params),
|
|
107
|
-
})
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return lastFetchResponse;
|
|
116
|
+
},
|
|
108
117
|
});
|
|
109
118
|
|
|
110
119
|
const promise = new Promise<{
|
|
111
120
|
result: AnimaSDKResult | null;
|
|
112
|
-
error:
|
|
121
|
+
error: CodegenError | null;
|
|
113
122
|
}>((resolve) => {
|
|
114
123
|
const result: Partial<AnimaSDKResult> = {};
|
|
115
124
|
|
|
@@ -145,12 +154,16 @@ export const useAnimaCodegen = ({
|
|
|
145
154
|
});
|
|
146
155
|
|
|
147
156
|
es.addEventListener("aborted", () => {
|
|
157
|
+
const error = new CodegenError({ name: "Aborted", reason: "Unknown" });
|
|
158
|
+
|
|
148
159
|
updateStatus((draft) => {
|
|
149
160
|
draft.status = "aborted";
|
|
161
|
+
(draft.result = null), (draft.error = error);
|
|
150
162
|
});
|
|
163
|
+
|
|
151
164
|
resolve({
|
|
152
165
|
result: null,
|
|
153
|
-
error
|
|
166
|
+
error,
|
|
154
167
|
});
|
|
155
168
|
});
|
|
156
169
|
|
|
@@ -189,35 +202,34 @@ export const useAnimaCodegen = ({
|
|
|
189
202
|
});
|
|
190
203
|
|
|
191
204
|
// TODO: For some reason, we receive errors even after the `done` event is triggered.
|
|
192
|
-
es.addEventListener("error", (error: ErrorEvent | MessageEvent) => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
205
|
+
es.addEventListener("error", async (error: ErrorEvent | MessageEvent) => {
|
|
206
|
+
let errorPayload: StreamMessageByType<"error"> | undefined;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
if (error instanceof MessageEvent) {
|
|
210
|
+
errorPayload = JSON.parse(error.data);
|
|
211
|
+
} else {
|
|
212
|
+
const response = await lastFetchResponse;
|
|
213
|
+
errorPayload = await response.json();
|
|
214
|
+
}
|
|
215
|
+
} catch {}
|
|
202
216
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
console.error("EventSource error:", error);
|
|
217
|
+
const codegenError = new CodegenError({
|
|
218
|
+
name: errorPayload?.payload.name ?? "Unknown error",
|
|
219
|
+
reason: errorPayload?.payload.message ?? "Unknown",
|
|
220
|
+
status: errorPayload?.payload.status,
|
|
221
|
+
detail: errorPayload?.payload.detail,
|
|
222
|
+
});
|
|
210
223
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
224
|
+
updateStatus((draft) => {
|
|
225
|
+
draft.status = "error";
|
|
226
|
+
draft.error = codegenError;
|
|
227
|
+
});
|
|
215
228
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
229
|
+
resolve({
|
|
230
|
+
result: null,
|
|
231
|
+
error: codegenError,
|
|
232
|
+
});
|
|
221
233
|
});
|
|
222
234
|
|
|
223
235
|
es.addEventListener("done", (event) => {
|
|
@@ -278,13 +290,6 @@ export const useAnimaCodegen = ({
|
|
|
278
290
|
return { result: null, error };
|
|
279
291
|
}
|
|
280
292
|
|
|
281
|
-
if (Object.keys(result?.files ?? {}).length === 0) {
|
|
282
|
-
return {
|
|
283
|
-
result: null,
|
|
284
|
-
error: new Error("No files received"),
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
293
|
return { result, error };
|
|
289
294
|
} finally {
|
|
290
295
|
es.close();
|