@ic-reactor/codegen 0.11.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,16 +54,16 @@ need to skip `index.generated.ts` and `index.ts`.
54
54
 
55
55
  You can also use individual generators if you need more granular control:
56
56
 
57
- - **`generateDeclarations`**: Generates `.js` (factory), `.d.ts` (types), and `.did` copy.
58
- - **`generateReactorFile`**: Generates the managed `index.generated.ts` implementation using either `DisplayReactor` or `Reactor`.
57
+ - **`generateDeclarations`**: Writes `declarations/<did-basename>.js` (factory), `.d.ts` (types), and a `.did` copy. The `declarations/` directory is wiped and recreated on every run.
58
+ - **`generateReactorFile`**: Generates the managed `index.generated.ts` implementation using any `ReactorClassName` — `Reactor`, `DisplayReactor` (default), `CandidReactor`, `CandidDisplayReactor`, or `MetadataDisplayReactor`. With `target: "react"` it also emits the six `createActorHooks` exports (`use<Canister>Query`, `use<Canister>SuspenseQuery`, `use<Canister>InfiniteQuery`, `use<Canister>SuspenseInfiniteQuery`, `use<Canister>Mutation`, `use<Canister>Method`). No `createQuery` / `createMutation` objects are generated.
59
59
  - **`generateReactorEntryFile`**: Generates the stable `index.ts` wrapper that re-exports from `index.generated.ts`.
60
60
  - **`generateClientFile`**: Generates a `ClientManager` boilerplate file that
61
61
  imports `ClientManager` from `@ic-reactor/react`.
62
62
 
63
63
  ## Utilities
64
64
 
65
- - **`parseDIDFile`**: Parses a `.did` file and extracts method signatures.
66
- - **`toPascalCase` / `toCamelCase`**: Naming helpers.
65
+ - **`parseDIDFile` / `extractMethods`**: Parse a `.did` file and extract method signatures.
66
+ - **`toPascalCase` / `getReactorName` / `getServiceTypeName`**: Naming helpers.
67
67
 
68
68
  ## License
69
69
 
package/dist/index.cjs CHANGED
@@ -30,6 +30,15 @@ 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
43
  extractMethods: () => extractMethods,
35
44
  generateClientFile: () => generateClientFile,
@@ -39,14 +48,15 @@ __export(index_exports, {
39
48
  getReactorName: () => getReactorName,
40
49
  getServiceTypeName: () => getServiceTypeName,
41
50
  parseDIDFile: () => parseDIDFile,
51
+ resolveContainedOutDir: () => resolveContainedOutDir,
42
52
  runCanisterPipeline: () => runCanisterPipeline,
43
53
  toPascalCase: () => toPascalCase
44
54
  });
45
55
  module.exports = __toCommonJS(index_exports);
46
56
 
47
57
  // src/pipeline.ts
48
- var import_node_fs2 = __toESM(require("fs"), 1);
49
- var import_node_path3 = __toESM(require("path"), 1);
58
+ var import_node_fs3 = __toESM(require("fs"), 1);
59
+ var import_node_path4 = __toESM(require("path"), 1);
50
60
 
51
61
  // src/generators/declarations.ts
52
62
  var import_parser = require("@ic-reactor/parser");
