@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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/pipeline.ts
2
- import fs2 from "fs";
3
- import path3 from "path";
2
+ import fs3 from "fs";
3
+ import path4 from "path";
4
4
 
5
5
  // src/generators/declarations.ts
6
6
  import { didToJs, didToTs } from "@ic-reactor/parser";
@@ -76,6 +76,9 @@ function getReactorName(canisterName) {
76
76
  function getServiceTypeName(canisterName) {
77
77
  return `${toPascalCase(canisterName)}Service`;
78
78
  }
79
+ function getHookPrefix(canisterName) {
80
+ return toPascalCase(canisterName);
81
+ }
79
82
 
80
83
  // src/generators/reactor.ts
81
84
  function getReactorClassImportSource(reactorClass, runtimeTarget) {
@@ -87,6 +90,10 @@ function getReactorClassImportSource(reactorClass, runtimeTarget) {
87
90
  case "CandidDisplayReactor":
88
91
  case "MetadataDisplayReactor":
89
92
  return "@ic-reactor/candid";
93
+ default:
94
+ throw new Error(
95
+ `Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
96
+ );
90
97
  }
91
98
  }
92
99
  function generateReactorFile(options) {
@@ -120,9 +127,9 @@ export const {
120
127
  useActorMethod: use${pascalName}Method,
121
128
  } = createActorHooks(${reactorName})
122
129
  ` : "";
123
- return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from "${reactorImportSource}"
124
- import { clientManager } from "${clientManagerPath}"
125
- import { idlFactory, type _SERVICE } from "${declarationsPath}"
130
+ return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
131
+ import { clientManager } from ${JSON.stringify(clientManagerPath)}
132
+ import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
126
133
 
127
134
  export type ${serviceName} = _SERVICE
128
135
 
@@ -131,11 +138,14 @@ export type ${serviceName} = _SERVICE
131
138
  *
132
139
  * Auto-generated by @ic-reactor/codegen \u2014 do not edit.
133
140
  * This file is overwritten whenever generation runs.
141
+ *
142
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
143
+ * factory modules). Avoid editing this managed file directly.
134
144
  */
135
145
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
136
146
  clientManager,
137
147
  idlFactory,
138
- ${canisterIdLine} name: "${canisterName}",
148
+ ${canisterIdLine} name: ${JSON.stringify(canisterName)},
139
149
  })${hookExports || "\n"}`;
140
150
  }
141
151
  function generateReactorEntryFile() {
@@ -144,11 +154,176 @@ function generateReactorEntryFile() {
144
154
  *
145
155
  * Created once by @ic-reactor/codegen and safe to customize.
146
156
  * Keep the re-export below if you want generated exports and types to stay in sync.
157
+ *
158
+ * Recommended customization points:
159
+ * - define reusable query/mutation factories
160
+ * - add app-specific hooks and cache invalidation wiring
161
+ * - compose generated APIs into route loaders/actions
162
+ *
163
+ * Do not edit \`index.generated.ts\`; it is regenerated on each codegen run.
164
+ * AI guide: https://ic-reactor.b3pay.net/llms-full.txt
165
+ * Skill install: npx skills add B3Pay/ic-reactor-skills --full-depth --skill ic-reactor-hooks
147
166
  */
148
167
  export * from "./index.generated"
149
168
  `;
150
169
  }
151
170
 
