@ic-reactor/codegen 0.7.2 → 0.9.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.
package/README.md CHANGED
@@ -24,7 +24,9 @@ await runCanisterPipeline({
24
24
  globalConfig: {
25
25
  outDir: "src/declarations",
26
26
  clientManagerPath: "../../clients",
27
+ target: "react",
27
28
  },
29
+ generateReactor: true,
28
30
  })
29
31
  ```
30
32
 
@@ -38,14 +40,23 @@ Set `canisterConfig.mode` to choose the generated reactor class:
38
40
  - `CandidDisplayReactor`
39
41
  - `MetadataDisplayReactor`
40
42
 
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.
43
+ Set `target` to control whether generated files include React hooks:
44
+
45
+ - `react` (default): generates the reactor plus bound `createActorHooks` exports
46
+ - `core`: generates only the typed reactor exports with no `@ic-reactor/react` dependency
47
+
48
+ Codegen now writes two files per canister: a managed `index.generated.ts` implementation that is regenerated on every run, and an `index.ts` entry wrapper. The wrapper is created once, then preserved unless it still matches the default generated wrapper or an older generated scaffold that can be migrated automatically.
49
+
50
+ Set `generateReactor: false` if you only want the bindgen/declaration output and
51
+ need to skip `index.generated.ts` and `index.ts`.
42
52
 
43
53
  ## Generators
44
54
 
45
55
  You can also use individual generators if you need more granular control:
46
56
 
47
57
  - **`generateDeclarations`**: Generates `.js` (factory), `.d.ts` (types), and `.did` copy.
48
- - **`generateReactorFile`**: Generates the `index.ts` file using either `DisplayReactor` or `Reactor`.
58
+ - **`generateReactorFile`**: Generates the managed `index.generated.ts` implementation using either `DisplayReactor` or `Reactor`.
59
+ - **`generateReactorEntryFile`**: Generates the stable `index.ts` wrapper that re-exports from `index.generated.ts`.
49
60
  - **`generateClientFile`**: Generates a `ClientManager` boilerplate file.
50
61
 
51
62
  ## Utilities
package/dist/index.cjs CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  extractMethods: () => extractMethods,
35
35
  generateClientFile: () => generateClientFile,
36
36
  generateDeclarations: () => generateDeclarations,
37
+ generateReactorEntryFile: () => generateReactorEntryFile,
37
38
  generateReactorFile: () => generateReactorFile,
38
39
  getReactorName: () => getReactorName,
39
40
  getServiceTypeName: () => getServiceTypeName,
@@ -123,11 +124,11 @@ function getServiceTypeName(canisterName) {
123
124
  }
124
125
 
125
126
  // src/generators/reactor.ts
126
- function getReactorClassImportSource(reactorClass) {
127
+ function getReactorClassImportSource(reactorClass, runtimeTarget) {
127
128
  switch (reactorClass) {
128
129
  case "Reactor":
129
130
  case "DisplayReactor":
130
- return "@ic-reactor/react";
131
+ return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react";
131
132
  case "CandidReactor":
132
133
  case "CandidDisplayReactor":
133
134
  case "MetadataDisplayReactor":
@@ -139,6 +140,8 @@ function generateReactorFile(options) {
139
140
  canisterName,
140
141
  didFile,
141
142
  clientManagerPath = "../../clients",
143
+ canisterId,
144
+ runtimeTarget = "react",
142
145
  reactorClass = "DisplayReactor"
143
146
  } = options;
144
147
  const pascalName = toPascalCase(canisterName);
@@ -146,9 +149,24 @@ function generateReactorFile(options) {
146
149
  const serviceName = getServiceTypeName(canisterName);
147
150
  const baseName = import_node_path2.default.basename(didFile, ".did");
148
151
  const declarationsPath = `./declarations/${baseName}`;
149
- const reactorImportSource = getReactorClassImportSource(reactorClass);
150
- return `import { createActorHooks } from "@ic-reactor/react"
151
- import { ${reactorClass} } from "${reactorImportSource}"
152
+ const reactorImportSource = getReactorClassImportSource(
153
+ reactorClass,
154
+ runtimeTarget
155
+ );
156
+ const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
157
+ ` : "";
158
+ const hookExports = runtimeTarget === "react" ? `
159
+
160
+ export const {
161
+ useActorQuery: use${pascalName}Query,
162
+ useActorSuspenseQuery: use${pascalName}SuspenseQuery,
163
+ useActorInfiniteQuery: use${pascalName}InfiniteQuery,
164
+ useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
165
+ useActorMutation: use${pascalName}Mutation,
166
+ useActorMethod: use${pascalName}Method,
167
+ } = createActorHooks(${reactorName})
168
+ ` : "";
169
+ return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from "${reactorImportSource}"
152
170
  import { clientManager } from "${clientManagerPath}"
153
171
  import { idlFactory, type _SERVICE } from "${declarationsPath}"
154
172
 
@@ -158,22 +176,22 @@ export type ${serviceName} = _SERVICE
158
176
  * ${pascalName} Reactor
159
177
  *
160
178
  * Auto-generated by @ic-reactor/codegen \u2014 do not edit.
161
- * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
179
+ * This file is overwritten whenever generation runs.
162
180
  */
163
181
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
164
182
  clientManager,
165
183
  idlFactory,
166
- name: "${canisterName}",
167
- })
168
-
169
- export const {
170
- useActorQuery: use${pascalName}Query,
171
- useActorSuspenseQuery: use${pascalName}SuspenseQuery,
172
- useActorInfiniteQuery: use${pascalName}InfiniteQuery,
173
- useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
174
- useActorMutation: use${pascalName}Mutation,
175
- useActorMethod: use${pascalName}Method,
176
- } = createActorHooks(${reactorName})
184
+ ${canisterIdLine} name: "${canisterName}",
185
+ })${hookExports || "\n"}`;
186
+ }
187
+ function generateReactorEntryFile() {
188
+ return `/**
189
+ * Canister entrypoint.
190
+ *
191
+ * Created once by @ic-reactor/codegen and safe to customize.
192
+ * Keep the re-export below if you want generated exports and types to stay in sync.
193
+ */
194
+ export * from "./index.generated"
177
195
  `;
178
196
  }
