@ic-reactor/codegen 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,7 +24,9 @@ await runCanisterPipeline({
24
24
  globalConfig: {
25
25
  outDir: "src/declarations",
26
26
  clientManagerPath: "../../clients",
27
+ target: "react",
27
28
  },
29
+ generateReactor: true,
28
30
  })
29
31
  ```
30
32
 
@@ -38,8 +40,16 @@ Set `canisterConfig.mode` to choose the generated reactor class:
38
40
  - `CandidDisplayReactor`
39
41
  - `MetadataDisplayReactor`
40
42
 
43
+ Set `target` to control whether generated files include React hooks:
44
+
45
+ - `react` (default): generates the reactor plus bound `createActorHooks` exports
46
+ - `core`: generates only the typed reactor exports with no `@ic-reactor/react` dependency
47
+
41
48
  Codegen now writes two files per canister: a managed `index.generated.ts` implementation that is regenerated on every run, and an `index.ts` entry wrapper. The wrapper is created once, then preserved unless it still matches the default generated wrapper or an older generated scaffold that can be migrated automatically.
42
49
 
50
+ Set `generateReactor: false` if you only want the bindgen/declaration output and
51
+ need to skip `index.generated.ts` and `index.ts`.
52
+
43
53
  ## Generators
44
54
 
45
55
  You can also use individual generators if you need more granular control:
package/dist/index.cjs CHANGED
@@ -124,11 +124,11 @@ function getServiceTypeName(canisterName) {
124
124
  }
125
125
 
126
126
  // src/generators/reactor.ts
127
- function getReactorClassImportSource(reactorClass) {
127
+ function getReactorClassImportSource(reactorClass, runtimeTarget) {
128
128
  switch (reactorClass) {
129
129
  case "Reactor":
130
130
  case "DisplayReactor":
131
- return "@ic-reactor/react";
131
+ return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react";
132
132
  case "CandidReactor":
133
133
  case "CandidDisplayReactor":
134
134
  case "MetadataDisplayReactor":
@@ -141,6 +141,7 @@ function generateReactorFile(options) {
141
141
  didFile,
142
142
  clientManagerPath = "../../clients",
143
143
  canisterId,
144
+ runtimeTarget = "react",
144
145
  reactorClass = "DisplayReactor"
145
146
  } = options;
146
147
  const pascalName = toPascalCase(canisterName);
@@ -148,11 +149,24 @@ function generateReactorFile(options) {
148
149
  const serviceName = getServiceTypeName(canisterName);
149
150
  const baseName = import_node_path2.default.basename(didFile, ".did");
150
151
  const declarationsPath = `./declarations/${baseName}`;
151
- const reactorImportSource = getReactorClassImportSource(reactorClass);
152
+ const reactorImportSource = getReactorClassImportSource(
153
+ reactorClass,
154
+ runtimeTarget
155
+ );
152
156
  const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
153
157
  ` : "";
154
- return `import { createActorHooks } from "@ic-reactor/react"
155
- import { ${reactorClass} } from "${reactorImportSource}"
158
+ const hookExports = runtimeTarget === "react" ? `
159
+
160
+ export const {
161
+ useActorQuery: use${pascalName}Query,
162
+ useActorSuspenseQuery: use${pascalName}SuspenseQuery,
163
+ useActorInfiniteQuery: use${pascalName}InfiniteQuery,
164
+ useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
165
+ useActorMutation: use${pascalName}Mutation,
166
+ useActorMethod: use${pascalName}Method,
167
+ } = createActorHooks(${reactorName})
168
+ ` : "";
169
+ return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from "${reactorImportSource}"
156
170
  import { clientManager } from "${clientManagerPath}"
157
171
  import { idlFactory, type _SERVICE } from "${declarationsPath}"
158
172
 
@@ -168,24 +182,14 @@ export const ${reactorName} = new ${reactorClass}<${serviceName}>({
168
182
  clientManager,
169
183
  idlFactory,
170
184
  ${canisterIdLine} name: "${canisterName}",
171
- })
172
-
173
- export const {
174
- useActorQuery: use${pascalName}Query,
175
- useActorSuspenseQuery: use${pascalName}SuspenseQuery,
176
- useActorInfiniteQuery: use${pascalName}InfiniteQuery,
177
- useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
178
- useActorMutation: use${pascalName}Mutation,
179
- useActorMethod: use${pascalName}Method,
180
- } = createActorHooks(${reactorName})
181
- `;
185
+ })${hookExports || "\n"}`;
182
186
  }
183
187
  function generateReactorEntryFile() {
184
188
  return `/**
185
189
  * Canister entrypoint.
186
190
  *
187
191
  * Created once by @ic-reactor/codegen and safe to customize.
188
- * Keep the re-export below if you want generated hooks and types to stay in sync.
192
+ * Keep the re-export below if you want generated exports and types to stay in sync.
189
193
  */
