@ic-reactor/codegen 0.7.2 → 0.8.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
@@ -38,14 +38,15 @@ Set `canisterConfig.mode` to choose the generated reactor class:
38
38
  - `CandidDisplayReactor`
39
39
  - `MetadataDisplayReactor`
40
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.
41
+ 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.
42
42
 
43
43
  ## Generators
44
44
 
45
45
  You can also use individual generators if you need more granular control:
46
46
 
47
47
  - **`generateDeclarations`**: Generates `.js` (factory), `.d.ts` (types), and `.did` copy.
48
- - **`generateReactorFile`**: Generates the `index.ts` file using either `DisplayReactor` or `Reactor`.
48
+ - **`generateReactorFile`**: Generates the managed `index.generated.ts` implementation using either `DisplayReactor` or `Reactor`.
49
+ - **`generateReactorEntryFile`**: Generates the stable `index.ts` wrapper that re-exports from `index.generated.ts`.
49
50
  - **`generateClientFile`**: Generates a `ClientManager` boilerplate file.
50
51
 
51
52
  ## 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,
@@ -139,6 +140,7 @@ function generateReactorFile(options) {
139
140
  canisterName,
140
141
  didFile,
141
142
  clientManagerPath = "../../clients",
143
+ canisterId,
142
144
  reactorClass = "DisplayReactor"
143
145
  } = options;
144
146
  const pascalName = toPascalCase(canisterName);
@@ -147,6 +149,8 @@ function generateReactorFile(options) {
147
149
  const baseName = import_node_path2.default.basename(didFile, ".did");
148
150
  const declarationsPath = `./declarations/${baseName}`;
149
151
  const reactorImportSource = getReactorClassImportSource(reactorClass);
152
+ const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
153
+ ` : "";
150
154
  return `import { createActorHooks } from "@ic-reactor/react"
151
155
  import { ${reactorClass} } from "${reactorImportSource}"
152
156
  import { clientManager } from "${clientManagerPath}"
@@ -158,12 +162,12 @@ export type ${serviceName} = _SERVICE
158
162
  * ${pascalName} Reactor
159
163
  *
160
164
  * Auto-generated by @ic-reactor/codegen \u2014 do not edit.
161
- * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
165
+ * This file is overwritten whenever generation runs.
162
166
  */
163
167
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
164
168
  clientManager,
165
169
  idlFactory,
166
- name: "${canisterName}",
170
+ ${canisterIdLine} name: "${canisterName}",
167
171
  })
168
172
 
