@ic-reactor/codegen 0.1.3 → 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/dist/index.js CHANGED
@@ -6,21 +6,6 @@ function toPascalCase(str) {
6
6
  function toCamelCase(str) {
7
7
  return camelCase(str);
8
8
  }
9
- function getHookFileName(methodName, hookType) {
10
- const camelMethod = toCamelCase(methodName);
11
- const pascalType = toPascalCase(hookType);
12
- return `${camelMethod}${pascalType}.ts`;
13
- }
14
- function getHookExportName(methodName, hookType) {
15
- const camelMethod = toCamelCase(methodName);
16
- const pascalType = toPascalCase(hookType);
17
- return `${camelMethod}${pascalType}`;
18
- }
19
- function getReactHookName(methodName, hookType) {
20
- const pascalMethod = toPascalCase(methodName);
21
- const pascalType = toPascalCase(hookType);
22
- return `use${pascalMethod}${pascalType}`;
23
- }
24
9
  function getReactorName(canisterName) {
25
10
  return `${toCamelCase(canisterName)}Reactor`;
26
11
  }
@@ -28,52 +13,13 @@ function getServiceTypeName(canisterName) {
28
13
  return `${toPascalCase(canisterName)}Service`;
29
14
  }
30
15
 
31
- // src/did.ts
32
- import fs from "fs";
33
- function parseDIDFile(didFilePath) {
34
- if (!fs.existsSync(didFilePath)) {
35
- throw new Error(`DID file not found: ${didFilePath}`);
36
- }
37
- const content = fs.readFileSync(didFilePath, "utf-8");
38
- return extractMethods(content);
39
- }
40
- function extractMethods(didContent) {
41
- const methods = [];
42
- const cleanContent = didContent.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
43
- const methodRegex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(?:func\s*)?\(([^)]*)\)\s*->\s*\(([^)]*)\)\s*(query|composite_query)?/g;
44
- let match;
45
- while ((match = methodRegex.exec(cleanContent)) !== null) {
46
- const name = match[1];
47
- const args = match[2].trim();
48
- const returnType = match[3].trim();
49
- const queryAnnotation = match[4];
50
- const isQuery = queryAnnotation === "query" || queryAnnotation === "composite_query";
51
- methods.push({
52
- name,
53
- type: isQuery ? "query" : "mutation",
54
- hasArgs: args.length > 0 && args !== "",
55
- argsDescription: args || void 0,
56
- returnDescription: returnType || void 0
57
- });
58
- }
59
- return methods;
60
- }
61
- function getMethodsByType(methods, type) {
62
- return methods.filter((m) => m.type === type);
63
- }
64
- function formatMethodForDisplay(method) {
65
- const typeLabel = method.type === "query" ? "query" : "update";
66
- const argsLabel = method.hasArgs ? "with args" : "no args";
67
- return `${method.name} (${typeLabel}, ${argsLabel})`;
68
- }
69
-
70
16
  // src/bindgen.ts
71
- import { generate } from "@icp-sdk/bindgen/core";
17
+ import { didToJs, didToTs } from "@ic-reactor/parser";
72
18
  import path from "path";
