@ic-reactor/codegen 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,7 +45,9 @@ Set `target` to control whether generated files include React hooks:
45
45
  - `react` (default): generates the reactor plus bound `createActorHooks` exports
46
46
  - `core`: generates only the typed reactor exports with no `@ic-reactor/react` dependency
47
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.
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. A scaffold that carries any export the old generator never wrote counts as user-owned and is left alone; one that is migrated is copied to `index.ts.bak` first.
49
+
50
+ Each canister needs its own `outDir`. Generating into a directory that already holds another canister's `index.generated.ts` fails rather than overwriting it, because every run replaces `declarations/` wholesale.
49
51
 
50
52
  Set `generateReactor: false` if you only want the bindgen/declaration output and
51
53
  need to skip `index.generated.ts` and `index.ts`.
@@ -54,7 +56,7 @@ need to skip `index.generated.ts` and `index.ts`.
54
56
 
55
57
  You can also use individual generators if you need more granular control:
56
58
 
57
- - **`generateDeclarations`**: Writes `declarations/<did-basename>.js` (factory), `.d.ts` (types), and a `.did` copy. The `declarations/` directory is wiped and recreated on every run.
59
+ - **`generateDeclarations`**: Writes `declarations/<did-basename>.js` (factory), `.d.ts` (types), and a `.did` copy. Generation runs in a staging directory that replaces `declarations/` only once every file is written, so a `.did` that fails to parse leaves the previous declarations untouched. A `.did` that parses but declares no `service` is rejected — it would produce no `idlFactory` and no `_SERVICE`.
58
60
  - **`generateReactorFile`**: Generates the managed `index.generated.ts` implementation using any `ReactorClassName` — `Reactor`, `DisplayReactor` (default), `CandidReactor`, `CandidDisplayReactor`, or `MetadataDisplayReactor`. With `target: "react"` it also emits the six `createActorHooks` exports (`use<Canister>Query`, `use<Canister>SuspenseQuery`, `use<Canister>InfiniteQuery`, `use<Canister>SuspenseInfiniteQuery`, `use<Canister>Mutation`, `use<Canister>Method`). No `createQuery` / `createMutation` objects are generated.
59
61
  - **`generateReactorEntryFile`**: Generates the stable `index.ts` wrapper that re-exports from `index.generated.ts`.
60
62
  - **`generateClientFile`**: Generates a `ClientManager` boilerplate file that
@@ -62,8 +64,17 @@ You can also use individual generators if you need more granular control:
62
64
 
63
65
  ## Utilities
64
66
 
65
- - **`parseDIDFile` / `extractMethods`**: Parse a `.did` file and extract method signatures.
66
67
  - **`toPascalCase` / `getReactorName` / `getServiceTypeName`**: Naming helpers.
