@ic-reactor/codegen 0.12.0 → 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.d.cts CHANGED
@@ -1,6 +1,3 @@
1
- export { generateCodecDeclarations } from './renderer.cjs';
2
- import '@ic-reactor/parser';
3
-
4
1
  /**
5
2
  * @ic-reactor/codegen — Core Types
6
3
  *
@@ -143,6 +140,116 @@ declare function getReactorName(canisterName: string): string;
143
140
  */
144
141
  declare function getServiceTypeName(canisterName: string): string;
145
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
+
146
253
  /**
147
254
  * Candid Parser Utilities
148
255
  *
@@ -286,4 +393,4 @@ interface ClientGeneratorOptions {
286
393
  */
287
394
  declare function generateClientFile(options?: ClientGeneratorOptions): string;
288
395
 
289
- 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
@@ -1,6 +1,3 @@
1
- export { generateCodecDeclarations } from './renderer.js';
2
- import '@ic-reactor/parser';
3
-
4
1
  /**
5
2
  * @ic-reactor/codegen — Core Types
6
3
  *
@@ -143,6 +140,116 @@ declare function getReactorName(canisterName: string): string;
143
140
  */
144
141
  declare function getServiceTypeName(canisterName: string): string;
145
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
+
146
253
  /**
147
254
  * Candid Parser Utilities
148
255
  *
@@ -286,4 +393,4 @@ interface ClientGeneratorOptions {
286
393
  */
287
394
  declare function generateClientFile(options?: ClientGeneratorOptions): string;
288
395
 
289
- 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.js CHANGED
@@ -1,10 +1,6 @@
1
- import {
2
- generateCodecDeclarations
3
- } from "./chunk-VBCR5IVT.js";
4
-
5
1
  // src/pipeline.ts
6
- import fs2 from "fs";
7
- import path3 from "path";
2
+ import fs3 from "fs";
3
+ import path4 from "path";
8
4
 
9
5
  // src/generators/declarations.ts
10
6
  import { didToJs, didToTs } from "@ic-reactor/parser";
@@ -80,6 +76,9 @@ function getReactorName(canisterName) {
80
76
  function getServiceTypeName(canisterName) {
81
77
  return `${toPascalCase(canisterName)}Service`;
82
78
  }
79
+ function getHookPrefix(canisterName) {
80
+ return toPascalCase(canisterName);
81
+ }
83
82
 
84
83
  // src/generators/reactor.ts
85
84
  function getReactorClassImportSource(reactorClass, runtimeTarget) {
@@ -91,6 +90,10 @@ function getReactorClassImportSource(reactorClass, runtimeTarget) {
91
90
  case "CandidDisplayReactor":
92
91
  case "MetadataDisplayReactor":
93
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
+ );
94
97
  }
95
98
  }
96
99
  function generateReactorFile(options) {
@@ -124,9 +127,9 @@ export const {
124
127
  useActorMethod: use${pascalName}Method,
125
128
  } = createActorHooks(${reactorName})
126
129
  ` : "";
127
- return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from "${reactorImportSource}"
128
- import { clientManager } from "${clientManagerPath}"
129
- import { idlFactory, type _SERVICE } from "${declarationsPath}"
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)}
130
133
 
131
134
  export type ${serviceName} = _SERVICE
132
135
 
@@ -142,7 +145,7 @@ export type ${serviceName} = _SERVICE
142
145
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
143
146
  clientManager,
144
147
  idlFactory,
145
- ${canisterIdLine} name: "${canisterName}",
148
+ ${canisterIdLine} name: ${JSON.stringify(canisterName)},
146
149
  })${hookExports || "\n"}`;
147
150
  }
