@ic-reactor/codegen 0.12.0 → 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,18 +1,248 @@
1
- import {
2
- generateCodecDeclarations
3
- } from "./chunk-VBCR5IVT.js";
4
-
5
1
  // src/pipeline.ts
6
- import fs2 from "fs";
2
+ import fs3 from "fs";
7
3
  import path3 from "path";
8
4
 
9
5
  // src/generators/declarations.ts
10
6
  import { didToJs, didToTs } from "@ic-reactor/parser";
11
- import path from "path";
7
+ import path2 from "path";
8
+ import fs2 from "fs";
9
+
10
+ // src/validate.ts
12
11
  import fs from "fs";
12
+ import path from "path";
13
+
14
+ // src/naming.ts
15
+ import { camelCase, pascalCase } from "change-case";
16
+ function toPascalCase(str) {
17
+ return pascalCase(str);
18
+ }
19
+ function toCamelCase(str) {
20
+ return camelCase(str);
21
+ }
22
+ function getReactorName(canisterName) {
23
+ return `${toCamelCase(canisterName)}Reactor`;
24
+ }
25
+ function getServiceTypeName(canisterName) {
26
+ return `${toPascalCase(canisterName)}Service`;
27
+ }
28
+ function getHookPrefix(canisterName) {
29
+ return toPascalCase(canisterName);
30
+ }
31
+
32
+ // src/validate.ts
33
+ var CodegenConfigError = class extends Error {
34
+ name = "CodegenConfigError";
35
+ constructor(message) {
36
+ super(message);
37
+ }
38
+ };
39
+ var CANISTER_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
40
+ var IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
41
+ var MAX_CANISTER_NAME_LENGTH = 64;
42
+ var HAS_URI_SCHEME = /^(?:[A-Za-z][A-Za-z0-9+.-]*:|\/\/)/;
43
+ var BREAKS_OUT_OF_LITERAL = /["'`\\\u0000-\u001f\u2028\u2029]/;
44
+ function assertSafeCanisterName(name) {
45
+ if (typeof name !== "string" || name.length === 0) {
46
+ throw new CodegenConfigError(
47
+ `Invalid canister name: expected a non-empty string, received ${name === void 0 ? "undefined" : JSON.stringify(name)}. Check the "canisters" entries in your ic-reactor.json (or the plugin's "canisters" option).`
48
+ );
49
+ }
50
+ if (name.length > MAX_CANISTER_NAME_LENGTH) {
51
+ throw new CodegenConfigError(
52
+ `Invalid canister name ${JSON.stringify(name)}: must be at most ${MAX_CANISTER_NAME_LENGTH} characters.`
53
+ );
54
+ }
55
+ if (!CANISTER_NAME_PATTERN.test(name)) {
56
+ throw new CodegenConfigError(
57
+ `Invalid canister name ${JSON.stringify(name)}: may contain only letters, digits, "_", "." and "-" (${CANISTER_NAME_PATTERN.source}). Canister names become directory names, so path separators, quotes and whitespace are not allowed.`
58
+ );
59
+ }
60
+ if (name === "." || name === "..") {
61
+ throw new CodegenConfigError(
62
+ `Invalid canister name ${JSON.stringify(name)}: refers to a directory, not a canister.`
63
+ );
64
+ }
65
+ if (toPascalCase(name).length === 0) {
66
+ throw new CodegenConfigError(
67
+ `Invalid canister name ${JSON.stringify(name)}: contains no letters or digits, so it collapses to an empty identifier in generated code.`
68
+ );
69
+ }
70
+ const derived = [
71
+ ["reactor constant", getReactorName(name)],
72
+ ["service type", getServiceTypeName(name)],
73
+ ["hook name", `use${getHookPrefix(name)}Query`]
74
+ ];
75
+ for (const [what, identifier] of derived) {
76
+ if (!IDENTIFIER_PATTERN.test(identifier)) {
77
+ throw new CodegenConfigError(
78
+ `Invalid canister name ${JSON.stringify(name)}: it derives the ${what} ${JSON.stringify(identifier)}, which is not a valid TypeScript identifier. Rename the canister so it does not begin with a digit.`
79
+ );
80
+ }
81
+ }
82
+ }
83
+ function assertSafeModuleSpecifier(label, specifier) {
84
+ if (typeof specifier !== "string" || specifier.length === 0) {
85
+ throw new CodegenConfigError(
86
+ `Invalid ${label}: expected a non-empty string, received ${specifier === void 0 ? "undefined" : JSON.stringify(specifier)}.`
87
+ );
88
+ }
89
+ if (BREAKS_OUT_OF_LITERAL.test(specifier)) {
90
+ throw new CodegenConfigError(
91
+ `Invalid ${label} ${JSON.stringify(specifier)}: must not contain quotes, backslashes or control characters.`
92
+ );
93
+ }
94
+ if (/^\s|\s$/.test(specifier)) {
95
+ throw new CodegenConfigError(
96
+ `Invalid ${label} ${JSON.stringify(specifier)}: must not begin or end with whitespace.`
97
+ );
98
+ }
99
+ if (HAS_URI_SCHEME.test(specifier.trim())) {
100
+ throw new CodegenConfigError(
101
+ `Invalid ${label} ${JSON.stringify(specifier)}: must be a relative path ("./\u2026", "../\u2026") or a bare package name. URLs are not allowed \u2014 the generated file imports from this specifier.`
102
+ );
103
+ }
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
+ }
123
+ var REACTOR_CLASS_NAMES = [
124
+ "Reactor",
125
+ "DisplayReactor",
126
+ "CandidReactor",
127
+ "CandidDisplayReactor",
128
+ "MetadataDisplayReactor"
129
+ ];
130
+ var CODEGEN_TARGETS = ["react", "core"];
131
+ function assertOneOf(label, value, allowed) {
132
+ if (typeof value !== "string" || !allowed.includes(value)) {
133
+ throw new CodegenConfigError(
134
+ `Invalid ${label} ${JSON.stringify(value)}: must be one of ${allowed.map((a) => JSON.stringify(a)).join(", ")}.`
135
+ );
136
+ }
137
+ }
138
+ function realpathAllowingMissing(target) {
139
+ let current = path.resolve(target);
140
+ const missing = [];
141
+ for (; ; ) {
142
+ try {
143
+ return path.join(fs.realpathSync(current), ...missing);
144
+ } catch {
145
+ const parent = path.dirname(current);
146
+ if (parent === current) return path.resolve(target);
147
+ missing.unshift(path.basename(current));
148
+ current = parent;
149
+ }
150
+ }
151
+ }
152
+ function assertContainedPath(label, resolved, projectRoot, original = resolved) {
153
+ const relative = path.relative(
154
+ realpathAllowingMissing(projectRoot),
155
+ realpathAllowingMissing(resolved)
156
+ );
157
+ const [firstSegment] = relative.split(/[\\/]/);
158
+ if (firstSegment === ".." || path.isAbsolute(relative)) {
159
+ throw new CodegenConfigError(
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.`
161
+ );
162
+ }
163
+ }
164
+ function resolveContainedOutDir(label, outDir, projectRoot) {
165
+ if (typeof outDir !== "string" || outDir.length === 0) {
166
+ throw new CodegenConfigError(
167
+ `Invalid ${label}: expected a non-empty string, received ${outDir === void 0 ? "undefined" : JSON.stringify(outDir)}.`
168
+ );
169
+ }
170
+ const resolved = path.isAbsolute(outDir) ? path.resolve(outDir) : path.resolve(projectRoot, outDir);
171
+ assertContainedPath(label, resolved, projectRoot, outDir);
172
+ return resolved;
173
+ }
174
+ function assertSafeCanisterConfig(options) {
175
+ const {
176
+ name,
177
+ canisterOutDir,
178
+ globalOutDir,
179
+ clientManagerPath,
180
+ projectRoot,
181
+ mode,
182
+ target
183
+ } = options;
184
+ assertSafeCanisterName(name);
185
+ assertSafeModuleSpecifier("clientManagerPath", clientManagerPath);
186
+ if (mode != null) assertOneOf("mode", mode, REACTOR_CLASS_NAMES);
187
+ if (target != null) assertOneOf("target", target, CODEGEN_TARGETS);
188
+ const outDir = canisterOutDir != null ? resolveContainedOutDir(
189
+ `outDir for canister ${JSON.stringify(name)}`,
190
+ canisterOutDir,
191
+ projectRoot
192
+ ) : path.join(
193
+ resolveContainedOutDir("outDir", globalOutDir, projectRoot),
194
+ name
195
+ );
196
+ assertContainedPath(
197
+ `output directory for canister ${JSON.stringify(name)}`,
198
+ outDir,
199
+ projectRoot
200
+ );
201
+ return { name, outDir, clientManagerPath };
202
+ }
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";
13
225
  async function generateDeclarations(options) {
14
226
  const { didFile, outDir, canisterName } = options;
15
- if (!fs.existsSync(didFile)) {
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 {
16
246
  return {
17
247
  success: false,
18
248
  declarationsDir: "",
@@ -20,25 +250,39 @@ async function generateDeclarations(options) {
20
250
  error: `DID file not found: ${didFile}`
21
251
  };
22
252
  }
23
- const declarationsDir = path.join(outDir, "declarations");
24
- const baseName = path.basename(didFile, ".did");
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;
25
262
  try {
26
- const didContent = fs.readFileSync(didFile, "utf-8");
27
- if (!fs.existsSync(outDir)) {
28
- fs.mkdirSync(outDir, { recursive: true });
29
- }
30
- if (fs.existsSync(declarationsDir)) {
31
- fs.rmSync(declarationsDir, { recursive: true, force: true });
32
- }
33
- fs.mkdirSync(declarationsDir, { recursive: true });
263
+ const didContent = fs2.readFileSync(didFile, "utf-8");
34
264
  const jsContent = didToJs(didContent);
35
265
  const tsContent = didToTs(didContent);
36
- const jsPath = path.join(declarationsDir, `${baseName}.js`);
37
- const dtsPath = path.join(declarationsDir, `${baseName}.d.ts`);
38
- const didCopyPath = path.join(declarationsDir, `${baseName}.did`);
39
- fs.writeFileSync(jsPath, jsContent);
40
- fs.writeFileSync(dtsPath, tsContent);
41
- fs.writeFileSync(didCopyPath, 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;
42
286
  return {
43
287
  success: true,
44
288
  declarationsDir,
@@ -56,29 +300,24 @@ async function generateDeclarations(options) {
56
300
  files: [],
57
301
  error: `[${canisterName}] Failed to generate declarations: ${message}`
58
302
  };
303
+ } finally {
304
+ if (staging) fs2.rmSync(staging, { recursive: true, force: true });
59
305
  }
60
306
  }
61
- function declarationsExist(outDir, canisterName) {
62
- const dtsPath = path.join(outDir, "declarations", `${canisterName}.d.ts`);
63
- return fs.existsSync(dtsPath);
64
- }
65
-
66
- // src/generators/reactor.ts
67
- import path2 from "path";
68
-
69
- // src/naming.ts
70
- import { camelCase, pascalCase } from "change-case";
71
- function toPascalCase(str) {
72
- return pascalCase(str);
73
- }
74
- function toCamelCase(str) {
75
- return camelCase(str);
76
- }
77
- function getReactorName(canisterName) {
78
- return `${toCamelCase(canisterName)}Reactor`;
79
- }
80
- function getServiceTypeName(canisterName) {
81
- return `${toPascalCase(canisterName)}Service`;
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
+ }
82
321
  }
83
322
 
84
323
  // src/generators/reactor.ts
@@ -91,6 +330,10 @@ function getReactorClassImportSource(reactorClass, runtimeTarget) {
91
330
  case "CandidDisplayReactor":
92
331
  case "MetadataDisplayReactor":
93
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
+ );
94
337
  }
95
338
  }
96
339
  function generateReactorFile(options) {
@@ -105,7 +348,7 @@ function generateReactorFile(options) {
105
348
  const pascalName = toPascalCase(canisterName);
106
349
  const reactorName = getReactorName(canisterName);
107
350
  const serviceName = getServiceTypeName(canisterName);
108
- const baseName = path2.basename(didFile, ".did");
351
+ const baseName = resolveDeclarationsBaseName(didFile);
109
352
  const declarationsPath = `./declarations/${baseName}`;
110
353
  const reactorImportSource = getReactorClassImportSource(
111
354
  reactorClass,
@@ -124,9 +367,9 @@ export const {
124
367
  useActorMethod: use${pascalName}Method,
125
368
  } = createActorHooks(${reactorName})
126
369
  ` : "";
127
- return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from "${reactorImportSource}"
128
- import { clientManager } from "${clientManagerPath}"
129
- import { idlFactory, type _SERVICE } from "${declarationsPath}"
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)}
130
373
 
131
374
  export type ${serviceName} = _SERVICE
132
375
 
@@ -142,7 +385,7 @@ export type ${serviceName} = _SERVICE
142
385
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
143
386
  clientManager,
144
387
  idlFactory,
145
- ${canisterIdLine} name: "${canisterName}",
388
+ ${canisterIdLine} name: ${JSON.stringify(canisterName)},
146
389
  })${hookExports || "\n"}`;
147
390
  }
148
391
  function generateReactorEntryFile() {
@@ -175,8 +418,48 @@ function resolveRuntimeTarget(canisterConfig, globalConfig) {
175
418
  function normalizeFileContent(content) {
176
419
  return content.replace(/\r\n/g, "\n").trim();
177
420
  }
178
- function isLegacyGeneratedIndexFile(content) {
179
- 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];
180
463
  }
181
464
  function isManagedEntryWrapper(content, expectedEntryContent) {
182
465
  return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
@@ -190,8 +473,30 @@ async function runCanisterPipeline(options) {
190
473
  } = options;
191
474
  const { name, didFile, clientManagerPath } = canisterConfig;
192
475
  const files = [];
476
+ let validated;
477
+ try {
478
+ validated = assertSafeCanisterConfig({
479
+ name,
480
+ canisterOutDir: canisterConfig.outDir,
481
+ globalOutDir: globalConfig.outDir,
482
+ clientManagerPath: clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients",
483
+ projectRoot,
484
+ mode: canisterConfig.mode,
485
+ target: canisterConfig.target ?? globalConfig.target
486
+ });
487
+ } catch (err) {
488
+ if (err instanceof CodegenConfigError) {
489
+ return {
490
+ canisterName: typeof name === "string" ? name : String(name),
491
+ success: false,
492
+ files,
493
+ error: err.message
494
+ };
495
+ }
496
+ throw err;
497
+ }
193
498
  const resolvedDidFile = path3.isAbsolute(didFile) ? didFile : path3.resolve(projectRoot, didFile);
194
- if (!fs2.existsSync(resolvedDidFile)) {
499
+ if (!fs3.existsSync(resolvedDidFile)) {
195
500
  return {
196
501
  canisterName: name,
197
502
  success: false,
@@ -199,8 +504,30 @@ async function runCanisterPipeline(options) {
199
504
  error: `DID file not found: ${resolvedDidFile}`
200
505
  };
201
506
  }
202
- const canisterOutDir = canisterConfig.outDir != null ? path3.isAbsolute(canisterConfig.outDir) ? canisterConfig.outDir : path3.resolve(projectRoot, canisterConfig.outDir) : path3.resolve(projectRoot, globalConfig.outDir, name);
203
- const resolvedClientManagerPath = clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients";
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
+ }
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
+ }
530
+ const resolvedClientManagerPath = validated.clientManagerPath;
204
531
  try {
205
532
  const declResult = await generateDeclarations({
206
533
  didFile: resolvedDidFile,
@@ -245,16 +572,21 @@ async function runCanisterPipeline(options) {
245
572
  reactorClass
246
573
  });
247
574
  const entryContent = generateReactorEntryFile();
248
- fs2.mkdirSync(canisterOutDir, { recursive: true });
249
- fs2.writeFileSync(reactorPath, reactorContent);
575
+ fs3.mkdirSync(canisterOutDir, { recursive: true });
576
+ fs3.writeFileSync(reactorPath, reactorContent);
250
577
  files.push({ success: true, filePath: reactorPath });
251
- if (!fs2.existsSync(entryPath)) {
252
- fs2.writeFileSync(entryPath, entryContent);
578
+ if (!fs3.existsSync(entryPath)) {
579
+ fs3.writeFileSync(entryPath, entryContent);
253
580
  files.push({ success: true, filePath: entryPath });
254
581
  } else {
255
- const existingEntryContent = fs2.readFileSync(entryPath, "utf-8");
256
- if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
257
- fs2.writeFileSync(entryPath, entryContent);
582
+ const existingEntryContent = fs3.readFileSync(entryPath, "utf-8");
583
+ if (isManagedEntryWrapper(existingEntryContent, entryContent)) {
584
+ fs3.writeFileSync(entryPath, entryContent);
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 });
258
590
  files.push({ success: true, filePath: entryPath });
259
591
  } else {
260
592
  files.push({ success: true, filePath: entryPath, skipped: true });
@@ -280,40 +612,6 @@ async function runCanisterPipeline(options) {
280
612
  };
281
613
  }
282
614
 
283
- // src/parser.ts
284
- import { didToJs as didToJs2 } from "@ic-reactor/parser";
285
- import fs3 from "fs";
286
- function extractMethods(didContent) {
287
- try {
288
- const jsContent = didToJs2(didContent);
289
- const methods = [];
290
- const serviceMatch = /IDL\.Service\(\{([\s\S]*?)\}\)/.exec(jsContent);
291
- if (!serviceMatch) return methods;
292
- const serviceBody = serviceMatch[1];
293
- const methodRegex = /['"]([\w]+)['"]\s*:\s*IDL\.Func\(\s*\[(.*?)\],\s*\[.*?\],\s*\[(.*?)\]\)/g;
294
- let match;
295
- while ((match = methodRegex.exec(serviceBody)) !== null) {
296
- const [, name, args, annotations] = match;
297
- methods.push({
298
- name,
299
- type: annotations.includes("'query'") ? "query" : "mutation",
300
- hasArgs: args.trim().length > 0
301
- });
302
- }
303
- return methods;
304
- } catch (error) {
305
- const msg = error instanceof Error ? error.message : String(error);
306
- throw new Error(`Failed to parse Candid: ${msg}`);
307
- }
308
- }
309
- function parseDIDFile(didFilePath) {
310
- if (!fs3.existsSync(didFilePath)) {
311
- throw new Error(`DID file not found: ${didFilePath}`);
312
- }
313
- const content = fs3.readFileSync(didFilePath, "utf-8");
314
- return extractMethods(content);
315
- }
316
-
317
615
  // src/generators/client.ts
318
616
  function generateClientFile(options = {}) {
319
617
  const { queryClientPath } = options;
@@ -335,16 +633,24 @@ export const clientManager = new ClientManager({
335
633
  `;
