@ic-reactor/codegen 0.12.1 → 0.13.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.
@@ -1,85 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2
- import fs from "node:fs"
3
- import path from "node:path"
4
- import { generateDeclarations } from "./generators/index.js"
5
-
6
- describe("Bindgen", () => {
7
- const mockDidFile = "mock/test.did"
8
- const mockOutDir = "mock/output"
9
- const mockCanisterName = "test_canister"
10
- const validDidContent = `service : {
11
- greet: (text) -> (text) query;
12
- }`
13
-
14
- beforeEach(() => {
15
- // Mock fs methods
16
- vi.spyOn(fs, "existsSync").mockImplementation((p) => {
17
- // Pretend the source DID file exists
18
- if (p.toString() === mockDidFile) return true
19
- return false
20
- })
21
-
22
- vi.spyOn(fs, "mkdirSync").mockImplementation(() => undefined as any)
23
- vi.spyOn(fs, "rmSync").mockImplementation(() => undefined)
24
- vi.spyOn(fs, "readFileSync").mockImplementation(() => validDidContent)
25
- vi.spyOn(fs, "writeFileSync").mockImplementation(() => undefined)
26
- })
27
-
28
- afterEach(() => {
29
- vi.restoreAllMocks()
30
- })
31
-
32
- it("generateDeclarations creates correct files", async () => {
33
- const result = await generateDeclarations({
34
- didFile: mockDidFile,
35
- outDir: mockOutDir,
36
- canisterName: mockCanisterName,
37
- })
38
-
39
- if (!result.success) {
40
- console.error(result.error)
41
- }
42
-
43
- expect(result.success).toBe(true)
44
- expect(result.declarationsDir).toBe(path.join(mockOutDir, "declarations"))
45
-
46
- // Verify file writes
47
- const declarationsDir = path.join(mockOutDir, "declarations")
48
- const jsPath = path.join(declarationsDir, "test.js")
49
- const dtsPath = path.join(declarationsDir, "test.d.ts")
50
-
51
- // Should create directory
52
- expect(fs.mkdirSync).toHaveBeenCalledWith(mockOutDir, { recursive: true })
53
- expect(fs.mkdirSync).toHaveBeenCalledWith(declarationsDir, {
54
- recursive: true,
55
- })
56
-
57
- // Validate exact content using snapshots
58
- expect(fs.writeFileSync).toHaveBeenCalledWith(jsPath, expect.any(String))
59
- expect(fs.writeFileSync).toHaveBeenCalledWith(dtsPath, expect.any(String))
60
-
61
- // Get the arguments of the calls to check content
62
- const jsCall = vi
63
- .mocked(fs.writeFileSync)
64
- .mock.calls.find((call) => call[0] === jsPath)
65
- const dtsCall = vi
66
- .mocked(fs.writeFileSync)
67
- .mock.calls.find((call) => call[0] === dtsPath)
68
-
69
- expect(jsCall?.[1]).toMatchSnapshot("js-declarations")
70
- expect(dtsCall?.[1]).toMatchSnapshot("ts-declarations")
71
- })
72
-
73
- it("returns error if DID file missing", async () => {
74
- vi.spyOn(fs, "existsSync").mockReturnValue(false)
75
-
76
- const result = await generateDeclarations({
77
- didFile: "missing.did",
78
- outDir: mockOutDir,
79
- canisterName: mockCanisterName,
80
- })
81
-
82
- expect(result.success).toBe(false)
83
- expect(result.error).toContain("DID file not found")
84
- })
85
- })
@@ -1,45 +0,0 @@
1
- import { describe, it, expect } from "vitest"
2
- import {
3
- toPascalCase,
4
- toCamelCase,
5
- getReactorName,
6
- getServiceTypeName,
7
- } from "./naming.js"
8
-
9
- describe("Naming Utilities", () => {
10
- describe("Base Conversions", () => {
11
- it("converts to PascalCase", () => {
12
- expect(toPascalCase("get_user")).toBe("GetUser")
13
- expect(toPascalCase("my-canister")).toBe("MyCanister")
14
- expect(toPascalCase("list_items_v2")).toBe("ListItemsV2")
15
- })
16
-
17
- it("converts to camelCase", () => {
18
- expect(toCamelCase("get_user")).toBe("getUser")
19
- expect(toCamelCase("my-canister")).toBe("myCanister")
20
- expect(toCamelCase("ListItems")).toBe("listItems")
21
- })
22
- })
23
-
24
- describe("Domain-Specific Naming", () => {
25
- it("generates reactor names", () => {
26
- expect(getReactorName("backend")).toBe("backendReactor")
27
- expect(getReactorName("my-canister")).toBe("myCanisterReactor")
28
- })
29
-
30
- it("generates service type names", () => {
31
- expect(getServiceTypeName("backend")).toBe("BackendService")
32
- expect(getServiceTypeName("update_item")).toBe("UpdateItemService")
33
- })
34
-
35
- it("generates reactor names", () => {
36
- expect(getReactorName("backend")).toBe("backendReactor")
37
- expect(getReactorName("my-canister")).toBe("myCanisterReactor")
38
- })
39
-
40
- it("generates service type names", () => {
41
- expect(getServiceTypeName("backend")).toBe("BackendService")
42
- expect(getServiceTypeName("my-canister")).toBe("MyCanisterService")
43
- })
44
- })
45
- })
package/src/parser.ts DELETED
@@ -1,79 +0,0 @@
1
- /**
2
- * Candid Parser Utilities
3
- *
4
- * Parses Candid .did files to extract service method signatures.
5
- * Used by the CLI for listing methods and by advanced generators.
6
- */
7
-
8
- import { didToJs } from "@ic-reactor/parser"
9
- import fs from "node:fs"
10
-
11
- // ─────────────────────────────────────────────────────────────────────────────
12
- // TYPES
13
- // ─────────────────────────────────────────────────────────────────────────────
14
-
15
- export type MethodType = "query" | "mutation"
16
-
17
- export interface MethodInfo {
18
- /** Method name as it appears in the Candid service */
19
- name: string
20
- /** "query" for read-only calls, "mutation" for update calls */
21
- type: MethodType
22
- /** True if the method takes at least one argument */
23
- hasArgs: boolean
24
- }
25
-
26
- // ─────────────────────────────────────────────────────────────────────────────
27
- // PARSERS
28
- // ─────────────────────────────────────────────────────────────────────────────
29
-
30
- /**
31
- * Extract method information from raw Candid source text.
32
- *
33
- * Compiles the Candid to JS and inspects the IDL.Service definition.
34
- */
35
- export function extractMethods(didContent: string): MethodInfo[] {
36
- try {
37
- const jsContent = didToJs(didContent)
38
-
39
- const methods: MethodInfo[] = []
40
-
41
- // Find the IDL.Service({...}) body
42
- const serviceMatch = /IDL\.Service\(\{([\s\S]*?)\}\)/.exec(jsContent)
43
- if (!serviceMatch) return methods
44
-
45
- const serviceBody = serviceMatch[1]
46
-
47
- // Match each method: 'methodName': IDL.Func([args], [rets], [annotations])
48
- const methodRegex =
49
- /['"]([\w]+)['"]\s*:\s*IDL\.Func\(\s*\[(.*?)\],\s*\[.*?\],\s*\[(.*?)\]\)/g
50
-
51
- let match: RegExpExecArray | null
52
- while ((match = methodRegex.exec(serviceBody)) !== null) {
53
- const [, name, args, annotations] = match
54
- methods.push({
55
- name,
56
- type: annotations.includes("'query'") ? "query" : "mutation",
57
- hasArgs: args.trim().length > 0,
58
- })
59
- }
60
-
61
- return methods
62
- } catch (error) {
63
- const msg = error instanceof Error ? error.message : String(error)
64
- throw new Error(`Failed to parse Candid: ${msg}`)
65
- }
66
- }
67
-
68
- /**
69
- * Parse a .did file from disk and return its methods.
70
- *
71
- * @throws if the file does not exist or fails to parse
72
- */
73
- export function parseDIDFile(didFilePath: string): MethodInfo[] {
74
- if (!fs.existsSync(didFilePath)) {
75
- throw new Error(`DID file not found: ${didFilePath}`)
76
- }
77
- const content = fs.readFileSync(didFilePath, "utf-8")
78
- return extractMethods(content)
79
- }
@@ -1,355 +0,0 @@
1
- import fs from "node:fs"
2
- import os from "node:os"
3
- import path from "node:path"
4
- import { afterEach, describe, expect, it } from "vitest"
5
- import { runCanisterPipeline } from "./pipeline.js"
6
-
7
- describe("Codegen pipeline", () => {
8
- const tempDirs: string[] = []
9
-
10
- afterEach(() => {
11
- for (const dir of tempDirs) {
12
- fs.rmSync(dir, { recursive: true, force: true })
13
- }
14
- tempDirs.length = 0
15
- })
16
-
17
- function createTempProject() {
18
- const projectRoot = fs.mkdtempSync(
19
- path.join(os.tmpdir(), "ic-reactor-codegen-pipeline-")
20
- )
21
- tempDirs.push(projectRoot)
22
- return projectRoot
23
- }
24
-
25
- function writeDid(projectRoot: string, fileName: string) {
26
- fs.writeFileSync(
27
- path.join(projectRoot, fileName),
28
- `service : {
29
- greet: (text) -> (text) query;
30
- }`
31
- )
32
- }
33
-
34
- it("uses per-canister mode to generate Reactor-based hooks", async () => {
35
- const projectRoot = createTempProject()
36
- writeDid(projectRoot, "workflow_engine.did")
37
-
38
- const result = await runCanisterPipeline({
39
- canisterConfig: {
40
- name: "workflow_engine",
41
- didFile: "workflow_engine.did",
42
- mode: "Reactor",
43
- },
44
- projectRoot,
45
- globalConfig: {
46
- outDir: "src/declarations",
47
- clientManagerPath: "../../clients",
48
- },
49
- })
50
-
51
- expect(result.success).toBe(true)
52
-
53
- const indexPath = path.join(
54
- projectRoot,
55
- "src/declarations/workflow_engine/index.generated.ts"
56
- )
57
- const generated = fs.readFileSync(indexPath, "utf-8")
58
-
59
- expect(generated).toContain("new Reactor<WorkflowEngineService>")
60
- expect(generated).not.toContain("new DisplayReactor<WorkflowEngineService>")
61
- expect(
62
- fs.readFileSync(
63
- path.join(projectRoot, "src/declarations/workflow_engine/index.ts"),
64
- "utf-8"
65
- )
66
- ).toContain('export * from "./index.generated"')
67
- })
68
-
69
- it("writes a configured canisterId into the generated reactor", async () => {
70
- const projectRoot = createTempProject()
71
- writeDid(projectRoot, "workflow.did")
72
-
73
- const result = await runCanisterPipeline({
74
- canisterConfig: {
75
- name: "workflow",
76
- didFile: "workflow.did",
77
- canisterId: "yq4ns-hyaaa-aaaap-akbna-cai",
78
- },
79
- projectRoot,
80
- globalConfig: {
81
- outDir: "src/declarations",
82
- clientManagerPath: "../../clients",
83
- },
84
- })
85
-
86
- expect(result.success).toBe(true)
87
-
88
- const indexPath = path.join(
89
- projectRoot,
90
- "src/declarations/workflow/index.generated.ts"
91
- )
92
- const generated = fs.readFileSync(indexPath, "utf-8")
93
-
94
- expect(generated).toContain('canisterId: "yq4ns-hyaaa-aaaap-akbna-cai"')
95
- expect(generated).toContain('name: "workflow"')
96
- })
97
-
98
- it("can generate declarations without creating reactor files", async () => {
99
- const projectRoot = createTempProject()
100
- writeDid(projectRoot, "backend.did")
101
-
102
- const result = await runCanisterPipeline({
103
- canisterConfig: {
104
- name: "backend",
105
- didFile: "backend.did",
106
- },
107
- projectRoot,
108
- globalConfig: {
109
- outDir: "src/declarations",
110
- clientManagerPath: "../../clients",
111
- },
112
- generateReactor: false,
113
- })
114
-
115
- expect(result.success).toBe(true)
116
- expect(
117
- fs.existsSync(
118
- path.join(
119
- projectRoot,
120
- "src/declarations/backend/declarations/backend.js"
121
- )
122
- )
123
- ).toBe(true)
124
- expect(
125
- fs.existsSync(
126
- path.join(
127
- projectRoot,
128
- "src/declarations/backend/declarations/backend.d.ts"
129
- )
130
- )
131
- ).toBe(true)
132
- expect(
133
- fs.existsSync(
134
- path.join(
135
- projectRoot,
136
- "src/declarations/backend/declarations/backend.did"
137
- )
138
- )
139
- ).toBe(true)
140
- expect(
141
- fs.existsSync(
142
- path.join(projectRoot, "src/declarations/backend/index.generated.ts")
143
- )
144
- ).toBe(false)
145
- expect(
146
- fs.existsSync(path.join(projectRoot, "src/declarations/backend/index.ts"))
147
- ).toBe(false)
148
- expect(
149
- result.files.some((file) => file.filePath.endsWith("index.generated.ts"))
150
- ).toBe(false)
151
- expect(
152
- result.files.some((file) => file.filePath.endsWith("index.ts"))
153
- ).toBe(false)
154
- })
155
-
156
- it("leaves existing reactor files untouched when reactor generation is disabled", async () => {
157
- const projectRoot = createTempProject()
158
- writeDid(projectRoot, "backend.did")
159
-
160
- const canisterOutDir = path.join(projectRoot, "src/declarations/backend")
161
- fs.mkdirSync(canisterOutDir, { recursive: true })
162
-
163
- const existingGenerated = "// existing generated reactor"
164
- const existingEntry = "// existing entry wrapper"
165
-
166
- fs.writeFileSync(
167
- path.join(canisterOutDir, "index.generated.ts"),
168
- existingGenerated
169
- )
170
- fs.writeFileSync(path.join(canisterOutDir, "index.ts"), existingEntry)
171
-
172
- const result = await runCanisterPipeline({
173
- canisterConfig: {
174
- name: "backend",
175
- didFile: "backend.did",
176
- },
177
- projectRoot,
178
- globalConfig: {
179
- outDir: "src/declarations",
180
- clientManagerPath: "../../clients",
181
- },
182
- generateReactor: false,
183
- })
184
-
185
- expect(result.success).toBe(true)
186
- expect(
187
- fs.readFileSync(path.join(canisterOutDir, "index.generated.ts"), "utf-8")
188
- ).toBe(existingGenerated)
189
- expect(
190
- fs.readFileSync(path.join(canisterOutDir, "index.ts"), "utf-8")
191
- ).toBe(existingEntry)
192
- })
193
-
194
- it("supports a core target without generating React hooks", async () => {
195
- const projectRoot = createTempProject()
196
- writeDid(projectRoot, "backend.did")
197
-
198
- const result = await runCanisterPipeline({
199
- canisterConfig: {
200
- name: "backend",
201
- didFile: "backend.did",
202
- },
203
- projectRoot,
204
- globalConfig: {
205
- outDir: "src/declarations",
206
- clientManagerPath: "../../clients",
207
- target: "core",
208
- },
209
- })
210
-
211
- expect(result.success).toBe(true)
212
-
213
- const indexPath = path.join(
214
- projectRoot,
215
- "src/declarations/backend/index.generated.ts"
216
- )
217
- const generated = fs.readFileSync(indexPath, "utf-8")
218
-
219
- expect(generated).toContain(
220
- 'import { DisplayReactor } from "@ic-reactor/core"'
221
- )
222
- expect(generated).not.toContain(
223
- 'import { createActorHooks } from "@ic-reactor/react"'
224
- )
225
- expect(generated).not.toContain("useBackendQuery")
226
- })
227
-
228
- it("does not overwrite user-modified index.ts on regenerate", async () => {
229
- const projectRoot = createTempProject()
230
- writeDid(projectRoot, "backend.did")
231
-
232
- const options = {
233
- canisterConfig: {
234
- name: "backend",
235
- didFile: "backend.did",
236
- },
237
- projectRoot,
238
- globalConfig: {
239
- outDir: "src/declarations",
240
- clientManagerPath: "../../clients",
241
- },
242
- } as const
243
-
244
- const first = await runCanisterPipeline(options)
245
- expect(first.success).toBe(true)
246
-
247
- const indexPath = path.join(
248
- projectRoot,
249
- "src/declarations/backend/index.ts"
250
- )
251
- fs.writeFileSync(
252
- indexPath,
253
- `// user custom canister file
254
- export const customBackendIndex = true
255
- `
256
- )
257
-
258
- const second = await runCanisterPipeline({
259
- ...options,
260
- canisterConfig: {
261
- ...options.canisterConfig,
262
- canisterId: "yq4ns-hyaaa-aaaap-akbna-cai",
263
- },
264
- })
265
- expect(second.success).toBe(true)
266
-
267
- const wrapper = fs.readFileSync(indexPath, "utf-8")
268
- expect(wrapper).toContain("customBackendIndex = true")
269
- const generatedImpl = fs.readFileSync(
270
- path.join(projectRoot, "src/declarations/backend/index.generated.ts"),
271
- "utf-8"
272
- )
273
- expect(generatedImpl).toContain('canisterId: "yq4ns-hyaaa-aaaap-akbna-cai"')
274
- expect(second.files).toEqual(
275
- expect.arrayContaining([
276
- expect.objectContaining({
277
- filePath: indexPath,
278
- skipped: true,
279
- success: true,
280
- }),
281
- ])
282
- )
283
- })
284
-
285
- it("migrates a legacy generated index.ts to the managed wrapper", async () => {
286
- const projectRoot = createTempProject()
287
- writeDid(projectRoot, "backend.did")
288
-
289
- const canisterOutDir = path.join(projectRoot, "src/declarations/backend")
290
- fs.mkdirSync(canisterOutDir, { recursive: true })
291
-
292
- const existingGeneratedIndex = `import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
293
- import { clientManager } from "../../clients"
294
- import { idlFactory, type _SERVICE } from "./declarations/backend"
295
-
296
- export type BackendService = _SERVICE
297
-
298
- /**
299
- * Backend Reactor
300
- *
301
- * Auto-generated by @ic-reactor/codegen — do not edit.
302
- */
303
- export const backendReactor = new DisplayReactor<BackendService>({
304
- clientManager,
305
- idlFactory,
306
- name: "backend",
307
- })
308
-
309
- export const {
310
- useActorQuery: useBackendQuery,
311
- } = createActorHooks(backendReactor)
312
- `
313
- fs.writeFileSync(
314
- path.join(canisterOutDir, "index.ts"),
315
- existingGeneratedIndex
316
- )
317
-
318
- const result = await runCanisterPipeline({
319
- canisterConfig: {
320
- name: "backend",
321
- didFile: "backend.did",
322
- },
323
- projectRoot,
324
- globalConfig: {
325
- outDir: "src/declarations",
326
- clientManagerPath: "../../clients",
327
- },
328
- })
329
-
330
- expect(result.success).toBe(true)
331
-
332
- const indexPath = path.join(canisterOutDir, "index.ts")
333
- const entry = fs.readFileSync(indexPath, "utf-8")
334
- const generated = fs.readFileSync(
335
- path.join(canisterOutDir, "index.generated.ts"),
336
- "utf-8"
337
- )
338
- expect(entry).toContain('export * from "./index.generated"')
339
- expect(entry).not.toBe(existingGeneratedIndex)
340
- expect(generated).toContain("new DisplayReactor<BackendService>")
341
- expect(generated).toContain("useBackendMutation")
342
- expect(result.files).toEqual(
343
- expect.arrayContaining([
344
- expect.objectContaining({
345
- filePath: path.join(canisterOutDir, "index.generated.ts"),
346
- success: true,
347
- }),
348
- expect.objectContaining({
349
- filePath: indexPath,
350
- success: true,
351
- }),
352
- ])
353
- )
354
- })
355
- })
@@ -1,84 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
- import {
3
- generateReactorEntryFile,
4
- generateReactorFile,
5
- } from "./generators/index.js"
6
-
7
- describe("Reactor generator", () => {
8
- it("keeps default behavior as DisplayReactor", () => {
9
- const content = generateReactorFile({
10
- canisterName: "backend",
11
- didFile: "mock/backend.did",
12
- clientManagerPath: "../../clients",
13
- })
14
-
15
- expect(content).toMatchSnapshot("display-reactor-index")
16
- expect(content).toContain("new DisplayReactor<BackendService>")
17
- expect(content).not.toContain("export const BackendReactorMode")
18
- })
19
-
20
- it("supports Reactor mode generation", () => {
21
- const content = generateReactorFile({
22
- canisterName: "workflow_engine",
23
- didFile: "mock/workflow_engine.did",
24
- reactorClass: "Reactor",
25
- })
26
-
27
- expect(content).toMatchSnapshot("reactor-index")
28
- expect(content).toContain("new Reactor<WorkflowEngineService>")
29
- expect(content).not.toContain("createWorkflowEngineDisplayReactor")
30
- })
31
-
32
- it("supports candid reactor subclasses", () => {
33
- const content = generateReactorFile({
34
- canisterName: "ledger",
35
- didFile: "mock/ledger.did",
36
- reactorClass: "MetadataDisplayReactor",
37
- })
38
-
39
- expect(content).toMatchSnapshot("metadata-display-reactor-index")
40
- expect(content).toContain(
41
- 'import { createActorHooks } from "@ic-reactor/react"'
42
- )
43
- expect(content).toContain(
44
- 'import { MetadataDisplayReactor } from "@ic-reactor/candid"'
45
- )
46
- expect(content).toContain("new MetadataDisplayReactor<LedgerService>")
47
- })
48
-
49
- it("supports core target generation without React hooks", () => {
50
- const content = generateReactorFile({
51
- canisterName: "backend",
52
- didFile: "mock/backend.did",
53
- runtimeTarget: "core",
54
- reactorClass: "DisplayReactor",
55
- })
56
-
57
- expect(content).toMatchSnapshot("core-display-reactor-index")
58
- expect(content).toContain(
59
- 'import { DisplayReactor } from "@ic-reactor/core"'
60
- )
61
- expect(content).not.toContain(
62
- 'import { createActorHooks } from "@ic-reactor/react"'
63
- )
64
- expect(content).not.toContain("useBackendQuery")
65
- })
66
-
67
- it("writes a fixed canisterId when configured", () => {
68
- const content = generateReactorFile({
69
- canisterName: "workflow",
70
- didFile: "mock/workflow.did",
71
- canisterId: "yq4ns-hyaaa-aaaap-akbna-cai",
72
- })
73
-
74
- expect(content).toContain('canisterId: "yq4ns-hyaaa-aaaap-akbna-cai"')
75
- expect(content).toContain('name: "workflow"')
76
- })
77
-
78
- it("generates a stable entry wrapper", () => {
79
- const content = generateReactorEntryFile()
80
-
81
- expect(content).toContain('export * from "./index.generated"')
82
- expect(content).toContain("safe to customize")
83
- })
84
- })