@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.cjs +543 -0
- package/dist/index.d.cts +237 -0
- package/dist/index.d.ts +237 -0
- package/dist/index.js +489 -0
- package/package.json +60 -0
- package/src/bindgen.ts +113 -0
- package/src/did.test.ts +102 -0
- package/src/did.ts +85 -0
- package/src/index.ts +51 -0
- package/src/naming.test.ts +59 -0
- package/src/naming.ts +83 -0
- package/src/templates/__snapshots__/infiniteQuery.test.ts.snap +44 -0
- package/src/templates/__snapshots__/mutation.test.ts.snap +59 -0
- package/src/templates/__snapshots__/query.test.ts.snap +64 -0
- package/src/templates/__snapshots__/reactor.test.ts.snap +163 -0
- package/src/templates/infiniteQuery.test.ts +42 -0
- package/src/templates/infiniteQuery.ts +74 -0
- package/src/templates/mutation.test.ts +43 -0
- package/src/templates/mutation.ts +51 -0
- package/src/templates/query.test.ts +44 -0
- package/src/templates/query.ts +71 -0
- package/src/templates/reactor.test.ts +78 -0
- package/src/templates/reactor.ts +290 -0
- package/src/types.ts +78 -0
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
|
+
}
|