68
+ - **`assertSafeCanisterConfig` and friends**: Validate a canister config before generating; the pipeline runs these itself.
69
+
70
+ `parseDIDFile` / `extractMethods` (and the `MethodInfo` / `MethodType` types)
71
+ have been removed. They regex-scraped the pretty-printed JS from
72
+ `didToJs`, which silently returned no methods for any service whose first
73
+ method took an inline record, dropped every method whose signature wrapped over
74
+ 80 columns, and reported `composite_query` as a mutation. Read a `.did` with
75
+ [`parseDid()`](https://ic-reactor.b3pay.net/v3/packages/parser) from
76
+ `@ic-reactor/parser` instead — it returns `{ name, mode, args, returns }` per
77
+ method, straight from the Rust parser.
67
78
 
68
79
  ## License
69
80
 
package/dist/index.cjs CHANGED
@@ -40,15 +40,14 @@ __export(index_exports, {
40
40
  assertSafeCanisterName: () => assertSafeCanisterName,
41
41
  assertSafeModuleSpecifier: () => assertSafeModuleSpecifier,
42
42
  declarationsExist: () => declarationsExist,
43
- extractMethods: () => extractMethods,
44
43
  generateClientFile: () => generateClientFile,
45
44
  generateDeclarations: () => generateDeclarations,
46
45
  generateReactorEntryFile: () => generateReactorEntryFile,
47
46
  generateReactorFile: () => generateReactorFile,
48
47
  getReactorName: () => getReactorName,
49
48
  getServiceTypeName: () => getServiceTypeName,
50
- parseDIDFile: () => parseDIDFile,
51
49
  resolveContainedOutDir: () => resolveContainedOutDir,
50
+ resolveDeclarationsBaseName: () => resolveDeclarationsBaseName,
52
51
  runCanisterPipeline: () => runCanisterPipeline,
53
52
  toPascalCase: () => toPascalCase
54
53
  });
@@ -56,67 +55,16 @@ module.exports = __toCommonJS(index_exports);
56
55
 
57
56
  // src/pipeline.ts
58
57
  var import_node_fs3 = __toESM(require("fs"), 1);
59
- var import_node_path4 = __toESM(require("path"), 1);
58
+ var import_node_path3 = __toESM(require("path"), 1);
60
59
 
61
60
  // src/generators/declarations.ts
62
61
  var import_parser = require("@ic-reactor/parser");
63
- var import_node_path = __toESM(require("path"), 1);
64
- var import_node_fs = __toESM(require("fs"), 1);
65
- async function generateDeclarations(options) {
66
- const { didFile, outDir, canisterName } = options;
67
- if (!import_node_fs.default.existsSync(didFile)) {
68
- return {
69
- success: false,
70
- declarationsDir: "",
71
- files: [],
72
- error: `DID file not found: ${didFile}`
73
- };
74
- }
75
- const declarationsDir = import_node_path.default.join(outDir, "declarations");
76
- const baseName = import_node_path.default.basename(didFile, ".did");
77
- try {
78
- const didContent = import_node_fs.default.readFileSync(didFile, "utf-8");
79
- if (!import_node_fs.default.existsSync(outDir)) {
80
- import_node_fs.default.mkdirSync(outDir, { recursive: true });
81
- }
82
- if (import_node_fs.default.existsSync(declarationsDir)) {
83
- import_node_fs.default.rmSync(declarationsDir, { recursive: true, force: true });
84
- }
85
- import_node_fs.default.mkdirSync(declarationsDir, { recursive: true });
86
- const jsContent = (0, import_parser.didToJs)(didContent);
87
- const tsContent = (0, import_parser.didToTs)(didContent);
88
- const jsPath = import_node_path.default.join(declarationsDir, `${baseName}.js`);
89
- const dtsPath = import_node_path.default.join(declarationsDir, `${baseName}.d.ts`);
90
- const didCopyPath = import_node_path.default.join(declarationsDir, `${baseName}.did`);
91
- import_node_fs.default.writeFileSync(jsPath, jsContent);
92
- import_node_fs.default.writeFileSync(dtsPath, tsContent);
93
- import_node_fs.default.writeFileSync(didCopyPath, didContent);
94
- return {
95
- success: true,
96
- declarationsDir,
97
- files: [
98
- { success: true, filePath: jsPath },
99
- { success: true, filePath: dtsPath },
100
- { success: true, filePath: didCopyPath }
101
- ]
102
- };
103
- } catch (error) {
104
- const message = error instanceof Error ? error.message : String(error);
105
- return {
106
- success: false,
107
- declarationsDir,
108
- files: [],
109
- error: `[${canisterName}] Failed to generate declarations: ${message}`
110
- };
111
- }
112
- }
113
- function declarationsExist(outDir, canisterName) {
114
- const dtsPath = import_node_path.default.join(outDir, "declarations", `${canisterName}.d.ts`);
115
- return import_node_fs.default.existsSync(dtsPath);
116
- }
117
-
118
- // src/generators/reactor.ts
119
62
  var import_node_path2 = __toESM(require("path"), 1);
63
+ var import_node_fs2 = __toESM(require("fs"), 1);
64
+
65
+ // src/validate.ts
66
+ var import_node_fs = __toESM(require("fs"), 1);
67
+ var import_node_path = __toESM(require("path"), 1);
120
68
 
121
69
  // src/naming.ts
122
70
  var import_change_case = require("change-case");
@@ -136,97 +84,7 @@ function getHookPrefix(canisterName) {
136
84
  return toPascalCase(canisterName);
137
85
  }
138
86
 
139
- // src/generators/reactor.ts
140
- function getReactorClassImportSource(reactorClass, runtimeTarget) {
141
- switch (reactorClass) {
142
- case "Reactor":
143
- case "DisplayReactor":
144
- return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react";
145
- case "CandidReactor":
146
- case "CandidDisplayReactor":
147
- case "MetadataDisplayReactor":
148
- return "@ic-reactor/candid";
149
- default:
150
- throw new Error(
151
- `Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
152
- );
153
- }
154
- }
155
- function generateReactorFile(options) {
156
- const {
157
- canisterName,
158
- didFile,
159
- clientManagerPath = "../../clients",
160
- canisterId,
161
- runtimeTarget = "react",
162
- reactorClass = "DisplayReactor"
163
- } = options;
164
- const pascalName = toPascalCase(canisterName);
165
- const reactorName = getReactorName(canisterName);
166
- const serviceName = getServiceTypeName(canisterName);
167
- const baseName = import_node_path2.default.basename(didFile, ".did");
168
- const declarationsPath = `./declarations/${baseName}`;
169
- const reactorImportSource = getReactorClassImportSource(
170
- reactorClass,
171
- runtimeTarget
172
- );
173
- const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
174
- ` : "";
175
- const hookExports = runtimeTarget === "react" ? `
176
-
177
- export const {
178
- useActorQuery: use${pascalName}Query,
179
- useActorSuspenseQuery: use${pascalName}SuspenseQuery,
180
- useActorInfiniteQuery: use${pascalName}InfiniteQuery,
181
- useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
182
- useActorMutation: use${pascalName}Mutation,
183
- useActorMethod: use${pascalName}Method,
184
- } = createActorHooks(${reactorName})
185
- ` : "";
186
- return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
187
- import { clientManager } from ${JSON.stringify(clientManagerPath)}
188
- import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
189
-
190
- export type ${serviceName} = _SERVICE
191
-
192
- /**
193
- * ${pascalName} Reactor
194
- *
195
- * Auto-generated by @ic-reactor/codegen \u2014 do not edit.
196
- * This file is overwritten whenever generation runs.
197
- *
198
- * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
199
- * factory modules). Avoid editing this managed file directly.
200
- */
201
- export const ${reactorName} = new ${reactorClass}<${serviceName}>({
202
- clientManager,
203
- idlFactory,
204
- ${canisterIdLine} name: ${JSON.stringify(canisterName)},
205
- })${hookExports || "\n"}`;
206
- }
207
- function generateReactorEntryFile() {
208
- return `/**
209
- * Canister entrypoint.
210
- *
211
- * Created once by @ic-reactor/codegen and safe to customize.
212
- * Keep the re-export below if you want generated exports and types to stay in sync.
213
- *
214
- * Recommended customization points:
215
- * - define reusable query/mutation factories
216
- * - add app-specific hooks and cache invalidation wiring
217
- * - compose generated APIs into route loaders/actions
218
- *
219
- * Do not edit \`index.generated.ts\`; it is regenerated on each codegen run.
220
- * AI guide: https://ic-reactor.b3pay.net/llms-full.txt
221
- * Skill install: npx skills add B3Pay/ic-reactor-skills --full-depth --skill ic-reactor-hooks
222
- */
223
- export * from "./index.generated"
224
- `;
225
- }
226
-
227
87
  // src/validate.ts
228
- var import_node_fs2 = __toESM(require("fs"), 1);
229
- var import_node_path3 = __toESM(require("path"), 1);
230
88
  var CodegenConfigError = class extends Error {
231
89
  name = "CodegenConfigError";
232
90
  constructor(message) {
@@ -299,6 +157,24 @@ function assertSafeModuleSpecifier(label, specifier) {
299
157
  );
300
158
  }
301
159
  }
160
+ function resolveDeclarationsBaseName(didFile) {
161
+ if (typeof didFile !== "string" || didFile.length === 0) {
162
+ throw new CodegenConfigError(
163
+ `Invalid didFile: expected a non-empty string, received ${didFile === void 0 ? "undefined" : JSON.stringify(didFile)}.`
164
+ );
165
+ }
166
+ const baseName = import_node_path.default.basename(didFile, ".did");
167
+ if (baseName === "" || baseName === "." || baseName === "..") {
168
+ throw new CodegenConfigError(
169
+ `Invalid didFile ${JSON.stringify(didFile)}: its file name ${JSON.stringify(baseName)} refers to a directory, not a Candid file. Generated declarations are named after the .did file, so this would emit an import of a directory.`
170
+ );
171
+ }
172
+ assertSafeModuleSpecifier(
173
+ `declarations import derived from didFile ${JSON.stringify(didFile)}`,
174
+ `./declarations/${baseName}`
175
+ );
176
+ return baseName;
177
+ }
302
178
  var REACTOR_CLASS_NAMES = [
303
179
  "Reactor",
304
180
  "DisplayReactor",
@@ -315,28 +191,28 @@ function assertOneOf(label, value, allowed) {
315
191
  }
316
192
  }
317
193
  function realpathAllowingMissing(target) {
318
- let current = import_node_path3.default.resolve(target);
194
+ let current = import_node_path.default.resolve(target);
319
195
  const missing = [];
320
196
  for (; ; ) {
321
197
  try {
322
- return import_node_path3.default.join(import_node_fs2.default.realpathSync(current), ...missing);
198
+ return import_node_path.default.join(import_node_fs.default.realpathSync(current), ...missing);
323
199
  } catch {
324
- const parent = import_node_path3.default.dirname(current);
325
- if (parent === current) return import_node_path3.default.resolve(target);
326
- missing.unshift(import_node_path3.default.basename(current));
200
+ const parent = import_node_path.default.dirname(current);
201
+ if (parent === current) return import_node_path.default.resolve(target);
202
+ missing.unshift(import_node_path.default.basename(current));
327
203
  current = parent;
328
204
  }
329
205
  }
330
206
  }
331
207
  function assertContainedPath(label, resolved, projectRoot, original = resolved) {
332
- const relative = import_node_path3.default.relative(
208
+ const relative = import_node_path.default.relative(
333
209
  realpathAllowingMissing(projectRoot),
334
210
  realpathAllowingMissing(resolved)
335
211
  );
336
212
  const [firstSegment] = relative.split(/[\\/]/);
337
- if (firstSegment === ".." || import_node_path3.default.isAbsolute(relative)) {
213
+ if (firstSegment === ".." || import_node_path.default.isAbsolute(relative)) {
338
214
  throw new CodegenConfigError(
339
- `Invalid ${label} ${JSON.stringify(original)}: resolves to ${JSON.stringify(resolved)}, which is outside the project root ${JSON.stringify(import_node_path3.default.resolve(projectRoot))}. Generated output must stay inside the project \u2014 generated directories are deleted and rewritten on every run.`
215
+ `Invalid ${label} ${JSON.stringify(original)}: resolves to ${JSON.stringify(resolved)}, which is outside the project root ${JSON.stringify(import_node_path.default.resolve(projectRoot))}. Generated output must stay inside the project \u2014 generated directories are deleted and rewritten on every run.`
340
216
  );
341
217
  }
342
218
  }
@@ -346,7 +222,7 @@ function resolveContainedOutDir(label, outDir, projectRoot) {
346
222
  `Invalid ${label}: expected a non-empty string, received ${outDir === void 0 ? "undefined" : JSON.stringify(outDir)}.`
347
223
  );
348
224
  }
349
- const resolved = import_node_path3.default.isAbsolute(outDir) ? import_node_path3.default.resolve(outDir) : import_node_path3.default.resolve(projectRoot, outDir);
225
+ const resolved = import_node_path.default.isAbsolute(outDir) ? import_node_path.default.resolve(outDir) : import_node_path.default.resolve(projectRoot, outDir);
350
226
  assertContainedPath(label, resolved, projectRoot, outDir);
351
227
  return resolved;
352
228
  }
@@ -368,7 +244,7 @@ function assertSafeCanisterConfig(options) {
368
244
  `outDir for canister ${JSON.stringify(name)}`,
369
245
  canisterOutDir,
370
246
  projectRoot
371
- ) : import_node_path3.default.join(
247
+ ) : import_node_path.default.join(
372
248
  resolveContainedOutDir("outDir", globalOutDir, projectRoot),
373
249
  name
374
250
  );
@@ -380,6 +256,213 @@ function assertSafeCanisterConfig(options) {
380
256
  return { name, outDir, clientManagerPath };
381
257
  }
382
258
 
259
+ // src/generators/declarations.ts
260
+ function replaceDirectory(from, to) {
261
+ if (!import_node_fs2.default.existsSync(to)) {
262
+ import_node_fs2.default.renameSync(from, to);
263
+ return;
264
+ }
265
+ const displaced = import_node_fs2.default.mkdtempSync(
266
+ import_node_path2.default.join(import_node_path2.default.dirname(to), `.${import_node_path2.default.basename(to)}.old-`)
267
+ );
268
+ import_node_fs2.default.rmdirSync(displaced);
269
+ import_node_fs2.default.renameSync(to, displaced);
270
+ try {
271
+ import_node_fs2.default.renameSync(from, to);
272
+ } catch (error) {
273
+ import_node_fs2.default.renameSync(displaced, to);
274
+ throw error;
275
+ }
276
+ import_node_fs2.default.rmSync(displaced, { recursive: true, force: true });
277
+ }
278
+ var HAS_IDL_FACTORY = /\bexport\s+const\s+idlFactory\b/;
279
+ var OWNER_FILE = ".ic-reactor-owner";
280
+ async function generateDeclarations(options) {
281
+ const { didFile, outDir, canisterName } = options;
282
+ const declarationsDir = import_node_path2.default.join(outDir, "declarations");
283
+ let baseName;
284
+ try {
285
+ baseName = resolveDeclarationsBaseName(didFile);
286
+ } catch (error) {
287
+ if (error instanceof CodegenConfigError) {
288
+ return {
289
+ success: false,
290
+ declarationsDir: "",
291
+ files: [],
292
+ error: `[${canisterName}] ${error.message}`
293
+ };
294
+ }
295
+ throw error;
296
+ }
297
+ let didStat;
298
+ try {
299
+ didStat = import_node_fs2.default.statSync(didFile);
300
+ } catch {
301
+ return {
302
+ success: false,
303
+ declarationsDir: "",
304
+ files: [],
305
+ error: `DID file not found: ${didFile}`
306
+ };
307
+ }
308
+ if (!didStat.isFile()) {
309
+ return {
310
+ success: false,
311
+ declarationsDir: "",
312
+ files: [],
313
+ error: `[${canisterName}] DID path is not a regular file: ${didFile}`
314
+ };
315
+ }
316
+ let staging;
317
+ try {
318
+ const didContent = import_node_fs2.default.readFileSync(didFile, "utf-8");
319
+ const jsContent = (0, import_parser.didToJs)(didContent);
320
+ const tsContent = (0, import_parser.didToTs)(didContent);
321
+ if (!HAS_IDL_FACTORY.test(jsContent)) {
322
+ return {
323
+ success: false,
324
+ declarationsDir,
325
+ files: [],
326
+ error: `[${canisterName}] ${didFile} produces no idlFactory, so there is no service to generate against. Point this canister at the .did file that declares its service.`
327
+ };
328
+ }
329
+ import_node_fs2.default.mkdirSync(outDir, { recursive: true });
330
+ import_node_fs2.default.writeFileSync(import_node_path2.default.join(outDir, OWNER_FILE), `${canisterName}
331
+ `);
332
+ staging = import_node_fs2.default.mkdtempSync(import_node_path2.default.join(outDir, ".declarations.tmp-"));
333
+ const jsPath = import_node_path2.default.join(declarationsDir, `${baseName}.js`);
334
+ const dtsPath = import_node_path2.default.join(declarationsDir, `${baseName}.d.ts`);
335
+ const didCopyPath = import_node_path2.default.join(declarationsDir, `${baseName}.did`);
336
+ import_node_fs2.default.writeFileSync(import_node_path2.default.join(staging, `${baseName}.js`), jsContent);
337
+ import_node_fs2.default.writeFileSync(import_node_path2.default.join(staging, `${baseName}.d.ts`), tsContent);
338
+ import_node_fs2.default.writeFileSync(import_node_path2.default.join(staging, `${baseName}.did`), didContent);
339
+ replaceDirectory(staging, declarationsDir);
340
+ staging = void 0;
341
+ return {
342
+ success: true,
343
+ declarationsDir,
344
+ files: [
345
+ { success: true, filePath: jsPath },
346
+ { success: true, filePath: dtsPath },
347
+ { success: true, filePath: didCopyPath }
348
+ ]
349
+ };
350
+ } catch (error) {
351
+ const message = error instanceof Error ? error.message : String(error);
352
+ return {
353
+ success: false,
354
+ declarationsDir,
355
+ files: [],
356
+ error: `[${canisterName}] Failed to generate declarations: ${message}`
357
+ };
358
+ } finally {
359
+ if (staging) import_node_fs2.default.rmSync(staging, { recursive: true, force: true });
360
+ }
361
+ }
362
+ function declarationsExist(outDir, canisterName, didFile) {
363
+ const declarationsDir = import_node_path2.default.join(outDir, "declarations");
364
+ if (didFile !== void 0) {
365
+ const baseName = import_node_path2.default.basename(didFile, ".did");
366
+ return import_node_fs2.default.existsSync(import_node_path2.default.join(declarationsDir, `${baseName}.d.ts`));
367
+ }
368
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(declarationsDir, `${canisterName}.d.ts`))) {
369
+ return true;
370
+ }
371
+ try {
372
+ return import_node_fs2.default.readdirSync(declarationsDir).some((entry) => entry.endsWith(".d.ts"));
373
+ } catch {
374
+ return false;
375
+ }
376
+ }
377
+
378
+ // src/generators/reactor.ts
379
+ function getReactorClassImportSource(reactorClass, runtimeTarget) {
380
+ switch (reactorClass) {
381
+ case "Reactor":
382
+ case "DisplayReactor":
383
+ return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react";
384
+ case "CandidReactor":
385
+ case "CandidDisplayReactor":
386
+ case "MetadataDisplayReactor":
387
+ return "@ic-reactor/candid";
388
+ default:
389
+ throw new Error(
390
+ `Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
391
+ );
392
+ }
393
+ }
394
+ function generateReactorFile(options) {
395
+ const {
396
+ canisterName,
397
+ didFile,
398
+ clientManagerPath = "../../clients",
399
+ canisterId,
400
+ runtimeTarget = "react",
401
+ reactorClass = "DisplayReactor"
402
+ } = options;
403
+ const pascalName = toPascalCase(canisterName);
404
+ const reactorName = getReactorName(canisterName);
405
+ const serviceName = getServiceTypeName(canisterName);
406
+ const baseName = resolveDeclarationsBaseName(didFile);
407
+ const declarationsPath = `./declarations/${baseName}`;
408
+ const reactorImportSource = getReactorClassImportSource(
409
+ reactorClass,
410
+ runtimeTarget
411
+ );
412
+ const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
413
+ ` : "";
414
+ const hookExports = runtimeTarget === "react" ? `
415
+
416
+ export const {
417
+ useActorQuery: use${pascalName}Query,
418
+ useActorSuspenseQuery: use${pascalName}SuspenseQuery,
419
+ useActorInfiniteQuery: use${pascalName}InfiniteQuery,
420
+ useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
421
+ useActorMutation: use${pascalName}Mutation,
422
+ useActorMethod: use${pascalName}Method,
423
+ } = createActorHooks(${reactorName})
424
+ ` : "";
425
+ return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
426
+ import { clientManager } from ${JSON.stringify(clientManagerPath)}
427
+ import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
428
+
429
+ export type ${serviceName} = _SERVICE
430
+
431
+ /**
432
+ * ${pascalName} Reactor
433
+ *
434
+ * Auto-generated by @ic-reactor/codegen \u2014 do not edit.
435
+ * This file is overwritten whenever generation runs.
436
+ *
437
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
438
+ * factory modules). Avoid editing this managed file directly.
439
+ */
440
+ export const ${reactorName} = new ${reactorClass}<${serviceName}>({
441
+ clientManager,
442
+ idlFactory,
443
+ ${canisterIdLine} name: ${JSON.stringify(canisterName)},
444
+ })${hookExports || "\n"}`;
445
+ }
446
+ function generateReactorEntryFile() {
447
+ return `/**
448
+ * Canister entrypoint.
449
+ *
450
+ * Created once by @ic-reactor/codegen and safe to customize.
451
+ * Keep the re-export below if you want generated exports and types to stay in sync.
452
+ *
453
+ * Recommended customization points:
454
+ * - define reusable query/mutation factories
455
+ * - add app-specific hooks and cache invalidation wiring
456
+ * - compose generated APIs into route loaders/actions
457
+ *
458
+ * Do not edit \`index.generated.ts\`; it is regenerated on each codegen run.
459
+ * AI guide: https://ic-reactor.b3pay.net/llms-full.txt
460
+ * Skill install: npx skills add B3Pay/ic-reactor-skills --full-depth --skill ic-reactor-hooks
461
+ */
462
+ export * from "./index.generated"
463
+ `;
464
+ }
465
+
383
466
  // src/pipeline.ts
384
467
  function resolveReactorClass(canisterConfig) {
385
468
  return canisterConfig.mode ?? "DisplayReactor";
@@ -390,8 +473,48 @@ function resolveRuntimeTarget(canisterConfig, globalConfig) {
390
473
  function normalizeFileContent(content) {
391
474
  return content.replace(/\r\n/g, "\n").trim();
392
475
  }
393
- function isLegacyGeneratedIndexFile(content) {
394
- return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(");
476
+ var GENERATED_MARKER = "Auto-generated by @ic-reactor/codegen";
477
+ var GENERATED_CANISTER_NAME = /^ {2}name: "([A-Za-z0-9_.-]+)",$/m;
478
+ var EXPORT_STATEMENT = /^export\b.*$/gm;
479
+ function isLegacyGeneratedIndexFile(content, canisterName) {
480
+ if (!content.includes(GENERATED_MARKER)) return false;
481
+ const reactorName = getReactorName(canisterName);
482
+ const serviceName = getServiceTypeName(canisterName);
483
+ if (!content.includes(`export const ${reactorName} = new `)) return false;
484
+ if (!content.includes(`= createActorHooks(${reactorName})`)) return false;
485
+ const allowed = [
486
+ `export type ${serviceName} = _SERVICE`,
487
+ `export const ${reactorName} = new `,
488
+ // Opens the destructured hook block; its members are not export statements.
489
+ "export const {"
490
+ ];
491
+ const statements = normalizeFileContent(content).match(EXPORT_STATEMENT) ?? [];
492
+ return statements.every(
493
+ (statement) => allowed.some((prefix) => statement.startsWith(prefix))
494
+ );
495
+ }
496
+ function backUpFile(filePath) {
497
+ let backupPath = `${filePath}.bak`;
498
+ for (let n = 2; import_node_fs3.default.existsSync(backupPath); n += 1) {
499
+ backupPath = `${filePath}.bak.${n}`;
500
+ }
501
+ import_node_fs3.default.copyFileSync(filePath, backupPath);
502
+ return backupPath;
503
+ }
504
+ function findOutDirOwner(outDir) {
505
+ try {
506
+ const owner = import_node_fs3.default.readFileSync(import_node_path3.default.join(outDir, OWNER_FILE), "utf-8").trim();
507
+ if (owner) return owner;
508
+ } catch {
509
+ }
510
+ let content;
511
+ try {
512
+ content = import_node_fs3.default.readFileSync(import_node_path3.default.join(outDir, "index.generated.ts"), "utf-8");
513
+ } catch {
514
+ return void 0;
515
+ }
516
+ if (!content.includes(GENERATED_MARKER)) return void 0;
517
+ return GENERATED_CANISTER_NAME.exec(content)?.[1];
395
518
  }
396
519
  function isManagedEntryWrapper(content, expectedEntryContent) {
397
520
  return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
@@ -427,7 +550,7 @@ async function runCanisterPipeline(options) {
427
550
  }
428
551
  throw err;
429
552
  }
430
- const resolvedDidFile = import_node_path4.default.isAbsolute(didFile) ? didFile : import_node_path4.default.resolve(projectRoot, didFile);
553
+ const resolvedDidFile = import_node_path3.default.isAbsolute(didFile) ? didFile : import_node_path3.default.resolve(projectRoot, didFile);
431
554
  if (!import_node_fs3.default.existsSync(resolvedDidFile)) {
432
555
  return {
433
556
  canisterName: name,
@@ -436,7 +559,29 @@ async function runCanisterPipeline(options) {
436
559
  error: `DID file not found: ${resolvedDidFile}`
437
560
  };
438
561
  }
562
+ try {
563
+ resolveDeclarationsBaseName(resolvedDidFile);
564
+ } catch (err) {
565
+ if (err instanceof CodegenConfigError) {
566
+ return {
567
+ canisterName: name,
568
+ success: false,
569
+ files,
570
+ error: `[${name}] ${err.message}`
571
+ };
572
+ }
573
+ throw err;
574
+ }
439
575
  const canisterOutDir = validated.outDir;
576
+ const owner = findOutDirOwner(canisterOutDir);
577
+ if (owner !== void 0 && owner !== name) {
578
+ return {
579
+ canisterName: name,
580
+ success: false,
581
+ files,
582
+ error: `[${name}] Output directory ${canisterOutDir} was generated for canister "${owner}". Two canisters cannot share an outDir \u2014 each run replaces the declarations directory and index.generated.ts, so they would overwrite each other. Give each canister its own "outDir". If you renamed "${owner}" to "${name}", delete that directory and regenerate.`
583
+ };
584
+ }
440
585
  const resolvedClientManagerPath = validated.clientManagerPath;
441
586
  try {
442
587
  const declResult = await generateDeclarations({
@@ -468,8 +613,8 @@ async function runCanisterPipeline(options) {
468
613
  files
469
614
  };
470
615
  }
471
- const reactorPath = import_node_path4.default.join(canisterOutDir, "index.generated.ts");
472
- const entryPath = import_node_path4.default.join(canisterOutDir, "index.ts");
616
+ const reactorPath = import_node_path3.default.join(canisterOutDir, "index.generated.ts");
617
+ const entryPath = import_node_path3.default.join(canisterOutDir, "index.ts");
473
618
  const reactorClass = resolveReactorClass(canisterConfig);
474
619
  const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
475
620
  try {
@@ -490,9 +635,14 @@ async function runCanisterPipeline(options) {
490
635
  files.push({ success: true, filePath: entryPath });
491
636
  } else {
492
637
  const existingEntryContent = import_node_fs3.default.readFileSync(entryPath, "utf-8");
493
- if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
638
+ if (isManagedEntryWrapper(existingEntryContent, entryContent)) {
494
639
  import_node_fs3.default.writeFileSync(entryPath, entryContent);
495
640
  files.push({ success: true, filePath: entryPath });
641
+ } else if (isLegacyGeneratedIndexFile(existingEntryContent, name)) {
642
+ const backupPath = backUpFile(entryPath);
643
+ import_node_fs3.default.writeFileSync(entryPath, entryContent);
644
+ files.push({ success: true, filePath: backupPath });
645
+ files.push({ success: true, filePath: entryPath });
496
646
  } else {
497
647
  files.push({ success: true, filePath: entryPath, skipped: true });
498
648
  }
@@ -517,40 +667,6 @@ async function runCanisterPipeline(options) {
517
667
  };
518
668
  }
519
669
 
520
- // src/parser.ts
521
- var import_parser2 = require("@ic-reactor/parser");
522
- var import_node_fs4 = __toESM(require("fs"), 1);
523
- function extractMethods(didContent) {
524
- try {
525
- const jsContent = (0, import_parser2.didToJs)(didContent);
526
- const methods = [];
527
- const serviceMatch = /IDL\.Service\(\{([\s\S]*?)\}\)/.exec(jsContent);
528
- if (!serviceMatch) return methods;
529
- const serviceBody = serviceMatch[1];
530
- const methodRegex = /['"]([\w]+)['"]\s*:\s*IDL\.Func\(\s*\[(.*?)\],\s*\[.*?\],\s*\[(.*?)\]\)/g;
531
- let match;
532
- while ((match = methodRegex.exec(serviceBody)) !== null) {
533
- const [, name, args, annotations] = match;
534
- methods.push({
535
- name,
536
- type: annotations.includes("'query'") ? "query" : "mutation",
537
- hasArgs: args.trim().length > 0
538
- });
539
- }
540
- return methods;
541
- } catch (error) {
542
- const msg = error instanceof Error ? error.message : String(error);
543
- throw new Error(`Failed to parse Candid: ${msg}`);
544
- }
545
- }
546
- function parseDIDFile(didFilePath) {
547
- if (!import_node_fs4.default.existsSync(didFilePath)) {
548
- throw new Error(`DID file not found: ${didFilePath}`);
549
- }
550
- const content = import_node_fs4.default.readFileSync(didFilePath, "utf-8");
551
- return extractMethods(content);
552
- }
553
-
554
670
  // src/generators/client.ts
555
671
  function generateClientFile(options = {}) {
556
672
  const { queryClientPath } = options;
@@ -583,15 +699,14 @@ export const clientManager = new ClientManager({
583
699
  assertSafeCanisterName,
584
700
  assertSafeModuleSpecifier,
585
701
  declarationsExist,
586
- extractMethods,
587
702
  generateClientFile,
588
703
  generateDeclarations,
589
704
  generateReactorEntryFile,
590
705
  generateReactorFile,
591
706
  getReactorName,
592
707
  getServiceTypeName,
593
- parseDIDFile,
594
708
  resolveContainedOutDir,
709
+ resolveDeclarationsBaseName,
595
710
  runCanisterPipeline,
596
711
  toPascalCase
597
712
  });