@ic-reactor/codegen 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +543 -0
- package/dist/index.d.cts +237 -0
- package/dist/index.d.ts +237 -0
- package/dist/index.js +489 -0
- package/package.json +60 -0
- package/src/bindgen.ts +113 -0
- package/src/did.test.ts +102 -0
- package/src/did.ts +85 -0
- package/src/index.ts +51 -0
- package/src/naming.test.ts +59 -0
- package/src/naming.ts +83 -0
- package/src/templates/__snapshots__/infiniteQuery.test.ts.snap +44 -0
- package/src/templates/__snapshots__/mutation.test.ts.snap +59 -0
- package/src/templates/__snapshots__/query.test.ts.snap +64 -0
- package/src/templates/__snapshots__/reactor.test.ts.snap +163 -0
- package/src/templates/infiniteQuery.test.ts +42 -0
- package/src/templates/infiniteQuery.ts +74 -0
- package/src/templates/mutation.test.ts +43 -0
- package/src/templates/mutation.ts +51 -0
- package/src/templates/query.test.ts +44 -0
- package/src/templates/query.ts +71 -0
- package/src/templates/reactor.test.ts +78 -0
- package/src/templates/reactor.ts +290 -0
- package/src/types.ts +78 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactor file template generator
|
|
3
|
+
*
|
|
4
|
+
* Generates the reactor instance file for a canister.
|
|
5
|
+
* Supports simple mode (generic hooks) and advanced mode (per-method typed hooks).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from "node:path"
|
|
9
|
+
import type { ReactorGeneratorOptions } from "../types.js"
|
|
10
|
+
import {
|
|
11
|
+
toPascalCase,
|
|
12
|
+
toCamelCase,
|
|
13
|
+
getReactorName,
|
|
14
|
+
getServiceTypeName,
|
|
15
|
+
} from "../naming.js"
|
|
16
|
+
import { extractMethods } from "../did.js"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate the reactor file content
|
|
20
|
+
*/
|
|
21
|
+
export function generateReactorFile(options: ReactorGeneratorOptions): string {
|
|
22
|
+
const {
|
|
23
|
+
canisterName,
|
|
24
|
+
canisterConfig,
|
|
25
|
+
globalClientManagerPath,
|
|
26
|
+
hasDeclarations = true,
|
|
27
|
+
advanced = false,
|
|
28
|
+
didContent,
|
|
29
|
+
} = options
|
|
30
|
+
|
|
31
|
+
const pascalName = toPascalCase(canisterName)
|
|
32
|
+
const reactorName = getReactorName(canisterName)
|
|
33
|
+
const serviceName = getServiceTypeName(canisterName)
|
|
34
|
+
const reactorType =
|
|
35
|
+
canisterConfig.useDisplayReactor !== false ? "DisplayReactor" : "Reactor"
|
|
36
|
+
|
|
37
|
+
const clientManagerPath =
|
|
38
|
+
canisterConfig.clientManagerPath ??
|
|
39
|
+
globalClientManagerPath ??
|
|
40
|
+
"../../lib/client"
|
|
41
|
+
|
|
42
|
+
const didFileName = path.basename(canisterConfig.didFile)
|
|
43
|
+
const declarationsPath = `./declarations/${didFileName}`
|
|
44
|
+
|
|
45
|
+
const vars: TemplateVars = {
|
|
46
|
+
canisterName,
|
|
47
|
+
pascalName,
|
|
48
|
+
reactorName,
|
|
49
|
+
serviceName,
|
|
50
|
+
reactorType,
|
|
51
|
+
clientManagerPath,
|
|
52
|
+
declarationsPath,
|
|
53
|
+
useDisplayReactor: canisterConfig.useDisplayReactor !== false,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!hasDeclarations) {
|
|
57
|
+
return generateFallbackReactorFile(vars)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (advanced && didContent) {
|
|
61
|
+
return generateAdvancedReactorFile(vars, didContent)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return generateSimpleReactorFile(vars)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
68
|
+
// SHARED TYPES
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
interface TemplateVars {
|
|
72
|
+
canisterName: string
|
|
73
|
+
pascalName: string
|
|
74
|
+
reactorName: string
|
|
75
|
+
serviceName: string
|
|
76
|
+
reactorType: string
|
|
77
|
+
clientManagerPath: string
|
|
78
|
+
declarationsPath: string
|
|
79
|
+
useDisplayReactor: boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
83
|
+
// SHARED SECTIONS
|
|
84
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
85
|
+
|
|
86
|
+
function reactorInstance(vars: TemplateVars): string {
|
|
87
|
+
const {
|
|
88
|
+
pascalName,
|
|
89
|
+
reactorName,
|
|
90
|
+
serviceName,
|
|
91
|
+
reactorType,
|
|
92
|
+
canisterName,
|
|
93
|
+
useDisplayReactor,
|
|
94
|
+
} = vars
|
|
95
|
+
return `/**
|
|
96
|
+
* ${pascalName} Reactor — ${useDisplayReactor ? "Display" : "Candid"} mode.
|
|
97
|
+
* ${useDisplayReactor ? "Automatically converts bigint → string, Principal → string, etc." : "Uses raw Candid types."}
|
|
98
|
+
*/
|
|
99
|
+
export const ${reactorName} = new ${reactorType}<${serviceName}>({
|
|
100
|
+
clientManager,
|
|
101
|
+
idlFactory,
|
|
102
|
+
name: "${canisterName}",
|
|
103
|
+
})`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function actorHooks(vars: TemplateVars): string {
|
|
107
|
+
const { pascalName, reactorName } = vars
|
|
108
|
+
return `const {
|
|
109
|
+
useActorQuery: use${pascalName}Query,
|
|
110
|
+
useActorSuspenseQuery: use${pascalName}SuspenseQuery,
|
|
111
|
+
useActorInfiniteQuery: use${pascalName}InfiniteQuery,
|
|
112
|
+
useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
|
|
113
|
+
useActorMutation: use${pascalName}Mutation,
|
|
114
|
+
useActorMethod: use${pascalName}Method,
|
|
115
|
+
} = createActorHooks(${reactorName})
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
use${pascalName}Query,
|
|
119
|
+
use${pascalName}SuspenseQuery,
|
|
120
|
+
use${pascalName}InfiniteQuery,
|
|
121
|
+
use${pascalName}SuspenseInfiniteQuery,
|
|
122
|
+
use${pascalName}Mutation,
|
|
123
|
+
use${pascalName}Method,
|
|
124
|
+
}`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
128
|
+
// SIMPLE MODE
|
|
129
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
130
|
+
|
|
131
|
+
function generateSimpleReactorFile(vars: TemplateVars): string {
|
|
132
|
+
const {
|
|
133
|
+
pascalName,
|
|
134
|
+
reactorType,
|
|
135
|
+
clientManagerPath,
|
|
136
|
+
declarationsPath,
|
|
137
|
+
serviceName,
|
|
138
|
+
} = vars
|
|
139
|
+
|
|
140
|
+
return `/**
|
|
141
|
+
* ${pascalName} Reactor
|
|
142
|
+
*
|
|
143
|
+
* Auto-generated by @ic-reactor/codegen
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
import { ${reactorType}, createActorHooks } from "@ic-reactor/react"
|
|
147
|
+
import { clientManager } from "${clientManagerPath}"
|
|
148
|
+
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
149
|
+
|
|
150
|
+
export type ${serviceName} = _SERVICE
|
|
151
|
+
|
|
152
|
+
${reactorInstance(vars)}
|
|
153
|
+
|
|
154
|
+
${actorHooks(vars)}
|
|
155
|
+
|
|
156
|
+
export { idlFactory }
|
|
157
|
+
`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
161
|
+
// ADVANCED MODE
|
|
162
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
163
|
+
|
|
164
|
+
function generateAdvancedReactorFile(
|
|
165
|
+
vars: TemplateVars,
|
|
166
|
+
didContent: string
|
|
167
|
+
): string {
|
|
168
|
+
const {
|
|
169
|
+
pascalName,
|
|
170
|
+
reactorName,
|
|
171
|
+
serviceName,
|
|
172
|
+
reactorType,
|
|
173
|
+
clientManagerPath,
|
|
174
|
+
declarationsPath,
|
|
175
|
+
} = vars
|
|
176
|
+
|
|
177
|
+
const methods = extractMethods(didContent)
|
|
178
|
+
|
|
179
|
+
// Determine which extra imports we need based on methods
|
|
180
|
+
const hasQueryWithoutArgs = methods.some(
|
|
181
|
+
(m) => m.type === "query" && !m.hasArgs
|
|
182
|
+
)
|
|
183
|
+
const hasMutationWithoutArgs = methods.some(
|
|
184
|
+
(m) => m.type === "mutation" && !m.hasArgs
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
const extraImports: string[] = []
|
|
188
|
+
if (hasQueryWithoutArgs) extraImports.push("createQuery")
|
|
189
|
+
if (hasMutationWithoutArgs) extraImports.push("createMutation")
|
|
190
|
+
|
|
191
|
+
// Generate per-method static hooks (no-args methods only)
|
|
192
|
+
const perMethodHooks = methods
|
|
193
|
+
.map(({ name, type, hasArgs }) => {
|
|
194
|
+
const camelMethod = toCamelCase(name)
|
|
195
|
+
|
|
196
|
+
if (type === "query") {
|
|
197
|
+
if (!hasArgs) {
|
|
198
|
+
return `
|
|
199
|
+
export const ${camelMethod}Query = createQuery(${reactorName}, {
|
|
200
|
+
functionName: "${name}",
|
|
201
|
+
})`
|
|
202
|
+
}
|
|
203
|
+
return "" // queries with args don't get static instances
|
|
204
|
+
} else {
|
|
205
|
+
if (!hasArgs) {
|
|
206
|
+
return `
|
|
207
|
+
export const ${camelMethod}Mutation = createMutation(${reactorName}, {
|
|
208
|
+
functionName: "${name}",
|
|
209
|
+
})`
|
|
210
|
+
}
|
|
211
|
+
return "" // mutations with args don't get static instances
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
|
|
216
|
+
return `/**
|
|
217
|
+
* ${pascalName} Reactor (Advanced)
|
|
218
|
+
*
|
|
219
|
+
* Auto-generated by @ic-reactor/codegen
|
|
220
|
+
* Includes reactor instance, actor hooks, and per-method static hooks.
|
|
221
|
+
*/
|
|
222
|
+
|
|
223
|
+
import {
|
|
224
|
+
${reactorType},
|
|
225
|
+
createActorHooks,${extraImports.length > 0 ? "\n " + extraImports.join(",\n ") + "," : ""}
|
|
226
|
+
} from "@ic-reactor/react"
|
|
227
|
+
import { clientManager } from "${clientManagerPath}"
|
|
228
|
+
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
229
|
+
|
|
230
|
+
type ${serviceName} = _SERVICE
|
|
231
|
+
|
|
232
|
+
${reactorInstance(vars)}
|
|
233
|
+
|
|
234
|
+
${actorHooks(vars)}
|
|
235
|
+
${
|
|
236
|
+
perMethodHooks.length > 0
|
|
237
|
+
? `
|
|
238
|
+
// Per-method static hooks (no-args methods only)
|
|
239
|
+
${perMethodHooks.join("\n")}
|
|
240
|
+
`
|
|
241
|
+
: ""
|
|
242
|
+
}
|
|
243
|
+
export { idlFactory }
|
|
244
|
+
export type { ${serviceName} }
|
|
245
|
+
`
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
249
|
+
// FALLBACK (NO DECLARATIONS)
|
|
250
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
251
|
+
|
|
252
|
+
function generateFallbackReactorFile(vars: TemplateVars): string {
|
|
253
|
+
const {
|
|
254
|
+
canisterName,
|
|
255
|
+
pascalName,
|
|
256
|
+
serviceName,
|
|
257
|
+
reactorType,
|
|
258
|
+
clientManagerPath,
|
|
259
|
+
declarationsPath,
|
|
260
|
+
} = vars
|
|
261
|
+
|
|
262
|
+
return `/**
|
|
263
|
+
* ${pascalName} Reactor
|
|
264
|
+
*
|
|
265
|
+
* Auto-generated by @ic-reactor/codegen
|
|
266
|
+
*
|
|
267
|
+
* ⚠️ Declarations were not generated. Run:
|
|
268
|
+
* npx @icp-sdk/bindgen --input <path-to-did> --output ./${canisterName}/declarations
|
|
269
|
+
* Then uncomment the import below and remove the fallback type.
|
|
270
|
+
*/
|
|
271
|
+
|
|
272
|
+
import { ${reactorType}, createActorHooks } from "@ic-reactor/react"
|
|
273
|
+
import { clientManager } from "${clientManagerPath}"
|
|
274
|
+
|
|
275
|
+
// TODO: Uncomment after generating declarations:
|
|
276
|
+
// import { idlFactory, type _SERVICE as ${serviceName} } from "${declarationsPath}"
|
|
277
|
+
|
|
278
|
+
// Fallback — replace with generated types
|
|
279
|
+
type ${serviceName} = Record<string, (...args: unknown[]) => Promise<unknown>>
|
|
280
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
281
|
+
const idlFactory = ({ IDL }: { IDL: any }) => IDL.Service({})
|
|
282
|
+
|
|
283
|
+
${reactorInstance(vars)}
|
|
284
|
+
|
|
285
|
+
${actorHooks(vars)}
|
|
286
|
+
|
|
287
|
+
export { idlFactory }
|
|
288
|
+
export type { ${serviceName} }
|
|
289
|
+
`
|
|
290
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for IC Reactor code generation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Information about a canister method extracted from a .did file
|
|
7
|
+
*/
|
|
8
|
+
export interface MethodInfo {
|
|
9
|
+
/** Method name as defined in the Candid interface */
|
|
10
|
+
name: string
|
|
11
|
+
/** Whether this method is a query or update (mutation) */
|
|
12
|
+
type: "query" | "mutation"
|
|
13
|
+
/** Whether the method accepts arguments */
|
|
14
|
+
hasArgs: boolean
|
|
15
|
+
/** Human-readable description of the arguments */
|
|
16
|
+
argsDescription?: string
|
|
17
|
+
/** Human-readable description of the return type */
|
|
18
|
+
returnDescription?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configuration for a single canister
|
|
23
|
+
*/
|
|
24
|
+
export interface CanisterConfig {
|
|
25
|
+
/** Name of the canister (used for variable naming) */
|
|
26
|
+
name?: string
|
|
27
|
+
/** Path to the .did file */
|
|
28
|
+
didFile: string
|
|
29
|
+
/** Output directory (default: ./src/canisters/<name>) */
|
|
30
|
+
outDir?: string
|
|
31
|
+
/** Use DisplayReactor for automatic type transformations (default: true) */
|
|
32
|
+
useDisplayReactor?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* Path to import ClientManager from (relative to generated file).
|
|
35
|
+
* The file at this path should export: { clientManager: ClientManager }
|
|
36
|
+
* Default: "../../lib/client"
|
|
37
|
+
*/
|
|
38
|
+
clientManagerPath?: string
|
|
39
|
+
/** Custom canister ID (optional, uses environment by default) */
|
|
40
|
+
canisterId?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hook generation type
|
|
45
|
+
*/
|
|
46
|
+
export type HookType =
|
|
47
|
+
| "query"
|
|
48
|
+
| "suspenseQuery"
|
|
49
|
+
| "infiniteQuery"
|
|
50
|
+
| "suspenseInfiniteQuery"
|
|
51
|
+
| "mutation"
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Options for generating a hook file
|
|
55
|
+
*/
|
|
56
|
+
export interface GeneratorOptions {
|
|
57
|
+
canisterName: string
|
|
58
|
+
methodName: string
|
|
59
|
+
methodType: "query" | "mutation"
|
|
60
|
+
hasArgs: boolean
|
|
61
|
+
outDir: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Options for reactor file generation
|
|
66
|
+
*/
|
|
67
|
+
export interface ReactorGeneratorOptions {
|
|
68
|
+
canisterName: string
|
|
69
|
+
canisterConfig: CanisterConfig
|
|
70
|
+
/** Global default clientManagerPath */
|
|
71
|
+
globalClientManagerPath?: string
|
|
72
|
+
/** Whether declarations have been generated */
|
|
73
|
+
hasDeclarations?: boolean
|
|
74
|
+
/** Whether to generate advanced per-method hooks (default: false) */
|
|
75
|
+
advanced?: boolean
|
|
76
|
+
/** DID content (required when advanced=true) */
|
|
77
|
+
didContent?: string
|
|
78
|
+
}
|