@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/dist/index.js CHANGED
@@ -1,66 +1,15 @@
1
1
  // src/pipeline.ts
2
2
  import fs3 from "fs";
3
- import path4 from "path";
3
+ import path3 from "path";
4
4
 
5
5
  // src/generators/declarations.ts
6
6
  import { didToJs, didToTs } from "@ic-reactor/parser";
7
- import path from "path";
8
- import fs from "fs";
9
- async function generateDeclarations(options) {
10
- const { didFile, outDir, canisterName } = options;
11
- if (!fs.existsSync(didFile)) {
12
- return {
13
- success: false,
14
- declarationsDir: "",
15
- files: [],
16
- error: `DID file not found: ${didFile}`
17
- };
18
- }
19
- const declarationsDir = path.join(outDir, "declarations");
20
- const baseName = path.basename(didFile, ".did");
21
- try {
22
- const didContent = fs.readFileSync(didFile, "utf-8");
23
- if (!fs.existsSync(outDir)) {
24
- fs.mkdirSync(outDir, { recursive: true });
25
- }
26
- if (fs.existsSync(declarationsDir)) {
27
- fs.rmSync(declarationsDir, { recursive: true, force: true });
28
- }
29
- fs.mkdirSync(declarationsDir, { recursive: true });
30
- const jsContent = didToJs(didContent);
31
- const tsContent = didToTs(didContent);
32
- const jsPath = path.join(declarationsDir, `${baseName}.js`);
33
- const dtsPath = path.join(declarationsDir, `${baseName}.d.ts`);
34
- const didCopyPath = path.join(declarationsDir, `${baseName}.did`);
35
- fs.writeFileSync(jsPath, jsContent);
36
- fs.writeFileSync(dtsPath, tsContent);
37
- fs.writeFileSync(didCopyPath, didContent);
38
- return {
39
- success: true,
40
- declarationsDir,
41
- files: [
42
- { success: true, filePath: jsPath },
43
- { success: true, filePath: dtsPath },
44
- { success: true, filePath: didCopyPath }
45
- ]
46
- };
47
- } catch (error) {
48
- const message = error instanceof Error ? error.message : String(error);
49
- return {
50
- success: false,
51
- declarationsDir,
52
- files: [],
53
- error: `[${canisterName}] Failed to generate declarations: ${message}`
54
- };
55
- }
56
- }
57
- function declarationsExist(outDir, canisterName) {
58
- const dtsPath = path.join(outDir, "declarations", `${canisterName}.d.ts`);
59
- return fs.existsSync(dtsPath);
60
- }
61
-
62
- // src/generators/reactor.ts
63
7
  import path2 from "path";
8
+ import fs2 from "fs";
9
+
10
+ // src/validate.ts
11
+ import fs from "fs";
12
+ import path from "path";
64
13
 
65
14
  // src/naming.ts
66
15
  import { camelCase, pascalCase } from "change-case";
@@ -80,97 +29,7 @@ function getHookPrefix(canisterName) {
80
29
  return toPascalCase(canisterName);
81
30
  }
82
31
 