190
194
  export * from "./index.generated"
191
195
  `;
@@ -195,6 +199,9 @@ export * from "./index.generated"
195
199
  function resolveReactorClass(canisterConfig) {
196
200
  return canisterConfig.mode ?? "DisplayReactor";
197
201
  }
202
+ function resolveRuntimeTarget(canisterConfig, globalConfig) {
203
+ return canisterConfig.target ?? globalConfig.target ?? "react";
204
+ }
198
205
  function normalizeFileContent(content) {
199
206
  return content.replace(/\r\n/g, "\n").trim();
200
207
  }
@@ -205,7 +212,12 @@ function isManagedEntryWrapper(content, expectedEntryContent) {
205
212
  return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
206
213
  }
207
214
  async function runCanisterPipeline(options) {
208
- const { canisterConfig, projectRoot, globalConfig } = options;
215
+ const {
216
+ canisterConfig,
217
+ projectRoot,
218
+ globalConfig,
219
+ generateReactor = true
220
+ } = options;
209
221
  const { name, didFile, clientManagerPath } = canisterConfig;
210
222
  const files = [];
211
223
  const resolvedDidFile = import_node_path3.default.isAbsolute(didFile) ? didFile : import_node_path3.default.resolve(projectRoot, didFile);
@@ -242,15 +254,24 @@ async function runCanisterPipeline(options) {
242
254
  error: `Declarations step failed: ${err instanceof Error ? err.message : String(err)}`
243
255
  };
244
256
  }
257
+ if (!generateReactor) {
258
+ return {
259
+ canisterName: name,
260
+ success: true,
261
+ files
262
+ };
263
+ }
245
264
  const reactorPath = import_node_path3.default.join(canisterOutDir, "index.generated.ts");
246
265
  const entryPath = import_node_path3.default.join(canisterOutDir, "index.ts");
247
266
  const reactorClass = resolveReactorClass(canisterConfig);
267
+ const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
248
268
  try {
249
269
  const reactorContent = generateReactorFile({
250
270
  canisterName: name,
251
271
  didFile: resolvedDidFile,
252
272
  clientManagerPath: resolvedClientManagerPath,
253
273
  canisterId: canisterConfig.canisterId,
274
+ runtimeTarget,
254
275
  reactorClass
255
276
  });
256
277
  const entryContent = generateReactorEntryFile();
package/dist/index.d.cts CHANGED
@@ -24,10 +24,16 @@ interface CanisterConfig {
24
24
  * Defaults to DisplayReactor for backward compatibility.
25
25
  */
26
26
  mode?: ReactorClassName;
27
+ /**
28
+ * Generated runtime target.
29
+ * `react` emits bound React hooks, `core` emits only the typed reactor exports.
30
+ */
31
+ target?: CodegenTarget;
27
32
  /** Optional fixed canister ID */
28
33
  canisterId?: string;
29
34
  }
30
35
  type ReactorClassName = "Reactor" | "DisplayReactor" | "CandidReactor" | "CandidDisplayReactor" | "MetadataDisplayReactor";
36
+ type CodegenTarget = "react" | "core";
31
37
  /**
32
38
  * Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
33
39
  */
@@ -44,6 +50,11 @@ interface CodegenConfig {
44
50
  * Individual canisters can override via `CanisterConfig.clientManagerPath`.
45
51
  */
46
52
  clientManagerPath?: string;
53
+ /**
54
+ * Default generated runtime target.
55
+ * Individual canisters can override via `CanisterConfig.target`.
56
+ */
57
+ target?: CodegenTarget;
47
58
  /** Canister configurations, keyed by canister name */
48
59
  canisters: Record<string, CanisterConfig>;
49
60
  }
@@ -68,8 +79,8 @@ interface GeneratorResult {
68
79
  * Pipeline steps (in order):
69
80
  * 1. Resolve paths (didFile, outDir)
70
81
  * 2. Generate declarations (JS + .d.ts + .did copy)
71
- * 3. Generate reactor implementation (`index.generated.ts`)
72
- * 4. Create or migrate the user entry (`index.ts`)
82
+ * 3. Optionally generate reactor implementation (`index.generated.ts`)
83
+ * 4. Optionally create or migrate the user entry (`index.ts`)
73
84
  */
74
85
 
75
86
  interface PipelineOptions {
@@ -83,7 +94,12 @@ interface PipelineOptions {
83
94
  /**
84
95
  * Global codegen config (for fallback outDir and clientManagerPath).
85
96
  */
86
- globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">;
97
+ globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath" | "target">;
98
+ /**
99
+ * Whether the managed reactor files should be generated.
100
+ * Defaults to true.
101
+ */
102
+ generateReactor?: boolean;
87
103
  }
88
104
  interface PipelineResult {
89
105
  canisterName: string;
@@ -193,8 +209,8 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
193
209
  /**
194
210
  * Reactor File Generator
195
211
  *
196
- * Generates the managed `index.generated.ts` implementation for a canister
197
- * plus the full set of typed hooks via `createActorHooks`.
212
+ * Generates the managed `index.generated.ts` implementation for a canister.
213
+ * React targets also emit the full set of typed hooks via `createActorHooks`.
198
214
  *
199
215
  * Generated output example (for canister "backend"):
200
216
  *
@@ -227,6 +243,8 @@ interface ReactorGeneratorOptions {
227
243
  clientManagerPath?: string;
228
244
  /** Optional fixed canister ID for the generated reactor */
229
245
  canisterId?: string;
246
+ /** Generated runtime target */
247
+ runtimeTarget?: CodegenTarget;
230
248
  /**
231
249
  * Which reactor class should back the generated hooks.
232
250
  * Default: "DisplayReactor" (backward compatible)
@@ -265,4 +283,4 @@ interface ClientGeneratorOptions {
265
283
  */
266
284
  declare function generateClientFile(options?: ClientGeneratorOptions): string;
267
285
 
268
- export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, 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 };
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 };
package/dist/index.d.ts CHANGED
@@ -24,10 +24,16 @@ interface CanisterConfig {
24
24
  * Defaults to DisplayReactor for backward compatibility.
25
25
  */
26
26
  mode?: ReactorClassName;
27
+ /**
28
+ * Generated runtime target.
29
+ * `react` emits bound React hooks, `core` emits only the typed reactor exports.
30
+ */
31
+ target?: CodegenTarget;
27
32
  /** Optional fixed canister ID */
28
33
  canisterId?: string;
29
34
  }
30
35
  type ReactorClassName = "Reactor" | "DisplayReactor" | "CandidReactor" | "CandidDisplayReactor" | "MetadataDisplayReactor";
36
+ type CodegenTarget = "react" | "core";
31
37
  /**
32
38
  * Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
33
39
  */
@@ -44,6 +50,11 @@ interface CodegenConfig {
44
50
  * Individual canisters can override via `CanisterConfig.clientManagerPath`.
45
51
  */
46
52
  clientManagerPath?: string;
53
+ /**
54
+ * Default generated runtime target.
55
+ * Individual canisters can override via `CanisterConfig.target`.
56
+ */
57
+ target?: CodegenTarget;
47
58
  /** Canister configurations, keyed by canister name */
48
59
  canisters: Record<string, CanisterConfig>;
49
60
  }
@@ -68,8 +79,8 @@ interface GeneratorResult {
68
79
  * Pipeline steps (in order):
69
80
  * 1. Resolve paths (didFile, outDir)
70
81
  * 2. Generate declarations (JS + .d.ts + .did copy)
71
- * 3. Generate reactor implementation (`index.generated.ts`)
72
- * 4. Create or migrate the user entry (`index.ts`)
82
+ * 3. Optionally generate reactor implementation (`index.generated.ts`)
83
+ * 4. Optionally create or migrate the user entry (`index.ts`)
73
84
  */
74
85
 
75
86
  interface PipelineOptions {
@@ -83,7 +94,12 @@ interface PipelineOptions {
83
94
  /**
84
95
  * Global codegen config (for fallback outDir and clientManagerPath).
85
96
  */
86
- globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">;
97
+ globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath" | "target">;
98
+ /**
99
+ * Whether the managed reactor files should be generated.
100
+ * Defaults to true.
101
+ */
102
+ generateReactor?: boolean;
87
103
  }
88
104
  interface PipelineResult {
89
105
  canisterName: string;
@@ -193,8 +209,8 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
193
209
  /**
194
210
  * Reactor File Generator
195
211
  *
196
- * Generates the managed `index.generated.ts` implementation for a canister
197
- * plus the full set of typed hooks via `createActorHooks`.
212
+ * Generates the managed `index.generated.ts` implementation for a canister.
213
+ * React targets also emit the full set of typed hooks via `createActorHooks`.
198
214
  *
199
215
  * Generated output example (for canister "backend"):
200
216
  *
@@ -227,6 +243,8 @@ interface ReactorGeneratorOptions {
227
243
  clientManagerPath?: string;
228
244
  /** Optional fixed canister ID for the generated reactor */
229
245
  canisterId?: string;
246
+ /** Generated runtime target */
247
+ runtimeTarget?: CodegenTarget;
230
248
  /**
231
249
  * Which reactor class should back the generated hooks.
232
250
  * Default: "DisplayReactor" (backward compatible)
@@ -265,4 +283,4 @@ interface ClientGeneratorOptions {
265
283
  */
266
284
  declare function generateClientFile(options?: ClientGeneratorOptions): string;
267
285
 
268
- export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, 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 };
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 };
package/dist/index.js CHANGED
@@ -78,11 +78,11 @@ function getServiceTypeName(canisterName) {
78
78
  }
79
79
 
80
80
  // src/generators/reactor.ts
81
- function getReactorClassImportSource(reactorClass) {
81
+ function getReactorClassImportSource(reactorClass, runtimeTarget) {
82
82
  switch (reactorClass) {
83
83
  case "Reactor":
84
84
  case "DisplayReactor":
85
- return "@ic-reactor/react";
85
+ return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react";
86
86
  case "CandidReactor":
87
87
  case "CandidDisplayReactor":
88
88
  case "MetadataDisplayReactor":
@@ -95,6 +95,7 @@ function generateReactorFile(options) {
95
95
  didFile,
96
96
  clientManagerPath = "../../clients",
97
97
  canisterId,
98
+ runtimeTarget = "react",
98
99
  reactorClass = "DisplayReactor"
99
100
  } = options;
100
101
  const pascalName = toPascalCase(canisterName);
@@ -102,11 +103,24 @@ function generateReactorFile(options) {
102
103
  const serviceName = getServiceTypeName(canisterName);
103
104
  const baseName = path2.basename(didFile, ".did");
104
105
  const declarationsPath = `./declarations/${baseName}`;
105
- const reactorImportSource = getReactorClassImportSource(reactorClass);
106
+ const reactorImportSource = getReactorClassImportSource(
107
+ reactorClass,
108
+ runtimeTarget
109
+ );
106
110
  const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
107
111
  ` : "";
