@ic-reactor/codegen 0.2.0 → 0.3.1

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.
@@ -1,67 +1,46 @@
1
1
  /**
2
2
  * Reactor file template generator
3
3
  *
4
- * Generates the reactor instance file for a canister.
5
- * Supports simple mode (generic hooks) and advanced mode (per-method typed hooks).
4
+ * Generates the reactor instance file for a canister using DisplayReactor.
5
+ * Standardizes the output to include typed hooks and clean imports.
6
6
  */
7
7
 
8
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"
9
+ import { toPascalCase, getReactorName, getServiceTypeName } from "../naming.js"
10
+
11
+ export type ReactorGeneratorOptions = {
12
+ canisterName: string
13
+ didFile: string
14
+ clientManagerPath?: string
15
+ }
17
16
 
18
17
  /**
19
18
  * Generate the reactor file content
20
19
  */
21
20
  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"
21
+ const pascalName = toPascalCase(options.canisterName)
22
+ const reactorName = getReactorName(options.canisterName)
23
+ const serviceName = getServiceTypeName(options.canisterName)
24
+ // Always use DisplayReactor for now
25
+ const reactorType = "DisplayReactor"
41
26
 
42
- const didFileName = path.basename(canisterConfig.didFile)
43
- const declarationsPath = `./declarations/${didFileName}`
27
+ const didFileName = path.basename(options.didFile)
28
+ const baseName = didFileName.replace(/\.did$/, "")
29
+ const declarationsPath = `./declarations/${baseName}`
30
+ const clientManagerPath = options.clientManagerPath ?? "../../clients"
44
31
 
45
32
  const vars: TemplateVars = {
46
- canisterName,
33
+ canisterName: options.canisterName,
47
34
  pascalName,
48
35
  reactorName,
49
36
  serviceName,
50
37
  reactorType,
51
38
  clientManagerPath,
52
39
  declarationsPath,
53
- useDisplayReactor: canisterConfig.useDisplayReactor !== false,
54
- }
55
-
56
- if (!hasDeclarations) {
57
- return generateFallbackReactorFile(vars)
58
40
  }
59
41
 
60
- if (advanced && didContent) {
61
- return generateAdvancedReactorFile(vars, didContent)
62
- }
63
-
64
- return generateSimpleReactorFile(vars)
42
+ // If we have DID content, we can generate method-specific hooks (clean & typed)
43
+ return generateStandardReactorFile(vars)
65
44
  }
66
45
 
67
46
  // ═══════════════════════════════════════════════════════════════════════════
@@ -76,36 +55,39 @@ interface TemplateVars {
76
55
  reactorType: string
77
56
  clientManagerPath: string
78
57
  declarationsPath: string
79
- useDisplayReactor: boolean
80
58
  }
81
59
 
82
60
  // ═══════════════════════════════════════════════════════════════════════════
83
- // SHARED SECTIONS
61
+ // STANDARD MODE
84
62
  // ═══════════════════════════════════════════════════════════════════════════
85
63
 