73
- import fs2 from "fs";
19
+ import fs from "fs";
74
20
  async function generateDeclarations(options) {
75
21
  const { didFile, outDir } = options;
76
- if (!fs2.existsSync(didFile)) {
22
+ if (!fs.existsSync(didFile)) {
77
23
  return {
78
24
  success: false,
79
25
  declarationsDir: "",
@@ -81,27 +27,25 @@ async function generateDeclarations(options) {
81
27
  };
82
28
  }
83
29
  const declarationsDir = path.join(outDir, "declarations");
30
+ const didFileName = path.basename(didFile);
84
31
  try {
85
- if (!fs2.existsSync(outDir)) {
86
- fs2.mkdirSync(outDir, { recursive: true });
32
+ const didContent = fs.readFileSync(didFile, "utf-8");
33
+ if (!fs.existsSync(outDir)) {
34
+ fs.mkdirSync(outDir, { recursive: true });
87
35
  }
88
- if (fs2.existsSync(declarationsDir)) {
89
- fs2.rmSync(declarationsDir, { recursive: true, force: true });
36
+ if (fs.existsSync(declarationsDir)) {
37
+ fs.rmSync(declarationsDir, { recursive: true, force: true });
90
38
  }
91
- fs2.mkdirSync(declarationsDir, { recursive: true });
92
- await generate({
93
- didFile,
94
- outDir,
95
- // Pass the parent directory; bindgen appends "declarations"
96
- output: {
97
- actor: {
98
- disabled: true
99
- // We don't need actor creation, we use Reactor
100
- },
101
- force: true
102
- // Overwrite existing files
103
- }
104
- });
39
+ fs.mkdirSync(declarationsDir, { recursive: true });
40
+ const jsContent = didToJs(didContent);
41
+ const tsContent = didToTs(didContent);
42
+ const baseName = didFileName.replace(/\.did$/, "");
43
+ const jsPath = path.join(declarationsDir, baseName + ".js");
44
+ const dtsPath = path.join(declarationsDir, baseName + ".d.ts");
45
+ const didPath = path.join(declarationsDir, didFileName);
46
+ fs.writeFileSync(jsPath, jsContent);
47
+ fs.writeFileSync(dtsPath, tsContent);
48
+ fs.writeFileSync(didPath, didContent);
105
49
  return {
106
50
  success: true,
107
51
  declarationsDir
@@ -116,77 +60,58 @@ async function generateDeclarations(options) {
116
60
  }
117
61
  function declarationsExist(outDir, canisterName) {
118
62
  const declarationsDir = path.join(outDir, "declarations");
119
- const didTsPath = path.join(declarationsDir, `${canisterName}.did.ts`);
120
- return fs2.existsSync(didTsPath);
121
- }
122
- function saveCandidFile(candidSource, outDir, canisterName) {
123
- const candidDir = path.join(outDir, "candid");
124
- if (!fs2.existsSync(candidDir)) {
125
- fs2.mkdirSync(candidDir, { recursive: true });
126
- }
127
- const candidPath = path.join(candidDir, `${canisterName}.did`);
128
- fs2.writeFileSync(candidPath, candidSource);
129
- return candidPath;
63
+ const didTsPath = path.join(declarationsDir, `${canisterName}.d.ts`);
64
+ return fs.existsSync(didTsPath);
130
65
  }
131
66
 
132
67
  // src/templates/reactor.ts
133
68
  import path2 from "path";
134
69
  function generateReactorFile(options) {
135
- const {
136
- canisterName,
137
- canisterConfig,
138
- globalClientManagerPath,
139
- hasDeclarations = true,
140
- advanced = false,
141
- didContent
142
- } = options;
143
- const pascalName = toPascalCase(canisterName);
144
- const reactorName = getReactorName(canisterName);
145
- const serviceName = getServiceTypeName(canisterName);
146
- const reactorType = canisterConfig.useDisplayReactor !== false ? "DisplayReactor" : "Reactor";
147
- const clientManagerPath = canisterConfig.clientManagerPath ?? globalClientManagerPath ?? "../../lib/client";
148
- const didFileName = path2.basename(canisterConfig.didFile);
149
- const declarationsPath = `./declarations/${didFileName}`;
70
+ const pascalName = toPascalCase(options.canisterName);
71
+ const reactorName = getReactorName(options.canisterName);
72
+ const serviceName = getServiceTypeName(options.canisterName);
73
+ const reactorType = "DisplayReactor";
74
+ const didFileName = path2.basename(options.didFile);
75
+ const baseName = didFileName.replace(/\.did$/, "");
76
+ const declarationsPath = `./declarations/${baseName}`;
77
+ const clientManagerPath = options.clientManagerPath ?? "../../clients";
150
78
  const vars = {
151
- canisterName,
79
+ canisterName: options.canisterName,
152
80
  pascalName,
153
81
  reactorName,
154
82
  serviceName,
155
83
  reactorType,
156
84
  clientManagerPath,
157
- declarationsPath,
158
- useDisplayReactor: canisterConfig.useDisplayReactor !== false
85
+ declarationsPath
159
86
  };
160
- if (!hasDeclarations) {
161
- return generateFallbackReactorFile(vars);
162
- }
163
- if (advanced && didContent) {
164
- return generateAdvancedReactorFile(vars, didContent);
165
- }
166
- return generateSimpleReactorFile(vars);
87
+ return generateStandardReactorFile(vars);
167
88
  }
168
- function reactorInstance(vars) {
89
+ function generateStandardReactorFile(vars) {
169
90
  const {
170
91
  pascalName,
171
92
  reactorName,
172
93
  serviceName,
173
94
  reactorType,
174
- canisterName,
175
- useDisplayReactor
95
+ clientManagerPath,
96
+ declarationsPath,
97
+ canisterName
176
98
  } = vars;
177
- return `/**
178
- * ${pascalName} Reactor \u2014 ${useDisplayReactor ? "Display" : "Candid"} mode.
179
- * ${useDisplayReactor ? "Automatically converts bigint \u2192 string, Principal \u2192 string, etc." : "Uses raw Candid types."}
99
+ return `import { ${reactorType}, createActorHooks } from "@ic-reactor/react"
100
+ import { clientManager } from "${clientManagerPath}"
101
+ import { idlFactory, type _SERVICE } from "${declarationsPath}"
102
+
103
+ export type ${serviceName} = _SERVICE
104
+
105
+ /**
106
+ * ${pascalName} Display Reactor
180
107
  */
181
108
  export const ${reactorName} = new ${reactorType}<${serviceName}>({
182
109
  clientManager,
183
110
  idlFactory,
184
111
  name: "${canisterName}",
185
- })`;
186
- }
187
- function actorHooks(vars) {
188
- const { pascalName, reactorName } = vars;
189
- return `const {
112
+ })
113
+
114
+ export const {
190
115
  useActorQuery: use${pascalName}Query,
191
116
  useActorSuspenseQuery: use${pascalName}SuspenseQuery,
192
117
  useActorInfiniteQuery: use${pascalName}InfiniteQuery,
@@ -194,296 +119,34 @@ function actorHooks(vars) {
194
119
  useActorMutation: use${pascalName}Mutation,
195
120
  useActorMethod: use${pascalName}Method,
196
121
  } = createActorHooks(${reactorName})
197
-
198
- export {
199
- use${pascalName}Query,
200
- use${pascalName}SuspenseQuery,
201
- use${pascalName}InfiniteQuery,
202
- use${pascalName}SuspenseInfiniteQuery,
203
- use${pascalName}Mutation,
204
- use${pascalName}Method,
205
- }`;
206
- }
207
- function generateSimpleReactorFile(vars) {
208
- const {
209
- pascalName,
210
- reactorType,
211
- clientManagerPath,
212
- declarationsPath,
213
- serviceName
214
- } = vars;
215
- return `/**
216
- * ${pascalName} Reactor
217
- *
218
- * Auto-generated by @ic-reactor/codegen
219
- */
220
-
221
- import { ${reactorType}, createActorHooks } from "@ic-reactor/react"
222
- import { clientManager } from "${clientManagerPath}"
223
- import { idlFactory, type _SERVICE } from "${declarationsPath}"
224
-
225
- export type ${serviceName} = _SERVICE
226
-
227
- ${reactorInstance(vars)}
228
-
229
- ${actorHooks(vars)}
230
-
231
- export { idlFactory }
232
122
  `;
233
123
  }
234
- function generateAdvancedReactorFile(vars, didContent) {
235
- const {
236
- pascalName,
237
- reactorName,
238
- serviceName,
239
- reactorType,
240
- clientManagerPath,
241
- declarationsPath
242
- } = vars;
243
- const methods = extractMethods(didContent);
244
- const hasQueryWithoutArgs = methods.some(
245
- (m) => m.type === "query" && !m.hasArgs
246
- );
247
- const hasMutationWithoutArgs = methods.some(
248
- (m) => m.type === "mutation" && !m.hasArgs
249
- );
250
- const extraImports = [];
251
- if (hasQueryWithoutArgs) extraImports.push("createQuery");
252
- if (hasMutationWithoutArgs) extraImports.push("createMutation");
253
- const perMethodHooks = methods.map(({ name, type, hasArgs }) => {
254
- const camelMethod = toCamelCase(name);
255
- if (type === "query") {
256
- if (!hasArgs) {
257
- return `
258
- export const ${camelMethod}Query = createQuery(${reactorName}, {
259
- functionName: "${name}",
260
- })`;
261
- }
262
- return "";
263
- } else {
264
- if (!hasArgs) {
265
- return `
266
- export const ${camelMethod}Mutation = createMutation(${reactorName}, {
267
- functionName: "${name}",
268
- })`;
269
- }
270
- return "";
271
- }
272
- }).filter(Boolean);
273
- return `/**
274
- * ${pascalName} Reactor (Advanced)
275
- *
276
- * Auto-generated by @ic-reactor/codegen
277
- * Includes reactor instance, actor hooks, and per-method static hooks.
278
- */
279
-
280
- import {
281
- ${reactorType},
282
- createActorHooks,${extraImports.length > 0 ? "\n " + extraImports.join(",\n ") + "," : ""}
283
- } from "@ic-reactor/react"
284
- import { clientManager } from "${clientManagerPath}"
285
- import { idlFactory, type _SERVICE } from "${declarationsPath}"
286
124
 
287
- type ${serviceName} = _SERVICE
125
+ // src/templates/client.ts
126
+ function generateClientFile(options = {}) {
127
+ const { queryClientPath } = options;
128
+ return `import { ClientManager } from "@ic-reactor/react"
129
+ ${queryClientPath ? `import { queryClient } from "${queryClientPath}"` : `import { QueryClient } from "@tanstack/react-query"
288
130
 
289
- ${reactorInstance(vars)}
131
+ export const queryClient = new QueryClient()`}
290
132
 
291
- ${actorHooks(vars)}
292
- ${perMethodHooks.length > 0 ? `
293
- // Per-method static hooks (no-args methods only)
294
- ${perMethodHooks.join("\n")}
295
- ` : ""}
296
- export { idlFactory }
297
- export type { ${serviceName} }
298
- `;
299
- }
300
- function generateFallbackReactorFile(vars) {
301
- const {
302
- canisterName,
303
- pascalName,
304
- serviceName,
305
- reactorType,
306
- clientManagerPath,
307
- declarationsPath
308
- } = vars;
309
- return `/**
310
- * ${pascalName} Reactor
311
- *
133
+ /**
134
+ * IC Reactor Client Manager
135
+ *
312
136
  * Auto-generated by @ic-reactor/codegen
313
- *
314
- * \u26A0\uFE0F Declarations were not generated. Run:
315
- * npx @icp-sdk/bindgen --input <path-to-did> --output ./${canisterName}/declarations
316
- * Then uncomment the import below and remove the fallback type.
317
137
  */
318
-
319
- import { ${reactorType}, createActorHooks } from "@ic-reactor/react"
320
- import { clientManager } from "${clientManagerPath}"
321
-
322
- // TODO: Uncomment after generating declarations:
323
- // import { idlFactory, type _SERVICE as ${serviceName} } from "${declarationsPath}"
324
-
325
- // Fallback \u2014 replace with generated types
326
- type ${serviceName} = Record<string, (...args: unknown[]) => Promise<unknown>>
327
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
328
- const idlFactory = ({ IDL }: { IDL: any }) => IDL.Service({})
329
-
330
- ${reactorInstance(vars)}
331
-
332
- ${actorHooks(vars)}
333
-
334
- export { idlFactory }
335
- export type { ${serviceName} }
336
- `;
337
- }
338
-
339
- // src/templates/query.ts
340
- function generateQueryHook(options) {
341
- const { canisterName, method, type = "query" } = options;
342
- const reactorName = getReactorName(canisterName);
343
- const hookExportName = getHookExportName(method.name, type);
344
- const isSuspense = type === "suspenseQuery";
345
- const creatorFn = isSuspense ? "createSuspenseQuery" : "createQuery";
346
- const factoryFn = isSuspense ? "createSuspenseQueryFactory" : "createQueryFactory";
347
- const hookName = isSuspense ? "useSuspenseQuery" : "useQuery";
348
- if (method.hasArgs) {
349
- return `/**
350
- * Query Factory: ${method.name}
351
- *
352
- * Auto-generated by @ic-reactor/codegen
353
- *
354
- * @example
355
- * const { data } = ${hookExportName}([arg1, arg2]).${hookName}()
356
- * const data = await ${hookExportName}([arg1, arg2]).fetch()
357
- * ${hookExportName}([arg1, arg2]).invalidate()
358
- */
359
-
360
- import { ${factoryFn} } from "@ic-reactor/react"
361
- import { ${reactorName} } from "../reactor"
362
-
363
- export const ${hookExportName} = ${factoryFn}(${reactorName}, {
364
- functionName: "${method.name}",
138
+ export const clientManager = new ClientManager({
139
+ queryClient,
140
+ withCanisterEnv: true,
365
141
  })
366
- `;
367
- }
368
- return `/**
369
- * Query: ${method.name}
370
- *
371
- * Auto-generated by @ic-reactor/codegen
372
- *
373
- * @example
374
- * const { data } = ${hookExportName}.${hookName}()
375
- * const data = await ${hookExportName}.fetch()
376
- * ${hookExportName}.invalidate()
377
- */
378
-
379
- import { ${creatorFn} } from "@ic-reactor/react"
380
- import { ${reactorName} } from "../reactor"
381
-
382
- export const ${hookExportName} = ${creatorFn}(${reactorName}, {
383
- functionName: "${method.name}",
384
- })
385
- `;
386
- }
387
-
388
- // src/templates/mutation.ts
389
- function generateMutationHook(options) {
390
- const { canisterName, method } = options;
391
- const pascalMethod = toPascalCase(method.name);
392
- const reactorName = getReactorName(canisterName);
393
- const hookExportName = getHookExportName(method.name, "mutation");
394
- return `/**
395
- * Mutation: ${method.name}
396
- *
397
- * Auto-generated by @ic-reactor/codegen
398
- *
399
- * @example
400
- * const { mutate, isPending } = ${hookExportName}.useMutation()
401
- * mutate(${method.hasArgs ? "[arg1, arg2]" : "[]"})
402
- *
403
- * // Direct execution (outside React)
404
- * const result = await ${hookExportName}.execute(${method.hasArgs ? "[arg1, arg2]" : "[]"})
405
- */
406
-
407
- import { createMutation } from "@ic-reactor/react"
408
- import { ${reactorName} } from "../reactor"
409
-
410
- export const ${hookExportName} = createMutation(${reactorName}, {
411
- functionName: "${method.name}",
412
- })
413
-
414
- /** React hook for ${method.name} */
415
- export const use${pascalMethod}Mutation = ${hookExportName}.useMutation
416
-
417
- /** Execute ${method.name} directly (outside React) */
418
- export const execute${pascalMethod} = ${hookExportName}.execute
419
- `;
420
- }
421
-
422
- // src/templates/infiniteQuery.ts
423
- function generateInfiniteQueryHook(options) {
424
- const { canisterName, method, type = "infiniteQuery" } = options;
425
- const reactorName = getReactorName(canisterName);
426
- const serviceName = getServiceTypeName(canisterName);
427
- const hookExportName = getHookExportName(method.name, type);
428
- const reactHookName = getReactHookName(method.name, type);
429
- return `/**
430
- * Infinite Query: ${method.name}
431
- *
432
- * Auto-generated by @ic-reactor/codegen
433
- *
434
- * \u26A0\uFE0F CUSTOMIZATION REQUIRED: Configure getArgs and getNextPageParam below.
435
- *
436
- * @example
437
- * const { data, fetchNextPage, hasNextPage } = ${hookExportName}.useInfiniteQuery()
438
- * const allItems = data?.pages.flatMap(page => page.items) ?? []
439
- */
440
-
441
- import { createInfiniteQuery } from "@ic-reactor/react"
442
- import { ${reactorName}, type ${serviceName} } from "../reactor"
443
-
444
- /** Define your pagination cursor type */
445
- type PageCursor = number
446
-
447
- export const ${hookExportName} = createInfiniteQuery(${reactorName}, {
448
- functionName: "${method.name}",
449
-
450
- initialPageParam: 0 as PageCursor,
451
-
452
- /** Convert page param to method arguments \u2014 customize for your API */
453
- getArgs: (pageParam: PageCursor) => {
454
- return [{ offset: pageParam, limit: 10 }] as Parameters<${serviceName}["${method.name}"]>
455
- },
456
-
457
- /** Extract next page param \u2014 return undefined when no more pages */
458
- getNextPageParam: (lastPage, allPages, lastPageParam) => {
459
- // Example: offset-based
460
- // if (lastPage.items.length < 10) return undefined
461
- // return lastPageParam + 10
462
- return undefined
463
- },
464
- })
465
-
466
- /** React hook for paginated ${method.name} */
467
- export const ${reactHookName} = ${hookExportName}.useInfiniteQuery
468
142
  `;
469
143
  }
470
144
  export {
471
145
  declarationsExist,
472
- extractMethods,
473
- formatMethodForDisplay,
146
+ generateClientFile,
474
147
  generateDeclarations,
475
- generateInfiniteQueryHook,
476
- generateMutationHook,
477
- generateQueryHook,
478
148
  generateReactorFile,
479
- getHookExportName,
480
- getHookFileName,
481
- getMethodsByType,
482
- getReactHookName,
483
149
  getReactorName,
484
150
  getServiceTypeName,
485
- parseDIDFile,
486
- saveCandidFile,
487
- toCamelCase,
488
151
  toPascalCase
489
152
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/codegen",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "Shared code generation utilities for IC Reactor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -33,19 +33,11 @@
33
33
  "directory": "packages/codegen"
34
34
  },
35
35
  "dependencies": {
36
- "change-case": "^5.4.4"
37
- },
38
- "peerDependencies": {
39
- "@icp-sdk/bindgen": "^0.2.0"
40
- },
41
- "peerDependenciesMeta": {
42
- "@icp-sdk/bindgen": {
43
- "optional": true
44
- }
36
+ "change-case": "^5.4.4",
37
+ "@ic-reactor/parser": "0.4.5"
45
38
  },
46
39
  "devDependencies": {
47
- "@icp-sdk/bindgen": "^0.2.1",
48
- "@types/node": "^25.2.2",
40
+ "@types/node": "^25.2.3",
49
41
  "tsup": "^8.5.1",
50
42
  "typescript": "^5.9.3",
51
43
  "vitest": "^4.0.18"
@@ -0,0 +1,18 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Bindgen > generateDeclarations creates correct files > js-declarations 1`] = `
4
+ "export const idlFactory = ({ IDL }) => {
5
+ return IDL.Service({ 'greet' : IDL.Func([IDL.Text], [IDL.Text], ['query']) });
6
+ };
7
+ export const init = ({ IDL }) => { return []; };"
8
+ `;
9
+
10
+ exports[`Bindgen > generateDeclarations creates correct files > ts-declarations 1`] = `
11
+ "import type { Principal } from '@icp-sdk/core/principal';
12
+ import type { ActorMethod } from '@icp-sdk/core/agent';
13
+ import type { IDL } from '@icp-sdk/core/candid';
14
+
15
+ export interface _SERVICE { 'greet' : ActorMethod<[string], string> }
16
+ export declare const idlFactory: IDL.InterfaceFactory;
17
+ export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[];"
18
+ `;
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2
+ import { generateDeclarations } from "./bindgen"
3
+ import fs from "node:fs"
4
+ import path from "node:path"
5
+
6
+ describe("Bindgen", () => {
7
+ const mockDidFile = "mock/test.did"
8
+ const mockOutDir = "mock/output"
9
+ const mockCanisterName = "test_canister"
10
+ const validDidContent = `service : {
11
+ greet: (text) -> (text) query;
12
+ }`
13
+
14
+ beforeEach(() => {
15
+ // Mock fs methods
16
+ vi.spyOn(fs, "existsSync").mockImplementation((p) => {
17
+ // Pretend the source DID file exists
18
+ if (p.toString() === mockDidFile) return true
19
+ return false
20
+ })
21
+
22
+ vi.spyOn(fs, "mkdirSync").mockImplementation(() => undefined as any)
23
+ vi.spyOn(fs, "rmSync").mockImplementation(() => undefined)
24
+ vi.spyOn(fs, "readFileSync").mockImplementation(() => validDidContent)
25
+ vi.spyOn(fs, "writeFileSync").mockImplementation(() => undefined)
26
+ })
27
+
28
+ afterEach(() => {
29
+ vi.restoreAllMocks()
30
+ })
31
+
32
+ it("generateDeclarations creates correct files", async () => {
33
+ const result = await generateDeclarations({
34
+ didFile: mockDidFile,
35
+ outDir: mockOutDir,
36
+ canisterName: mockCanisterName,
37
+ })
38
+
39
+ if (!result.success) {
40
+ console.error(result.error)
41
+ }
42
+
43
+ expect(result.success).toBe(true)
44
+ expect(result.declarationsDir).toBe(path.join(mockOutDir, "declarations"))
45
+
46
+ // Verify file writes
47
+ const declarationsDir = path.join(mockOutDir, "declarations")
48
+ const jsPath = path.join(declarationsDir, "test.js")
49
+ const dtsPath = path.join(declarationsDir, "test.d.ts")
50
+
51
+ // Should create directory
52
+ expect(fs.mkdirSync).toHaveBeenCalledWith(mockOutDir, { recursive: true })
53
+ expect(fs.mkdirSync).toHaveBeenCalledWith(declarationsDir, {
54
+ recursive: true,
55
+ })
56
+
57
+ // Validate exact content using snapshots
58
+ expect(fs.writeFileSync).toHaveBeenCalledWith(jsPath, expect.any(String))
59
+ expect(fs.writeFileSync).toHaveBeenCalledWith(dtsPath, expect.any(String))
60
+
61
+ // Get the arguments of the calls to check content
62
+ const jsCall = vi
63
+ .mocked(fs.writeFileSync)
64
+ .mock.calls.find((call) => call[0] === jsPath)
65
+ const dtsCall = vi
66
+ .mocked(fs.writeFileSync)
67
+ .mock.calls.find((call) => call[0] === dtsPath)
68
+
69
+ expect(jsCall?.[1]).toMatchSnapshot("js-declarations")
70
+ expect(dtsCall?.[1]).toMatchSnapshot("ts-declarations")
71
+ })
72
+
73
+ it("returns error if DID file missing", async () => {
74
+ vi.spyOn(fs, "existsSync").mockReturnValue(false)
75
+
76
+ const result = await generateDeclarations({
77
+ didFile: "missing.did",
78
+ outDir: mockOutDir,
79
+ canisterName: mockCanisterName,
80
+ })
81
+
82
+ expect(result.success).toBe(false)
83
+ expect(result.error).toContain("DID file not found")
84
+ })
85
+ })