83
- // src/generators/reactor.ts
84
- function getReactorClassImportSource(reactorClass, runtimeTarget) {
85
- switch (reactorClass) {
86
- case "Reactor":
87
- case "DisplayReactor":
88
- return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react";
89
- case "CandidReactor":
90
- case "CandidDisplayReactor":
91
- case "MetadataDisplayReactor":
92
- return "@ic-reactor/candid";
93
- default:
94
- throw new Error(
95
- `Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
96
- );
97
- }
98
- }
99
- function generateReactorFile(options) {
100
- const {
101
- canisterName,
102
- didFile,
103
- clientManagerPath = "../../clients",
104
- canisterId,
105
- runtimeTarget = "react",
106
- reactorClass = "DisplayReactor"
107
- } = options;
108
- const pascalName = toPascalCase(canisterName);
109
- const reactorName = getReactorName(canisterName);
110
- const serviceName = getServiceTypeName(canisterName);
111
- const baseName = path2.basename(didFile, ".did");
112
- const declarationsPath = `./declarations/${baseName}`;
113
- const reactorImportSource = getReactorClassImportSource(
114
- reactorClass,
115
- runtimeTarget
116
- );
117
- const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
118
- ` : "";
119
- const hookExports = runtimeTarget === "react" ? `
120
-
121
- export const {
122
- useActorQuery: use${pascalName}Query,
123
- useActorSuspenseQuery: use${pascalName}SuspenseQuery,
124
- useActorInfiniteQuery: use${pascalName}InfiniteQuery,
125
- useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
126
- useActorMutation: use${pascalName}Mutation,
127
- useActorMethod: use${pascalName}Method,
128
- } = createActorHooks(${reactorName})
129
- ` : "";
130
- return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
131
- import { clientManager } from ${JSON.stringify(clientManagerPath)}
132
- import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
133
-
134
- export type ${serviceName} = _SERVICE
135
-
136
- /**
137
- * ${pascalName} Reactor
138
- *
139
- * Auto-generated by @ic-reactor/codegen \u2014 do not edit.
140
- * This file is overwritten whenever generation runs.
141
- *
142
- * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
143
- * factory modules). Avoid editing this managed file directly.
144
- */
145
- export const ${reactorName} = new ${reactorClass}<${serviceName}>({
146
- clientManager,
147
- idlFactory,
148
- ${canisterIdLine} name: ${JSON.stringify(canisterName)},
149
- })${hookExports || "\n"}`;
150
- }
151
- function generateReactorEntryFile() {
152
- return `/**
153
- * Canister entrypoint.
154
- *
155
- * Created once by @ic-reactor/codegen and safe to customize.
156
- * Keep the re-export below if you want generated exports and types to stay in sync.
157
- *
158
- * Recommended customization points:
159
- * - define reusable query/mutation factories
160
- * - add app-specific hooks and cache invalidation wiring
161
- * - compose generated APIs into route loaders/actions
162
- *
163
- * Do not edit \`index.generated.ts\`; it is regenerated on each codegen run.
164
- * AI guide: https://ic-reactor.b3pay.net/llms-full.txt
165
- * Skill install: npx skills add B3Pay/ic-reactor-skills --full-depth --skill ic-reactor-hooks
166
- */
167
- export * from "./index.generated"
168
- `;
169
- }
170
-
171
32
  // src/validate.ts
172
- import fs2 from "fs";
173
- import path3 from "path";
174
33
  var CodegenConfigError = class extends Error {
175
34
  name = "CodegenConfigError";
176
35
  constructor(message) {
@@ -243,6 +102,24 @@ function assertSafeModuleSpecifier(label, specifier) {
243
102
  );
244
103
  }
245
104
  }
105
+ function resolveDeclarationsBaseName(didFile) {
106
+ if (typeof didFile !== "string" || didFile.length === 0) {
107
+ throw new CodegenConfigError(
108
+ `Invalid didFile: expected a non-empty string, received ${didFile === void 0 ? "undefined" : JSON.stringify(didFile)}.`
109
+ );
110
+ }
111
+ const baseName = path.basename(didFile, ".did");
112
+ if (baseName === "" || baseName === "." || baseName === "..") {
113
+ throw new CodegenConfigError(
114
+ `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.`
115
+ );
116
+ }
117
+ assertSafeModuleSpecifier(
118
+ `declarations import derived from didFile ${JSON.stringify(didFile)}`,
119
+ `./declarations/${baseName}`
120
+ );
121
+ return baseName;
122
+ }
246
123
  var REACTOR_CLASS_NAMES = [
247
124
  "Reactor",
248
125
  "DisplayReactor",
@@ -259,28 +136,28 @@ function assertOneOf(label, value, allowed) {
259
136
  }
260
137
  }
261
138
  function realpathAllowingMissing(target) {
262
- let current = path3.resolve(target);
139
+ let current = path.resolve(target);
263
140
  const missing = [];
264
141
  for (; ; ) {
265
142
  try {
266
- return path3.join(fs2.realpathSync(current), ...missing);
143
+ return path.join(fs.realpathSync(current), ...missing);
267
144
  } catch {
268
- const parent = path3.dirname(current);
269
- if (parent === current) return path3.resolve(target);
270
- missing.unshift(path3.basename(current));
145
+ const parent = path.dirname(current);
146
+ if (parent === current) return path.resolve(target);
147
+ missing.unshift(path.basename(current));
271
148
  current = parent;
272
149
  }
273
150
  }
274
151
  }
275
152
  function assertContainedPath(label, resolved, projectRoot, original = resolved) {
276
- const relative = path3.relative(
153
+ const relative = path.relative(
277
154
  realpathAllowingMissing(projectRoot),
278
155
  realpathAllowingMissing(resolved)
279
156
  );
280
157
  const [firstSegment] = relative.split(/[\\/]/);
281
- if (firstSegment === ".." || path3.isAbsolute(relative)) {
158
+ if (firstSegment === ".." || path.isAbsolute(relative)) {
282
159
  throw new CodegenConfigError(
283
- `Invalid ${label} ${JSON.stringify(original)}: resolves to ${JSON.stringify(resolved)}, which is outside the project root ${JSON.stringify(path3.resolve(projectRoot))}. Generated output must stay inside the project \u2014 generated directories are deleted and rewritten on every run.`
160
+ `Invalid ${label} ${JSON.stringify(original)}: resolves to ${JSON.stringify(resolved)}, which is outside the project root ${JSON.stringify(path.resolve(projectRoot))}. Generated output must stay inside the project \u2014 generated directories are deleted and rewritten on every run.`
284
161
  );
285
162
  }
286
163
  }
@@ -290,7 +167,7 @@ function resolveContainedOutDir(label, outDir, projectRoot) {
290
167
  `Invalid ${label}: expected a non-empty string, received ${outDir === void 0 ? "undefined" : JSON.stringify(outDir)}.`
291
168
  );
292
169
  }
293
- const resolved = path3.isAbsolute(outDir) ? path3.resolve(outDir) : path3.resolve(projectRoot, outDir);
170
+ const resolved = path.isAbsolute(outDir) ? path.resolve(outDir) : path.resolve(projectRoot, outDir);
294
171
  assertContainedPath(label, resolved, projectRoot, outDir);
295
172
  return resolved;
296
173
  }
@@ -312,7 +189,7 @@ function assertSafeCanisterConfig(options) {
312
189
  `outDir for canister ${JSON.stringify(name)}`,
313
190
  canisterOutDir,
314
191
  projectRoot
315
- ) : path3.join(
192
+ ) : path.join(
316
193
  resolveContainedOutDir("outDir", globalOutDir, projectRoot),
317
194
  name
318
195
  );
@@ -324,6 +201,213 @@ function assertSafeCanisterConfig(options) {
324
201
  return { name, outDir, clientManagerPath };
325
202
  }
326
203
 
204
+ // src/generators/declarations.ts
205
+ function replaceDirectory(from, to) {
206
+ if (!fs2.existsSync(to)) {
207
+ fs2.renameSync(from, to);
208
+ return;
209
+ }
210
+ const displaced = fs2.mkdtempSync(
211
+ path2.join(path2.dirname(to), `.${path2.basename(to)}.old-`)
212
+ );
213
+ fs2.rmdirSync(displaced);
214
+ fs2.renameSync(to, displaced);
215
+ try {
216
+ fs2.renameSync(from, to);
217
+ } catch (error) {
218
+ fs2.renameSync(displaced, to);
219
+ throw error;
220
+ }
221
+ fs2.rmSync(displaced, { recursive: true, force: true });
222
+ }
223
+ var HAS_IDL_FACTORY = /\bexport\s+const\s+idlFactory\b/;
224
+ var OWNER_FILE = ".ic-reactor-owner";
225
+ async function generateDeclarations(options) {
226
+ const { didFile, outDir, canisterName } = options;
227
+ const declarationsDir = path2.join(outDir, "declarations");
228
+ let baseName;
229
+ try {
230
+ baseName = resolveDeclarationsBaseName(didFile);
231
+ } catch (error) {
232
+ if (error instanceof CodegenConfigError) {
233
+ return {
234
+ success: false,
235
+ declarationsDir: "",
236
+ files: [],
237
+ error: `[${canisterName}] ${error.message}`
238
+ };
239
+ }
240
+ throw error;
241
+ }
242
+ let didStat;
243
+ try {
244
+ didStat = fs2.statSync(didFile);
245
+ } catch {
246
+ return {
247
+ success: false,
248
+ declarationsDir: "",
249
+ files: [],
250
+ error: `DID file not found: ${didFile}`
251
+ };
252
+ }
253
+ if (!didStat.isFile()) {
254
+ return {
255
+ success: false,
256
+ declarationsDir: "",
257
+ files: [],
258
+ error: `[${canisterName}] DID path is not a regular file: ${didFile}`
259
+ };
260
+ }
261
+ let staging;
262
+ try {
263
+ const didContent = fs2.readFileSync(didFile, "utf-8");
264
+ const jsContent = didToJs(didContent);
265
+ const tsContent = didToTs(didContent);
266
+ if (!HAS_IDL_FACTORY.test(jsContent)) {
267
+ return {
268
+ success: false,
269
+ declarationsDir,
270
+ files: [],
271
+ 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.`
272
+ };
273
+ }
274
+ fs2.mkdirSync(outDir, { recursive: true });
275
+ fs2.writeFileSync(path2.join(outDir, OWNER_FILE), `${canisterName}
276
+ `);
277
+ staging = fs2.mkdtempSync(path2.join(outDir, ".declarations.tmp-"));
278
+ const jsPath = path2.join(declarationsDir, `${baseName}.js`);
279
+ const dtsPath = path2.join(declarationsDir, `${baseName}.d.ts`);
280
+ const didCopyPath = path2.join(declarationsDir, `${baseName}.did`);
281
+ fs2.writeFileSync(path2.join(staging, `${baseName}.js`), jsContent);
282
+ fs2.writeFileSync(path2.join(staging, `${baseName}.d.ts`), tsContent);
283
+ fs2.writeFileSync(path2.join(staging, `${baseName}.did`), didContent);
284
+ replaceDirectory(staging, declarationsDir);
285
+ staging = void 0;
286
+ return {
287
+ success: true,
288
+ declarationsDir,
289
+ files: [
290
+ { success: true, filePath: jsPath },
291
+ { success: true, filePath: dtsPath },
292
+ { success: true, filePath: didCopyPath }
293
+ ]
294
+ };
295
+ } catch (error) {
296
+ const message = error instanceof Error ? error.message : String(error);
297
+ return {
298
+ success: false,
299
+ declarationsDir,
300
+ files: [],
301
+ error: `[${canisterName}] Failed to generate declarations: ${message}`
302
+ };
303
+ } finally {
304
+ if (staging) fs2.rmSync(staging, { recursive: true, force: true });
305
+ }
306
+ }
307
+ function declarationsExist(outDir, canisterName, didFile) {
308
+ const declarationsDir = path2.join(outDir, "declarations");
309
+ if (didFile !== void 0) {
310
+ const baseName = path2.basename(didFile, ".did");
311
+ return fs2.existsSync(path2.join(declarationsDir, `${baseName}.d.ts`));
312
+ }
313
+ if (fs2.existsSync(path2.join(declarationsDir, `${canisterName}.d.ts`))) {
314
+ return true;
315
+ }
316
+ try {
317
+ return fs2.readdirSync(declarationsDir).some((entry) => entry.endsWith(".d.ts"));
318
+ } catch {
319
+ return false;
320
+ }
321
+ }
322
+
323
+ // src/generators/reactor.ts
324
+ function getReactorClassImportSource(reactorClass, runtimeTarget) {
325
+ switch (reactorClass) {
326
+ case "Reactor":
327
+ case "DisplayReactor":
328
+ return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react";
329
+ case "CandidReactor":
330
+ case "CandidDisplayReactor":
331
+ case "MetadataDisplayReactor":
332
+ return "@ic-reactor/candid";
333
+ default:
334
+ throw new Error(
335
+ `Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
336
+ );
337
+ }
338
+ }
339
+ function generateReactorFile(options) {
340
+ const {
341
+ canisterName,
342
+ didFile,
343
+ clientManagerPath = "../../clients",
344
+ canisterId,
345
+ runtimeTarget = "react",
346
+ reactorClass = "DisplayReactor"
347
+ } = options;
348
+ const pascalName = toPascalCase(canisterName);
349
+ const reactorName = getReactorName(canisterName);
350
+ const serviceName = getServiceTypeName(canisterName);
351
+ const baseName = resolveDeclarationsBaseName(didFile);
352
+ const declarationsPath = `./declarations/${baseName}`;
353
+ const reactorImportSource = getReactorClassImportSource(
354
+ reactorClass,
355
+ runtimeTarget
356
+ );
357
+ const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
358
+ ` : "";
359
+ const hookExports = runtimeTarget === "react" ? `
360
+
361
+ export const {
362
+ useActorQuery: use${pascalName}Query,
363
+ useActorSuspenseQuery: use${pascalName}SuspenseQuery,
364
+ useActorInfiniteQuery: use${pascalName}InfiniteQuery,
365
+ useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
366
+ useActorMutation: use${pascalName}Mutation,
367
+ useActorMethod: use${pascalName}Method,
368
+ } = createActorHooks(${reactorName})
369
+ ` : "";
370
+ return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
371
+ import { clientManager } from ${JSON.stringify(clientManagerPath)}
372
+ import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
373
+
374
+ export type ${serviceName} = _SERVICE
375
+
376
+ /**
377
+ * ${pascalName} Reactor
378
+ *
379
+ * Auto-generated by @ic-reactor/codegen \u2014 do not edit.
380
+ * This file is overwritten whenever generation runs.
381
+ *
382
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
383
+ * factory modules). Avoid editing this managed file directly.
384
+ */
385
+ export const ${reactorName} = new ${reactorClass}<${serviceName}>({
386
+ clientManager,
387
+ idlFactory,
388
+ ${canisterIdLine} name: ${JSON.stringify(canisterName)},
389
+ })${hookExports || "\n"}`;
390
+ }
391
+ function generateReactorEntryFile() {
392
+ return `/**
393
+ * Canister entrypoint.
394
+ *
395
+ * Created once by @ic-reactor/codegen and safe to customize.
396
+ * Keep the re-export below if you want generated exports and types to stay in sync.
397
+ *
398
+ * Recommended customization points:
399
+ * - define reusable query/mutation factories
400
+ * - add app-specific hooks and cache invalidation wiring
401
+ * - compose generated APIs into route loaders/actions
402
+ *
403
+ * Do not edit \`index.generated.ts\`; it is regenerated on each codegen run.
404
+ * AI guide: https://ic-reactor.b3pay.net/llms-full.txt
405
+ * Skill install: npx skills add B3Pay/ic-reactor-skills --full-depth --skill ic-reactor-hooks
406
+ */
407
+ export * from "./index.generated"
408
+ `;
409
+ }
410
+
327
411
  // src/pipeline.ts
328
412
  function resolveReactorClass(canisterConfig) {
329
413
  return canisterConfig.mode ?? "DisplayReactor";
@@ -334,8 +418,48 @@ function resolveRuntimeTarget(canisterConfig, globalConfig) {
334
418
  function normalizeFileContent(content) {
335
419
  return content.replace(/\r\n/g, "\n").trim();
336
420
  }
337
- function isLegacyGeneratedIndexFile(content) {
338
- return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(");
421
+ var GENERATED_MARKER = "Auto-generated by @ic-reactor/codegen";
422
+ var GENERATED_CANISTER_NAME = /^ {2}name: "([A-Za-z0-9_.-]+)",$/m;
423
+ var EXPORT_STATEMENT = /^export\b.*$/gm;
424
+ function isLegacyGeneratedIndexFile(content, canisterName) {
425
+ if (!content.includes(GENERATED_MARKER)) return false;
426
+ const reactorName = getReactorName(canisterName);
427
+ const serviceName = getServiceTypeName(canisterName);
428
+ if (!content.includes(`export const ${reactorName} = new `)) return false;
429
+ if (!content.includes(`= createActorHooks(${reactorName})`)) return false;
430
+ const allowed = [
431
+ `export type ${serviceName} = _SERVICE`,
432
+ `export const ${reactorName} = new `,
433
+ // Opens the destructured hook block; its members are not export statements.
434
+ "export const {"
435
+ ];
436
+ const statements = normalizeFileContent(content).match(EXPORT_STATEMENT) ?? [];
437
+ return statements.every(
438
+ (statement) => allowed.some((prefix) => statement.startsWith(prefix))
439
+ );
440
+ }
441
+ function backUpFile(filePath) {
442
+ let backupPath = `${filePath}.bak`;
443
+ for (let n = 2; fs3.existsSync(backupPath); n += 1) {
444
+ backupPath = `${filePath}.bak.${n}`;
445
+ }
446
+ fs3.copyFileSync(filePath, backupPath);
447
+ return backupPath;
448
+ }
449
+ function findOutDirOwner(outDir) {
450
+ try {
451
+ const owner = fs3.readFileSync(path3.join(outDir, OWNER_FILE), "utf-8").trim();
452
+ if (owner) return owner;
453
+ } catch {
454
+ }
455
+ let content;
456
+ try {
457
+ content = fs3.readFileSync(path3.join(outDir, "index.generated.ts"), "utf-8");
458
+ } catch {
459
+ return void 0;
460
+ }
461
+ if (!content.includes(GENERATED_MARKER)) return void 0;
462
+ return GENERATED_CANISTER_NAME.exec(content)?.[1];
339
463
  }
340
464
  function isManagedEntryWrapper(content, expectedEntryContent) {
341
465
  return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
@@ -371,7 +495,7 @@ async function runCanisterPipeline(options) {
371
495
  }
372
496
  throw err;
373
497
  }
374
- const resolvedDidFile = path4.isAbsolute(didFile) ? didFile : path4.resolve(projectRoot, didFile);
498
+ const resolvedDidFile = path3.isAbsolute(didFile) ? didFile : path3.resolve(projectRoot, didFile);
375
499
  if (!fs3.existsSync(resolvedDidFile)) {
376
500
  return {
377
501
  canisterName: name,
@@ -380,7 +504,29 @@ async function runCanisterPipeline(options) {
380
504
  error: `DID file not found: ${resolvedDidFile}`
381
505
  };
382
506
  }
507
+ try {
508
+ resolveDeclarationsBaseName(resolvedDidFile);
509
+ } catch (err) {
510
+ if (err instanceof CodegenConfigError) {
511
+ return {
512
+ canisterName: name,
513
+ success: false,
514
+ files,
515
+ error: `[${name}] ${err.message}`
516
+ };
517
+ }
518
+ throw err;
519
+ }
383
520
  const canisterOutDir = validated.outDir;
521
+ const owner = findOutDirOwner(canisterOutDir);
522
+ if (owner !== void 0 && owner !== name) {
523
+ return {
524
+ canisterName: name,
525
+ success: false,
526
+ files,
527
+ 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.`
528
+ };
529
+ }
384
530
  const resolvedClientManagerPath = validated.clientManagerPath;
385
531
  try {
386
532
  const declResult = await generateDeclarations({
@@ -412,8 +558,8 @@ async function runCanisterPipeline(options) {
412
558
  files
413
559
  };
414
560
  }
415
- const reactorPath = path4.join(canisterOutDir, "index.generated.ts");
416
- const entryPath = path4.join(canisterOutDir, "index.ts");
561
+ const reactorPath = path3.join(canisterOutDir, "index.generated.ts");
562
+ const entryPath = path3.join(canisterOutDir, "index.ts");
417
563
  const reactorClass = resolveReactorClass(canisterConfig);
418
564
  const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
419
565
  try {
@@ -434,9 +580,14 @@ async function runCanisterPipeline(options) {
434
580
  files.push({ success: true, filePath: entryPath });
435
581
  } else {
436
582
  const existingEntryContent = fs3.readFileSync(entryPath, "utf-8");
437
- if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
583
+ if (isManagedEntryWrapper(existingEntryContent, entryContent)) {
438
584
  fs3.writeFileSync(entryPath, entryContent);
439
585
  files.push({ success: true, filePath: entryPath });
586
+ } else if (isLegacyGeneratedIndexFile(existingEntryContent, name)) {
587
+ const backupPath = backUpFile(entryPath);
588
+ fs3.writeFileSync(entryPath, entryContent);
589
+ files.push({ success: true, filePath: backupPath });
590
+ files.push({ success: true, filePath: entryPath });
440
591
  } else {
441
592
  files.push({ success: true, filePath: entryPath, skipped: true });
442
593
  }
@@ -461,40 +612,6 @@ async function runCanisterPipeline(options) {
461
612
  };
462
613
  }
463
614
 
464
- // src/parser.ts
465
- import { didToJs as didToJs2 } from "@ic-reactor/parser";
466
- import fs4 from "fs";
467
- function extractMethods(didContent) {
468
- try {
469
- const jsContent = didToJs2(didContent);
470
- const methods = [];
471
- const serviceMatch = /IDL\.Service\(\{([\s\S]*?)\}\)/.exec(jsContent);
472
- if (!serviceMatch) return methods;
473
- const serviceBody = serviceMatch[1];
474
- const methodRegex = /['"]([\w]+)['"]\s*:\s*IDL\.Func\(\s*\[(.*?)\],\s*\[.*?\],\s*\[(.*?)\]\)/g;
475
- let match;
476
- while ((match = methodRegex.exec(serviceBody)) !== null) {
477
- const [, name, args, annotations] = match;
478
- methods.push({
479
- name,
480
- type: annotations.includes("'query'") ? "query" : "mutation",
481
- hasArgs: args.trim().length > 0
482
- });
483
- }
484
- return methods;
485
- } catch (error) {
486
- const msg = error instanceof Error ? error.message : String(error);
487
- throw new Error(`Failed to parse Candid: ${msg}`);
488
- }
489
- }
490
- function parseDIDFile(didFilePath) {
491
- if (!fs4.existsSync(didFilePath)) {
492
- throw new Error(`DID file not found: ${didFilePath}`);
493
- }
494
- const content = fs4.readFileSync(didFilePath, "utf-8");
495
- return extractMethods(content);
496
- }
497
-
498
615
  // src/generators/client.ts
499
616
  function generateClientFile(options = {}) {
500
617
  const { queryClientPath } = options;
@@ -526,15 +643,14 @@ export {
526
643
  assertSafeCanisterName,
527
644
  assertSafeModuleSpecifier,
528
645
  declarationsExist,
529
- extractMethods,
530
646
  generateClientFile,
531
647
  generateDeclarations,
532
648
  generateReactorEntryFile,
533
649
  generateReactorFile,
534
650
  getReactorName,
535
651
  getServiceTypeName,
536
- parseDIDFile,
537
652
  resolveContainedOutDir,
653
+ resolveDeclarationsBaseName,
538
654
  runCanisterPipeline,
539
655
  toPascalCase
540
656
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/codegen",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "Shared code generation utilities for IC Reactor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,6 +22,10 @@
22
22
  "files": [
23
23
  "dist",
24
24
  "src",
25
+ "!src/**/*.test.*",
26
+ "!src/**/*.spec.*",
27
+ "!src/**/__snapshots__/**",
28
+ "!**/*.tsbuildinfo",
25
29
  "README.md",
26
30
  "llms.txt"
27
31
  ],
@@ -43,10 +47,10 @@
43
47
  "homepage": "https://ic-reactor.b3pay.net/v3/packages/codegen",
44
48
  "dependencies": {
45
49
  "change-case": "^5.4.4",
46
- "@ic-reactor/parser": "0.4.7"
50
+ "@ic-reactor/parser": "0.4.8"
47
51
  },
48
52
  "devDependencies": {
49
- "@types/node": "^26.1.1",
53
+ "@types/node": "^26.2.0",
50
54
  "tsup": "^8.5.1",
51
55
  "typescript": "^6.0.3",
52
56
  "vitest": "^4.1.10"