@@ -122,6 +132,9 @@ function getReactorName(canisterName) {
122
132
  function getServiceTypeName(canisterName) {
123
133
  return `${toPascalCase(canisterName)}Service`;
124
134
  }
135
+ function getHookPrefix(canisterName) {
136
+ return toPascalCase(canisterName);
137
+ }
125
138
 
126
139
  // src/generators/reactor.ts
127
140
  function getReactorClassImportSource(reactorClass, runtimeTarget) {
@@ -133,6 +146,10 @@ function getReactorClassImportSource(reactorClass, runtimeTarget) {
133
146
  case "CandidDisplayReactor":
134
147
  case "MetadataDisplayReactor":
135
148
  return "@ic-reactor/candid";
149
+ default:
150
+ throw new Error(
151
+ `Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
152
+ );
136
153
  }
137
154
  }
138
155
  function generateReactorFile(options) {
@@ -166,9 +183,9 @@ export const {
166
183
  useActorMethod: use${pascalName}Method,
167
184
  } = createActorHooks(${reactorName})
168
185
  ` : "";
169
- return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from "${reactorImportSource}"
170
- import { clientManager } from "${clientManagerPath}"
171
- import { idlFactory, type _SERVICE } from "${declarationsPath}"
186
+ return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
187
+ import { clientManager } from ${JSON.stringify(clientManagerPath)}
188
+ import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
172
189
 
173
190
  export type ${serviceName} = _SERVICE
174
191
 
@@ -177,11 +194,14 @@ export type ${serviceName} = _SERVICE
177
194
  *
178
195
  * Auto-generated by @ic-reactor/codegen \u2014 do not edit.
179
196
  * This file is overwritten whenever generation runs.
197
+ *
198
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
199
+ * factory modules). Avoid editing this managed file directly.
180
200
  */
181
201
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
182
202
  clientManager,
183
203
  idlFactory,
184
- ${canisterIdLine} name: "${canisterName}",
204
+ ${canisterIdLine} name: ${JSON.stringify(canisterName)},
185
205
  })${hookExports || "\n"}`;
186
206
  }
187
207
  function generateReactorEntryFile() {
@@ -190,11 +210,176 @@ function generateReactorEntryFile() {
190
210
  *
191
211
  * Created once by @ic-reactor/codegen and safe to customize.
192
212
  * Keep the re-export below if you want generated exports and types to stay in sync.
213
+ *
214
+ * Recommended customization points:
215
+ * - define reusable query/mutation factories
216
+ * - add app-specific hooks and cache invalidation wiring
217
+ * - compose generated APIs into route loaders/actions
218
+ *
219
+ * Do not edit \`index.generated.ts\`; it is regenerated on each codegen run.
220
+ * AI guide: https://ic-reactor.b3pay.net/llms-full.txt
221
+ * Skill install: npx skills add B3Pay/ic-reactor-skills --full-depth --skill ic-reactor-hooks
193
222
  */
194
223
  export * from "./index.generated"
195
224
  `;
196
225
  }
197
226
 
