@ic-reactor/cli 0.0.0-dev3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +191 -731
- package/package.json +14 -12
- package/schema.json +24 -2
- package/src/commands/add.ts +447 -0
- package/src/commands/fetch.ts +600 -0
- package/src/commands/index.ts +9 -0
- package/src/commands/init.ts +224 -0
- package/src/commands/list.ts +144 -0
- package/src/commands/sync.ts +259 -0
- package/src/generators/index.ts +8 -0
- package/src/generators/infiniteQuery.ts +34 -0
- package/src/generators/mutation.ts +29 -0
- package/src/generators/query.ts +32 -0
- package/src/generators/reactor.ts +41 -0
- package/src/index.ts +63 -0
- package/src/types.ts +26 -0
- package/src/utils/config.ts +114 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/network.ts +139 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init command
|
|
3
|
+
*
|
|
4
|
+
* Initializes ic-reactor configuration in the project.
|
|
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
|
+
CONFIG_FILE_NAME,
|
|
13
|
+
DEFAULT_CONFIG,
|
|
14
|
+
findConfigFile,
|
|
15
|
+
saveConfig,
|
|
16
|
+
getProjectRoot,
|
|
17
|
+
ensureDir,
|
|
18
|
+
} from "../utils/config.js"
|
|
19
|
+
import type { ReactorConfig, CanisterConfig } from "../types.js"
|
|
20
|
+
|
|
21
|
+
interface InitOptions {
|
|
22
|
+
yes?: boolean
|
|
23
|
+
outDir?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function initCommand(options: InitOptions) {
|
|
27
|
+
console.log()
|
|
28
|
+
p.intro(pc.cyan("🔧 ic-reactor CLI Setup"))
|
|
29
|
+
|
|
30
|
+
// Check if config already exists
|
|
31
|
+
const existingConfig = findConfigFile()
|
|
32
|
+
if (existingConfig) {
|
|
33
|
+
const shouldOverwrite = await p.confirm({
|
|
34
|
+
message: `Config file already exists at ${pc.yellow(existingConfig)}. Overwrite?`,
|
|
35
|
+
initialValue: false,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if (p.isCancel(shouldOverwrite) || !shouldOverwrite) {
|
|
39
|
+
p.cancel("Setup cancelled.")
|
|
40
|
+
process.exit(0)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const projectRoot = getProjectRoot()
|
|
45
|
+
let config: ReactorConfig
|
|
46
|
+
|
|
47
|
+
if (options.yes) {
|
|
48
|
+
// Use defaults
|
|
49
|
+
config = { ...DEFAULT_CONFIG }
|
|
50
|
+
if (options.outDir) {
|
|
51
|
+
config.outDir = options.outDir
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
// Interactive mode
|
|
55
|
+
const outDir = await p.text({
|
|
56
|
+
message: "Where should generated hooks be placed?",
|
|
57
|
+
placeholder: "src/canisters",
|
|
58
|
+
defaultValue: "src/canisters",
|
|
59
|
+
validate: (value) => {
|
|
60
|
+
if (!value) return "Output directory is required"
|
|
61
|
+
return undefined
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (p.isCancel(outDir)) {
|
|
66
|
+
p.cancel("Setup cancelled.")
|
|
67
|
+
process.exit(0)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Ask if they want to add a canister now
|
|
71
|
+
const addCanister = await p.confirm({
|
|
72
|
+
message: "Would you like to add a canister now?",
|
|
73
|
+
initialValue: true,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (p.isCancel(addCanister)) {
|
|
77
|
+
p.cancel("Setup cancelled.")
|
|
78
|
+
process.exit(0)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
config = {
|
|
82
|
+
...DEFAULT_CONFIG,
|
|
83
|
+
outDir: outDir as string,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (addCanister) {
|
|
87
|
+
const canisterInfo = await promptForCanister(projectRoot)
|
|
88
|
+
if (canisterInfo) {
|
|
89
|
+
config.canisters[canisterInfo.name] = canisterInfo.config
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Save config file
|
|
95
|
+
const configPath = path.join(projectRoot, CONFIG_FILE_NAME)
|
|
96
|
+
saveConfig(config, configPath)
|
|
97
|
+
|
|
98
|
+
// Create output directory
|
|
99
|
+
const fullOutDir = path.join(projectRoot, config.outDir)
|
|
100
|
+
ensureDir(fullOutDir)
|
|
101
|
+
|
|
102
|
+
// Create a sample client manager if it doesn't exist
|
|
103
|
+
const clientManagerPath = path.join(projectRoot, "src/lib/client.ts")
|
|
104
|
+
if (!fs.existsSync(clientManagerPath)) {
|
|
105
|
+
const createClient = await p.confirm({
|
|
106
|
+
message: "Create a sample client manager at src/lib/client.ts?",
|
|
107
|
+
initialValue: true,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (!p.isCancel(createClient) && createClient) {
|
|
111
|
+
ensureDir(path.dirname(clientManagerPath))
|
|
112
|
+
fs.writeFileSync(clientManagerPath, getClientManagerTemplate())
|
|
113
|
+
p.log.success(`Created ${pc.green("src/lib/client.ts")}`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
p.log.success(`Created ${pc.green(CONFIG_FILE_NAME)}`)
|
|
118
|
+
p.log.success(`Created ${pc.green(config.outDir)} directory`)
|
|
119
|
+
|
|
120
|
+
console.log()
|
|
121
|
+
p.note(
|
|
122
|
+
`Next steps:
|
|
123
|
+
|
|
124
|
+
1. ${pc.cyan("Add a canister:")}
|
|
125
|
+
${pc.dim("npx @ic-reactor/cli add")}
|
|
126
|
+
|
|
127
|
+
2. ${pc.cyan("List available methods:")}
|
|
128
|
+
${pc.dim("npx @ic-reactor/cli list -c <canister-name>")}
|
|
129
|
+
|
|
130
|
+
3. ${pc.cyan("Add hooks for specific methods:")}
|
|
131
|
+
${pc.dim("npx @ic-reactor/cli add -c <canister> -m <method>")}`,
|
|
132
|
+
"Getting Started"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
p.outro(pc.green("✓ ic-reactor initialized successfully!"))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function promptForCanister(
|
|
139
|
+
projectRoot: string
|
|
140
|
+
): Promise<{ name: string; config: CanisterConfig } | null> {
|
|
141
|
+
const name = await p.text({
|
|
142
|
+
message: "Canister name",
|
|
143
|
+
placeholder: "backend",
|
|
144
|
+
validate: (value) => {
|
|
145
|
+
if (!value) return "Canister name is required"
|
|
146
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value)) {
|
|
147
|
+
return "Canister name must start with a letter and contain only letters, numbers, hyphens, and underscores"
|
|
148
|
+
}
|
|
149
|
+
return undefined
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
if (p.isCancel(name)) return null
|
|
154
|
+
|
|
155
|
+
const didFile = await p.text({
|
|
156
|
+
message: "Path to .did file",
|
|
157
|
+
placeholder: "./backend.did",
|
|
158
|
+
validate: (value) => {
|
|
159
|
+
if (!value) return "DID file path is required"
|
|
160
|
+
const fullPath = path.resolve(projectRoot, value)
|
|
161
|
+
if (!fs.existsSync(fullPath)) {
|
|
162
|
+
return `File not found: ${value}`
|
|
163
|
+
}
|
|
164
|
+
return undefined
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
if (p.isCancel(didFile)) return null
|
|
169
|
+
|
|
170
|
+
const clientManagerPath = await p.text({
|
|
171
|
+
message:
|
|
172
|
+
"Import path to your client manager (relative from generated hooks)",
|
|
173
|
+
placeholder: "../../lib/client",
|
|
174
|
+
defaultValue: "../../lib/client",
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
if (p.isCancel(clientManagerPath)) return null
|
|
178
|
+
|
|
179
|
+
const useDisplayReactor = await p.confirm({
|
|
180
|
+
message:
|
|
181
|
+
"Use DisplayReactor? (auto-converts bigint → string, Principal → string)",
|
|
182
|
+
initialValue: true,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
if (p.isCancel(useDisplayReactor)) return null
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
name: name as string,
|
|
189
|
+
config: {
|
|
190
|
+
didFile: didFile as string,
|
|
191
|
+
clientManagerPath: clientManagerPath as string,
|
|
192
|
+
useDisplayReactor: useDisplayReactor as boolean,
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getClientManagerTemplate(): string {
|
|
198
|
+
return `/**
|
|
199
|
+
* IC Client Manager
|
|
200
|
+
*
|
|
201
|
+
* This file configures the IC agent and client manager for your application.
|
|
202
|
+
* Customize the agent options based on your environment.
|
|
203
|
+
*/
|
|
204
|
+
|
|
205
|
+
import { ClientManager } from "@ic-reactor/react"
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The client manager handles agent lifecycle and authentication.
|
|
209
|
+
*
|
|
210
|
+
* Configuration options:
|
|
211
|
+
* - host: IC network host (defaults to process env or mainnet)
|
|
212
|
+
* - identity: Initial identity (optional, can be set later)
|
|
213
|
+
* - verifyQuerySignatures: Verify query signatures (recommended for production)
|
|
214
|
+
*
|
|
215
|
+
* For local development, the agent will automatically detect local replica.
|
|
216
|
+
*/
|
|
217
|
+
export const clientManager = new ClientManager({
|
|
218
|
+
// Uncomment for explicit host configuration:
|
|
219
|
+
// host: process.env.DFX_NETWORK === "local"
|
|
220
|
+
// ? "http://localhost:4943"
|
|
221
|
+
// : "https://icp-api.io",
|
|
222
|
+
})
|
|
223
|
+
`
|
|
224
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List command
|
|
3
|
+
*
|
|
4
|
+
* List available methods from a canister's DID file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as p from "@clack/prompts"
|
|
8
|
+
import path from "node:path"
|
|
9
|
+
import pc from "picocolors"
|
|
10
|
+
import { loadConfig, getProjectRoot, findConfigFile } from "../utils/config.js"
|
|
11
|
+
import { parseDIDFile } from "@ic-reactor/codegen"
|
|
12
|
+
|
|
13
|
+
interface ListOptions {
|
|
14
|
+
canister?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function listCommand(options: ListOptions) {
|
|
18
|
+
console.log()
|
|
19
|
+
p.intro(pc.cyan("📋 List Canister Methods"))
|
|
20
|
+
|
|
21
|
+
// Load config
|
|
22
|
+
const configPath = findConfigFile()
|
|
23
|
+
if (!configPath) {
|
|
24
|
+
p.log.error(
|
|
25
|
+
`No ${pc.yellow("reactor.config.json")} found. Run ${pc.cyan("npx @ic-reactor/cli init")} first.`
|
|
26
|
+
)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const config = loadConfig(configPath)
|
|
31
|
+
if (!config) {
|
|
32
|
+
p.log.error(`Failed to load config from ${pc.yellow(configPath)}`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const projectRoot = getProjectRoot()
|
|
37
|
+
const canisterNames = Object.keys(config.canisters)
|
|
38
|
+
|
|
39
|
+
if (canisterNames.length === 0) {
|
|
40
|
+
p.log.error(
|
|
41
|
+
`No canisters configured. Add a canister to ${pc.yellow("reactor.config.json")} first.`
|
|
42
|
+
)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Select canister
|
|
47
|
+
let selectedCanister = options.canister
|
|
48
|
+
|
|
49
|
+
if (!selectedCanister) {
|
|
50
|
+
if (canisterNames.length === 1) {
|
|
51
|
+
selectedCanister = canisterNames[0]
|
|
52
|
+
} else {
|
|
53
|
+
const result = await p.select({
|
|
54
|
+
message: "Select a canister",
|
|
55
|
+
options: canisterNames.map((name) => ({
|
|
56
|
+
value: name,
|
|
57
|
+
label: name,
|
|
58
|
+
})),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
if (p.isCancel(result)) {
|
|
62
|
+
p.cancel("Cancelled.")
|
|
63
|
+
process.exit(0)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
selectedCanister = result as string
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const canisterConfig = config.canisters[selectedCanister]
|
|
71
|
+
if (!canisterConfig) {
|
|
72
|
+
p.log.error(`Canister ${pc.yellow(selectedCanister)} not found in config.`)
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Parse DID file
|
|
77
|
+
const didFilePath = path.resolve(projectRoot, canisterConfig.didFile)
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const methods = parseDIDFile(didFilePath)
|
|
81
|
+
|
|
82
|
+
if (methods.length === 0) {
|
|
83
|
+
p.log.warn(`No methods found in ${pc.yellow(didFilePath)}`)
|
|
84
|
+
process.exit(0)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const queries = methods.filter((m) => m.type === "query")
|
|
88
|
+
const mutations = methods.filter((m) => m.type === "mutation")
|
|
89
|
+
const generatedMethods = config.generatedHooks[selectedCanister] ?? []
|
|
90
|
+
|
|
91
|
+
// Display queries
|
|
92
|
+
if (queries.length > 0) {
|
|
93
|
+
console.log()
|
|
94
|
+
console.log(pc.bold(pc.cyan(" Queries:")))
|
|
95
|
+
for (const method of queries) {
|
|
96
|
+
const isGenerated = generatedMethods.includes(method.name)
|
|
97
|
+
const status = isGenerated ? pc.green("✓") : pc.dim("○")
|
|
98
|
+
const argsHint = method.hasArgs ? pc.dim("(args)") : pc.dim("()")
|
|
99
|
+
console.log(` ${status} ${method.name} ${argsHint}`)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Display mutations
|
|
104
|
+
if (mutations.length > 0) {
|
|
105
|
+
console.log()
|
|
106
|
+
console.log(pc.bold(pc.yellow(" Mutations (Updates):")))
|
|
107
|
+
for (const method of mutations) {
|
|
108
|
+
const isGenerated = generatedMethods.includes(method.name)
|
|
109
|
+
const status = isGenerated ? pc.green("✓") : pc.dim("○")
|
|
110
|
+
const argsHint = method.hasArgs ? pc.dim("(args)") : pc.dim("()")
|
|
111
|
+
console.log(` ${status} ${method.name} ${argsHint}`)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Summary
|
|
116
|
+
console.log()
|
|
117
|
+
const generatedCount = generatedMethods.length
|
|
118
|
+
const totalCount = methods.length
|
|
119
|
+
|
|
120
|
+
p.note(
|
|
121
|
+
`Total: ${pc.bold(totalCount.toString())} methods\n` +
|
|
122
|
+
`Generated: ${pc.green(generatedCount.toString())} / ${totalCount}\n\n` +
|
|
123
|
+
`${pc.green("✓")} = hook generated\n` +
|
|
124
|
+
`${pc.dim("○")} = not yet generated`,
|
|
125
|
+
selectedCanister
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if (generatedCount < totalCount) {
|
|
129
|
+
console.log()
|
|
130
|
+
console.log(
|
|
131
|
+
pc.dim(
|
|
132
|
+
` Run ${pc.cyan(`npx @ic-reactor/cli add -c ${selectedCanister}`)} to add hooks`
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
p.log.error(
|
|
138
|
+
`Failed to parse DID file: ${pc.yellow(didFilePath)}\n${(error as Error).message}`
|
|
139
|
+
)
|
|
140
|
+
process.exit(1)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log()
|
|
144
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync command
|
|
3
|
+
*
|
|
4
|
+
* Regenerate hooks when DID files change.
|
|
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
|
+
getProjectRoot,
|
|
14
|
+
findConfigFile,
|
|
15
|
+
ensureDir,
|
|
16
|
+
} from "../utils/config.js"
|
|
17
|
+
import { parseDIDFile } from "@ic-reactor/codegen"
|
|
18
|
+
import {
|
|
19
|
+
generateReactorFile,
|
|
20
|
+
generateQueryHook,
|
|
21
|
+
generateMutationHook,
|
|
22
|
+
} from "../generators/index.js"
|
|
23
|
+
import { getHookFileName } from "@ic-reactor/codegen"
|
|
24
|
+
import { generateDeclarations } from "@ic-reactor/codegen"
|
|
25
|
+
import type { MethodInfo } from "@ic-reactor/codegen"
|
|
26
|
+
|
|
27
|
+
interface SyncOptions {
|
|
28
|
+
canister?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function syncCommand(options: SyncOptions) {
|
|
32
|
+
console.log()
|
|
33
|
+
p.intro(pc.cyan("🔄 Sync Canister Hooks"))
|
|
34
|
+
|
|
35
|
+
// Load config
|
|
36
|
+
const configPath = findConfigFile()
|
|
37
|
+
if (!configPath) {
|
|
38
|
+
p.log.error(
|
|
39
|
+
`No ${pc.yellow("reactor.config.json")} found. Run ${pc.cyan("npx @ic-reactor/cli init")} first.`
|
|
40
|
+
)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const config = loadConfig(configPath)
|
|
45
|
+
if (!config) {
|
|
46
|
+
p.log.error(`Failed to load config from ${pc.yellow(configPath)}`)
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const projectRoot = getProjectRoot()
|
|
51
|
+
const canisterNames = Object.keys(config.canisters)
|
|
52
|
+
|
|
53
|
+
if (canisterNames.length === 0) {
|
|
54
|
+
p.log.error("No canisters configured.")
|
|
55
|
+
process.exit(1)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Select canisters to sync
|
|
59
|
+
let canistersToSync: string[]
|
|
60
|
+
|
|
61
|
+
if (options.canister) {
|
|
62
|
+
if (!config.canisters[options.canister]) {
|
|
63
|
+
p.log.error(
|
|
64
|
+
`Canister ${pc.yellow(options.canister)} not found in config.`
|
|
65
|
+
)
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
canistersToSync = [options.canister]
|
|
69
|
+
} else {
|
|
70
|
+
// Sync all canisters with generated hooks
|
|
71
|
+
canistersToSync = canisterNames.filter(
|
|
72
|
+
(name) => (config.generatedHooks[name]?.length ?? 0) > 0
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (canistersToSync.length === 0) {
|
|
76
|
+
p.log.warn("No hooks have been generated yet. Run `add` first.")
|
|
77
|
+
process.exit(0)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const spinner = p.spinner()
|
|
82
|
+
spinner.start("Syncing hooks...")
|
|
83
|
+
|
|
84
|
+
let totalUpdated = 0
|
|
85
|
+
let totalSkipped = 0
|
|
86
|
+
const errors: string[] = []
|
|
87
|
+
|
|
88
|
+
for (const canisterName of canistersToSync) {
|
|
89
|
+
const canisterConfig = config.canisters[canisterName]
|
|
90
|
+
const generatedMethods = config.generatedHooks[canisterName] ?? []
|
|
91
|
+
|
|
92
|
+
if (generatedMethods.length === 0) {
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Parse DID file
|
|
97
|
+
const didFilePath = path.resolve(projectRoot, canisterConfig.didFile)
|
|
98
|
+
let methods: MethodInfo[]
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
methods = parseDIDFile(didFilePath)
|
|
102
|
+
} catch (error) {
|
|
103
|
+
errors.push(
|
|
104
|
+
`${canisterName}: Failed to parse DID file - ${(error as Error).message}`
|
|
105
|
+
)
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Normalize hooks to objects
|
|
110
|
+
const hooks = generatedMethods.map((h) =>
|
|
111
|
+
typeof h === "string" ? { name: h } : h
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// Check for removed methods
|
|
115
|
+
const currentMethodNames = methods.map((m) => m.name)
|
|
116
|
+
const removedMethods = hooks
|
|
117
|
+
.filter((h) => !currentMethodNames.includes(h.name))
|
|
118
|
+
.map((h) => h.name)
|
|
119
|
+
|
|
120
|
+
if (removedMethods.length > 0) {
|
|
121
|
+
p.log.warn(
|
|
122
|
+
`${canisterName}: Methods removed from DID: ${pc.yellow(removedMethods.join(", "))}`
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check for new methods
|
|
127
|
+
const generatedNames = hooks.map((h) => h.name)
|
|
128
|
+
const newMethods = methods.filter((m) => !generatedNames.includes(m.name))
|
|
129
|
+
|
|
130
|
+
if (newMethods.length > 0) {
|
|
131
|
+
p.log.info(
|
|
132
|
+
`${canisterName}: New methods available: ${pc.cyan(newMethods.map((m) => m.name).join(", "))}`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Regenerate declarations if missing
|
|
137
|
+
const canisterOutDir = path.join(projectRoot, config.outDir, canisterName)
|
|
138
|
+
const declarationsDir = path.join(canisterOutDir, "declarations")
|
|
139
|
+
|
|
140
|
+
// Check if declarations exist and have files (not just nested empty dir)
|
|
141
|
+
const declarationsExist =
|
|
142
|
+
fs.existsSync(declarationsDir) &&
|
|
143
|
+
fs
|
|
144
|
+
.readdirSync(declarationsDir)
|
|
145
|
+
.some((f) => f.endsWith(".ts") || f.endsWith(".js"))
|
|
146
|
+
|
|
147
|
+
if (!declarationsExist) {
|
|
148
|
+
spinner.message(`Regenerating declarations for ${canisterName}...`)
|
|
149
|
+
const bindgenResult = await generateDeclarations({
|
|
150
|
+
didFile: didFilePath,
|
|
151
|
+
outDir: canisterOutDir,
|
|
152
|
+
canisterName,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
if (bindgenResult.success) {
|
|
156
|
+
totalUpdated++
|
|
157
|
+
} else {
|
|
158
|
+
p.log.warn(
|
|
159
|
+
`Could not regenerate declarations for ${canisterName}: ${bindgenResult.error}`
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Regenerate reactor.ts
|
|
165
|
+
const reactorPath = path.join(canisterOutDir, "reactor.ts")
|
|
166
|
+
|
|
167
|
+
const reactorContent = generateReactorFile({
|
|
168
|
+
canisterName,
|
|
169
|
+
canisterConfig,
|
|
170
|
+
config,
|
|
171
|
+
outDir: canisterOutDir,
|
|
172
|
+
})
|
|
173
|
+
fs.writeFileSync(reactorPath, reactorContent)
|
|
174
|
+
totalUpdated++
|
|
175
|
+
|
|
176
|
+
// Regenerate existing hooks
|
|
177
|
+
const hooksOutDir = path.join(canisterOutDir, "hooks")
|
|
178
|
+
ensureDir(hooksOutDir)
|
|
179
|
+
|
|
180
|
+
for (const hookConfig of hooks) {
|
|
181
|
+
const methodName = hookConfig.name
|
|
182
|
+
const method = methods.find((m) => m.name === methodName)
|
|
183
|
+
|
|
184
|
+
if (!method) {
|
|
185
|
+
// Method was removed, skip but warn
|
|
186
|
+
totalSkipped++
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Determine hook type
|
|
191
|
+
let hookType: string = hookConfig.type || (method.type as string)
|
|
192
|
+
|
|
193
|
+
// If no explicit type in config, try to infer from existing files (backward compat)
|
|
194
|
+
if (!hookConfig.type) {
|
|
195
|
+
const infiniteQueryFileName = getHookFileName(
|
|
196
|
+
methodName,
|
|
197
|
+
"infiniteQuery"
|
|
198
|
+
)
|
|
199
|
+
if (fs.existsSync(path.join(hooksOutDir, infiniteQueryFileName))) {
|
|
200
|
+
hookType = "infiniteQuery"
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const fileName = getHookFileName(methodName, hookType)
|
|
205
|
+
let content: string
|
|
206
|
+
|
|
207
|
+
if (hookType.includes("Query")) {
|
|
208
|
+
content = generateQueryHook({
|
|
209
|
+
canisterName,
|
|
210
|
+
method,
|
|
211
|
+
config,
|
|
212
|
+
type: hookType as any,
|
|
213
|
+
})
|
|
214
|
+
} else {
|
|
215
|
+
content = generateMutationHook({
|
|
216
|
+
canisterName,
|
|
217
|
+
method,
|
|
218
|
+
config,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const filePath = path.join(hooksOutDir, fileName)
|
|
223
|
+
|
|
224
|
+
// Check if file exists and has been customized
|
|
225
|
+
if (fs.existsSync(filePath)) {
|
|
226
|
+
const existingContent = fs.readFileSync(filePath, "utf-8")
|
|
227
|
+
|
|
228
|
+
// If content is different from what we'd generate, skip (user customized)
|
|
229
|
+
if (existingContent !== content) {
|
|
230
|
+
totalSkipped++
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fs.writeFileSync(filePath, content)
|
|
236
|
+
totalUpdated++
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
spinner.stop("Sync complete!")
|
|
241
|
+
|
|
242
|
+
// Display results
|
|
243
|
+
if (errors.length > 0) {
|
|
244
|
+
console.log()
|
|
245
|
+
p.log.error("Errors encountered:")
|
|
246
|
+
for (const error of errors) {
|
|
247
|
+
console.log(` ${pc.red("•")} ${error}`)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log()
|
|
252
|
+
p.note(
|
|
253
|
+
`Updated: ${pc.green(totalUpdated.toString())} files\n` +
|
|
254
|
+
`Skipped: ${pc.dim(totalSkipped.toString())} files (preserved customizations)`,
|
|
255
|
+
"Summary"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
p.outro(pc.green("✓ Sync complete!"))
|
|
259
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infinite Query hook generator
|
|
3
|
+
*
|
|
4
|
+
* Generates createInfiniteQuery-based hooks for paginated canister methods.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type MethodInfo,
|
|
9
|
+
type HookType,
|
|
10
|
+
generateInfiniteQueryHook as generateInfiniteQueryHookFromCodegen,
|
|
11
|
+
} from "@ic-reactor/codegen"
|
|
12
|
+
import type { ReactorConfig } from "../types.js"
|
|
13
|
+
|
|
14
|
+
export interface InfiniteQueryHookOptions {
|
|
15
|
+
canisterName: string
|
|
16
|
+
method: MethodInfo
|
|
17
|
+
type?: HookType
|
|
18
|
+
config: ReactorConfig // Unused
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate an infinite query hook file content
|
|
23
|
+
*/
|
|
24
|
+
export function generateInfiniteQueryHook(
|
|
25
|
+
options: InfiniteQueryHookOptions
|
|
26
|
+
): string {
|
|
27
|
+
const { canisterName, method, type } = options
|
|
28
|
+
|
|
29
|
+
return generateInfiniteQueryHookFromCodegen({
|
|
30
|
+
canisterName,
|
|
31
|
+
method,
|
|
32
|
+
type,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation hook generator
|
|
3
|
+
*
|
|
4
|
+
* Generates createMutation-based hooks for canister update methods.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type MethodInfo,
|
|
9
|
+
generateMutationHook as generateMutationHookFromCodegen,
|
|
10
|
+
} from "@ic-reactor/codegen"
|
|
11
|
+
import type { ReactorConfig } from "../types.js"
|
|
12
|
+
|
|
13
|
+
export interface MutationHookOptions {
|
|
14
|
+
canisterName: string
|
|
15
|
+
method: MethodInfo
|
|
16
|
+
config: ReactorConfig // Unused
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate a mutation hook file content
|
|
21
|
+
*/
|
|
22
|
+
export function generateMutationHook(options: MutationHookOptions): string {
|
|
23
|
+
const { canisterName, method } = options
|
|
24
|
+
|
|
25
|
+
return generateMutationHookFromCodegen({
|
|
26
|
+
canisterName,
|
|
27
|
+
method,
|
|
28
|
+
})
|
|
29
|
+
}
|