@ic-reactor/cli 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -193
- package/dist/index.js +77 -1080
- package/package.json +3 -3
- package/src/commands/index.ts +0 -2
- package/src/commands/init.ts +7 -7
- package/src/commands/sync.ts +20 -157
- package/src/generators/index.ts +0 -7
- package/src/generators/reactor.ts +1 -12
- package/src/index.ts +0 -20
- package/src/types.ts +1 -1
- package/src/utils/config.ts +2 -2
- package/src/commands/add.ts +0 -447
- package/src/commands/fetch.ts +0 -600
- package/src/generators/infiniteQuery.ts +0 -34
- package/src/generators/mutation.ts +0 -29
- package/src/generators/query.ts +0 -32
package/src/commands/add.ts
DELETED
|
@@ -1,447 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Add command
|
|
3
|
-
*
|
|
4
|
-
* Interactively add hooks for canister methods.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as p from "@clack/prompts"
|
|
8
|
-
import fs from "node:fs"
|
|
9
|
-
import path from "node:path"
|
|
10
|
-
import pc from "picocolors"
|
|
11
|
-
import {
|
|
12
|
-
loadConfig,
|
|
13
|
-
saveConfig,
|
|
14
|
-
getProjectRoot,
|
|
15
|
-
ensureDir,
|
|
16
|
-
fileExists,
|
|
17
|
-
findConfigFile,
|
|
18
|
-
} from "../utils/config.js"
|
|
19
|
-
import { parseDIDFile, formatMethodForDisplay } from "@ic-reactor/codegen"
|
|
20
|
-
import {
|
|
21
|
-
generateReactorFile,
|
|
22
|
-
generateQueryHook,
|
|
23
|
-
generateMutationHook,
|
|
24
|
-
generateInfiniteQueryHook,
|
|
25
|
-
} from "../generators/index.js"
|
|
26
|
-
import { getHookFileName } from "@ic-reactor/codegen"
|
|
27
|
-
import { generateDeclarations } from "@ic-reactor/codegen"
|
|
28
|
-
import type { MethodInfo, HookType } from "@ic-reactor/codegen"
|
|
29
|
-
|
|
30
|
-
interface AddOptions {
|
|
31
|
-
canister?: string
|
|
32
|
-
methods?: string[]
|
|
33
|
-
all?: boolean
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function addCommand(options: AddOptions) {
|
|
37
|
-
console.log()
|
|
38
|
-
p.intro(pc.cyan("🔧 Add Canister Hooks"))
|
|
39
|
-
|
|
40
|
-
// Load config
|
|
41
|
-
const configPath = findConfigFile()
|
|
42
|
-
if (!configPath) {
|
|
43
|
-
p.log.error(
|
|
44
|
-
`No ${pc.yellow("reactor.config.json")} found. Run ${pc.cyan("npx @ic-reactor/cli init")} first.`
|
|
45
|
-
)
|
|
46
|
-
process.exit(1)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const config = loadConfig(configPath)
|
|
50
|
-
if (!config) {
|
|
51
|
-
p.log.error(`Failed to load config from ${pc.yellow(configPath)}`)
|
|
52
|
-
process.exit(1)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const projectRoot = getProjectRoot()
|
|
56
|
-
const canisterNames = Object.keys(config.canisters)
|
|
57
|
-
|
|
58
|
-
// Check if we have any canisters configured
|
|
59
|
-
if (canisterNames.length === 0) {
|
|
60
|
-
p.log.error(
|
|
61
|
-
`No canisters configured. Add a canister to ${pc.yellow("reactor.config.json")} first.`
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
const addNow = await p.confirm({
|
|
65
|
-
message: "Would you like to add a canister now?",
|
|
66
|
-
initialValue: true,
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
if (p.isCancel(addNow) || !addNow) {
|
|
70
|
-
process.exit(1)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Prompt for canister info
|
|
74
|
-
const canisterInfo = await promptForNewCanister(projectRoot)
|
|
75
|
-
if (!canisterInfo) {
|
|
76
|
-
p.cancel("Cancelled.")
|
|
77
|
-
process.exit(0)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
config.canisters[canisterInfo.name] = canisterInfo.config
|
|
81
|
-
saveConfig(config, configPath)
|
|
82
|
-
canisterNames.push(canisterInfo.name)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Select canister
|
|
86
|
-
let selectedCanister = options.canister
|
|
87
|
-
|
|
88
|
-
if (!selectedCanister) {
|
|
89
|
-
if (canisterNames.length === 1) {
|
|
90
|
-
selectedCanister = canisterNames[0]
|
|
91
|
-
} else {
|
|
92
|
-
const result = await p.select({
|
|
93
|
-
message: "Select a canister",
|
|
94
|
-
options: canisterNames.map((name) => ({
|
|
95
|
-
value: name,
|
|
96
|
-
label: name,
|
|
97
|
-
})),
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
if (p.isCancel(result)) {
|
|
101
|
-
p.cancel("Cancelled.")
|
|
102
|
-
process.exit(0)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
selectedCanister = result as string
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const canisterConfig = config.canisters[selectedCanister]
|
|
110
|
-
if (!canisterConfig) {
|
|
111
|
-
p.log.error(`Canister ${pc.yellow(selectedCanister)} not found in config.`)
|
|
112
|
-
process.exit(1)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Parse DID file
|
|
116
|
-
const didFilePath = path.resolve(projectRoot, canisterConfig.didFile)
|
|
117
|
-
let methods: MethodInfo[]
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
methods = parseDIDFile(didFilePath)
|
|
121
|
-
} catch (error) {
|
|
122
|
-
p.log.error(
|
|
123
|
-
`Failed to parse DID file: ${pc.yellow(didFilePath)}\n${(error as Error).message}`
|
|
124
|
-
)
|
|
125
|
-
process.exit(1)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (methods.length === 0) {
|
|
129
|
-
p.log.warn(`No methods found in ${pc.yellow(didFilePath)}`)
|
|
130
|
-
process.exit(0)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
p.log.info(
|
|
134
|
-
`Found ${pc.green(methods.length.toString())} methods in ${pc.dim(selectedCanister)}`
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
// Select methods
|
|
138
|
-
let selectedMethods: MethodInfo[]
|
|
139
|
-
|
|
140
|
-
if (options.all) {
|
|
141
|
-
selectedMethods = methods
|
|
142
|
-
} else if (options.methods && options.methods.length > 0) {
|
|
143
|
-
// Parse comma-separated method names
|
|
144
|
-
const requestedMethods = options.methods
|
|
145
|
-
.flatMap((m) => m.split(","))
|
|
146
|
-
.map((m) => m.trim())
|
|
147
|
-
.filter((m) => m.length > 0)
|
|
148
|
-
|
|
149
|
-
selectedMethods = methods.filter((m) => requestedMethods.includes(m.name))
|
|
150
|
-
const notFound = requestedMethods.filter(
|
|
151
|
-
(name) => !methods.some((m) => m.name === name)
|
|
152
|
-
)
|
|
153
|
-
if (notFound.length > 0) {
|
|
154
|
-
p.log.warn(`Methods not found: ${pc.yellow(notFound.join(", "))}`)
|
|
155
|
-
}
|
|
156
|
-
} else {
|
|
157
|
-
// Interactive selection
|
|
158
|
-
const alreadyGenerated = config.generatedHooks[selectedCanister] ?? []
|
|
159
|
-
|
|
160
|
-
const result = await p.multiselect({
|
|
161
|
-
message: "Select methods to add hooks for",
|
|
162
|
-
options: methods.map((method) => {
|
|
163
|
-
const isGenerated = alreadyGenerated.includes(method.name)
|
|
164
|
-
return {
|
|
165
|
-
value: method.name,
|
|
166
|
-
label: formatMethodForDisplay(method),
|
|
167
|
-
hint: isGenerated ? pc.dim("(already generated)") : undefined,
|
|
168
|
-
}
|
|
169
|
-
}),
|
|
170
|
-
required: true,
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
if (p.isCancel(result)) {
|
|
174
|
-
p.cancel("Cancelled.")
|
|
175
|
-
process.exit(0)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
selectedMethods = methods.filter((m) =>
|
|
179
|
-
(result as string[]).includes(m.name)
|
|
180
|
-
)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (selectedMethods.length === 0) {
|
|
184
|
-
p.log.warn("No methods selected.")
|
|
185
|
-
process.exit(0)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Prompt for hook types for query methods
|
|
189
|
-
const methodsWithHookTypes: Array<{
|
|
190
|
-
method: MethodInfo
|
|
191
|
-
hookType: HookType
|
|
192
|
-
}> = []
|
|
193
|
-
|
|
194
|
-
for (const method of selectedMethods) {
|
|
195
|
-
if (method.type === "query") {
|
|
196
|
-
const hookType = await p.select({
|
|
197
|
-
message: `Hook type for ${pc.cyan(method.name)}`,
|
|
198
|
-
options: [
|
|
199
|
-
{ value: "query", label: "Query", hint: "Standard query hook" },
|
|
200
|
-
{
|
|
201
|
-
value: "suspenseQuery",
|
|
202
|
-
label: "Suspense Query",
|
|
203
|
-
hint: "For React Suspense",
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
value: "infiniteQuery",
|
|
207
|
-
label: "Infinite Query",
|
|
208
|
-
hint: "Paginated/infinite scroll",
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
value: "suspenseInfiniteQuery",
|
|
212
|
-
label: "Suspense Infinite Query",
|
|
213
|
-
hint: "Paginated with Suspense",
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
value: "skip",
|
|
217
|
-
label: "Skip",
|
|
218
|
-
hint: "Don't generate hook for this method",
|
|
219
|
-
},
|
|
220
|
-
],
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
if (p.isCancel(hookType)) {
|
|
224
|
-
p.cancel("Cancelled.")
|
|
225
|
-
process.exit(0)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Skip this method if user chose "skip"
|
|
229
|
-
if (hookType === "skip") {
|
|
230
|
-
p.log.info(`Skipping ${pc.dim(method.name)}`)
|
|
231
|
-
continue
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
methodsWithHookTypes.push({ method, hookType: hookType as HookType })
|
|
235
|
-
} else {
|
|
236
|
-
// For mutations, also allow skipping
|
|
237
|
-
const hookType = await p.select({
|
|
238
|
-
message: `Hook type for ${pc.yellow(method.name)} (mutation)`,
|
|
239
|
-
options: [
|
|
240
|
-
{
|
|
241
|
-
value: "mutation",
|
|
242
|
-
label: "Mutation",
|
|
243
|
-
hint: "Standard mutation hook",
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
value: "skip",
|
|
247
|
-
label: "Skip",
|
|
248
|
-
hint: "Don't generate hook for this method",
|
|
249
|
-
},
|
|
250
|
-
],
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
if (p.isCancel(hookType)) {
|
|
254
|
-
p.cancel("Cancelled.")
|
|
255
|
-
process.exit(0)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (hookType === "skip") {
|
|
259
|
-
p.log.info(`Skipping ${pc.dim(method.name)}`)
|
|
260
|
-
continue
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
methodsWithHookTypes.push({ method, hookType: "mutation" })
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Check if any methods were selected after skipping
|
|
268
|
-
if (methodsWithHookTypes.length === 0) {
|
|
269
|
-
p.log.warn("All methods were skipped. Nothing to generate.")
|
|
270
|
-
process.exit(0)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Generate files
|
|
274
|
-
const canisterOutDir = path.join(projectRoot, config.outDir, selectedCanister)
|
|
275
|
-
const hooksOutDir = path.join(canisterOutDir, "hooks")
|
|
276
|
-
ensureDir(hooksOutDir)
|
|
277
|
-
|
|
278
|
-
const spinner = p.spinner()
|
|
279
|
-
spinner.start("Generating hooks...")
|
|
280
|
-
|
|
281
|
-
const generatedFiles: string[] = []
|
|
282
|
-
|
|
283
|
-
// Generate reactor.ts if it doesn't exist
|
|
284
|
-
const reactorPath = path.join(canisterOutDir, "reactor.ts")
|
|
285
|
-
if (!fileExists(reactorPath)) {
|
|
286
|
-
// Generate TypeScript declarations using bindgen
|
|
287
|
-
spinner.message("Generating TypeScript declarations...")
|
|
288
|
-
const bindgenResult = await generateDeclarations({
|
|
289
|
-
didFile: didFilePath,
|
|
290
|
-
outDir: canisterOutDir,
|
|
291
|
-
canisterName: selectedCanister,
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
if (bindgenResult.success) {
|
|
295
|
-
generatedFiles.push("declarations/")
|
|
296
|
-
} else {
|
|
297
|
-
p.log.warn(`Could not generate declarations: ${bindgenResult.error}`)
|
|
298
|
-
p.log.info(
|
|
299
|
-
`You can manually run: npx @icp-sdk/bindgen --input ${didFilePath} --output ${canisterOutDir}/declarations`
|
|
300
|
-
)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
spinner.message("Generating reactor...")
|
|
304
|
-
const reactorContent = generateReactorFile({
|
|
305
|
-
canisterName: selectedCanister,
|
|
306
|
-
canisterConfig: canisterConfig,
|
|
307
|
-
config: config,
|
|
308
|
-
outDir: canisterOutDir,
|
|
309
|
-
hasDeclarations: bindgenResult.success,
|
|
310
|
-
})
|
|
311
|
-
fs.writeFileSync(reactorPath, reactorContent)
|
|
312
|
-
generatedFiles.push("reactor.ts")
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Generate hooks for each method
|
|
316
|
-
for (const { method, hookType } of methodsWithHookTypes) {
|
|
317
|
-
const fileName = getHookFileName(method.name, hookType)
|
|
318
|
-
const filePath = path.join(hooksOutDir, fileName)
|
|
319
|
-
|
|
320
|
-
let content: string
|
|
321
|
-
|
|
322
|
-
switch (hookType) {
|
|
323
|
-
case "query":
|
|
324
|
-
case "suspenseQuery":
|
|
325
|
-
content = generateQueryHook({
|
|
326
|
-
canisterName: selectedCanister,
|
|
327
|
-
method,
|
|
328
|
-
config,
|
|
329
|
-
})
|
|
330
|
-
break
|
|
331
|
-
case "infiniteQuery":
|
|
332
|
-
case "suspenseInfiniteQuery":
|
|
333
|
-
content = generateInfiniteQueryHook({
|
|
334
|
-
canisterName: selectedCanister,
|
|
335
|
-
method,
|
|
336
|
-
config,
|
|
337
|
-
})
|
|
338
|
-
break
|
|
339
|
-
case "mutation":
|
|
340
|
-
content = generateMutationHook({
|
|
341
|
-
canisterName: selectedCanister,
|
|
342
|
-
method,
|
|
343
|
-
config,
|
|
344
|
-
})
|
|
345
|
-
break
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
fs.writeFileSync(filePath, content)
|
|
349
|
-
generatedFiles.push(path.join("hooks", fileName))
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Generate index.ts barrel export
|
|
353
|
-
const indexPath = path.join(hooksOutDir, "index.ts")
|
|
354
|
-
let existingExports: string[] = []
|
|
355
|
-
|
|
356
|
-
if (fs.existsSync(indexPath)) {
|
|
357
|
-
const content = fs.readFileSync(indexPath, "utf-8")
|
|
358
|
-
existingExports = content
|
|
359
|
-
.split("\n")
|
|
360
|
-
.filter((line) => line.trim().startsWith("export * from"))
|
|
361
|
-
.map((line) => line.trim())
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const newExports = methodsWithHookTypes.map(({ method, hookType }) => {
|
|
365
|
-
const fileName = getHookFileName(method.name, hookType).replace(".ts", "")
|
|
366
|
-
return `export * from "./${fileName}"`
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
const allExports = [...new Set([...existingExports, ...newExports])]
|
|
370
|
-
|
|
371
|
-
const indexContent = `/**
|
|
372
|
-
* Hook barrel exports
|
|
373
|
-
*
|
|
374
|
-
* Auto-generated by @ic-reactor/cli
|
|
375
|
-
*/
|
|
376
|
-
|
|
377
|
-
${allExports.join("\n")}
|
|
378
|
-
`
|
|
379
|
-
fs.writeFileSync(indexPath, indexContent)
|
|
380
|
-
generatedFiles.push("hooks/index.ts")
|
|
381
|
-
|
|
382
|
-
// Update config with generated hooks
|
|
383
|
-
const existingHooks = config.generatedHooks[selectedCanister] ?? []
|
|
384
|
-
|
|
385
|
-
const newHookConfigs = methodsWithHookTypes.map(({ method, hookType }) => ({
|
|
386
|
-
name: method.name,
|
|
387
|
-
type: hookType,
|
|
388
|
-
}))
|
|
389
|
-
|
|
390
|
-
const filteredExisting = existingHooks.filter((h) => {
|
|
391
|
-
const name = typeof h === "string" ? h : h.name
|
|
392
|
-
return !newHookConfigs.some((n) => n.name === name)
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
config.generatedHooks[selectedCanister] = [
|
|
396
|
-
...filteredExisting,
|
|
397
|
-
...newHookConfigs,
|
|
398
|
-
]
|
|
399
|
-
saveConfig(config, configPath)
|
|
400
|
-
|
|
401
|
-
spinner.stop("Hooks generated!")
|
|
402
|
-
|
|
403
|
-
// Display results
|
|
404
|
-
console.log()
|
|
405
|
-
p.note(
|
|
406
|
-
generatedFiles.map((f) => pc.green(`✓ ${f}`)).join("\n"),
|
|
407
|
-
`Generated in ${pc.dim(path.relative(projectRoot, canisterOutDir))}`
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
p.outro(pc.green("✓ Done!"))
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
async function promptForNewCanister(projectRoot: string) {
|
|
414
|
-
const name = await p.text({
|
|
415
|
-
message: "Canister name",
|
|
416
|
-
placeholder: "backend",
|
|
417
|
-
validate: (value) => {
|
|
418
|
-
if (!value) return "Canister name is required"
|
|
419
|
-
return undefined
|
|
420
|
-
},
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
if (p.isCancel(name)) return null
|
|
424
|
-
|
|
425
|
-
const didFile = await p.text({
|
|
426
|
-
message: "Path to .did file",
|
|
427
|
-
placeholder: "./backend.did",
|
|
428
|
-
validate: (value) => {
|
|
429
|
-
if (!value) return "DID file path is required"
|
|
430
|
-
const fullPath = path.resolve(projectRoot, value)
|
|
431
|
-
if (!fs.existsSync(fullPath)) {
|
|
432
|
-
return `File not found: ${value}`
|
|
433
|
-
}
|
|
434
|
-
return undefined
|
|
435
|
-
},
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
if (p.isCancel(didFile)) return null
|
|
439
|
-
|
|
440
|
-
return {
|
|
441
|
-
name: name as string,
|
|
442
|
-
config: {
|
|
443
|
-
didFile: didFile as string,
|
|
444
|
-
useDisplayReactor: true,
|
|
445
|
-
},
|
|
446
|
-
}
|
|
447
|
-
}
|