@ic-reactor/codegen 0.7.1 → 0.7.2
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 +14 -1
- package/dist/index.cjs +42 -6
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +42 -6
- package/package.json +2 -2
- package/src/__snapshots__/reactor.test.ts.snap +94 -0
- package/src/generators/reactor.ts +31 -4
- package/src/index.ts +6 -1
- package/src/pipeline.test.ts +163 -0
- package/src/pipeline.ts +33 -5
- package/src/reactor.test.ts +45 -0
- package/src/types.ts +12 -0
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ import { runCanisterPipeline } from "@ic-reactor/codegen"
|
|
|
17
17
|
await runCanisterPipeline({
|
|
18
18
|
canisterConfig: {
|
|
19
19
|
name: "backend",
|
|
20
|
+
mode: "DisplayReactor",
|
|
20
21
|
didFile: "./backend.did",
|
|
21
22
|
},
|
|
22
23
|
projectRoot: process.cwd(),
|
|
@@ -27,12 +28,24 @@ await runCanisterPipeline({
|
|
|
27
28
|
})
|
|
28
29
|
```
|
|
29
30
|
|
|
31
|
+
## Reactor Class Configuration
|
|
32
|
+
|
|
33
|
+
Set `canisterConfig.mode` to choose the generated reactor class:
|
|
34
|
+
|
|
35
|
+
- `DisplayReactor` (default)
|
|
36
|
+
- `Reactor`
|
|
37
|
+
- `CandidReactor`
|
|
38
|
+
- `CandidDisplayReactor`
|
|
39
|
+
- `MetadataDisplayReactor`
|
|
40
|
+
|
|
41
|
+
Codegen writes a single `index.ts`. It overwrites that file only while it is still recognized as a generated file; if you replace it with your own file, regeneration leaves it untouched.
|
|
42
|
+
|
|
30
43
|
## Generators
|
|
31
44
|
|
|
32
45
|
You can also use individual generators if you need more granular control:
|
|
33
46
|
|
|
34
47
|
- **`generateDeclarations`**: Generates `.js` (factory), `.d.ts` (types), and `.did` copy.
|
|
35
|
-
- **`generateReactorFile`**: Generates the `index.ts` file
|
|
48
|
+
- **`generateReactorFile`**: Generates the `index.ts` file using either `DisplayReactor` or `Reactor`.
|
|
36
49
|
- **`generateClientFile`**: Generates a `ClientManager` boilerplate file.
|
|
37
50
|
|
|
38
51
|
## Utilities
|
package/dist/index.cjs
CHANGED
|
@@ -123,14 +123,32 @@ function getServiceTypeName(canisterName) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// src/generators/reactor.ts
|
|
126
|
+
function getReactorClassImportSource(reactorClass) {
|
|
127
|
+
switch (reactorClass) {
|
|
128
|
+
case "Reactor":
|
|
129
|
+
case "DisplayReactor":
|
|
130
|
+
return "@ic-reactor/react";
|
|
131
|
+
case "CandidReactor":
|
|
132
|
+
case "CandidDisplayReactor":
|
|
133
|
+
case "MetadataDisplayReactor":
|
|
134
|
+
return "@ic-reactor/candid";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
126
137
|
function generateReactorFile(options) {
|
|
127
|
-
const {
|
|
138
|
+
const {
|
|
139
|
+
canisterName,
|
|
140
|
+
didFile,
|
|
141
|
+
clientManagerPath = "../../clients",
|
|
142
|
+
reactorClass = "DisplayReactor"
|
|
143
|
+
} = options;
|
|
128
144
|
const pascalName = toPascalCase(canisterName);
|
|
129
145
|
const reactorName = getReactorName(canisterName);
|
|
130
146
|
const serviceName = getServiceTypeName(canisterName);
|
|
131
147
|
const baseName = import_node_path2.default.basename(didFile, ".did");
|
|
132
148
|
const declarationsPath = `./declarations/${baseName}`;
|
|
133
|
-
|
|
149
|
+
const reactorImportSource = getReactorClassImportSource(reactorClass);
|
|
150
|
+
return `import { createActorHooks } from "@ic-reactor/react"
|
|
151
|
+
import { ${reactorClass} } from "${reactorImportSource}"
|
|
134
152
|
import { clientManager } from "${clientManagerPath}"
|
|
135
153
|
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
136
154
|
|
|
@@ -142,7 +160,7 @@ export type ${serviceName} = _SERVICE
|
|
|
142
160
|
* Auto-generated by @ic-reactor/codegen \u2014 do not edit.
|
|
143
161
|
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
144
162
|
*/
|
|
145
|
-
export const ${reactorName} = new
|
|
163
|
+
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
146
164
|
clientManager,
|
|
147
165
|
idlFactory,
|
|
148
166
|
name: "${canisterName}",
|
|
@@ -160,6 +178,12 @@ export const {
|
|
|
160
178
|
}
|
|
161
179
|
|
|
162
180
|
// src/pipeline.ts
|
|
181
|
+
function resolveReactorClass(canisterConfig) {
|
|
182
|
+
return canisterConfig.mode ?? "DisplayReactor";
|
|
183
|
+
}
|
|
184
|
+
function isGeneratedIndexFile(content) {
|
|
185
|
+
return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(");
|
|
186
|
+
}
|
|
163
187
|
async function runCanisterPipeline(options) {
|
|
164
188
|
const { canisterConfig, projectRoot, globalConfig } = options;
|
|
165
189
|
const { name, didFile, clientManagerPath } = canisterConfig;
|
|
@@ -199,15 +223,27 @@ async function runCanisterPipeline(options) {
|
|
|
199
223
|
};
|
|
200
224
|
}
|
|
201
225
|
const reactorPath = import_node_path3.default.join(canisterOutDir, "index.ts");
|
|
226
|
+
const reactorClass = resolveReactorClass(canisterConfig);
|
|
202
227
|
try {
|
|
203
228
|
const reactorContent = generateReactorFile({
|
|
204
229
|
canisterName: name,
|
|
205
230
|
didFile: resolvedDidFile,
|
|
206
|
-
clientManagerPath: resolvedClientManagerPath
|
|
231
|
+
clientManagerPath: resolvedClientManagerPath,
|
|
232
|
+
reactorClass
|
|
207
233
|
});
|
|
208
234
|
import_node_fs2.default.mkdirSync(canisterOutDir, { recursive: true });
|
|
209
|
-
import_node_fs2.default.
|
|
210
|
-
|
|
235
|
+
if (!import_node_fs2.default.existsSync(reactorPath)) {
|
|
236
|
+
import_node_fs2.default.writeFileSync(reactorPath, reactorContent);
|
|
237
|
+
files.push({ success: true, filePath: reactorPath });
|
|
238
|
+
} else {
|
|
239
|
+
const existingIndexContent = import_node_fs2.default.readFileSync(reactorPath, "utf-8");
|
|
240
|
+
if (isGeneratedIndexFile(existingIndexContent)) {
|
|
241
|
+
import_node_fs2.default.writeFileSync(reactorPath, reactorContent);
|
|
242
|
+
files.push({ success: true, filePath: reactorPath });
|
|
243
|
+
} else {
|
|
244
|
+
files.push({ success: true, filePath: reactorPath, skipped: true });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
211
247
|
} catch (err) {
|
|
212
248
|
files.push({
|
|
213
249
|
success: false,
|
package/dist/index.d.cts
CHANGED
|
@@ -19,9 +19,15 @@ interface CanisterConfig {
|
|
|
19
19
|
* Example: "../../clients" → `import { clientManager } from "../../clients"`
|
|
20
20
|
*/
|
|
21
21
|
clientManagerPath?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Reactor class used for generated hooks in this canister.
|
|
24
|
+
* Defaults to DisplayReactor for backward compatibility.
|
|
25
|
+
*/
|
|
26
|
+
mode?: ReactorClassName;
|
|
22
27
|
/** Optional fixed canister ID */
|
|
23
28
|
canisterId?: string;
|
|
24
29
|
}
|
|
30
|
+
type ReactorClassName = "Reactor" | "DisplayReactor" | "CandidReactor" | "CandidDisplayReactor" | "MetadataDisplayReactor";
|
|
25
31
|
/**
|
|
26
32
|
* Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
|
|
27
33
|
*/
|
|
@@ -62,7 +68,7 @@ interface GeneratorResult {
|
|
|
62
68
|
* Pipeline steps (in order):
|
|
63
69
|
* 1. Resolve paths (didFile, outDir)
|
|
64
70
|
* 2. Generate declarations (JS + .d.ts + .did copy)
|
|
65
|
-
* 3. Generate reactor file (index.ts)
|
|
71
|
+
* 3. Generate reactor file (`index.ts`)
|
|
66
72
|
*/
|
|
67
73
|
|
|
68
74
|
interface PipelineOptions {
|
|
@@ -204,6 +210,7 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
|
|
|
204
210
|
* ...
|
|
205
211
|
* } = createActorHooks(backendReactor)
|
|
206
212
|
*/
|
|
213
|
+
|
|
207
214
|
interface ReactorGeneratorOptions {
|
|
208
215
|
/** Canister name (e.g. "backend") */
|
|
209
216
|
canisterName: string;
|
|
@@ -217,9 +224,14 @@ interface ReactorGeneratorOptions {
|
|
|
217
224
|
* Default: "../../clients"
|
|
218
225
|
*/
|
|
219
226
|
clientManagerPath?: string;
|
|
227
|
+
/**
|
|
228
|
+
* Which reactor class should back the generated hooks.
|
|
229
|
+
* Default: "DisplayReactor" (backward compatible)
|
|
230
|
+
*/
|
|
231
|
+
reactorClass?: ReactorClassName;
|
|
220
232
|
}
|
|
221
233
|
/**
|
|
222
|
-
* Generate the content of a canister's `index.ts`
|
|
234
|
+
* Generate the content of a canister's `index.ts` file.
|
|
223
235
|
*/
|
|
224
236
|
declare function generateReactorFile(options: ReactorGeneratorOptions): string;
|
|
225
237
|
|
|
@@ -246,4 +258,4 @@ interface ClientGeneratorOptions {
|
|
|
246
258
|
*/
|
|
247
259
|
declare function generateClientFile(options?: ClientGeneratorOptions): string;
|
|
248
260
|
|
|
249
|
-
export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
|
|
261
|
+
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, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
|
package/dist/index.d.ts
CHANGED
|
@@ -19,9 +19,15 @@ interface CanisterConfig {
|
|
|
19
19
|
* Example: "../../clients" → `import { clientManager } from "../../clients"`
|
|
20
20
|
*/
|
|
21
21
|
clientManagerPath?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Reactor class used for generated hooks in this canister.
|
|
24
|
+
* Defaults to DisplayReactor for backward compatibility.
|
|
25
|
+
*/
|
|
26
|
+
mode?: ReactorClassName;
|
|
22
27
|
/** Optional fixed canister ID */
|
|
23
28
|
canisterId?: string;
|
|
24
29
|
}
|
|
30
|
+
type ReactorClassName = "Reactor" | "DisplayReactor" | "CandidReactor" | "CandidDisplayReactor" | "MetadataDisplayReactor";
|
|
25
31
|
/**
|
|
26
32
|
* Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
|
|
27
33
|
*/
|
|
@@ -62,7 +68,7 @@ interface GeneratorResult {
|
|
|
62
68
|
* Pipeline steps (in order):
|
|
63
69
|
* 1. Resolve paths (didFile, outDir)
|
|
64
70
|
* 2. Generate declarations (JS + .d.ts + .did copy)
|
|
65
|
-
* 3. Generate reactor file (index.ts)
|
|
71
|
+
* 3. Generate reactor file (`index.ts`)
|
|
66
72
|
*/
|
|
67
73
|
|
|
68
74
|
interface PipelineOptions {
|
|
@@ -204,6 +210,7 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
|
|
|
204
210
|
* ...
|
|
205
211
|
* } = createActorHooks(backendReactor)
|
|
206
212
|
*/
|
|
213
|
+
|
|
207
214
|
interface ReactorGeneratorOptions {
|
|
208
215
|
/** Canister name (e.g. "backend") */
|
|
209
216
|
canisterName: string;
|
|
@@ -217,9 +224,14 @@ interface ReactorGeneratorOptions {
|
|
|
217
224
|
* Default: "../../clients"
|
|
218
225
|
*/
|
|
219
226
|
clientManagerPath?: string;
|
|
227
|
+
/**
|
|
228
|
+
* Which reactor class should back the generated hooks.
|
|
229
|
+
* Default: "DisplayReactor" (backward compatible)
|
|
230
|
+
*/
|
|
231
|
+
reactorClass?: ReactorClassName;
|
|
220
232
|
}
|
|
221
233
|
/**
|
|
222
|
-
* Generate the content of a canister's `index.ts`
|
|
234
|
+
* Generate the content of a canister's `index.ts` file.
|
|
223
235
|
*/
|
|
224
236
|
declare function generateReactorFile(options: ReactorGeneratorOptions): string;
|
|
225
237
|
|
|
@@ -246,4 +258,4 @@ interface ClientGeneratorOptions {
|
|
|
246
258
|
*/
|
|
247
259
|
declare function generateClientFile(options?: ClientGeneratorOptions): string;
|
|
248
260
|
|
|
249
|
-
export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
|
|
261
|
+
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, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
|
package/dist/index.js
CHANGED
|
@@ -78,14 +78,32 @@ function getServiceTypeName(canisterName) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// src/generators/reactor.ts
|
|
81
|
+
function getReactorClassImportSource(reactorClass) {
|
|
82
|
+
switch (reactorClass) {
|
|
83
|
+
case "Reactor":
|
|
84
|
+
case "DisplayReactor":
|
|
85
|
+
return "@ic-reactor/react";
|
|
86
|
+
case "CandidReactor":
|
|
87
|
+
case "CandidDisplayReactor":
|
|
88
|
+
case "MetadataDisplayReactor":
|
|
89
|
+
return "@ic-reactor/candid";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
81
92
|
function generateReactorFile(options) {
|
|
82
|
-
const {
|
|
93
|
+
const {
|
|
94
|
+
canisterName,
|
|
95
|
+
didFile,
|
|
96
|
+
clientManagerPath = "../../clients",
|
|
97
|
+
reactorClass = "DisplayReactor"
|
|
98
|
+
} = options;
|
|
83
99
|
const pascalName = toPascalCase(canisterName);
|
|
84
100
|
const reactorName = getReactorName(canisterName);
|
|
85
101
|
const serviceName = getServiceTypeName(canisterName);
|
|
86
102
|
const baseName = path2.basename(didFile, ".did");
|
|
87
103
|
const declarationsPath = `./declarations/${baseName}`;
|
|
88
|
-
|
|
104
|
+
const reactorImportSource = getReactorClassImportSource(reactorClass);
|
|
105
|
+
return `import { createActorHooks } from "@ic-reactor/react"
|
|
106
|
+
import { ${reactorClass} } from "${reactorImportSource}"
|
|
89
107
|
import { clientManager } from "${clientManagerPath}"
|
|
90
108
|
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
91
109
|
|
|
@@ -97,7 +115,7 @@ export type ${serviceName} = _SERVICE
|
|
|
97
115
|
* Auto-generated by @ic-reactor/codegen \u2014 do not edit.
|
|
98
116
|
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
99
117
|
*/
|
|
100
|
-
export const ${reactorName} = new
|
|
118
|
+
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
101
119
|
clientManager,
|
|
102
120
|
idlFactory,
|
|
103
121
|
name: "${canisterName}",
|
|
@@ -115,6 +133,12 @@ export const {
|
|
|
115
133
|
}
|
|
116
134
|
|
|
117
135
|
// src/pipeline.ts
|
|
136
|
+
function resolveReactorClass(canisterConfig) {
|
|
137
|
+
return canisterConfig.mode ?? "DisplayReactor";
|
|
138
|
+
}
|
|
139
|
+
function isGeneratedIndexFile(content) {
|
|
140
|
+
return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(");
|
|
141
|
+
}
|
|
118
142
|
async function runCanisterPipeline(options) {
|
|
119
143
|
const { canisterConfig, projectRoot, globalConfig } = options;
|
|
120
144
|
const { name, didFile, clientManagerPath } = canisterConfig;
|
|
@@ -154,15 +178,27 @@ async function runCanisterPipeline(options) {
|
|
|
154
178
|
};
|
|
155
179
|
}
|
|
156
180
|
const reactorPath = path3.join(canisterOutDir, "index.ts");
|
|
181
|
+
const reactorClass = resolveReactorClass(canisterConfig);
|
|
157
182
|
try {
|
|
158
183
|
const reactorContent = generateReactorFile({
|
|
159
184
|
canisterName: name,
|
|
160
185
|
didFile: resolvedDidFile,
|
|
161
|
-
clientManagerPath: resolvedClientManagerPath
|
|
186
|
+
clientManagerPath: resolvedClientManagerPath,
|
|
187
|
+
reactorClass
|
|
162
188
|
});
|
|
163
189
|
fs2.mkdirSync(canisterOutDir, { recursive: true });
|
|
164
|
-
fs2.
|
|
165
|
-
|
|
190
|
+
if (!fs2.existsSync(reactorPath)) {
|
|
191
|
+
fs2.writeFileSync(reactorPath, reactorContent);
|
|
192
|
+
files.push({ success: true, filePath: reactorPath });
|
|
193
|
+
} else {
|
|
194
|
+
const existingIndexContent = fs2.readFileSync(reactorPath, "utf-8");
|
|
195
|
+
if (isGeneratedIndexFile(existingIndexContent)) {
|
|
196
|
+
fs2.writeFileSync(reactorPath, reactorContent);
|
|
197
|
+
files.push({ success: true, filePath: reactorPath });
|
|
198
|
+
} else {
|
|
199
|
+
files.push({ success: true, filePath: reactorPath, skipped: true });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
166
202
|
} catch (err) {
|
|
167
203
|
files.push({
|
|
168
204
|
success: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ic-reactor/codegen",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Shared code generation utilities for IC Reactor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@ic-reactor/parser": "0.4.6"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "^25.
|
|
40
|
+
"@types/node": "^25.3.0",
|
|
41
41
|
"tsup": "^8.5.1",
|
|
42
42
|
"typescript": "^5.9.3",
|
|
43
43
|
"vitest": "^4.0.18"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`Reactor generator > keeps default behavior as DisplayReactor > display-reactor-index 1`] = `
|
|
4
|
+
"import { createActorHooks } from "@ic-reactor/react"
|
|
5
|
+
import { DisplayReactor } from "@ic-reactor/react"
|
|
6
|
+
import { clientManager } from "../../clients"
|
|
7
|
+
import { idlFactory, type _SERVICE } from "./declarations/backend"
|
|
8
|
+
|
|
9
|
+
export type BackendService = _SERVICE
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Backend Reactor
|
|
13
|
+
*
|
|
14
|
+
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
15
|
+
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
16
|
+
*/
|
|
17
|
+
export const backendReactor = new DisplayReactor<BackendService>({
|
|
18
|
+
clientManager,
|
|
19
|
+
idlFactory,
|
|
20
|
+
name: "backend",
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const {
|
|
24
|
+
useActorQuery: useBackendQuery,
|
|
25
|
+
useActorSuspenseQuery: useBackendSuspenseQuery,
|
|
26
|
+
useActorInfiniteQuery: useBackendInfiniteQuery,
|
|
27
|
+
useActorSuspenseInfiniteQuery: useBackendSuspenseInfiniteQuery,
|
|
28
|
+
useActorMutation: useBackendMutation,
|
|
29
|
+
useActorMethod: useBackendMethod,
|
|
30
|
+
} = createActorHooks(backendReactor)
|
|
31
|
+
"
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
exports[`Reactor generator > supports Reactor mode generation > reactor-index 1`] = `
|
|
35
|
+
"import { createActorHooks } from "@ic-reactor/react"
|
|
36
|
+
import { Reactor } from "@ic-reactor/react"
|
|
37
|
+
import { clientManager } from "../../clients"
|
|
38
|
+
import { idlFactory, type _SERVICE } from "./declarations/workflow_engine"
|
|
39
|
+
|
|
40
|
+
export type WorkflowEngineService = _SERVICE
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* WorkflowEngine Reactor
|
|
44
|
+
*
|
|
45
|
+
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
46
|
+
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
47
|
+
*/
|
|
48
|
+
export const workflowEngineReactor = new Reactor<WorkflowEngineService>({
|
|
49
|
+
clientManager,
|
|
50
|
+
idlFactory,
|
|
51
|
+
name: "workflow_engine",
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
export const {
|
|
55
|
+
useActorQuery: useWorkflowEngineQuery,
|
|
56
|
+
useActorSuspenseQuery: useWorkflowEngineSuspenseQuery,
|
|
57
|
+
useActorInfiniteQuery: useWorkflowEngineInfiniteQuery,
|
|
58
|
+
useActorSuspenseInfiniteQuery: useWorkflowEngineSuspenseInfiniteQuery,
|
|
59
|
+
useActorMutation: useWorkflowEngineMutation,
|
|
60
|
+
useActorMethod: useWorkflowEngineMethod,
|
|
61
|
+
} = createActorHooks(workflowEngineReactor)
|
|
62
|
+
"
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
exports[`Reactor generator > supports candid reactor subclasses > metadata-display-reactor-index 1`] = `
|
|
66
|
+
"import { createActorHooks } from "@ic-reactor/react"
|
|
67
|
+
import { MetadataDisplayReactor } from "@ic-reactor/candid"
|
|
68
|
+
import { clientManager } from "../../clients"
|
|
69
|
+
import { idlFactory, type _SERVICE } from "./declarations/ledger"
|
|
70
|
+
|
|
71
|
+
export type LedgerService = _SERVICE
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ledger Reactor
|
|
75
|
+
*
|
|
76
|
+
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
77
|
+
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
78
|
+
*/
|
|
79
|
+
export const ledgerReactor = new MetadataDisplayReactor<LedgerService>({
|
|
80
|
+
clientManager,
|
|
81
|
+
idlFactory,
|
|
82
|
+
name: "ledger",
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
export const {
|
|
86
|
+
useActorQuery: useLedgerQuery,
|
|
87
|
+
useActorSuspenseQuery: useLedgerSuspenseQuery,
|
|
88
|
+
useActorInfiniteQuery: useLedgerInfiniteQuery,
|
|
89
|
+
useActorSuspenseInfiniteQuery: useLedgerSuspenseInfiniteQuery,
|
|
90
|
+
useActorMutation: useLedgerMutation,
|
|
91
|
+
useActorMethod: useLedgerMethod,
|
|
92
|
+
} = createActorHooks(ledgerReactor)
|
|
93
|
+
"
|
|
94
|
+
`;
|
|
@@ -22,6 +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
26
|
|
|
26
27
|
export interface ReactorGeneratorOptions {
|
|
27
28
|
/** Canister name (e.g. "backend") */
|
|
@@ -36,13 +37,37 @@ export interface ReactorGeneratorOptions {
|
|
|
36
37
|
* Default: "../../clients"
|
|
37
38
|
*/
|
|
38
39
|
clientManagerPath?: string
|
|
40
|
+
/**
|
|
41
|
+
* Which reactor class should back the generated hooks.
|
|
42
|
+
* Default: "DisplayReactor" (backward compatible)
|
|
43
|
+
*/
|
|
44
|
+
reactorClass?: ReactorClassName
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getReactorClassImportSource(
|
|
48
|
+
reactorClass: ReactorClassName
|
|
49
|
+
): "@ic-reactor/react" | "@ic-reactor/candid" {
|
|
50
|
+
switch (reactorClass) {
|
|
51
|
+
case "Reactor":
|
|
52
|
+
case "DisplayReactor":
|
|
53
|
+
return "@ic-reactor/react"
|
|
54
|
+
case "CandidReactor":
|
|
55
|
+
case "CandidDisplayReactor":
|
|
56
|
+
case "MetadataDisplayReactor":
|
|
57
|
+
return "@ic-reactor/candid"
|
|
58
|
+
}
|
|
39
59
|
}
|
|
40
60
|
|
|
41
61
|
/**
|
|
42
|
-
* Generate the content of a canister's `index.ts`
|
|
62
|
+
* Generate the content of a canister's `index.ts` file.
|
|
43
63
|
*/
|
|
44
64
|
export function generateReactorFile(options: ReactorGeneratorOptions): string {
|
|
45
|
-
const {
|
|
65
|
+
const {
|
|
66
|
+
canisterName,
|
|
67
|
+
didFile,
|
|
68
|
+
clientManagerPath = "../../clients",
|
|
69
|
+
reactorClass = "DisplayReactor",
|
|
70
|
+
} = options
|
|
46
71
|
|
|
47
72
|
const pascalName = toPascalCase(canisterName)
|
|
48
73
|
const reactorName = getReactorName(canisterName)
|
|
@@ -51,8 +76,10 @@ export function generateReactorFile(options: ReactorGeneratorOptions): string {
|
|
|
51
76
|
// Derive the declarations import path from the .did filename
|
|
52
77
|
const baseName = path.basename(didFile, ".did")
|
|
53
78
|
const declarationsPath = `./declarations/${baseName}`
|
|
79
|
+
const reactorImportSource = getReactorClassImportSource(reactorClass)
|
|
54
80
|
|
|
55
|
-
return `import {
|
|
81
|
+
return `import { createActorHooks } from "@ic-reactor/react"
|
|
82
|
+
import { ${reactorClass} } from "${reactorImportSource}"
|
|
56
83
|
import { clientManager } from "${clientManagerPath}"
|
|
57
84
|
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
58
85
|
|
|
@@ -64,7 +91,7 @@ export type ${serviceName} = _SERVICE
|
|
|
64
91
|
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
65
92
|
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
66
93
|
*/
|
|
67
|
-
export const ${reactorName} = new
|
|
94
|
+
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
68
95
|
clientManager,
|
|
69
96
|
idlFactory,
|
|
70
97
|
name: "${canisterName}",
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// Core Types
|
|
9
|
-
export type {
|
|
9
|
+
export type {
|
|
10
|
+
CanisterConfig,
|
|
11
|
+
CodegenConfig,
|
|
12
|
+
GeneratorResult,
|
|
13
|
+
ReactorClassName,
|
|
14
|
+
} from "./types.js"
|
|
10
15
|
|
|
11
16
|
// Pipeline (Primary Entry Point)
|
|
12
17
|
export { runCanisterPipeline } from "./pipeline.js"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
5
|
+
import { runCanisterPipeline } from "./pipeline"
|
|
6
|
+
|
|
7
|
+
describe("Codegen pipeline", () => {
|
|
8
|
+
const tempDirs: string[] = []
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
for (const dir of tempDirs) {
|
|
12
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
13
|
+
}
|
|
14
|
+
tempDirs.length = 0
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
function createTempProject() {
|
|
18
|
+
const projectRoot = fs.mkdtempSync(
|
|
19
|
+
path.join(os.tmpdir(), "ic-reactor-codegen-pipeline-")
|
|
20
|
+
)
|
|
21
|
+
tempDirs.push(projectRoot)
|
|
22
|
+
return projectRoot
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeDid(projectRoot: string, fileName: string) {
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(projectRoot, fileName),
|
|
28
|
+
`service : {
|
|
29
|
+
greet: (text) -> (text) query;
|
|
30
|
+
}`
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
it("uses per-canister mode to generate Reactor-based hooks", async () => {
|
|
35
|
+
const projectRoot = createTempProject()
|
|
36
|
+
writeDid(projectRoot, "workflow_engine.did")
|
|
37
|
+
|
|
38
|
+
const result = await runCanisterPipeline({
|
|
39
|
+
canisterConfig: {
|
|
40
|
+
name: "workflow_engine",
|
|
41
|
+
didFile: "workflow_engine.did",
|
|
42
|
+
mode: "Reactor",
|
|
43
|
+
},
|
|
44
|
+
projectRoot,
|
|
45
|
+
globalConfig: {
|
|
46
|
+
outDir: "src/declarations",
|
|
47
|
+
clientManagerPath: "../../clients",
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
expect(result.success).toBe(true)
|
|
52
|
+
|
|
53
|
+
const indexPath = path.join(
|
|
54
|
+
projectRoot,
|
|
55
|
+
"src/declarations/workflow_engine/index.ts"
|
|
56
|
+
)
|
|
57
|
+
const generated = fs.readFileSync(indexPath, "utf-8")
|
|
58
|
+
|
|
59
|
+
expect(generated).toContain("new Reactor<WorkflowEngineService>")
|
|
60
|
+
expect(generated).not.toContain("new DisplayReactor<WorkflowEngineService>")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("does not overwrite user-modified index.ts on regenerate", async () => {
|
|
64
|
+
const projectRoot = createTempProject()
|
|
65
|
+
writeDid(projectRoot, "backend.did")
|
|
66
|
+
|
|
67
|
+
const options = {
|
|
68
|
+
canisterConfig: {
|
|
69
|
+
name: "backend",
|
|
70
|
+
didFile: "backend.did",
|
|
71
|
+
},
|
|
72
|
+
projectRoot,
|
|
73
|
+
globalConfig: {
|
|
74
|
+
outDir: "src/declarations",
|
|
75
|
+
clientManagerPath: "../../clients",
|
|
76
|
+
},
|
|
77
|
+
} as const
|
|
78
|
+
|
|
79
|
+
const first = await runCanisterPipeline(options)
|
|
80
|
+
expect(first.success).toBe(true)
|
|
81
|
+
|
|
82
|
+
const indexPath = path.join(
|
|
83
|
+
projectRoot,
|
|
84
|
+
"src/declarations/backend/index.ts"
|
|
85
|
+
)
|
|
86
|
+
fs.writeFileSync(
|
|
87
|
+
indexPath,
|
|
88
|
+
`// user custom canister file
|
|
89
|
+
export const customBackendIndex = true
|
|
90
|
+
`
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const second = await runCanisterPipeline(options)
|
|
94
|
+
expect(second.success).toBe(true)
|
|
95
|
+
|
|
96
|
+
const wrapper = fs.readFileSync(indexPath, "utf-8")
|
|
97
|
+
expect(wrapper).toContain("customBackendIndex = true")
|
|
98
|
+
expect(second.files).toEqual(
|
|
99
|
+
expect.arrayContaining([
|
|
100
|
+
expect.objectContaining({
|
|
101
|
+
filePath: indexPath,
|
|
102
|
+
skipped: true,
|
|
103
|
+
success: true,
|
|
104
|
+
}),
|
|
105
|
+
])
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("overwrites legacy generated index.ts during regeneration", async () => {
|
|
110
|
+
const projectRoot = createTempProject()
|
|
111
|
+
writeDid(projectRoot, "backend.did")
|
|
112
|
+
|
|
113
|
+
const canisterOutDir = path.join(projectRoot, "src/declarations/backend")
|
|
114
|
+
fs.mkdirSync(canisterOutDir, { recursive: true })
|
|
115
|
+
|
|
116
|
+
// Simulate a generated index.ts content from older versions.
|
|
117
|
+
fs.writeFileSync(
|
|
118
|
+
path.join(canisterOutDir, "index.ts"),
|
|
119
|
+
`import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
|
|
120
|
+
import { clientManager } from "../../clients"
|
|
121
|
+
import { idlFactory, type _SERVICE } from "./declarations/backend"
|
|
122
|
+
|
|
123
|
+
export type BackendService = _SERVICE
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Backend Reactor
|
|
127
|
+
*
|
|
128
|
+
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
129
|
+
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
130
|
+
*/
|
|
131
|
+
export const backendReactor = new DisplayReactor<BackendService>({
|
|
132
|
+
clientManager,
|
|
133
|
+
idlFactory,
|
|
134
|
+
name: "backend",
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
export const {
|
|
138
|
+
useActorQuery: useBackendQuery,
|
|
139
|
+
} = createActorHooks(backendReactor)
|
|
140
|
+
`
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const result = await runCanisterPipeline({
|
|
144
|
+
canisterConfig: {
|
|
145
|
+
name: "backend",
|
|
146
|
+
didFile: "backend.did",
|
|
147
|
+
},
|
|
148
|
+
projectRoot,
|
|
149
|
+
globalConfig: {
|
|
150
|
+
outDir: "src/declarations",
|
|
151
|
+
clientManagerPath: "../../clients",
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
expect(result.success).toBe(true)
|
|
156
|
+
|
|
157
|
+
const indexPath = path.join(canisterOutDir, "index.ts")
|
|
158
|
+
const generated = fs.readFileSync(indexPath, "utf-8")
|
|
159
|
+
expect(generated).toContain("new DisplayReactor<BackendService>")
|
|
160
|
+
expect(generated).toContain("useBackendMutation")
|
|
161
|
+
expect(generated).not.toContain('export * from "./index.generated"')
|
|
162
|
+
})
|
|
163
|
+
})
|
package/src/pipeline.ts
CHANGED
|
@@ -7,12 +7,17 @@
|
|
|
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 file (index.ts)
|
|
10
|
+
* 3. Generate reactor file (`index.ts`)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import fs from "node:fs"
|
|
14
14
|
import path from "node:path"
|
|
15
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
CanisterConfig,
|
|
17
|
+
CodegenConfig,
|
|
18
|
+
GeneratorResult,
|
|
19
|
+
ReactorClassName,
|
|
20
|
+
} from "./types.js"
|
|
16
21
|
import { generateDeclarations } from "./generators/declarations.js"
|
|
17
22
|
import { generateReactorFile } from "./generators/reactor.js"
|
|
18
23
|
|
|
@@ -30,6 +35,17 @@ export interface PipelineOptions {
|
|
|
30
35
|
globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
function resolveReactorClass(canisterConfig: CanisterConfig): ReactorClassName {
|
|
39
|
+
return canisterConfig.mode ?? "DisplayReactor"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isGeneratedIndexFile(content: string): boolean {
|
|
43
|
+
return (
|
|
44
|
+
content.includes("Auto-generated by @ic-reactor/codegen") &&
|
|
45
|
+
content.includes("createActorHooks(")
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
export interface PipelineResult {
|
|
34
50
|
canisterName: string
|
|
35
51
|
success: boolean
|
|
@@ -109,18 +125,30 @@ export async function runCanisterPipeline(
|
|
|
109
125
|
// ── Step 2: Reactor file ───────────────────────────────────────────────────
|
|
110
126
|
|
|
111
127
|
const reactorPath = path.join(canisterOutDir, "index.ts")
|
|
128
|
+
const reactorClass = resolveReactorClass(canisterConfig)
|
|
112
129
|
|
|
113
130
|
try {
|
|
114
131
|
const reactorContent = generateReactorFile({
|
|
115
132
|
canisterName: name,
|
|
116
133
|
didFile: resolvedDidFile,
|
|
117
134
|
clientManagerPath: resolvedClientManagerPath,
|
|
135
|
+
reactorClass,
|
|
118
136
|
})
|
|
119
137
|
|
|
120
138
|
fs.mkdirSync(canisterOutDir, { recursive: true })
|
|
121
|
-
fs.
|
|
122
|
-
|
|
123
|
-
|
|
139
|
+
if (!fs.existsSync(reactorPath)) {
|
|
140
|
+
fs.writeFileSync(reactorPath, reactorContent)
|
|
141
|
+
files.push({ success: true, filePath: reactorPath })
|
|
142
|
+
} else {
|
|
143
|
+
const existingIndexContent = fs.readFileSync(reactorPath, "utf-8")
|
|
144
|
+
|
|
145
|
+
if (isGeneratedIndexFile(existingIndexContent)) {
|
|
146
|
+
fs.writeFileSync(reactorPath, reactorContent)
|
|
147
|
+
files.push({ success: true, filePath: reactorPath })
|
|
148
|
+
} else {
|
|
149
|
+
files.push({ success: true, filePath: reactorPath, skipped: true })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
124
152
|
} catch (err) {
|
|
125
153
|
files.push({
|
|
126
154
|
success: false,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { generateReactorFile } from "./generators"
|
|
3
|
+
|
|
4
|
+
describe("Reactor generator", () => {
|
|
5
|
+
it("keeps default behavior as DisplayReactor", () => {
|
|
6
|
+
const content = generateReactorFile({
|
|
7
|
+
canisterName: "backend",
|
|
8
|
+
didFile: "mock/backend.did",
|
|
9
|
+
clientManagerPath: "../../clients",
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
expect(content).toMatchSnapshot("display-reactor-index")
|
|
13
|
+
expect(content).toContain("new DisplayReactor<BackendService>")
|
|
14
|
+
expect(content).not.toContain("export const BackendReactorMode")
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("supports Reactor mode generation", () => {
|
|
18
|
+
const content = generateReactorFile({
|
|
19
|
+
canisterName: "workflow_engine",
|
|
20
|
+
didFile: "mock/workflow_engine.did",
|
|
21
|
+
reactorClass: "Reactor",
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(content).toMatchSnapshot("reactor-index")
|
|
25
|
+
expect(content).toContain("new Reactor<WorkflowEngineService>")
|
|
26
|
+
expect(content).not.toContain("createWorkflowEngineDisplayReactor")
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("supports candid reactor subclasses", () => {
|
|
30
|
+
const content = generateReactorFile({
|
|
31
|
+
canisterName: "ledger",
|
|
32
|
+
didFile: "mock/ledger.did",
|
|
33
|
+
reactorClass: "MetadataDisplayReactor",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(content).toMatchSnapshot("metadata-display-reactor-index")
|
|
37
|
+
expect(content).toContain(
|
|
38
|
+
'import { createActorHooks } from "@ic-reactor/react"'
|
|
39
|
+
)
|
|
40
|
+
expect(content).toContain(
|
|
41
|
+
'import { MetadataDisplayReactor } from "@ic-reactor/candid"'
|
|
42
|
+
)
|
|
43
|
+
expect(content).toContain("new MetadataDisplayReactor<LedgerService>")
|
|
44
|
+
})
|
|
45
|
+
})
|
package/src/types.ts
CHANGED
|
@@ -24,10 +24,22 @@ export interface CanisterConfig {
|
|
|
24
24
|
* Example: "../../clients" → `import { clientManager } from "../../clients"`
|
|
25
25
|
*/
|
|
26
26
|
clientManagerPath?: string
|
|
27
|
+
/**
|
|
28
|
+
* Reactor class used for generated hooks in this canister.
|
|
29
|
+
* Defaults to DisplayReactor for backward compatibility.
|
|
30
|
+
*/
|
|
31
|
+
mode?: ReactorClassName
|
|
27
32
|
/** Optional fixed canister ID */
|
|
28
33
|
canisterId?: string
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
export type ReactorClassName =
|
|
37
|
+
| "Reactor"
|
|
38
|
+
| "DisplayReactor"
|
|
39
|
+
| "CandidReactor"
|
|
40
|
+
| "CandidDisplayReactor"
|
|
41
|
+
| "MetadataDisplayReactor"
|
|
42
|
+
|
|
31
43
|
/**
|
|
32
44
|
* Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
|
|
33
45
|
*/
|