108
- return `import { createActorHooks } from "@ic-reactor/react"
109
- import { ${reactorClass} } from "${reactorImportSource}"
112
+ const hookExports = runtimeTarget === "react" ? `
113
+
114
+ export const {
115
+ useActorQuery: use${pascalName}Query,
116
+ useActorSuspenseQuery: use${pascalName}SuspenseQuery,
117
+ useActorInfiniteQuery: use${pascalName}InfiniteQuery,
118
+ useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
119
+ useActorMutation: use${pascalName}Mutation,
120
+ useActorMethod: use${pascalName}Method,
121
+ } = createActorHooks(${reactorName})
122
+ ` : "";
123
+ return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from "${reactorImportSource}"
110
124
  import { clientManager } from "${clientManagerPath}"
111
125
  import { idlFactory, type _SERVICE } from "${declarationsPath}"
112
126
 
@@ -122,24 +136,14 @@ export const ${reactorName} = new ${reactorClass}<${serviceName}>({
122
136
  clientManager,
123
137
  idlFactory,
124
138
  ${canisterIdLine} name: "${canisterName}",
125
- })
126
-
127
- export const {
128
- useActorQuery: use${pascalName}Query,
129
- useActorSuspenseQuery: use${pascalName}SuspenseQuery,
130
- useActorInfiniteQuery: use${pascalName}InfiniteQuery,
131
- useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
132
- useActorMutation: use${pascalName}Mutation,
133
- useActorMethod: use${pascalName}Method,
134
- } = createActorHooks(${reactorName})
135
- `;
139
+ })${hookExports || "\n"}`;
136
140
  }
137
141
  function generateReactorEntryFile() {
138
142
  return `/**
139
143
  * Canister entrypoint.
140
144
  *
141
145
  * Created once by @ic-reactor/codegen and safe to customize.
142
- * Keep the re-export below if you want generated hooks and types to stay in sync.
146
+ * Keep the re-export below if you want generated exports and types to stay in sync.
143
147
  */