169
173
  export const {
@@ -176,14 +180,30 @@ export const {
176
180
  } = createActorHooks(${reactorName})
177
181
  `;
178
182
  }
183
+ function generateReactorEntryFile() {
184
+ return `/**
185
+ * Canister entrypoint.
186
+ *
187
+ * Created once by @ic-reactor/codegen and safe to customize.
188
+ * Keep the re-export below if you want generated hooks and types to stay in sync.
189
+ */
190
+ export * from "./index.generated"
191
+ `;
192
+ }
179
193
 
180
194
  // src/pipeline.ts
181
195
  function resolveReactorClass(canisterConfig) {
182
196
  return canisterConfig.mode ?? "DisplayReactor";
183
197
  }
184
- function isGeneratedIndexFile(content) {
198
+ function normalizeFileContent(content) {
199
+ return content.replace(/\r\n/g, "\n").trim();
200
+ }
201
+ function isLegacyGeneratedIndexFile(content) {
185
202
  return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(");
186
203
  }
204
+ function isManagedEntryWrapper(content, expectedEntryContent) {
205
+ return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
206
+ }
187
207
  async function runCanisterPipeline(options) {
188
208
  const { canisterConfig, projectRoot, globalConfig } = options;
189
209
  const { name, didFile, clientManagerPath } = canisterConfig;
@@ -222,26 +242,31 @@ async function runCanisterPipeline(options) {
222
242
  error: `Declarations step failed: ${err instanceof Error ? err.message : String(err)}`
223
243
  };
224
244
  }
225
- const reactorPath = import_node_path3.default.join(canisterOutDir, "index.ts");
245
+ const reactorPath = import_node_path3.default.join(canisterOutDir, "index.generated.ts");
246
+ const entryPath = import_node_path3.default.join(canisterOutDir, "index.ts");
226
247
  const reactorClass = resolveReactorClass(canisterConfig);
227
248
  try {
228
249
  const reactorContent = generateReactorFile({
229
250
  canisterName: name,
230
251
  didFile: resolvedDidFile,
231
252
  clientManagerPath: resolvedClientManagerPath,
253
+ canisterId: canisterConfig.canisterId,
232
254
  reactorClass
233
255
  });
256
+ const entryContent = generateReactorEntryFile();
234
257
  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 });
258
+ import_node_fs2.default.writeFileSync(reactorPath, reactorContent);
259
+ files.push({ success: true, filePath: reactorPath });
260
+ if (!import_node_fs2.default.existsSync(entryPath)) {
261
+ import_node_fs2.default.writeFileSync(entryPath, entryContent);
262
+ files.push({ success: true, filePath: entryPath });
238
263
  } 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 });
264
+ const existingEntryContent = import_node_fs2.default.readFileSync(entryPath, "utf-8");
265
+ if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
266
+ import_node_fs2.default.writeFileSync(entryPath, entryContent);
267
+ files.push({ success: true, filePath: entryPath });
243
268
  } else {
244
- files.push({ success: true, filePath: reactorPath, skipped: true });
269
+ files.push({ success: true, filePath: entryPath, skipped: true });
245
270
  }
246
271
  }
247
272
  } catch (err) {
@@ -325,6 +350,7 @@ export const clientManager = new ClientManager({
325
350
  extractMethods,
326
351
  generateClientFile,
327
352
  generateDeclarations,
353
+ generateReactorEntryFile,
328
354
  generateReactorFile,
329
355
  getReactorName,
330
356
  getServiceTypeName,
package/dist/index.d.cts CHANGED
@@ -68,7 +68,8 @@ interface GeneratorResult {
68
68
  * Pipeline steps (in order):
69
69
  * 1. Resolve paths (didFile, outDir)
70
70
  * 2. Generate declarations (JS + .d.ts + .did copy)
71
- * 3. Generate reactor file (`index.ts`)
71
+ * 3. Generate reactor implementation (`index.generated.ts`)
72
+ * 4. Create or migrate the user entry (`index.ts`)
72
73
  */
73
74
 
74
75
  interface PipelineOptions {
@@ -192,7 +193,7 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
192
193
  /**
193
194
  * Reactor File Generator
194
195
  *
195
- * Generates the main `index.ts` for a canister — a DisplayReactor instance
196
+ * Generates the managed `index.generated.ts` implementation for a canister
196
197
  * plus the full set of typed hooks via `createActorHooks`.
197
198
  *
198
199
  * Generated output example (for canister "backend"):
@@ -224,6 +225,8 @@ interface ReactorGeneratorOptions {
224
225
  * Default: "../../clients"
225
226
  */
226
227
  clientManagerPath?: string;
228
+ /** Optional fixed canister ID for the generated reactor */
229
+ canisterId?: string;
227
230
  /**
228
231
  * Which reactor class should back the generated hooks.
229
232
  * Default: "DisplayReactor" (backward compatible)
@@ -231,9 +234,13 @@ interface ReactorGeneratorOptions {
231
234
  reactorClass?: ReactorClassName;
232
235
  }
233
236
  /**
234
- * Generate the content of a canister's `index.ts` file.
237
+ * Generate the content of a canister's managed implementation file.
235
238
  */
236
239
  declare function generateReactorFile(options: ReactorGeneratorOptions): string;
240
+ /**
241
+ * Generate the user-facing `index.ts` wrapper content.
242
+ */
243
+ declare function generateReactorEntryFile(): string;
237
244
 
238
245
  /**
239
246
  * Client Manager Generator
@@ -258,4 +265,4 @@ interface ClientGeneratorOptions {
258
265
  */
259
266
  declare function generateClientFile(options?: ClientGeneratorOptions): string;
260
267
 
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 };
268
+ 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, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
package/dist/index.d.ts CHANGED
@@ -68,7 +68,8 @@ interface GeneratorResult {
68
68
  * Pipeline steps (in order):
69
69
  * 1. Resolve paths (didFile, outDir)
70
70
  * 2. Generate declarations (JS + .d.ts + .did copy)
71
- * 3. Generate reactor file (`index.ts`)
71
+ * 3. Generate reactor implementation (`index.generated.ts`)
72
+ * 4. Create or migrate the user entry (`index.ts`)
72
73
  */
73
74
 
74
75
  interface PipelineOptions {
@@ -192,7 +193,7 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
192
193
  /**
193
194
  * Reactor File Generator
194
195
  *
195
- * Generates the main `index.ts` for a canister — a DisplayReactor instance
196
+ * Generates the managed `index.generated.ts` implementation for a canister
196
197
  * plus the full set of typed hooks via `createActorHooks`.
197
198
  *
198
199
  * Generated output example (for canister "backend"):
@@ -224,6 +225,8 @@ interface ReactorGeneratorOptions {
224
225
  * Default: "../../clients"
225
226
  */
226
227
  clientManagerPath?: string;
228
+ /** Optional fixed canister ID for the generated reactor */
229
+ canisterId?: string;
227
230
  /**
228
231
  * Which reactor class should back the generated hooks.
229
232
  * Default: "DisplayReactor" (backward compatible)
@@ -231,9 +234,13 @@ interface ReactorGeneratorOptions {
231
234
  reactorClass?: ReactorClassName;
232
235
  }
233
236
  /**
234
- * Generate the content of a canister's `index.ts` file.
237
+ * Generate the content of a canister's managed implementation file.
235
238
  */
236
239
  declare function generateReactorFile(options: ReactorGeneratorOptions): string;
240
+ /**
241
+ * Generate the user-facing `index.ts` wrapper content.
242
+ */
243
+ declare function generateReactorEntryFile(): string;
237
244
 
238
245
  /**
239
246
  * Client Manager Generator
@@ -258,4 +265,4 @@ interface ClientGeneratorOptions {
258
265
  */
259
266
  declare function generateClientFile(options?: ClientGeneratorOptions): string;
260
267
 
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 };
268
+ 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, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
package/dist/index.js CHANGED
@@ -94,6 +94,7 @@ function generateReactorFile(options) {
94
94
  canisterName,
95
95
  didFile,
96
96
  clientManagerPath = "../../clients",
97
+ canisterId,
97
98
  reactorClass = "DisplayReactor"
98
99
  } = options;
99
100
  const pascalName = toPascalCase(canisterName);
@@ -102,6 +103,8 @@ function generateReactorFile(options) {
102
103
  const baseName = path2.basename(didFile, ".did");
103
104
  const declarationsPath = `./declarations/${baseName}`;
104
105
  const reactorImportSource = getReactorClassImportSource(reactorClass);
106
+ const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
107
+ ` : "";
105
108
  return `import { createActorHooks } from "@ic-reactor/react"
106
109
  import { ${reactorClass} } from "${reactorImportSource}"
107
110
  import { clientManager } from "${clientManagerPath}"
@@ -113,12 +116,12 @@ export type ${serviceName} = _SERVICE
113
116
  * ${pascalName} Reactor
114
117
  *
115
118
  * Auto-generated by @ic-reactor/codegen \u2014 do not edit.
116
- * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
119
+ * This file is overwritten whenever generation runs.
117
120
  */
118
121
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
119
122
  clientManager,
120
123
  idlFactory,
121
- name: "${canisterName}",
124
+ ${canisterIdLine} name: "${canisterName}",
122
125
  })
123
126
 
124
127
  export const {
@@ -131,14 +134,30 @@ export const {
131
134
  } = createActorHooks(${reactorName})
132
135
  `;
133
136
  }