148
151
  function generateReactorEntryFile() {
@@ -165,6 +168,162 @@ export * from "./index.generated"
165
168
  `;
166
169
  }
167
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
+
168
327
  // src/pipeline.ts
169
328
  function resolveReactorClass(canisterConfig) {
170
329
  return canisterConfig.mode ?? "DisplayReactor";
@@ -190,8 +349,30 @@ async function runCanisterPipeline(options) {
190
349
  } = options;
191
350
  const { name, didFile, clientManagerPath } = canisterConfig;
192
351
  const files = [];
193
- const resolvedDidFile = path3.isAbsolute(didFile) ? didFile : path3.resolve(projectRoot, didFile);
194
- 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)) {
195
376
  return {
196
377
  canisterName: name,
197
378
  success: false,
@@ -199,8 +380,8 @@ async function runCanisterPipeline(options) {
199
380
  error: `DID file not found: ${resolvedDidFile}`
200
381
  };
201
382
  }
202
- const canisterOutDir = canisterConfig.outDir != null ? path3.isAbsolute(canisterConfig.outDir) ? canisterConfig.outDir : path3.resolve(projectRoot, canisterConfig.outDir) : path3.resolve(projectRoot, globalConfig.outDir, name);
203
- const resolvedClientManagerPath = clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients";
383
+ const canisterOutDir = validated.outDir;
384
+ const resolvedClientManagerPath = validated.clientManagerPath;
204
385
  try {
205
386
  const declResult = await generateDeclarations({
206
387
  didFile: resolvedDidFile,
@@ -231,8 +412,8 @@ async function runCanisterPipeline(options) {
231
412
  files
232
413
  };
233
414
  }
234
- const reactorPath = path3.join(canisterOutDir, "index.generated.ts");
235
- const entryPath = path3.join(canisterOutDir, "index.ts");
415
+ const reactorPath = path4.join(canisterOutDir, "index.generated.ts");
416
+ const entryPath = path4.join(canisterOutDir, "index.ts");
236
417
  const reactorClass = resolveReactorClass(canisterConfig);
237
418
  const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
238
419
  try {
@@ -245,16 +426,16 @@ async function runCanisterPipeline(options) {
245
426
  reactorClass
246
427
  });
247
428
  const entryContent = generateReactorEntryFile();
248
- fs2.mkdirSync(canisterOutDir, { recursive: true });
249
- fs2.writeFileSync(reactorPath, reactorContent);
429
+ fs3.mkdirSync(canisterOutDir, { recursive: true });
430
+ fs3.writeFileSync(reactorPath, reactorContent);
250
431
  files.push({ success: true, filePath: reactorPath });
251
- if (!fs2.existsSync(entryPath)) {
252
- fs2.writeFileSync(entryPath, entryContent);
432
+ if (!fs3.existsSync(entryPath)) {
433
+ fs3.writeFileSync(entryPath, entryContent);
253
434
  files.push({ success: true, filePath: entryPath });
254
435
  } else {
255
- const existingEntryContent = fs2.readFileSync(entryPath, "utf-8");
436
+ const existingEntryContent = fs3.readFileSync(entryPath, "utf-8");
256
437
  if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
257
- fs2.writeFileSync(entryPath, entryContent);
438
+ fs3.writeFileSync(entryPath, entryContent);
258
439
  files.push({ success: true, filePath: entryPath });
259
440
  } else {
260
441
  files.push({ success: true, filePath: entryPath, skipped: true });
@@ -282,7 +463,7 @@ async function runCanisterPipeline(options) {
282
463
 
283
464
  // src/parser.ts
284
465
  import { didToJs as didToJs2 } from "@ic-reactor/parser";
285
- import fs3 from "fs";
466
+ import fs4 from "fs";
286
467
  function extractMethods(didContent) {
287
468
  try {
288
469
  const jsContent = didToJs2(didContent);
@@ -307,10 +488,10 @@ function extractMethods(didContent) {
307
488
  }
308
489
  }
309
490
  function parseDIDFile(didFilePath) {
310
- if (!fs3.existsSync(didFilePath)) {
491
+ if (!fs4.existsSync(didFilePath)) {
311
492
  throw new Error(`DID file not found: ${didFilePath}`);
312
493
  }
313
- const content = fs3.readFileSync(didFilePath, "utf-8");
494
+ const content = fs4.readFileSync(didFilePath, "utf-8");
314
495
  return extractMethods(content);
315
496
  }
316
497
 
@@ -335,16 +516,25 @@ export const clientManager = new ClientManager({
335
516
  `;
336
517
  }
337
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,
338
528
  declarationsExist,
339
529
  extractMethods,
340
530
  generateClientFile,
341
- generateCodecDeclarations,
342
531
  generateDeclarations,
343
532
  generateReactorEntryFile,
344
533
  generateReactorFile,
345
534
  getReactorName,
346
535
  getServiceTypeName,
347
536
  parseDIDFile,
537
+ resolveContainedOutDir,
348
538
  runCanisterPipeline,
349
539
  toPascalCase
350
540
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/codegen",
3
- "version": "0.12.0",
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",
@@ -17,16 +17,6 @@
17
17
  "default": "./dist/index.cjs"
18
18
  }
19
19
  },
20
- "./renderer": {
21
- "import": {
22
- "types": "./dist/renderer.d.ts",
23
- "default": "./dist/renderer.js"
24
- },
25
- "require": {
26
- "types": "./dist/renderer.d.cts",
27
- "default": "./dist/renderer.cjs"
28
- }
29
- },
30
20
  "./package.json": "./package.json"
31
21
  },
32
22
  "files": [
@@ -62,8 +52,8 @@
62
52
  "vitest": "^4.1.10"
63
53
  },
64
54
  "scripts": {
65
- "build": "tsup src/index.ts src/renderer.ts --format esm,cjs --dts --tsconfig tsconfig.json",
66
- "dev": "tsup src/index.ts src/renderer.ts --format esm,cjs --dts --watch --tsconfig tsconfig.json",
55
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean --tsconfig tsconfig.json",
56
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch --tsconfig tsconfig.json",
67
57
  "test": "vitest run",
68
58
  "test:watch": "vitest",
69
59
  "typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
@@ -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
 
@@ -123,7 +134,7 @@ export type ${serviceName} = _SERVICE
123
134
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
124
135
  clientManager,
125
136
  idlFactory,
126
- ${canisterIdLine} name: "${canisterName}",
137
+ ${canisterIdLine} name: ${JSON.stringify(canisterName)},
127
138
  })${hookExports || "\n"}`
128
139
  }
129
140