336
634
  }
337
635
  export {
636
+ CANISTER_NAME_PATTERN,
637
+ CODEGEN_TARGETS,
638
+ CodegenConfigError,
639
+ REACTOR_CLASS_NAMES,
640
+ assertContainedPath,
641
+ assertOneOf,
642
+ assertSafeCanisterConfig,
643
+ assertSafeCanisterName,
644
+ assertSafeModuleSpecifier,
338
645
  declarationsExist,
339
- extractMethods,
340
646
  generateClientFile,
341
- generateCodecDeclarations,
342
647
  generateDeclarations,
343
648
  generateReactorEntryFile,
344
649
  generateReactorFile,
345
650
  getReactorName,
346
651
  getServiceTypeName,
347
- parseDIDFile,
652
+ resolveContainedOutDir,
653
+ resolveDeclarationsBaseName,
348
654
  runCanisterPipeline,
349
655
  toPascalCase
350
656
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/codegen",
3
- "version": "0.12.0",
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",
@@ -17,21 +17,15 @@
17
17
  "default": "./dist/index.cjs"
18
18
  }
19
19
  },
20
- "./renderer": {
21
- "import": {
22
- "types": "./dist/renderer.d.ts",
23
- "default": "./dist/renderer.js"
24
- },
25
- "require": {
26
- "types": "./dist/renderer.d.cts",
27
- "default": "./dist/renderer.cjs"
28
- }
29
- },
30
20
  "./package.json": "./package.json"