179
197
 
@@ -181,11 +199,25 @@ export const {
181
199
  function resolveReactorClass(canisterConfig) {
182
200
  return canisterConfig.mode ?? "DisplayReactor";
183
201
  }
184
- function isGeneratedIndexFile(content) {
202
+ function resolveRuntimeTarget(canisterConfig, globalConfig) {
203
+ return canisterConfig.target ?? globalConfig.target ?? "react";
204
+ }
205
+ function normalizeFileContent(content) {
206
+ return content.replace(/\r\n/g, "\n").trim();
207
+ }
208
+ function isLegacyGeneratedIndexFile(content) {
185
209
  return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(");
186
210
  }
211
+ function isManagedEntryWrapper(content, expectedEntryContent) {
212
+ return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
213
+ }
187
214
  async function runCanisterPipeline(options) {
188
- const { canisterConfig, projectRoot, globalConfig } = options;
215
+ const {
216
+ canisterConfig,
217
+ projectRoot,
218
+ globalConfig,
219
+ generateReactor = true
220
+ } = options;
189
221
  const { name, didFile, clientManagerPath } = canisterConfig;
190
222
  const files = [];
191
223
  const resolvedDidFile = import_node_path3.default.isAbsolute(didFile) ? didFile : import_node_path3.default.resolve(projectRoot, didFile);
@@ -222,26 +254,40 @@ async function runCanisterPipeline(options) {
222
254
  error: `Declarations step failed: ${err instanceof Error ? err.message : String(err)}`
223
255
  };
224
256
  }
225
- const reactorPath = import_node_path3.default.join(canisterOutDir, "index.ts");
257
+ if (!generateReactor) {
258
+ return {
259
+ canisterName: name,
260
+ success: true,
261
+ files
262
+ };
263
+ }
264
+ const reactorPath = import_node_path3.default.join(canisterOutDir, "index.generated.ts");
265
+ const entryPath = import_node_path3.default.join(canisterOutDir, "index.ts");
226
266
  const reactorClass = resolveReactorClass(canisterConfig);
267
+ const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
227
268
  try {
228
269
  const reactorContent = generateReactorFile({
229
270
  canisterName: name,
230
271
  didFile: resolvedDidFile,
231
272
  clientManagerPath: resolvedClientManagerPath,
273
+ canisterId: canisterConfig.canisterId,
274
+ runtimeTarget,
232
275
  reactorClass
233
276
  });
277
+ const entryContent = generateReactorEntryFile();
234
278
  import_node_fs2.default.mkdirSync(canisterOutDir, { recursive: true });
235
- if (!import_node_fs2.default.existsSync(reactorPath)) {
236
- import_node_fs2.default.writeFileSync(reactorPath, reactorContent);
237
- files.push({ success: true, filePath: reactorPath });
279
+ import_node_fs2.default.writeFileSync(reactorPath, reactorContent);
280
+ files.push({ success: true, filePath: reactorPath });
281
+ if (!import_node_fs2.default.existsSync(entryPath)) {
282
+ import_node_fs2.default.writeFileSync(entryPath, entryContent);
283
+ files.push({ success: true, filePath: entryPath });
238
284
  } 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 });
285
+ const existingEntryContent = import_node_fs2.default.readFileSync(entryPath, "utf-8");
286
+ if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
287
+ import_node_fs2.default.writeFileSync(entryPath, entryContent);
288
+ files.push({ success: true, filePath: entryPath });
243
289
  } else {
244
- files.push({ success: true, filePath: reactorPath, skipped: true });
290
+ files.push({ success: true, filePath: entryPath, skipped: true });
245
291
  }
246
292
  }
247
293
  } catch (err) {
@@ -325,6 +371,7 @@ export const clientManager = new ClientManager({
325
371
  extractMethods,
326
372
  generateClientFile,
327
373
  generateDeclarations,
374
+ generateReactorEntryFile,
328
375
  generateReactorFile,
329
376
  getReactorName,
330
377
  getServiceTypeName,
package/dist/index.d.cts CHANGED
@@ -24,10 +24,16 @@ interface CanisterConfig {
24
24
  * Defaults to DisplayReactor for backward compatibility.
25
25
  */
26
26
  mode?: ReactorClassName;
27
+ /**
28
+ * Generated runtime target.
29
+ * `react` emits bound React hooks, `core` emits only the typed reactor exports.
30
+ */
31
+ target?: CodegenTarget;
27
32
  /** Optional fixed canister ID */
28
33
  canisterId?: string;
29
34
  }
30
35
  type ReactorClassName = "Reactor" | "DisplayReactor" | "CandidReactor" | "CandidDisplayReactor" | "MetadataDisplayReactor";
36
+ type CodegenTarget = "react" | "core";
31
37
  /**
32
38
  * Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
33
39
  */
@@ -44,6 +50,11 @@ interface CodegenConfig {
44
50
  * Individual canisters can override via `CanisterConfig.clientManagerPath`.
45
51
  */
46
52
  clientManagerPath?: string;
53
+ /**
54
+ * Default generated runtime target.
55
+ * Individual canisters can override via `CanisterConfig.target`.
56
+ */
57
+ target?: CodegenTarget;
47
58
  /** Canister configurations, keyed by canister name */
48
59
  canisters: Record<string, CanisterConfig>;
49
60
  }
@@ -68,7 +79,8 @@ interface GeneratorResult {
68
79
  * Pipeline steps (in order):
69
80
  * 1. Resolve paths (didFile, outDir)
70
81
  * 2. Generate declarations (JS + .d.ts + .did copy)
71
- * 3. Generate reactor file (`index.ts`)
82
+ * 3. Optionally generate reactor implementation (`index.generated.ts`)
83
+ * 4. Optionally create or migrate the user entry (`index.ts`)
72
84
  */
73
85
 
74
86
  interface PipelineOptions {
@@ -82,7 +94,12 @@ interface PipelineOptions {
82
94
  /**
83
95
  * Global codegen config (for fallback outDir and clientManagerPath).
84
96
  */
85
- globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">;
97
+ globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath" | "target">;
98
+ /**
99
+ * Whether the managed reactor files should be generated.
100
+ * Defaults to true.
101
+ */
102
+ generateReactor?: boolean;
86
103
  }
87
104
  interface PipelineResult {
88
105
  canisterName: string;
@@ -192,8 +209,8 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
192
209
  /**
193
210
  * Reactor File Generator
194
211
  *
195
- * Generates the main `index.ts` for a canister — a DisplayReactor instance
196
- * plus the full set of typed hooks via `createActorHooks`.
212
+ * Generates the managed `index.generated.ts` implementation for a canister.
213
+ * React targets also emit the full set of typed hooks via `createActorHooks`.
197
214
  *
198
215
  * Generated output example (for canister "backend"):
199
216
  *
@@ -224,6 +241,10 @@ interface ReactorGeneratorOptions {
224
241
  * Default: "../../clients"
225
242
  */
226
243
  clientManagerPath?: string;
244
+ /** Optional fixed canister ID for the generated reactor */
245
+ canisterId?: string;
246
+ /** Generated runtime target */
247
+ runtimeTarget?: CodegenTarget;
227
248
  /**
228
249
  * Which reactor class should back the generated hooks.
229
250
  * Default: "DisplayReactor" (backward compatible)
@@ -231,9 +252,13 @@ interface ReactorGeneratorOptions {
231
252
  reactorClass?: ReactorClassName;
232
253
  }
233
254
  /**
234
- * Generate the content of a canister's `index.ts` file.
255
+ * Generate the content of a canister's managed implementation file.
235
256
  */
236
257
  declare function generateReactorFile(options: ReactorGeneratorOptions): string;
258
+ /**
259
+ * Generate the user-facing `index.ts` wrapper content.
260
+ */
261
+ declare function generateReactorEntryFile(): string;
237
262
 
238
263
  /**
239
264
  * Client Manager Generator
@@ -258,4 +283,4 @@ interface ClientGeneratorOptions {
258
283
  */
259
284
  declare function generateClientFile(options?: ClientGeneratorOptions): string;
260
285
 
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 };
286
+ export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorClassName, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
package/dist/index.d.ts CHANGED
@@ -24,10 +24,16 @@ interface CanisterConfig {
24
24
  * Defaults to DisplayReactor for backward compatibility.
25
25
  */
26
26
  mode?: ReactorClassName;
27
+ /**
28
+ * Generated runtime target.
29
+ * `react` emits bound React hooks, `core` emits only the typed reactor exports.
30
+ */
31
+ target?: CodegenTarget;
27
32
  /** Optional fixed canister ID */
28
33
  canisterId?: string;
29
34
  }
30
35
  type ReactorClassName = "Reactor" | "DisplayReactor" | "CandidReactor" | "CandidDisplayReactor" | "MetadataDisplayReactor";
36
+ type CodegenTarget = "react" | "core";
31
37
  /**
32
38
  * Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
33
39
  */
@@ -44,6 +50,11 @@ interface CodegenConfig {
44
50
  * Individual canisters can override via `CanisterConfig.clientManagerPath`.
45
51
  */
46
52
  clientManagerPath?: string;
53
+ /**
54
+ * Default generated runtime target.
55
+ * Individual canisters can override via `CanisterConfig.target`.
56
+ */
57
+ target?: CodegenTarget;
47
58
  /** Canister configurations, keyed by canister name */
48
59
  canisters: Record<string, CanisterConfig>;
49
60
  }
@@ -68,7 +79,8 @@ interface GeneratorResult {
68
79
  * Pipeline steps (in order):
69
80
  * 1. Resolve paths (didFile, outDir)
70
81
  * 2. Generate declarations (JS + .d.ts + .did copy)
71
- * 3. Generate reactor file (`index.ts`)
82
+ * 3. Optionally generate reactor implementation (`index.generated.ts`)
83
+ * 4. Optionally create or migrate the user entry (`index.ts`)
72
84
  */
73
85
 
74
86
  interface PipelineOptions {
@@ -82,7 +94,12 @@ interface PipelineOptions {
82
94
  /**
83
95
  * Global codegen config (for fallback outDir and clientManagerPath).
84
96
  */
85
- globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">;
97
+ globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath" | "target">;
98
+ /**
99
+ * Whether the managed reactor files should be generated.
100
+ * Defaults to true.
101
+ */
102
+ generateReactor?: boolean;
86
103
  }
87
104
  interface PipelineResult {
88
105
  canisterName: string;
@@ -192,8 +209,8 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
192
209
  /**
193
210
  * Reactor File Generator
194
211
  *
195
- * Generates the main `index.ts` for a canister — a DisplayReactor instance
196
- * plus the full set of typed hooks via `createActorHooks`.
212
+ * Generates the managed `index.generated.ts` implementation for a canister.
213
+ * React targets also emit the full set of typed hooks via `createActorHooks`.
197
214
  *
198
215
  * Generated output example (for canister "backend"):
199
216
  *
@@ -224,6 +241,10 @@ interface ReactorGeneratorOptions {
224
241
  * Default: "../../clients"
225
242
  */
226
243
  clientManagerPath?: string;
244
+ /** Optional fixed canister ID for the generated reactor */
245
+ canisterId?: string;
246
+ /** Generated runtime target */
247
+ runtimeTarget?: CodegenTarget;
227
248
  /**
228
249
  * Which reactor class should back the generated hooks.
229
250
  * Default: "DisplayReactor" (backward compatible)
@@ -231,9 +252,13 @@ interface ReactorGeneratorOptions {
231
252
  reactorClass?: ReactorClassName;
232
253
  }
233
254
  /**
234
- * Generate the content of a canister's `index.ts` file.
255
+ * Generate the content of a canister's managed implementation file.
235
256
  */
236
257
  declare function generateReactorFile(options: ReactorGeneratorOptions): string;
258
+ /**
259
+ * Generate the user-facing `index.ts` wrapper content.
260
+ */
261
+ declare function generateReactorEntryFile(): string;
237
262
 
238
263
  /**
239
264
  * Client Manager Generator
@@ -258,4 +283,4 @@ interface ClientGeneratorOptions {
258
283
  */
259
284
  declare function generateClientFile(options?: ClientGeneratorOptions): string;
260
285
 
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 };
286
+ export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorClassName, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
package/dist/index.js CHANGED
@@ -78,11 +78,11 @@ function getServiceTypeName(canisterName) {
78
78
  }
79
79
 
80
80
  // src/generators/reactor.ts
81
- function getReactorClassImportSource(reactorClass) {
81
+ function getReactorClassImportSource(reactorClass, runtimeTarget) {
82
82
  switch (reactorClass) {
83
83
  case "Reactor":
84
84
  case "DisplayReactor":
85
- return "@ic-reactor/react";
85
+ return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react";
86
86
  case "CandidReactor":
87
87
  case "CandidDisplayReactor":
88
88
  case "MetadataDisplayReactor":
@@ -94,6 +94,8 @@ function generateReactorFile(options) {
94
94
  canisterName,
95
95
  didFile,
96
96
  clientManagerPath = "../../clients",
97
+ canisterId,
98
+ runtimeTarget = "react",
97
99
  reactorClass = "DisplayReactor"
98
100
  } = options;
99
101
  const pascalName = toPascalCase(canisterName);
@@ -101,9 +103,24 @@ function generateReactorFile(options) {
101
103
  const serviceName = getServiceTypeName(canisterName);
102
104
  const baseName = path2.basename(didFile, ".did");
103
105
  const declarationsPath = `./declarations/${baseName}`;
104
- const reactorImportSource = getReactorClassImportSource(reactorClass);
105
- return `import { createActorHooks } from "@ic-reactor/react"
106
- import { ${reactorClass} } from "${reactorImportSource}"
106
+ const reactorImportSource = getReactorClassImportSource(
107
+ reactorClass,
108
+ runtimeTarget
109
+ );
110
+ const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
111
+ ` : "";
112
+ const hookExports = runtimeTarget === "react" ? `
113
+
114
+ export const {
115
+ useActorQuery: use${pascalName}Query,
116
+ useActorSuspenseQuery: use${pascalName}SuspenseQuery,
117
+ useActorInfiniteQuery: use${pascalName}InfiniteQuery,
118
+ useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
119
+ useActorMutation: use${pascalName}Mutation,
120
+ useActorMethod: use${pascalName}Method,
121
+ } = createActorHooks(${reactorName})
122
+ ` : "";
123
+ return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from "${reactorImportSource}"
107
124
  import { clientManager } from "${clientManagerPath}"
108
125
  import { idlFactory, type _SERVICE } from "${declarationsPath}"
109
126
 
@@ -113,22 +130,22 @@ export type ${serviceName} = _SERVICE
113
130
  * ${pascalName} Reactor
114
131
  *
115
132
  * Auto-generated by @ic-reactor/codegen \u2014 do not edit.
116
- * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
133
+ * This file is overwritten whenever generation runs.
117
134
  */
118
135
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
119
136
  clientManager,
120
137
  idlFactory,
121
- name: "${canisterName}",
122
- })
123
-
124
- export const {
125
- useActorQuery: use${pascalName}Query,
126
- useActorSuspenseQuery: use${pascalName}SuspenseQuery,
127
- useActorInfiniteQuery: use${pascalName}InfiniteQuery,
128
- useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
129
- useActorMutation: use${pascalName}Mutation,
130
- useActorMethod: use${pascalName}Method,
131
- } = createActorHooks(${reactorName})
138
+ ${canisterIdLine} name: "${canisterName}",
139
+ })${hookExports || "\n"}`;
140
+ }
141
+ function generateReactorEntryFile() {
142
+ return `/**
143
+ * Canister entrypoint.
144
+ *
145
+ * Created once by @ic-reactor/codegen and safe to customize.
146
+ * Keep the re-export below if you want generated exports and types to stay in sync.
147
+ */
148
+ export * from "./index.generated"
132
149
  `;
133
150
  }
134
151
 
@@ -136,11 +153,25 @@ export const {
136
153
  function resolveReactorClass(canisterConfig) {
137
154
  return canisterConfig.mode ?? "DisplayReactor";
138
155
  }
139
- function isGeneratedIndexFile(content) {
156
+ function resolveRuntimeTarget(canisterConfig, globalConfig) {
157
+ return canisterConfig.target ?? globalConfig.target ?? "react";
158
+ }
159
+ function normalizeFileContent(content) {
160
+ return content.replace(/\r\n/g, "\n").trim();
161
+ }
162
+ function isLegacyGeneratedIndexFile(content) {
140
163
  return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(");
141
164
  }
165
+ function isManagedEntryWrapper(content, expectedEntryContent) {
166
+ return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
167
+ }
142
168
  async function runCanisterPipeline(options) {
143
- const { canisterConfig, projectRoot, globalConfig } = options;
169
+ const {
170
+ canisterConfig,
171
+ projectRoot,
172
+ globalConfig,
173
+ generateReactor = true
174
+ } = options;
144
175
  const { name, didFile, clientManagerPath } = canisterConfig;
145
176
  const files = [];
146
177
  const resolvedDidFile = path3.isAbsolute(didFile) ? didFile : path3.resolve(projectRoot, didFile);
@@ -177,26 +208,40 @@ async function runCanisterPipeline(options) {
177
208
  error: `Declarations step failed: ${err instanceof Error ? err.message : String(err)}`
178
209
  };
179
210
  }
180
- const reactorPath = path3.join(canisterOutDir, "index.ts");
211
+ if (!generateReactor) {
212
+ return {
213
+ canisterName: name,
214
+ success: true,
215
+ files
216
+ };
217
+ }
218
+ const reactorPath = path3.join(canisterOutDir, "index.generated.ts");
219
+ const entryPath = path3.join(canisterOutDir, "index.ts");
181
220
  const reactorClass = resolveReactorClass(canisterConfig);
221
+ const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
182
222
  try {
183
223
  const reactorContent = generateReactorFile({
184
224
  canisterName: name,
185
225
  didFile: resolvedDidFile,
186
226
  clientManagerPath: resolvedClientManagerPath,
227
+ canisterId: canisterConfig.canisterId,
228
+ runtimeTarget,
187
229
  reactorClass
188
230
  });
231
+ const entryContent = generateReactorEntryFile();
189
232
  fs2.mkdirSync(canisterOutDir, { recursive: true });
190
- if (!fs2.existsSync(reactorPath)) {
191
- fs2.writeFileSync(reactorPath, reactorContent);
192
- files.push({ success: true, filePath: reactorPath });
233
+ fs2.writeFileSync(reactorPath, reactorContent);
234
+ files.push({ success: true, filePath: reactorPath });
235
+ if (!fs2.existsSync(entryPath)) {
236
+ fs2.writeFileSync(entryPath, entryContent);
237
+ files.push({ success: true, filePath: entryPath });
193
238
  } 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 });
239
+ const existingEntryContent = fs2.readFileSync(entryPath, "utf-8");
240
+ if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
241
+ fs2.writeFileSync(entryPath, entryContent);
242
+ files.push({ success: true, filePath: entryPath });
198
243
  } else {
199
- files.push({ success: true, filePath: reactorPath, skipped: true });
244
+ files.push({ success: true, filePath: entryPath, skipped: true });
200
245
  }
201
246
  }
202
247
  } catch (err) {
@@ -279,6 +324,7 @@ export {
279
324
  extractMethods,
280
325
  generateClientFile,
281
326
  generateDeclarations,
327
+ generateReactorEntryFile,
282
328
  generateReactorFile,
283
329
  getReactorName,
284
330
  getServiceTypeName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/codegen",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
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.3.0",
40
+ "@types/node": "^25.3.5",
41
41
  "tsup": "^8.5.1",
42
42
  "typescript": "^5.9.3",
43
43
  "vitest": "^4.0.18"
@@ -12,7 +12,7 @@ export type BackendService = _SERVICE
12
12
  * Backend Reactor
13
13
  *
14
14
  * Auto-generated by @ic-reactor/codegen — do not edit.
15
- * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
15
+ * This file is overwritten whenever generation runs.
16
16
  */
17
17
  export const backendReactor = new DisplayReactor<BackendService>({
18
18
  clientManager,
@@ -43,7 +43,7 @@ export type WorkflowEngineService = _SERVICE
43
43
  * WorkflowEngine Reactor
44
44
  *
45
45
  * Auto-generated by @ic-reactor/codegen — do not edit.
46
- * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
46
+ * This file is overwritten whenever generation runs.
47
47
  */
48
48
  export const workflowEngineReactor = new Reactor<WorkflowEngineService>({
49
49
  clientManager,
@@ -74,7 +74,7 @@ export type LedgerService = _SERVICE
74
74
  * Ledger Reactor
75
75
  *
76
76
  * Auto-generated by @ic-reactor/codegen — do not edit.
77
- * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
77
+ * This file is overwritten whenever generation runs.
78
78
  */
79
79
  export const ledgerReactor = new MetadataDisplayReactor<LedgerService>({
80
80
  clientManager,
@@ -92,3 +92,24 @@ export const {
92
92
  } = createActorHooks(ledgerReactor)
93
93
  "
94
94
  `;
95
+
96
+ exports[`Reactor generator > supports core target generation without React hooks > core-display-reactor-index 1`] = `
97
+ "import { DisplayReactor } from "@ic-reactor/core"
98
+ import { clientManager } from "../../clients"
99
+ import { idlFactory, type _SERVICE } from "./declarations/backend"
100
+
101
+ export type BackendService = _SERVICE
102
+
103
+ /**
104
+ * Backend Reactor
105
+ *
106
+ * Auto-generated by @ic-reactor/codegen — do not edit.
107
+ * This file is overwritten whenever generation runs.
108
+ */
109
+ export const backendReactor = new DisplayReactor<BackendService>({
110
+ clientManager,
111
+ idlFactory,
112
+ name: "backend",
113
+ })
114
+ "
115
+ `;
@@ -11,7 +11,7 @@ export type {
11
11
  DeclarationsGeneratorResult,
12
12
  } from "./declarations.js"
13
13
 
14
- export { generateReactorFile } from "./reactor.js"
14
+ export { generateReactorFile, generateReactorEntryFile } from "./reactor.js"
15
15
  export type { ReactorGeneratorOptions } from "./reactor.js"
16
16
 
17
17
  export { generateClientFile } from "./client.js"
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Reactor File Generator
3
3
  *
4
- * Generates the main `index.ts` for a canister — a DisplayReactor instance
5
- * plus the full set of typed hooks via `createActorHooks`.
4
+ * Generates the managed `index.generated.ts` implementation for a canister.
5
+ * React targets also emit the full set of typed hooks via `createActorHooks`.
6
6
  *
7
7
  * Generated output example (for canister "backend"):
8
8
  *
@@ -22,7 +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
+ import type { CodegenTarget, ReactorClassName } from "../types.js"
26
26
 
27
27
  export interface ReactorGeneratorOptions {
28
28
  /** Canister name (e.g. "backend") */
@@ -37,6 +37,10 @@ export interface ReactorGeneratorOptions {
37
37
  * Default: "../../clients"
38
38
  */
39
39
  clientManagerPath?: string
40
+ /** Optional fixed canister ID for the generated reactor */
41
+ canisterId?: string
42
+ /** Generated runtime target */
43
+ runtimeTarget?: CodegenTarget
40
44
  /**
41
45
  * Which reactor class should back the generated hooks.
42
46
  * Default: "DisplayReactor" (backward compatible)
@@ -45,12 +49,13 @@ export interface ReactorGeneratorOptions {
45
49
  }
46
50
 
47
51
  function getReactorClassImportSource(
48
- reactorClass: ReactorClassName
49
- ): "@ic-reactor/react" | "@ic-reactor/candid" {
52
+ reactorClass: ReactorClassName,
53
+ runtimeTarget: CodegenTarget
54
+ ): "@ic-reactor/react" | "@ic-reactor/core" | "@ic-reactor/candid" {
50
55
  switch (reactorClass) {
51
56
  case "Reactor":
52
57
  case "DisplayReactor":
53
- return "@ic-reactor/react"
58
+ return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react"
54
59
  case "CandidReactor":
55
60
  case "CandidDisplayReactor":
56
61
  case "MetadataDisplayReactor":
@@ -59,13 +64,15 @@ function getReactorClassImportSource(
59
64
  }
60
65
 
61
66
  /**
62
- * Generate the content of a canister's `index.ts` file.
67
+ * Generate the content of a canister's managed implementation file.
63
68
  */
64
69
  export function generateReactorFile(options: ReactorGeneratorOptions): string {
65
70
  const {
66
71
  canisterName,
67
72
  didFile,
68
73
  clientManagerPath = "../../clients",
74
+ canisterId,
75
+ runtimeTarget = "react",
69
76
  reactorClass = "DisplayReactor",
70
77
  } = options
71
78
 
@@ -76,10 +83,29 @@ export function generateReactorFile(options: ReactorGeneratorOptions): string {
76
83
  // Derive the declarations import path from the .did filename
77
84
  const baseName = path.basename(didFile, ".did")
78
85
  const declarationsPath = `./declarations/${baseName}`
79
- const reactorImportSource = getReactorClassImportSource(reactorClass)
86
+ const reactorImportSource = getReactorClassImportSource(
87
+ reactorClass,
88
+ runtimeTarget
89
+ )
90
+ const canisterIdLine = canisterId
91
+ ? ` canisterId: ${JSON.stringify(canisterId)},\n`
92
+ : ""
93
+ const hookExports =
94
+ runtimeTarget === "react"
95
+ ? `
80
96
 
81
- return `import { createActorHooks } from "@ic-reactor/react"
82
- import { ${reactorClass} } from "${reactorImportSource}"
97
+ export const {
98
+ useActorQuery: use${pascalName}Query,
99
+ useActorSuspenseQuery: use${pascalName}SuspenseQuery,
100
+ useActorInfiniteQuery: use${pascalName}InfiniteQuery,
101
+ useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
102
+ useActorMutation: use${pascalName}Mutation,
103
+ useActorMethod: use${pascalName}Method,
104
+ } = createActorHooks(${reactorName})
105
+ `
106
+ : ""
107
+
108
+ return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from "${reactorImportSource}"
83
109
  import { clientManager } from "${clientManagerPath}"
84
110
  import { idlFactory, type _SERVICE } from "${declarationsPath}"
85
111
 
@@ -89,21 +115,25 @@ export type ${serviceName} = _SERVICE
89
115
  * ${pascalName} Reactor
90
116
  *
91
117
  * Auto-generated by @ic-reactor/codegen — do not edit.
92
- * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
118
+ * This file is overwritten whenever generation runs.
93
119
  */
94
120
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
95
121
  clientManager,
96
122
  idlFactory,
97
- name: "${canisterName}",
98
- })
123
+ ${canisterIdLine} name: "${canisterName}",
124
+ })${hookExports || "\n"}`
125
+ }
99
126
 
100
- export const {
101
- useActorQuery: use${pascalName}Query,
102
- useActorSuspenseQuery: use${pascalName}SuspenseQuery,
103
- useActorInfiniteQuery: use${pascalName}InfiniteQuery,
104
- useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
105
- useActorMutation: use${pascalName}Mutation,
106
- useActorMethod: use${pascalName}Method,
107
- } = createActorHooks(${reactorName})
127
+ /**
128
+ * Generate the user-facing `index.ts` wrapper content.
129
+ */
130
+ export function generateReactorEntryFile(): string {
131
+ return `/**
132
+ * Canister entrypoint.
133
+ *
134
+ * Created once by @ic-reactor/codegen and safe to customize.
135
+ * Keep the re-export below if you want generated exports and types to stay in sync.
136
+ */
137
+ export * from "./index.generated"
108
138
  `
109
139
  }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  export type {
10
10
  CanisterConfig,
11
11
  CodegenConfig,
12
+ CodegenTarget,
12
13
  GeneratorResult,
13
14
  ReactorClassName,
14
15
  } from "./types.js"
@@ -52,12 +52,177 @@ describe("Codegen pipeline", () => {
52
52
 
53
53
  const indexPath = path.join(
54
54
  projectRoot,
55
- "src/declarations/workflow_engine/index.ts"
55
+ "src/declarations/workflow_engine/index.generated.ts"
56
56
  )
57
57
  const generated = fs.readFileSync(indexPath, "utf-8")
58
58
 
59
59
  expect(generated).toContain("new Reactor<WorkflowEngineService>")
60
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")
61
226
  })
62
227
 
63
228
  it("does not overwrite user-modified index.ts on regenerate", async () => {
@@ -90,11 +255,22 @@ export const customBackendIndex = true
90
255
  `
91
256
  )
92
257
 
93
- const second = await runCanisterPipeline(options)
258
+ const second = await runCanisterPipeline({
259
+ ...options,
260
+ canisterConfig: {
261
+ ...options.canisterConfig,
262
+ canisterId: "yq4ns-hyaaa-aaaap-akbna-cai",
263
+ },
264
+ })
94
265
  expect(second.success).toBe(true)
95
266
 
96
267
  const wrapper = fs.readFileSync(indexPath, "utf-8")
97
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"')
98
274
  expect(second.files).toEqual(
99
275
  expect.arrayContaining([
100
276
  expect.objectContaining({
@@ -106,17 +282,14 @@ export const customBackendIndex = true
106
282
  )
107
283
  })
108
284
 
109
- it("overwrites legacy generated index.ts during regeneration", async () => {
285
+ it("migrates a legacy generated index.ts to the managed wrapper", async () => {
110
286
  const projectRoot = createTempProject()
111
287
  writeDid(projectRoot, "backend.did")
112
288
 
113
289
  const canisterOutDir = path.join(projectRoot, "src/declarations/backend")
114
290
  fs.mkdirSync(canisterOutDir, { recursive: true })
115
291
 
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"
292
+ const existingGeneratedIndex = `import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
120
293
  import { clientManager } from "../../clients"
121
294
  import { idlFactory, type _SERVICE } from "./declarations/backend"
122
295
 
@@ -126,7 +299,6 @@ export type BackendService = _SERVICE
126
299
  * Backend Reactor
127
300
  *
128
301
  * Auto-generated by @ic-reactor/codegen — do not edit.
129
- * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
130
302
  */
131
303
  export const backendReactor = new DisplayReactor<BackendService>({
132
304
  clientManager,
@@ -138,6 +310,9 @@ export const {
138
310
  useActorQuery: useBackendQuery,
139
311
  } = createActorHooks(backendReactor)
140
312
  `
313
+ fs.writeFileSync(
314
+ path.join(canisterOutDir, "index.ts"),
315
+ existingGeneratedIndex
141
316
  )
142
317
 
143
318
  const result = await runCanisterPipeline({
@@ -155,9 +330,26 @@ export const {
155
330
  expect(result.success).toBe(true)
156
331
 
157
332
  const indexPath = path.join(canisterOutDir, "index.ts")
158
- const generated = fs.readFileSync(indexPath, "utf-8")
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)
159
340
  expect(generated).toContain("new DisplayReactor<BackendService>")
160
341
  expect(generated).toContain("useBackendMutation")
161
- expect(generated).not.toContain('export * from "./index.generated"')
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
+ )
162
354
  })
163
355
  })
package/src/pipeline.ts CHANGED
@@ -7,7 +7,8 @@
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. Optionally generate reactor implementation (`index.generated.ts`)
11
+ * 4. Optionally create or migrate the user entry (`index.ts`)
11
12
  */
12
13
 
13
14
  import fs from "node:fs"
@@ -15,11 +16,15 @@ import path from "node:path"
15
16
  import type {
16
17
  CanisterConfig,
17
18
  CodegenConfig,
19
+ CodegenTarget,
18
20
  GeneratorResult,
19
21
  ReactorClassName,
20
22
  } from "./types.js"
21
23
  import { generateDeclarations } from "./generators/declarations.js"
22
- import { generateReactorFile } from "./generators/reactor.js"
24
+ import {
25
+ generateReactorEntryFile,
26
+ generateReactorFile,
27
+ } from "./generators/reactor.js"
23
28
 
24
29
  export interface PipelineOptions {
25
30
  /** Canister name and config */
@@ -32,20 +37,45 @@ export interface PipelineOptions {
32
37
  /**
33
38
  * Global codegen config (for fallback outDir and clientManagerPath).
34
39
  */
35
- globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">
40
+ globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath" | "target">
41
+ /**
42
+ * Whether the managed reactor files should be generated.
43
+ * Defaults to true.
44
+ */
45
+ generateReactor?: boolean
36
46
  }
37
47
 
38
48
  function resolveReactorClass(canisterConfig: CanisterConfig): ReactorClassName {
39
49
  return canisterConfig.mode ?? "DisplayReactor"
40
50
  }
41
51
 
42
- function isGeneratedIndexFile(content: string): boolean {
52
+ function resolveRuntimeTarget(
53
+ canisterConfig: CanisterConfig,
54
+ globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath" | "target">
55
+ ): CodegenTarget {
56
+ return canisterConfig.target ?? globalConfig.target ?? "react"
57
+ }
58
+
59
+ function normalizeFileContent(content: string): string {
60
+ return content.replace(/\r\n/g, "\n").trim()
61
+ }
62
+
63
+ function isLegacyGeneratedIndexFile(content: string): boolean {
43
64
  return (
44
65
  content.includes("Auto-generated by @ic-reactor/codegen") &&
45
66
  content.includes("createActorHooks(")
46
67
  )
47
68
  }
48
69
 
70
+ function isManagedEntryWrapper(
71
+ content: string,
72
+ expectedEntryContent: string
73
+ ): boolean {
74
+ return (
75
+ normalizeFileContent(content) === normalizeFileContent(expectedEntryContent)
76
+ )
77
+ }
78
+
49
79
  export interface PipelineResult {
50
80
  canisterName: string
51
81
  success: boolean
@@ -62,7 +92,12 @@ export interface PipelineResult {
62
92
  export async function runCanisterPipeline(
63
93
  options: PipelineOptions
64
94
  ): Promise<PipelineResult> {
65
- const { canisterConfig, projectRoot, globalConfig } = options
95
+ const {
96
+ canisterConfig,
97
+ projectRoot,
98
+ globalConfig,
99
+ generateReactor = true,
100
+ } = options
66
101
  const { name, didFile, clientManagerPath } = canisterConfig
67
102
 
68
103
  const files: GeneratorResult[] = []
@@ -122,31 +157,50 @@ export async function runCanisterPipeline(
122
157
  }
123
158
  }
124
159
 
160
+ if (!generateReactor) {
161
+ return {
162
+ canisterName: name,
163
+ success: true,
164
+ files,
165
+ }
166
+ }
167
+
125
168
  // ── Step 2: Reactor file ───────────────────────────────────────────────────
126
169
 
127
- const reactorPath = path.join(canisterOutDir, "index.ts")
170
+ const reactorPath = path.join(canisterOutDir, "index.generated.ts")
171
+ const entryPath = path.join(canisterOutDir, "index.ts")
128
172
  const reactorClass = resolveReactorClass(canisterConfig)
173
+ const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig)
129
174
 
130
175
  try {
131
176
  const reactorContent = generateReactorFile({
132
177
  canisterName: name,
133
178
  didFile: resolvedDidFile,
134
179
  clientManagerPath: resolvedClientManagerPath,
180
+ canisterId: canisterConfig.canisterId,
181
+ runtimeTarget,
135
182
  reactorClass,
136
183
  })
184
+ const entryContent = generateReactorEntryFile()
137
185
 
138
186
  fs.mkdirSync(canisterOutDir, { recursive: true })
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")
187
+ fs.writeFileSync(reactorPath, reactorContent)
188
+ files.push({ success: true, filePath: reactorPath })
144
189
 
145
- if (isGeneratedIndexFile(existingIndexContent)) {
146
- fs.writeFileSync(reactorPath, reactorContent)
147
- files.push({ success: true, filePath: reactorPath })
190
+ if (!fs.existsSync(entryPath)) {
191
+ fs.writeFileSync(entryPath, entryContent)
192
+ files.push({ success: true, filePath: entryPath })
193
+ } else {
194
+ const existingEntryContent = fs.readFileSync(entryPath, "utf-8")
195
+
196
+ if (
197
+ isLegacyGeneratedIndexFile(existingEntryContent) ||
198
+ isManagedEntryWrapper(existingEntryContent, entryContent)
199
+ ) {
200
+ fs.writeFileSync(entryPath, entryContent)
201
+ files.push({ success: true, filePath: entryPath })
148
202
  } else {
149
- files.push({ success: true, filePath: reactorPath, skipped: true })
203
+ files.push({ success: true, filePath: entryPath, skipped: true })
150
204
  }
151
205
  }
152
206
  } catch (err) {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import { generateReactorFile } from "./generators"
2
+ import { generateReactorEntryFile, generateReactorFile } from "./generators"
3
3
 
4
4
  describe("Reactor generator", () => {
5
5
  it("keeps default behavior as DisplayReactor", () => {
@@ -42,4 +42,40 @@ describe("Reactor generator", () => {
42
42
  )
43
43
  expect(content).toContain("new MetadataDisplayReactor<LedgerService>")
44
44
  })
45
+
46
+ it("supports core target generation without React hooks", () => {
47
+ const content = generateReactorFile({
48
+ canisterName: "backend",
49
+ didFile: "mock/backend.did",
50
+ runtimeTarget: "core",
51
+ reactorClass: "DisplayReactor",
52
+ })
53
+
54
+ expect(content).toMatchSnapshot("core-display-reactor-index")
55
+ expect(content).toContain(
56
+ 'import { DisplayReactor } from "@ic-reactor/core"'
57
+ )
58
+ expect(content).not.toContain(
59
+ 'import { createActorHooks } from "@ic-reactor/react"'
60
+ )
61
+ expect(content).not.toContain("useBackendQuery")
62
+ })
63
+
64
+ it("writes a fixed canisterId when configured", () => {
65
+ const content = generateReactorFile({
66
+ canisterName: "workflow",
67
+ didFile: "mock/workflow.did",
68
+ canisterId: "yq4ns-hyaaa-aaaap-akbna-cai",
69
+ })
70
+
71
+ expect(content).toContain('canisterId: "yq4ns-hyaaa-aaaap-akbna-cai"')
72
+ expect(content).toContain('name: "workflow"')
73
+ })
74
+
75
+ it("generates a stable entry wrapper", () => {
76
+ const content = generateReactorEntryFile()
77
+
78
+ expect(content).toContain('export * from "./index.generated"')
79
+ expect(content).toContain("safe to customize")
80
+ })
45
81
  })
package/src/types.ts CHANGED
@@ -29,6 +29,11 @@ export interface CanisterConfig {
29
29
  * Defaults to DisplayReactor for backward compatibility.
30
30
  */
31
31
  mode?: ReactorClassName
32
+ /**
33
+ * Generated runtime target.
34
+ * `react` emits bound React hooks, `core` emits only the typed reactor exports.
35
+ */
36
+ target?: CodegenTarget
32
37
  /** Optional fixed canister ID */
33
38
  canisterId?: string
34
39
  }
@@ -40,6 +45,8 @@ export type ReactorClassName =
40
45
  | "CandidDisplayReactor"
41
46
  | "MetadataDisplayReactor"
42
47
 
48
+ export type CodegenTarget = "react" | "core"
49
+
43
50
  /**
44
51
  * Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
45
52
  */
@@ -56,6 +63,11 @@ export interface CodegenConfig {
56
63
  * Individual canisters can override via `CanisterConfig.clientManagerPath`.
57
64
  */
58
65
  clientManagerPath?: string
66
+ /**
67
+ * Default generated runtime target.
68
+ * Individual canisters can override via `CanisterConfig.target`.
69
+ */
70
+ target?: CodegenTarget
59
71
  /** Canister configurations, keyed by canister name */
60
72
  canisters: Record<string, CanisterConfig>
61
73
  }