@ic-reactor/codegen 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/dist/index.js ADDED
@@ -0,0 +1,489 @@
1
+ // src/naming.ts
2
+ import { camelCase, pascalCase } from "change-case";
3
+ function toPascalCase(str) {
4
+ return pascalCase(str);
5
+ }
6
+ function toCamelCase(str) {
7
+ return camelCase(str);
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
+ function getReactorName(canisterName) {
25
+ return `${toCamelCase(canisterName)}Reactor`;
26
+ }
27
+ function getServiceTypeName(canisterName) {
28
+ return `${toPascalCase(canisterName)}Service`;
29
+ }
30
+
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
+ // src/bindgen.ts
71
+ import { generate } from "@icp-sdk/bindgen/core";
72
+ import path from "path";
73
+ import fs2 from "fs";
74
+ async function generateDeclarations(options) {
75
+ const { didFile, outDir } = options;
76
+ if (!fs2.existsSync(didFile)) {
77
+ return {
78
+ success: false,
79
+ declarationsDir: "",
80
+ error: `DID file not found: ${didFile}`
81
+ };
82
+ }
83
+ const declarationsDir = path.join(outDir, "declarations");
84
+ try {
85
+ if (!fs2.existsSync(outDir)) {
86
+ fs2.mkdirSync(outDir, { recursive: true });
87
+ }
88
+ if (fs2.existsSync(declarationsDir)) {
89
+ fs2.rmSync(declarationsDir, { recursive: true, force: true });
90
+ }
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
+ });
105
+ return {
106
+ success: true,
107
+ declarationsDir
108
+ };
109
+ } catch (error) {
110
+ return {
111
+ success: false,
112
+ declarationsDir,
113
+ error: error instanceof Error ? error.message : String(error)
114
+ };
115
+ }
116
+ }
117
+ function declarationsExist(outDir, canisterName) {
118
+ 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;
130
+ }
131
+
132
+ // src/templates/reactor.ts
133
+ import path2 from "path";
134
+ 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}`;
150
+ const vars = {
151
+ canisterName,
152
+ pascalName,
153
+ reactorName,
154
+ serviceName,
155
+ reactorType,
156
+ clientManagerPath,
157
+ declarationsPath,
158
+ useDisplayReactor: canisterConfig.useDisplayReactor !== false
159
+ };
160
+ if (!hasDeclarations) {
161
+ return generateFallbackReactorFile(vars);
162
+ }
163
+ if (advanced && didContent) {
164
+ return generateAdvancedReactorFile(vars, didContent);
165
+ }
166
+ return generateSimpleReactorFile(vars);
167
+ }
168
+ function reactorInstance(vars) {
169
+ const {
170
+ pascalName,
171
+ reactorName,
172
+ serviceName,
173
+ reactorType,
174
+ canisterName,
175
+ useDisplayReactor
176
+ } = 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."}
180
+ */
181
+ export const ${reactorName} = new ${reactorType}<${serviceName}>({
182
+ clientManager,
183
+ idlFactory,
184
+ name: "${canisterName}",
185
+ })`;
186
+ }
187
+ function actorHooks(vars) {
188
+ const { pascalName, reactorName } = vars;
189
+ return `const {
190
+ useActorQuery: use${pascalName}Query,
191
+ useActorSuspenseQuery: use${pascalName}SuspenseQuery,
192
+ useActorInfiniteQuery: use${pascalName}InfiniteQuery,
193
+ useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
194
+ useActorMutation: use${pascalName}Mutation,
195
+ useActorMethod: use${pascalName}Method,
196
+ } = 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
+ `;
233
+ }
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
+
287
+ type ${serviceName} = _SERVICE
288
+
289
+ ${reactorInstance(vars)}
290
+
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
+ *
312
+ * 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
+ */
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}",
365
+ })
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
+ `;
469
+ }
470
+ export {
471
+ declarationsExist,
472
+ extractMethods,
473
+ formatMethodForDisplay,
474
+ generateDeclarations,
475
+ generateInfiniteQueryHook,
476
+ generateMutationHook,
477
+ generateQueryHook,
478
+ generateReactorFile,
479
+ getHookExportName,
480
+ getHookFileName,
481
+ getMethodsByType,
482
+ getReactHookName,
483
+ getReactorName,
484
+ getServiceTypeName,
485
+ parseDIDFile,
486
+ saveCandidFile,
487
+ toCamelCase,
488
+ toPascalCase
489
+ };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@ic-reactor/codegen",
3
+ "version": "0.1.0",
4
+ "description": "Shared code generation utilities for IC Reactor",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format esm,cjs --dts --tsconfig tsconfig.json",
22
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch --tsconfig tsconfig.json",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "typecheck": "tsc --noEmit"
26
+ },
27
+ "keywords": [
28
+ "internet-computer",
29
+ "candid",
30
+ "ic-reactor",
31
+ "codegen",
32
+ "dfinity",
33
+ "icp"
34
+ ],
35
+ "author": "Behrad Deylami",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/B3Pay/ic-reactor.git",
40
+ "directory": "packages/codegen"
41
+ },
42
+ "dependencies": {
43
+ "change-case": "^5.4.4"
44
+ },
45
+ "peerDependencies": {
46
+ "@icp-sdk/bindgen": "^0.2.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "@icp-sdk/bindgen": {
50
+ "optional": true
51
+ }
52
+ },
53
+ "devDependencies": {
54
+ "@icp-sdk/bindgen": "^0.2.1",
55
+ "@types/node": "^25.2.2",
56
+ "tsup": "^8.5.1",
57
+ "typescript": "^5.9.3",
58
+ "vitest": "^4.0.18"
59
+ }
60
+ }
package/src/bindgen.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Bindgen utilities
3
+ *
4
+ * Generates TypeScript declarations from Candid files using @icp-sdk/bindgen.
5
+ */
6
+
7
+ import { generate } from "@icp-sdk/bindgen/core"
8
+ import path from "node:path"
9
+ import fs from "node:fs"
10
+
11
+ export interface BindgenOptions {
12
+ /** Path to the .did file */
13
+ didFile: string
14
+ /** Output directory for generated declarations */
15
+ outDir: string
16
+ /** Canister name (used for naming) */
17
+ canisterName: string
18
+ }
19
+
20
+ export interface BindgenResult {
21
+ success: boolean
22
+ declarationsDir: string
23
+ error?: string
24
+ }
25
+
26
+ /**
27
+ * Generate TypeScript declarations from a Candid file
28
+ *
29
+ * This creates:
30
+ * - declarations/<canisterName>.did.ts - IDL factory and types
31
+ */
32
+ export async function generateDeclarations(
33
+ options: BindgenOptions
34
+ ): Promise<BindgenResult> {
35
+ const { didFile, outDir } = options
36
+
37
+ // Ensure the .did file exists
38
+ if (!fs.existsSync(didFile)) {
39
+ return {
40
+ success: false,
41
+ declarationsDir: "",
42
+ error: `DID file not found: ${didFile}`,
43
+ }
44
+ }
45
+
46
+ const declarationsDir = path.join(outDir, "declarations")
47
+
48
+ try {
49
+ // Ensure the output directory exists
50
+ if (!fs.existsSync(outDir)) {
51
+ fs.mkdirSync(outDir, { recursive: true })
52
+ }
53
+
54
+ // Clean existing declarations before regenerating
55
+ if (fs.existsSync(declarationsDir)) {
56
+ fs.rmSync(declarationsDir, { recursive: true, force: true })
57
+ }
58
+ fs.mkdirSync(declarationsDir, { recursive: true })
59
+
60
+ // Note: bindgen appends "declarations" internally, so we pass the parent directory
61
+ await generate({
62
+ didFile,
63
+ outDir, // Pass the parent directory; bindgen appends "declarations"
64
+ output: {
65
+ actor: {
66
+ disabled: true, // We don't need actor creation, we use Reactor
67
+ },
68
+ force: true, // Overwrite existing files
69
+ },
70
+ })
71
+
72
+ return {
73
+ success: true,
74
+ declarationsDir,
75
+ }
76
+ } catch (error) {
77
+ return {
78
+ success: false,
79
+ declarationsDir,
80
+ error: error instanceof Error ? error.message : String(error),
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Check if declarations already exist for a canister
87
+ */
88
+ export function declarationsExist(
89
+ outDir: string,
90
+ canisterName: string
91
+ ): boolean {
92
+ const declarationsDir = path.join(outDir, "declarations")
93
+ const didTsPath = path.join(declarationsDir, `${canisterName}.did.ts`)
94
+ return fs.existsSync(didTsPath)
95
+ }
96
+
97
+ /**
98
+ * Save a Candid source to a file (for use with fetch command)
99
+ */
100
+ export function saveCandidFile(
101
+ candidSource: string,
102
+ outDir: string,
103
+ canisterName: string
104
+ ): string {
105
+ const candidDir = path.join(outDir, "candid")
106
+ if (!fs.existsSync(candidDir)) {
107
+ fs.mkdirSync(candidDir, { recursive: true })
108
+ }
109
+
110
+ const candidPath = path.join(candidDir, `${canisterName}.did`)
111
+ fs.writeFileSync(candidPath, candidSource)
112
+ return candidPath
113
+ }