144
148
  export * from "./index.generated"
145
149
  `;
@@ -149,6 +153,9 @@ export * from "./index.generated"
149
153
  function resolveReactorClass(canisterConfig) {
150
154
  return canisterConfig.mode ?? "DisplayReactor";
151
155
  }
156
+ function resolveRuntimeTarget(canisterConfig, globalConfig) {
157
+ return canisterConfig.target ?? globalConfig.target ?? "react";
158
+ }
152
159
  function normalizeFileContent(content) {
153
160
  return content.replace(/\r\n/g, "\n").trim();
154
161
  }
@@ -159,7 +166,12 @@ function isManagedEntryWrapper(content, expectedEntryContent) {
159
166
  return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
160
167
  }
161
168
  async function runCanisterPipeline(options) {
162
- const { canisterConfig, projectRoot, globalConfig } = options;
169
+ const {
170
+ canisterConfig,
171
+ projectRoot,
172
+ globalConfig,
173
+ generateReactor = true
174
+ } = options;
163
175
  const { name, didFile, clientManagerPath } = canisterConfig;
164
176
  const files = [];
165
177
  const resolvedDidFile = path3.isAbsolute(didFile) ? didFile : path3.resolve(projectRoot, didFile);
@@ -196,15 +208,24 @@ async function runCanisterPipeline(options) {
196
208
  error: `Declarations step failed: ${err instanceof Error ? err.message : String(err)}`
197
209
  };
198
210
  }
211
+ if (!generateReactor) {
212
+ return {
213
+ canisterName: name,
214
+ success: true,
215
+ files
216
+ };
217
+ }
199
218
  const reactorPath = path3.join(canisterOutDir, "index.generated.ts");
200
219
  const entryPath = path3.join(canisterOutDir, "index.ts");
201
220
  const reactorClass = resolveReactorClass(canisterConfig);
221
+ const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
202
222
  try {
203
223
  const reactorContent = generateReactorFile({
204
224
  canisterName: name,
205
225
  didFile: resolvedDidFile,
206
226
  clientManagerPath: resolvedClientManagerPath,
207
227
  canisterId: canisterConfig.canisterId,
228
+ runtimeTarget,
208
229
  reactorClass
209
230
  });
210
231
  const entryContent = generateReactorEntryFile();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/codegen",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Shared code generation utilities for IC Reactor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -92,3 +92,24 @@ export const {
92
92
  } = createActorHooks(ledgerReactor)
93
93
  "
94
94
  `;
