@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.cjs CHANGED
@@ -30,32 +30,274 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ CANISTER_NAME_PATTERN: () => CANISTER_NAME_PATTERN,
34
+ CODEGEN_TARGETS: () => CODEGEN_TARGETS,
35
+ CodegenConfigError: () => CodegenConfigError,
36
+ REACTOR_CLASS_NAMES: () => REACTOR_CLASS_NAMES,
37
+ assertContainedPath: () => assertContainedPath,
38
+ assertOneOf: () => assertOneOf,
39
+ assertSafeCanisterConfig: () => assertSafeCanisterConfig,
40
+ assertSafeCanisterName: () => assertSafeCanisterName,
41
+ assertSafeModuleSpecifier: () => assertSafeModuleSpecifier,
33
42
  declarationsExist: () => declarationsExist,
34
- extractMethods: () => extractMethods,
35
43
  generateClientFile: () => generateClientFile,
36
- generateCodecDeclarations: () => generateCodecDeclarations,
37
44
  generateDeclarations: () => generateDeclarations,
38
45
  generateReactorEntryFile: () => generateReactorEntryFile,
39
46
  generateReactorFile: () => generateReactorFile,
40
47
  getReactorName: () => getReactorName,
41
48
  getServiceTypeName: () => getServiceTypeName,
42
- parseDIDFile: () => parseDIDFile,
49
+ resolveContainedOutDir: () => resolveContainedOutDir,
50
+ resolveDeclarationsBaseName: () => resolveDeclarationsBaseName,
43
51
  runCanisterPipeline: () => runCanisterPipeline,
44
52
  toPascalCase: () => toPascalCase
45
53
  });
46
54
  module.exports = __toCommonJS(index_exports);
47
55
 
48
56
  // src/pipeline.ts
49
- var import_node_fs2 = __toESM(require("fs"), 1);
57
+ var import_node_fs3 = __toESM(require("fs"), 1);
50
58
  var import_node_path3 = __toESM(require("path"), 1);
51
59
 
52
60
  // src/generators/declarations.ts
53
61
  var import_parser = require("@ic-reactor/parser");
54
- var import_node_path = __toESM(require("path"), 1);
62
+ var import_node_path2 = __toESM(require("path"), 1);
63
+ var import_node_fs2 = __toESM(require("fs"), 1);
64
+
65
+ // src/validate.ts
55
66
  var import_node_fs = __toESM(require("fs"), 1);
