@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,163 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Reactor File Generation > Advanced Mode > generates per-method hooks correctly 1`] = `
4
+ "/**
5
+ * MyCanister Reactor (Advanced)
6
+ *
7
+ * Auto-generated by @ic-reactor/codegen
8
+ * Includes reactor instance, actor hooks, and per-method static hooks.
9
+ */
10
+
11
+ import {
12
+ DisplayReactor,
13
+ createActorHooks,
14
+ createQuery,
15
+ createMutation,
16
+ } from "@ic-reactor/react"
17
+ import { clientManager } from "../../client"
18
+ import { idlFactory, type _SERVICE } from "./declarations/my_canister.did"
19
+
20
+ type MyCanisterService = _SERVICE
21
+
22
+ /**
23
+ * MyCanister Reactor — Display mode.
24
+ * Automatically converts bigint → string, Principal → string, etc.
25
+ */
26
+ export const myCanisterReactor = new DisplayReactor<MyCanisterService>({
27
+ clientManager,
28
+ idlFactory,
29
+ name: "my_canister",
30
+ })
31
+
32
+ const {
33
+ useActorQuery: useMyCanisterQuery,
34
+ useActorSuspenseQuery: useMyCanisterSuspenseQuery,
35
+ useActorInfiniteQuery: useMyCanisterInfiniteQuery,
36
+ useActorSuspenseInfiniteQuery: useMyCanisterSuspenseInfiniteQuery,
37
+ useActorMutation: useMyCanisterMutation,
38
+ useActorMethod: useMyCanisterMethod,
39
+ } = createActorHooks(myCanisterReactor)
40
+
41
+ export {
42
+ useMyCanisterQuery,
43
+ useMyCanisterSuspenseQuery,
44
+ useMyCanisterInfiniteQuery,
45
+ useMyCanisterSuspenseInfiniteQuery,
46
+ useMyCanisterMutation,
47
+ useMyCanisterMethod,
48
+ }
49
+
50
+ // Per-method static hooks (no-args methods only)
51
+
52
+ export const listItemsQuery = createQuery(myCanisterReactor, {
53
+ functionName: "list_items",
54
+ })
55
+
56
+ export const updateStatusMutation = createMutation(myCanisterReactor, {
57
+ functionName: "update_status",
58
+ })
59
+
60
+ export { idlFactory }
61
+ export type { MyCanisterService }
62
+ "
63
+ `;
64
+
65
+ exports[`Reactor File Generation > Fallback Mode > generates correctly without declarations 1`] = `
66
+ "/**
67
+ * MyCanister Reactor
68
+ *
69
+ * Auto-generated by @ic-reactor/codegen
70
+ *
71
+ * ⚠️ Declarations were not generated. Run:
72
+ * npx @icp-sdk/bindgen --input <path-to-did> --output ./my_canister/declarations
73
+ * Then uncomment the import below and remove the fallback type.
74
+ */
75
+
76
+ import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
77
+ import { clientManager } from "../../client"
78
+
79
+ // TODO: Uncomment after generating declarations:
80
+ // import { idlFactory, type _SERVICE as MyCanisterService } from "./declarations/my_canister.did"
81
+
82
+ // Fallback — replace with generated types
83
+ type MyCanisterService = Record<string, (...args: unknown[]) => Promise<unknown>>
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ const idlFactory = ({ IDL }: { IDL: any }) => IDL.Service({})
86
+
87
+ /**
88
+ * MyCanister Reactor — Display mode.
89
+ * Automatically converts bigint → string, Principal → string, etc.
90
+ */
91
+ export const myCanisterReactor = new DisplayReactor<MyCanisterService>({
92
+ clientManager,
93
+ idlFactory,
94
+ name: "my_canister",
95
+ })
96
+
97
+ const {
98
+ useActorQuery: useMyCanisterQuery,
99
+ useActorSuspenseQuery: useMyCanisterSuspenseQuery,
100
+ useActorInfiniteQuery: useMyCanisterInfiniteQuery,
101
+ useActorSuspenseInfiniteQuery: useMyCanisterSuspenseInfiniteQuery,
102
+ useActorMutation: useMyCanisterMutation,
103
+ useActorMethod: useMyCanisterMethod,
104
+ } = createActorHooks(myCanisterReactor)
105
+
106
+ export {
107
+ useMyCanisterQuery,
108
+ useMyCanisterSuspenseQuery,
109
+ useMyCanisterInfiniteQuery,
110
+ useMyCanisterSuspenseInfiniteQuery,
111
+ useMyCanisterMutation,
112
+ useMyCanisterMethod,
113
+ }
114
+
115
+ export { idlFactory }
116
+ export type { MyCanisterService }
117
+ "
118
+ `;
119
+
120
+ exports[`Reactor File Generation > Simple Mode > generates correctly 1`] = `
121
+ "/**
122
+ * MyCanister Reactor
123
+ *
124
+ * Auto-generated by @ic-reactor/codegen
125
+ */
126
+
127
+ import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
128
+ import { clientManager } from "../../client"
129
+ import { idlFactory, type _SERVICE } from "./declarations/my_canister.did"
130
+
131
+ export type MyCanisterService = _SERVICE
132
+
133
+ /**
134
+ * MyCanister Reactor — Display mode.
135
+ * Automatically converts bigint → string, Principal → string, etc.
136
+ */
137
+ export const myCanisterReactor = new DisplayReactor<MyCanisterService>({
138
+ clientManager,
139
+ idlFactory,
140
+ name: "my_canister",
141
+ })
142
+
143
+ const {
144
+ useActorQuery: useMyCanisterQuery,
145
+ useActorSuspenseQuery: useMyCanisterSuspenseQuery,
146
+ useActorInfiniteQuery: useMyCanisterInfiniteQuery,
147
+ useActorSuspenseInfiniteQuery: useMyCanisterSuspenseInfiniteQuery,
148
+ useActorMutation: useMyCanisterMutation,
149
+ useActorMethod: useMyCanisterMethod,
150
+ } = createActorHooks(myCanisterReactor)
151
+
152
+ export {
153
+ useMyCanisterQuery,
154
+ useMyCanisterSuspenseQuery,
155
+ useMyCanisterInfiniteQuery,
156
+ useMyCanisterSuspenseInfiniteQuery,
157
+ useMyCanisterMutation,
158
+ useMyCanisterMethod,
159
+ }
160
+
161
+ export { idlFactory }
162
+ "
163
+ `;
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import {
3
+ generateInfiniteQueryHook,
4
+ InfiniteQueryHookOptions,
5
+ } from "./infiniteQuery"
6
+ import type { MethodInfo } from "../types"
7
+
8
+ describe("Infinite Query Hook Generation", () => {
9
+ const listItems: MethodInfo = {
10
+ name: "list_items",
11
+ type: "query",
12
+ hasArgs: true,
13
+ }
14
+
15
+ it("generates infinite query hook correctly", () => {
16
+ const options: InfiniteQueryHookOptions = {
17
+ canisterName: "my_canister",
18
+ method: listItems,
19
+ type: "infiniteQuery",
20
+ }
21
+ const result = generateInfiniteQueryHook(options)
22
+ expect(result).toMatchSnapshot()
23
+ expect(result).toContain("createInfiniteQuery(myCanisterReactor, {")
24
+ expect(result).toContain(
25
+ "export const useListItemsInfiniteQuery = listItemsInfiniteQuery.useInfiniteQuery"
26
+ )
27
+ // Should contain default pagination logic placeholders
28
+ expect(result).toContain("initialPageParam: 0 as PageCursor")
29
+ })
30
+
31
+ it("handles suspense infinite query type", () => {
32
+ const options: InfiniteQueryHookOptions = {
33
+ canisterName: "my_canister",
34
+ method: listItems,
35
+ type: "suspenseInfiniteQuery",
36
+ }
37
+ const result = generateInfiniteQueryHook(options)
38
+ expect(result).toContain(
39
+ "export const useListItemsSuspenseInfiniteQuery = listItemsSuspenseInfiniteQuery.useInfiniteQuery"
40
+ )
41
+ })
42
+ })
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Infinite Query hook template generator
3
+ *
4
+ * Generates createInfiniteQuery-based hooks for paginated canister methods.
5
+ */
6
+
7
+ import type { MethodInfo, HookType } from "../types.js"
8
+ import {
9
+ getReactorName,
10
+ getServiceTypeName,
11
+ getHookExportName,
12
+ getReactHookName,
13
+ } from "../naming.js"
14
+
15
+ export interface InfiniteQueryHookOptions {
16
+ canisterName: string
17
+ method: MethodInfo
18
+ type?: HookType
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 = "infiniteQuery" } = options
28
+
29
+ const reactorName = getReactorName(canisterName)
30
+ const serviceName = getServiceTypeName(canisterName)
31
+ const hookExportName = getHookExportName(method.name, type)
32
+ const reactHookName = getReactHookName(method.name, type)
33
+
34
+ return `/**
35
+ * Infinite Query: ${method.name}
36
+ *
37
+ * Auto-generated by @ic-reactor/codegen
38
+ *
39
+ * ⚠️ CUSTOMIZATION REQUIRED: Configure getArgs and getNextPageParam below.
40
+ *
41
+ * @example
42
+ * const { data, fetchNextPage, hasNextPage } = ${hookExportName}.useInfiniteQuery()
43
+ * const allItems = data?.pages.flatMap(page => page.items) ?? []
44
+ */
45
+
46
+ import { createInfiniteQuery } from "@ic-reactor/react"
47
+ import { ${reactorName}, type ${serviceName} } from "../reactor"
48
+
49
+ /** Define your pagination cursor type */
50
+ type PageCursor = number
51
+
52
+ export const ${hookExportName} = createInfiniteQuery(${reactorName}, {
53
+ functionName: "${method.name}",
54
+
55
+ initialPageParam: 0 as PageCursor,
56
+
57
+ /** Convert page param to method arguments — customize for your API */
58
+ getArgs: (pageParam: PageCursor) => {
59
+ return [{ offset: pageParam, limit: 10 }] as Parameters<${serviceName}["${method.name}"]>
60
+ },
61
+
62
+ /** Extract next page param — return undefined when no more pages */
63
+ getNextPageParam: (lastPage, allPages, lastPageParam) => {
64
+ // Example: offset-based
65
+ // if (lastPage.items.length < 10) return undefined
66
+ // return lastPageParam + 10
67
+ return undefined
68
+ },
69
+ })
70
+
71
+ /** React hook for paginated ${method.name} */
72
+ export const ${reactHookName} = ${hookExportName}.useInfiniteQuery
73
+ `
74
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { generateMutationHook, MutationHookOptions } from "./mutation"
3
+ import type { MethodInfo } from "../types"
4
+
5
+ describe("Mutation Hook Generation", () => {
6
+ const methodWithArgs: MethodInfo = {
7
+ name: "create_item",
8
+ type: "mutation",
9
+ hasArgs: true,
10
+ }
11
+
12
+ const methodNoArgs: MethodInfo = {
13
+ name: "init_system",
14
+ type: "mutation",
15
+ hasArgs: false,
16
+ }
17
+
18
+ it("generates hook correctly for mutation with args", () => {
19
+ const options: MutationHookOptions = {
20
+ canisterName: "my_canister",
21
+ method: methodWithArgs,
22
+ }
23
+ const result = generateMutationHook(options)
24
+ expect(result).toMatchSnapshot()
25
+ expect(result).toContain("createMutation(myCanisterReactor, {")
26
+ expect(result).toContain(
27
+ "export const useCreateItemMutation = createItemMutation.useMutation"
28
+ )
29
+ })
30
+
31
+ it("generates hook correctly for mutation without args", () => {
32
+ const options: MutationHookOptions = {
33
+ canisterName: "my_canister",
34
+ method: methodNoArgs,
35
+ }
36
+ const result = generateMutationHook(options)
37
+ expect(result).toMatchSnapshot()
38
+ expect(result).toContain("createMutation(myCanisterReactor, {")
39
+ expect(result).toContain(
40
+ "export const executeInitSystem = initSystemMutation.execute"
41
+ )
42
+ })
43
+ })
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Mutation hook template generator
3
+ *
4
+ * Generates createMutation-based hooks for canister update methods.
5
+ */
6
+
7
+ import type { MethodInfo } from "../types.js"
8
+ import { toPascalCase, getHookExportName, getReactorName } from "../naming.js"
9
+
10
+ export interface MutationHookOptions {
11
+ canisterName: string
12
+ method: MethodInfo
13
+ }
14
+
15
+ /**
16
+ * Generate a mutation hook file content
17
+ */
18
+ export function generateMutationHook(options: MutationHookOptions): string {
19
+ const { canisterName, method } = options
20
+
21
+ const pascalMethod = toPascalCase(method.name)
22
+ const reactorName = getReactorName(canisterName)
23
+ const hookExportName = getHookExportName(method.name, "mutation")
24
+
25
+ return `/**
26
+ * Mutation: ${method.name}
27
+ *
28
+ * Auto-generated by @ic-reactor/codegen
29
+ *
30
+ * @example
31
+ * const { mutate, isPending } = ${hookExportName}.useMutation()
32
+ * mutate(${method.hasArgs ? "[arg1, arg2]" : "[]"})
33
+ *
34
+ * // Direct execution (outside React)
35
+ * const result = await ${hookExportName}.execute(${method.hasArgs ? "[arg1, arg2]" : "[]"})
36
+ */
37
+
38
+ import { createMutation } from "@ic-reactor/react"
39
+ import { ${reactorName} } from "../reactor"
40
+
41
+ export const ${hookExportName} = createMutation(${reactorName}, {
42
+ functionName: "${method.name}",
43
+ })
44
+
45
+ /** React hook for ${method.name} */
46
+ export const use${pascalMethod}Mutation = ${hookExportName}.useMutation
47
+
48
+ /** Execute ${method.name} directly (outside React) */
49
+ export const execute${pascalMethod} = ${hookExportName}.execute
50
+ `
51
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { generateQueryHook, QueryHookOptions } from "./query"
3
+ import type { MethodInfo } from "../types"
4
+
5
+ describe("Query Hook Generation", () => {
6
+ const methodWithArgs: MethodInfo = {
7
+ name: "get_user",
8
+ type: "query",
9
+ hasArgs: true,
10
+ }
11
+
12
+ const methodNoArgs: MethodInfo = {
13
+ name: "list_items",
14
+ type: "query",
15
+ hasArgs: false,
16
+ }
17
+
18
+ it("generates hook for query with arguments (factory)", () => {
19
+ const options: QueryHookOptions = {
20
+ canisterName: "my_canister",
21
+ method: methodWithArgs,
22
+ }
23
+ expect(generateQueryHook(options)).toMatchSnapshot()
24
+ })
25
+
26
+ it("generates hook for query without arguments (static)", () => {
27
+ const options: QueryHookOptions = {
28
+ canisterName: "my_canister",
29
+ method: methodNoArgs,
30
+ }
31
+ expect(generateQueryHook(options)).toMatchSnapshot()
32
+ })
33
+
34
+ it("generates suspense hooks correctly", () => {
35
+ const options: QueryHookOptions = {
36
+ canisterName: "my_canister",
37
+ method: methodWithArgs,
38
+ type: "suspenseQuery",
39
+ }
40
+ const result = generateQueryHook(options)
41
+ expect(result).toMatchSnapshot()
42
+ expect(result).toContain("createSuspenseQueryFactory")
43
+ })
44
+ })
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Query hook template generator
3
+ *
4
+ * Generates createQuery/createQueryFactory-based hooks for canister query methods.
5
+ */
6
+
7
+ import type { MethodInfo, HookType } from "../types.js"
8
+ import { getHookExportName, getReactorName } from "../naming.js"
9
+
10
+ export interface QueryHookOptions {
11
+ canisterName: string
12
+ method: MethodInfo
13
+ type?: HookType
14
+ }
15
+
16
+ /**
17
+ * Generate a query hook file content
18
+ */
19
+ export function generateQueryHook(options: QueryHookOptions): string {
20
+ const { canisterName, method, type = "query" } = options
21
+
22
+ const reactorName = getReactorName(canisterName)
23
+ const hookExportName = getHookExportName(method.name, type)
24
+
25
+ const isSuspense = type === "suspenseQuery"
26
+ const creatorFn = isSuspense ? "createSuspenseQuery" : "createQuery"
27
+ const factoryFn = isSuspense
28
+ ? "createSuspenseQueryFactory"
29
+ : "createQueryFactory"
30
+ const hookName = isSuspense ? "useSuspenseQuery" : "useQuery"
31
+
32
+ if (method.hasArgs) {
33
+ return `/**
34
+ * Query Factory: ${method.name}
35
+ *
36
+ * Auto-generated by @ic-reactor/codegen
37
+ *
38
+ * @example
39
+ * const { data } = ${hookExportName}([arg1, arg2]).${hookName}()
40
+ * const data = await ${hookExportName}([arg1, arg2]).fetch()
41
+ * ${hookExportName}([arg1, arg2]).invalidate()
42
+ */
43
+
44
+ import { ${factoryFn} } from "@ic-reactor/react"
45
+ import { ${reactorName} } from "../reactor"
46
+
47
+ export const ${hookExportName} = ${factoryFn}(${reactorName}, {
48
+ functionName: "${method.name}",
49
+ })
50
+ `
51
+ }
52
+
53
+ return `/**
54
+ * Query: ${method.name}
55
+ *
56
+ * Auto-generated by @ic-reactor/codegen
57
+ *
58
+ * @example
59
+ * const { data } = ${hookExportName}.${hookName}()
60
+ * const data = await ${hookExportName}.fetch()
61
+ * ${hookExportName}.invalidate()
62
+ */
63
+
64
+ import { ${creatorFn} } from "@ic-reactor/react"
65
+ import { ${reactorName} } from "../reactor"
66
+
67
+ export const ${hookExportName} = ${creatorFn}(${reactorName}, {
68
+ functionName: "${method.name}",
69
+ })
70
+ `
71
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { generateReactorFile } from "./reactor"
3
+ import type { ReactorGeneratorOptions } from "../types"
4
+
5
+ describe("Reactor File Generation", () => {
6
+ const baseOptions: ReactorGeneratorOptions = {
7
+ canisterName: "my_canister",
8
+ canisterConfig: {
9
+ didFile: "path/to/my_canister.did",
10
+ outDir: "src/declarations/my_canister",
11
+ clientManagerPath: "../../client",
12
+ },
13
+ hasDeclarations: true,
14
+ }
15
+
16
+ describe("Simple Mode", () => {
17
+ it("generates correctly", () => {
18
+ const result = generateReactorFile(baseOptions)
19
+ expect(result).toMatchSnapshot()
20
+ })
21
+
22
+ it("uses default client path if not provided", () => {
23
+ const options = { ...baseOptions, canisterConfig: { didFile: "foo.did" } }
24
+ const result = generateReactorFile(options)
25
+ expect(result).toContain(
26
+ 'import { clientManager } from "../../lib/client"'
27
+ )
28
+ })
29
+ })
30
+
31
+ describe("Advanced Mode", () => {
32
+ it("generates per-method hooks correctly", () => {
33
+ const didContent = `service : {
34
+ get_user: (nat) -> (opt User) query;
35
+ list_items: () -> (vec Item) query;
36
+ create_item: (Item) -> (Result);
37
+ update_status: () -> (Result); // No args mutation
38
+ }`
39
+
40
+ const options: ReactorGeneratorOptions = {
41
+ ...baseOptions,
42
+ advanced: true,
43
+ didContent,
44
+ }
45
+
46
+ const result = generateReactorFile(options)
47
+ expect(result).toMatchSnapshot()
48
+ // Should include static query creation for no-arg method
49
+ expect(result).toContain(
50
+ "export const listItemsQuery = createQuery(myCanisterReactor, {"
51
+ )
52
+ // Should handle mutation
53
+ expect(result).toContain(
54
+ "export const updateStatusMutation = createMutation(myCanisterReactor, {"
55
+ )
56
+ // Should NOT handle methods with args
57
+ expect(result).not.toContain("export const getUserQuery = createQuery")
58
+ expect(result).not.toContain(
59
+ "export const createItemMutation = createMutation"
60
+ )
61
+ })
62
+ })
63
+
64
+ describe("Fallback Mode", () => {
65
+ it("generates correctly without declarations", () => {
66
+ const options: ReactorGeneratorOptions = {
67
+ ...baseOptions,
68
+ hasDeclarations: false,
69
+ }
70
+ const result = generateReactorFile(options)
71
+ expect(result).toMatchSnapshot()
72
+
73
+ expect(result).toContain(
74
+ "type MyCanisterService = Record<string, (...args: unknown[]) => Promise<unknown>>"
75
+ )
76
+ })
77
+ })
78
+ })