@effing/annie 0.1.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/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ O'Saasy License
2
+
3
+ Copyright © 2026, Trackuity BV.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ 1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ 2. No licensee or downstream recipient may use the Software (including any modified or derivative versions) to directly compete with the original Licensor by offering it to third parties as a hosted, managed, or Software-as-a-Service (SaaS) product or cloud service where the primary value of the service is the functionality of the Software itself.
10
+
11
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # @effing/annie
2
+
3
+ **Generate TAR archives of PNG or JPEG frames for animated layers.**
4
+
5
+ > Part of the [**Effing**](../../README.md) family — programmatic video creation with TypeScript.
6
+
7
+ Annie is a simple animation format: a TAR archive containing sequentially-named PNG or JPEG frames. Generate frames server-side, stream them to the browser or FFmpeg.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @effing/annie
13
+ ```
14
+
15
+ ## Concepts
16
+
17
+ ### Annie Format
18
+
19
+ An Annie is a TAR archive where each entry is a PNG or JPEG frame:
20
+
21
+ ```
22
+ frame_00000
23
+ frame_00001
24
+ frame_00002
25
+ ...
26
+ ```
27
+
28
+ This format is:
29
+
30
+ - **Streamable** — frames can be written as they're generated
31
+ - **Universal** — TAR is supported everywhere
32
+ - **FFmpeg-compatible** — can be piped directly to FFmpeg as image input
33
+
34
+ ### Generation Patterns
35
+
36
+ Annie supports two generation patterns:
37
+
38
+ **Buffer** — Collect all frames, return complete archive:
39
+
40
+ ```typescript
41
+ const archive = await annieBuffer(frames);
42
+ ```
43
+
44
+ **Stream** — Yield chunks as frames are generated:
45
+
46
+ ```typescript
47
+ const stream = annieStream(frames);
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ ```typescript
53
+ import { annieStream, annieBuffer } from "@effing/annie";
54
+ import { pngFromSatori } from "@effing/satori";
55
+ import { tween, easeOutQuad } from "@effing/tween";
56
+
57
+ // Define a frame generator
58
+ async function* generateFrames() {
59
+ yield* tween(90, async ({ lower: progress }) => {
60
+ const scale = 1 + 0.3 * easeOutQuad(progress);
61
+ return pngFromSatori(
62
+ <div style={{
63
+ width: 1080,
64
+ height: 1920,
65
+ display: "flex",
66
+ alignItems: "center",
67
+ justifyContent: "center",
68
+ fontSize: 72,
69
+ transform: `scale(${scale})`
70
+ }}>
71
+ Hello World!
72
+ </div>,
73
+ { width: 1080, height: 1920, fonts: myFonts }
74
+ );
75
+ });
76
+ }
77
+
78
+ // Stream response
79
+ const stream = annieStream(generateFrames(), { signal: request.signal });
80
+ return new Response(stream, {
81
+ headers: { "Content-Type": "application/x-tar" }
82
+ });
83
+ ```
84
+
85
+ ## API Overview
86
+
87
+ ### Functions
88
+
89
+ #### `annieStream(frames, options?)`
90
+
91
+ Create a `ReadableStream` that produces TAR chunks as frames are generated.
92
+
93
+ ```typescript
94
+ function annieStream(
95
+ frames: AsyncIterable<Buffer>,
96
+ options?: AnnieStreamOptions,
97
+ ): ReadableStream<Buffer>;
98
+ ```
99
+
100
+ **Options:**
101
+
102
+ - `signal` — AbortSignal for cancellation
103
+
104
+ #### `annieBuffer(frames)`
105
+
106
+ Collect all frames and return a complete TAR buffer.
107
+
108
+ ```typescript
109
+ function annieBuffer(frames: AsyncIterable<Buffer>): Promise<Buffer>;
110
+ ```
111
+
112
+ ### Response Helper
113
+
114
+ #### `annieResponse(frames, options?)`
115
+
116
+ Create a complete `Response` object with proper headers.
117
+
118
+ ```typescript
119
+ import { annieResponse } from "@effing/annie";
120
+
121
+ return annieResponse(generateFrames(), {
122
+ signal: request.signal,
123
+ headers: { "Cache-Control": "public, max-age=3600" },
124
+ });
125
+ ```
126
+
127
+ ## Examples
128
+
129
+ ### With Express/Node.js
130
+
131
+ ```typescript
132
+ import { Readable } from "stream";
133
+ import { annieStream } from "@effing/annie";
134
+
135
+ app.get("/animation.tar", async (req, res) => {
136
+ const stream = annieStream(generateFrames());
137
+ res.setHeader("Content-Type", "application/x-tar");
138
+ Readable.fromWeb(stream).pipe(res);
139
+ });
140
+ ```
141
+
142
+ ### With React Router / Remix
143
+
144
+ ```typescript
145
+ import { annieResponse } from "@effing/annie";
146
+
147
+ export async function loader({ request }: LoaderFunctionArgs) {
148
+ return annieResponse(generateFrames(), { signal: request.signal });
149
+ }
150
+ ```
151
+
152
+ ### Saving to File
153
+
154
+ ```typescript
155
+ import { writeFile } from "fs/promises";
156
+ import { annieBuffer } from "@effing/annie";
157
+
158
+ const buffer = await annieBuffer(generateFrames());
159
+ await writeFile("animation.tar", buffer);
160
+ ```
161
+
162
+ ### FFmpeg Integration
163
+
164
+ Note that Annie TAR archives can be piped directly to FFmpeg. Extract the frames with `tar -xO` and pipe to FFmpeg's image2pipe input:
165
+
166
+ ```bash
167
+ # Create animated PNG (loops forever)
168
+ tar -xO < animation.tar | ffmpeg -f image2pipe -framerate 30 -i - -plays 0 -c:v apng -f apng output.png
169
+
170
+ # Create MP4 video
171
+ tar -xO < animation.tar | ffmpeg -f image2pipe -framerate 30 -i - -c:v libx264 -pix_fmt yuv420p output.mp4
172
+
173
+ # Create GIF
174
+ tar -xO < animation.tar | ffmpeg -f image2pipe -framerate 30 -i - output.gif
175
+ ```
176
+
177
+ ## Related Packages
178
+
179
+ - [`@effing/tween`](../tween) — Step iteration and easing functions for frame generation
180
+ - [`@effing/satori`](../satori) — Render JSX to PNG for each frame
181
+ - [`@effing/annie-player`](../annie-player) — Play Annies in the browser
182
+ - [`@effing/effie`](../effie) — Use Annies as layers in video compositions
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Annie generation utilities (TAR archives of PNG frames)
3
+ */
4
+ /**
5
+ * Options for annie stream generation
6
+ */
7
+ type AnnieStreamOptions = {
8
+ /** Abort signal for cancellation */
9
+ signal?: AbortSignal;
10
+ };
11
+ /**
12
+ * Collect all frames into a single annie Buffer (TAR archive)
13
+ *
14
+ * @param frames Async iterator yielding PNG or JPEG frame buffers
15
+ * @returns Complete annie as a Buffer
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const frames = renderAnnieFrames(annieId, props, { width, height });
20
+ * const annie = await annieBuffer(frames);
21
+ * await fs.writeFile("animation.tar", annie);
22
+ * ```
23
+ */
24
+ declare function annieBuffer(frames: AsyncIterable<Buffer>): Promise<Buffer>;
25
+ /**
26
+ * Create a ReadableStream that produces an annie (TAR archive of frames)
27
+ *
28
+ * Use this when you need the stream but want to customize the Response yourself.
29
+ *
30
+ * @param frames Async iterator yielding PNG or JPEG frame buffers
31
+ * @param options Configuration options
32
+ * @returns ReadableStream of annie data
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const frames = renderAnnieFrames(annieId, props, { width, height });
37
+ * const stream = annieStream(frames, { signal: request.signal });
38
+ * return new Response(stream, {
39
+ * headers: { "Content-Type": "application/x-tar" }
40
+ * });
41
+ * ```
42
+ */
43
+ declare function annieStream(frames: AsyncIterable<Buffer>, options?: AnnieStreamOptions): ReadableStream<Buffer>;
44
+
45
+ /**
46
+ * Options for annie Response generation
47
+ */
48
+ type AnnieResponseOptions = AnnieStreamOptions & {
49
+ /** Additional headers to include in the response */
50
+ headers?: HeadersInit;
51
+ /** Cache-Control header value (default: "public, max-age=3600") */
52
+ cacheControl?: string;
53
+ /** Filename for Content-Disposition header (without .tar extension) */
54
+ filename?: string;
55
+ };
56
+ /**
57
+ * Create an HTTP Response that streams an annie
58
+ *
59
+ * This is the most convenient way to serve an annie animation from a web server.
60
+ * It handles all the streaming, headers, and cleanup automatically.
61
+ *
62
+ * @param frames Async iterator yielding PNG or JPEG frame buffers
63
+ * @param options Configuration options
64
+ * @returns Response streaming the annie
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * // In a route handler:
69
+ * export async function loader({ request, params }: LoaderFunctionArgs) {
70
+ * const frames = renderAnnieFrames(annieId, props, { width, height });
71
+ * return annieResponse(frames, {
72
+ * signal: request.signal,
73
+ * filename: "my-animation",
74
+ * });
75
+ * }
76
+ * ```
77
+ */
78
+ declare function annieResponse(frames: AsyncIterable<Buffer>, options?: AnnieResponseOptions): Response;
79
+
80
+ export { type AnnieResponseOptions, type AnnieStreamOptions, annieBuffer, annieResponse, annieStream };
package/dist/index.js ADDED
@@ -0,0 +1,110 @@
1
+ // src/generate.ts
2
+ var TAR_BLOCK_SIZE = 512;
3
+ function createTarHeader(name, size) {
4
+ const header = Buffer.alloc(TAR_BLOCK_SIZE);
5
+ const nameBytes = Buffer.from(name.slice(0, 100), "utf8");
6
+ nameBytes.copy(header, 0);
7
+ header.write("0000664 ", 100, "utf8");
8
+ header.write("0001750 ", 108, "utf8");
9
+ header.write("0001750 ", 116, "utf8");
10
+ const sizeOctal = size.toString(8).padStart(11, "0") + " ";
11
+ header.write(sizeOctal, 124, "utf8");
12
+ const mtime = Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0") + " ";
13
+ header.write(mtime, 136, "utf8");
14
+ header.write(" ", 148, "utf8");
15
+ header.write("0", 156, "utf8");
16
+ header.write("ustar ", 257, "utf8");
17
+ header.write(" \0", 263, "utf8");
18
+ let checksum = 0;
19
+ for (let i = 0; i < TAR_BLOCK_SIZE; i++) {
20
+ checksum += header[i];
21
+ }
22
+ const checksumOctal = checksum.toString(8).padStart(6, "0") + "\0 ";
23
+ header.write(checksumOctal, 148, "utf8");
24
+ return header;
25
+ }
26
+ function padToBlockSize(data) {
27
+ const remainder = data.length % TAR_BLOCK_SIZE;
28
+ if (remainder === 0) return data;
29
+ const padding = Buffer.alloc(TAR_BLOCK_SIZE - remainder);
30
+ return Buffer.concat([data, padding]);
31
+ }
32
+ async function* tarChunks(frames, options = {}) {
33
+ const { framePrefix = "frame_", frameDigits = 5, signal } = options;
34
+ let i = 0;
35
+ for await (const frame of frames) {
36
+ if (signal?.aborted) {
37
+ return;
38
+ }
39
+ const name = `${framePrefix}${i.toString().padStart(frameDigits, "0")}`;
40
+ yield createTarHeader(name, frame.length);
41
+ yield padToBlockSize(frame);
42
+ i++;
43
+ }
44
+ if (!signal?.aborted) {
45
+ yield Buffer.alloc(TAR_BLOCK_SIZE * 2);
46
+ }
47
+ }
48
+ async function annieBuffer(frames) {
49
+ const chunks = [];
50
+ for await (const chunk of tarChunks(frames)) {
51
+ chunks.push(chunk);
52
+ }
53
+ return Buffer.concat(chunks);
54
+ }
55
+ function annieStream(frames, options = {}) {
56
+ const abortController = new AbortController();
57
+ const combinedSignal = options.signal ? AbortSignal.any([options.signal, abortController.signal]) : abortController.signal;
58
+ let iterator = null;
59
+ return new ReadableStream({
60
+ async start() {
61
+ iterator = tarChunks(frames, { signal: combinedSignal });
62
+ },
63
+ async pull(controller) {
64
+ if (!iterator) return;
65
+ try {
66
+ const { value, done } = await iterator.next();
67
+ if (done) {
68
+ controller.close();
69
+ } else {
70
+ controller.enqueue(value);
71
+ }
72
+ } catch (err) {
73
+ if (!combinedSignal.aborted) {
74
+ controller.error(err);
75
+ }
76
+ }
77
+ },
78
+ cancel() {
79
+ abortController.abort();
80
+ iterator?.return(void 0).catch(() => {
81
+ });
82
+ }
83
+ });
84
+ }
85
+
86
+ // src/response.ts
87
+ function annieResponse(frames, options = {}) {
88
+ const {
89
+ headers: extraHeaders,
90
+ cacheControl = "public, max-age=3600",
91
+ filename,
92
+ ...streamOptions
93
+ } = options;
94
+ const stream = annieStream(frames, streamOptions);
95
+ const headers = new Headers(extraHeaders);
96
+ headers.set("Content-Type", "application/x-tar");
97
+ if (cacheControl) {
98
+ headers.set("Cache-Control", cacheControl);
99
+ }
100
+ if (filename) {
101
+ headers.set("Content-Disposition", `inline; filename="${filename}.tar"`);
102
+ }
103
+ return new Response(stream, { status: 200, headers });
104
+ }
105
+ export {
106
+ annieBuffer,
107
+ annieResponse,
108
+ annieStream
109
+ };
110
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/generate.ts","../src/response.ts"],"sourcesContent":["/**\n * Annie generation utilities (TAR archives of PNG frames)\n */\n\nconst TAR_BLOCK_SIZE = 512;\n\n/**\n * Create a TAR header for a file entry\n */\nfunction createTarHeader(name: string, size: number): Buffer {\n const header = Buffer.alloc(TAR_BLOCK_SIZE);\n\n // Filename (100 bytes)\n const nameBytes = Buffer.from(name.slice(0, 100), \"utf8\");\n nameBytes.copy(header, 0);\n\n // Mode (8 bytes octal) - 0000664 = regular file, rw-rw-r--\n header.write(\"0000664 \", 100, \"utf8\");\n\n // UID (8 bytes octal)\n header.write(\"0001750 \", 108, \"utf8\");\n\n // GID (8 bytes octal)\n header.write(\"0001750 \", 116, \"utf8\");\n\n // Size (12 bytes octal)\n const sizeOctal = size.toString(8).padStart(11, \"0\") + \" \";\n header.write(sizeOctal, 124, \"utf8\");\n\n // Modification time (12 bytes octal)\n const mtime =\n Math.floor(Date.now() / 1000)\n .toString(8)\n .padStart(11, \"0\") + \" \";\n header.write(mtime, 136, \"utf8\");\n\n // Checksum placeholder (8 bytes) - fill with spaces initially\n header.write(\" \", 148, \"utf8\");\n\n // Type flag (1 byte) - '0' for regular file\n header.write(\"0\", 156, \"utf8\");\n\n // Magic (6 bytes)\n header.write(\"ustar \", 257, \"utf8\");\n\n // Version (2 bytes)\n header.write(\" \\0\", 263, \"utf8\");\n\n // Calculate checksum (sum of all bytes, treating checksum field as spaces)\n let checksum = 0;\n for (let i = 0; i < TAR_BLOCK_SIZE; i++) {\n checksum += header[i];\n }\n\n // Write checksum (8 bytes octal)\n const checksumOctal = checksum.toString(8).padStart(6, \"0\") + \"\\0 \";\n header.write(checksumOctal, 148, \"utf8\");\n\n return header;\n}\n\n/**\n * Pad data to TAR block size (512 bytes)\n */\nfunction padToBlockSize(data: Buffer): Buffer {\n const remainder = data.length % TAR_BLOCK_SIZE;\n if (remainder === 0) return data;\n const padding = Buffer.alloc(TAR_BLOCK_SIZE - remainder);\n return Buffer.concat([data, padding]);\n}\n\n/**\n * Options for annie stream generation\n */\nexport type AnnieStreamOptions = {\n /** Abort signal for cancellation */\n signal?: AbortSignal;\n};\n\ntype ChunkOptions = {\n framePrefix?: string;\n frameDigits?: number;\n signal?: AbortSignal;\n};\n\n/**\n * Generate TAR archive chunks from an async iterator of frame buffers\n */\nasync function* tarChunks(\n frames: AsyncIterable<Buffer>,\n options: ChunkOptions = {},\n): AsyncGenerator<Buffer> {\n const { framePrefix = \"frame_\", frameDigits = 5, signal } = options;\n let i = 0;\n for await (const frame of frames) {\n if (signal?.aborted) {\n return;\n }\n\n const name = `${framePrefix}${i.toString().padStart(frameDigits, \"0\")}`;\n\n // Yield TAR header\n yield createTarHeader(name, frame.length);\n\n // Yield padded frame data\n yield padToBlockSize(frame);\n\n i++;\n }\n\n // Write two empty blocks (1024 bytes) to indicate end of TAR archive\n if (!signal?.aborted) {\n yield Buffer.alloc(TAR_BLOCK_SIZE * 2);\n }\n}\n\n/**\n * Collect all frames into a single annie Buffer (TAR archive)\n *\n * @param frames Async iterator yielding PNG or JPEG frame buffers\n * @returns Complete annie as a Buffer\n *\n * @example\n * ```ts\n * const frames = renderAnnieFrames(annieId, props, { width, height });\n * const annie = await annieBuffer(frames);\n * await fs.writeFile(\"animation.tar\", annie);\n * ```\n */\nexport async function annieBuffer(\n frames: AsyncIterable<Buffer>,\n): Promise<Buffer> {\n const chunks: Buffer[] = [];\n for await (const chunk of tarChunks(frames)) {\n chunks.push(chunk);\n }\n return Buffer.concat(chunks);\n}\n\n/**\n * Create a ReadableStream that produces an annie (TAR archive of frames)\n *\n * Use this when you need the stream but want to customize the Response yourself.\n *\n * @param frames Async iterator yielding PNG or JPEG frame buffers\n * @param options Configuration options\n * @returns ReadableStream of annie data\n *\n * @example\n * ```ts\n * const frames = renderAnnieFrames(annieId, props, { width, height });\n * const stream = annieStream(frames, { signal: request.signal });\n * return new Response(stream, {\n * headers: { \"Content-Type\": \"application/x-tar\" }\n * });\n * ```\n */\nexport function annieStream(\n frames: AsyncIterable<Buffer>,\n options: AnnieStreamOptions = {},\n): ReadableStream<Buffer> {\n const abortController = new AbortController();\n const combinedSignal = options.signal\n ? AbortSignal.any([options.signal, abortController.signal])\n : abortController.signal;\n\n let iterator: AsyncGenerator<Buffer> | null = null;\n\n return new ReadableStream({\n async start() {\n iterator = tarChunks(frames, { signal: combinedSignal });\n },\n async pull(controller) {\n if (!iterator) return;\n\n try {\n const { value, done } = await iterator.next();\n if (done) {\n controller.close();\n } else {\n controller.enqueue(value);\n }\n } catch (err) {\n if (!combinedSignal.aborted) {\n controller.error(err);\n }\n }\n },\n cancel() {\n abortController.abort();\n iterator?.return(undefined).catch(() => {});\n },\n });\n}\n","import { annieStream } from \"./generate\";\nimport type { AnnieStreamOptions } from \"./generate\";\n\n/**\n * Options for annie Response generation\n */\nexport type AnnieResponseOptions = AnnieStreamOptions & {\n /** Additional headers to include in the response */\n headers?: HeadersInit;\n /** Cache-Control header value (default: \"public, max-age=3600\") */\n cacheControl?: string;\n /** Filename for Content-Disposition header (without .tar extension) */\n filename?: string;\n};\n\n/**\n * Create an HTTP Response that streams an annie\n *\n * This is the most convenient way to serve an annie animation from a web server.\n * It handles all the streaming, headers, and cleanup automatically.\n *\n * @param frames Async iterator yielding PNG or JPEG frame buffers\n * @param options Configuration options\n * @returns Response streaming the annie\n *\n * @example\n * ```ts\n * // In a route handler:\n * export async function loader({ request, params }: LoaderFunctionArgs) {\n * const frames = renderAnnieFrames(annieId, props, { width, height });\n * return annieResponse(frames, {\n * signal: request.signal,\n * filename: \"my-animation\",\n * });\n * }\n * ```\n */\nexport function annieResponse(\n frames: AsyncIterable<Buffer>,\n options: AnnieResponseOptions = {},\n): Response {\n const {\n headers: extraHeaders,\n cacheControl = \"public, max-age=3600\",\n filename,\n ...streamOptions\n } = options;\n\n const stream = annieStream(frames, streamOptions);\n\n const headers = new Headers(extraHeaders);\n headers.set(\"Content-Type\", \"application/x-tar\");\n if (cacheControl) {\n headers.set(\"Cache-Control\", cacheControl);\n }\n if (filename) {\n headers.set(\"Content-Disposition\", `inline; filename=\"${filename}.tar\"`);\n }\n\n return new Response(stream, { status: 200, headers });\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@effing/annie",
3
+ "version": "0.1.0",
4
+ "description": "Annie animation frame generator utilities",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "dependencies": {
16
+ "@effing/serde": "0.1.0"
17
+ },
18
+ "devDependencies": {
19
+ "tsup": "^8.0.0",
20
+ "typescript": "^5.9.3",
21
+ "vitest": "^3.2.4"
22
+ },
23
+ "peerDependencies": {
24
+ "zod": "^3.0.0"
25
+ },
26
+ "peerDependenciesMeta": {
27
+ "zod": {
28
+ "optional": true
29
+ }
30
+ },
31
+ "keywords": [
32
+ "animation",
33
+ "frames",
34
+ "video",
35
+ "satori"
36
+ ],
37
+ "license": "O'Saasy",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "typecheck": "tsc --noEmit",
44
+ "test": "vitest run"
45
+ }
46
+ }