@ic-reactor/codegen 0.2.0 → 0.3.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.
- package/dist/index.cjs +61 -409
- package/dist/index.d.cts +17 -171
- package/dist/index.d.ts +17 -171
- package/dist/index.js +60 -397
- package/package.json +4 -12
- package/src/__snapshots__/bindgen.test.ts.snap +18 -0
- package/src/bindgen.test.ts +85 -0
- package/src/bindgen.ts +21 -33
- package/src/index.ts +6 -37
- package/src/templates/__snapshots__/client.test.ts.snap +35 -0
- package/src/templates/__snapshots__/reactor.test.ts.snap +5 -140
- package/src/templates/client.test.ts +20 -0
- package/src/templates/client.ts +37 -0
- package/src/templates/reactor.test.ts +5 -60
- package/src/templates/reactor.ts +36 -227
- package/src/did.test.ts +0 -102
- package/src/did.ts +0 -85
- package/src/templates/__snapshots__/infiniteQuery.test.ts.snap +0 -44
- package/src/templates/__snapshots__/mutation.test.ts.snap +0 -59
- package/src/templates/__snapshots__/query.test.ts.snap +0 -64
- package/src/templates/infiniteQuery.test.ts +0 -42
- package/src/templates/infiniteQuery.ts +0 -74
- package/src/templates/mutation.test.ts +0 -43
- package/src/templates/mutation.ts +0 -51
- package/src/templates/query.test.ts +0 -44
- package/src/templates/query.ts +0 -71
- package/src/types.ts +0 -78
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 {
|
|
17
|
+
import { didToJs, didToTs } from "@ic-reactor/parser";
|
|
72
18
|
import path from "path";
|
|
73
|
-
import
|
|
19
|
+
import fs from "fs";
|
|
74
20
|
async function generateDeclarations(options) {
|
|
75
21
|
const { didFile, outDir } = options;
|
|
76
|
-
if (!
|
|
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
|
-
|
|
86
|
-
|
|
32
|
+
const didContent = fs.readFileSync(didFile, "utf-8");
|
|
33
|
+
if (!fs.existsSync(outDir)) {
|
|
34
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
87
35
|
}
|
|
88
|
-
if (
|
|
89
|
-
|
|
36
|
+
if (fs.existsSync(declarationsDir)) {
|
|
37
|
+
fs.rmSync(declarationsDir, { recursive: true, force: true });
|
|
90
38
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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}.
|
|
120
|
-
return
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
|
89
|
+
function generateStandardReactorFile(vars) {
|
|
169
90
|
const {
|
|
170
91
|
pascalName,
|
|
171
92
|
reactorName,
|
|
172
93
|
serviceName,
|
|
173
94
|
reactorType,
|
|
174
|
-
|
|
175
|
-
|
|
95
|
+
clientManagerPath,
|
|
96
|
+
declarationsPath,
|
|
97
|
+
canisterName
|
|
176
98
|
} = vars;
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
+
export const queryClient = new QueryClient()`}
|
|
290
132
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.1",
|
|
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.6"
|
|
45
38
|
},
|
|
46
39
|
"devDependencies": {
|
|
47
|
-
"@
|
|
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
|
+
})
|