@mihirsarya/manim-scroll-next 0.1.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.
@@ -0,0 +1,63 @@
1
+ import type { ExtractedAnimation } from "./extractor";
2
+ export interface CacheEntry {
3
+ /** The hash of the animation props */
4
+ hash: string;
5
+ /** Scene name */
6
+ scene: string;
7
+ /** Original props */
8
+ props: Record<string, unknown>;
9
+ /** Path to the manifest.json file */
10
+ manifestPath: string;
11
+ /** URL path for runtime use */
12
+ manifestUrl: string;
13
+ }
14
+ export interface CacheManifest {
15
+ /** Version of the cache format */
16
+ version: number;
17
+ /** Map of animation hash to manifest URL */
18
+ animations: Record<string, string>;
19
+ }
20
+ /**
21
+ * Compute a deterministic hash for animation props.
22
+ * This hash is used as the cache key and for runtime lookup.
23
+ *
24
+ * Note: This uses a simple hash function that works identically
25
+ * in both Node.js and browser environments.
26
+ */
27
+ export declare function computePropsHash(scene: string, props: Record<string, unknown>): string;
28
+ /**
29
+ * Check if an animation is already cached.
30
+ */
31
+ export declare function isCached(hash: string, publicDir: string): boolean;
32
+ /**
33
+ * Get the cache entry for an animation if it exists.
34
+ */
35
+ export declare function getCacheEntry(hash: string, publicDir: string): CacheEntry | null;
36
+ /**
37
+ * Determine which animations need to be rendered (not in cache).
38
+ */
39
+ export declare function getAnimationsToRender(animations: ExtractedAnimation[], publicDir: string): {
40
+ cached: CacheEntry[];
41
+ toRender: ExtractedAnimation[];
42
+ };
43
+ /**
44
+ * Ensure the manim-assets directory exists.
45
+ */
46
+ export declare function ensureAssetDir(publicDir: string): string;
47
+ /**
48
+ * Get the output directory for a specific animation hash.
49
+ */
50
+ export declare function getOutputDir(hash: string, publicDir: string): string;
51
+ /**
52
+ * Write the runtime cache manifest that maps hashes to manifest URLs.
53
+ * This is used by the React component to look up animations at runtime.
54
+ */
55
+ export declare function writeCacheManifest(animations: ExtractedAnimation[], publicDir: string): void;
56
+ /**
57
+ * Read the existing cache manifest if it exists.
58
+ */
59
+ export declare function readCacheManifest(publicDir: string): CacheManifest | null;
60
+ /**
61
+ * Clean up orphaned cache entries that are no longer referenced.
62
+ */
63
+ export declare function cleanOrphanedCache(animations: ExtractedAnimation[], publicDir: string): void;
package/dist/cache.js ADDED
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.computePropsHash = computePropsHash;
37
+ exports.isCached = isCached;
38
+ exports.getCacheEntry = getCacheEntry;
39
+ exports.getAnimationsToRender = getAnimationsToRender;
40
+ exports.ensureAssetDir = ensureAssetDir;
41
+ exports.getOutputDir = getOutputDir;
42
+ exports.writeCacheManifest = writeCacheManifest;
43
+ exports.readCacheManifest = readCacheManifest;
44
+ exports.cleanOrphanedCache = cleanOrphanedCache;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const CACHE_VERSION = 1;
48
+ /**
49
+ * Compute a deterministic hash for animation props.
50
+ * This hash is used as the cache key and for runtime lookup.
51
+ *
52
+ * Note: This uses a simple hash function that works identically
53
+ * in both Node.js and browser environments.
54
+ */
55
+ function computePropsHash(scene, props) {
56
+ // Create a deterministic string representation
57
+ // Sort keys to ensure consistent ordering
58
+ const sortedProps = sortObjectKeys(props);
59
+ const data = JSON.stringify({ scene, props: sortedProps });
60
+ // Use djb2 hash algorithm - fast and produces good distribution
61
+ let hash = 5381;
62
+ for (let i = 0; i < data.length; i++) {
63
+ hash = ((hash << 5) + hash + data.charCodeAt(i)) | 0;
64
+ }
65
+ // Convert to positive hex string, padded to 8 chars
66
+ const hexHash = (hash >>> 0).toString(16).padStart(8, "0");
67
+ return hexHash;
68
+ }
69
+ /**
70
+ * Recursively sort object keys for deterministic JSON stringification.
71
+ */
72
+ function sortObjectKeys(obj) {
73
+ if (obj === null || typeof obj !== "object") {
74
+ return obj;
75
+ }
76
+ if (Array.isArray(obj)) {
77
+ return obj.map(sortObjectKeys);
78
+ }
79
+ const sorted = {};
80
+ const keys = Object.keys(obj).sort();
81
+ for (const key of keys) {
82
+ sorted[key] = sortObjectKeys(obj[key]);
83
+ }
84
+ return sorted;
85
+ }
86
+ /**
87
+ * Check if an animation is already cached.
88
+ */
89
+ function isCached(hash, publicDir) {
90
+ const manifestPath = path.join(publicDir, "manim-assets", hash, "manifest.json");
91
+ return fs.existsSync(manifestPath);
92
+ }
93
+ /**
94
+ * Get the cache entry for an animation if it exists.
95
+ */
96
+ function getCacheEntry(hash, publicDir) {
97
+ const assetDir = path.join(publicDir, "manim-assets", hash);
98
+ const manifestPath = path.join(assetDir, "manifest.json");
99
+ if (!fs.existsSync(manifestPath)) {
100
+ return null;
101
+ }
102
+ try {
103
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
104
+ return {
105
+ hash,
106
+ scene: manifest.scene,
107
+ props: {},
108
+ manifestPath,
109
+ manifestUrl: `/manim-assets/${hash}/manifest.json`,
110
+ };
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ /**
117
+ * Determine which animations need to be rendered (not in cache).
118
+ */
119
+ function getAnimationsToRender(animations, publicDir) {
120
+ const cached = [];
121
+ const toRender = [];
122
+ for (const animation of animations) {
123
+ const hash = computePropsHash(animation.scene, animation.props);
124
+ const entry = getCacheEntry(hash, publicDir);
125
+ if (entry) {
126
+ cached.push(entry);
127
+ }
128
+ else {
129
+ toRender.push(animation);
130
+ }
131
+ }
132
+ return { cached, toRender };
133
+ }
134
+ /**
135
+ * Ensure the manim-assets directory exists.
136
+ */
137
+ function ensureAssetDir(publicDir) {
138
+ const assetDir = path.join(publicDir, "manim-assets");
139
+ if (!fs.existsSync(assetDir)) {
140
+ fs.mkdirSync(assetDir, { recursive: true });
141
+ }
142
+ return assetDir;
143
+ }
144
+ /**
145
+ * Get the output directory for a specific animation hash.
146
+ */
147
+ function getOutputDir(hash, publicDir) {
148
+ return path.join(publicDir, "manim-assets", hash);
149
+ }
150
+ /**
151
+ * Write the runtime cache manifest that maps hashes to manifest URLs.
152
+ * This is used by the React component to look up animations at runtime.
153
+ */
154
+ function writeCacheManifest(animations, publicDir) {
155
+ const manifest = {
156
+ version: CACHE_VERSION,
157
+ animations: {},
158
+ };
159
+ for (const animation of animations) {
160
+ const hash = computePropsHash(animation.scene, animation.props);
161
+ manifest.animations[hash] = `/manim-assets/${hash}/manifest.json`;
162
+ }
163
+ const manifestPath = path.join(publicDir, "manim-assets", "cache-manifest.json");
164
+ ensureAssetDir(publicDir);
165
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
166
+ }
167
+ /**
168
+ * Read the existing cache manifest if it exists.
169
+ */
170
+ function readCacheManifest(publicDir) {
171
+ const manifestPath = path.join(publicDir, "manim-assets", "cache-manifest.json");
172
+ if (!fs.existsSync(manifestPath)) {
173
+ return null;
174
+ }
175
+ try {
176
+ const content = fs.readFileSync(manifestPath, "utf-8");
177
+ const manifest = JSON.parse(content);
178
+ if (manifest.version !== CACHE_VERSION) {
179
+ return null;
180
+ }
181
+ return manifest;
182
+ }
183
+ catch {
184
+ return null;
185
+ }
186
+ }
187
+ /**
188
+ * Clean up orphaned cache entries that are no longer referenced.
189
+ */
190
+ function cleanOrphanedCache(animations, publicDir) {
191
+ const assetDir = path.join(publicDir, "manim-assets");
192
+ if (!fs.existsSync(assetDir)) {
193
+ return;
194
+ }
195
+ // Get all valid hashes
196
+ const validHashes = new Set(animations.map((a) => computePropsHash(a.scene, a.props)));
197
+ // Check each directory in manim-assets
198
+ const entries = fs.readdirSync(assetDir, { withFileTypes: true });
199
+ for (const entry of entries) {
200
+ if (entry.isDirectory() && !validHashes.has(entry.name)) {
201
+ // This hash is no longer used, remove it
202
+ const dirPath = path.join(assetDir, entry.name);
203
+ fs.rmSync(dirPath, { recursive: true, force: true });
204
+ }
205
+ }
206
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,292 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const cache_1 = require("./cache");
40
+ // Mock fs module
41
+ vitest_1.vi.mock("fs");
42
+ (0, vitest_1.describe)("computePropsHash", () => {
43
+ (0, vitest_1.it)("should produce consistent hashes for same input", () => {
44
+ const hash1 = (0, cache_1.computePropsHash)("TextScene", { text: "Hello", fontSize: 72 });
45
+ const hash2 = (0, cache_1.computePropsHash)("TextScene", { text: "Hello", fontSize: 72 });
46
+ (0, vitest_1.expect)(hash1).toBe(hash2);
47
+ });
48
+ (0, vitest_1.it)("should produce different hashes for different scenes", () => {
49
+ const hash1 = (0, cache_1.computePropsHash)("TextScene", { text: "Hello" });
50
+ const hash2 = (0, cache_1.computePropsHash)("CustomScene", { text: "Hello" });
51
+ (0, vitest_1.expect)(hash1).not.toBe(hash2);
52
+ });
53
+ (0, vitest_1.it)("should produce different hashes for different props", () => {
54
+ const hash1 = (0, cache_1.computePropsHash)("TextScene", { text: "Hello" });
55
+ const hash2 = (0, cache_1.computePropsHash)("TextScene", { text: "World" });
56
+ (0, vitest_1.expect)(hash1).not.toBe(hash2);
57
+ });
58
+ (0, vitest_1.it)("should normalize key order", () => {
59
+ const hash1 = (0, cache_1.computePropsHash)("TextScene", { a: 1, b: 2, c: 3 });
60
+ const hash2 = (0, cache_1.computePropsHash)("TextScene", { c: 3, a: 1, b: 2 });
61
+ (0, vitest_1.expect)(hash1).toBe(hash2);
62
+ });
63
+ (0, vitest_1.it)("should handle empty props", () => {
64
+ const hash = (0, cache_1.computePropsHash)("TextScene", {});
65
+ (0, vitest_1.expect)(hash).toHaveLength(8);
66
+ (0, vitest_1.expect)(hash).toMatch(/^[0-9a-f]{8}$/);
67
+ });
68
+ (0, vitest_1.it)("should handle nested objects with sorted keys", () => {
69
+ const hash1 = (0, cache_1.computePropsHash)("Scene", { config: { b: 2, a: 1 } });
70
+ const hash2 = (0, cache_1.computePropsHash)("Scene", { config: { a: 1, b: 2 } });
71
+ (0, vitest_1.expect)(hash1).toBe(hash2);
72
+ });
73
+ (0, vitest_1.it)("should handle arrays (preserving order)", () => {
74
+ const hash1 = (0, cache_1.computePropsHash)("Scene", { items: [1, 2, 3] });
75
+ const hash2 = (0, cache_1.computePropsHash)("Scene", { items: [3, 2, 1] });
76
+ (0, vitest_1.expect)(hash1).not.toBe(hash2);
77
+ });
78
+ });
79
+ (0, vitest_1.describe)("isCached", () => {
80
+ (0, vitest_1.beforeEach)(() => {
81
+ vitest_1.vi.resetAllMocks();
82
+ });
83
+ (0, vitest_1.it)("should return true when manifest exists", () => {
84
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
85
+ const result = (0, cache_1.isCached)("abc12345", "/public");
86
+ (0, vitest_1.expect)(result).toBe(true);
87
+ (0, vitest_1.expect)(fs.existsSync).toHaveBeenCalledWith(path.join("/public", "manim-assets", "abc12345", "manifest.json"));
88
+ });
89
+ (0, vitest_1.it)("should return false when manifest does not exist", () => {
90
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(false);
91
+ const result = (0, cache_1.isCached)("abc12345", "/public");
92
+ (0, vitest_1.expect)(result).toBe(false);
93
+ });
94
+ });
95
+ (0, vitest_1.describe)("getCacheEntry", () => {
96
+ (0, vitest_1.beforeEach)(() => {
97
+ vitest_1.vi.resetAllMocks();
98
+ });
99
+ (0, vitest_1.it)("should return null when manifest does not exist", () => {
100
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(false);
101
+ const result = (0, cache_1.getCacheEntry)("abc12345", "/public");
102
+ (0, vitest_1.expect)(result).toBeNull();
103
+ });
104
+ (0, vitest_1.it)("should return cache entry when manifest exists", () => {
105
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
106
+ vitest_1.vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ scene: "TextScene", fps: 30 }));
107
+ const result = (0, cache_1.getCacheEntry)("abc12345", "/public");
108
+ (0, vitest_1.expect)(result).toEqual({
109
+ hash: "abc12345",
110
+ scene: "TextScene",
111
+ props: {},
112
+ manifestPath: path.join("/public", "manim-assets", "abc12345", "manifest.json"),
113
+ manifestUrl: "/manim-assets/abc12345/manifest.json",
114
+ });
115
+ });
116
+ (0, vitest_1.it)("should return null when manifest is invalid JSON", () => {
117
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
118
+ vitest_1.vi.mocked(fs.readFileSync).mockReturnValue("invalid json");
119
+ const result = (0, cache_1.getCacheEntry)("abc12345", "/public");
120
+ (0, vitest_1.expect)(result).toBeNull();
121
+ });
122
+ });
123
+ (0, vitest_1.describe)("getAnimationsToRender", () => {
124
+ (0, vitest_1.beforeEach)(() => {
125
+ vitest_1.vi.resetAllMocks();
126
+ });
127
+ (0, vitest_1.it)("should separate cached and uncached animations", () => {
128
+ const animations = [
129
+ { id: "1", filePath: "a.tsx", line: 1, scene: "TextScene", props: { text: "Hello" } },
130
+ { id: "2", filePath: "b.tsx", line: 2, scene: "TextScene", props: { text: "World" } },
131
+ ];
132
+ // First animation is cached, second is not
133
+ vitest_1.vi.mocked(fs.existsSync).mockImplementation((p) => {
134
+ const pathStr = String(p);
135
+ return pathStr.includes((0, cache_1.computePropsHash)("TextScene", { text: "Hello" }));
136
+ });
137
+ vitest_1.vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ scene: "TextScene" }));
138
+ const result = (0, cache_1.getAnimationsToRender)(animations, "/public");
139
+ (0, vitest_1.expect)(result.cached).toHaveLength(1);
140
+ (0, vitest_1.expect)(result.toRender).toHaveLength(1);
141
+ (0, vitest_1.expect)(result.toRender[0].props.text).toBe("World");
142
+ });
143
+ (0, vitest_1.it)("should return all as toRender when none are cached", () => {
144
+ const animations = [
145
+ { id: "1", filePath: "a.tsx", line: 1, scene: "TextScene", props: { text: "Hello" } },
146
+ ];
147
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(false);
148
+ const result = (0, cache_1.getAnimationsToRender)(animations, "/public");
149
+ (0, vitest_1.expect)(result.cached).toHaveLength(0);
150
+ (0, vitest_1.expect)(result.toRender).toHaveLength(1);
151
+ });
152
+ (0, vitest_1.it)("should return all as cached when all exist", () => {
153
+ const animations = [
154
+ { id: "1", filePath: "a.tsx", line: 1, scene: "TextScene", props: { text: "Hello" } },
155
+ ];
156
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
157
+ vitest_1.vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ scene: "TextScene" }));
158
+ const result = (0, cache_1.getAnimationsToRender)(animations, "/public");
159
+ (0, vitest_1.expect)(result.cached).toHaveLength(1);
160
+ (0, vitest_1.expect)(result.toRender).toHaveLength(0);
161
+ });
162
+ });
163
+ (0, vitest_1.describe)("ensureAssetDir", () => {
164
+ (0, vitest_1.beforeEach)(() => {
165
+ vitest_1.vi.resetAllMocks();
166
+ });
167
+ (0, vitest_1.it)("should create directory if it does not exist", () => {
168
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(false);
169
+ vitest_1.vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
170
+ const result = (0, cache_1.ensureAssetDir)("/public");
171
+ (0, vitest_1.expect)(result).toBe(path.join("/public", "manim-assets"));
172
+ (0, vitest_1.expect)(fs.mkdirSync).toHaveBeenCalledWith(path.join("/public", "manim-assets"), { recursive: true });
173
+ });
174
+ (0, vitest_1.it)("should not create directory if it exists", () => {
175
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
176
+ const result = (0, cache_1.ensureAssetDir)("/public");
177
+ (0, vitest_1.expect)(result).toBe(path.join("/public", "manim-assets"));
178
+ (0, vitest_1.expect)(fs.mkdirSync).not.toHaveBeenCalled();
179
+ });
180
+ });
181
+ (0, vitest_1.describe)("getOutputDir", () => {
182
+ (0, vitest_1.it)("should return correct output directory path", () => {
183
+ const result = (0, cache_1.getOutputDir)("abc12345", "/public");
184
+ (0, vitest_1.expect)(result).toBe(path.join("/public", "manim-assets", "abc12345"));
185
+ });
186
+ });
187
+ (0, vitest_1.describe)("writeCacheManifest", () => {
188
+ (0, vitest_1.beforeEach)(() => {
189
+ vitest_1.vi.resetAllMocks();
190
+ });
191
+ (0, vitest_1.it)("should write cache manifest with correct content", () => {
192
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
193
+ vitest_1.vi.mocked(fs.writeFileSync).mockReturnValue(undefined);
194
+ const animations = [
195
+ { id: "1", filePath: "a.tsx", line: 1, scene: "TextScene", props: { text: "Hello" } },
196
+ { id: "2", filePath: "b.tsx", line: 2, scene: "TextScene", props: { text: "World" } },
197
+ ];
198
+ (0, cache_1.writeCacheManifest)(animations, "/public");
199
+ (0, vitest_1.expect)(fs.writeFileSync).toHaveBeenCalledWith(path.join("/public", "manim-assets", "cache-manifest.json"), vitest_1.expect.stringContaining('"version":'));
200
+ });
201
+ (0, vitest_1.it)("should generate correct hash for each animation", () => {
202
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
203
+ let writtenContent = "";
204
+ vitest_1.vi.mocked(fs.writeFileSync).mockImplementation((_, content) => {
205
+ writtenContent = String(content);
206
+ });
207
+ const animations = [
208
+ { id: "1", filePath: "a.tsx", line: 1, scene: "TextScene", props: { text: "Test" } },
209
+ ];
210
+ (0, cache_1.writeCacheManifest)(animations, "/public");
211
+ const manifest = JSON.parse(writtenContent);
212
+ const hash = (0, cache_1.computePropsHash)("TextScene", { text: "Test" });
213
+ (0, vitest_1.expect)(manifest.version).toBe(1);
214
+ (0, vitest_1.expect)(manifest.animations[hash]).toBe(`/manim-assets/${hash}/manifest.json`);
215
+ });
216
+ });
217
+ (0, vitest_1.describe)("readCacheManifest", () => {
218
+ (0, vitest_1.beforeEach)(() => {
219
+ vitest_1.vi.resetAllMocks();
220
+ });
221
+ (0, vitest_1.it)("should return null when manifest does not exist", () => {
222
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(false);
223
+ const result = (0, cache_1.readCacheManifest)("/public");
224
+ (0, vitest_1.expect)(result).toBeNull();
225
+ });
226
+ (0, vitest_1.it)("should return manifest when it exists and is valid", () => {
227
+ const manifest = {
228
+ version: 1,
229
+ animations: { abc123: "/manim-assets/abc123/manifest.json" },
230
+ };
231
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
232
+ vitest_1.vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(manifest));
233
+ const result = (0, cache_1.readCacheManifest)("/public");
234
+ (0, vitest_1.expect)(result).toEqual(manifest);
235
+ });
236
+ (0, vitest_1.it)("should return null for wrong version", () => {
237
+ const manifest = {
238
+ version: 999, // Wrong version
239
+ animations: {},
240
+ };
241
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
242
+ vitest_1.vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(manifest));
243
+ const result = (0, cache_1.readCacheManifest)("/public");
244
+ (0, vitest_1.expect)(result).toBeNull();
245
+ });
246
+ (0, vitest_1.it)("should return null for invalid JSON", () => {
247
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
248
+ vitest_1.vi.mocked(fs.readFileSync).mockReturnValue("invalid json");
249
+ const result = (0, cache_1.readCacheManifest)("/public");
250
+ (0, vitest_1.expect)(result).toBeNull();
251
+ });
252
+ });
253
+ (0, vitest_1.describe)("cleanOrphanedCache", () => {
254
+ (0, vitest_1.beforeEach)(() => {
255
+ vitest_1.vi.resetAllMocks();
256
+ });
257
+ (0, vitest_1.it)("should do nothing if asset directory does not exist", () => {
258
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(false);
259
+ (0, cache_1.cleanOrphanedCache)([], "/public");
260
+ (0, vitest_1.expect)(fs.readdirSync).not.toHaveBeenCalled();
261
+ });
262
+ (0, vitest_1.it)("should remove orphaned directories", () => {
263
+ const animations = [
264
+ { id: "1", filePath: "a.tsx", line: 1, scene: "TextScene", props: { text: "Keep" } },
265
+ ];
266
+ const validHash = (0, cache_1.computePropsHash)("TextScene", { text: "Keep" });
267
+ const orphanHash = "deadbeef";
268
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
269
+ vitest_1.vi.mocked(fs.readdirSync).mockReturnValue([
270
+ { name: validHash, isDirectory: () => true },
271
+ { name: orphanHash, isDirectory: () => true },
272
+ { name: "cache-manifest.json", isDirectory: () => false },
273
+ ]);
274
+ vitest_1.vi.mocked(fs.rmSync).mockReturnValue(undefined);
275
+ (0, cache_1.cleanOrphanedCache)(animations, "/public");
276
+ (0, vitest_1.expect)(fs.rmSync).toHaveBeenCalledWith(path.join("/public", "manim-assets", orphanHash), { recursive: true, force: true });
277
+ (0, vitest_1.expect)(fs.rmSync).toHaveBeenCalledTimes(1);
278
+ });
279
+ (0, vitest_1.it)("should not remove valid cache entries", () => {
280
+ const animations = [
281
+ { id: "1", filePath: "a.tsx", line: 1, scene: "TextScene", props: { text: "Keep" } },
282
+ ];
283
+ const validHash = (0, cache_1.computePropsHash)("TextScene", { text: "Keep" });
284
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
285
+ vitest_1.vi.mocked(fs.readdirSync).mockReturnValue([
286
+ { name: validHash, isDirectory: () => true },
287
+ ]);
288
+ vitest_1.vi.mocked(fs.rmSync).mockReturnValue(undefined);
289
+ (0, cache_1.cleanOrphanedCache)(animations, "/public");
290
+ (0, vitest_1.expect)(fs.rmSync).not.toHaveBeenCalled();
291
+ });
292
+ });
@@ -0,0 +1,29 @@
1
+ export interface ExtractedAnimation {
2
+ /** Unique identifier based on file path and location */
3
+ id: string;
4
+ /** Source file path */
5
+ filePath: string;
6
+ /** Line number in source */
7
+ line: number;
8
+ /** Scene name (defaults to "TextScene") */
9
+ scene: string;
10
+ /** Props to pass to the Manim scene */
11
+ props: Record<string, unknown>;
12
+ }
13
+ interface ExtractorOptions {
14
+ /** Root directory to scan */
15
+ rootDir: string;
16
+ /** Glob patterns to include (default: ["**\/*.tsx", "**\/*.jsx"]) */
17
+ include?: string[];
18
+ /** Glob patterns to exclude (default: ["node_modules/**", ".next/**"]) */
19
+ exclude?: string[];
20
+ }
21
+ /**
22
+ * Scan a directory for ManimScroll components and extract their configurations.
23
+ */
24
+ export declare function extractAnimations(options: ExtractorOptions): Promise<ExtractedAnimation[]>;
25
+ /**
26
+ * Extract animations from a single file (useful for watch mode).
27
+ */
28
+ export declare function extractAnimationsFromFile(filePath: string): ExtractedAnimation[];
29
+ export {};