@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,102 @@
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 ADDED
@@ -0,0 +1,85 @@
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
+ }
package/src/index.ts ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @ic-reactor/codegen
3
+ *
4
+ * Shared code generation utilities for IC Reactor.
5
+ * Used by both @ic-reactor/vite-plugin and @ic-reactor/cli.
6
+ */
7
+
8
+ // Types
9
+ export type {
10
+ MethodInfo,
11
+ CanisterConfig,
12
+ HookType,
13
+ GeneratorOptions,
14
+ ReactorGeneratorOptions,
15
+ } from "./types.js"
16
+
17
+ // Naming utilities
18
+ export {
19
+ toPascalCase,
20
+ toCamelCase,
21
+ getHookFileName,
22
+ getHookExportName,
23
+ getReactHookName,
24
+ getReactorName,
25
+ getServiceTypeName,
26
+ } from "./naming.js"
27
+
28
+ // DID parsing
29
+ export {
30
+ parseDIDFile,
31
+ extractMethods,
32
+ getMethodsByType,
33
+ formatMethodForDisplay,
34
+ } from "./did.js"
35
+
36
+ // Bindgen utilities
37
+ export {
38
+ generateDeclarations,
39
+ declarationsExist,
40
+ saveCandidFile,
41
+ } from "./bindgen.js"
42
+ export type { BindgenOptions, BindgenResult } from "./bindgen.js"
43
+
44
+ // Template generators
45
+ export { generateReactorFile } from "./templates/reactor.js"
46
+ export { generateQueryHook } from "./templates/query.js"
47
+ export type { QueryHookOptions } from "./templates/query.js"
48
+ export { generateMutationHook } from "./templates/mutation.js"
49
+ export type { MutationHookOptions } from "./templates/mutation.js"
50
+ export { generateInfiniteQueryHook } from "./templates/infiniteQuery.js"
51
+ export type { InfiniteQueryHookOptions } from "./templates/infiniteQuery.js"
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import {
3
+ toPascalCase,
4
+ toCamelCase,
5
+ getHookFileName,
6
+ getHookExportName,
7
+ getReactHookName,
8
+ getReactorName,
9
+ getServiceTypeName,
10
+ } from "./naming"
11
+
12
+ describe("Naming Utilities", () => {
13
+ describe("Base Conversions", () => {
14
+ it("converts to PascalCase", () => {
15
+ expect(toPascalCase("get_user")).toBe("GetUser")
16
+ expect(toPascalCase("my-canister")).toBe("MyCanister")
17
+ expect(toPascalCase("list_items_v2")).toBe("ListItemsV2")
18
+ })
19
+
20
+ it("converts to camelCase", () => {
21
+ expect(toCamelCase("get_user")).toBe("getUser")
22
+ expect(toCamelCase("my-canister")).toBe("myCanister")
23
+ expect(toCamelCase("ListItems")).toBe("listItems")
24
+ })
25
+ })
26
+
27
+ describe("Domain-Specific Naming", () => {
28
+ it("generates hook file names", () => {
29
+ expect(getHookFileName("get_user", "query")).toBe("getUserQuery.ts")
30
+ expect(getHookFileName("update_item", "mutation")).toBe(
31
+ "updateItemMutation.ts"
32
+ )
33
+ })
34
+
35
+ it("generates hook export names", () => {
36
+ expect(getHookExportName("get_user", "query")).toBe("getUserQuery")
37
+ expect(getHookExportName("update_item", "mutation")).toBe(
38
+ "updateItemMutation"
39
+ )
40
+ })
41
+
42
+ it("generates React hook names", () => {
43
+ expect(getReactHookName("get_user", "query")).toBe("useGetUserQuery")
44
+ expect(getReactHookName("update_item", "mutation")).toBe(
45
+ "useUpdateItemMutation"
46
+ )
47
+ })
48
+
49
+ it("generates reactor names", () => {
50
+ expect(getReactorName("backend")).toBe("backendReactor")
51
+ expect(getReactorName("my-canister")).toBe("myCanisterReactor")
52
+ })
53
+
54
+ it("generates service type names", () => {
55
+ expect(getServiceTypeName("backend")).toBe("BackendService")
56
+ expect(getServiceTypeName("my-canister")).toBe("MyCanisterService")
57
+ })
58
+ })
59
+ })
package/src/naming.ts ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Naming utilities for code generation
3
+ *
4
+ * Uses the `change-case` library for base transformations,
5
+ * with domain-specific helpers for IC Reactor patterns.
6
+ */
7
+
8
+ import { camelCase, pascalCase } from "change-case"
9
+
10
+ // ═══════════════════════════════════════════════════════════════════════════
11
+ // BASE CASE CONVERSIONS
12
+ // ═══════════════════════════════════════════════════════════════════════════
13
+
14
+ /**
15
+ * Convert string to PascalCase
16
+ * @example toPascalCase("get_message") → "GetMessage"
17
+ * @example toPascalCase("my-canister") → "MyCanister"
18
+ */
19
+ export function toPascalCase(str: string): string {
20
+ return pascalCase(str)
21
+ }
22
+
23
+ /**
24
+ * Convert string to camelCase
25
+ * @example toCamelCase("get_message") → "getMessage"
26
+ * @example toCamelCase("my-canister") → "myCanister"
27
+ */
28
+ export function toCamelCase(str: string): string {
29
+ return camelCase(str)
30
+ }
31
+
32
+ // ═══════════════════════════════════════════════════════════════════════════
33
+ // DOMAIN-SPECIFIC NAMING
34
+ // ═══════════════════════════════════════════════════════════════════════════
35
+
36
+ /**
37
+ * Generate hook file name
38
+ * @example getHookFileName("get_message", "query") → "getMessageQuery.ts"
39
+ */
40
+ export function getHookFileName(methodName: string, hookType: string): string {
41
+ const camelMethod = toCamelCase(methodName)
42
+ const pascalType = toPascalCase(hookType)
43
+ return `${camelMethod}${pascalType}.ts`
44
+ }
45
+
46
+ /**
47
+ * Generate hook export name
48
+ * @example getHookExportName("get_message", "query") → "getMessageQuery"
49
+ */
50
+ export function getHookExportName(
51
+ methodName: string,
52
+ hookType: string
53
+ ): string {
54
+ const camelMethod = toCamelCase(methodName)
55
+ const pascalType = toPascalCase(hookType)
56
+ return `${camelMethod}${pascalType}`
57
+ }
58
+
59
+ /**
60
+ * Generate React hook name (with use prefix)
61
+ * @example getReactHookName("get_message", "query") → "useGetMessageQuery"
62
+ */
63
+ export function getReactHookName(methodName: string, hookType: string): string {
64
+ const pascalMethod = toPascalCase(methodName)
65
+ const pascalType = toPascalCase(hookType)
66
+ return `use${pascalMethod}${pascalType}`
67
+ }
68
+
69
+ /**
70
+ * Generate reactor variable name
71
+ * @example getReactorName("backend") → "backendReactor"
72
+ */
73
+ export function getReactorName(canisterName: string): string {
74
+ return `${toCamelCase(canisterName)}Reactor`
75
+ }
76
+
77
+ /**
78
+ * Generate service type name
79
+ * @example getServiceTypeName("backend") → "BackendService"
80
+ */
81
+ export function getServiceTypeName(canisterName: string): string {
82
+ return `${toPascalCase(canisterName)}Service`
83
+ }
@@ -0,0 +1,44 @@
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
+ `;
@@ -0,0 +1,59 @@
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
+ `;
@@ -0,0 +1,64 @@
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
+ `;