137
+ function generateReactorEntryFile() {
138
+ return `/**
139
+ * Canister entrypoint.
140
+ *
141
+ * Created once by @ic-reactor/codegen and safe to customize.
142
+ * Keep the re-export below if you want generated hooks and types to stay in sync.
143
+ */
144
+ export * from "./index.generated"
145
+ `;
146
+ }
134
147
 
135
148
  // src/pipeline.ts
136
149
  function resolveReactorClass(canisterConfig) {
137
150
  return canisterConfig.mode ?? "DisplayReactor";
138
151
  }
139
- function isGeneratedIndexFile(content) {
152
+ function normalizeFileContent(content) {
153
+ return content.replace(/\r\n/g, "\n").trim();
154
+ }
155
+ function isLegacyGeneratedIndexFile(content) {
140
156
  return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(");
141
157
  }
158
+ function isManagedEntryWrapper(content, expectedEntryContent) {
159
+ return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
160
+ }
142
161
  async function runCanisterPipeline(options) {
143
162
  const { canisterConfig, projectRoot, globalConfig } = options;
144
163
  const { name, didFile, clientManagerPath } = canisterConfig;
@@ -177,26 +196,31 @@ async function runCanisterPipeline(options) {
177
196
  error: `Declarations step failed: ${err instanceof Error ? err.message : String(err)}`
178
197
  };
179
198
  }
180
- const reactorPath = path3.join(canisterOutDir, "index.ts");
199
+ const reactorPath = path3.join(canisterOutDir, "index.generated.ts");
200
+ const entryPath = path3.join(canisterOutDir, "index.ts");
181
201
  const reactorClass = resolveReactorClass(canisterConfig);
182
202
  try {
183
203
  const reactorContent = generateReactorFile({
184
204
  canisterName: name,
185
205
  didFile: resolvedDidFile,
186
206
  clientManagerPath: resolvedClientManagerPath,
207
+ canisterId: canisterConfig.canisterId,
187
208
  reactorClass
188
209
  });
210
+ const entryContent = generateReactorEntryFile();
189
211
  fs2.mkdirSync(canisterOutDir, { recursive: true });
190
- if (!fs2.existsSync(reactorPath)) {
191
- fs2.writeFileSync(reactorPath, reactorContent);
192
- files.push({ success: true, filePath: reactorPath });
212
+ fs2.writeFileSync(reactorPath, reactorContent);
213
+ files.push({ success: true, filePath: reactorPath });
214
+ if (!fs2.existsSync(entryPath)) {
215
+ fs2.writeFileSync(entryPath, entryContent);
216
+ files.push({ success: true, filePath: entryPath });
193
217
  } 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 });
218
+ const existingEntryContent = fs2.readFileSync(entryPath, "utf-8");
219
+ if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
220
+ fs2.writeFileSync(entryPath, entryContent);
221
+ files.push({ success: true, filePath: entryPath });
198
222
  } else {
199
- files.push({ success: true, filePath: reactorPath, skipped: true });
223
+ files.push({ success: true, filePath: entryPath, skipped: true });
200
224
  }
201
225
  }
