@ic-reactor/codegen 0.7.1 → 0.7.2

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.
package/README.md CHANGED
@@ -17,6 +17,7 @@ import { runCanisterPipeline } from "@ic-reactor/codegen"
17
17
  await runCanisterPipeline({
18
18
  canisterConfig: {
19
19
  name: "backend",
20
+ mode: "DisplayReactor",
20
21
  didFile: "./backend.did",
21
22
  },
22
23
  projectRoot: process.cwd(),
@@ -27,12 +28,24 @@ await runCanisterPipeline({
27
28
  })
28
29
  ```
29
30
 
31
+ ## Reactor Class Configuration
32
+
33
+ Set `canisterConfig.mode` to choose the generated reactor class:
34
+
35
+ - `DisplayReactor` (default)
36
+ - `Reactor`
37
+ - `CandidReactor`
38
+ - `CandidDisplayReactor`
39
+ - `MetadataDisplayReactor`
40
+
41
+ Codegen writes a single `index.ts`. It overwrites that file only while it is still recognized as a generated file; if you replace it with your own file, regeneration leaves it untouched.
42
+
30
43
  ## Generators
31
44
 
32
45
  You can also use individual generators if you need more granular control:
33
46
 
34
47
  - **`generateDeclarations`**: Generates `.js` (factory), `.d.ts` (types), and `.did` copy.
35
- - **`generateReactorFile`**: Generates the `index.ts` file with `DisplayReactor` and hooks.
48
+ - **`generateReactorFile`**: Generates the `index.ts` file using either `DisplayReactor` or `Reactor`.
36
49
  - **`generateClientFile`**: Generates a `ClientManager` boilerplate file.
37
50
 
38
51
  ## Utilities
package/dist/index.cjs CHANGED
@@ -123,14 +123,32 @@ function getServiceTypeName(canisterName) {
123
123
  }
124
124
 
125
125
  // src/generators/reactor.ts
126
+ function getReactorClassImportSource(reactorClass) {
127
+ switch (reactorClass) {
128
+ case "Reactor":
129
+ case "DisplayReactor":
130
+ return "@ic-reactor/react";
131
+ case "CandidReactor":
132
+ case "CandidDisplayReactor":
133
+ case "MetadataDisplayReactor":
134
+ return "@ic-reactor/candid";
135
+ }
136
+ }
126
137
  function generateReactorFile(options) {
127
- const { canisterName, didFile, clientManagerPath = "../../clients" } = options;
138
+ const {
139
+ canisterName,
140
+ didFile,
141
+ clientManagerPath = "../../clients",
142
+ reactorClass = "DisplayReactor"
143
+ } = options;
128
144
  const pascalName = toPascalCase(canisterName);
129
145
  const reactorName = getReactorName(canisterName);
130
146
  const serviceName = getServiceTypeName(canisterName);
131
147
  const baseName = import_node_path2.default.basename(didFile, ".did");
132
148
  const declarationsPath = `./declarations/${baseName}`;
133
- return `import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
149
+ const reactorImportSource = getReactorClassImportSource(reactorClass);
150
+ return `import { createActorHooks } from "@ic-reactor/react"
151
+ import { ${reactorClass} } from "${reactorImportSource}"
134
152
  import { clientManager } from "${clientManagerPath}"
135
153
  import { idlFactory, type _SERVICE } from "${declarationsPath}"
136
154
 
@@ -142,7 +160,7 @@ export type ${serviceName} = _SERVICE
142
160
  * Auto-generated by @ic-reactor/codegen \u2014 do not edit.
143
161
  * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
144
162
  */
145
- export const ${reactorName} = new DisplayReactor<${serviceName}>({
163
+ export const ${reactorName} = new ${reactorClass}<${serviceName}>({
146
164
  clientManager,
147
165
  idlFactory,
148
166
  name: "${canisterName}",
@@ -160,6 +178,12 @@ export const {
160
178
  }
161
179
 
162
180
  // src/pipeline.ts
181
+ function resolveReactorClass(canisterConfig) {
182
+ return canisterConfig.mode ?? "DisplayReactor";
183
+ }
184
+ function isGeneratedIndexFile(content) {
185
+ return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(");
186
+ }
163
187
  async function runCanisterPipeline(options) {
164
188
  const { canisterConfig, projectRoot, globalConfig } = options;
165
189
  const { name, didFile, clientManagerPath } = canisterConfig;
@@ -199,15 +223,27 @@ async function runCanisterPipeline(options) {
199
223
  };
200
224
  }
201
225
  const reactorPath = import_node_path3.default.join(canisterOutDir, "index.ts");
226
+ const reactorClass = resolveReactorClass(canisterConfig);
202
227
  try {
203
228
  const reactorContent = generateReactorFile({
204
229
  canisterName: name,
205
230
  didFile: resolvedDidFile,
206
- clientManagerPath: resolvedClientManagerPath
231
+ clientManagerPath: resolvedClientManagerPath,
232
+ reactorClass
207
233
  });
208
234
  import_node_fs2.default.mkdirSync(canisterOutDir, { recursive: true });
209
- import_node_fs2.default.writeFileSync(reactorPath, reactorContent);
210
- files.push({ success: true, filePath: reactorPath });
235
+ if (!import_node_fs2.default.existsSync(reactorPath)) {
236
+ import_node_fs2.default.writeFileSync(reactorPath, reactorContent);
237
+ files.push({ success: true, filePath: reactorPath });
238
+ } else {
239
+ const existingIndexContent = import_node_fs2.default.readFileSync(reactorPath, "utf-8");
240
+ if (isGeneratedIndexFile(existingIndexContent)) {
241
+ import_node_fs2.default.writeFileSync(reactorPath, reactorContent);
242
+ files.push({ success: true, filePath: reactorPath });
243
+ } else {
244
+ files.push({ success: true, filePath: reactorPath, skipped: true });
245
+ }
246
+ }
211
247
  } catch (err) {
212
248
  files.push({
213
249
  success: false,
package/dist/index.d.cts CHANGED
@@ -19,9 +19,15 @@ interface CanisterConfig {
19
19
  * Example: "../../clients" → `import { clientManager } from "../../clients"`
20
20
  */
21
21
  clientManagerPath?: string;
22
+ /**
23
+ * Reactor class used for generated hooks in this canister.
24
+ * Defaults to DisplayReactor for backward compatibility.
25
+ */
26
+ mode?: ReactorClassName;
22
27
  /** Optional fixed canister ID */
23
28
  canisterId?: string;
24
29
  }
30
+ type ReactorClassName = "Reactor" | "DisplayReactor" | "CandidReactor" | "CandidDisplayReactor" | "MetadataDisplayReactor";
25
31
  /**
26
32
  * Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
27
33
  */
@@ -62,7 +68,7 @@ interface GeneratorResult {
62
68
  * Pipeline steps (in order):
63
69
  * 1. Resolve paths (didFile, outDir)
64
70
  * 2. Generate declarations (JS + .d.ts + .did copy)
65
- * 3. Generate reactor file (index.ts)
71
+ * 3. Generate reactor file (`index.ts`)
66
72
  */
67
73
 
68
74
  interface PipelineOptions {
@@ -204,6 +210,7 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
204
210
  * ...
205
211
  * } = createActorHooks(backendReactor)
206
212
  */
213
+
207
214
  interface ReactorGeneratorOptions {
208
215
  /** Canister name (e.g. "backend") */
209
216
  canisterName: string;
@@ -217,9 +224,14 @@ interface ReactorGeneratorOptions {
217
224
  * Default: "../../clients"
218
225
  */
219
226
  clientManagerPath?: string;
227
+ /**
228
+ * Which reactor class should back the generated hooks.
229
+ * Default: "DisplayReactor" (backward compatible)
230
+ */
231
+ reactorClass?: ReactorClassName;
220
232
  }
221
233
  /**
222
- * Generate the content of a canister's `index.ts` reactor file.
234
+ * Generate the content of a canister's `index.ts` file.
223
235
  */
224
236
  declare function generateReactorFile(options: ReactorGeneratorOptions): string;
225
237
 
@@ -246,4 +258,4 @@ interface ClientGeneratorOptions {
246
258
  */
247
259
  declare function generateClientFile(options?: ClientGeneratorOptions): string;
248
260
 
249
- export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
261
+ export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorClassName, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
package/dist/index.d.ts CHANGED
@@ -19,9 +19,15 @@ interface CanisterConfig {
19
19
  * Example: "../../clients" → `import { clientManager } from "../../clients"`
20
20
  */
21
21
  clientManagerPath?: string;
22
+ /**
23
+ * Reactor class used for generated hooks in this canister.
24
+ * Defaults to DisplayReactor for backward compatibility.
25
+ */
26
+ mode?: ReactorClassName;
22
27
  /** Optional fixed canister ID */
23
28
  canisterId?: string;
24
29
  }
30
+ type ReactorClassName = "Reactor" | "DisplayReactor" | "CandidReactor" | "CandidDisplayReactor" | "MetadataDisplayReactor";
25
31
  /**
26
32
  * Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
27
33
  */
@@ -62,7 +68,7 @@ interface GeneratorResult {
62
68
  * Pipeline steps (in order):
63
69
  * 1. Resolve paths (didFile, outDir)
64
70
  * 2. Generate declarations (JS + .d.ts + .did copy)
65
- * 3. Generate reactor file (index.ts)
71
+ * 3. Generate reactor file (`index.ts`)
66
72
  */
67
73
 
68
74
  interface PipelineOptions {
@@ -204,6 +210,7 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
204
210
  * ...
205
211
  * } = createActorHooks(backendReactor)
206
212
  */
213
+
207
214
  interface ReactorGeneratorOptions {
208
215
  /** Canister name (e.g. "backend") */
209
216
  canisterName: string;
@@ -217,9 +224,14 @@ interface ReactorGeneratorOptions {
217
224
  * Default: "../../clients"
218
225
  */
219
226
  clientManagerPath?: string;
227
+ /**
228
+ * Which reactor class should back the generated hooks.
229
+ * Default: "DisplayReactor" (backward compatible)
230
+ */
231
+ reactorClass?: ReactorClassName;
220
232
  }
221
233
  /**
222
- * Generate the content of a canister's `index.ts` reactor file.
234
+ * Generate the content of a canister's `index.ts` file.
223
235
  */
224
236
  declare function generateReactorFile(options: ReactorGeneratorOptions): string;
225
237
 
@@ -246,4 +258,4 @@ interface ClientGeneratorOptions {
246
258
  */
247
259
  declare function generateClientFile(options?: ClientGeneratorOptions): string;
248
260
 
249
- export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
261
+ export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorClassName, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
package/dist/index.js CHANGED
@@ -78,14 +78,32 @@ function getServiceTypeName(canisterName) {
78
78
  }
79
79
 
80
80
  // src/generators/reactor.ts
81
+ function getReactorClassImportSource(reactorClass) {
82
+ switch (reactorClass) {
83
+ case "Reactor":
84
+ case "DisplayReactor":
85
+ return "@ic-reactor/react";
86
+ case "CandidReactor":
87
+ case "CandidDisplayReactor":
88
+ case "MetadataDisplayReactor":
89
+ return "@ic-reactor/candid";
90
+ }
91
+ }
81
92
  function generateReactorFile(options) {
82
- const { canisterName, didFile, clientManagerPath = "../../clients" } = options;
93
+ const {
94
+ canisterName,
95
+ didFile,
96
+ clientManagerPath = "../../clients",
97
+ reactorClass = "DisplayReactor"
98
+ } = options;
83
99
  const pascalName = toPascalCase(canisterName);
84
100
  const reactorName = getReactorName(canisterName);
85
101
  const serviceName = getServiceTypeName(canisterName);
86
102
  const baseName = path2.basename(didFile, ".did");
87
103
  const declarationsPath = `./declarations/${baseName}`;
88
- return `import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
104
+ const reactorImportSource = getReactorClassImportSource(reactorClass);
105
+ return `import { createActorHooks } from "@ic-reactor/react"
106
+ import { ${reactorClass} } from "${reactorImportSource}"
89
107
  import { clientManager } from "${clientManagerPath}"
90
108
  import { idlFactory, type _SERVICE } from "${declarationsPath}"
91
109
 
@@ -97,7 +115,7 @@ export type ${serviceName} = _SERVICE
97
115
  * Auto-generated by @ic-reactor/codegen \u2014 do not edit.
98
116
  * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
99
117
  */
100
- export const ${reactorName} = new DisplayReactor<${serviceName}>({
118
+ export const ${reactorName} = new ${reactorClass}<${serviceName}>({
101
119
  clientManager,
102
120
  idlFactory,
103
121
  name: "${canisterName}",
@@ -115,6 +133,12 @@ export const {
115
133
  }
116
134
 
117
135
  // src/pipeline.ts
136
+ function resolveReactorClass(canisterConfig) {
137
+ return canisterConfig.mode ?? "DisplayReactor";
138
+ }
139
+ function isGeneratedIndexFile(content) {
140
+ return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(");
141
+ }
118
142
  async function runCanisterPipeline(options) {
119
143
  const { canisterConfig, projectRoot, globalConfig } = options;
120
144
  const { name, didFile, clientManagerPath } = canisterConfig;
@@ -154,15 +178,27 @@ async function runCanisterPipeline(options) {
154
178
  };
155
179
  }
156
180
  const reactorPath = path3.join(canisterOutDir, "index.ts");
181
+ const reactorClass = resolveReactorClass(canisterConfig);
157
182
  try {
158
183
  const reactorContent = generateReactorFile({
159
184
  canisterName: name,
160
185
  didFile: resolvedDidFile,
161
- clientManagerPath: resolvedClientManagerPath
186
+ clientManagerPath: resolvedClientManagerPath,
187
+ reactorClass
162
188
  });
163
189
  fs2.mkdirSync(canisterOutDir, { recursive: true });
164
- fs2.writeFileSync(reactorPath, reactorContent);
165
- files.push({ success: true, filePath: reactorPath });
190
+ if (!fs2.existsSync(reactorPath)) {
191
+ fs2.writeFileSync(reactorPath, reactorContent);
192
+ files.push({ success: true, filePath: reactorPath });
193
+ } else {
194
+ const existingIndexContent = fs2.readFileSync(reactorPath, "utf-8");
195
+ if (isGeneratedIndexFile(existingIndexContent)) {
196
+ fs2.writeFileSync(reactorPath, reactorContent);
197
+ files.push({ success: true, filePath: reactorPath });
198
+ } else {
199
+ files.push({ success: true, filePath: reactorPath, skipped: true });
200
+ }
201
+ }
166
202
  } catch (err) {
167
203
  files.push({
168
204
  success: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/codegen",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Shared code generation utilities for IC Reactor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -37,7 +37,7 @@
37
37
  "@ic-reactor/parser": "0.4.6"
38
38
  },
39
39
  "devDependencies": {
40
- "@types/node": "^25.2.3",
40
+ "@types/node": "^25.3.0",
41
41
  "tsup": "^8.5.1",
42
42
  "typescript": "^5.9.3",
43
43
  "vitest": "^4.0.18"
@@ -0,0 +1,94 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Reactor generator > keeps default behavior as DisplayReactor > display-reactor-index 1`] = `
4
+ "import { createActorHooks } from "@ic-reactor/react"
5
+ import { DisplayReactor } from "@ic-reactor/react"
6
+ import { clientManager } from "../../clients"
7
+ import { idlFactory, type _SERVICE } from "./declarations/backend"
8
+
9
+ export type BackendService = _SERVICE
10
+
11
+ /**
12
+ * Backend Reactor
13
+ *
14
+ * Auto-generated by @ic-reactor/codegen — do not edit.
15
+ * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
16
+ */
17
+ export const backendReactor = new DisplayReactor<BackendService>({
18
+ clientManager,
19
+ idlFactory,
20
+ name: "backend",
21
+ })
22
+
23
+ export const {
24
+ useActorQuery: useBackendQuery,
25
+ useActorSuspenseQuery: useBackendSuspenseQuery,
26
+ useActorInfiniteQuery: useBackendInfiniteQuery,
27
+ useActorSuspenseInfiniteQuery: useBackendSuspenseInfiniteQuery,
28
+ useActorMutation: useBackendMutation,
29
+ useActorMethod: useBackendMethod,
30
+ } = createActorHooks(backendReactor)
31
+ "
32
+ `;
33
+
34
+ exports[`Reactor generator > supports Reactor mode generation > reactor-index 1`] = `
35
+ "import { createActorHooks } from "@ic-reactor/react"
36
+ import { Reactor } from "@ic-reactor/react"
37
+ import { clientManager } from "../../clients"
38
+ import { idlFactory, type _SERVICE } from "./declarations/workflow_engine"
39
+
40
+ export type WorkflowEngineService = _SERVICE
41
+
42
+ /**
43
+ * WorkflowEngine Reactor
44
+ *
45
+ * Auto-generated by @ic-reactor/codegen — do not edit.
46
+ * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
47
+ */
48
+ export const workflowEngineReactor = new Reactor<WorkflowEngineService>({
49
+ clientManager,
50
+ idlFactory,
51
+ name: "workflow_engine",
52
+ })
53
+
54
+ export const {
55
+ useActorQuery: useWorkflowEngineQuery,
56
+ useActorSuspenseQuery: useWorkflowEngineSuspenseQuery,
57
+ useActorInfiniteQuery: useWorkflowEngineInfiniteQuery,
58
+ useActorSuspenseInfiniteQuery: useWorkflowEngineSuspenseInfiniteQuery,
59
+ useActorMutation: useWorkflowEngineMutation,
60
+ useActorMethod: useWorkflowEngineMethod,
61
+ } = createActorHooks(workflowEngineReactor)
62
+ "
63
+ `;
64
+
65
+ exports[`Reactor generator > supports candid reactor subclasses > metadata-display-reactor-index 1`] = `
66
+ "import { createActorHooks } from "@ic-reactor/react"
67
+ import { MetadataDisplayReactor } from "@ic-reactor/candid"
68
+ import { clientManager } from "../../clients"
69
+ import { idlFactory, type _SERVICE } from "./declarations/ledger"
70
+
71
+ export type LedgerService = _SERVICE
72
+
73
+ /**
74
+ * Ledger Reactor
75
+ *
76
+ * Auto-generated by @ic-reactor/codegen — do not edit.
77
+ * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
78
+ */
79
+ export const ledgerReactor = new MetadataDisplayReactor<LedgerService>({
80
+ clientManager,
81
+ idlFactory,
82
+ name: "ledger",
83
+ })
84
+
85
+ export const {
86
+ useActorQuery: useLedgerQuery,
87
+ useActorSuspenseQuery: useLedgerSuspenseQuery,
88
+ useActorInfiniteQuery: useLedgerInfiniteQuery,
89
+ useActorSuspenseInfiniteQuery: useLedgerSuspenseInfiniteQuery,
90
+ useActorMutation: useLedgerMutation,
91
+ useActorMethod: useLedgerMethod,
92
+ } = createActorHooks(ledgerReactor)
93
+ "
94
+ `;
@@ -22,6 +22,7 @@
22
22
 
23
23
  import path from "node:path"
24
24
  import { toPascalCase, getReactorName, getServiceTypeName } from "../naming.js"
25
+ import type { ReactorClassName } from "../types.js"
25
26
 
26
27
  export interface ReactorGeneratorOptions {
27
28
  /** Canister name (e.g. "backend") */
@@ -36,13 +37,37 @@ export interface ReactorGeneratorOptions {
36
37
  * Default: "../../clients"
37
38
  */
38
39
  clientManagerPath?: string
40
+ /**
41
+ * Which reactor class should back the generated hooks.
42
+ * Default: "DisplayReactor" (backward compatible)
43
+ */
44
+ reactorClass?: ReactorClassName
45
+ }
46
+
47
+ function getReactorClassImportSource(
48
+ reactorClass: ReactorClassName
49
+ ): "@ic-reactor/react" | "@ic-reactor/candid" {
50
+ switch (reactorClass) {
51
+ case "Reactor":
52
+ case "DisplayReactor":
53
+ return "@ic-reactor/react"
54
+ case "CandidReactor":
55
+ case "CandidDisplayReactor":
56
+ case "MetadataDisplayReactor":
57
+ return "@ic-reactor/candid"
58
+ }
39
59
  }
40
60
 
41
61
  /**
42
- * Generate the content of a canister's `index.ts` reactor file.
62
+ * Generate the content of a canister's `index.ts` file.
43
63
  */
44
64
  export function generateReactorFile(options: ReactorGeneratorOptions): string {
45
- const { canisterName, didFile, clientManagerPath = "../../clients" } = options
65
+ const {
66
+ canisterName,
67
+ didFile,
68
+ clientManagerPath = "../../clients",
69
+ reactorClass = "DisplayReactor",
70
+ } = options
46
71
 
47
72
  const pascalName = toPascalCase(canisterName)
48
73
  const reactorName = getReactorName(canisterName)
@@ -51,8 +76,10 @@ export function generateReactorFile(options: ReactorGeneratorOptions): string {
51
76
  // Derive the declarations import path from the .did filename
52
77
  const baseName = path.basename(didFile, ".did")
53
78
  const declarationsPath = `./declarations/${baseName}`
79
+ const reactorImportSource = getReactorClassImportSource(reactorClass)
54
80
 
55
- return `import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
81
+ return `import { createActorHooks } from "@ic-reactor/react"
82
+ import { ${reactorClass} } from "${reactorImportSource}"
56
83
  import { clientManager } from "${clientManagerPath}"
57
84
  import { idlFactory, type _SERVICE } from "${declarationsPath}"
58
85
 
@@ -64,7 +91,7 @@ export type ${serviceName} = _SERVICE
64
91
  * Auto-generated by @ic-reactor/codegen — do not edit.
65
92
  * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
66
93
  */
67
- export const ${reactorName} = new DisplayReactor<${serviceName}>({
94
+ export const ${reactorName} = new ${reactorClass}<${serviceName}>({
68
95
  clientManager,
69
96
  idlFactory,
70
97
  name: "${canisterName}",
package/src/index.ts CHANGED
@@ -6,7 +6,12 @@
6
6
  */
7
7
 
8
8
  // Core Types
9
- export type { CanisterConfig, CodegenConfig, GeneratorResult } from "./types.js"
9
+ export type {
10
+ CanisterConfig,
11
+ CodegenConfig,
12
+ GeneratorResult,
13
+ ReactorClassName,
14
+ } from "./types.js"
10
15
 
11
16
  // Pipeline (Primary Entry Point)
12
17
  export { runCanisterPipeline } from "./pipeline.js"
@@ -0,0 +1,163 @@
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"
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.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
+ })
62
+
63
+ it("does not overwrite user-modified index.ts on regenerate", async () => {
64
+ const projectRoot = createTempProject()
65
+ writeDid(projectRoot, "backend.did")
66
+
67
+ const options = {
68
+ canisterConfig: {
69
+ name: "backend",
70
+ didFile: "backend.did",
71
+ },
72
+ projectRoot,
73
+ globalConfig: {
74
+ outDir: "src/declarations",
75
+ clientManagerPath: "../../clients",
76
+ },
77
+ } as const
78
+
79
+ const first = await runCanisterPipeline(options)
80
+ expect(first.success).toBe(true)
81
+
82
+ const indexPath = path.join(
83
+ projectRoot,
84
+ "src/declarations/backend/index.ts"
85
+ )
86
+ fs.writeFileSync(
87
+ indexPath,
88
+ `// user custom canister file
89
+ export const customBackendIndex = true
90
+ `
91
+ )
92
+
93
+ const second = await runCanisterPipeline(options)
94
+ expect(second.success).toBe(true)
95
+
96
+ const wrapper = fs.readFileSync(indexPath, "utf-8")
97
+ expect(wrapper).toContain("customBackendIndex = true")
98
+ expect(second.files).toEqual(
99
+ expect.arrayContaining([
100
+ expect.objectContaining({
101
+ filePath: indexPath,
102
+ skipped: true,
103
+ success: true,
104
+ }),
105
+ ])
106
+ )
107
+ })
108
+
109
+ it("overwrites legacy generated index.ts during regeneration", async () => {
110
+ const projectRoot = createTempProject()
111
+ writeDid(projectRoot, "backend.did")
112
+
113
+ const canisterOutDir = path.join(projectRoot, "src/declarations/backend")
114
+ fs.mkdirSync(canisterOutDir, { recursive: true })
115
+
116
+ // Simulate a generated index.ts content from older versions.
117
+ fs.writeFileSync(
118
+ path.join(canisterOutDir, "index.ts"),
119
+ `import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
120
+ import { clientManager } from "../../clients"
121
+ import { idlFactory, type _SERVICE } from "./declarations/backend"
122
+
123
+ export type BackendService = _SERVICE
124
+
125
+ /**
126
+ * Backend Reactor
127
+ *
128
+ * Auto-generated by @ic-reactor/codegen — do not edit.
129
+ * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
130
+ */
131
+ export const backendReactor = new DisplayReactor<BackendService>({
132
+ clientManager,
133
+ idlFactory,
134
+ name: "backend",
135
+ })
136
+
137
+ export const {
138
+ useActorQuery: useBackendQuery,
139
+ } = createActorHooks(backendReactor)
140
+ `
141
+ )
142
+
143
+ const result = await runCanisterPipeline({
144
+ canisterConfig: {
145
+ name: "backend",
146
+ didFile: "backend.did",
147
+ },
148
+ projectRoot,
149
+ globalConfig: {
150
+ outDir: "src/declarations",
151
+ clientManagerPath: "../../clients",
152
+ },
153
+ })
154
+
155
+ expect(result.success).toBe(true)
156
+
157
+ const indexPath = path.join(canisterOutDir, "index.ts")
158
+ const generated = fs.readFileSync(indexPath, "utf-8")
159
+ expect(generated).toContain("new DisplayReactor<BackendService>")
160
+ expect(generated).toContain("useBackendMutation")
161
+ expect(generated).not.toContain('export * from "./index.generated"')
162
+ })
163
+ })
package/src/pipeline.ts CHANGED
@@ -7,12 +7,17 @@
7
7
  * Pipeline steps (in order):
8
8
  * 1. Resolve paths (didFile, outDir)
9
9
  * 2. Generate declarations (JS + .d.ts + .did copy)
10
- * 3. Generate reactor file (index.ts)
10
+ * 3. Generate reactor file (`index.ts`)
11
11
  */
12
12
 
13
13
  import fs from "node:fs"
14
14
  import path from "node:path"
15
- import type { CanisterConfig, CodegenConfig, GeneratorResult } from "./types.js"
15
+ import type {
16
+ CanisterConfig,
17
+ CodegenConfig,
18
+ GeneratorResult,
19
+ ReactorClassName,
20
+ } from "./types.js"
16
21
  import { generateDeclarations } from "./generators/declarations.js"
17
22
  import { generateReactorFile } from "./generators/reactor.js"
18
23
 
@@ -30,6 +35,17 @@ export interface PipelineOptions {
30
35
  globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">
31
36
  }
32
37
 
38
+ function resolveReactorClass(canisterConfig: CanisterConfig): ReactorClassName {
39
+ return canisterConfig.mode ?? "DisplayReactor"
40
+ }
41
+
42
+ function isGeneratedIndexFile(content: string): boolean {
43
+ return (
44
+ content.includes("Auto-generated by @ic-reactor/codegen") &&
45
+ content.includes("createActorHooks(")
46
+ )
47
+ }
48
+
33
49
  export interface PipelineResult {
34
50
  canisterName: string
35
51
  success: boolean
@@ -109,18 +125,30 @@ export async function runCanisterPipeline(
109
125
  // ── Step 2: Reactor file ───────────────────────────────────────────────────
110
126
 
111
127
  const reactorPath = path.join(canisterOutDir, "index.ts")
128
+ const reactorClass = resolveReactorClass(canisterConfig)
112
129
 
113
130
  try {
114
131
  const reactorContent = generateReactorFile({
115
132
  canisterName: name,
116
133
  didFile: resolvedDidFile,
117
134
  clientManagerPath: resolvedClientManagerPath,
135
+ reactorClass,
118
136
  })
119
137
 
120
138
  fs.mkdirSync(canisterOutDir, { recursive: true })
121
- fs.writeFileSync(reactorPath, reactorContent)
122
-
123
- files.push({ success: true, filePath: reactorPath })
139
+ if (!fs.existsSync(reactorPath)) {
140
+ fs.writeFileSync(reactorPath, reactorContent)
141
+ files.push({ success: true, filePath: reactorPath })
142
+ } else {
143
+ const existingIndexContent = fs.readFileSync(reactorPath, "utf-8")
144
+
145
+ if (isGeneratedIndexFile(existingIndexContent)) {
146
+ fs.writeFileSync(reactorPath, reactorContent)
147
+ files.push({ success: true, filePath: reactorPath })
148
+ } else {
149
+ files.push({ success: true, filePath: reactorPath, skipped: true })
150
+ }
151
+ }
124
152
  } catch (err) {
125
153
  files.push({
126
154
  success: false,
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { generateReactorFile } from "./generators"
3
+
4
+ describe("Reactor generator", () => {
5
+ it("keeps default behavior as DisplayReactor", () => {
6
+ const content = generateReactorFile({
7
+ canisterName: "backend",
8
+ didFile: "mock/backend.did",
9
+ clientManagerPath: "../../clients",
10
+ })
11
+
12
+ expect(content).toMatchSnapshot("display-reactor-index")
13
+ expect(content).toContain("new DisplayReactor<BackendService>")
14
+ expect(content).not.toContain("export const BackendReactorMode")
15
+ })
16
+
17
+ it("supports Reactor mode generation", () => {
18
+ const content = generateReactorFile({
19
+ canisterName: "workflow_engine",
20
+ didFile: "mock/workflow_engine.did",
21
+ reactorClass: "Reactor",
22
+ })
23
+
24
+ expect(content).toMatchSnapshot("reactor-index")
25
+ expect(content).toContain("new Reactor<WorkflowEngineService>")
26
+ expect(content).not.toContain("createWorkflowEngineDisplayReactor")
27
+ })
28
+
29
+ it("supports candid reactor subclasses", () => {
30
+ const content = generateReactorFile({
31
+ canisterName: "ledger",
32
+ didFile: "mock/ledger.did",
33
+ reactorClass: "MetadataDisplayReactor",
34
+ })
35
+
36
+ expect(content).toMatchSnapshot("metadata-display-reactor-index")
37
+ expect(content).toContain(
38
+ 'import { createActorHooks } from "@ic-reactor/react"'
39
+ )
40
+ expect(content).toContain(
41
+ 'import { MetadataDisplayReactor } from "@ic-reactor/candid"'
42
+ )
43
+ expect(content).toContain("new MetadataDisplayReactor<LedgerService>")
44
+ })
45
+ })
package/src/types.ts CHANGED
@@ -24,10 +24,22 @@ export interface CanisterConfig {
24
24
  * Example: "../../clients" → `import { clientManager } from "../../clients"`
25
25
  */
26
26
  clientManagerPath?: string
27
+ /**
28
+ * Reactor class used for generated hooks in this canister.
29
+ * Defaults to DisplayReactor for backward compatibility.
30
+ */
31
+ mode?: ReactorClassName
27
32
  /** Optional fixed canister ID */
28
33
  canisterId?: string
29
34
  }
30
35
 
36
+ export type ReactorClassName =
37
+ | "Reactor"
38
+ | "DisplayReactor"
39
+ | "CandidReactor"
40
+ | "CandidDisplayReactor"
41
+ | "MetadataDisplayReactor"
42
+
31
43
  /**
32
44
  * Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
33
45
  */