@ic-reactor/codegen 0.7.1 → 0.8.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 +15 -1
- package/dist/index.cjs +69 -7
- package/dist/index.d.cts +23 -4
- package/dist/index.d.ts +23 -4
- package/dist/index.js +68 -7
- package/package.json +2 -2
- package/src/__snapshots__/reactor.test.ts.snap +94 -0
- package/src/generators/index.ts +1 -1
- package/src/generators/reactor.ts +54 -7
- package/src/index.ts +6 -1
- package/src/pipeline.test.ts +225 -0
- package/src/pipeline.ts +59 -5
- package/src/reactor.test.ts +63 -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,25 @@ 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 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
|
+
|
|
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`
|
|
48
|
+
- **`generateReactorFile`**: Generates the managed `index.generated.ts` implementation using either `DisplayReactor` or `Reactor`.
|
|
49
|
+
- **`generateReactorEntryFile`**: Generates the stable `index.ts` wrapper that re-exports from `index.generated.ts`.
|
|
36
50
|
- **`generateClientFile`**: Generates a `ClientManager` boilerplate file.
|
|
37
51
|
|
|
38
52
|
## Utilities
|
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
extractMethods: () => extractMethods,
|
|
35
35
|
generateClientFile: () => generateClientFile,
|
|
36
36
|
generateDeclarations: () => generateDeclarations,
|
|
37
|
+
generateReactorEntryFile: () => generateReactorEntryFile,
|
|
37
38
|
generateReactorFile: () => generateReactorFile,
|
|
38
39
|
getReactorName: () => getReactorName,
|
|
39
40
|
getServiceTypeName: () => getServiceTypeName,
|
|
@@ -123,14 +124,35 @@ function getServiceTypeName(canisterName) {
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
// src/generators/reactor.ts
|
|
127
|
+
function getReactorClassImportSource(reactorClass) {
|
|
128
|
+
switch (reactorClass) {
|
|
129
|
+
case "Reactor":
|
|
130
|
+
case "DisplayReactor":
|
|
131
|
+
return "@ic-reactor/react";
|
|
132
|
+
case "CandidReactor":
|
|
133
|
+
case "CandidDisplayReactor":
|
|
134
|
+
case "MetadataDisplayReactor":
|
|
135
|
+
return "@ic-reactor/candid";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
126
138
|
function generateReactorFile(options) {
|
|
127
|
-
const {
|
|
139
|
+
const {
|
|
140
|
+
canisterName,
|
|
141
|
+
didFile,
|
|
142
|
+
clientManagerPath = "../../clients",
|
|
143
|
+
canisterId,
|
|
144
|
+
reactorClass = "DisplayReactor"
|
|
145
|
+
} = options;
|
|
128
146
|
const pascalName = toPascalCase(canisterName);
|
|
129
147
|
const reactorName = getReactorName(canisterName);
|
|
130
148
|
const serviceName = getServiceTypeName(canisterName);
|
|
131
149
|
const baseName = import_node_path2.default.basename(didFile, ".did");
|
|
132
150
|
const declarationsPath = `./declarations/${baseName}`;
|
|
133
|
-
|
|
151
|
+
const reactorImportSource = getReactorClassImportSource(reactorClass);
|
|
152
|
+
const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
|
|
153
|
+
` : "";
|
|
154
|
+
return `import { createActorHooks } from "@ic-reactor/react"
|
|
155
|
+
import { ${reactorClass} } from "${reactorImportSource}"
|
|
134
156
|
import { clientManager } from "${clientManagerPath}"
|
|
135
157
|
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
136
158
|
|
|
@@ -140,12 +162,12 @@ export type ${serviceName} = _SERVICE
|
|
|
140
162
|
* ${pascalName} Reactor
|
|
141
163
|
*
|
|
142
164
|
* Auto-generated by @ic-reactor/codegen \u2014 do not edit.
|
|
143
|
-
*
|
|
165
|
+
* This file is overwritten whenever generation runs.
|
|
144
166
|
*/
|
|
145
|
-
export const ${reactorName} = new
|
|
167
|
+
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
146
168
|
clientManager,
|
|
147
169
|
idlFactory,
|
|
148
|
-
name: "${canisterName}",
|
|
170
|
+
${canisterIdLine} name: "${canisterName}",
|
|
149
171
|
})
|
|
150
172
|
|
|
151
173
|
export const {
|
|
@@ -158,8 +180,30 @@ export const {
|
|
|
158
180
|
} = createActorHooks(${reactorName})
|
|
159
181
|
`;
|
|
160
182
|
}
|
|
183
|
+
function generateReactorEntryFile() {
|
|
184
|
+
return `/**
|
|
185
|
+
* Canister entrypoint.
|
|
186
|
+
*
|
|
187
|
+
* 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.
|
|
189
|
+
*/
|
|
190
|
+
export * from "./index.generated"
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
161
193
|
|
|
162
194
|
// src/pipeline.ts
|
|
195
|
+
function resolveReactorClass(canisterConfig) {
|
|
196
|
+
return canisterConfig.mode ?? "DisplayReactor";
|
|
197
|
+
}
|
|
198
|
+
function normalizeFileContent(content) {
|
|
199
|
+
return content.replace(/\r\n/g, "\n").trim();
|
|
200
|
+
}
|
|
201
|
+
function isLegacyGeneratedIndexFile(content) {
|
|
202
|
+
return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(");
|
|
203
|
+
}
|
|
204
|
+
function isManagedEntryWrapper(content, expectedEntryContent) {
|
|
205
|
+
return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
|
|
206
|
+
}
|
|
163
207
|
async function runCanisterPipeline(options) {
|
|
164
208
|
const { canisterConfig, projectRoot, globalConfig } = options;
|
|
165
209
|
const { name, didFile, clientManagerPath } = canisterConfig;
|
|
@@ -198,16 +242,33 @@ async function runCanisterPipeline(options) {
|
|
|
198
242
|
error: `Declarations step failed: ${err instanceof Error ? err.message : String(err)}`
|
|
199
243
|
};
|
|
200
244
|
}
|
|
201
|
-
const reactorPath = import_node_path3.default.join(canisterOutDir, "index.ts");
|
|
245
|
+
const reactorPath = import_node_path3.default.join(canisterOutDir, "index.generated.ts");
|
|
246
|
+
const entryPath = import_node_path3.default.join(canisterOutDir, "index.ts");
|
|
247
|
+
const reactorClass = resolveReactorClass(canisterConfig);
|
|
202
248
|
try {
|
|
203
249
|
const reactorContent = generateReactorFile({
|
|
204
250
|
canisterName: name,
|
|
205
251
|
didFile: resolvedDidFile,
|
|
206
|
-
clientManagerPath: resolvedClientManagerPath
|
|
252
|
+
clientManagerPath: resolvedClientManagerPath,
|
|
253
|
+
canisterId: canisterConfig.canisterId,
|
|
254
|
+
reactorClass
|
|
207
255
|
});
|
|
256
|
+
const entryContent = generateReactorEntryFile();
|
|
208
257
|
import_node_fs2.default.mkdirSync(canisterOutDir, { recursive: true });
|
|
209
258
|
import_node_fs2.default.writeFileSync(reactorPath, reactorContent);
|
|
210
259
|
files.push({ success: true, filePath: reactorPath });
|
|
260
|
+
if (!import_node_fs2.default.existsSync(entryPath)) {
|
|
261
|
+
import_node_fs2.default.writeFileSync(entryPath, entryContent);
|
|
262
|
+
files.push({ success: true, filePath: entryPath });
|
|
263
|
+
} else {
|
|
264
|
+
const existingEntryContent = import_node_fs2.default.readFileSync(entryPath, "utf-8");
|
|
265
|
+
if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
|
|
266
|
+
import_node_fs2.default.writeFileSync(entryPath, entryContent);
|
|
267
|
+
files.push({ success: true, filePath: entryPath });
|
|
268
|
+
} else {
|
|
269
|
+
files.push({ success: true, filePath: entryPath, skipped: true });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
211
272
|
} catch (err) {
|
|
212
273
|
files.push({
|
|
213
274
|
success: false,
|
|
@@ -289,6 +350,7 @@ export const clientManager = new ClientManager({
|
|
|
289
350
|
extractMethods,
|
|
290
351
|
generateClientFile,
|
|
291
352
|
generateDeclarations,
|
|
353
|
+
generateReactorEntryFile,
|
|
292
354
|
generateReactorFile,
|
|
293
355
|
getReactorName,
|
|
294
356
|
getServiceTypeName,
|
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,8 @@ 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
|
|
71
|
+
* 3. Generate reactor implementation (`index.generated.ts`)
|
|
72
|
+
* 4. Create or migrate the user entry (`index.ts`)
|
|
66
73
|
*/
|
|
67
74
|
|
|
68
75
|
interface PipelineOptions {
|
|
@@ -186,7 +193,7 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
|
|
|
186
193
|
/**
|
|
187
194
|
* Reactor File Generator
|
|
188
195
|
*
|
|
189
|
-
* Generates the
|
|
196
|
+
* Generates the managed `index.generated.ts` implementation for a canister
|
|
190
197
|
* plus the full set of typed hooks via `createActorHooks`.
|
|
191
198
|
*
|
|
192
199
|
* Generated output example (for canister "backend"):
|
|
@@ -204,6 +211,7 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
|
|
|
204
211
|
* ...
|
|
205
212
|
* } = createActorHooks(backendReactor)
|
|
206
213
|
*/
|
|
214
|
+
|
|
207
215
|
interface ReactorGeneratorOptions {
|
|
208
216
|
/** Canister name (e.g. "backend") */
|
|
209
217
|
canisterName: string;
|
|
@@ -217,11 +225,22 @@ interface ReactorGeneratorOptions {
|
|
|
217
225
|
* Default: "../../clients"
|
|
218
226
|
*/
|
|
219
227
|
clientManagerPath?: string;
|
|
228
|
+
/** Optional fixed canister ID for the generated reactor */
|
|
229
|
+
canisterId?: string;
|
|
230
|
+
/**
|
|
231
|
+
* Which reactor class should back the generated hooks.
|
|
232
|
+
* Default: "DisplayReactor" (backward compatible)
|
|
233
|
+
*/
|
|
234
|
+
reactorClass?: ReactorClassName;
|
|
220
235
|
}
|
|
221
236
|
/**
|
|
222
|
-
* Generate the content of a canister's
|
|
237
|
+
* Generate the content of a canister's managed implementation file.
|
|
223
238
|
*/
|
|
224
239
|
declare function generateReactorFile(options: ReactorGeneratorOptions): string;
|
|
240
|
+
/**
|
|
241
|
+
* Generate the user-facing `index.ts` wrapper content.
|
|
242
|
+
*/
|
|
243
|
+
declare function generateReactorEntryFile(): string;
|
|
225
244
|
|
|
226
245
|
/**
|
|
227
246
|
* Client Manager Generator
|
|
@@ -246,4 +265,4 @@ interface ClientGeneratorOptions {
|
|
|
246
265
|
*/
|
|
247
266
|
declare function generateClientFile(options?: ClientGeneratorOptions): string;
|
|
248
267
|
|
|
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 };
|
|
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 };
|
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,8 @@ 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
|
|
71
|
+
* 3. Generate reactor implementation (`index.generated.ts`)
|
|
72
|
+
* 4. Create or migrate the user entry (`index.ts`)
|
|
66
73
|
*/
|
|
67
74
|
|
|
68
75
|
interface PipelineOptions {
|
|
@@ -186,7 +193,7 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
|
|
|
186
193
|
/**
|
|
187
194
|
* Reactor File Generator
|
|
188
195
|
*
|
|
189
|
-
* Generates the
|
|
196
|
+
* Generates the managed `index.generated.ts` implementation for a canister
|
|
190
197
|
* plus the full set of typed hooks via `createActorHooks`.
|
|
191
198
|
*
|
|
192
199
|
* Generated output example (for canister "backend"):
|
|
@@ -204,6 +211,7 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
|
|
|
204
211
|
* ...
|
|
205
212
|
* } = createActorHooks(backendReactor)
|
|
206
213
|
*/
|
|
214
|
+
|
|
207
215
|
interface ReactorGeneratorOptions {
|
|
208
216
|
/** Canister name (e.g. "backend") */
|
|
209
217
|
canisterName: string;
|
|
@@ -217,11 +225,22 @@ interface ReactorGeneratorOptions {
|
|
|
217
225
|
* Default: "../../clients"
|
|
218
226
|
*/
|
|
219
227
|
clientManagerPath?: string;
|
|
228
|
+
/** Optional fixed canister ID for the generated reactor */
|
|
229
|
+
canisterId?: string;
|
|
230
|
+
/**
|
|
231
|
+
* Which reactor class should back the generated hooks.
|
|
232
|
+
* Default: "DisplayReactor" (backward compatible)
|
|
233
|
+
*/
|
|
234
|
+
reactorClass?: ReactorClassName;
|
|
220
235
|
}
|
|
221
236
|
/**
|
|
222
|
-
* Generate the content of a canister's
|
|
237
|
+
* Generate the content of a canister's managed implementation file.
|
|
223
238
|
*/
|
|
224
239
|
declare function generateReactorFile(options: ReactorGeneratorOptions): string;
|
|
240
|
+
/**
|
|
241
|
+
* Generate the user-facing `index.ts` wrapper content.
|
|
242
|
+
*/
|
|
243
|
+
declare function generateReactorEntryFile(): string;
|
|
225
244
|
|
|
226
245
|
/**
|
|
227
246
|
* Client Manager Generator
|
|
@@ -246,4 +265,4 @@ interface ClientGeneratorOptions {
|
|
|
246
265
|
*/
|
|
247
266
|
declare function generateClientFile(options?: ClientGeneratorOptions): string;
|
|
248
267
|
|
|
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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -78,14 +78,35 @@ 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
|
+
canisterId,
|
|
98
|
+
reactorClass = "DisplayReactor"
|
|
99
|
+
} = options;
|
|
83
100
|
const pascalName = toPascalCase(canisterName);
|
|
84
101
|
const reactorName = getReactorName(canisterName);
|
|
85
102
|
const serviceName = getServiceTypeName(canisterName);
|
|
86
103
|
const baseName = path2.basename(didFile, ".did");
|
|
87
104
|
const declarationsPath = `./declarations/${baseName}`;
|
|
88
|
-
|
|
105
|
+
const reactorImportSource = getReactorClassImportSource(reactorClass);
|
|
106
|
+
const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
|
|
107
|
+
` : "";
|
|
108
|
+
return `import { createActorHooks } from "@ic-reactor/react"
|
|
109
|
+
import { ${reactorClass} } from "${reactorImportSource}"
|
|
89
110
|
import { clientManager } from "${clientManagerPath}"
|
|
90
111
|
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
91
112
|
|
|
@@ -95,12 +116,12 @@ export type ${serviceName} = _SERVICE
|
|
|
95
116
|
* ${pascalName} Reactor
|
|
96
117
|
*
|
|
97
118
|
* Auto-generated by @ic-reactor/codegen \u2014 do not edit.
|
|
98
|
-
*
|
|
119
|
+
* This file is overwritten whenever generation runs.
|
|
99
120
|
*/
|
|
100
|
-
export const ${reactorName} = new
|
|
121
|
+
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
101
122
|
clientManager,
|
|
102
123
|
idlFactory,
|
|
103
|
-
name: "${canisterName}",
|
|
124
|
+
${canisterIdLine} name: "${canisterName}",
|
|
104
125
|
})
|
|
105
126
|
|
|
106
127
|
export const {
|
|
@@ -113,8 +134,30 @@ export const {
|
|
|
113
134
|
} = createActorHooks(${reactorName})
|
|
114
135
|
`;
|
|
115
136
|
}
|
|
137
|
+
function generateReactorEntryFile() {
|
|
138
|
+
return `/**
|
|
139
|
+
* Canister entrypoint.
|
|
140
|
+
*
|
|
141
|
+
* 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.
|
|
143
|
+
*/
|
|
144
|
+
export * from "./index.generated"
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
116
147
|
|
|
117
148
|
// src/pipeline.ts
|
|
149
|
+
function resolveReactorClass(canisterConfig) {
|
|
150
|
+
return canisterConfig.mode ?? "DisplayReactor";
|
|
151
|
+
}
|
|
152
|
+
function normalizeFileContent(content) {
|
|
153
|
+
return content.replace(/\r\n/g, "\n").trim();
|
|
154
|
+
}
|
|
155
|
+
function isLegacyGeneratedIndexFile(content) {
|
|
156
|
+
return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(");
|
|
157
|
+
}
|
|
158
|
+
function isManagedEntryWrapper(content, expectedEntryContent) {
|
|
159
|
+
return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
|
|
160
|
+
}
|
|
118
161
|
async function runCanisterPipeline(options) {
|
|
119
162
|
const { canisterConfig, projectRoot, globalConfig } = options;
|
|
120
163
|
const { name, didFile, clientManagerPath } = canisterConfig;
|
|
@@ -153,16 +196,33 @@ async function runCanisterPipeline(options) {
|
|
|
153
196
|
error: `Declarations step failed: ${err instanceof Error ? err.message : String(err)}`
|
|
154
197
|
};
|
|
155
198
|
}
|
|
156
|
-
const reactorPath = path3.join(canisterOutDir, "index.ts");
|
|
199
|
+
const reactorPath = path3.join(canisterOutDir, "index.generated.ts");
|
|
200
|
+
const entryPath = path3.join(canisterOutDir, "index.ts");
|
|
201
|
+
const reactorClass = resolveReactorClass(canisterConfig);
|
|
157
202
|
try {
|
|
158
203
|
const reactorContent = generateReactorFile({
|
|
159
204
|
canisterName: name,
|
|
160
205
|
didFile: resolvedDidFile,
|
|
161
|
-
clientManagerPath: resolvedClientManagerPath
|
|
206
|
+
clientManagerPath: resolvedClientManagerPath,
|
|
207
|
+
canisterId: canisterConfig.canisterId,
|
|
208
|
+
reactorClass
|
|
162
209
|
});
|
|
210
|
+
const entryContent = generateReactorEntryFile();
|
|
163
211
|
fs2.mkdirSync(canisterOutDir, { recursive: true });
|
|
164
212
|
fs2.writeFileSync(reactorPath, reactorContent);
|
|
165
213
|
files.push({ success: true, filePath: reactorPath });
|
|
214
|
+
if (!fs2.existsSync(entryPath)) {
|
|
215
|
+
fs2.writeFileSync(entryPath, entryContent);
|
|
216
|
+
files.push({ success: true, filePath: entryPath });
|
|
217
|
+
} else {
|
|
218
|
+
const existingEntryContent = fs2.readFileSync(entryPath, "utf-8");
|
|
219
|
+
if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
|
|
220
|
+
fs2.writeFileSync(entryPath, entryContent);
|
|
221
|
+
files.push({ success: true, filePath: entryPath });
|
|
222
|
+
} else {
|
|
223
|
+
files.push({ success: true, filePath: entryPath, skipped: true });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
166
226
|
} catch (err) {
|
|
167
227
|
files.push({
|
|
168
228
|
success: false,
|
|
@@ -243,6 +303,7 @@ export {
|
|
|
243
303
|
extractMethods,
|
|
244
304
|
generateClientFile,
|
|
245
305
|
generateDeclarations,
|
|
306
|
+
generateReactorEntryFile,
|
|
246
307
|
generateReactorFile,
|
|
247
308
|
getReactorName,
|
|
248
309
|
getServiceTypeName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ic-reactor/codegen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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.5",
|
|
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
|
+
* This file is overwritten whenever generation runs.
|
|
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
|
+
* This file is overwritten whenever generation runs.
|
|
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
|
+
* This file is overwritten whenever generation runs.
|
|
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
|
+
`;
|
package/src/generators/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type {
|
|
|
11
11
|
DeclarationsGeneratorResult,
|
|
12
12
|
} from "./declarations.js"
|
|
13
13
|
|
|
14
|
-
export { generateReactorFile } from "./reactor.js"
|
|
14
|
+
export { generateReactorFile, generateReactorEntryFile } from "./reactor.js"
|
|
15
15
|
export type { ReactorGeneratorOptions } from "./reactor.js"
|
|
16
16
|
|
|
17
17
|
export { generateClientFile } from "./client.js"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reactor File Generator
|
|
3
3
|
*
|
|
4
|
-
* Generates the
|
|
4
|
+
* Generates the managed `index.generated.ts` implementation for a canister
|
|
5
5
|
* plus the full set of typed hooks via `createActorHooks`.
|
|
6
6
|
*
|
|
7
7
|
* Generated output example (for canister "backend"):
|
|
@@ -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,40 @@ export interface ReactorGeneratorOptions {
|
|
|
36
37
|
* Default: "../../clients"
|
|
37
38
|
*/
|
|
38
39
|
clientManagerPath?: string
|
|
40
|
+
/** Optional fixed canister ID for the generated reactor */
|
|
41
|
+
canisterId?: string
|
|
42
|
+
/**
|
|
43
|
+
* Which reactor class should back the generated hooks.
|
|
44
|
+
* Default: "DisplayReactor" (backward compatible)
|
|
45
|
+
*/
|
|
46
|
+
reactorClass?: ReactorClassName
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getReactorClassImportSource(
|
|
50
|
+
reactorClass: ReactorClassName
|
|
51
|
+
): "@ic-reactor/react" | "@ic-reactor/candid" {
|
|
52
|
+
switch (reactorClass) {
|
|
53
|
+
case "Reactor":
|
|
54
|
+
case "DisplayReactor":
|
|
55
|
+
return "@ic-reactor/react"
|
|
56
|
+
case "CandidReactor":
|
|
57
|
+
case "CandidDisplayReactor":
|
|
58
|
+
case "MetadataDisplayReactor":
|
|
59
|
+
return "@ic-reactor/candid"
|
|
60
|
+
}
|
|
39
61
|
}
|
|
40
62
|
|
|
41
63
|
/**
|
|
42
|
-
* Generate the content of a canister's
|
|
64
|
+
* Generate the content of a canister's managed implementation file.
|
|
43
65
|
*/
|
|
44
66
|
export function generateReactorFile(options: ReactorGeneratorOptions): string {
|
|
45
|
-
const {
|
|
67
|
+
const {
|
|
68
|
+
canisterName,
|
|
69
|
+
didFile,
|
|
70
|
+
clientManagerPath = "../../clients",
|
|
71
|
+
canisterId,
|
|
72
|
+
reactorClass = "DisplayReactor",
|
|
73
|
+
} = options
|
|
46
74
|
|
|
47
75
|
const pascalName = toPascalCase(canisterName)
|
|
48
76
|
const reactorName = getReactorName(canisterName)
|
|
@@ -51,8 +79,13 @@ export function generateReactorFile(options: ReactorGeneratorOptions): string {
|
|
|
51
79
|
// Derive the declarations import path from the .did filename
|
|
52
80
|
const baseName = path.basename(didFile, ".did")
|
|
53
81
|
const declarationsPath = `./declarations/${baseName}`
|
|
82
|
+
const reactorImportSource = getReactorClassImportSource(reactorClass)
|
|
83
|
+
const canisterIdLine = canisterId
|
|
84
|
+
? ` canisterId: ${JSON.stringify(canisterId)},\n`
|
|
85
|
+
: ""
|
|
54
86
|
|
|
55
|
-
return `import {
|
|
87
|
+
return `import { createActorHooks } from "@ic-reactor/react"
|
|
88
|
+
import { ${reactorClass} } from "${reactorImportSource}"
|
|
56
89
|
import { clientManager } from "${clientManagerPath}"
|
|
57
90
|
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
58
91
|
|
|
@@ -62,12 +95,12 @@ export type ${serviceName} = _SERVICE
|
|
|
62
95
|
* ${pascalName} Reactor
|
|
63
96
|
*
|
|
64
97
|
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
65
|
-
*
|
|
98
|
+
* This file is overwritten whenever generation runs.
|
|
66
99
|
*/
|
|
67
|
-
export const ${reactorName} = new
|
|
100
|
+
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
68
101
|
clientManager,
|
|
69
102
|
idlFactory,
|
|
70
|
-
name: "${canisterName}",
|
|
103
|
+
${canisterIdLine} name: "${canisterName}",
|
|
71
104
|
})
|
|
72
105
|
|
|
73
106
|
export const {
|
|
@@ -80,3 +113,17 @@ export const {
|
|
|
80
113
|
} = createActorHooks(${reactorName})
|
|
81
114
|
`
|
|
82
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate the user-facing `index.ts` wrapper content.
|
|
119
|
+
*/
|
|
120
|
+
export function generateReactorEntryFile(): string {
|
|
121
|
+
return `/**
|
|
122
|
+
* Canister entrypoint.
|
|
123
|
+
*
|
|
124
|
+
* 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.
|
|
126
|
+
*/
|
|
127
|
+
export * from "./index.generated"
|
|
128
|
+
`
|
|
129
|
+
}
|
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,225 @@
|
|
|
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.generated.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
|
+
expect(
|
|
62
|
+
fs.readFileSync(
|
|
63
|
+
path.join(projectRoot, "src/declarations/workflow_engine/index.ts"),
|
|
64
|
+
"utf-8"
|
|
65
|
+
)
|
|
66
|
+
).toContain('export * from "./index.generated"')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("writes a configured canisterId into the generated reactor", async () => {
|
|
70
|
+
const projectRoot = createTempProject()
|
|
71
|
+
writeDid(projectRoot, "workflow.did")
|
|
72
|
+
|
|
73
|
+
const result = await runCanisterPipeline({
|
|
74
|
+
canisterConfig: {
|
|
75
|
+
name: "workflow",
|
|
76
|
+
didFile: "workflow.did",
|
|
77
|
+
canisterId: "yq4ns-hyaaa-aaaap-akbna-cai",
|
|
78
|
+
},
|
|
79
|
+
projectRoot,
|
|
80
|
+
globalConfig: {
|
|
81
|
+
outDir: "src/declarations",
|
|
82
|
+
clientManagerPath: "../../clients",
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(result.success).toBe(true)
|
|
87
|
+
|
|
88
|
+
const indexPath = path.join(
|
|
89
|
+
projectRoot,
|
|
90
|
+
"src/declarations/workflow/index.generated.ts"
|
|
91
|
+
)
|
|
92
|
+
const generated = fs.readFileSync(indexPath, "utf-8")
|
|
93
|
+
|
|
94
|
+
expect(generated).toContain('canisterId: "yq4ns-hyaaa-aaaap-akbna-cai"')
|
|
95
|
+
expect(generated).toContain('name: "workflow"')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("does not overwrite user-modified index.ts on regenerate", async () => {
|
|
99
|
+
const projectRoot = createTempProject()
|
|
100
|
+
writeDid(projectRoot, "backend.did")
|
|
101
|
+
|
|
102
|
+
const options = {
|
|
103
|
+
canisterConfig: {
|
|
104
|
+
name: "backend",
|
|
105
|
+
didFile: "backend.did",
|
|
106
|
+
},
|
|
107
|
+
projectRoot,
|
|
108
|
+
globalConfig: {
|
|
109
|
+
outDir: "src/declarations",
|
|
110
|
+
clientManagerPath: "../../clients",
|
|
111
|
+
},
|
|
112
|
+
} as const
|
|
113
|
+
|
|
114
|
+
const first = await runCanisterPipeline(options)
|
|
115
|
+
expect(first.success).toBe(true)
|
|
116
|
+
|
|
117
|
+
const indexPath = path.join(
|
|
118
|
+
projectRoot,
|
|
119
|
+
"src/declarations/backend/index.ts"
|
|
120
|
+
)
|
|
121
|
+
fs.writeFileSync(
|
|
122
|
+
indexPath,
|
|
123
|
+
`// user custom canister file
|
|
124
|
+
export const customBackendIndex = true
|
|
125
|
+
`
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const second = await runCanisterPipeline({
|
|
129
|
+
...options,
|
|
130
|
+
canisterConfig: {
|
|
131
|
+
...options.canisterConfig,
|
|
132
|
+
canisterId: "yq4ns-hyaaa-aaaap-akbna-cai",
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
expect(second.success).toBe(true)
|
|
136
|
+
|
|
137
|
+
const wrapper = fs.readFileSync(indexPath, "utf-8")
|
|
138
|
+
expect(wrapper).toContain("customBackendIndex = true")
|
|
139
|
+
const generatedImpl = fs.readFileSync(
|
|
140
|
+
path.join(projectRoot, "src/declarations/backend/index.generated.ts"),
|
|
141
|
+
"utf-8"
|
|
142
|
+
)
|
|
143
|
+
expect(generatedImpl).toContain('canisterId: "yq4ns-hyaaa-aaaap-akbna-cai"')
|
|
144
|
+
expect(second.files).toEqual(
|
|
145
|
+
expect.arrayContaining([
|
|
146
|
+
expect.objectContaining({
|
|
147
|
+
filePath: indexPath,
|
|
148
|
+
skipped: true,
|
|
149
|
+
success: true,
|
|
150
|
+
}),
|
|
151
|
+
])
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it("migrates a legacy generated index.ts to the managed wrapper", async () => {
|
|
156
|
+
const projectRoot = createTempProject()
|
|
157
|
+
writeDid(projectRoot, "backend.did")
|
|
158
|
+
|
|
159
|
+
const canisterOutDir = path.join(projectRoot, "src/declarations/backend")
|
|
160
|
+
fs.mkdirSync(canisterOutDir, { recursive: true })
|
|
161
|
+
|
|
162
|
+
const existingGeneratedIndex = `import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
|
|
163
|
+
import { clientManager } from "../../clients"
|
|
164
|
+
import { idlFactory, type _SERVICE } from "./declarations/backend"
|
|
165
|
+
|
|
166
|
+
export type BackendService = _SERVICE
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Backend Reactor
|
|
170
|
+
*
|
|
171
|
+
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
172
|
+
*/
|
|
173
|
+
export const backendReactor = new DisplayReactor<BackendService>({
|
|
174
|
+
clientManager,
|
|
175
|
+
idlFactory,
|
|
176
|
+
name: "backend",
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
export const {
|
|
180
|
+
useActorQuery: useBackendQuery,
|
|
181
|
+
} = createActorHooks(backendReactor)
|
|
182
|
+
`
|
|
183
|
+
fs.writeFileSync(
|
|
184
|
+
path.join(canisterOutDir, "index.ts"),
|
|
185
|
+
existingGeneratedIndex
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
const result = await runCanisterPipeline({
|
|
189
|
+
canisterConfig: {
|
|
190
|
+
name: "backend",
|
|
191
|
+
didFile: "backend.did",
|
|
192
|
+
},
|
|
193
|
+
projectRoot,
|
|
194
|
+
globalConfig: {
|
|
195
|
+
outDir: "src/declarations",
|
|
196
|
+
clientManagerPath: "../../clients",
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
expect(result.success).toBe(true)
|
|
201
|
+
|
|
202
|
+
const indexPath = path.join(canisterOutDir, "index.ts")
|
|
203
|
+
const entry = fs.readFileSync(indexPath, "utf-8")
|
|
204
|
+
const generated = fs.readFileSync(
|
|
205
|
+
path.join(canisterOutDir, "index.generated.ts"),
|
|
206
|
+
"utf-8"
|
|
207
|
+
)
|
|
208
|
+
expect(entry).toContain('export * from "./index.generated"')
|
|
209
|
+
expect(entry).not.toBe(existingGeneratedIndex)
|
|
210
|
+
expect(generated).toContain("new DisplayReactor<BackendService>")
|
|
211
|
+
expect(generated).toContain("useBackendMutation")
|
|
212
|
+
expect(result.files).toEqual(
|
|
213
|
+
expect.arrayContaining([
|
|
214
|
+
expect.objectContaining({
|
|
215
|
+
filePath: path.join(canisterOutDir, "index.generated.ts"),
|
|
216
|
+
success: true,
|
|
217
|
+
}),
|
|
218
|
+
expect.objectContaining({
|
|
219
|
+
filePath: indexPath,
|
|
220
|
+
success: true,
|
|
221
|
+
}),
|
|
222
|
+
])
|
|
223
|
+
)
|
|
224
|
+
})
|
|
225
|
+
})
|
package/src/pipeline.ts
CHANGED
|
@@ -7,14 +7,23 @@
|
|
|
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
|
|
10
|
+
* 3. Generate reactor implementation (`index.generated.ts`)
|
|
11
|
+
* 4. Create or migrate the user entry (`index.ts`)
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import fs from "node:fs"
|
|
14
15
|
import path from "node:path"
|
|
15
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
CanisterConfig,
|
|
18
|
+
CodegenConfig,
|
|
19
|
+
GeneratorResult,
|
|
20
|
+
ReactorClassName,
|
|
21
|
+
} from "./types.js"
|
|
16
22
|
import { generateDeclarations } from "./generators/declarations.js"
|
|
17
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
generateReactorEntryFile,
|
|
25
|
+
generateReactorFile,
|
|
26
|
+
} from "./generators/reactor.js"
|
|
18
27
|
|
|
19
28
|
export interface PipelineOptions {
|
|
20
29
|
/** Canister name and config */
|
|
@@ -30,6 +39,30 @@ export interface PipelineOptions {
|
|
|
30
39
|
globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">
|
|
31
40
|
}
|
|
32
41
|
|
|
42
|
+
function resolveReactorClass(canisterConfig: CanisterConfig): ReactorClassName {
|
|
43
|
+
return canisterConfig.mode ?? "DisplayReactor"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeFileContent(content: string): string {
|
|
47
|
+
return content.replace(/\r\n/g, "\n").trim()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isLegacyGeneratedIndexFile(content: string): boolean {
|
|
51
|
+
return (
|
|
52
|
+
content.includes("Auto-generated by @ic-reactor/codegen") &&
|
|
53
|
+
content.includes("createActorHooks(")
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isManagedEntryWrapper(
|
|
58
|
+
content: string,
|
|
59
|
+
expectedEntryContent: string
|
|
60
|
+
): boolean {
|
|
61
|
+
return (
|
|
62
|
+
normalizeFileContent(content) === normalizeFileContent(expectedEntryContent)
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
33
66
|
export interface PipelineResult {
|
|
34
67
|
canisterName: string
|
|
35
68
|
success: boolean
|
|
@@ -108,19 +141,40 @@ export async function runCanisterPipeline(
|
|
|
108
141
|
|
|
109
142
|
// ── Step 2: Reactor file ───────────────────────────────────────────────────
|
|
110
143
|
|
|
111
|
-
const reactorPath = path.join(canisterOutDir, "index.ts")
|
|
144
|
+
const reactorPath = path.join(canisterOutDir, "index.generated.ts")
|
|
145
|
+
const entryPath = path.join(canisterOutDir, "index.ts")
|
|
146
|
+
const reactorClass = resolveReactorClass(canisterConfig)
|
|
112
147
|
|
|
113
148
|
try {
|
|
114
149
|
const reactorContent = generateReactorFile({
|
|
115
150
|
canisterName: name,
|
|
116
151
|
didFile: resolvedDidFile,
|
|
117
152
|
clientManagerPath: resolvedClientManagerPath,
|
|
153
|
+
canisterId: canisterConfig.canisterId,
|
|
154
|
+
reactorClass,
|
|
118
155
|
})
|
|
156
|
+
const entryContent = generateReactorEntryFile()
|
|
119
157
|
|
|
120
158
|
fs.mkdirSync(canisterOutDir, { recursive: true })
|
|
121
159
|
fs.writeFileSync(reactorPath, reactorContent)
|
|
122
|
-
|
|
123
160
|
files.push({ success: true, filePath: reactorPath })
|
|
161
|
+
|
|
162
|
+
if (!fs.existsSync(entryPath)) {
|
|
163
|
+
fs.writeFileSync(entryPath, entryContent)
|
|
164
|
+
files.push({ success: true, filePath: entryPath })
|
|
165
|
+
} else {
|
|
166
|
+
const existingEntryContent = fs.readFileSync(entryPath, "utf-8")
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
isLegacyGeneratedIndexFile(existingEntryContent) ||
|
|
170
|
+
isManagedEntryWrapper(existingEntryContent, entryContent)
|
|
171
|
+
) {
|
|
172
|
+
fs.writeFileSync(entryPath, entryContent)
|
|
173
|
+
files.push({ success: true, filePath: entryPath })
|
|
174
|
+
} else {
|
|
175
|
+
files.push({ success: true, filePath: entryPath, skipped: true })
|
|
176
|
+
}
|
|
177
|
+
}
|
|
124
178
|
} catch (err) {
|
|
125
179
|
files.push({
|
|
126
180
|
success: false,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { generateReactorEntryFile, 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
|
+
|
|
46
|
+
it("writes a fixed canisterId when configured", () => {
|
|
47
|
+
const content = generateReactorFile({
|
|
48
|
+
canisterName: "workflow",
|
|
49
|
+
didFile: "mock/workflow.did",
|
|
50
|
+
canisterId: "yq4ns-hyaaa-aaaap-akbna-cai",
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(content).toContain('canisterId: "yq4ns-hyaaa-aaaap-akbna-cai"')
|
|
54
|
+
expect(content).toContain('name: "workflow"')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("generates a stable entry wrapper", () => {
|
|
58
|
+
const content = generateReactorEntryFile()
|
|
59
|
+
|
|
60
|
+
expect(content).toContain('export * from "./index.generated"')
|
|
61
|
+
expect(content).toContain("safe to customize")
|
|
62
|
+
})
|
|
63
|
+
})
|
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
|
*/
|