227
+ // src/validate.ts
228
+ var import_node_fs2 = __toESM(require("fs"), 1);
229
+ var import_node_path3 = __toESM(require("path"), 1);
230
+ var CodegenConfigError = class extends Error {
231
+ name = "CodegenConfigError";
232
+ constructor(message) {
233
+ super(message);
234
+ }
235
+ };
236
+ var CANISTER_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
237
+ var IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
238
+ var MAX_CANISTER_NAME_LENGTH = 64;
239
+ var HAS_URI_SCHEME = /^(?:[A-Za-z][A-Za-z0-9+.-]*:|\/\/)/;
240
+ var BREAKS_OUT_OF_LITERAL = /["'`\\\u0000-\u001f\u2028\u2029]/;
241
+ function assertSafeCanisterName(name) {
242
+ if (typeof name !== "string" || name.length === 0) {
243
+ throw new CodegenConfigError(
244
+ `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).`
245
+ );
246
+ }
247
+ if (name.length > MAX_CANISTER_NAME_LENGTH) {
248
+ throw new CodegenConfigError(
249
+ `Invalid canister name ${JSON.stringify(name)}: must be at most ${MAX_CANISTER_NAME_LENGTH} characters.`
250
+ );
251
+ }
252
+ if (!CANISTER_NAME_PATTERN.test(name)) {
253
+ throw new CodegenConfigError(
254
+ `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.`
255
+ );
256
+ }
257
+ if (name === "." || name === "..") {
258
+ throw new CodegenConfigError(
259
+ `Invalid canister name ${JSON.stringify(name)}: refers to a directory, not a canister.`
260
+ );
261
+ }
262
+ if (toPascalCase(name).length === 0) {
263
+ throw new CodegenConfigError(
264
+ `Invalid canister name ${JSON.stringify(name)}: contains no letters or digits, so it collapses to an empty identifier in generated code.`
265
+ );
266
+ }
267
+ const derived = [
268
+ ["reactor constant", getReactorName(name)],
269
+ ["service type", getServiceTypeName(name)],
270
+ ["hook name", `use${getHookPrefix(name)}Query`]
271
+ ];
272
+ for (const [what, identifier] of derived) {
273
+ if (!IDENTIFIER_PATTERN.test(identifier)) {
274
+ throw new CodegenConfigError(
275
+ `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.`
276
+ );
277
+ }
278
+ }
279
+ }
280
+ function assertSafeModuleSpecifier(label, specifier) {
281
+ if (typeof specifier !== "string" || specifier.length === 0) {
282
+ throw new CodegenConfigError(
283
+ `Invalid ${label}: expected a non-empty string, received ${specifier === void 0 ? "undefined" : JSON.stringify(specifier)}.`
284
+ );
285
+ }
286
+ if (BREAKS_OUT_OF_LITERAL.test(specifier)) {
287
+ throw new CodegenConfigError(
288
+ `Invalid ${label} ${JSON.stringify(specifier)}: must not contain quotes, backslashes or control characters.`
289
+ );
290
+ }
291
+ if (/^\s|\s$/.test(specifier)) {
292
+ throw new CodegenConfigError(
293
+ `Invalid ${label} ${JSON.stringify(specifier)}: must not begin or end with whitespace.`
294
+ );
295
+ }
296
+ if (HAS_URI_SCHEME.test(specifier.trim())) {
297
+ throw new CodegenConfigError(
298
+ `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.`
299
+ );
300
+ }
301
+ }
302
+ var REACTOR_CLASS_NAMES = [
303
+ "Reactor",
304
+ "DisplayReactor",
305
+ "CandidReactor",
306
+ "CandidDisplayReactor",
307
+ "MetadataDisplayReactor"
308
+ ];
309
+ var CODEGEN_TARGETS = ["react", "core"];
310
+ function assertOneOf(label, value, allowed) {
311
+ if (typeof value !== "string" || !allowed.includes(value)) {
312
+ throw new CodegenConfigError(
313
+ `Invalid ${label} ${JSON.stringify(value)}: must be one of ${allowed.map((a) => JSON.stringify(a)).join(", ")}.`
314
+ );
315
+ }
316
+ }
317
+ function realpathAllowingMissing(target) {
318
+ let current = import_node_path3.default.resolve(target);
319
+ const missing = [];
320
+ for (; ; ) {
321
+ try {
322
+ return import_node_path3.default.join(import_node_fs2.default.realpathSync(current), ...missing);
323
+ } catch {
324
+ const parent = import_node_path3.default.dirname(current);
325
+ if (parent === current) return import_node_path3.default.resolve(target);
326
+ missing.unshift(import_node_path3.default.basename(current));
327
+ current = parent;
328
+ }
329
+ }
330
+ }
331
+ function assertContainedPath(label, resolved, projectRoot, original = resolved) {
332
+ const relative = import_node_path3.default.relative(
333
+ realpathAllowingMissing(projectRoot),
334
+ realpathAllowingMissing(resolved)
335
+ );
336
+ const [firstSegment] = relative.split(/[\\/]/);
337
+ if (firstSegment === ".." || import_node_path3.default.isAbsolute(relative)) {
338
+ throw new CodegenConfigError(
339
+ `Invalid ${label} ${JSON.stringify(original)}: resolves to ${JSON.stringify(resolved)}, which is outside the project root ${JSON.stringify(import_node_path3.default.resolve(projectRoot))}. Generated output must stay inside the project \u2014 generated directories are deleted and rewritten on every run.`
340
+ );
341
+ }
342
+ }
343
+ function resolveContainedOutDir(label, outDir, projectRoot) {
344
+ if (typeof outDir !== "string" || outDir.length === 0) {
345
+ throw new CodegenConfigError(
346
+ `Invalid ${label}: expected a non-empty string, received ${outDir === void 0 ? "undefined" : JSON.stringify(outDir)}.`
347
+ );
348
+ }
349
+ const resolved = import_node_path3.default.isAbsolute(outDir) ? import_node_path3.default.resolve(outDir) : import_node_path3.default.resolve(projectRoot, outDir);
350
+ assertContainedPath(label, resolved, projectRoot, outDir);
351
+ return resolved;
352
+ }
353
+ function assertSafeCanisterConfig(options) {
354
+ const {
355
+ name,
356
+ canisterOutDir,
357
+ globalOutDir,
358
+ clientManagerPath,
359
+ projectRoot,
360
+ mode,
361
+ target
362
+ } = options;
363
+ assertSafeCanisterName(name);
364
+ assertSafeModuleSpecifier("clientManagerPath", clientManagerPath);
365
+ if (mode != null) assertOneOf("mode", mode, REACTOR_CLASS_NAMES);
366
+ if (target != null) assertOneOf("target", target, CODEGEN_TARGETS);
367
+ const outDir = canisterOutDir != null ? resolveContainedOutDir(
368
+ `outDir for canister ${JSON.stringify(name)}`,
369
+ canisterOutDir,
370
+ projectRoot
371
+ ) : import_node_path3.default.join(
372
+ resolveContainedOutDir("outDir", globalOutDir, projectRoot),
373
+ name
374
+ );
375
+ assertContainedPath(
376
+ `output directory for canister ${JSON.stringify(name)}`,
377
+ outDir,
378
+ projectRoot
379
+ );
380
+ return { name, outDir, clientManagerPath };
381
+ }
382
+
198
383
  // src/pipeline.ts
199
384
  function resolveReactorClass(canisterConfig) {
200
385
  return canisterConfig.mode ?? "DisplayReactor";
@@ -220,8 +405,30 @@ async function runCanisterPipeline(options) {
220
405
  } = options;
221
406
  const { name, didFile, clientManagerPath } = canisterConfig;
222
407
  const files = [];
223
- const resolvedDidFile = import_node_path3.default.isAbsolute(didFile) ? didFile : import_node_path3.default.resolve(projectRoot, didFile);
224
- if (!import_node_fs2.default.existsSync(resolvedDidFile)) {
408
+ let validated;
409
+ try {
410
+ validated = assertSafeCanisterConfig({
411
+ name,
412
+ canisterOutDir: canisterConfig.outDir,
413
+ globalOutDir: globalConfig.outDir,
414
+ clientManagerPath: clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients",
415
+ projectRoot,
416
+ mode: canisterConfig.mode,
417
+ target: canisterConfig.target ?? globalConfig.target
418
+ });
419
+ } catch (err) {
420
+ if (err instanceof CodegenConfigError) {
421
+ return {
422
+ canisterName: typeof name === "string" ? name : String(name),
423
+ success: false,
424
+ files,
425
+ error: err.message
426
+ };
427
+ }
428
+ throw err;
429
+ }
430
+ const resolvedDidFile = import_node_path4.default.isAbsolute(didFile) ? didFile : import_node_path4.default.resolve(projectRoot, didFile);
431
+ if (!import_node_fs3.default.existsSync(resolvedDidFile)) {
225
432
  return {
226
433
  canisterName: name,
227
434
  success: false,
@@ -229,8 +436,8 @@ async function runCanisterPipeline(options) {
229
436
  error: `DID file not found: ${resolvedDidFile}`
230
437
  };
231
438
  }
232
- 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);
233
- const resolvedClientManagerPath = clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients";
439
+ const canisterOutDir = validated.outDir;
440
+ const resolvedClientManagerPath = validated.clientManagerPath;
234
441
  try {
235
442
  const declResult = await generateDeclarations({
236
443
  didFile: resolvedDidFile,
@@ -261,8 +468,8 @@ async function runCanisterPipeline(options) {
261
468
  files
262
469
  };
263
470
  }
264
- const reactorPath = import_node_path3.default.join(canisterOutDir, "index.generated.ts");
265
- const entryPath = import_node_path3.default.join(canisterOutDir, "index.ts");
471
+ const reactorPath = import_node_path4.default.join(canisterOutDir, "index.generated.ts");
472
+ const entryPath = import_node_path4.default.join(canisterOutDir, "index.ts");
266
473
  const reactorClass = resolveReactorClass(canisterConfig);
267
474
  const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
268
475
  try {
@@ -275,16 +482,16 @@ async function runCanisterPipeline(options) {
275
482
  reactorClass
276
483
  });
277
484
  const entryContent = generateReactorEntryFile();
278
- import_node_fs2.default.mkdirSync(canisterOutDir, { recursive: true });
279
- import_node_fs2.default.writeFileSync(reactorPath, reactorContent);
485
+ import_node_fs3.default.mkdirSync(canisterOutDir, { recursive: true });
486
+ import_node_fs3.default.writeFileSync(reactorPath, reactorContent);
280
487
  files.push({ success: true, filePath: reactorPath });
281
- if (!import_node_fs2.default.existsSync(entryPath)) {
282
- import_node_fs2.default.writeFileSync(entryPath, entryContent);
488
+ if (!import_node_fs3.default.existsSync(entryPath)) {
489
+ import_node_fs3.default.writeFileSync(entryPath, entryContent);
283
490
  files.push({ success: true, filePath: entryPath });
284
491
  } else {
285
- const existingEntryContent = import_node_fs2.default.readFileSync(entryPath, "utf-8");
492
+ const existingEntryContent = import_node_fs3.default.readFileSync(entryPath, "utf-8");
286
493
  if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
287
- import_node_fs2.default.writeFileSync(entryPath, entryContent);
494
+ import_node_fs3.default.writeFileSync(entryPath, entryContent);
288
495
  files.push({ success: true, filePath: entryPath });
289
496
  } else {
290
497
  files.push({ success: true, filePath: entryPath, skipped: true });
@@ -312,7 +519,7 @@ async function runCanisterPipeline(options) {
312
519
 
313
520
  // src/parser.ts
314
521
  var import_parser2 = require("@ic-reactor/parser");
315
- var import_node_fs3 = __toESM(require("fs"), 1);
522
+ var import_node_fs4 = __toESM(require("fs"), 1);
316
523
  function extractMethods(didContent) {
317
524
  try {
318
525
  const jsContent = (0, import_parser2.didToJs)(didContent);
@@ -337,10 +544,10 @@ function extractMethods(didContent) {
337
544
  }
338
545
  }
339
546
  function parseDIDFile(didFilePath) {
340
- if (!import_node_fs3.default.existsSync(didFilePath)) {
547
+ if (!import_node_fs4.default.existsSync(didFilePath)) {
341
548
  throw new Error(`DID file not found: ${didFilePath}`);
342
549
  }
343
- const content = import_node_fs3.default.readFileSync(didFilePath, "utf-8");
550
+ const content = import_node_fs4.default.readFileSync(didFilePath, "utf-8");
344
551
  return extractMethods(content);
345
552
  }
346
553
 
@@ -361,12 +568,20 @@ ${queryClientImport}
361
568
  */
362
569
  export const clientManager = new ClientManager({
363
570
  queryClient,
364
- withCanisterEnv: true,
365
571
  })
366
572
  `;
367
573
  }
368
574
  // Annotate the CommonJS export names for ESM import in node:
369
575
  0 && (module.exports = {
576
+ CANISTER_NAME_PATTERN,
577
+ CODEGEN_TARGETS,
578
+ CodegenConfigError,
579
+ REACTOR_CLASS_NAMES,
580
+ assertContainedPath,
581
+ assertOneOf,
582
+ assertSafeCanisterConfig,
583
+ assertSafeCanisterName,
584
+ assertSafeModuleSpecifier,
370
585
  declarationsExist,
371
586
  extractMethods,
372
587
  generateClientFile,
@@ -376,6 +591,7 @@ export const clientManager = new ClientManager({
376
591
  getReactorName,
377
592
  getServiceTypeName,
378
593
  parseDIDFile,
594
+ resolveContainedOutDir,
379
595
  runCanisterPipeline,
380
596
  toPascalCase
381
597
  });
package/dist/index.d.cts CHANGED
@@ -140,6 +140,116 @@ declare function getReactorName(canisterName: string): string;
140
140
  */
141
141
  declare function getServiceTypeName(canisterName: string): string;
142
142
 
143
+ /**
144
+ * Config validation for the codegen pipeline.
145
+ *
146
+ * Every value validated here arrives from a project's `ic-reactor.json` or from
147
+ * `@ic-reactor/vite-plugin` options — that is, from a file in a repository the
148
+ * user may have merely cloned. The pipeline turns those values into filesystem
149
+ * paths it recursively deletes and into source text it writes into the user's
150
+ * bundle, so they are treated as untrusted input rather than as configuration.
151
+ *
152
+ * Validation lives here, at the pipeline's own entry point, rather than in the
153
+ * CLI's interactive prompt: the prompt only covers one of the three entry paths
154
+ * (hand-edited config and plugin options bypass it entirely).
155
+ */
156
+
157
+ /**
158
+ * Thrown when a canister config would produce an unsafe path or unsafe
159
+ * generated source. Callers convert this into a `PipelineResult` error.
160
+ */
161
+ declare class CodegenConfigError extends Error {
162
+ readonly name = "CodegenConfigError";
163
+ constructor(message: string);
164
+ }
165
+ /**
166
+ * Characters allowed in a canister name.
167
+ *
168
+ * A canister name becomes a directory segment, so this excludes path
169
+ * separators, quotes, whitespace and control characters. It deliberately does
170
+ * NOT require a leading letter: `dfx` places no restriction on canister names,
171
+ * and real projects use `_private` and `my.canister`, both of which derive
172
+ * perfectly good identifiers. Identifier validity is enforced separately, on
173
+ * the *derived* names, by {@link assertSafeCanisterName}.
174
+ */
175
+ declare const CANISTER_NAME_PATTERN: RegExp;
176
+ /**
177
+ * Assert that a canister name is safe to use as a path segment and as the stem
178
+ * of a generated identifier.
179
+ *
180
+ * Rejecting a leading digit here is what stops `2048_game` from reaching the
181
+ * generator, where it would become `export const 2048GameReactor` — invalid
182
+ * TypeScript emitted with a success status.
183
+ */
184
+ declare function assertSafeCanisterName(name: unknown): asserts name is string;
185
+ /**
186
+ * Assert that a module specifier we interpolate into an `import` statement is a
187
+ * relative path or a bare package name — never a URL.
188
+ *
189
+ * A `https://…` specifier here would make the generated module pull code from a
190
+ * remote host at import time, inside the user's own bundle.
191
+ */
192
+ declare function assertSafeModuleSpecifier(label: string, specifier: unknown): asserts specifier is string;
193
+ /** Reactor classes the generator knows how to emit an import for. */
194
+ declare const REACTOR_CLASS_NAMES: readonly ReactorClassName[];
195
+ /** Runtime targets the generator knows how to emit. */
196
+ declare const CODEGEN_TARGETS: readonly CodegenTarget[];
197
+ /**
198
+ * Assert a config value is one of a closed set.
199
+ *
200
+ * `mode` and `target` are interpolated into emitted source as bare identifiers
201
+ * and module specifiers, so an unrecognized value is not merely unsupported —
202
+ * it is injected.
203
+ */
204
+ declare function assertOneOf<T extends string>(label: string, value: unknown, allowed: readonly T[]): asserts value is T;
205
+ /**
206
+ * Assert that an already-resolved absolute path lies inside `projectRoot`.
207
+ *
208
+ * Containment is decided on the real on-disk locations: `path.resolve` does not
209
+ * follow symlinks, so a purely lexical comparison can be walked out of via a
210
+ * symlink placed inside the project root.
211
+ *
212
+ * @param original - the path as the user wrote it, for the error message
213
+ */
214
+ declare function assertContainedPath(label: string, resolved: string, projectRoot: string, original?: string): void;
215
+ /**
216
+ * Resolve `outDir` against `projectRoot` and assert the result stays inside it.
217
+ *
218
+ * The pipeline recursively deletes `<outDir>/declarations` before every
219
+ * generation, so an `outDir` that escapes the project root turns a config file
220
+ * into an arbitrary-directory delete.
221
+ */
222
+ declare function resolveContainedOutDir(label: string, outDir: unknown, projectRoot: string): string;
223
+ interface ValidatedCanisterPaths {
224
+ /** The validated canister name. */
225
+ name: string;
226
+ /** Absolute output directory, guaranteed to be inside `projectRoot`. */
227
+ outDir: string;
228
+ /** Validated module specifier for the client manager import. */
229
+ clientManagerPath: string;
230
+ }
231
+ interface ValidateCanisterConfigOptions {
232
+ name: unknown;
233
+ /** Per-canister `outDir`, if the config sets one. */
234
+ canisterOutDir?: unknown;
235
+ /** Global `outDir`; the canister name is appended to it when used. */
236
+ globalOutDir: unknown;
237
+ clientManagerPath: unknown;
238
+ projectRoot: string;
239
+ /** Resolved reactor class (`canisterConfig.mode`), if the config sets one. */
240
+ mode?: unknown;
241
+ /** Resolved runtime target (`canisterConfig.target` / global `target`). */
242
+ target?: unknown;
243
+ }
244
+ /**
245
+ * Validate one canister's config and return the resolved, contained paths the
246
+ * pipeline should use.
247
+ *
248
+ * Call this before any filesystem work: it is the single choke point that all
249
+ * three entry paths (CLI, vite plugin, direct API) share.
250
+ */
251
+ declare function assertSafeCanisterConfig(options: ValidateCanisterConfigOptions): ValidatedCanisterPaths;
252
+
143
253
  /**
144
254
  * Candid Parser Utilities
145
255
  *
@@ -283,4 +393,4 @@ interface ClientGeneratorOptions {
283
393
  */
284
394
  declare function generateClientFile(options?: ClientGeneratorOptions): string;
285
395
 
286
- export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorClassName, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
396
+ export { CANISTER_NAME_PATTERN, CODEGEN_TARGETS, type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, CodegenConfigError, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, REACTOR_CLASS_NAMES, type ReactorClassName, type ReactorGeneratorOptions, type ValidateCanisterConfigOptions, type ValidatedCanisterPaths, assertContainedPath, assertOneOf, assertSafeCanisterConfig, assertSafeCanisterName, assertSafeModuleSpecifier, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, resolveContainedOutDir, runCanisterPipeline, toPascalCase };
package/dist/index.d.ts CHANGED
@@ -140,6 +140,116 @@ declare function getReactorName(canisterName: string): string;
140
140
  */
141
141
  declare function getServiceTypeName(canisterName: string): string;
142
142
 
143
+ /**
144
+ * Config validation for the codegen pipeline.
145
+ *
146
+ * Every value validated here arrives from a project's `ic-reactor.json` or from
147
+ * `@ic-reactor/vite-plugin` options — that is, from a file in a repository the
148
+ * user may have merely cloned. The pipeline turns those values into filesystem
149
+ * paths it recursively deletes and into source text it writes into the user's
150
+ * bundle, so they are treated as untrusted input rather than as configuration.
151
+ *
152
+ * Validation lives here, at the pipeline's own entry point, rather than in the
153
+ * CLI's interactive prompt: the prompt only covers one of the three entry paths
154
+ * (hand-edited config and plugin options bypass it entirely).
155
+ */
156
+
157
+ /**
158
+ * Thrown when a canister config would produce an unsafe path or unsafe
159
+ * generated source. Callers convert this into a `PipelineResult` error.
160
+ */
161
+ declare class CodegenConfigError extends Error {
162
+ readonly name = "CodegenConfigError";
163
+ constructor(message: string);
164
+ }
165
+ /**
166
+ * Characters allowed in a canister name.
167
+ *
168
+ * A canister name becomes a directory segment, so this excludes path
169
+ * separators, quotes, whitespace and control characters. It deliberately does
170
+ * NOT require a leading letter: `dfx` places no restriction on canister names,
171
+ * and real projects use `_private` and `my.canister`, both of which derive
172
+ * perfectly good identifiers. Identifier validity is enforced separately, on
173
+ * the *derived* names, by {@link assertSafeCanisterName}.
174
+ */
175
+ declare const CANISTER_NAME_PATTERN: RegExp;
176
+ /**
177
+ * Assert that a canister name is safe to use as a path segment and as the stem
178
+ * of a generated identifier.
179
+ *
180
+ * Rejecting a leading digit here is what stops `2048_game` from reaching the
181
+ * generator, where it would become `export const 2048GameReactor` — invalid
182
+ * TypeScript emitted with a success status.
183
+ */
184
+ declare function assertSafeCanisterName(name: unknown): asserts name is string;
185
+ /**
186
+ * Assert that a module specifier we interpolate into an `import` statement is a
187
+ * relative path or a bare package name — never a URL.
188
+ *
189
+ * A `https://…` specifier here would make the generated module pull code from a
190
+ * remote host at import time, inside the user's own bundle.
191
+ */
192
+ declare function assertSafeModuleSpecifier(label: string, specifier: unknown): asserts specifier is string;
193
+ /** Reactor classes the generator knows how to emit an import for. */
194
+ declare const REACTOR_CLASS_NAMES: readonly ReactorClassName[];
195
+ /** Runtime targets the generator knows how to emit. */
196
+ declare const CODEGEN_TARGETS: readonly CodegenTarget[];
197
+ /**
198
+ * Assert a config value is one of a closed set.
199
+ *
200
+ * `mode` and `target` are interpolated into emitted source as bare identifiers
201
+ * and module specifiers, so an unrecognized value is not merely unsupported —
202
+ * it is injected.
203
+ */
204
+ declare function assertOneOf<T extends string>(label: string, value: unknown, allowed: readonly T[]): asserts value is T;
205
+ /**
206
+ * Assert that an already-resolved absolute path lies inside `projectRoot`.
207
+ *
208
+ * Containment is decided on the real on-disk locations: `path.resolve` does not
209
+ * follow symlinks, so a purely lexical comparison can be walked out of via a
210
+ * symlink placed inside the project root.
211
+ *
212
+ * @param original - the path as the user wrote it, for the error message
213
+ */
214
+ declare function assertContainedPath(label: string, resolved: string, projectRoot: string, original?: string): void;
215
+ /**
216
+ * Resolve `outDir` against `projectRoot` and assert the result stays inside it.
217
+ *
218
+ * The pipeline recursively deletes `<outDir>/declarations` before every
219
+ * generation, so an `outDir` that escapes the project root turns a config file
220
+ * into an arbitrary-directory delete.
221
+ */
222
+ declare function resolveContainedOutDir(label: string, outDir: unknown, projectRoot: string): string;
223
+ interface ValidatedCanisterPaths {
224
+ /** The validated canister name. */
225
+ name: string;
226
+ /** Absolute output directory, guaranteed to be inside `projectRoot`. */
227
+ outDir: string;
228
+ /** Validated module specifier for the client manager import. */
229
+ clientManagerPath: string;
230
+ }
231
+ interface ValidateCanisterConfigOptions {
232
+ name: unknown;
233
+ /** Per-canister `outDir`, if the config sets one. */
234
+ canisterOutDir?: unknown;
235
+ /** Global `outDir`; the canister name is appended to it when used. */
236
+ globalOutDir: unknown;
237
+ clientManagerPath: unknown;
238
+ projectRoot: string;
239
+ /** Resolved reactor class (`canisterConfig.mode`), if the config sets one. */
240
+ mode?: unknown;
241
+ /** Resolved runtime target (`canisterConfig.target` / global `target`). */
242
+ target?: unknown;
243
+ }
244
+ /**
245
+ * Validate one canister's config and return the resolved, contained paths the
246
+ * pipeline should use.
247
+ *
248
+ * Call this before any filesystem work: it is the single choke point that all
249
+ * three entry paths (CLI, vite plugin, direct API) share.
250
+ */
251
+ declare function assertSafeCanisterConfig(options: ValidateCanisterConfigOptions): ValidatedCanisterPaths;
252
+
143
253
  /**
144
254
  * Candid Parser Utilities
145
255
  *
@@ -283,4 +393,4 @@ interface ClientGeneratorOptions {
283
393
  */
284
394
  declare function generateClientFile(options?: ClientGeneratorOptions): string;
285
395
 
286
- export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorClassName, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
396
+ export { CANISTER_NAME_PATTERN, CODEGEN_TARGETS, type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, CodegenConfigError, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, REACTOR_CLASS_NAMES, type ReactorClassName, type ReactorGeneratorOptions, type ValidateCanisterConfigOptions, type ValidatedCanisterPaths, assertContainedPath, assertOneOf, assertSafeCanisterConfig, assertSafeCanisterName, assertSafeModuleSpecifier, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, resolveContainedOutDir, runCanisterPipeline, toPascalCase };