67
+ var import_node_path = __toESM(require("path"), 1);
68
+
69
+ // src/naming.ts
70
+ var import_change_case = require("change-case");
71
+ function toPascalCase(str) {
72
+ return (0, import_change_case.pascalCase)(str);
73
+ }
74
+ function toCamelCase(str) {
75
+ return (0, import_change_case.camelCase)(str);
76
+ }
77
+ function getReactorName(canisterName) {
78
+ return `${toCamelCase(canisterName)}Reactor`;
79
+ }
80
+ function getServiceTypeName(canisterName) {
81
+ return `${toPascalCase(canisterName)}Service`;
82
+ }
83
+ function getHookPrefix(canisterName) {
84
+ return toPascalCase(canisterName);
85
+ }
86
+
87
+ // src/validate.ts
88
+ var CodegenConfigError = class extends Error {
89
+ name = "CodegenConfigError";
90
+ constructor(message) {
91
+ super(message);
92
+ }
93
+ };
94
+ var CANISTER_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
95
+ var IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
96
+ var MAX_CANISTER_NAME_LENGTH = 64;
97
+ var HAS_URI_SCHEME = /^(?:[A-Za-z][A-Za-z0-9+.-]*:|\/\/)/;
98
+ var BREAKS_OUT_OF_LITERAL = /["'`\\\u0000-\u001f\u2028\u2029]/;
99
+ function assertSafeCanisterName(name) {
100
+ if (typeof name !== "string" || name.length === 0) {
101
+ throw new CodegenConfigError(
102
+ `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).`
103
+ );
104
+ }
105
+ if (name.length > MAX_CANISTER_NAME_LENGTH) {
106
+ throw new CodegenConfigError(
107
+ `Invalid canister name ${JSON.stringify(name)}: must be at most ${MAX_CANISTER_NAME_LENGTH} characters.`
108
+ );
109
+ }
110
+ if (!CANISTER_NAME_PATTERN.test(name)) {
111
+ throw new CodegenConfigError(
112
+ `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.`
113
+ );
114
+ }
115
+ if (name === "." || name === "..") {
116
+ throw new CodegenConfigError(
117
+ `Invalid canister name ${JSON.stringify(name)}: refers to a directory, not a canister.`
118
+ );
119
+ }
120
+ if (toPascalCase(name).length === 0) {
121
+ throw new CodegenConfigError(
122
+ `Invalid canister name ${JSON.stringify(name)}: contains no letters or digits, so it collapses to an empty identifier in generated code.`
123
+ );
124
+ }
125
+ const derived = [
126
+ ["reactor constant", getReactorName(name)],
127
+ ["service type", getServiceTypeName(name)],
128
+ ["hook name", `use${getHookPrefix(name)}Query`]
129
+ ];
130
+ for (const [what, identifier] of derived) {
131
+ if (!IDENTIFIER_PATTERN.test(identifier)) {
132
+ throw new CodegenConfigError(
133
+ `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.`
134
+ );
135
+ }
136
+ }
137
+ }
138
+ function assertSafeModuleSpecifier(label, specifier) {
139
+ if (typeof specifier !== "string" || specifier.length === 0) {
140
+ throw new CodegenConfigError(
141
+ `Invalid ${label}: expected a non-empty string, received ${specifier === void 0 ? "undefined" : JSON.stringify(specifier)}.`
142
+ );
143
+ }
144
+ if (BREAKS_OUT_OF_LITERAL.test(specifier)) {
145
+ throw new CodegenConfigError(
146
+ `Invalid ${label} ${JSON.stringify(specifier)}: must not contain quotes, backslashes or control characters.`
147
+ );
148
+ }
149
+ if (/^\s|\s$/.test(specifier)) {
150
+ throw new CodegenConfigError(
151
+ `Invalid ${label} ${JSON.stringify(specifier)}: must not begin or end with whitespace.`
152
+ );
153
+ }
154
+ if (HAS_URI_SCHEME.test(specifier.trim())) {
155
+ throw new CodegenConfigError(
156
+ `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.`
157
+ );
158
+ }
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
+ }
178
+ var REACTOR_CLASS_NAMES = [
179
+ "Reactor",
180
+ "DisplayReactor",
181
+ "CandidReactor",
182
+ "CandidDisplayReactor",
183
+ "MetadataDisplayReactor"
184
+ ];
185
+ var CODEGEN_TARGETS = ["react", "core"];
186
+ function assertOneOf(label, value, allowed) {
187
+ if (typeof value !== "string" || !allowed.includes(value)) {
188
+ throw new CodegenConfigError(
189
+ `Invalid ${label} ${JSON.stringify(value)}: must be one of ${allowed.map((a) => JSON.stringify(a)).join(", ")}.`
190
+ );
191
+ }
192
+ }
193
+ function realpathAllowingMissing(target) {
194
+ let current = import_node_path.default.resolve(target);
195
+ const missing = [];
196
+ for (; ; ) {
197
+ try {
198
+ return import_node_path.default.join(import_node_fs.default.realpathSync(current), ...missing);
199
+ } catch {
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));
203
+ current = parent;
204
+ }
205
+ }
206
+ }
207
+ function assertContainedPath(label, resolved, projectRoot, original = resolved) {
208
+ const relative = import_node_path.default.relative(
209
+ realpathAllowingMissing(projectRoot),
210
+ realpathAllowingMissing(resolved)
211
+ );
212
+ const [firstSegment] = relative.split(/[\\/]/);
213
+ if (firstSegment === ".." || import_node_path.default.isAbsolute(relative)) {
214
+ throw new CodegenConfigError(
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.`
216
+ );
217
+ }
218
+ }
219
+ function resolveContainedOutDir(label, outDir, projectRoot) {
220
+ if (typeof outDir !== "string" || outDir.length === 0) {
221
+ throw new CodegenConfigError(
222
+ `Invalid ${label}: expected a non-empty string, received ${outDir === void 0 ? "undefined" : JSON.stringify(outDir)}.`
223
+ );
224
+ }
225
+ const resolved = import_node_path.default.isAbsolute(outDir) ? import_node_path.default.resolve(outDir) : import_node_path.default.resolve(projectRoot, outDir);
226
+ assertContainedPath(label, resolved, projectRoot, outDir);
227
+ return resolved;
228
+ }
229
+ function assertSafeCanisterConfig(options) {
230
+ const {
231
+ name,
232
+ canisterOutDir,
233
+ globalOutDir,
234
+ clientManagerPath,
235
+ projectRoot,
236
+ mode,
237
+ target
238
+ } = options;
239
+ assertSafeCanisterName(name);
240
+ assertSafeModuleSpecifier("clientManagerPath", clientManagerPath);
241
+ if (mode != null) assertOneOf("mode", mode, REACTOR_CLASS_NAMES);
242
+ if (target != null) assertOneOf("target", target, CODEGEN_TARGETS);
243
+ const outDir = canisterOutDir != null ? resolveContainedOutDir(
244
+ `outDir for canister ${JSON.stringify(name)}`,
245
+ canisterOutDir,
246
+ projectRoot
247
+ ) : import_node_path.default.join(
248
+ resolveContainedOutDir("outDir", globalOutDir, projectRoot),
249
+ name
250
+ );
251
+ assertContainedPath(
252
+ `output directory for canister ${JSON.stringify(name)}`,
253
+ outDir,
254
+ projectRoot
255
+ );
256
+ return { name, outDir, clientManagerPath };
257
+ }
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";
56
280
  async function generateDeclarations(options) {
57
281
  const { didFile, outDir, canisterName } = options;
58
- if (!import_node_fs.default.existsSync(didFile)) {
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 {
59
301
  return {
60
302
  success: false,
61
303
  declarationsDir: "",
@@ -63,25 +305,39 @@ async function generateDeclarations(options) {
63
305
  error: `DID file not found: ${didFile}`
64
306
  };
65
307
  }
66
- const declarationsDir = import_node_path.default.join(outDir, "declarations");
67
- const baseName = import_node_path.default.basename(didFile, ".did");
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;
68
317
  try {
69
- const didContent = import_node_fs.default.readFileSync(didFile, "utf-8");
70
- if (!import_node_fs.default.existsSync(outDir)) {
71
- import_node_fs.default.mkdirSync(outDir, { recursive: true });
72
- }
73
- if (import_node_fs.default.existsSync(declarationsDir)) {
74
- import_node_fs.default.rmSync(declarationsDir, { recursive: true, force: true });
75
- }
76
- import_node_fs.default.mkdirSync(declarationsDir, { recursive: true });
318
+ const didContent = import_node_fs2.default.readFileSync(didFile, "utf-8");
77
319
  const jsContent = (0, import_parser.didToJs)(didContent);
78
320
  const tsContent = (0, import_parser.didToTs)(didContent);
79
- const jsPath = import_node_path.default.join(declarationsDir, `${baseName}.js`);
80
- const dtsPath = import_node_path.default.join(declarationsDir, `${baseName}.d.ts`);
81
- const didCopyPath = import_node_path.default.join(declarationsDir, `${baseName}.did`);
82
- import_node_fs.default.writeFileSync(jsPath, jsContent);
83
- import_node_fs.default.writeFileSync(dtsPath, tsContent);
84
- import_node_fs.default.writeFileSync(didCopyPath, 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;
85
341
  return {
86
342
  success: true,
87
343
  declarationsDir,
@@ -99,29 +355,24 @@ async function generateDeclarations(options) {
99
355
  files: [],
100
356
  error: `[${canisterName}] Failed to generate declarations: ${message}`
101
357
  };
358
+ } finally {
359
+ if (staging) import_node_fs2.default.rmSync(staging, { recursive: true, force: true });
102
360
  }
103
361
  }
104
- function declarationsExist(outDir, canisterName) {
105
- const dtsPath = import_node_path.default.join(outDir, "declarations", `${canisterName}.d.ts`);
106
- return import_node_fs.default.existsSync(dtsPath);
107
- }
108
-
109
- // src/generators/reactor.ts
110
- var import_node_path2 = __toESM(require("path"), 1);
111
-
112
- // src/naming.ts
113
- var import_change_case = require("change-case");
114
- function toPascalCase(str) {
115
- return (0, import_change_case.pascalCase)(str);
116
- }
117
- function toCamelCase(str) {
118
- return (0, import_change_case.camelCase)(str);
119
- }
120
- function getReactorName(canisterName) {
121
- return `${toCamelCase(canisterName)}Reactor`;
122
- }
123
- function getServiceTypeName(canisterName) {
124
- return `${toPascalCase(canisterName)}Service`;
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
+ }
125
376
  }
126
377
 
127
378
  // src/generators/reactor.ts
@@ -134,6 +385,10 @@ function getReactorClassImportSource(reactorClass, runtimeTarget) {
134
385
  case "CandidDisplayReactor":
135
386
  case "MetadataDisplayReactor":
136
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
+ );
137
392
  }
138
393
  }
139
394
  function generateReactorFile(options) {
@@ -148,7 +403,7 @@ function generateReactorFile(options) {
148
403
  const pascalName = toPascalCase(canisterName);
149
404
  const reactorName = getReactorName(canisterName);
150
405
  const serviceName = getServiceTypeName(canisterName);
151
- const baseName = import_node_path2.default.basename(didFile, ".did");
406
+ const baseName = resolveDeclarationsBaseName(didFile);
152
407
  const declarationsPath = `./declarations/${baseName}`;
153
408
  const reactorImportSource = getReactorClassImportSource(
154
409
  reactorClass,
@@ -167,9 +422,9 @@ export const {
167
422
  useActorMethod: use${pascalName}Method,
168
423
  } = createActorHooks(${reactorName})
169
424
  ` : "";
170
- return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from "${reactorImportSource}"
171
- import { clientManager } from "${clientManagerPath}"
172
- import { idlFactory, type _SERVICE } from "${declarationsPath}"
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)}
173
428
 
174
429
  export type ${serviceName} = _SERVICE
175
430
 
@@ -185,7 +440,7 @@ export type ${serviceName} = _SERVICE
185
440
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
186
441
  clientManager,
187
442
  idlFactory,
188
- ${canisterIdLine} name: "${canisterName}",
443
+ ${canisterIdLine} name: ${JSON.stringify(canisterName)},
189
444
  })${hookExports || "\n"}`;
190
445
  }
191
446
  function generateReactorEntryFile() {
@@ -218,8 +473,48 @@ function resolveRuntimeTarget(canisterConfig, globalConfig) {
218
473
  function normalizeFileContent(content) {
219
474
  return content.replace(/\r\n/g, "\n").trim();
220
475
  }
221
- function isLegacyGeneratedIndexFile(content) {
222
- 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];
223
518
  }
224
519
  function isManagedEntryWrapper(content, expectedEntryContent) {
225
520
  return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
@@ -233,8 +528,30 @@ async function runCanisterPipeline(options) {
233
528
  } = options;
234
529
  const { name, didFile, clientManagerPath } = canisterConfig;
235
530
  const files = [];
531
+ let validated;
532
+ try {
533
+ validated = assertSafeCanisterConfig({
534
+ name,
535
+ canisterOutDir: canisterConfig.outDir,
536
+ globalOutDir: globalConfig.outDir,
537
+ clientManagerPath: clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients",
538
+ projectRoot,
539
+ mode: canisterConfig.mode,
540
+ target: canisterConfig.target ?? globalConfig.target
541
+ });
542
+ } catch (err) {
543
+ if (err instanceof CodegenConfigError) {
544
+ return {
545
+ canisterName: typeof name === "string" ? name : String(name),
546
+ success: false,
547
+ files,
548
+ error: err.message
549
+ };
550
+ }
551
+ throw err;
552
+ }
236
553
  const resolvedDidFile = import_node_path3.default.isAbsolute(didFile) ? didFile : import_node_path3.default.resolve(projectRoot, didFile);
237
- if (!import_node_fs2.default.existsSync(resolvedDidFile)) {
554
+ if (!import_node_fs3.default.existsSync(resolvedDidFile)) {
238
555
  return {
239
556
  canisterName: name,
240
557
  success: false,
@@ -242,8 +559,30 @@ async function runCanisterPipeline(options) {
242
559
  error: `DID file not found: ${resolvedDidFile}`
243
560
  };
244
561
  }
245
- const canisterOutDir = canisterConfig.outDir != null ? import_node_path3.default.isAbsolute(canisterConfig.outDir) ? canisterConfig.outDir : import_node_path3.default.resolve(projectRoot, canisterConfig.outDir) : import_node_path3.default.resolve(projectRoot, globalConfig.outDir, name);
246
- const resolvedClientManagerPath = clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients";
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
+ }
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
+ }
585
+ const resolvedClientManagerPath = validated.clientManagerPath;
247
586
  try {
248
587
  const declResult = await generateDeclarations({
249
588
  didFile: resolvedDidFile,
@@ -288,16 +627,21 @@ async function runCanisterPipeline(options) {
288
627
  reactorClass
289
628
  });
290
629
  const entryContent = generateReactorEntryFile();
291
- import_node_fs2.default.mkdirSync(canisterOutDir, { recursive: true });
292
- import_node_fs2.default.writeFileSync(reactorPath, reactorContent);
630
+ import_node_fs3.default.mkdirSync(canisterOutDir, { recursive: true });
631
+ import_node_fs3.default.writeFileSync(reactorPath, reactorContent);
293
632
  files.push({ success: true, filePath: reactorPath });
294
- if (!import_node_fs2.default.existsSync(entryPath)) {
295
- import_node_fs2.default.writeFileSync(entryPath, entryContent);
633
+ if (!import_node_fs3.default.existsSync(entryPath)) {
634
+ import_node_fs3.default.writeFileSync(entryPath, entryContent);
296
635
  files.push({ success: true, filePath: entryPath });
297
636
  } else {
298
- const existingEntryContent = import_node_fs2.default.readFileSync(entryPath, "utf-8");
299
- if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
300
- import_node_fs2.default.writeFileSync(entryPath, entryContent);
637
+ const existingEntryContent = import_node_fs3.default.readFileSync(entryPath, "utf-8");
638
+ if (isManagedEntryWrapper(existingEntryContent, entryContent)) {
639
+ import_node_fs3.default.writeFileSync(entryPath, entryContent);
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 });
301
645
  files.push({ success: true, filePath: entryPath });
302
646
  } else {
303
647
  files.push({ success: true, filePath: entryPath, skipped: true });
@@ -323,40 +667,6 @@ async function runCanisterPipeline(options) {
323
667
  };
324
668
  }
325
669
 
326
- // src/parser.ts
327
- var import_parser2 = require("@ic-reactor/parser");
328
- var import_node_fs3 = __toESM(require("fs"), 1);
329
- function extractMethods(didContent) {
330
- try {
331
- const jsContent = (0, import_parser2.didToJs)(didContent);
332
- const methods = [];
333
- const serviceMatch = /IDL\.Service\(\{([\s\S]*?)\}\)/.exec(jsContent);
334
- if (!serviceMatch) return methods;
335
- const serviceBody = serviceMatch[1];
336
- const methodRegex = /['"]([\w]+)['"]\s*:\s*IDL\.Func\(\s*\[(.*?)\],\s*\[.*?\],\s*\[(.*?)\]\)/g;
337
- let match;
338
- while ((match = methodRegex.exec(serviceBody)) !== null) {
339
- const [, name, args, annotations] = match;
340
- methods.push({
341
- name,
342
- type: annotations.includes("'query'") ? "query" : "mutation",
343
- hasArgs: args.trim().length > 0
344
- });
345
- }
346
- return methods;
347
- } catch (error) {
348
- const msg = error instanceof Error ? error.message : String(error);
349
- throw new Error(`Failed to parse Candid: ${msg}`);
350
- }
351
- }
352
- function parseDIDFile(didFilePath) {
353
- if (!import_node_fs3.default.existsSync(didFilePath)) {
354
- throw new Error(`DID file not found: ${didFilePath}`);
355
- }
356
- const content = import_node_fs3.default.readFileSync(didFilePath, "utf-8");
357
- return extractMethods(content);
358
- }
359
-
360
670
  // src/generators/client.ts
361
671
  function generateClientFile(options = {}) {
362
672
  const { queryClientPath } = options;
@@ -377,620 +687,26 @@ export const clientManager = new ClientManager({
377
687
  })
378
688
  `;
