@ic-reactor/cli 0.0.0-dev
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/README.md +267 -0
- package/dist/index.js +1709 -0
- package/package.json +56 -0
- package/schema.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1709 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
import fs2 from "fs";
|
|
9
|
+
import path2 from "path";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
|
|
12
|
+
// src/utils/config.ts
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
var CONFIG_FILE_NAME = "reactor.config.json";
|
|
16
|
+
var DEFAULT_CONFIG = {
|
|
17
|
+
$schema: "https://raw.githubusercontent.com/B3Pay/ic-reactor/main/packages/cli/schema.json",
|
|
18
|
+
outDir: "src/canisters",
|
|
19
|
+
canisters: {},
|
|
20
|
+
generatedHooks: {}
|
|
21
|
+
};
|
|
22
|
+
function findConfigFile(startDir = process.cwd()) {
|
|
23
|
+
let currentDir = startDir;
|
|
24
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
25
|
+
const configPath = path.join(currentDir, CONFIG_FILE_NAME);
|
|
26
|
+
if (fs.existsSync(configPath)) {
|
|
27
|
+
return configPath;
|
|
28
|
+
}
|
|
29
|
+
currentDir = path.dirname(currentDir);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function loadConfig(configPath) {
|
|
34
|
+
const filePath = configPath ?? findConfigFile();
|
|
35
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
40
|
+
return JSON.parse(content);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function saveConfig(config, configPath = path.join(process.cwd(), CONFIG_FILE_NAME)) {
|
|
46
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
47
|
+
}
|
|
48
|
+
function getProjectRoot() {
|
|
49
|
+
let currentDir = process.cwd();
|
|
50
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
51
|
+
if (fs.existsSync(path.join(currentDir, "package.json"))) {
|
|
52
|
+
return currentDir;
|
|
53
|
+
}
|
|
54
|
+
currentDir = path.dirname(currentDir);
|
|
55
|
+
}
|
|
56
|
+
return process.cwd();
|
|
57
|
+
}
|
|
58
|
+
function ensureDir(dirPath) {
|
|
59
|
+
if (!fs.existsSync(dirPath)) {
|
|
60
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function fileExists(filePath) {
|
|
64
|
+
return fs.existsSync(filePath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/commands/init.ts
|
|
68
|
+
async function initCommand(options) {
|
|
69
|
+
console.log();
|
|
70
|
+
p.intro(pc.cyan("\u{1F527} ic-reactor CLI Setup"));
|
|
71
|
+
const existingConfig = findConfigFile();
|
|
72
|
+
if (existingConfig) {
|
|
73
|
+
const shouldOverwrite = await p.confirm({
|
|
74
|
+
message: `Config file already exists at ${pc.yellow(existingConfig)}. Overwrite?`,
|
|
75
|
+
initialValue: false
|
|
76
|
+
});
|
|
77
|
+
if (p.isCancel(shouldOverwrite) || !shouldOverwrite) {
|
|
78
|
+
p.cancel("Setup cancelled.");
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const projectRoot = getProjectRoot();
|
|
83
|
+
let config;
|
|
84
|
+
if (options.yes) {
|
|
85
|
+
config = { ...DEFAULT_CONFIG };
|
|
86
|
+
if (options.outDir) {
|
|
87
|
+
config.outDir = options.outDir;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
const outDir = await p.text({
|
|
91
|
+
message: "Where should generated hooks be placed?",
|
|
92
|
+
placeholder: "src/canisters",
|
|
93
|
+
defaultValue: "src/canisters",
|
|
94
|
+
validate: (value) => {
|
|
95
|
+
if (!value) return "Output directory is required";
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
if (p.isCancel(outDir)) {
|
|
100
|
+
p.cancel("Setup cancelled.");
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
const addCanister = await p.confirm({
|
|
104
|
+
message: "Would you like to add a canister now?",
|
|
105
|
+
initialValue: true
|
|
106
|
+
});
|
|
107
|
+
if (p.isCancel(addCanister)) {
|
|
108
|
+
p.cancel("Setup cancelled.");
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
config = {
|
|
112
|
+
...DEFAULT_CONFIG,
|
|
113
|
+
outDir
|
|
114
|
+
};
|
|
115
|
+
if (addCanister) {
|
|
116
|
+
const canisterInfo = await promptForCanister(projectRoot);
|
|
117
|
+
if (canisterInfo) {
|
|
118
|
+
config.canisters[canisterInfo.name] = canisterInfo.config;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const configPath = path2.join(projectRoot, CONFIG_FILE_NAME);
|
|
123
|
+
saveConfig(config, configPath);
|
|
124
|
+
const fullOutDir = path2.join(projectRoot, config.outDir);
|
|
125
|
+
ensureDir(fullOutDir);
|
|
126
|
+
const clientManagerPath = path2.join(projectRoot, "src/lib/client.ts");
|
|
127
|
+
if (!fs2.existsSync(clientManagerPath)) {
|
|
128
|
+
const createClient = await p.confirm({
|
|
129
|
+
message: "Create a sample client manager at src/lib/client.ts?",
|
|
130
|
+
initialValue: true
|
|
131
|
+
});
|
|
132
|
+
if (!p.isCancel(createClient) && createClient) {
|
|
133
|
+
ensureDir(path2.dirname(clientManagerPath));
|
|
134
|
+
fs2.writeFileSync(clientManagerPath, getClientManagerTemplate());
|
|
135
|
+
p.log.success(`Created ${pc.green("src/lib/client.ts")}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
p.log.success(`Created ${pc.green(CONFIG_FILE_NAME)}`);
|
|
139
|
+
p.log.success(`Created ${pc.green(config.outDir)} directory`);
|
|
140
|
+
console.log();
|
|
141
|
+
p.note(
|
|
142
|
+
`Next steps:
|
|
143
|
+
|
|
144
|
+
1. ${pc.cyan("Add a canister:")}
|
|
145
|
+
${pc.dim("npx @ic-reactor/cli add")}
|
|
146
|
+
|
|
147
|
+
2. ${pc.cyan("List available methods:")}
|
|
148
|
+
${pc.dim("npx @ic-reactor/cli list -c <canister-name>")}
|
|
149
|
+
|
|
150
|
+
3. ${pc.cyan("Add hooks for specific methods:")}
|
|
151
|
+
${pc.dim("npx @ic-reactor/cli add -c <canister> -m <method>")}`,
|
|
152
|
+
"Getting Started"
|
|
153
|
+
);
|
|
154
|
+
p.outro(pc.green("\u2713 ic-reactor initialized successfully!"));
|
|
155
|
+
}
|
|
156
|
+
async function promptForCanister(projectRoot) {
|
|
157
|
+
const name = await p.text({
|
|
158
|
+
message: "Canister name",
|
|
159
|
+
placeholder: "backend",
|
|
160
|
+
validate: (value) => {
|
|
161
|
+
if (!value) return "Canister name is required";
|
|
162
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value)) {
|
|
163
|
+
return "Canister name must start with a letter and contain only letters, numbers, hyphens, and underscores";
|
|
164
|
+
}
|
|
165
|
+
return void 0;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
if (p.isCancel(name)) return null;
|
|
169
|
+
const didFile = await p.text({
|
|
170
|
+
message: "Path to .did file",
|
|
171
|
+
placeholder: "./backend.did",
|
|
172
|
+
validate: (value) => {
|
|
173
|
+
if (!value) return "DID file path is required";
|
|
174
|
+
const fullPath = path2.resolve(projectRoot, value);
|
|
175
|
+
if (!fs2.existsSync(fullPath)) {
|
|
176
|
+
return `File not found: ${value}`;
|
|
177
|
+
}
|
|
178
|
+
return void 0;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
if (p.isCancel(didFile)) return null;
|
|
182
|
+
const clientManagerPath = await p.text({
|
|
183
|
+
message: "Import path to your client manager (relative from generated hooks)",
|
|
184
|
+
placeholder: "../../lib/client",
|
|
185
|
+
defaultValue: "../../lib/client"
|
|
186
|
+
});
|
|
187
|
+
if (p.isCancel(clientManagerPath)) return null;
|
|
188
|
+
const useDisplayReactor = await p.confirm({
|
|
189
|
+
message: "Use DisplayReactor? (auto-converts bigint \u2192 string, Principal \u2192 string)",
|
|
190
|
+
initialValue: true
|
|
191
|
+
});
|
|
192
|
+
if (p.isCancel(useDisplayReactor)) return null;
|
|
193
|
+
return {
|
|
194
|
+
name,
|
|
195
|
+
config: {
|
|
196
|
+
didFile,
|
|
197
|
+
clientManagerPath,
|
|
198
|
+
useDisplayReactor
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function getClientManagerTemplate() {
|
|
203
|
+
return `/**
|
|
204
|
+
* IC Client Manager
|
|
205
|
+
*
|
|
206
|
+
* This file configures the IC agent and client manager for your application.
|
|
207
|
+
* Customize the agent options based on your environment.
|
|
208
|
+
*/
|
|
209
|
+
|
|
210
|
+
import { ClientManager } from "@ic-reactor/react"
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The client manager handles agent lifecycle and authentication.
|
|
214
|
+
*
|
|
215
|
+
* Configuration options:
|
|
216
|
+
* - host: IC network host (defaults to process env or mainnet)
|
|
217
|
+
* - identity: Initial identity (optional, can be set later)
|
|
218
|
+
* - verifyQuerySignatures: Verify query signatures (recommended for production)
|
|
219
|
+
*
|
|
220
|
+
* For local development, the agent will automatically detect local replica.
|
|
221
|
+
*/
|
|
222
|
+
export const clientManager = new ClientManager({
|
|
223
|
+
// Uncomment for explicit host configuration:
|
|
224
|
+
// host: process.env.DFX_NETWORK === "local"
|
|
225
|
+
// ? "http://localhost:4943"
|
|
226
|
+
// : "https://icp-api.io",
|
|
227
|
+
})
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/commands/add.ts
|
|
232
|
+
import * as p2 from "@clack/prompts";
|
|
233
|
+
import fs4 from "fs";
|
|
234
|
+
import path3 from "path";
|
|
235
|
+
import pc2 from "picocolors";
|
|
236
|
+
|
|
237
|
+
// src/parsers/did.ts
|
|
238
|
+
import fs3 from "fs";
|
|
239
|
+
function parseDIDFile(didFilePath) {
|
|
240
|
+
if (!fs3.existsSync(didFilePath)) {
|
|
241
|
+
throw new Error(`DID file not found: ${didFilePath}`);
|
|
242
|
+
}
|
|
243
|
+
const content = fs3.readFileSync(didFilePath, "utf-8");
|
|
244
|
+
return extractMethods(content);
|
|
245
|
+
}
|
|
246
|
+
function extractMethods(didContent) {
|
|
247
|
+
const methods = [];
|
|
248
|
+
const cleanContent = didContent.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
249
|
+
const methodRegex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(?:func\s*)?\(([^)]*)\)\s*->\s*\(([^)]*)\)\s*(query|composite_query)?/g;
|
|
250
|
+
let match;
|
|
251
|
+
while ((match = methodRegex.exec(cleanContent)) !== null) {
|
|
252
|
+
const name = match[1];
|
|
253
|
+
const args = match[2].trim();
|
|
254
|
+
const returnType = match[3].trim();
|
|
255
|
+
const queryAnnotation = match[4];
|
|
256
|
+
const isQuery = queryAnnotation === "query" || queryAnnotation === "composite_query";
|
|
257
|
+
methods.push({
|
|
258
|
+
name,
|
|
259
|
+
type: isQuery ? "query" : "mutation",
|
|
260
|
+
hasArgs: args.length > 0 && args !== "",
|
|
261
|
+
argsDescription: args || void 0,
|
|
262
|
+
returnDescription: returnType || void 0
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return methods;
|
|
266
|
+
}
|
|
267
|
+
function formatMethodForDisplay(method) {
|
|
268
|
+
const typeLabel = method.type === "query" ? "query" : "update";
|
|
269
|
+
const argsLabel = method.hasArgs ? "with args" : "no args";
|
|
270
|
+
return `${method.name} (${typeLabel}, ${argsLabel})`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/utils/naming.ts
|
|
274
|
+
function toPascalCase(str) {
|
|
275
|
+
return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
276
|
+
}
|
|
277
|
+
function toCamelCase(str) {
|
|
278
|
+
const pascal = toPascalCase(str);
|
|
279
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
280
|
+
}
|
|
281
|
+
function getHookFileName(methodName, hookType) {
|
|
282
|
+
const camelMethod = toCamelCase(methodName);
|
|
283
|
+
const pascalType = toPascalCase(hookType);
|
|
284
|
+
return `${camelMethod}${pascalType}.ts`;
|
|
285
|
+
}
|
|
286
|
+
function getHookExportName(methodName, hookType) {
|
|
287
|
+
const camelMethod = toCamelCase(methodName);
|
|
288
|
+
const pascalType = toPascalCase(hookType);
|
|
289
|
+
return `${camelMethod}${pascalType}`;
|
|
290
|
+
}
|
|
291
|
+
function getReactHookName(methodName, hookType) {
|
|
292
|
+
const pascalMethod = toPascalCase(methodName);
|
|
293
|
+
const pascalType = toPascalCase(hookType);
|
|
294
|
+
return `use${pascalMethod}${pascalType}`;
|
|
295
|
+
}
|
|
296
|
+
function getReactorName(canisterName) {
|
|
297
|
+
return `${toCamelCase(canisterName)}Reactor`;
|
|
298
|
+
}
|
|
299
|
+
function getServiceTypeName(canisterName) {
|
|
300
|
+
return `${toPascalCase(canisterName)}Service`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/generators/reactor.ts
|
|
304
|
+
function generateReactorFile(options) {
|
|
305
|
+
const { canisterName, canisterConfig } = options;
|
|
306
|
+
const pascalName = toPascalCase(canisterName);
|
|
307
|
+
const reactorName = getReactorName(canisterName);
|
|
308
|
+
const serviceName = getServiceTypeName(canisterName);
|
|
309
|
+
const reactorType = canisterConfig.useDisplayReactor !== false ? "DisplayReactor" : "Reactor";
|
|
310
|
+
const clientManagerPath = canisterConfig.clientManagerPath ?? "../../lib/client";
|
|
311
|
+
const declarationsPath = `./declarations/${canisterName}.did`;
|
|
312
|
+
return `/**
|
|
313
|
+
* ${pascalName} Reactor
|
|
314
|
+
*
|
|
315
|
+
* Auto-generated by @ic-reactor/cli
|
|
316
|
+
* This file provides the shared reactor instance for the ${canisterName} canister.
|
|
317
|
+
*
|
|
318
|
+
* You can customize this file to add global configuration.
|
|
319
|
+
*/
|
|
320
|
+
|
|
321
|
+
import { ${reactorType}, createActorHooks, createAuthHooks } from "@ic-reactor/react"
|
|
322
|
+
import { clientManager } from "${clientManagerPath}"
|
|
323
|
+
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
324
|
+
|
|
325
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
326
|
+
// SERVICE TYPE
|
|
327
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
328
|
+
|
|
329
|
+
export type ${serviceName} = _SERVICE
|
|
330
|
+
|
|
331
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
332
|
+
// REACTOR INSTANCE
|
|
333
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* ${pascalName} Reactor with ${canisterConfig.useDisplayReactor !== false ? "Display" : "Candid"} type transformations.
|
|
337
|
+
* ${canisterConfig.useDisplayReactor !== false ? "Automatically converts bigint \u2192 string, Principal \u2192 string, etc." : "Uses raw Candid types."}
|
|
338
|
+
*/
|
|
339
|
+
export const ${reactorName} = new ${reactorType}<${serviceName}>({
|
|
340
|
+
clientManager,
|
|
341
|
+
idlFactory,
|
|
342
|
+
name: "${canisterName}",
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
346
|
+
// BASE HOOKS
|
|
347
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Base actor hooks - use these directly or import method-specific hooks.
|
|
351
|
+
*/
|
|
352
|
+
export const {
|
|
353
|
+
useActorQuery,
|
|
354
|
+
useActorMutation,
|
|
355
|
+
useActorSuspenseQuery,
|
|
356
|
+
useActorInfiniteQuery,
|
|
357
|
+
useActorSuspenseInfiniteQuery,
|
|
358
|
+
useActorMethod,
|
|
359
|
+
} = createActorHooks(${reactorName})
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Auth hooks for the client manager.
|
|
363
|
+
*/
|
|
364
|
+
export const { useAuth, useAgentState, useUserPrincipal } = createAuthHooks(clientManager)
|
|
365
|
+
|
|
366
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
367
|
+
// RE-EXPORTS
|
|
368
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
369
|
+
|
|
370
|
+
export { idlFactory }
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/generators/query.ts
|
|
375
|
+
function generateQueryHook(options) {
|
|
376
|
+
const { canisterName, method } = options;
|
|
377
|
+
const pascalMethod = toPascalCase(method.name);
|
|
378
|
+
const reactorName = getReactorName(canisterName);
|
|
379
|
+
const serviceName = getServiceTypeName(canisterName);
|
|
380
|
+
const hookExportName = getHookExportName(method.name, "query");
|
|
381
|
+
const reactHookName = getReactHookName(method.name, "query");
|
|
382
|
+
if (method.hasArgs) {
|
|
383
|
+
return `/**
|
|
384
|
+
* Query Hook: ${method.name}
|
|
385
|
+
*
|
|
386
|
+
* Auto-generated by @ic-reactor/cli
|
|
387
|
+
* This hook wraps the ${method.name} query method.
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* // With the factory (for dynamic args)
|
|
391
|
+
* const query = ${hookExportName}([arg1, arg2])
|
|
392
|
+
* const { data } = query.useQuery()
|
|
393
|
+
*
|
|
394
|
+
* // Or use the hook directly
|
|
395
|
+
* const { data } = ${reactHookName}([arg1, arg2])
|
|
396
|
+
*/
|
|
397
|
+
|
|
398
|
+
import { createQueryFactory } from "@ic-reactor/react"
|
|
399
|
+
import { ${reactorName}, type ${serviceName} } from "../reactor"
|
|
400
|
+
|
|
401
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
402
|
+
// QUERY FACTORY
|
|
403
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Query factory for ${method.name}
|
|
407
|
+
*
|
|
408
|
+
* Creates a query instance with the provided arguments.
|
|
409
|
+
* Each unique set of args gets its own cached query.
|
|
410
|
+
*/
|
|
411
|
+
export const ${hookExportName} = createQueryFactory(${reactorName}, {
|
|
412
|
+
functionName: "${method.name}",
|
|
413
|
+
// Customize your query options:
|
|
414
|
+
// staleTime: 5 * 60 * 1000,
|
|
415
|
+
// select: (data) => data,
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
419
|
+
// CONVENIENCE HOOK
|
|
420
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* React hook for ${method.name}
|
|
424
|
+
*
|
|
425
|
+
* @param args - Arguments to pass to the canister method
|
|
426
|
+
* @param options - Additional React Query options
|
|
427
|
+
*/
|
|
428
|
+
export function ${reactHookName}(
|
|
429
|
+
args: Parameters<${serviceName}["${method.name}"]>,
|
|
430
|
+
options?: Parameters<ReturnType<typeof ${hookExportName}>["useQuery"]>[0]
|
|
431
|
+
) {
|
|
432
|
+
return ${hookExportName}(args).useQuery(options)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
436
|
+
// UTILITY FUNCTIONS
|
|
437
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Fetch ${method.name} directly (for loaders, server components, etc.)
|
|
441
|
+
*/
|
|
442
|
+
export function fetch${pascalMethod}(args: Parameters<${serviceName}["${method.name}"]>) {
|
|
443
|
+
return ${hookExportName}(args).fetch()
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Invalidate ${method.name} query cache
|
|
448
|
+
*/
|
|
449
|
+
export function invalidate${pascalMethod}(args: Parameters<${serviceName}["${method.name}"]>) {
|
|
450
|
+
return ${hookExportName}(args).invalidate()
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get ${method.name} query key (for cache manipulation)
|
|
455
|
+
*/
|
|
456
|
+
export function get${pascalMethod}QueryKey(args: Parameters<${serviceName}["${method.name}"]>) {
|
|
457
|
+
return ${hookExportName}(args).getQueryKey()
|
|
458
|
+
}
|
|
459
|
+
`;
|
|
460
|
+
} else {
|
|
461
|
+
return `/**
|
|
462
|
+
* Query Hook: ${method.name}
|
|
463
|
+
*
|
|
464
|
+
* Auto-generated by @ic-reactor/cli
|
|
465
|
+
* This hook wraps the ${method.name} query method.
|
|
466
|
+
*
|
|
467
|
+
* @example
|
|
468
|
+
* // Use the hook
|
|
469
|
+
* const { data, isLoading } = ${reactHookName}()
|
|
470
|
+
*
|
|
471
|
+
* // Or access the query object directly
|
|
472
|
+
* const data = await ${hookExportName}.fetch()
|
|
473
|
+
* ${hookExportName}.invalidate()
|
|
474
|
+
*/
|
|
475
|
+
|
|
476
|
+
import { createQuery } from "@ic-reactor/react"
|
|
477
|
+
import { ${reactorName} } from "../reactor"
|
|
478
|
+
|
|
479
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
480
|
+
// QUERY INSTANCE
|
|
481
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Query for ${method.name}
|
|
485
|
+
*
|
|
486
|
+
* Provides:
|
|
487
|
+
* - .useQuery() - React hook
|
|
488
|
+
* - .fetch() - Direct fetch (for loaders)
|
|
489
|
+
* - .invalidate() - Invalidate cache
|
|
490
|
+
* - .getQueryKey() - Get query key
|
|
491
|
+
* - .getCacheData() - Read from cache
|
|
492
|
+
*/
|
|
493
|
+
export const ${hookExportName} = createQuery(${reactorName}, {
|
|
494
|
+
functionName: "${method.name}",
|
|
495
|
+
// Customize your query options:
|
|
496
|
+
// staleTime: 5 * 60 * 1000,
|
|
497
|
+
// select: (data) => data,
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
501
|
+
// CONVENIENCE EXPORTS
|
|
502
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* React hook for ${method.name}
|
|
506
|
+
*/
|
|
507
|
+
export const ${reactHookName} = ${hookExportName}.useQuery
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Fetch ${method.name} directly (for loaders, server components, etc.)
|
|
511
|
+
*/
|
|
512
|
+
export const fetch${pascalMethod} = ${hookExportName}.fetch
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Invalidate ${method.name} query cache
|
|
516
|
+
*/
|
|
517
|
+
export const invalidate${pascalMethod} = ${hookExportName}.invalidate
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Get ${method.name} query key (for cache manipulation)
|
|
521
|
+
*/
|
|
522
|
+
export const get${pascalMethod}QueryKey = ${hookExportName}.getQueryKey
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Get cached data for ${method.name}
|
|
526
|
+
*/
|
|
527
|
+
export const get${pascalMethod}CacheData = ${hookExportName}.getCacheData
|
|
528
|
+
`;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/generators/mutation.ts
|
|
533
|
+
function generateMutationHook(options) {
|
|
534
|
+
const { canisterName, method } = options;
|
|
535
|
+
const pascalMethod = toPascalCase(method.name);
|
|
536
|
+
const reactorName = getReactorName(canisterName);
|
|
537
|
+
const serviceName = getServiceTypeName(canisterName);
|
|
538
|
+
const hookExportName = getHookExportName(method.name, "mutation");
|
|
539
|
+
const reactHookName = getReactHookName(method.name, "mutation");
|
|
540
|
+
return `/**
|
|
541
|
+
* Mutation Hook: ${method.name}
|
|
542
|
+
*
|
|
543
|
+
* Auto-generated by @ic-reactor/cli
|
|
544
|
+
* This hook wraps the ${method.name} update method.
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* // Use the hook in a component
|
|
548
|
+
* const { mutate, isPending } = ${reactHookName}()
|
|
549
|
+
*
|
|
550
|
+
* // Call the mutation
|
|
551
|
+
* mutate(${method.hasArgs ? "[arg1, arg2]" : "[]"})
|
|
552
|
+
*
|
|
553
|
+
* // Or execute directly
|
|
554
|
+
* const result = await execute${pascalMethod}(${method.hasArgs ? "[arg1, arg2]" : "[]"})
|
|
555
|
+
*/
|
|
556
|
+
|
|
557
|
+
import { createMutation } from "@ic-reactor/react"
|
|
558
|
+
import { ${reactorName}, type ${serviceName} } from "../reactor"
|
|
559
|
+
|
|
560
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
561
|
+
// MUTATION INSTANCE
|
|
562
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Mutation for ${method.name}
|
|
566
|
+
*
|
|
567
|
+
* Provides:
|
|
568
|
+
* - .useMutation() - React hook
|
|
569
|
+
* - .execute() - Direct execution
|
|
570
|
+
*/
|
|
571
|
+
export const ${hookExportName} = createMutation(${reactorName}, {
|
|
572
|
+
functionName: "${method.name}",
|
|
573
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
574
|
+
// INVALIDATION
|
|
575
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
576
|
+
// Uncomment and import query keys to auto-invalidate on success:
|
|
577
|
+
// invalidateQueries: [
|
|
578
|
+
// someQuery.getQueryKey(),
|
|
579
|
+
// ],
|
|
580
|
+
|
|
581
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
582
|
+
// SUCCESS HANDLER
|
|
583
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
584
|
+
// onSuccess: (data, variables, context) => {
|
|
585
|
+
// console.log("${method.name} succeeded:", data)
|
|
586
|
+
// },
|
|
587
|
+
|
|
588
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
589
|
+
// ERROR HANDLERS
|
|
590
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
591
|
+
// Handle canister-level errors (from Result { Err })
|
|
592
|
+
// onCanisterError: (error, variables) => {
|
|
593
|
+
// console.error("Canister error:", error)
|
|
594
|
+
// },
|
|
595
|
+
|
|
596
|
+
// Handle all errors (network, canister, etc.)
|
|
597
|
+
// onError: (error, variables, context) => {
|
|
598
|
+
// console.error("Error:", error)
|
|
599
|
+
// },
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
603
|
+
// CONVENIENCE EXPORTS
|
|
604
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* React hook for ${method.name}
|
|
608
|
+
*
|
|
609
|
+
* @param options - Mutation options (onSuccess, onError, etc.)
|
|
610
|
+
*/
|
|
611
|
+
export const ${reactHookName} = ${hookExportName}.useMutation
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Execute ${method.name} directly (outside of React)
|
|
615
|
+
*
|
|
616
|
+
* @param args - Arguments to pass to the canister method
|
|
617
|
+
*/
|
|
618
|
+
export const execute${pascalMethod} = ${hookExportName}.execute
|
|
619
|
+
`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/generators/infiniteQuery.ts
|
|
623
|
+
function generateInfiniteQueryHook(options) {
|
|
624
|
+
const { canisterName, method } = options;
|
|
625
|
+
const pascalMethod = toPascalCase(method.name);
|
|
626
|
+
const camelMethod = toCamelCase(method.name);
|
|
627
|
+
const reactorName = getReactorName(canisterName);
|
|
628
|
+
const serviceName = getServiceTypeName(canisterName);
|
|
629
|
+
return `/**
|
|
630
|
+
* Infinite Query Hook: ${method.name}
|
|
631
|
+
*
|
|
632
|
+
* Auto-generated by @ic-reactor/cli
|
|
633
|
+
* This hook wraps the ${method.name} method for infinite/paginated queries.
|
|
634
|
+
*
|
|
635
|
+
* \u26A0\uFE0F CUSTOMIZATION REQUIRED:
|
|
636
|
+
* You need to configure getArgs and getNextPageParam based on your API.
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* const {
|
|
640
|
+
* data,
|
|
641
|
+
* fetchNextPage,
|
|
642
|
+
* hasNextPage,
|
|
643
|
+
* isFetching,
|
|
644
|
+
* } = ${camelMethod}InfiniteQuery.useInfiniteQuery()
|
|
645
|
+
*
|
|
646
|
+
* // Flatten all pages
|
|
647
|
+
* const allItems = data?.pages.flatMap(page => page.items) ?? []
|
|
648
|
+
*/
|
|
649
|
+
|
|
650
|
+
import { createInfiniteQuery } from "@ic-reactor/react"
|
|
651
|
+
import { ${reactorName}, type ${serviceName} } from "../reactor"
|
|
652
|
+
|
|
653
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
654
|
+
// CURSOR TYPE
|
|
655
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Define your pagination cursor type here.
|
|
659
|
+
* Common patterns:
|
|
660
|
+
* - number (offset-based)
|
|
661
|
+
* - string (cursor-based)
|
|
662
|
+
* - { offset: number; limit: number }
|
|
663
|
+
*/
|
|
664
|
+
type PageCursor = number
|
|
665
|
+
|
|
666
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
667
|
+
// INFINITE QUERY INSTANCE
|
|
668
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Infinite query for ${method.name}
|
|
672
|
+
*
|
|
673
|
+
* Provides:
|
|
674
|
+
* - .useInfiniteQuery() - React hook with pagination
|
|
675
|
+
* - .fetch() - Fetch first page
|
|
676
|
+
* - .invalidate() - Invalidate all pages
|
|
677
|
+
* - .getQueryKey() - Get query key
|
|
678
|
+
* - .getCacheData() - Read from cache
|
|
679
|
+
*/
|
|
680
|
+
export const ${camelMethod}InfiniteQuery = createInfiniteQuery(${reactorName}, {
|
|
681
|
+
functionName: "${method.name}",
|
|
682
|
+
|
|
683
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
684
|
+
// PAGINATION CONFIG (CUSTOMIZE THESE)
|
|
685
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Initial page parameter (e.g., 0 for offset, null for cursor)
|
|
689
|
+
*/
|
|
690
|
+
initialPageParam: 0 as PageCursor,
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Convert page param to method arguments.
|
|
694
|
+
* Customize this based on your canister API.
|
|
695
|
+
*/
|
|
696
|
+
getArgs: (pageParam: PageCursor) => {
|
|
697
|
+
// Example: offset-based pagination
|
|
698
|
+
return [{ offset: pageParam, limit: 10 }] as Parameters<${serviceName}["${method.name}"]>
|
|
699
|
+
|
|
700
|
+
// Example: cursor-based pagination
|
|
701
|
+
// return [{ cursor: pageParam, limit: 10 }] as Parameters<${serviceName}["${method.name}"]>
|
|
702
|
+
},
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Extract next page param from the last page.
|
|
706
|
+
* Return undefined/null when there are no more pages.
|
|
707
|
+
*/
|
|
708
|
+
getNextPageParam: (lastPage, allPages, lastPageParam) => {
|
|
709
|
+
// Example: offset-based - return next offset or undefined if no more
|
|
710
|
+
// const items = lastPage.items ?? []
|
|
711
|
+
// if (items.length < 10) return undefined
|
|
712
|
+
// return lastPageParam + 10
|
|
713
|
+
|
|
714
|
+
// Example: cursor-based
|
|
715
|
+
// return lastPage.nextCursor ?? undefined
|
|
716
|
+
|
|
717
|
+
// Placeholder - customize for your API
|
|
718
|
+
return undefined
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
722
|
+
// OPTIONAL CONFIG
|
|
723
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
724
|
+
|
|
725
|
+
// Bi-directional scrolling (optional)
|
|
726
|
+
// getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
|
|
727
|
+
// return firstPageParam > 0 ? firstPageParam - 10 : undefined
|
|
728
|
+
// },
|
|
729
|
+
|
|
730
|
+
// Max pages to keep in cache (for memory management)
|
|
731
|
+
// maxPages: 10,
|
|
732
|
+
|
|
733
|
+
// How long data stays fresh
|
|
734
|
+
// staleTime: 5 * 60 * 1000,
|
|
735
|
+
|
|
736
|
+
// Transform the data
|
|
737
|
+
// select: (data) => ({
|
|
738
|
+
// pages: data.pages,
|
|
739
|
+
// pageParams: data.pageParams,
|
|
740
|
+
// items: data.pages.flatMap(page => page.items),
|
|
741
|
+
// }),
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
745
|
+
// CONVENIENCE EXPORTS
|
|
746
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* React hook for paginated ${method.name}
|
|
750
|
+
*/
|
|
751
|
+
export const use${pascalMethod}InfiniteQuery = ${camelMethod}InfiniteQuery.useInfiniteQuery
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Fetch first page of ${method.name}
|
|
755
|
+
*/
|
|
756
|
+
export const fetch${pascalMethod}FirstPage = ${camelMethod}InfiniteQuery.fetch
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Invalidate all cached pages
|
|
760
|
+
*/
|
|
761
|
+
export const invalidate${pascalMethod}Pages = ${camelMethod}InfiniteQuery.invalidate
|
|
762
|
+
`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/commands/add.ts
|
|
766
|
+
async function addCommand(options) {
|
|
767
|
+
console.log();
|
|
768
|
+
p2.intro(pc2.cyan("\u{1F527} Add Canister Hooks"));
|
|
769
|
+
const configPath = findConfigFile();
|
|
770
|
+
if (!configPath) {
|
|
771
|
+
p2.log.error(
|
|
772
|
+
`No ${pc2.yellow("reactor.config.json")} found. Run ${pc2.cyan("npx @ic-reactor/cli init")} first.`
|
|
773
|
+
);
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
const config = loadConfig(configPath);
|
|
777
|
+
if (!config) {
|
|
778
|
+
p2.log.error(`Failed to load config from ${pc2.yellow(configPath)}`);
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
const projectRoot = getProjectRoot();
|
|
782
|
+
const canisterNames = Object.keys(config.canisters);
|
|
783
|
+
if (canisterNames.length === 0) {
|
|
784
|
+
p2.log.error(
|
|
785
|
+
`No canisters configured. Add a canister to ${pc2.yellow("reactor.config.json")} first.`
|
|
786
|
+
);
|
|
787
|
+
const addNow = await p2.confirm({
|
|
788
|
+
message: "Would you like to add a canister now?",
|
|
789
|
+
initialValue: true
|
|
790
|
+
});
|
|
791
|
+
if (p2.isCancel(addNow) || !addNow) {
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
const canisterInfo = await promptForNewCanister(projectRoot);
|
|
795
|
+
if (!canisterInfo) {
|
|
796
|
+
p2.cancel("Cancelled.");
|
|
797
|
+
process.exit(0);
|
|
798
|
+
}
|
|
799
|
+
config.canisters[canisterInfo.name] = canisterInfo.config;
|
|
800
|
+
saveConfig(config, configPath);
|
|
801
|
+
canisterNames.push(canisterInfo.name);
|
|
802
|
+
}
|
|
803
|
+
let selectedCanister = options.canister;
|
|
804
|
+
if (!selectedCanister) {
|
|
805
|
+
if (canisterNames.length === 1) {
|
|
806
|
+
selectedCanister = canisterNames[0];
|
|
807
|
+
} else {
|
|
808
|
+
const result = await p2.select({
|
|
809
|
+
message: "Select a canister",
|
|
810
|
+
options: canisterNames.map((name) => ({
|
|
811
|
+
value: name,
|
|
812
|
+
label: name
|
|
813
|
+
}))
|
|
814
|
+
});
|
|
815
|
+
if (p2.isCancel(result)) {
|
|
816
|
+
p2.cancel("Cancelled.");
|
|
817
|
+
process.exit(0);
|
|
818
|
+
}
|
|
819
|
+
selectedCanister = result;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
const canisterConfig = config.canisters[selectedCanister];
|
|
823
|
+
if (!canisterConfig) {
|
|
824
|
+
p2.log.error(`Canister ${pc2.yellow(selectedCanister)} not found in config.`);
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
827
|
+
const didFilePath = path3.resolve(projectRoot, canisterConfig.didFile);
|
|
828
|
+
let methods;
|
|
829
|
+
try {
|
|
830
|
+
methods = parseDIDFile(didFilePath);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
p2.log.error(
|
|
833
|
+
`Failed to parse DID file: ${pc2.yellow(didFilePath)}
|
|
834
|
+
${error.message}`
|
|
835
|
+
);
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
if (methods.length === 0) {
|
|
839
|
+
p2.log.warn(`No methods found in ${pc2.yellow(didFilePath)}`);
|
|
840
|
+
process.exit(0);
|
|
841
|
+
}
|
|
842
|
+
p2.log.info(
|
|
843
|
+
`Found ${pc2.green(methods.length.toString())} methods in ${pc2.dim(selectedCanister)}`
|
|
844
|
+
);
|
|
845
|
+
let selectedMethods;
|
|
846
|
+
if (options.all) {
|
|
847
|
+
selectedMethods = methods;
|
|
848
|
+
} else if (options.methods && options.methods.length > 0) {
|
|
849
|
+
selectedMethods = methods.filter((m) => options.methods.includes(m.name));
|
|
850
|
+
const notFound = options.methods.filter(
|
|
851
|
+
(name) => !methods.some((m) => m.name === name)
|
|
852
|
+
);
|
|
853
|
+
if (notFound.length > 0) {
|
|
854
|
+
p2.log.warn(`Methods not found: ${pc2.yellow(notFound.join(", "))}`);
|
|
855
|
+
}
|
|
856
|
+
} else {
|
|
857
|
+
const alreadyGenerated = config.generatedHooks[selectedCanister] ?? [];
|
|
858
|
+
const result = await p2.multiselect({
|
|
859
|
+
message: "Select methods to add hooks for",
|
|
860
|
+
options: methods.map((method) => {
|
|
861
|
+
const isGenerated = alreadyGenerated.includes(method.name);
|
|
862
|
+
return {
|
|
863
|
+
value: method.name,
|
|
864
|
+
label: formatMethodForDisplay(method),
|
|
865
|
+
hint: isGenerated ? pc2.dim("(already generated)") : void 0
|
|
866
|
+
};
|
|
867
|
+
}),
|
|
868
|
+
required: true
|
|
869
|
+
});
|
|
870
|
+
if (p2.isCancel(result)) {
|
|
871
|
+
p2.cancel("Cancelled.");
|
|
872
|
+
process.exit(0);
|
|
873
|
+
}
|
|
874
|
+
selectedMethods = methods.filter(
|
|
875
|
+
(m) => result.includes(m.name)
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
if (selectedMethods.length === 0) {
|
|
879
|
+
p2.log.warn("No methods selected.");
|
|
880
|
+
process.exit(0);
|
|
881
|
+
}
|
|
882
|
+
const methodsWithHookTypes = [];
|
|
883
|
+
for (const method of selectedMethods) {
|
|
884
|
+
if (method.type === "query") {
|
|
885
|
+
const hookType = await p2.select({
|
|
886
|
+
message: `Hook type for ${pc2.cyan(method.name)}`,
|
|
887
|
+
options: [
|
|
888
|
+
{ value: "query", label: "Query", hint: "Standard query hook" },
|
|
889
|
+
{
|
|
890
|
+
value: "suspenseQuery",
|
|
891
|
+
label: "Suspense Query",
|
|
892
|
+
hint: "For React Suspense"
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
value: "infiniteQuery",
|
|
896
|
+
label: "Infinite Query",
|
|
897
|
+
hint: "Paginated/infinite scroll"
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
value: "suspenseInfiniteQuery",
|
|
901
|
+
label: "Suspense Infinite Query",
|
|
902
|
+
hint: "Paginated with Suspense"
|
|
903
|
+
}
|
|
904
|
+
]
|
|
905
|
+
});
|
|
906
|
+
if (p2.isCancel(hookType)) {
|
|
907
|
+
p2.cancel("Cancelled.");
|
|
908
|
+
process.exit(0);
|
|
909
|
+
}
|
|
910
|
+
methodsWithHookTypes.push({ method, hookType });
|
|
911
|
+
} else {
|
|
912
|
+
methodsWithHookTypes.push({ method, hookType: "mutation" });
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const canisterOutDir = path3.join(projectRoot, config.outDir, selectedCanister);
|
|
916
|
+
const hooksOutDir = path3.join(canisterOutDir, "hooks");
|
|
917
|
+
ensureDir(hooksOutDir);
|
|
918
|
+
const spinner4 = p2.spinner();
|
|
919
|
+
spinner4.start("Generating hooks...");
|
|
920
|
+
const generatedFiles = [];
|
|
921
|
+
const reactorPath = path3.join(canisterOutDir, "reactor.ts");
|
|
922
|
+
if (!fileExists(reactorPath)) {
|
|
923
|
+
const reactorContent = generateReactorFile({
|
|
924
|
+
canisterName: selectedCanister,
|
|
925
|
+
canisterConfig,
|
|
926
|
+
config,
|
|
927
|
+
outDir: canisterOutDir
|
|
928
|
+
});
|
|
929
|
+
fs4.writeFileSync(reactorPath, reactorContent);
|
|
930
|
+
generatedFiles.push("reactor.ts");
|
|
931
|
+
}
|
|
932
|
+
for (const { method, hookType } of methodsWithHookTypes) {
|
|
933
|
+
const fileName = getHookFileName(method.name, hookType);
|
|
934
|
+
const filePath = path3.join(hooksOutDir, fileName);
|
|
935
|
+
let content;
|
|
936
|
+
switch (hookType) {
|
|
937
|
+
case "query":
|
|
938
|
+
case "suspenseQuery":
|
|
939
|
+
content = generateQueryHook({
|
|
940
|
+
canisterName: selectedCanister,
|
|
941
|
+
method,
|
|
942
|
+
config
|
|
943
|
+
});
|
|
944
|
+
break;
|
|
945
|
+
case "infiniteQuery":
|
|
946
|
+
case "suspenseInfiniteQuery":
|
|
947
|
+
content = generateInfiniteQueryHook({
|
|
948
|
+
canisterName: selectedCanister,
|
|
949
|
+
method,
|
|
950
|
+
config
|
|
951
|
+
});
|
|
952
|
+
break;
|
|
953
|
+
case "mutation":
|
|
954
|
+
content = generateMutationHook({
|
|
955
|
+
canisterName: selectedCanister,
|
|
956
|
+
method,
|
|
957
|
+
config
|
|
958
|
+
});
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
fs4.writeFileSync(filePath, content);
|
|
962
|
+
generatedFiles.push(path3.join("hooks", fileName));
|
|
963
|
+
}
|
|
964
|
+
const indexPath = path3.join(hooksOutDir, "index.ts");
|
|
965
|
+
const indexContent = generateIndexFile(methodsWithHookTypes);
|
|
966
|
+
fs4.writeFileSync(indexPath, indexContent);
|
|
967
|
+
generatedFiles.push("hooks/index.ts");
|
|
968
|
+
config.generatedHooks[selectedCanister] = [
|
|
969
|
+
.../* @__PURE__ */ new Set([
|
|
970
|
+
...config.generatedHooks[selectedCanister] ?? [],
|
|
971
|
+
...selectedMethods.map((m) => m.name)
|
|
972
|
+
])
|
|
973
|
+
];
|
|
974
|
+
saveConfig(config, configPath);
|
|
975
|
+
spinner4.stop("Hooks generated!");
|
|
976
|
+
console.log();
|
|
977
|
+
p2.note(
|
|
978
|
+
generatedFiles.map((f) => pc2.green(`\u2713 ${f}`)).join("\n"),
|
|
979
|
+
`Generated in ${pc2.dim(path3.relative(projectRoot, canisterOutDir))}`
|
|
980
|
+
);
|
|
981
|
+
p2.outro(pc2.green("\u2713 Done!"));
|
|
982
|
+
}
|
|
983
|
+
async function promptForNewCanister(projectRoot) {
|
|
984
|
+
const name = await p2.text({
|
|
985
|
+
message: "Canister name",
|
|
986
|
+
placeholder: "backend",
|
|
987
|
+
validate: (value) => {
|
|
988
|
+
if (!value) return "Canister name is required";
|
|
989
|
+
return void 0;
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
if (p2.isCancel(name)) return null;
|
|
993
|
+
const didFile = await p2.text({
|
|
994
|
+
message: "Path to .did file",
|
|
995
|
+
placeholder: "./backend.did",
|
|
996
|
+
validate: (value) => {
|
|
997
|
+
if (!value) return "DID file path is required";
|
|
998
|
+
const fullPath = path3.resolve(projectRoot, value);
|
|
999
|
+
if (!fs4.existsSync(fullPath)) {
|
|
1000
|
+
return `File not found: ${value}`;
|
|
1001
|
+
}
|
|
1002
|
+
return void 0;
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
if (p2.isCancel(didFile)) return null;
|
|
1006
|
+
return {
|
|
1007
|
+
name,
|
|
1008
|
+
config: {
|
|
1009
|
+
didFile,
|
|
1010
|
+
clientManagerPath: "../../lib/client",
|
|
1011
|
+
useDisplayReactor: true
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function generateIndexFile(methods) {
|
|
1016
|
+
const exports = methods.map(({ method, hookType }) => {
|
|
1017
|
+
const fileName = getHookFileName(method.name, hookType).replace(".ts", "");
|
|
1018
|
+
return `export * from "./${fileName}"`;
|
|
1019
|
+
});
|
|
1020
|
+
return `/**
|
|
1021
|
+
* Hook barrel exports
|
|
1022
|
+
*
|
|
1023
|
+
* Auto-generated by @ic-reactor/cli
|
|
1024
|
+
*/
|
|
1025
|
+
|
|
1026
|
+
${exports.join("\n")}
|
|
1027
|
+
`;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/commands/sync.ts
|
|
1031
|
+
import * as p3 from "@clack/prompts";
|
|
1032
|
+
import fs5 from "fs";
|
|
1033
|
+
import path4 from "path";
|
|
1034
|
+
import pc3 from "picocolors";
|
|
1035
|
+
async function syncCommand(options) {
|
|
1036
|
+
console.log();
|
|
1037
|
+
p3.intro(pc3.cyan("\u{1F504} Sync Canister Hooks"));
|
|
1038
|
+
const configPath = findConfigFile();
|
|
1039
|
+
if (!configPath) {
|
|
1040
|
+
p3.log.error(
|
|
1041
|
+
`No ${pc3.yellow("reactor.config.json")} found. Run ${pc3.cyan("npx @ic-reactor/cli init")} first.`
|
|
1042
|
+
);
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
}
|
|
1045
|
+
const config = loadConfig(configPath);
|
|
1046
|
+
if (!config) {
|
|
1047
|
+
p3.log.error(`Failed to load config from ${pc3.yellow(configPath)}`);
|
|
1048
|
+
process.exit(1);
|
|
1049
|
+
}
|
|
1050
|
+
const projectRoot = getProjectRoot();
|
|
1051
|
+
const canisterNames = Object.keys(config.canisters);
|
|
1052
|
+
if (canisterNames.length === 0) {
|
|
1053
|
+
p3.log.error("No canisters configured.");
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
let canistersToSync;
|
|
1057
|
+
if (options.canister) {
|
|
1058
|
+
if (!config.canisters[options.canister]) {
|
|
1059
|
+
p3.log.error(
|
|
1060
|
+
`Canister ${pc3.yellow(options.canister)} not found in config.`
|
|
1061
|
+
);
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
}
|
|
1064
|
+
canistersToSync = [options.canister];
|
|
1065
|
+
} else {
|
|
1066
|
+
canistersToSync = canisterNames.filter(
|
|
1067
|
+
(name) => (config.generatedHooks[name]?.length ?? 0) > 0
|
|
1068
|
+
);
|
|
1069
|
+
if (canistersToSync.length === 0) {
|
|
1070
|
+
p3.log.warn("No hooks have been generated yet. Run `add` first.");
|
|
1071
|
+
process.exit(0);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const spinner4 = p3.spinner();
|
|
1075
|
+
spinner4.start("Syncing hooks...");
|
|
1076
|
+
let totalUpdated = 0;
|
|
1077
|
+
let totalSkipped = 0;
|
|
1078
|
+
const errors = [];
|
|
1079
|
+
for (const canisterName of canistersToSync) {
|
|
1080
|
+
const canisterConfig = config.canisters[canisterName];
|
|
1081
|
+
const generatedMethods = config.generatedHooks[canisterName] ?? [];
|
|
1082
|
+
if (generatedMethods.length === 0) {
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
const didFilePath = path4.resolve(projectRoot, canisterConfig.didFile);
|
|
1086
|
+
let methods;
|
|
1087
|
+
try {
|
|
1088
|
+
methods = parseDIDFile(didFilePath);
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
errors.push(
|
|
1091
|
+
`${canisterName}: Failed to parse DID file - ${error.message}`
|
|
1092
|
+
);
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
const currentMethodNames = methods.map((m) => m.name);
|
|
1096
|
+
const removedMethods = generatedMethods.filter(
|
|
1097
|
+
(name) => !currentMethodNames.includes(name)
|
|
1098
|
+
);
|
|
1099
|
+
if (removedMethods.length > 0) {
|
|
1100
|
+
p3.log.warn(
|
|
1101
|
+
`${canisterName}: Methods removed from DID: ${pc3.yellow(removedMethods.join(", "))}`
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
const newMethods = methods.filter((m) => !generatedMethods.includes(m.name));
|
|
1105
|
+
if (newMethods.length > 0) {
|
|
1106
|
+
p3.log.info(
|
|
1107
|
+
`${canisterName}: New methods available: ${pc3.cyan(newMethods.map((m) => m.name).join(", "))}`
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
const canisterOutDir = path4.join(projectRoot, config.outDir, canisterName);
|
|
1111
|
+
const reactorPath = path4.join(canisterOutDir, "reactor.ts");
|
|
1112
|
+
const reactorContent = generateReactorFile({
|
|
1113
|
+
canisterName,
|
|
1114
|
+
canisterConfig,
|
|
1115
|
+
config,
|
|
1116
|
+
outDir: canisterOutDir
|
|
1117
|
+
});
|
|
1118
|
+
fs5.writeFileSync(reactorPath, reactorContent);
|
|
1119
|
+
totalUpdated++;
|
|
1120
|
+
const hooksOutDir = path4.join(canisterOutDir, "hooks");
|
|
1121
|
+
ensureDir(hooksOutDir);
|
|
1122
|
+
for (const methodName of generatedMethods) {
|
|
1123
|
+
const method = methods.find((m) => m.name === methodName);
|
|
1124
|
+
if (!method) {
|
|
1125
|
+
totalSkipped++;
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const queryFileName = getHookFileName(methodName, "query");
|
|
1129
|
+
const mutationFileName = getHookFileName(methodName, "mutation");
|
|
1130
|
+
const infiniteQueryFileName = getHookFileName(methodName, "infiniteQuery");
|
|
1131
|
+
let content;
|
|
1132
|
+
let fileName;
|
|
1133
|
+
if (fs5.existsSync(path4.join(hooksOutDir, infiniteQueryFileName))) {
|
|
1134
|
+
fileName = infiniteQueryFileName;
|
|
1135
|
+
totalSkipped++;
|
|
1136
|
+
continue;
|
|
1137
|
+
} else if (method.type === "query") {
|
|
1138
|
+
fileName = queryFileName;
|
|
1139
|
+
content = generateQueryHook({
|
|
1140
|
+
canisterName,
|
|
1141
|
+
method,
|
|
1142
|
+
config
|
|
1143
|
+
});
|
|
1144
|
+
} else {
|
|
1145
|
+
fileName = mutationFileName;
|
|
1146
|
+
content = generateMutationHook({
|
|
1147
|
+
canisterName,
|
|
1148
|
+
method,
|
|
1149
|
+
config
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
fs5.writeFileSync(path4.join(hooksOutDir, fileName), content);
|
|
1153
|
+
totalUpdated++;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
spinner4.stop("Sync complete!");
|
|
1157
|
+
if (errors.length > 0) {
|
|
1158
|
+
console.log();
|
|
1159
|
+
p3.log.error("Errors encountered:");
|
|
1160
|
+
for (const error of errors) {
|
|
1161
|
+
console.log(` ${pc3.red("\u2022")} ${error}`);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
console.log();
|
|
1165
|
+
p3.note(
|
|
1166
|
+
`Updated: ${pc3.green(totalUpdated.toString())} files
|
|
1167
|
+
Skipped: ${pc3.dim(totalSkipped.toString())} files (preserved customizations)`,
|
|
1168
|
+
"Summary"
|
|
1169
|
+
);
|
|
1170
|
+
p3.outro(pc3.green("\u2713 Sync complete!"));
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/commands/list.ts
|
|
1174
|
+
import * as p4 from "@clack/prompts";
|
|
1175
|
+
import path5 from "path";
|
|
1176
|
+
import pc4 from "picocolors";
|
|
1177
|
+
async function listCommand(options) {
|
|
1178
|
+
console.log();
|
|
1179
|
+
p4.intro(pc4.cyan("\u{1F4CB} List Canister Methods"));
|
|
1180
|
+
const configPath = findConfigFile();
|
|
1181
|
+
if (!configPath) {
|
|
1182
|
+
p4.log.error(
|
|
1183
|
+
`No ${pc4.yellow("reactor.config.json")} found. Run ${pc4.cyan("npx @ic-reactor/cli init")} first.`
|
|
1184
|
+
);
|
|
1185
|
+
process.exit(1);
|
|
1186
|
+
}
|
|
1187
|
+
const config = loadConfig(configPath);
|
|
1188
|
+
if (!config) {
|
|
1189
|
+
p4.log.error(`Failed to load config from ${pc4.yellow(configPath)}`);
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
}
|
|
1192
|
+
const projectRoot = getProjectRoot();
|
|
1193
|
+
const canisterNames = Object.keys(config.canisters);
|
|
1194
|
+
if (canisterNames.length === 0) {
|
|
1195
|
+
p4.log.error(
|
|
1196
|
+
`No canisters configured. Add a canister to ${pc4.yellow("reactor.config.json")} first.`
|
|
1197
|
+
);
|
|
1198
|
+
process.exit(1);
|
|
1199
|
+
}
|
|
1200
|
+
let selectedCanister = options.canister;
|
|
1201
|
+
if (!selectedCanister) {
|
|
1202
|
+
if (canisterNames.length === 1) {
|
|
1203
|
+
selectedCanister = canisterNames[0];
|
|
1204
|
+
} else {
|
|
1205
|
+
const result = await p4.select({
|
|
1206
|
+
message: "Select a canister",
|
|
1207
|
+
options: canisterNames.map((name) => ({
|
|
1208
|
+
value: name,
|
|
1209
|
+
label: name
|
|
1210
|
+
}))
|
|
1211
|
+
});
|
|
1212
|
+
if (p4.isCancel(result)) {
|
|
1213
|
+
p4.cancel("Cancelled.");
|
|
1214
|
+
process.exit(0);
|
|
1215
|
+
}
|
|
1216
|
+
selectedCanister = result;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const canisterConfig = config.canisters[selectedCanister];
|
|
1220
|
+
if (!canisterConfig) {
|
|
1221
|
+
p4.log.error(`Canister ${pc4.yellow(selectedCanister)} not found in config.`);
|
|
1222
|
+
process.exit(1);
|
|
1223
|
+
}
|
|
1224
|
+
const didFilePath = path5.resolve(projectRoot, canisterConfig.didFile);
|
|
1225
|
+
try {
|
|
1226
|
+
const methods = parseDIDFile(didFilePath);
|
|
1227
|
+
if (methods.length === 0) {
|
|
1228
|
+
p4.log.warn(`No methods found in ${pc4.yellow(didFilePath)}`);
|
|
1229
|
+
process.exit(0);
|
|
1230
|
+
}
|
|
1231
|
+
const queries = methods.filter((m) => m.type === "query");
|
|
1232
|
+
const mutations = methods.filter((m) => m.type === "mutation");
|
|
1233
|
+
const generatedMethods = config.generatedHooks[selectedCanister] ?? [];
|
|
1234
|
+
if (queries.length > 0) {
|
|
1235
|
+
console.log();
|
|
1236
|
+
console.log(pc4.bold(pc4.cyan(" Queries:")));
|
|
1237
|
+
for (const method of queries) {
|
|
1238
|
+
const isGenerated = generatedMethods.includes(method.name);
|
|
1239
|
+
const status = isGenerated ? pc4.green("\u2713") : pc4.dim("\u25CB");
|
|
1240
|
+
const argsHint = method.hasArgs ? pc4.dim("(args)") : pc4.dim("()");
|
|
1241
|
+
console.log(` ${status} ${method.name} ${argsHint}`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (mutations.length > 0) {
|
|
1245
|
+
console.log();
|
|
1246
|
+
console.log(pc4.bold(pc4.yellow(" Mutations (Updates):")));
|
|
1247
|
+
for (const method of mutations) {
|
|
1248
|
+
const isGenerated = generatedMethods.includes(method.name);
|
|
1249
|
+
const status = isGenerated ? pc4.green("\u2713") : pc4.dim("\u25CB");
|
|
1250
|
+
const argsHint = method.hasArgs ? pc4.dim("(args)") : pc4.dim("()");
|
|
1251
|
+
console.log(` ${status} ${method.name} ${argsHint}`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
console.log();
|
|
1255
|
+
const generatedCount = generatedMethods.length;
|
|
1256
|
+
const totalCount = methods.length;
|
|
1257
|
+
p4.note(
|
|
1258
|
+
`Total: ${pc4.bold(totalCount.toString())} methods
|
|
1259
|
+
Generated: ${pc4.green(generatedCount.toString())} / ${totalCount}
|
|
1260
|
+
|
|
1261
|
+
${pc4.green("\u2713")} = hook generated
|
|
1262
|
+
${pc4.dim("\u25CB")} = not yet generated`,
|
|
1263
|
+
selectedCanister
|
|
1264
|
+
);
|
|
1265
|
+
if (generatedCount < totalCount) {
|
|
1266
|
+
console.log();
|
|
1267
|
+
console.log(
|
|
1268
|
+
pc4.dim(
|
|
1269
|
+
` Run ${pc4.cyan(`npx @ic-reactor/cli add -c ${selectedCanister}`)} to add hooks`
|
|
1270
|
+
)
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
p4.log.error(
|
|
1275
|
+
`Failed to parse DID file: ${pc4.yellow(didFilePath)}
|
|
1276
|
+
${error.message}`
|
|
1277
|
+
);
|
|
1278
|
+
process.exit(1);
|
|
1279
|
+
}
|
|
1280
|
+
console.log();
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/commands/fetch.ts
|
|
1284
|
+
import * as p5 from "@clack/prompts";
|
|
1285
|
+
import fs6 from "fs";
|
|
1286
|
+
import path6 from "path";
|
|
1287
|
+
import pc5 from "picocolors";
|
|
1288
|
+
|
|
1289
|
+
// src/utils/network.ts
|
|
1290
|
+
import { HttpAgent, Actor } from "@icp-sdk/core/agent";
|
|
1291
|
+
import { Principal } from "@icp-sdk/core/principal";
|
|
1292
|
+
import { IDL } from "@icp-sdk/core/candid";
|
|
1293
|
+
var IC_HOST = "https://icp-api.io";
|
|
1294
|
+
var LOCAL_HOST = "http://127.0.0.1:4943";
|
|
1295
|
+
async function fetchCandidFromCanister(options) {
|
|
1296
|
+
const { canisterId, network = "ic", host } = options;
|
|
1297
|
+
let principal;
|
|
1298
|
+
try {
|
|
1299
|
+
principal = Principal.fromText(canisterId);
|
|
1300
|
+
} catch {
|
|
1301
|
+
throw new Error(`Invalid canister ID: ${canisterId}`);
|
|
1302
|
+
}
|
|
1303
|
+
const agentHost = host ?? (network === "local" ? LOCAL_HOST : IC_HOST);
|
|
1304
|
+
const agent = await HttpAgent.create({
|
|
1305
|
+
host: agentHost,
|
|
1306
|
+
// Don't verify signatures for CLI queries
|
|
1307
|
+
verifyQuerySignatures: false
|
|
1308
|
+
});
|
|
1309
|
+
if (network === "local") {
|
|
1310
|
+
try {
|
|
1311
|
+
await agent.fetchRootKey();
|
|
1312
|
+
} catch {
|
|
1313
|
+
throw new Error(
|
|
1314
|
+
`Failed to connect to local replica at ${agentHost}. Is it running?`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const candidInterface = IDL.Service({
|
|
1319
|
+
__get_candid_interface_tmp_hack: IDL.Func([], [IDL.Text], ["query"])
|
|
1320
|
+
});
|
|
1321
|
+
const actor = Actor.createActor(() => candidInterface, {
|
|
1322
|
+
agent,
|
|
1323
|
+
canisterId: principal
|
|
1324
|
+
});
|
|
1325
|
+
try {
|
|
1326
|
+
const candidSource = await actor.__get_candid_interface_tmp_hack();
|
|
1327
|
+
if (!candidSource || candidSource.trim() === "") {
|
|
1328
|
+
throw new Error("Canister returned empty Candid interface");
|
|
1329
|
+
}
|
|
1330
|
+
const methods = extractMethods(candidSource);
|
|
1331
|
+
return {
|
|
1332
|
+
candidSource,
|
|
1333
|
+
methods,
|
|
1334
|
+
canisterId,
|
|
1335
|
+
network
|
|
1336
|
+
};
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1339
|
+
if (message.includes("Replica Error")) {
|
|
1340
|
+
throw new Error(
|
|
1341
|
+
`Canister ${canisterId} does not expose a Candid interface. The canister may not support the __get_candid_interface_tmp_hack method.`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
if (message.includes("not found") || message.includes("404")) {
|
|
1345
|
+
throw new Error(`Canister ${canisterId} not found on ${network} network.`);
|
|
1346
|
+
}
|
|
1347
|
+
throw new Error(`Failed to fetch Candid from canister: ${message}`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function isValidCanisterId(canisterId) {
|
|
1351
|
+
try {
|
|
1352
|
+
Principal.fromText(canisterId);
|
|
1353
|
+
return true;
|
|
1354
|
+
} catch {
|
|
1355
|
+
return false;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function shortenCanisterId(canisterId) {
|
|
1359
|
+
if (canisterId.length <= 15) return canisterId;
|
|
1360
|
+
return `${canisterId.slice(0, 5)}...${canisterId.slice(-5)}`;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/commands/fetch.ts
|
|
1364
|
+
async function fetchCommand(options) {
|
|
1365
|
+
console.log();
|
|
1366
|
+
p5.intro(pc5.cyan("\u{1F310} Fetch from Live Canister"));
|
|
1367
|
+
const projectRoot = getProjectRoot();
|
|
1368
|
+
let canisterId = options.canisterId;
|
|
1369
|
+
if (!canisterId) {
|
|
1370
|
+
const input = await p5.text({
|
|
1371
|
+
message: "Enter canister ID",
|
|
1372
|
+
placeholder: "ryjl3-tyaaa-aaaaa-aaaba-cai",
|
|
1373
|
+
validate: (value) => {
|
|
1374
|
+
if (!value) return "Canister ID is required";
|
|
1375
|
+
if (!isValidCanisterId(value)) {
|
|
1376
|
+
return "Invalid canister ID format";
|
|
1377
|
+
}
|
|
1378
|
+
return void 0;
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
if (p5.isCancel(input)) {
|
|
1382
|
+
p5.cancel("Cancelled.");
|
|
1383
|
+
process.exit(0);
|
|
1384
|
+
}
|
|
1385
|
+
canisterId = input;
|
|
1386
|
+
}
|
|
1387
|
+
if (!isValidCanisterId(canisterId)) {
|
|
1388
|
+
p5.log.error(`Invalid canister ID: ${pc5.yellow(canisterId)}`);
|
|
1389
|
+
process.exit(1);
|
|
1390
|
+
}
|
|
1391
|
+
let network = options.network;
|
|
1392
|
+
if (!network) {
|
|
1393
|
+
const result = await p5.select({
|
|
1394
|
+
message: "Select network",
|
|
1395
|
+
options: [
|
|
1396
|
+
{ value: "ic", label: "IC Mainnet", hint: "icp-api.io" },
|
|
1397
|
+
{ value: "local", label: "Local Replica", hint: "localhost:4943" }
|
|
1398
|
+
]
|
|
1399
|
+
});
|
|
1400
|
+
if (p5.isCancel(result)) {
|
|
1401
|
+
p5.cancel("Cancelled.");
|
|
1402
|
+
process.exit(0);
|
|
1403
|
+
}
|
|
1404
|
+
network = result;
|
|
1405
|
+
}
|
|
1406
|
+
const spinner4 = p5.spinner();
|
|
1407
|
+
spinner4.start(`Fetching Candid from ${shortenCanisterId(canisterId)}...`);
|
|
1408
|
+
let candidResult;
|
|
1409
|
+
try {
|
|
1410
|
+
candidResult = await fetchCandidFromCanister({
|
|
1411
|
+
canisterId,
|
|
1412
|
+
network
|
|
1413
|
+
});
|
|
1414
|
+
spinner4.stop(
|
|
1415
|
+
`Found ${pc5.green(candidResult.methods.length.toString())} methods`
|
|
1416
|
+
);
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
spinner4.stop("Failed to fetch Candid");
|
|
1419
|
+
p5.log.error(error.message);
|
|
1420
|
+
process.exit(1);
|
|
1421
|
+
}
|
|
1422
|
+
const { methods, candidSource } = candidResult;
|
|
1423
|
+
if (methods.length === 0) {
|
|
1424
|
+
p5.log.warn("No methods found in canister interface");
|
|
1425
|
+
process.exit(0);
|
|
1426
|
+
}
|
|
1427
|
+
let canisterName = options.name;
|
|
1428
|
+
if (!canisterName) {
|
|
1429
|
+
const input = await p5.text({
|
|
1430
|
+
message: "Name for this canister (used in generated code)",
|
|
1431
|
+
placeholder: toCamelCase(canisterId.split("-")[0]),
|
|
1432
|
+
defaultValue: toCamelCase(canisterId.split("-")[0]),
|
|
1433
|
+
validate: (value) => {
|
|
1434
|
+
if (!value) return "Name is required";
|
|
1435
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value)) {
|
|
1436
|
+
return "Name must start with a letter and contain only letters, numbers, hyphens, and underscores";
|
|
1437
|
+
}
|
|
1438
|
+
return void 0;
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
if (p5.isCancel(input)) {
|
|
1442
|
+
p5.cancel("Cancelled.");
|
|
1443
|
+
process.exit(0);
|
|
1444
|
+
}
|
|
1445
|
+
canisterName = input;
|
|
1446
|
+
}
|
|
1447
|
+
let selectedMethods;
|
|
1448
|
+
if (options.all) {
|
|
1449
|
+
selectedMethods = methods;
|
|
1450
|
+
} else if (options.methods && options.methods.length > 0) {
|
|
1451
|
+
selectedMethods = methods.filter((m) => options.methods.includes(m.name));
|
|
1452
|
+
const notFound = options.methods.filter(
|
|
1453
|
+
(name) => !methods.some((m) => m.name === name)
|
|
1454
|
+
);
|
|
1455
|
+
if (notFound.length > 0) {
|
|
1456
|
+
p5.log.warn(`Methods not found: ${pc5.yellow(notFound.join(", "))}`);
|
|
1457
|
+
}
|
|
1458
|
+
} else {
|
|
1459
|
+
const result = await p5.multiselect({
|
|
1460
|
+
message: "Select methods to generate hooks for",
|
|
1461
|
+
options: methods.map((method) => ({
|
|
1462
|
+
value: method.name,
|
|
1463
|
+
label: formatMethodForDisplay(method)
|
|
1464
|
+
})),
|
|
1465
|
+
required: true
|
|
1466
|
+
});
|
|
1467
|
+
if (p5.isCancel(result)) {
|
|
1468
|
+
p5.cancel("Cancelled.");
|
|
1469
|
+
process.exit(0);
|
|
1470
|
+
}
|
|
1471
|
+
selectedMethods = methods.filter(
|
|
1472
|
+
(m) => result.includes(m.name)
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
if (selectedMethods.length === 0) {
|
|
1476
|
+
p5.log.warn("No methods selected.");
|
|
1477
|
+
process.exit(0);
|
|
1478
|
+
}
|
|
1479
|
+
const methodsWithHookTypes = [];
|
|
1480
|
+
for (const method of selectedMethods) {
|
|
1481
|
+
if (method.type === "query") {
|
|
1482
|
+
const hookType = await p5.select({
|
|
1483
|
+
message: `Hook type for ${pc5.cyan(method.name)}`,
|
|
1484
|
+
options: [
|
|
1485
|
+
{ value: "query", label: "Query", hint: "Standard query hook" },
|
|
1486
|
+
{
|
|
1487
|
+
value: "suspenseQuery",
|
|
1488
|
+
label: "Suspense Query",
|
|
1489
|
+
hint: "For React Suspense"
|
|
1490
|
+
},
|
|
1491
|
+
{
|
|
1492
|
+
value: "infiniteQuery",
|
|
1493
|
+
label: "Infinite Query",
|
|
1494
|
+
hint: "Paginated/infinite scroll"
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
value: "suspenseInfiniteQuery",
|
|
1498
|
+
label: "Suspense Infinite Query",
|
|
1499
|
+
hint: "Paginated with Suspense"
|
|
1500
|
+
}
|
|
1501
|
+
]
|
|
1502
|
+
});
|
|
1503
|
+
if (p5.isCancel(hookType)) {
|
|
1504
|
+
p5.cancel("Cancelled.");
|
|
1505
|
+
process.exit(0);
|
|
1506
|
+
}
|
|
1507
|
+
methodsWithHookTypes.push({ method, hookType });
|
|
1508
|
+
} else {
|
|
1509
|
+
methodsWithHookTypes.push({ method, hookType: "mutation" });
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
let configPath = findConfigFile();
|
|
1513
|
+
let config;
|
|
1514
|
+
if (!configPath) {
|
|
1515
|
+
configPath = path6.join(projectRoot, CONFIG_FILE_NAME);
|
|
1516
|
+
config = { ...DEFAULT_CONFIG };
|
|
1517
|
+
p5.log.info(`Creating ${pc5.yellow(CONFIG_FILE_NAME)}`);
|
|
1518
|
+
} else {
|
|
1519
|
+
config = loadConfig(configPath) ?? { ...DEFAULT_CONFIG };
|
|
1520
|
+
}
|
|
1521
|
+
const canisterConfig = {
|
|
1522
|
+
didFile: `./candid/${canisterName}.did`,
|
|
1523
|
+
clientManagerPath: "../../lib/client",
|
|
1524
|
+
useDisplayReactor: true,
|
|
1525
|
+
canisterId
|
|
1526
|
+
};
|
|
1527
|
+
config.canisters[canisterName] = canisterConfig;
|
|
1528
|
+
const canisterOutDir = path6.join(projectRoot, config.outDir, canisterName);
|
|
1529
|
+
const hooksOutDir = path6.join(canisterOutDir, "hooks");
|
|
1530
|
+
const candidDir = path6.join(projectRoot, "candid");
|
|
1531
|
+
ensureDir(hooksOutDir);
|
|
1532
|
+
ensureDir(candidDir);
|
|
1533
|
+
const genSpinner = p5.spinner();
|
|
1534
|
+
genSpinner.start("Generating hooks...");
|
|
1535
|
+
const generatedFiles = [];
|
|
1536
|
+
const candidPath = path6.join(candidDir, `${canisterName}.did`);
|
|
1537
|
+
fs6.writeFileSync(candidPath, candidSource);
|
|
1538
|
+
generatedFiles.push(`candid/${canisterName}.did`);
|
|
1539
|
+
const reactorPath = path6.join(canisterOutDir, "reactor.ts");
|
|
1540
|
+
const reactorContent = generateReactorFileForFetch({
|
|
1541
|
+
canisterName,
|
|
1542
|
+
canisterConfig,
|
|
1543
|
+
canisterId
|
|
1544
|
+
});
|
|
1545
|
+
fs6.writeFileSync(reactorPath, reactorContent);
|
|
1546
|
+
generatedFiles.push("reactor.ts");
|
|
1547
|
+
for (const { method, hookType } of methodsWithHookTypes) {
|
|
1548
|
+
const fileName = getHookFileName(method.name, hookType);
|
|
1549
|
+
const filePath = path6.join(hooksOutDir, fileName);
|
|
1550
|
+
let content;
|
|
1551
|
+
switch (hookType) {
|
|
1552
|
+
case "query":
|
|
1553
|
+
case "suspenseQuery":
|
|
1554
|
+
content = generateQueryHook({
|
|
1555
|
+
canisterName,
|
|
1556
|
+
method,
|
|
1557
|
+
config
|
|
1558
|
+
});
|
|
1559
|
+
break;
|
|
1560
|
+
case "infiniteQuery":
|
|
1561
|
+
case "suspenseInfiniteQuery":
|
|
1562
|
+
content = generateInfiniteQueryHook({
|
|
1563
|
+
canisterName,
|
|
1564
|
+
method,
|
|
1565
|
+
config
|
|
1566
|
+
});
|
|
1567
|
+
break;
|
|
1568
|
+
case "mutation":
|
|
1569
|
+
content = generateMutationHook({
|
|
1570
|
+
canisterName,
|
|
1571
|
+
method,
|
|
1572
|
+
config
|
|
1573
|
+
});
|
|
1574
|
+
break;
|
|
1575
|
+
}
|
|
1576
|
+
fs6.writeFileSync(filePath, content);
|
|
1577
|
+
generatedFiles.push(path6.join("hooks", fileName));
|
|
1578
|
+
}
|
|
1579
|
+
const indexPath = path6.join(hooksOutDir, "index.ts");
|
|
1580
|
+
const indexContent = generateIndexFile2(methodsWithHookTypes);
|
|
1581
|
+
fs6.writeFileSync(indexPath, indexContent);
|
|
1582
|
+
generatedFiles.push("hooks/index.ts");
|
|
1583
|
+
config.generatedHooks[canisterName] = [
|
|
1584
|
+
.../* @__PURE__ */ new Set([
|
|
1585
|
+
...config.generatedHooks[canisterName] ?? [],
|
|
1586
|
+
...selectedMethods.map((m) => m.name)
|
|
1587
|
+
])
|
|
1588
|
+
];
|
|
1589
|
+
saveConfig(config, configPath);
|
|
1590
|
+
genSpinner.stop("Hooks generated!");
|
|
1591
|
+
console.log();
|
|
1592
|
+
p5.note(
|
|
1593
|
+
generatedFiles.map((f) => pc5.green(`\u2713 ${f}`)).join("\n"),
|
|
1594
|
+
`Generated in ${pc5.dim(path6.relative(projectRoot, canisterOutDir))}`
|
|
1595
|
+
);
|
|
1596
|
+
console.log();
|
|
1597
|
+
p5.note(
|
|
1598
|
+
`Canister ID: ${pc5.cyan(canisterId)}
|
|
1599
|
+
Network: ${pc5.yellow(network)}
|
|
1600
|
+
Name: ${pc5.green(canisterName)}
|
|
1601
|
+
Methods: ${pc5.dim(selectedMethods.map((m) => m.name).join(", "))}`,
|
|
1602
|
+
"Canister Info"
|
|
1603
|
+
);
|
|
1604
|
+
p5.outro(pc5.green("\u2713 Done!"));
|
|
1605
|
+
}
|
|
1606
|
+
function generateReactorFileForFetch(options) {
|
|
1607
|
+
const { canisterName, canisterConfig, canisterId } = options;
|
|
1608
|
+
const pascalName = canisterName.charAt(0).toUpperCase() + canisterName.slice(1);
|
|
1609
|
+
const reactorName = `${canisterName}Reactor`;
|
|
1610
|
+
const serviceName = `${pascalName}Service`;
|
|
1611
|
+
const reactorType = canisterConfig.useDisplayReactor !== false ? "DisplayReactor" : "Reactor";
|
|
1612
|
+
const clientManagerPath = canisterConfig.clientManagerPath ?? "../../lib/client";
|
|
1613
|
+
return `/**
|
|
1614
|
+
* ${pascalName} Reactor
|
|
1615
|
+
*
|
|
1616
|
+
* Auto-generated by @ic-reactor/cli
|
|
1617
|
+
* Fetched from canister: ${canisterId}
|
|
1618
|
+
*
|
|
1619
|
+
* You can customize this file to add global configuration.
|
|
1620
|
+
*/
|
|
1621
|
+
|
|
1622
|
+
import { ${reactorType}, createActorHooks, createAuthHooks } from "@ic-reactor/react"
|
|
1623
|
+
import { clientManager } from "${clientManagerPath}"
|
|
1624
|
+
|
|
1625
|
+
// Import generated declarations
|
|
1626
|
+
// Note: You may need to run 'npx @icp-sdk/bindgen' to generate TypeScript types
|
|
1627
|
+
// For now, we use a generic service type
|
|
1628
|
+
import { idlFactory } from "./declarations/${canisterName}.did"
|
|
1629
|
+
|
|
1630
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1631
|
+
// SERVICE TYPE
|
|
1632
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1633
|
+
|
|
1634
|
+
// TODO: Generate proper types using @icp-sdk/bindgen
|
|
1635
|
+
// npx @icp-sdk/bindgen --input ./candid/${canisterName}.did --output ./src/canisters/${canisterName}/declarations
|
|
1636
|
+
type ${serviceName} = Record<string, (...args: unknown[]) => Promise<unknown>>
|
|
1637
|
+
|
|
1638
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1639
|
+
// REACTOR INSTANCE
|
|
1640
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* ${pascalName} Reactor
|
|
1644
|
+
*
|
|
1645
|
+
* Canister ID: ${canisterId}
|
|
1646
|
+
*/
|
|
1647
|
+
export const ${reactorName} = new ${reactorType}<${serviceName}>({
|
|
1648
|
+
clientManager,
|
|
1649
|
+
idlFactory,
|
|
1650
|
+
canisterId: "${canisterId}",
|
|
1651
|
+
name: "${canisterName}",
|
|
1652
|
+
})
|
|
1653
|
+
|
|
1654
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1655
|
+
// BASE HOOKS
|
|
1656
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* Base actor hooks - use these directly or import method-specific hooks.
|
|
1660
|
+
*/
|
|
1661
|
+
export const {
|
|
1662
|
+
useActorQuery,
|
|
1663
|
+
useActorMutation,
|
|
1664
|
+
useActorSuspenseQuery,
|
|
1665
|
+
useActorInfiniteQuery,
|
|
1666
|
+
useActorSuspenseInfiniteQuery,
|
|
1667
|
+
useActorMethod,
|
|
1668
|
+
} = createActorHooks(${reactorName})
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* Auth hooks for the client manager.
|
|
1672
|
+
*/
|
|
1673
|
+
export const { useAuth, useAgentState, useUserPrincipal } = createAuthHooks(clientManager)
|
|
1674
|
+
|
|
1675
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1676
|
+
// RE-EXPORTS
|
|
1677
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1678
|
+
|
|
1679
|
+
export { idlFactory }
|
|
1680
|
+
export type { ${serviceName} }
|
|
1681
|
+
`;
|
|
1682
|
+
}
|
|
1683
|
+
function generateIndexFile2(methods) {
|
|
1684
|
+
const exports = methods.map(({ method, hookType }) => {
|
|
1685
|
+
const fileName = getHookFileName(method.name, hookType).replace(".ts", "");
|
|
1686
|
+
return `export * from "./${fileName}"`;
|
|
1687
|
+
});
|
|
1688
|
+
return `/**
|
|
1689
|
+
* Hook barrel exports
|
|
1690
|
+
*
|
|
1691
|
+
* Auto-generated by @ic-reactor/cli
|
|
1692
|
+
*/
|
|
1693
|
+
|
|
1694
|
+
${exports.join("\n")}
|
|
1695
|
+
`;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/index.ts
|
|
1699
|
+
import pc6 from "picocolors";
|
|
1700
|
+
var program = new Command();
|
|
1701
|
+
program.name("ic-reactor").description(
|
|
1702
|
+
pc6.cyan("\u{1F527} Generate shadcn-style React hooks for ICP canisters")
|
|
1703
|
+
).version("3.0.0");
|
|
1704
|
+
program.command("init").description("Initialize ic-reactor configuration in your project").option("-y, --yes", "Skip prompts and use defaults").option("-o, --out-dir <path>", "Output directory for generated hooks").action(initCommand);
|
|
1705
|
+
program.command("add").description("Add hooks for canister methods (from local .did file)").option("-c, --canister <name>", "Canister name to add hooks for").option("-m, --methods <methods...>", "Method names to generate hooks for").option("-a, --all", "Add hooks for all methods").action(addCommand);
|
|
1706
|
+
program.command("fetch").description("Fetch Candid from a live canister and generate hooks").option("-i, --canister-id <id>", "Canister ID to fetch from").option("-n, --network <network>", "Network: 'ic' or 'local'", "ic").option("--name <name>", "Name for the canister in generated code").option("-m, --methods <methods...>", "Method names to generate hooks for").option("-a, --all", "Add hooks for all methods").action(fetchCommand);
|
|
1707
|
+
program.command("sync").description("Sync hooks with .did file changes").option("-c, --canister <name>", "Canister to sync").action(syncCommand);
|
|
1708
|
+
program.command("list").description("List available methods from a canister").option("-c, --canister <name>", "Canister to list methods from").action(listCommand);
|
|
1709
|
+
program.parse();
|