31
21
  },
32
22
  "files": [
33
23
  "dist",
34
24
  "src",
25
+ "!src/**/*.test.*",
26
+ "!src/**/*.spec.*",
27
+ "!src/**/__snapshots__/**",
28
+ "!**/*.tsbuildinfo",
35
29
  "README.md",
36
30
  "llms.txt"
37
31
  ],
@@ -53,17 +47,17 @@
53
47
  "homepage": "https://ic-reactor.b3pay.net/v3/packages/codegen",
54
48
  "dependencies": {
55
49
  "change-case": "^5.4.4",
56
- "@ic-reactor/parser": "0.4.7"
50
+ "@ic-reactor/parser": "0.4.8"
57
51
  },
58
52
  "devDependencies": {
59
- "@types/node": "^26.1.1",
53
+ "@types/node": "^26.2.0",
60
54
  "tsup": "^8.5.1",
61
55
  "typescript": "^6.0.3",
62
56
  "vitest": "^4.1.10"
63
57
  },
64
58
  "scripts": {
65
- "build": "tsup src/index.ts src/renderer.ts --format esm,cjs --dts --tsconfig tsconfig.json",
66
- "dev": "tsup src/index.ts src/renderer.ts --format esm,cjs --dts --watch --tsconfig tsconfig.json",
59
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean --tsconfig tsconfig.json",
60
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch --tsconfig tsconfig.json",
67
61
  "test": "vitest run",
68
62
  "test:watch": "vitest",
69
63
  "typecheck": "tsc --noEmit -p tsconfig.typecheck.json"