95
+
96
+ exports[`Reactor generator > supports core target generation without React hooks > core-display-reactor-index 1`] = `
97
+ "import { DisplayReactor } from "@ic-reactor/core"
98
+ import { clientManager } from "../../clients"
99
+ import { idlFactory, type _SERVICE } from "./declarations/backend"
100
+
101
+ export type BackendService = _SERVICE
102
+
103
+ /**
104
+ * Backend Reactor
105
+ *
106
+ * Auto-generated by @ic-reactor/codegen — do not edit.
107
+ * This file is overwritten whenever generation runs.
108
+ */
109
+ export const backendReactor = new DisplayReactor<BackendService>({
110
+ clientManager,
111
+ idlFactory,
112
+ name: "backend",
113
+ })
114
+ "
115
+ `;
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Reactor File Generator
3
3
  *
4
- * Generates the managed `index.generated.ts` implementation for a canister
5
- * plus the full set of typed hooks via `createActorHooks`.
4
+ * Generates the managed `index.generated.ts` implementation for a canister.
5
+ * React targets also emit the full set of typed hooks via `createActorHooks`.
6
6
  *
7
7
  * Generated output example (for canister "backend"):
8
8
  *
@@ -22,7 +22,7 @@
22
22
 
23
23
  import path from "node:path"
24
24
  import { toPascalCase, getReactorName, getServiceTypeName } from "../naming.js"
25
- import type { ReactorClassName } from "../types.js"
25
+ import type { CodegenTarget, ReactorClassName } from "../types.js"
26
26
 