86
- function reactorInstance(vars: TemplateVars): string {
64
+ function generateStandardReactorFile(vars: TemplateVars): string {
87
65
  const {
88
66
  pascalName,
89
67
  reactorName,
90
68
  serviceName,
91
69
  reactorType,
70
+ clientManagerPath,
71
+ declarationsPath,
92
72
  canisterName,
93
- useDisplayReactor,
94
73
  } = vars
95
- return `/**
96
- * ${pascalName} Reactor — ${useDisplayReactor ? "Display" : "Candid"} mode.
97
- * ${useDisplayReactor ? "Automatically converts bigint → string, Principal → string, etc." : "Uses raw Candid types."}
74
+
75
+ return `import { ${reactorType}, createActorHooks } from "@ic-reactor/react"
76
+ import { clientManager } from "${clientManagerPath}"
77
+ import { idlFactory, type _SERVICE } from "${declarationsPath}"
78
+
79
+ export type ${serviceName} = _SERVICE
80
+
81
+ /**
82
+ * ${pascalName} Display Reactor
98
83
  */
99
84
  export const ${reactorName} = new ${reactorType}<${serviceName}>({
100
85
  clientManager,
101
86
  idlFactory,
102
87
  name: "${canisterName}",
103
- })`
104
- }
88
+ })
105
89
 
106
- function actorHooks(vars: TemplateVars): string {
107
- const { pascalName, reactorName } = vars
108
- return `const {
90
+ export const {
109
91
  useActorQuery: use${pascalName}Query,
110
92
  useActorSuspenseQuery: use${pascalName}SuspenseQuery,
111
93
  useActorInfiniteQuery: use${pascalName}InfiniteQuery,
@@ -113,178 +95,5 @@ function actorHooks(vars: TemplateVars): string {
113
95
  useActorMutation: use${pascalName}Mutation,
114
96
  useActorMethod: use${pascalName}Method,
115
97
  } = 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
98
  `
290
99
  }
package/src/did.test.ts DELETED
@@ -1,102 +0,0 @@
1
- import { describe, it, expect } from "vitest"
2
- import { extractMethods, formatMethodForDisplay } from "./did"
3
- import type { MethodInfo } from "./types"
4
-
5
- describe("DID Utilities", () => {
6
- describe("extractMethods", () => {
7
- it("extracts query methods", () => {
8
- const didContent = `
9
- type User = record { name : text; age : nat };
10
- type Item = record { name : text; price : nat };
11
-
12
- service : {
13
- get_user: (nat) -> (opt User) query;
14
- list_items: () -> (vec Item) query;
15
- }`
16
- const methods = extractMethods(didContent)
17
- expect(methods).toHaveLength(2)
18
- expect(methods[0]).toMatchObject({
19
- name: "get_user",
20
- type: "query",
21
- hasArgs: true,
22
- })
23
- expect(methods[1]).toMatchObject({
24
- name: "list_items",
25
- type: "query",
26
- hasArgs: false,
27
- })
28
- })
29
-
30
- it("extracts mutation (update) methods", () => {
31
- const didContent = `service : {
32
- create_user: (User) -> (Result);
33
- update_item: (nat, Item) -> (Result);
34
- }`
35
- const methods = extractMethods(didContent)
36
- expect(methods).toHaveLength(2)
37
- expect(methods[0]).toMatchObject({
38
- name: "create_user",
39
- type: "mutation",
40
- hasArgs: true,
41
- })
42
- expect(methods[1]).toMatchObject({
43
- name: "update_item",
44
- type: "mutation",
45
- hasArgs: true,
46
- })
47
- })
48
-
49
- it("handles composite_query as query", () => {
50
- const didContent = `service : {
51
- search: (text) -> (vec Item) composite_query;
52
- }`
53
- const methods = extractMethods(didContent)
54
- expect(methods[0].type).toBe("query")
55
- })
56
-
57
- it("ignores comments", () => {
58
- const didContent = `service : {
59
- // This is a comment
60
- get_data: () -> (text) query;
61
- /* create_data: (text) -> (); */
62
- }`
63
- const methods = extractMethods(didContent)
64
- expect(methods).toHaveLength(1)
65
- expect(methods[0].name).toBe("get_data")
66
- })
67
-
68
- it("extracts argument and return types correctly", () => {
69
- const didContent = `service : {
70
- weird_method: (record { foo: text; bar: nat }) -> (variant { Ok: null; Err: text }) query;
71
- }`
72
- const methods = extractMethods(didContent)
73
- expect(methods[0].name).toBe("weird_method")
74
- expect(methods[0].argsDescription).toContain(
75
- "record { foo: text; bar: nat }"
76
- )
77
- expect(methods[0].returnDescription).toContain(
78
- "variant { Ok: null; Err: text }"
79
- )
80
- })
81
- })
82
-
83
- describe("formatMethodForDisplay", () => {
84
- it("formats query method with args", () => {
85
- const method: MethodInfo = {
86
- name: "search",
87
- type: "query",
88
- hasArgs: true,
89
- }
90
- expect(formatMethodForDisplay(method)).toBe("search (query, with args)")
91
- })
92
-
93
- it("formats update method without args", () => {
94
- const method: MethodInfo = {
95
- name: "init",
96
- type: "mutation",
97
- hasArgs: false,
98
- }
99
- expect(formatMethodForDisplay(method)).toBe("init (update, no args)")
100
- })
101
- })
102
- })
package/src/did.ts DELETED
@@ -1,85 +0,0 @@
1
- /**
2
- * DID file parser
3
- *
4
- * Extracts method information from Candid interface definition files.
5
- * Based on the CLI's parser implementation (with comment stripping).
6
- */
7
-
8
- import fs from "node:fs"
9
- import type { MethodInfo } from "./types.js"
10
-
11
- /**
12
- * Parse a .did file and extract method information
13
- */
14
- export function parseDIDFile(didFilePath: string): MethodInfo[] {
15
- if (!fs.existsSync(didFilePath)) {
16
- throw new Error(`DID file not found: ${didFilePath}`)
17
- }
18
-
19
- const content = fs.readFileSync(didFilePath, "utf-8")
20
- return extractMethods(content)
21
- }
22
-
23
- /**
24
- * Extract methods from DID content
25
- *
26
- * Handles formats like:
27
- * - `name : (args) -> (result)`
28
- * - `name : (args) -> (result) query`
29
- * - `name : (args) -> (result) composite_query`
30
- * - `name : func (args) -> (result)`
31
- */
32
- export function extractMethods(didContent: string): MethodInfo[] {
33
- const methods: MethodInfo[] = []
34
-
35
- // Remove comments
36
- const cleanContent = didContent
37
- .replace(/\/\/.*$/gm, "") // Single line comments
38
- .replace(/\/\*[\s\S]*?\*\//g, "") // Multi-line comments
39
-
40
- // Match method definitions
41
- // Pattern: name : [func] (args) -> (result) [query|composite_query]
42
- const methodRegex =
43
- /([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(?:func\s*)?\(([^)]*)\)\s*->\s*\(([^)]*)\)\s*(query|composite_query)?/g
44
-
45
- let match
46
- while ((match = methodRegex.exec(cleanContent)) !== null) {
47
- const name = match[1]
48
- const args = match[2].trim()
49
- const returnType = match[3].trim()
50
- const queryAnnotation = match[4]
51
-
52
- // Determine if it's a query or mutation
53
- const isQuery =
54
- queryAnnotation === "query" || queryAnnotation === "composite_query"
55
-
56
- methods.push({
57
- name,
58
- type: isQuery ? "query" : "mutation",
59
- hasArgs: args.length > 0 && args !== "",
60
- argsDescription: args || undefined,
61
- returnDescription: returnType || undefined,
62
- })
63
- }
64
-
65
- return methods
66
- }
67
-
68
- /**
69
- * Get methods by type
70
- */
71
- export function getMethodsByType(
72
- methods: MethodInfo[],
73
- type: "query" | "mutation"
74
- ): MethodInfo[] {
75
- return methods.filter((m) => m.type === type)
76
- }
77
-
78
- /**
79
- * Format method info for display
80
- */
81
- export function formatMethodForDisplay(method: MethodInfo): string {
82
- const typeLabel = method.type === "query" ? "query" : "update"
83
- const argsLabel = method.hasArgs ? "with args" : "no args"
84
- return `${method.name} (${typeLabel}, ${argsLabel})`
85
- }
@@ -1,44 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`Infinite Query Hook Generation > generates infinite query hook correctly 1`] = `
4
- "/**
5
- * Infinite Query: list_items
6
- *
7
- * Auto-generated by @ic-reactor/codegen
8
- *
9
- * ⚠️ CUSTOMIZATION REQUIRED: Configure getArgs and getNextPageParam below.
10
- *
11
- * @example
12
- * const { data, fetchNextPage, hasNextPage } = listItemsInfiniteQuery.useInfiniteQuery()
13
- * const allItems = data?.pages.flatMap(page => page.items) ?? []
14
- */
15
-
16
- import { createInfiniteQuery } from "@ic-reactor/react"
17
- import { myCanisterReactor, type MyCanisterService } from "../reactor"
18
-
19
- /** Define your pagination cursor type */
20
- type PageCursor = number
21
-
22
- export const listItemsInfiniteQuery = createInfiniteQuery(myCanisterReactor, {
23
- functionName: "list_items",
24
-
25
- initialPageParam: 0 as PageCursor,
26
-
27
- /** Convert page param to method arguments — customize for your API */
28
- getArgs: (pageParam: PageCursor) => {
29
- return [{ offset: pageParam, limit: 10 }] as Parameters<MyCanisterService["list_items"]>
30
- },
31
-
32
- /** Extract next page param — return undefined when no more pages */
33
- getNextPageParam: (lastPage, allPages, lastPageParam) => {
34
- // Example: offset-based
35
- // if (lastPage.items.length < 10) return undefined
36
- // return lastPageParam + 10
37
- return undefined
38
- },
39
- })
40
-
41
- /** React hook for paginated list_items */
42
- export const useListItemsInfiniteQuery = listItemsInfiniteQuery.useInfiniteQuery
43
- "
44
- `;
@@ -1,59 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`Mutation Hook Generation > generates hook correctly for mutation with args 1`] = `
4
- "/**
5
- * Mutation: create_item
6
- *
7
- * Auto-generated by @ic-reactor/codegen
8
- *
9
- * @example
10
- * const { mutate, isPending } = createItemMutation.useMutation()
11
- * mutate([arg1, arg2])
12
- *
13
- * // Direct execution (outside React)
14
- * const result = await createItemMutation.execute([arg1, arg2])
15
- */
16
-
17
- import { createMutation } from "@ic-reactor/react"
18
- import { myCanisterReactor } from "../reactor"
19
-
20
- export const createItemMutation = createMutation(myCanisterReactor, {
21
- functionName: "create_item",
22
- })
23
-
24
- /** React hook for create_item */
25
- export const useCreateItemMutation = createItemMutation.useMutation
26
-
27
- /** Execute create_item directly (outside React) */
28
- export const executeCreateItem = createItemMutation.execute
29
- "
30
- `;
31
-
32
- exports[`Mutation Hook Generation > generates hook correctly for mutation without args 1`] = `
33
- "/**
34
- * Mutation: init_system
35
- *
36
- * Auto-generated by @ic-reactor/codegen
37
- *
38
- * @example
39
- * const { mutate, isPending } = initSystemMutation.useMutation()
40
- * mutate([])
41
- *
42
- * // Direct execution (outside React)
43
- * const result = await initSystemMutation.execute([])
44
- */
45
-
46
- import { createMutation } from "@ic-reactor/react"
47
- import { myCanisterReactor } from "../reactor"
48
-
49
- export const initSystemMutation = createMutation(myCanisterReactor, {
50
- functionName: "init_system",
51
- })
52
-
53
- /** React hook for init_system */
54
- export const useInitSystemMutation = initSystemMutation.useMutation
55
-
56
- /** Execute init_system directly (outside React) */
57
- export const executeInitSystem = initSystemMutation.execute
58
- "
59
- `;
@@ -1,64 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`Query Hook Generation > generates hook for query with arguments (factory) 1`] = `
4
- "/**
5
- * Query Factory: get_user
6
- *
7
- * Auto-generated by @ic-reactor/codegen
8
- *
9
- * @example
10
- * const { data } = getUserQuery([arg1, arg2]).useQuery()
11
- * const data = await getUserQuery([arg1, arg2]).fetch()
12
- * getUserQuery([arg1, arg2]).invalidate()
13
- */
14
-
15
- import { createQueryFactory } from "@ic-reactor/react"
16
- import { myCanisterReactor } from "../reactor"
17
-
18
- export const getUserQuery = createQueryFactory(myCanisterReactor, {
19
- functionName: "get_user",
20
- })
21
- "
22
- `;
23
-
24
- exports[`Query Hook Generation > generates hook for query without arguments (static) 1`] = `
25
- "/**
26
- * Query: list_items
27
- *
28
- * Auto-generated by @ic-reactor/codegen
29
- *
30
- * @example
31
- * const { data } = listItemsQuery.useQuery()
32
- * const data = await listItemsQuery.fetch()
33
- * listItemsQuery.invalidate()
34
- */
35
-
36
- import { createQuery } from "@ic-reactor/react"
37
- import { myCanisterReactor } from "../reactor"
38
-
39
- export const listItemsQuery = createQuery(myCanisterReactor, {
40
- functionName: "list_items",
41
- })
42
- "
43
- `;
44
-
45
- exports[`Query Hook Generation > generates suspense hooks correctly 1`] = `
46
- "/**
47
- * Query Factory: get_user
48
- *
49
- * Auto-generated by @ic-reactor/codegen
50
- *
51
- * @example
52
- * const { data } = getUserSuspenseQuery([arg1, arg2]).useSuspenseQuery()
53
- * const data = await getUserSuspenseQuery([arg1, arg2]).fetch()
54
- * getUserSuspenseQuery([arg1, arg2]).invalidate()
55
- */
56
-
57
- import { createSuspenseQueryFactory } from "@ic-reactor/react"
58
- import { myCanisterReactor } from "../reactor"
59
-
60
- export const getUserSuspenseQuery = createSuspenseQueryFactory(myCanisterReactor, {
61
- functionName: "get_user",
62
- })
63
- "
64
- `;