202
226
  } catch (err) {
@@ -279,6 +303,7 @@ export {
279
303
  extractMethods,
280
304
  generateClientFile,
281
305
  generateDeclarations,
306
+ generateReactorEntryFile,
282
307
  generateReactorFile,
283
308
  getReactorName,
284
309
  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.8.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,
@@ -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,7 +1,7 @@
1
1
  /**
2
2
  * Reactor File Generator
3
3
  *
4
- * Generates the main `index.ts` for a canister — a DisplayReactor instance
4
+ * Generates the managed `index.generated.ts` implementation for a canister
5
5
  * plus the full set of typed hooks via `createActorHooks`.
6
6
  *
7
7
  * Generated output example (for canister "backend"):
@@ -37,6 +37,8 @@ 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
40
42
  /**
41
43
  * Which reactor class should back the generated hooks.
42
44
  * Default: "DisplayReactor" (backward compatible)
@@ -59,13 +61,14 @@ function getReactorClassImportSource(
59
61
  }
60
62
 
61
63
  /**
62
- * Generate the content of a canister's `index.ts` file.
64
+ * Generate the content of a canister's managed implementation file.
63
65
  */
64
66
  export function generateReactorFile(options: ReactorGeneratorOptions): string {
65
67
  const {
66
68
  canisterName,
67
69
  didFile,
68
70
  clientManagerPath = "../../clients",
71
+ canisterId,
69
72
  reactorClass = "DisplayReactor",
70
73
  } = options
71
74
 
@@ -77,6 +80,9 @@ export function generateReactorFile(options: ReactorGeneratorOptions): string {
77
80
  const baseName = path.basename(didFile, ".did")
78
81
  const declarationsPath = `./declarations/${baseName}`
79
82
  const reactorImportSource = getReactorClassImportSource(reactorClass)
83
+ const canisterIdLine = canisterId
84
+ ? ` canisterId: ${JSON.stringify(canisterId)},\n`
85
+ : ""
80
86
 
81
87
  return `import { createActorHooks } from "@ic-reactor/react"
82
88
  import { ${reactorClass} } from "${reactorImportSource}"
@@ -89,12 +95,12 @@ export type ${serviceName} = _SERVICE
89
95
  * ${pascalName} Reactor
90
96
  *
91
97
  * Auto-generated by @ic-reactor/codegen — do not edit.
92
- * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
98
+ * This file is overwritten whenever generation runs.
93
99
  */
94
100
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
95
101
  clientManager,
96
102
  idlFactory,
97
- name: "${canisterName}",
103
+ ${canisterIdLine} name: "${canisterName}",
98
104
  })
99
105
 
100
106
  export const {
@@ -107,3 +113,17 @@ export const {
107
113
  } = createActorHooks(${reactorName})
108
114
  `
109
115
  }
116
+
117
+ /**
118
+ * Generate the user-facing `index.ts` wrapper content.
119
+ */
120
+ export function generateReactorEntryFile(): string {
121
+ return `/**
122
+ * Canister entrypoint.
123
+ *
124
+ * Created once by @ic-reactor/codegen and safe to customize.
125
+ * Keep the re-export below if you want generated hooks and types to stay in sync.
126
+ */
127
+ export * from "./index.generated"
128
+ `
129
+ }
@@ -52,12 +52,47 @@ 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"')
61
96
  })
62
97
 
63
98
  it("does not overwrite user-modified index.ts on regenerate", async () => {
@@ -90,11 +125,22 @@ export const customBackendIndex = true
90
125
  `
91
126
  )
92
127
 
93
- const second = await runCanisterPipeline(options)
128
+ const second = await runCanisterPipeline({
129
+ ...options,
130
+ canisterConfig: {
131
+ ...options.canisterConfig,
132
+ canisterId: "yq4ns-hyaaa-aaaap-akbna-cai",
133
+ },
134
+ })
94
135
  expect(second.success).toBe(true)
95
136
 
96
137
  const wrapper = fs.readFileSync(indexPath, "utf-8")
97
138
  expect(wrapper).toContain("customBackendIndex = true")
139
+ const generatedImpl = fs.readFileSync(
140
+ path.join(projectRoot, "src/declarations/backend/index.generated.ts"),
141
+ "utf-8"
142
+ )
143
+ expect(generatedImpl).toContain('canisterId: "yq4ns-hyaaa-aaaap-akbna-cai"')
98
144
  expect(second.files).toEqual(
99
145
  expect.arrayContaining([
100
146
  expect.objectContaining({
@@ -106,17 +152,14 @@ export const customBackendIndex = true
106
152
  )
107
153
  })
108
154
 
109
- it("overwrites legacy generated index.ts during regeneration", async () => {
155
+ it("migrates a legacy generated index.ts to the managed wrapper", async () => {
110
156
  const projectRoot = createTempProject()
111
157
  writeDid(projectRoot, "backend.did")
112
158
 
113
159
  const canisterOutDir = path.join(projectRoot, "src/declarations/backend")
114
160
  fs.mkdirSync(canisterOutDir, { recursive: true })
115
161
 
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"
162
+ const existingGeneratedIndex = `import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
120
163
  import { clientManager } from "../../clients"
121
164
  import { idlFactory, type _SERVICE } from "./declarations/backend"
122
165
 
@@ -126,7 +169,6 @@ export type BackendService = _SERVICE
126
169
  * Backend Reactor
127
170
  *
128
171
  * Auto-generated by @ic-reactor/codegen — do not edit.
129
- * Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
130
172
  */
131
173
  export const backendReactor = new DisplayReactor<BackendService>({
132
174
  clientManager,
@@ -138,6 +180,9 @@ export const {
138
180
  useActorQuery: useBackendQuery,
139
181
  } = createActorHooks(backendReactor)
140
182
  `
183
+ fs.writeFileSync(
184
+ path.join(canisterOutDir, "index.ts"),
185
+ existingGeneratedIndex
141
186
  )
142
187
 
143
188
  const result = await runCanisterPipeline({
@@ -155,9 +200,26 @@ export const {
155
200
  expect(result.success).toBe(true)
156
201
 
157
202
  const indexPath = path.join(canisterOutDir, "index.ts")
158
- const generated = fs.readFileSync(indexPath, "utf-8")
203
+ const entry = fs.readFileSync(indexPath, "utf-8")
204
+ const generated = fs.readFileSync(
205
+ path.join(canisterOutDir, "index.generated.ts"),
206
+ "utf-8"
207
+ )
208
+ expect(entry).toContain('export * from "./index.generated"')
209
+ expect(entry).not.toBe(existingGeneratedIndex)
159
210
  expect(generated).toContain("new DisplayReactor<BackendService>")
160
211
  expect(generated).toContain("useBackendMutation")
161
- expect(generated).not.toContain('export * from "./index.generated"')
212
+ expect(result.files).toEqual(
213
+ expect.arrayContaining([
214
+ expect.objectContaining({
215
+ filePath: path.join(canisterOutDir, "index.generated.ts"),
216
+ success: true,
217
+ }),
218
+ expect.objectContaining({
219
+ filePath: indexPath,
220
+ success: true,
221
+ }),
222
+ ])
223
+ )
162
224
  })
163
225
  })
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. Generate reactor implementation (`index.generated.ts`)
11
+ * 4. Create or migrate the user entry (`index.ts`)
11
12
  */
12
13
 
13
14
  import fs from "node:fs"
@@ -19,7 +20,10 @@ import type {
19
20
  ReactorClassName,
20
21
  } from "./types.js"
21
22
  import { generateDeclarations } from "./generators/declarations.js"
22
- import { generateReactorFile } from "./generators/reactor.js"
23
+ import {
24
+ generateReactorEntryFile,
25
+ generateReactorFile,
26
+ } from "./generators/reactor.js"
23
27
 
24
28
  export interface PipelineOptions {
25
29
  /** Canister name and config */
@@ -39,13 +43,26 @@ function resolveReactorClass(canisterConfig: CanisterConfig): ReactorClassName {
39
43
  return canisterConfig.mode ?? "DisplayReactor"
40
44
  }
41
45
 
42
- function isGeneratedIndexFile(content: string): boolean {
46
+ function normalizeFileContent(content: string): string {
47
+ return content.replace(/\r\n/g, "\n").trim()
48
+ }
49
+
50
+ function isLegacyGeneratedIndexFile(content: string): boolean {
43
51
  return (
44
52
  content.includes("Auto-generated by @ic-reactor/codegen") &&
45
53
  content.includes("createActorHooks(")
46
54
  )
47
55
  }
48
56
 
57
+ function isManagedEntryWrapper(
58
+ content: string,
59
+ expectedEntryContent: string
60
+ ): boolean {
61
+ return (
62
+ normalizeFileContent(content) === normalizeFileContent(expectedEntryContent)
63
+ )
64
+ }
65
+
49
66
  export interface PipelineResult {
50
67
  canisterName: string
51
68
  success: boolean
@@ -124,7 +141,8 @@ export async function runCanisterPipeline(
124
141
 
125
142
  // ── Step 2: Reactor file ───────────────────────────────────────────────────
126
143
 
127
- const reactorPath = path.join(canisterOutDir, "index.ts")
144
+ const reactorPath = path.join(canisterOutDir, "index.generated.ts")
145
+ const entryPath = path.join(canisterOutDir, "index.ts")
128
146
  const reactorClass = resolveReactorClass(canisterConfig)
129
147
 
130
148
  try {
@@ -132,21 +150,29 @@ export async function runCanisterPipeline(
132
150
  canisterName: name,
133
151
  didFile: resolvedDidFile,
134
152
  clientManagerPath: resolvedClientManagerPath,
153
+ canisterId: canisterConfig.canisterId,
135
154
  reactorClass,
136
155
  })
156
+ const entryContent = generateReactorEntryFile()
137
157
 
138
158
  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")
159
+ fs.writeFileSync(reactorPath, reactorContent)
160
+ files.push({ success: true, filePath: reactorPath })
144
161
 
145
- if (isGeneratedIndexFile(existingIndexContent)) {
146
- fs.writeFileSync(reactorPath, reactorContent)
147
- files.push({ success: true, filePath: reactorPath })
162
+ if (!fs.existsSync(entryPath)) {
163
+ fs.writeFileSync(entryPath, entryContent)
164
+ files.push({ success: true, filePath: entryPath })
165
+ } else {
166
+ const existingEntryContent = fs.readFileSync(entryPath, "utf-8")
167
+
168
+ if (
169
+ isLegacyGeneratedIndexFile(existingEntryContent) ||
170
+ isManagedEntryWrapper(existingEntryContent, entryContent)
171
+ ) {
172
+ fs.writeFileSync(entryPath, entryContent)
173
+ files.push({ success: true, filePath: entryPath })
148
174
  } else {
149
- files.push({ success: true, filePath: reactorPath, skipped: true })
175
+ files.push({ success: true, filePath: entryPath, skipped: true })
150
176
  }
151
177
  }
152
178
  } 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,22 @@ describe("Reactor generator", () => {
42
42
  )
43
43
  expect(content).toContain("new MetadataDisplayReactor<LedgerService>")
44
44
  })
45
+
46
+ it("writes a fixed canisterId when configured", () => {
47
+ const content = generateReactorFile({
48
+ canisterName: "workflow",
49
+ didFile: "mock/workflow.did",
50
+ canisterId: "yq4ns-hyaaa-aaaap-akbna-cai",
51
+ })
52
+
53
+ expect(content).toContain('canisterId: "yq4ns-hyaaa-aaaap-akbna-cai"')
54
+ expect(content).toContain('name: "workflow"')
55
+ })
56
+
57
+ it("generates a stable entry wrapper", () => {
58
+ const content = generateReactorEntryFile()
59
+
60
+ expect(content).toContain('export * from "./index.generated"')
61
+ expect(content).toContain("safe to customize")
62
+ })
45
63
  })