27
27
  export interface ReactorGeneratorOptions {
28
28
  /** Canister name (e.g. "backend") */
@@ -39,6 +39,8 @@ export interface ReactorGeneratorOptions {
39
39
  clientManagerPath?: string
40
40
  /** Optional fixed canister ID for the generated reactor */
41
41
  canisterId?: string
42
+ /** Generated runtime target */
43
+ runtimeTarget?: CodegenTarget
42
44
  /**
43
45
  * Which reactor class should back the generated hooks.
44
46
  * Default: "DisplayReactor" (backward compatible)
@@ -47,12 +49,13 @@ export interface ReactorGeneratorOptions {
47
49
  }
48
50
 
49
51
  function getReactorClassImportSource(
50
- reactorClass: ReactorClassName
51
- ): "@ic-reactor/react" | "@ic-reactor/candid" {
52
+ reactorClass: ReactorClassName,
53
+ runtimeTarget: CodegenTarget
54
+ ): "@ic-reactor/react" | "@ic-reactor/core" | "@ic-reactor/candid" {
52
55
  switch (reactorClass) {
53
56
  case "Reactor":
54
57
  case "DisplayReactor":
55
- return "@ic-reactor/react"
58
+ return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react"
56
59
  case "CandidReactor":
57
60
  case "CandidDisplayReactor":
58
61
  case "MetadataDisplayReactor":
@@ -69,6 +72,7 @@ export function generateReactorFile(options: ReactorGeneratorOptions): string {
69
72
  didFile,
70
73
  clientManagerPath = "../../clients",
71
74
  canisterId,
75
+ runtimeTarget = "react",
72
76
  reactorClass = "DisplayReactor",
73
77
  } = options
74
78
 
@@ -79,13 +83,29 @@ export function generateReactorFile(options: ReactorGeneratorOptions): string {
79
83
  // Derive the declarations import path from the .did filename
80
84
  const baseName = path.basename(didFile, ".did")
81
85
  const declarationsPath = `./declarations/${baseName}`
82
- const reactorImportSource = getReactorClassImportSource(reactorClass)
86
+ const reactorImportSource = getReactorClassImportSource(
87
+ reactorClass,
88
+ runtimeTarget
89
+ )
83
90
  const canisterIdLine = canisterId
84
91
  ? ` canisterId: ${JSON.stringify(canisterId)},\n`
85
92
  : ""
93
+ const hookExports =
94
+ runtimeTarget === "react"
95
+ ? `
86
96
 
87
- return `import { createActorHooks } from "@ic-reactor/react"
88
- import { ${reactorClass} } from "${reactorImportSource}"
97
+ export const {
98
+ useActorQuery: use${pascalName}Query,
99
+ useActorSuspenseQuery: use${pascalName}SuspenseQuery,
100
+ useActorInfiniteQuery: use${pascalName}InfiniteQuery,
101
+ useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
102
+ useActorMutation: use${pascalName}Mutation,
103
+ useActorMethod: use${pascalName}Method,
104
+ } = createActorHooks(${reactorName})
105
+ `
106
+ : ""
107
+
108
+ return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from "${reactorImportSource}"
89
109
  import { clientManager } from "${clientManagerPath}"
90
110
  import { idlFactory, type _SERVICE } from "${declarationsPath}"
91
111
 
@@ -101,17 +121,7 @@ export const ${reactorName} = new ${reactorClass}<${serviceName}>({
101
121
  clientManager,
102
122
  idlFactory,
103
123
  ${canisterIdLine} name: "${canisterName}",
104
- })
105
-
106
- export const {
107
- useActorQuery: use${pascalName}Query,
108
- useActorSuspenseQuery: use${pascalName}SuspenseQuery,
109
- useActorInfiniteQuery: use${pascalName}InfiniteQuery,
110
- useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
111
- useActorMutation: use${pascalName}Mutation,
112
- useActorMethod: use${pascalName}Method,
113
- } = createActorHooks(${reactorName})
114
- `
124
+ })${hookExports || "\n"}`
115
125
  }
116
126
 
117
127
  /**
@@ -122,7 +132,7 @@ export function generateReactorEntryFile(): string {
122
132
  * Canister entrypoint.
123
133
  *
124
134
  * Created once by @ic-reactor/codegen and safe to customize.
125
- * Keep the re-export below if you want generated hooks and types to stay in sync.
135
+ * Keep the re-export below if you want generated exports and types to stay in sync.
126
136
  */
127
137
  export * from "./index.generated"
128
138
  `
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  export type {
10
10
  CanisterConfig,
11
11
  CodegenConfig,
12
+ CodegenTarget,
12
13
  GeneratorResult,
13
14
  ReactorClassName,
14
15
  } from "./types.js"
@@ -95,6 +95,136 @@ describe("Codegen pipeline", () => {
95
95
  expect(generated).toContain('name: "workflow"')
96
96
  })
97
97
 
98
+ it("can generate declarations without creating reactor files", async () => {
99
+ const projectRoot = createTempProject()
100
+ writeDid(projectRoot, "backend.did")
101
+
102
+ const result = await runCanisterPipeline({
103
+ canisterConfig: {
104
+ name: "backend",
105
+ didFile: "backend.did",
106
+ },
107
+ projectRoot,
108
+ globalConfig: {
109
+ outDir: "src/declarations",
110
+ clientManagerPath: "../../clients",
111
+ },
112
+ generateReactor: false,
113
+ })
114
+
115
+ expect(result.success).toBe(true)
116
+ expect(
117
+ fs.existsSync(
118
+ path.join(
119
+ projectRoot,
120
+ "src/declarations/backend/declarations/backend.js"
121
+ )
122
+ )
123
+ ).toBe(true)
124
+ expect(
125
+ fs.existsSync(
126
+ path.join(
127
+ projectRoot,
128
+ "src/declarations/backend/declarations/backend.d.ts"
129
+ )
130
+ )
131
+ ).toBe(true)
132
+ expect(
133
+ fs.existsSync(
134
+ path.join(
135
+ projectRoot,
136
+ "src/declarations/backend/declarations/backend.did"
137
+ )
138
+ )
139
+ ).toBe(true)
140
+ expect(
141
+ fs.existsSync(
142
+ path.join(projectRoot, "src/declarations/backend/index.generated.ts")
143
+ )
144
+ ).toBe(false)
145
+ expect(
146
+ fs.existsSync(path.join(projectRoot, "src/declarations/backend/index.ts"))
147
+ ).toBe(false)
148
+ expect(
149
+ result.files.some((file) => file.filePath.endsWith("index.generated.ts"))
150
+ ).toBe(false)
151
+ expect(
152
+ result.files.some((file) => file.filePath.endsWith("index.ts"))
153
+ ).toBe(false)
154
+ })
155
+
156
+ it("leaves existing reactor files untouched when reactor generation is disabled", async () => {
157
+ const projectRoot = createTempProject()
158
+ writeDid(projectRoot, "backend.did")
159
+
160
+ const canisterOutDir = path.join(projectRoot, "src/declarations/backend")
161
+ fs.mkdirSync(canisterOutDir, { recursive: true })
162
+
163
+ const existingGenerated = "// existing generated reactor"
164
+ const existingEntry = "// existing entry wrapper"
165
+
166
+ fs.writeFileSync(
167
+ path.join(canisterOutDir, "index.generated.ts"),
168
+ existingGenerated
169
+ )
170
+ fs.writeFileSync(path.join(canisterOutDir, "index.ts"), existingEntry)
171
+
172
+ const result = await runCanisterPipeline({
173
+ canisterConfig: {
174
+ name: "backend",
175
+ didFile: "backend.did",
176
+ },
177
+ projectRoot,
178
+ globalConfig: {
179
+ outDir: "src/declarations",
180
+ clientManagerPath: "../../clients",
181
+ },
182
+ generateReactor: false,
183
+ })
184
+
185
+ expect(result.success).toBe(true)
186
+ expect(
187
+ fs.readFileSync(path.join(canisterOutDir, "index.generated.ts"), "utf-8")
188
+ ).toBe(existingGenerated)
189
+ expect(
190
+ fs.readFileSync(path.join(canisterOutDir, "index.ts"), "utf-8")
191
+ ).toBe(existingEntry)
192
+ })
193
+
194
+ it("supports a core target without generating React hooks", async () => {
195
+ const projectRoot = createTempProject()
196
+ writeDid(projectRoot, "backend.did")
197
+
198
+ const result = await runCanisterPipeline({
199
+ canisterConfig: {
200
+ name: "backend",
201
+ didFile: "backend.did",
202
+ },
203
+ projectRoot,
204
+ globalConfig: {
205
+ outDir: "src/declarations",
206
+ clientManagerPath: "../../clients",
207
+ target: "core",
208
+ },
209
+ })
210
+
211
+ expect(result.success).toBe(true)
212
+
213
+ const indexPath = path.join(
214
+ projectRoot,
215
+ "src/declarations/backend/index.generated.ts"
216
+ )
217
+ const generated = fs.readFileSync(indexPath, "utf-8")
218
+
219
+ expect(generated).toContain(
220
+ 'import { DisplayReactor } from "@ic-reactor/core"'
221
+ )
222
+ expect(generated).not.toContain(
223
+ 'import { createActorHooks } from "@ic-reactor/react"'
224
+ )
225
+ expect(generated).not.toContain("useBackendQuery")
226
+ })
227
+
98
228
  it("does not overwrite user-modified index.ts on regenerate", async () => {
99
229
  const projectRoot = createTempProject()
100
230
  writeDid(projectRoot, "backend.did")
package/src/pipeline.ts CHANGED
@@ -7,8 +7,8 @@
7
7
  * Pipeline steps (in order):
8
8
  * 1. Resolve paths (didFile, outDir)
9
9
  * 2. Generate declarations (JS + .d.ts + .did copy)
10
- * 3. Generate reactor implementation (`index.generated.ts`)
11
- * 4. Create or migrate the user entry (`index.ts`)
10
+ * 3. Optionally generate reactor implementation (`index.generated.ts`)
11
+ * 4. Optionally create or migrate the user entry (`index.ts`)
12
12
  */
13
13
 
14
14
  import fs from "node:fs"
@@ -16,6 +16,7 @@ import path from "node:path"
16
16
  import type {
17
17
  CanisterConfig,
18
18
  CodegenConfig,
19
+ CodegenTarget,
19
20
  GeneratorResult,
20
21
  ReactorClassName,
21
22
  } from "./types.js"
@@ -36,13 +37,25 @@ export interface PipelineOptions {
36
37
  /**
37
38
  * Global codegen config (for fallback outDir and clientManagerPath).
38
39
  */
39
- globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">
40
+ globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath" | "target">
41
+ /**
42
+ * Whether the managed reactor files should be generated.
43
+ * Defaults to true.
44
+ */
45
+ generateReactor?: boolean
40
46
  }
41
47
 
42
48
  function resolveReactorClass(canisterConfig: CanisterConfig): ReactorClassName {
43
49
  return canisterConfig.mode ?? "DisplayReactor"
44
50
  }
45
51
 
52
+ function resolveRuntimeTarget(
53
+ canisterConfig: CanisterConfig,
54
+ globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath" | "target">
55
+ ): CodegenTarget {
56
+ return canisterConfig.target ?? globalConfig.target ?? "react"
57
+ }
58
+
46
59
  function normalizeFileContent(content: string): string {
47
60
  return content.replace(/\r\n/g, "\n").trim()
48
61
  }
@@ -79,7 +92,12 @@ export interface PipelineResult {
79
92
  export async function runCanisterPipeline(
80
93
  options: PipelineOptions
81
94
  ): Promise<PipelineResult> {
82
- const { canisterConfig, projectRoot, globalConfig } = options
95
+ const {
96
+ canisterConfig,
97
+ projectRoot,
98
+ globalConfig,
99
+ generateReactor = true,
100
+ } = options
83
101
  const { name, didFile, clientManagerPath } = canisterConfig
84
102
 
85
103
  const files: GeneratorResult[] = []
@@ -139,11 +157,20 @@ export async function runCanisterPipeline(
139
157
  }
140
158
  }
141
159
 
160
+ if (!generateReactor) {
161
+ return {
162
+ canisterName: name,
163
+ success: true,
164
+ files,
165
+ }
166
+ }
167
+
142
168
  // ── Step 2: Reactor file ───────────────────────────────────────────────────
143
169
 
144
170
  const reactorPath = path.join(canisterOutDir, "index.generated.ts")
145
171
  const entryPath = path.join(canisterOutDir, "index.ts")
146
172
  const reactorClass = resolveReactorClass(canisterConfig)
173
+ const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig)
147
174
 
148
175
  try {
149
176
  const reactorContent = generateReactorFile({
@@ -151,6 +178,7 @@ export async function runCanisterPipeline(
151
178
  didFile: resolvedDidFile,
152
179
  clientManagerPath: resolvedClientManagerPath,
153
180
  canisterId: canisterConfig.canisterId,
181
+ runtimeTarget,
154
182
  reactorClass,
155
183
  })
156
184
  const entryContent = generateReactorEntryFile()
@@ -43,6 +43,24 @@ describe("Reactor generator", () => {
43
43
  expect(content).toContain("new MetadataDisplayReactor<LedgerService>")
44
44
  })
45
45
 
46
+ it("supports core target generation without React hooks", () => {
47
+ const content = generateReactorFile({
48
+ canisterName: "backend",
49
+ didFile: "mock/backend.did",
50
+ runtimeTarget: "core",
51
+ reactorClass: "DisplayReactor",
52
+ })
53
+
54
+ expect(content).toMatchSnapshot("core-display-reactor-index")
55
+ expect(content).toContain(
56
+ 'import { DisplayReactor } from "@ic-reactor/core"'
57
+ )
58
+ expect(content).not.toContain(
59
+ 'import { createActorHooks } from "@ic-reactor/react"'
60
+ )
61
+ expect(content).not.toContain("useBackendQuery")
62
+ })
63
+
46
64
  it("writes a fixed canisterId when configured", () => {
47
65
  const content = generateReactorFile({
48
66
  canisterName: "workflow",
package/src/types.ts CHANGED
@@ -29,6 +29,11 @@ export interface CanisterConfig {
29
29
  * Defaults to DisplayReactor for backward compatibility.
30
30
  */
31
31
  mode?: ReactorClassName
32
+ /**
33
+ * Generated runtime target.
34
+ * `react` emits bound React hooks, `core` emits only the typed reactor exports.
35
+ */
36
+ target?: CodegenTarget
32
37
  /** Optional fixed canister ID */
33
38
  canisterId?: string
34
39
  }
@@ -40,6 +45,8 @@ export type ReactorClassName =
40
45
  | "CandidDisplayReactor"
41
46
  | "MetadataDisplayReactor"
42
47
 
48
+ export type CodegenTarget = "react" | "core"
49
+
43
50
  /**
44
51
  * Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
45
52
  */
@@ -56,6 +63,11 @@ export interface CodegenConfig {
56
63
  * Individual canisters can override via `CanisterConfig.clientManagerPath`.
57
64
  */
58
65
  clientManagerPath?: string
66
+ /**
67
+ * Default generated runtime target.
68
+ * Individual canisters can override via `CanisterConfig.target`.
69
+ */
70
+ target?: CodegenTarget
59
71
  /** Canister configurations, keyed by canister name */
60
72
  canisters: Record<string, CanisterConfig>
61
73
  }