@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.
@@ -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
+ }