379
689
  }
380
-
381
- // src/metadata-rules.json
382
- var metadata_rules_default = {
383
- defaultValidationMessages: {
384
- minimum: {
385
- template: "Must be at least {value}"
386
- },
387
- maximum: {
388
- template: "Must be at most {value}"
389
- },
390
- minLength: {
391
- template: "Must be at least {value} character{plural}"
392
- },
393
- maxLength: {
394
- template: "Must be at most {value} character{plural}"
395
- }
396
- },
397
- email: {
398
- helper: "email",
399
- regex: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
400
- jsonSchemaFormat: "email",
401
- errorMessage: "Must be a valid email address"
402
- },
403
- "date-time": {
404
- helper: "dateTime",
405
- regex: "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|[+-](?:[01]\\d|2[0-3]):[0-5]\\d))$",
406
- jsonSchemaFormat: "date-time",
407
- errorMessage: "Must be a valid ISO datetime"
408
- },
409
- datetime: {
410
- helper: "datetime",
411
- regex: "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|[+-](?:[01]\\d|2[0-3]):[0-5]\\d))$",
412
- jsonSchemaFormat: "date-time",
413
- errorMessage: "Must be a valid ISO datetime"
414
- },
415
- date: {
416
- helper: "date",
417
- regex: "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
418
- jsonSchemaFormat: "date",
419
- errorMessage: "Must be a valid ISO date"
420
- },
421
- time: {
422
- helper: "time",
423
- regex: "^(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?$",
424
- errorMessage: "Must be a valid ISO time"
425
- },
426
- duration: {
427
- helper: "duration",
428
- regex: "^P(?:(\\d+W)|(?!.*W)(?=\\d|T\\d)(\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+([.,]\\d+)?S)?)?)$",
429
- jsonSchemaFormat: "duration",
430
- errorMessage: "Must be a valid ISO duration"
431
- },
432
- url: {
433
- helper: "url",
434
- regex: "^[a-zA-Z][a-zA-Z\\d+.-]*:.+",
435
- jsonSchemaFormat: "uri",
436
- errorMessage: "Must be a valid URL"
437
- },
438
- uri: {
439
- helper: "uri",
440
- regex: "^[a-zA-Z][a-zA-Z\\d+.-]*:.+",
441
- jsonSchemaFormat: "uri",
442
- errorMessage: "Must be a valid URI"
443
- },
444
- httpsUrl: {
445
- helper: "httpsUrl",
446
- regex: "^https://.+",
447
- jsonSchemaFormat: "uri",
448
- errorMessage: "Must be a valid HTTPS URL"
449
- },
450
- ipv4: {
451
- helper: "ipv4",
452
- regex: "^(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.|$)){4}$",
453
- jsonSchemaFormat: "ipv4",
454
- errorMessage: "Must be a valid IPv4 address"
455
- },
456
- ipv6: {
457
- helper: "ipv6",
458
- regex: "^((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|::1|::|(?:[0-9A-Fa-f]{1,4}:){1,7}:|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?:(?::[0-9A-Fa-f]{1,4}){1,6}))$",
459
- jsonSchemaFormat: "ipv6",
460
- errorMessage: "Must be a valid IPv6 address"
461
- },
462
- uuid: {
463
- helper: "uuid",
464
- regex: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$",
465
- jsonSchemaFormat: "uuid",
466
- errorMessage: "Must be a valid UUID"
467
- },
468
- guid: {
469
- helper: "guid",
470
- regex: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$",
471
- jsonSchemaFormat: "uuid",
472
- errorMessage: "Must be a valid GUID"
473
- },
474
- base64: {
475
- helper: "base64",
476
- regex: "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$",
477
- contentEncoding: "base64",
478
- errorMessage: "Must be valid base64"
479
- },
480
- base64url: {
481
- helper: "base64url",
482
- regex: "^[A-Za-z0-9_-]+$",
483
- errorMessage: "Must be valid base64url"
484
- },
485
- cuid: {
486
- helper: "cuid",
487
- regex: "^c[a-z0-9]{24}$",
488
- errorMessage: "Must be a valid CUID"
489
- },
490
- cuid2: {
491
- helper: "cuid2",
492
- regex: "^[a-z][a-z0-9]*$",
493
- errorMessage: "Must be a valid CUID2"
494
- },
495
- ulid: {
496
- helper: "ulid",
497
- regex: "^[0-9A-HJKMNP-TV-Z]{26}$",
498
- errorMessage: "Must be a valid ULID"
499
- },
500
- nanoid: {
501
- helper: "nanoid",
502
- regex: "^[A-Za-z0-9_-]{21}$",
503
- errorMessage: "Must be a valid nanoid"
504
- },
505
- emoji: {
506
- helper: "emoji",
507
- regex: "^\\p{Extended_Pictographic}+$",
508
- errorMessage: "Must contain only emoji characters"
509
- },
510
- cidrv4: {
511
- helper: "cidrv4",
512
- regex: "^(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)/(?:3[0-2]|[12]?\\d)$",
513
- errorMessage: "Must be a valid IPv4 CIDR range"
514
- },
515
- cidrv6: {
516
- helper: "cidrv6",
517
- regex: "^[0-9A-Fa-f:]+/(?:12[0-8]|1[01]\\d|\\d?\\d)$",
518
- errorMessage: "Must be a valid IPv6 CIDR range"
519
- },
520
- mac: {
521
- helper: "mac",
522
- regex: "^(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$",
523
- errorMessage: "Must be a valid MAC address"
524
- }
525
- };
526
-
527
- // src/metadata.ts
528
- var metadataRulesFile = metadata_rules_default;
529
- var defaultValidationMessages = metadataRulesFile.defaultValidationMessages ?? {};
530
- var rawRuleEntries = Object.entries(metadata_rules_default).filter(
531
- ([type]) => type !== "defaultValidationMessages"
532
- );
533
- var BUILT_IN_JSDOC_FORMAT_TYPES = Object.fromEntries(
534
- rawRuleEntries.map(([type, rule]) => [
535
- type,
536
- {
537
- regex: rule.regex,
538
- jsonSchemaFormat: rule.jsonSchemaFormat,
539
- contentEncoding: rule.contentEncoding,
540
- errorMessage: rule.errorMessage
541
- }
542
- ])
543
- );
544
- var BUILT_IN_FORMAT_HELPERS = Object.fromEntries(
545
- rawRuleEntries.filter(([, rule]) => rule.helper).map(([type, rule]) => [type, rule.helper])
546
- );
547
- function normalizeFormatDefinition(definition) {
548
- if (!definition) return void 0;
549
- return typeof definition === "string" ? { regex: definition } : definition;
550
- }
551
- function formatDefinitionFor(type, options) {
552
- const builtIn = BUILT_IN_JSDOC_FORMAT_TYPES[type];
553
- const custom = normalizeFormatDefinition(
554
- options.customJSDocFormatTypes?.[type]
555
- );
556
- if (!custom) return builtIn;
557
- return {
558
- ...builtIn,
559
- ...custom,
560
- errorMessage: custom.errorMessage ?? builtIn?.errorMessage
561
- };
562
- }
563
- function stripUndefined(value) {
564
- return Object.fromEntries(
565
- Object.entries(value).filter(([, entry]) => entry !== void 0)
566
- );
567
- }
568
- function defaultBoundMessage(kind, value) {
569
- const template = defaultValidationMessages[kind]?.template ?? (kind === "minimum" ? "Must be at least {value}" : kind === "maximum" ? "Must be at most {value}" : kind === "minLength" ? "Must be at least {value} character{plural}" : "Must be at most {value} character{plural}");
570
- return template.replace("{value}", value).replace("{plural}", value === "1" ? "" : "s");
571
- }
572
- function withDefaultBoundMessage(bound, kind) {
573
- if (!bound || bound.message !== void 0) return bound;
574
- return {
575
- ...bound,
576
- message: defaultBoundMessage(kind, bound.value)
577
- };
578
- }
579
- function normalizeValidationMetadata(validation, options) {
580
- const normalized = {
581
- ...validation,
582
- minimum: withDefaultBoundMessage(validation.minimum, "minimum"),
583
- maximum: withDefaultBoundMessage(validation.maximum, "maximum"),
584
- minLength: withDefaultBoundMessage(validation.minLength, "minLength"),
585
- maxLength: withDefaultBoundMessage(validation.maxLength, "maxLength")
586
- };
587
- const format = validation.format;
588
- const formatDefinition = format ? formatDefinitionFor(format.type, options) : void 0;
589
- if (format && formatDefinition) {
590
- normalized.format = stripUndefined({
591
- ...format,
592
- regex: formatDefinition.regex,
593
- jsonSchemaFormat: formatDefinition.jsonSchemaFormat,
594
- contentEncoding: formatDefinition.contentEncoding,
595
- errorMessage: format.message ?? formatDefinition.errorMessage
596
- });
597
- } else if (format) {
598
- normalized.format = format;
599
- }
600
- return normalized;
601
- }
602
-
603
- // src/renderer.ts
604
- var reservedWords = /* @__PURE__ */ new Set([
605
- "break",
606
- "case",
607
- "catch",
608
- "class",
609
- "const",
610
- "continue",
611
- "debugger",
612
- "default",
613
- "delete",
614
- "do",
615
- "else",
616
- "export",
617
- "extends",
618
- "false",
619
- "finally",
620
- "for",
621
- "function",
622
- "if",
623
- "import",
624
- "in",
625
- "instanceof",
626
- "new",
627
- "null",
628
- "return",
629
- "super",
630
- "switch",
631
- "this",
632
- "throw",
633
- "true",
634
- "try",
635
- "typeof",
636
- "var",
637
- "void",
638
- "while",
639
- "with",
640
- "yield",
641
- "let",
642
- "package",
643
- "private",
644
- "protected",
645
- "public",
646
- "static"
647
- ]);
648
- function isValidIdentifier(name) {
649
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !reservedWords.has(name);
650
- }
651
- function assertIdentifier(name, label) {
652
- if (!isValidIdentifier(name)) {
653
- throw new Error(`${label} must be a valid TypeScript identifier: ${name}`);
654
- }
655
- }
656
- function propertyName(name) {
657
- return isValidIdentifier(name) ? name : JSON.stringify(name);
658
- }
659
- function normalizeOptions(options = {}) {
660
- return typeof options === "string" ? { serviceExportName: options } : options;
661
- }
662
- function toServiceExportName(canisterName) {
663
- return canisterName.replace(/[-\s]+/g, "_").toUpperCase();
664
- }
665
- function resolveServiceExportName(options, declaredTypeNames) {
666
- if (options.serviceExportName) return options.serviceExportName;
667
- if (options.canisterName) {
668
- let name = toServiceExportName(options.canisterName);
669
- if (declaredTypeNames.has(name)) {
670
- name = `${name}_SERVICE`;
671
- }
672
- return name;
673
- }
674
- return "_SERVICE";
675
- }
676
- function hasMetadata(metadata) {
677
- return metadata != null && Object.keys(metadata).length > 0;
678
- }
679
- function stripUndefined2(value) {
680
- return Object.fromEntries(
681
- Object.entries(value).filter(([, entry]) => entry !== void 0)
682
- );
683
- }
684
- function isEmptyObject(value) {
685
- return value != null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
686
- }
687
- function textFormatHelperFor(expression, metadata, options) {
688
- const format = metadata.validation?.format;
689
- if (!format || expression !== "c.text()") return void 0;
690
- if (options.customJSDocFormatTypes?.[format.type]) {
691
- return void 0;
692
- }
693
- return BUILT_IN_FORMAT_HELPERS[format.type];
694
- }
695
- function hasOnlyDescriptionDocs(docs, description) {
696
- return docs != null && description != null && docs.length === 1 && docs[0] === description;
697
- }
698
- function metadataForRender(metadata, options) {
699
- if (!metadata.validation) {
700
- return metadata;
701
- }
702
- return {
703
- ...metadata,
704
- validation: normalizeValidationMetadata(metadata.validation, {
705
- customJSDocFormatTypes: options.customJSDocFormatTypes
706
- })
707
- };
708
- }
709
- function applyMetadata(expression, metadata, options) {
710
- if (!hasMetadata(metadata)) return expression;
711
- const textFormatHelper = textFormatHelperFor(expression, metadata, options);
712
- const textFormatMessage = metadata.validation?.format?.message;
713
- const renderedMetadata = metadataForRender(metadata, options);
714
- const { description, ...metadataRest } = renderedMetadata;
715
- const rest = { ...metadataRest };
716
- let result = textFormatHelper ? `c.${textFormatHelper}(${textFormatMessage ? JSON.stringify(textFormatMessage) : ""})` : expression;
717
- if (textFormatHelper) {
718
- delete rest.docs;
719
- if (rest.validation) {
720
- const { format: _format, ...validationRest } = rest.validation;
721
- rest.validation = stripUndefined2(validationRest);
722
- if (isEmptyObject(rest.validation)) {
723
- delete rest.validation;
724
- }
725
- }
726
- } else if (hasOnlyDescriptionDocs(rest.docs, description)) {
727
- delete rest.docs;
728
- }
729
- if (description) {
730
- result += `.describe(${JSON.stringify(description)})`;
731
- }
732
- if (Object.keys(rest).length > 0) {
733
- result += `.meta(${JSON.stringify(rest)})`;
734
- }
735
- return result;
736
- }
737
- function getReferencedNames(type) {
738
- const refs = [];
739
- function visit(node) {
740
- switch (node.kind) {
741
- case "reference":
742
- refs.push(node.name);
743
- break;
744
- case "opt":
745
- case "vec":
746
- visit(node.type);
747
- break;
748
- case "record":
749
- case "variant":
750
- for (const field of node.fields) {
751
- visit(field.type);
752
- }
753
- break;
754
- case "tuple":
755
- for (const item of node.types) {
756
- visit(item);
757
- }
758
- break;
759
- }
760
- }
761
- visit(type);
762
- return refs;
763
- }
764
- function sortDeclarations(declarations) {
765
- const sorted = [];
766
- const visited = /* @__PURE__ */ new Set();
767
- const visiting = [];
768
- const declarationByName = new Map(
769
- declarations.map((decl) => [decl.name, decl])
770
- );
771
- function visit(name) {
772
- if (visited.has(name)) return;
773
- const cycleStart = visiting.indexOf(name);
774
- if (cycleStart !== -1) {
775
- const cycle = [...visiting.slice(cycleStart), name].join(" -> ");
776
- throw new Error(`Recursive Candid types are not supported yet: ${cycle}`);
777
- }
778
- const declaration = declarationByName.get(name);
779
- if (!declaration) return;
780
- visiting.push(name);
781
- for (const dependency of getReferencedNames(declaration.type)) {
782
- visit(dependency);
783
- }
784
- visiting.pop();
785
- visited.add(name);
786
- sorted.push(declaration);
787
- }
788
- for (const declaration of declarations) {
789
- visit(declaration.name);
790
- }
791
- return sorted;
792
- }
793
- function renderType(type, indent = "", options = {}) {
794
- const nextIndent = `${indent} `;
795
- let expression;
796
- switch (type.kind) {
797
- case "null":
798
- expression = "c.null()";
799
- break;
800
- case "bool":
801
- expression = "c.bool()";
802
- break;
803
- case "nat":
804
- expression = "c.nat()";
805
- break;
806
- case "int":
807
- expression = "c.int()";
808
- break;
809
- case "nat8":
810
- expression = "c.nat8()";
811
- break;
812
- case "nat16":
813
- expression = "c.nat16()";
814
- break;
815
- case "nat32":
816
- expression = "c.nat32()";
817
- break;
818
- case "nat64":
819
- expression = "c.nat64()";
820
- break;
821
- case "int8":
822
- expression = "c.int8()";
823
- break;
824
- case "int16":
825
- expression = "c.int16()";
826
- break;
827
- case "int32":
828
- expression = "c.int32()";
829
- break;
830
- case "int64":
831
- expression = "c.int64()";
832
- break;
833
- case "float32":
834
- expression = "c.float32()";
835
- break;
836
- case "float64":
837
- expression = "c.float64()";
838
- break;
839
- case "text":
840
- expression = "c.text()";
841
- break;
842
- case "reserved":
843
- expression = "c.reserved()";
844
- break;
845
- case "empty":
846
- expression = "c.empty()";
847
- break;
848
- case "principal":
849
- expression = "c.principal()";
850
- break;
851
- case "blob":
852
- expression = "c.blob()";
853
- break;
854
- case "reference":
855
- assertIdentifier(type.name, "Type reference");
856
- expression = type.name;
857
- break;
858
- case "opt":
859
- expression = `c.opt(${renderType(type.type, indent, options)})`;
860
- break;
861
- case "vec":
862
- expression = `c.vec(${renderType(type.type, indent, options)})`;
863
- break;
864
- case "record": {
865
- if (type.fields.length === 0) {
866
- expression = "c.record({})";
867
- } else {
868
- const fields = type.fields.map((field) => {
869
- const fieldExpression = applyMetadata(
870
- renderType(field.type, nextIndent, options),
871
- field.metadata,
872
- options
873
- );
874
- return `${nextIndent}${propertyName(field.name)}: ${fieldExpression},`;
875
- }).join("\n");
876
- expression = `c.record({
877
- ${fields}
878
- ${indent}})`;
879
- }
880
- break;
881
- }
882
- case "variant": {
883
- if (type.fields.length === 0) {
884
- expression = "c.variant({})";
885
- } else {
886
- const fields = type.fields.map((field) => {
887
- const fieldExpression = applyMetadata(
888
- renderType(field.type, nextIndent, options),
889
- field.metadata,
890
- options
891
- );
892
- return `${nextIndent}${propertyName(field.name)}: ${fieldExpression},`;
893
- }).join("\n");
894
- expression = `c.variant({
895
- ${fields}
896
- ${indent}})`;
897
- }
898
- break;
899
- }
900
- case "tuple":
901
- expression = `c.tuple([${type.types.map((item) => renderType(item, indent, options)).join(", ")}])`;
902
- break;
903
- case "func":
904
- case "service":
905
- case "class":
906
- case "unknown":
907
- case "knot":
908
- case "future":
909
- expression = `/* c.${type.kind} is not supported */ c.reserved()`;
910
- break;
911
- }
912
- return applyMetadata(expression, type.metadata, options);
913
- }
914
- function renderMethodReturn(method, options) {
915
- if (method.mode === "oneway") return "";
916
- if (method.returns.length === 0) return "";
917
- if (method.returns.length === 1) {
918
- return `, ${renderType(method.returns[0], " ", options)}`;
919
- }
920
- return `, [${method.returns.map((returnType) => renderType(returnType, " ", options)).join(", ")}]`;
921
- }
922
- function generateCodecDeclarations(schema, optionsOrServiceExportName = {}) {
923
- const options = normalizeOptions(optionsOrServiceExportName);
924
- const lines = ['import { c } from "@ic-reactor/cod"', ""];
925
- for (const declaration of sortDeclarations(schema.types)) {
926
- assertIdentifier(declaration.name, "Type declaration name");
927
- lines.push(
928
- `export const ${declaration.name} = ${applyMetadata(
929
- renderType(declaration.type, "", options),
930
- declaration.metadata,
931
- options
932
- )}`
933
- );
934
- lines.push(
935
- `export type ${declaration.name} = c.infer<typeof ${declaration.name}>`
936
- );
937
- lines.push("");
938
- }
939
- if (schema.service) {
940
- const includeCompatibilityExports = options.includeCompatibilityExports ?? false;
941
- const declaredTypeNames = new Set(schema.types.map((t) => t.name));
942
- const serviceExportName = resolveServiceExportName(
943
- options,
944
- declaredTypeNames
945
- );
946
- assertIdentifier(serviceExportName, "Service export name");
947
- const methods = schema.service.methods.map((method) => {
948
- const args = method.args.map((arg) => renderType(arg, " ", options)).join(", ");
949
- const methodExpression = `c.${method.mode}([${args}]${renderMethodReturn(
950
- method,
951
- options
952
- )})`;
953
- return ` ${propertyName(method.name)}: ${applyMetadata(
954
- methodExpression,
955
- method.metadata,
956
- options
957
- )},`;
958
- }).join("\n");
959
- const serviceExpression = methods.length > 0 ? `c.service({
960
- ${methods}
961
- })` : "c.service({})";
962
- lines.push(
963
- `export const ${serviceExportName} = ${applyMetadata(
964
- serviceExpression,
965
- schema.service.metadata,
966
- options
967
- )}`
968
- );
969
- lines.push("");
970
- if (includeCompatibilityExports) {
971
- lines.push(`export const idlFactory = ${serviceExportName}.idlFactory`);
972
- lines.push(
973
- `export type _SERVICE = c.ServiceOf<typeof ${serviceExportName}>`
974
- );
975
- lines.push("");
976
- lines.push(`export const manifest = ${serviceExportName}.manifest()`);
977
- }
978
- }
979
- return `${lines.join("\n").trimEnd()}
980
- `;
981
- }
982
690
  // Annotate the CommonJS export names for ESM import in node:
983
691
  0 && (module.exports = {
692
+ CANISTER_NAME_PATTERN,
693
+ CODEGEN_TARGETS,
694
+ CodegenConfigError,
695
+ REACTOR_CLASS_NAMES,
696
+ assertContainedPath,
697
+ assertOneOf,
698
+ assertSafeCanisterConfig,
699
+ assertSafeCanisterName,
700
+ assertSafeModuleSpecifier,
984
701
  declarationsExist,
985
- extractMethods,
986
702
  generateClientFile,
987
- generateCodecDeclarations,
988
703
  generateDeclarations,
989
704
  generateReactorEntryFile,
990
705
  generateReactorFile,
991
706
  getReactorName,
992
707
  getServiceTypeName,
993
- parseDIDFile,
708
+ resolveContainedOutDir,
709
+ resolveDeclarationsBaseName,
994
710
  runCanisterPipeline,
995
711
  toPascalCase
996
712
  });