171
+ // src/validate.ts
172
+ import fs2 from "fs";
173
+ import path3 from "path";
174
+ var CodegenConfigError = class extends Error {
175
+ name = "CodegenConfigError";
176
+ constructor(message) {
177
+ super(message);
178
+ }
179
+ };
180
+ var CANISTER_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
181
+ var IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
182
+ var MAX_CANISTER_NAME_LENGTH = 64;
183
+ var HAS_URI_SCHEME = /^(?:[A-Za-z][A-Za-z0-9+.-]*:|\/\/)/;
184
+ var BREAKS_OUT_OF_LITERAL = /["'`\\\u0000-\u001f\u2028\u2029]/;
185
+ function assertSafeCanisterName(name) {
186
+ if (typeof name !== "string" || name.length === 0) {
187
+ throw new CodegenConfigError(
188
+ `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).`
189
+ );
190
+ }
191
+ if (name.length > MAX_CANISTER_NAME_LENGTH) {
192
+ throw new CodegenConfigError(
193
+ `Invalid canister name ${JSON.stringify(name)}: must be at most ${MAX_CANISTER_NAME_LENGTH} characters.`
194
+ );
195
+ }
196
+ if (!CANISTER_NAME_PATTERN.test(name)) {
197
+ throw new CodegenConfigError(
198
+ `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.`
199
+ );
200
+ }
201
+ if (name === "." || name === "..") {
202
+ throw new CodegenConfigError(
203
+ `Invalid canister name ${JSON.stringify(name)}: refers to a directory, not a canister.`
204
+ );
205
+ }
206
+ if (toPascalCase(name).length === 0) {
207
+ throw new CodegenConfigError(
208
+ `Invalid canister name ${JSON.stringify(name)}: contains no letters or digits, so it collapses to an empty identifier in generated code.`
209
+ );
210
+ }
211
+ const derived = [
212
+ ["reactor constant", getReactorName(name)],
213
+ ["service type", getServiceTypeName(name)],
214
+ ["hook name", `use${getHookPrefix(name)}Query`]
215
+ ];
216
+ for (const [what, identifier] of derived) {
217
+ if (!IDENTIFIER_PATTERN.test(identifier)) {
218
+ throw new CodegenConfigError(
219
+ `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.`
220
+ );
221
+ }
222
+ }
223
+ }
224
+ function assertSafeModuleSpecifier(label, specifier) {
225
+ if (typeof specifier !== "string" || specifier.length === 0) {
226
+ throw new CodegenConfigError(
227
+ `Invalid ${label}: expected a non-empty string, received ${specifier === void 0 ? "undefined" : JSON.stringify(specifier)}.`
228
+ );
229
+ }
230
+ if (BREAKS_OUT_OF_LITERAL.test(specifier)) {
231
+ throw new CodegenConfigError(
232
+ `Invalid ${label} ${JSON.stringify(specifier)}: must not contain quotes, backslashes or control characters.`
233
+ );
234
+ }
235
+ if (/^\s|\s$/.test(specifier)) {
236
+ throw new CodegenConfigError(
237
+ `Invalid ${label} ${JSON.stringify(specifier)}: must not begin or end with whitespace.`
238
+ );
239
+ }
240
+ if (HAS_URI_SCHEME.test(specifier.trim())) {
241
+ throw new CodegenConfigError(
242
+ `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.`
243
+ );
244
+ }
245
+ }
246
+ var REACTOR_CLASS_NAMES = [
247
+ "Reactor",
248
+ "DisplayReactor",
249
+ "CandidReactor",
250
+ "CandidDisplayReactor",
251
+ "MetadataDisplayReactor"
252
+ ];
253
+ var CODEGEN_TARGETS = ["react", "core"];
254
+ function assertOneOf(label, value, allowed) {
255
+ if (typeof value !== "string" || !allowed.includes(value)) {
256
+ throw new CodegenConfigError(
257
+ `Invalid ${label} ${JSON.stringify(value)}: must be one of ${allowed.map((a) => JSON.stringify(a)).join(", ")}.`
258
+ );
259
+ }
260
+ }
261
+ function realpathAllowingMissing(target) {
262
+ let current = path3.resolve(target);
263
+ const missing = [];
264
+ for (; ; ) {
265
+ try {
266
+ return path3.join(fs2.realpathSync(current), ...missing);
267
+ } catch {
268
+ const parent = path3.dirname(current);
269
+ if (parent === current) return path3.resolve(target);
270
+ missing.unshift(path3.basename(current));
271
+ current = parent;
272
+ }
273
+ }
274
+ }
275
+ function assertContainedPath(label, resolved, projectRoot, original = resolved) {
276
+ const relative = path3.relative(
277
+ realpathAllowingMissing(projectRoot),
278
+ realpathAllowingMissing(resolved)
279
+ );
280
+ const [firstSegment] = relative.split(/[\\/]/);
281
+ if (firstSegment === ".." || path3.isAbsolute(relative)) {
282
+ throw new CodegenConfigError(
283
+ `Invalid ${label} ${JSON.stringify(original)}: resolves to ${JSON.stringify(resolved)}, which is outside the project root ${JSON.stringify(path3.resolve(projectRoot))}. Generated output must stay inside the project \u2014 generated directories are deleted and rewritten on every run.`
284
+ );
285
+ }
286
+ }
287
+ function resolveContainedOutDir(label, outDir, projectRoot) {
288
+ if (typeof outDir !== "string" || outDir.length === 0) {
289
+ throw new CodegenConfigError(
290
+ `Invalid ${label}: expected a non-empty string, received ${outDir === void 0 ? "undefined" : JSON.stringify(outDir)}.`
291
+ );
292
+ }
293
+ const resolved = path3.isAbsolute(outDir) ? path3.resolve(outDir) : path3.resolve(projectRoot, outDir);
294
+ assertContainedPath(label, resolved, projectRoot, outDir);
295
+ return resolved;
296
+ }
297
+ function assertSafeCanisterConfig(options) {
298
+ const {
299
+ name,
300
+ canisterOutDir,
301
+ globalOutDir,
302
+ clientManagerPath,
303
+ projectRoot,
304
+ mode,
305
+ target
306
+ } = options;
307
+ assertSafeCanisterName(name);
308
+ assertSafeModuleSpecifier("clientManagerPath", clientManagerPath);
309
+ if (mode != null) assertOneOf("mode", mode, REACTOR_CLASS_NAMES);
310
+ if (target != null) assertOneOf("target", target, CODEGEN_TARGETS);
311
+ const outDir = canisterOutDir != null ? resolveContainedOutDir(
312
+ `outDir for canister ${JSON.stringify(name)}`,
313
+ canisterOutDir,
314
+ projectRoot
315
+ ) : path3.join(
316
+ resolveContainedOutDir("outDir", globalOutDir, projectRoot),
317
+ name
318
+ );
319
+ assertContainedPath(
320
+ `output directory for canister ${JSON.stringify(name)}`,
321
+ outDir,
322
+ projectRoot
323
+ );
324
+ return { name, outDir, clientManagerPath };
325
+ }
326
+
152
327
  // src/pipeline.ts
153
328
  function resolveReactorClass(canisterConfig) {
154
329
  return canisterConfig.mode ?? "DisplayReactor";
@@ -174,8 +349,30 @@ async function runCanisterPipeline(options) {
174
349
  } = options;
175
350
  const { name, didFile, clientManagerPath } = canisterConfig;
176
351
  const files = [];
177
- const resolvedDidFile = path3.isAbsolute(didFile) ? didFile : path3.resolve(projectRoot, didFile);
178
- if (!fs2.existsSync(resolvedDidFile)) {
352
+ let validated;
353
+ try {
354
+ validated = assertSafeCanisterConfig({
355
+ name,
356
+ canisterOutDir: canisterConfig.outDir,
357
+ globalOutDir: globalConfig.outDir,
358
+ clientManagerPath: clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients",
359
+ projectRoot,
360
+ mode: canisterConfig.mode,
361
+ target: canisterConfig.target ?? globalConfig.target
362
+ });
363
+ } catch (err) {
364
+ if (err instanceof CodegenConfigError) {
365
+ return {
366
+ canisterName: typeof name === "string" ? name : String(name),
367
+ success: false,
368
+ files,
369
+ error: err.message
370
+ };
371
+ }
372
+ throw err;
373
+ }
374
+ const resolvedDidFile = path4.isAbsolute(didFile) ? didFile : path4.resolve(projectRoot, didFile);
375
+ if (!fs3.existsSync(resolvedDidFile)) {
179
376
  return {
180
377
  canisterName: name,
181
378
  success: false,
@@ -183,8 +380,8 @@ async function runCanisterPipeline(options) {
183
380
  error: `DID file not found: ${resolvedDidFile}`
184
381
  };
185
382
  }
186
- const canisterOutDir = canisterConfig.outDir != null ? path3.isAbsolute(canisterConfig.outDir) ? canisterConfig.outDir : path3.resolve(projectRoot, canisterConfig.outDir) : path3.resolve(projectRoot, globalConfig.outDir, name);
187
- const resolvedClientManagerPath = clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients";
383
+ const canisterOutDir = validated.outDir;
384
+ const resolvedClientManagerPath = validated.clientManagerPath;
188
385
  try {
189
386
  const declResult = await generateDeclarations({
190
387
  didFile: resolvedDidFile,
@@ -215,8 +412,8 @@ async function runCanisterPipeline(options) {
215
412
  files
216
413
  };
217
414
  }
218
- const reactorPath = path3.join(canisterOutDir, "index.generated.ts");
219
- const entryPath = path3.join(canisterOutDir, "index.ts");
415
+ const reactorPath = path4.join(canisterOutDir, "index.generated.ts");
416
+ const entryPath = path4.join(canisterOutDir, "index.ts");
220
417
  const reactorClass = resolveReactorClass(canisterConfig);
221
418
  const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
222
419
  try {
@@ -229,16 +426,16 @@ async function runCanisterPipeline(options) {
229
426
  reactorClass
230
427
  });
231
428
  const entryContent = generateReactorEntryFile();
232
- fs2.mkdirSync(canisterOutDir, { recursive: true });
233
- fs2.writeFileSync(reactorPath, reactorContent);
429
+ fs3.mkdirSync(canisterOutDir, { recursive: true });
430
+ fs3.writeFileSync(reactorPath, reactorContent);
234
431
  files.push({ success: true, filePath: reactorPath });
235
- if (!fs2.existsSync(entryPath)) {
236
- fs2.writeFileSync(entryPath, entryContent);
432
+ if (!fs3.existsSync(entryPath)) {
433
+ fs3.writeFileSync(entryPath, entryContent);
237
434
  files.push({ success: true, filePath: entryPath });
238
435
  } else {
239
- const existingEntryContent = fs2.readFileSync(entryPath, "utf-8");
436
+ const existingEntryContent = fs3.readFileSync(entryPath, "utf-8");
240
437
  if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
241
- fs2.writeFileSync(entryPath, entryContent);
438
+ fs3.writeFileSync(entryPath, entryContent);
242
439
  files.push({ success: true, filePath: entryPath });
243
440
  } else {
244
441
  files.push({ success: true, filePath: entryPath, skipped: true });
@@ -266,7 +463,7 @@ async function runCanisterPipeline(options) {
266
463
 
267
464
  // src/parser.ts
268
465
  import { didToJs as didToJs2 } from "@ic-reactor/parser";
269
- import fs3 from "fs";
466
+ import fs4 from "fs";
270
467
  function extractMethods(didContent) {
271
468
  try {
272
469
  const jsContent = didToJs2(didContent);
@@ -291,10 +488,10 @@ function extractMethods(didContent) {
291
488
  }
292
489
  }
293
490
  function parseDIDFile(didFilePath) {
294
- if (!fs3.existsSync(didFilePath)) {
491
+ if (!fs4.existsSync(didFilePath)) {
295
492
  throw new Error(`DID file not found: ${didFilePath}`);
296
493
  }
297
- const content = fs3.readFileSync(didFilePath, "utf-8");
494
+ const content = fs4.readFileSync(didFilePath, "utf-8");
298
495
  return extractMethods(content);
299
496
  }
300
497
 
@@ -315,11 +512,19 @@ ${queryClientImport}
315
512
  */
316
513
  export const clientManager = new ClientManager({
317
514
  queryClient,
318
- withCanisterEnv: true,
319
515
  })
320
516
  `;
321
517
  }
322
518
  export {
519
+ CANISTER_NAME_PATTERN,
520
+ CODEGEN_TARGETS,
521
+ CodegenConfigError,
522
+ REACTOR_CLASS_NAMES,
523
+ assertContainedPath,
524
+ assertOneOf,
525
+ assertSafeCanisterConfig,
526
+ assertSafeCanisterName,
527
+ assertSafeModuleSpecifier,
323
528
  declarationsExist,
324
529
  extractMethods,
325
530
  generateClientFile,
@@ -329,6 +534,7 @@ export {
329
534
  getReactorName,
330
535
  getServiceTypeName,
331
536
  parseDIDFile,
537
+ resolveContainedOutDir,
332
538
  runCanisterPipeline,
333
539
  toPascalCase
334
540
  };
package/llms.txt ADDED
@@ -0,0 +1,28 @@
1
+ # @ic-reactor/codegen — AI Quick Guide
2
+
3
+ Shared generation pipeline used by `@ic-reactor/cli` and
4
+ `@ic-reactor/vite-plugin`.
5
+
6
+ ## Install
7
+
8
+ Most apps should install CLI or Vite plugin instead of this package directly.
9
+
10
+ ```bash
11
+ pnpm add -D @ic-reactor/codegen
12
+ ```
13
+
14
+ ## Main Use Case
15
+
16
+ Use `runCanisterPipeline(...)` when you need programmatic generation in custom
17
+ tooling/automation.
18
+
19
+ ## Generated File Model
20
+
21
+ - `index.generated.ts` is managed output
22
+ - `index.ts` is a stable wrapper
23
+ - app-specific logic should live in wrappers or adjacent modules
24
+
25
+ ## Full References
26
+
27
+ - Full AI guide: https://ic-reactor.b3pay.net/llms-full.txt
28
+ - Codegen docs: https://ic-reactor.b3pay.net/v3/packages/codegen
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/codegen",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
4
4
  "description": "Shared code generation utilities for IC Reactor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -8,14 +8,22 @@
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js",
13
- "require": "./dist/index.cjs"
14
- }
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "./package.json": "./package.json"
15
21
  },
16
22
  "files": [
17
23
  "dist",
18
- "src"
24
+ "src",
25
+ "README.md",
26
+ "llms.txt"
19
27
  ],
20
28
  "keywords": [
21
29
  "internet-computer",
@@ -32,21 +40,22 @@
32
40
  "url": "git+https://github.com/B3Pay/ic-reactor.git",
33
41
  "directory": "packages/codegen"
34
42
  },
43
+ "homepage": "https://ic-reactor.b3pay.net/v3/packages/codegen",
35
44
  "dependencies": {
36
45
  "change-case": "^5.4.4",
37
- "@ic-reactor/parser": "0.4.6"
46
+ "@ic-reactor/parser": "0.4.7"
38
47
  },
39
48
  "devDependencies": {
40
- "@types/node": "^25.5.2",
49
+ "@types/node": "^26.1.1",
41
50
  "tsup": "^8.5.1",
42
- "typescript": "^5.9.3",
43
- "vitest": "^4.1.2"
51
+ "typescript": "^6.0.3",
52
+ "vitest": "^4.1.10"
44
53
  },
45
54
  "scripts": {
46
- "build": "tsup src/index.ts --format esm,cjs --dts --tsconfig tsconfig.json",
55
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean --tsconfig tsconfig.json",
47
56
  "dev": "tsup src/index.ts --format esm,cjs --dts --watch --tsconfig tsconfig.json",
48
57
  "test": "vitest run",
49
58
  "test:watch": "vitest",
50
- "typecheck": "tsc --noEmit"
59
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
51
60
  }
52
61
  }
@@ -13,6 +13,9 @@ export type BackendService = _SERVICE
13
13
  *
14
14
  * Auto-generated by @ic-reactor/codegen — do not edit.
15
15
  * This file is overwritten whenever generation runs.
16
+ *
17
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
18
+ * factory modules). Avoid editing this managed file directly.
16
19
  */
17
20
  export const backendReactor = new DisplayReactor<BackendService>({
18
21
  clientManager,
@@ -44,6 +47,9 @@ export type WorkflowEngineService = _SERVICE
44
47
  *
45
48
  * Auto-generated by @ic-reactor/codegen — do not edit.
46
49
  * This file is overwritten whenever generation runs.
50
+ *
51
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
52
+ * factory modules). Avoid editing this managed file directly.
47
53
  */
48
54
  export const workflowEngineReactor = new Reactor<WorkflowEngineService>({
49
55
  clientManager,
@@ -75,6 +81,9 @@ export type LedgerService = _SERVICE
75
81
  *
76
82
  * Auto-generated by @ic-reactor/codegen — do not edit.
77
83
  * This file is overwritten whenever generation runs.
84
+ *
85
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
86
+ * factory modules). Avoid editing this managed file directly.
78
87
  */
79
88
  export const ledgerReactor = new MetadataDisplayReactor<LedgerService>({
80
89
  clientManager,
@@ -105,6 +114,9 @@ export type BackendService = _SERVICE
105
114
  *
106
115
  * Auto-generated by @ic-reactor/codegen — do not edit.
107
116
  * This file is overwritten whenever generation runs.
117
+ *
118
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
119
+ * factory modules). Avoid editing this managed file directly.
108
120
  */
109
121
  export const backendReactor = new DisplayReactor<BackendService>({
110
122
  clientManager,
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2
2
  import fs from "node:fs"
3
3
  import path from "node:path"
4
- import { generateDeclarations } from "./generators"
4
+ import { generateDeclarations } from "./generators/index.js"
5
5
 
6
6
  describe("Bindgen", () => {
7
7
  const mockDidFile = "mock/test.did"
@@ -43,7 +43,6 @@ ${queryClientImport}
43
43
  */
44
44
  export const clientManager = new ClientManager({
45
45
  queryClient,
46
- withCanisterEnv: true,
47
46
  })
48
47
  `
49
48
  }
@@ -60,6 +60,14 @@ function getReactorClassImportSource(
60
60
  case "CandidDisplayReactor":
61
61
  case "MetadataDisplayReactor":
62
62
  return "@ic-reactor/candid"
63
+ default:
64
+ // The pipeline validates `mode` against a closed set before we are
65
+ // reached. Failing closed here means a caller that skips validation gets
66
+ // an error rather than an unknown class name interpolated into source.
67
+ throw new Error(
68
+ `Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: ` +
69
+ `Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
70
+ )
63
71
  }
64
72
  }
65
73
 
@@ -105,9 +113,12 @@ export const {
105
113
  `
106
114
  : ""
107
115
 
108
- return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from "${reactorImportSource}"
109
- import { clientManager } from "${clientManagerPath}"
110
- import { idlFactory, type _SERVICE } from "${declarationsPath}"
116
+ // Every interpolation into emitted source is JSON.stringify'd. The pipeline
117
+ // validates these values before we are called; quoting them here as well
118
+ // means a future caller that skips validation cannot inject source text.
119
+ return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
120
+ import { clientManager } from ${JSON.stringify(clientManagerPath)}
121
+ import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
111
122
 
112
123
  export type ${serviceName} = _SERVICE
113
124
 
@@ -116,11 +127,14 @@ export type ${serviceName} = _SERVICE
116
127
  *
117
128
  * Auto-generated by @ic-reactor/codegen — do not edit.
118
129
  * This file is overwritten whenever generation runs.
130
+ *
131
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
132
+ * factory modules). Avoid editing this managed file directly.
119
133
  */
120
134
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
121
135
  clientManager,
122
136
  idlFactory,
123
- ${canisterIdLine} name: "${canisterName}",
137
+ ${canisterIdLine} name: ${JSON.stringify(canisterName)},
124
138
  })${hookExports || "\n"}`
125
139
  }
126
140
 
@@ -133,6 +147,15 @@ export function generateReactorEntryFile(): string {
133
147
  *
134
148
  * Created once by @ic-reactor/codegen and safe to customize.
135
149
  * Keep the re-export below if you want generated exports and types to stay in sync.
150
+ *
151
+ * Recommended customization points:
152
+ * - define reusable query/mutation factories
153
+ * - add app-specific hooks and cache invalidation wiring
154
+ * - compose generated APIs into route loaders/actions
155
+ *
156
+ * Do not edit \`index.generated.ts\`; it is regenerated on each codegen run.
157
+ * AI guide: https://ic-reactor.b3pay.net/llms-full.txt
158
+ * Skill install: npx skills add B3Pay/ic-reactor-skills --full-depth --skill ic-reactor-hooks
136
159
  */
137
160
  export * from "./index.generated"
138
161
  `
package/src/index.ts CHANGED
@@ -21,6 +21,25 @@ export type { PipelineOptions, PipelineResult } from "./pipeline.js"
21
21
  // Utilities
22
22
  export { toPascalCase, getReactorName, getServiceTypeName } from "./naming.js"
23
23
 
24
+ // Config validation (run by the pipeline; exported for callers that want to
25
+ // validate a config before invoking generation)
26
+ export {
27
+ assertSafeCanisterConfig,
28
+ assertSafeCanisterName,
29
+ assertSafeModuleSpecifier,
30
+ assertContainedPath,
31
+ assertOneOf,
32
+ resolveContainedOutDir,
33
+ CodegenConfigError,
34
+ CANISTER_NAME_PATTERN,
35
+ REACTOR_CLASS_NAMES,
36
+ CODEGEN_TARGETS,
37
+ } from "./validate.js"
38
+ export type {
39
+ ValidatedCanisterPaths,
40
+ ValidateCanisterConfigOptions,
41
+ } from "./validate.js"
42
+
24
43
  export { parseDIDFile, extractMethods } from "./parser.js"
25
44
  export type { MethodInfo, MethodType } from "./parser.js"
26
45
 
@@ -4,7 +4,7 @@ import {
4
4
  toCamelCase,
5
5
  getReactorName,
6
6
  getServiceTypeName,
7
- } from "./naming"
7
+ } from "./naming.js"
8
8
 
9
9
  describe("Naming Utilities", () => {
10
10
  describe("Base Conversions", () => {
@@ -2,7 +2,7 @@ import fs from "node:fs"
2
2
  import os from "node:os"
3
3
  import path from "node:path"
4
4
  import { afterEach, describe, expect, it } from "vitest"
5
- import { runCanisterPipeline } from "./pipeline"
5
+ import { runCanisterPipeline } from "./pipeline.js"
6
6
 
7
7
  describe("Codegen pipeline", () => {
8
8
  const tempDirs: string[] = []