@ic-reactor/codegen 0.5.0 → 0.6.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 +60 -1
- package/dist/index.cjs +78 -12
- package/dist/index.d.cts +30 -4
- package/dist/index.d.ts +30 -4
- package/dist/index.js +77 -12
- package/package.json +1 -1
- package/src/__snapshots__/reactor.test.ts.snap +100 -0
- package/src/generators/index.ts +1 -1
- package/src/generators/reactor.ts +57 -8
- package/src/index.ts +7 -1
- package/src/pipeline.test.ts +178 -0
- package/src/pipeline.ts +63 -9
- package/src/reactor.test.ts +42 -0
- package/src/types.ts +17 -0
package/README.md
CHANGED
|
@@ -23,16 +23,75 @@ await runCanisterPipeline({
|
|
|
23
23
|
globalConfig: {
|
|
24
24
|
outDir: "src/declarations",
|
|
25
25
|
clientManagerPath: "../../clients",
|
|
26
|
+
reactor: {
|
|
27
|
+
defaultMode: "display",
|
|
28
|
+
canisters: {
|
|
29
|
+
workflow_engine: "raw",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
26
32
|
},
|
|
27
33
|
})
|
|
28
34
|
```
|
|
29
35
|
|
|
36
|
+
## Reactor Mode Configuration
|
|
37
|
+
|
|
38
|
+
`reactor.defaultMode` controls whether generated default hook exports use:
|
|
39
|
+
|
|
40
|
+
- `display` → `DisplayReactor` (current/default behavior)
|
|
41
|
+
- `raw` → `Reactor` (raw Candid shapes)
|
|
42
|
+
|
|
43
|
+
`reactor.canisters` lets you override specific canisters without editing generated files.
|
|
44
|
+
|
|
45
|
+
## Generated File Layout (Stable Wrapper)
|
|
46
|
+
|
|
47
|
+
Each canister now generates:
|
|
48
|
+
|
|
49
|
+
- `index.generated.ts` (regenerated on every run)
|
|
50
|
+
- `index.ts` (created only if missing; not overwritten)
|
|
51
|
+
|
|
52
|
+
The stable wrapper defaults to:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
export * from "./index.generated"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This lets applications customize exports once and keep them across regenerations.
|
|
59
|
+
|
|
60
|
+
## Generated Output Examples
|
|
61
|
+
|
|
62
|
+
Display default:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
export function createBackendRawReactor() {
|
|
66
|
+
/* ... */
|
|
67
|
+
}
|
|
68
|
+
export function createBackendDisplayReactor() {
|
|
69
|
+
/* ... */
|
|
70
|
+
}
|
|
71
|
+
export const backendReactor = createBackendDisplayReactor()
|
|
72
|
+
export const BackendReactorMode = "display" as const
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Raw default:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
export function createWorkflowEngineRawReactor() {
|
|
79
|
+
/* ... */
|
|
80
|
+
}
|
|
81
|
+
export function createWorkflowEngineDisplayReactor() {
|
|
82
|
+
/* ... */
|
|
83
|
+
}
|
|
84
|
+
export const workflowEngineReactor = createWorkflowEngineRawReactor()
|
|
85
|
+
export const WorkflowEngineReactorMode = "raw" as const
|
|
86
|
+
```
|
|
87
|
+
|
|
30
88
|
## Generators
|
|
31
89
|
|
|
32
90
|
You can also use individual generators if you need more granular control:
|
|
33
91
|
|
|
34
92
|
- **`generateDeclarations`**: Generates `.js` (factory), `.d.ts` (types), and `.did` copy.
|
|
35
|
-
- **`generateReactorFile`**: Generates the `index.ts`
|
|
93
|
+
- **`generateReactorFile`**: Generates the `index.generated.ts` implementation with raw/display factories and typed hooks.
|
|
94
|
+
- **`generateReactorWrapperFile`**: Generates the stable `index.ts` wrapper (create-once, preserve-on-regenerate).
|
|
36
95
|
- **`generateClientFile`**: Generates a `ClientManager` boilerplate file.
|
|
37
96
|
|
|
38
97
|
## Utilities
|
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
generateClientFile: () => generateClientFile,
|
|
36
36
|
generateDeclarations: () => generateDeclarations,
|
|
37
37
|
generateReactorFile: () => generateReactorFile,
|
|
38
|
+
generateReactorWrapperFile: () => generateReactorWrapperFile,
|
|
38
39
|
getReactorName: () => getReactorName,
|
|
39
40
|
getServiceTypeName: () => getServiceTypeName,
|
|
40
41
|
parseDIDFile: () => parseDIDFile,
|
|
@@ -124,13 +125,21 @@ function getServiceTypeName(canisterName) {
|
|
|
124
125
|
|
|
125
126
|
// src/generators/reactor.ts
|
|
126
127
|
function generateReactorFile(options) {
|
|
127
|
-
const {
|
|
128
|
+
const {
|
|
129
|
+
canisterName,
|
|
130
|
+
didFile,
|
|
131
|
+
clientManagerPath = "../../clients",
|
|
132
|
+
reactorMode = "display"
|
|
133
|
+
} = options;
|
|
128
134
|
const pascalName = toPascalCase(canisterName);
|
|
129
135
|
const reactorName = getReactorName(canisterName);
|
|
130
136
|
const serviceName = getServiceTypeName(canisterName);
|
|
137
|
+
const rawFactoryName = `create${pascalName}RawReactor`;
|
|
138
|
+
const displayFactoryName = `create${pascalName}DisplayReactor`;
|
|
139
|
+
const defaultFactoryName = reactorMode === "raw" ? rawFactoryName : displayFactoryName;
|
|
131
140
|
const baseName = import_node_path2.default.basename(didFile, ".did");
|
|
132
141
|
const declarationsPath = `./declarations/${baseName}`;
|
|
133
|
-
return `import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
|
|
142
|
+
return `import { DisplayReactor, Reactor, createActorHooks } from "@ic-reactor/react"
|
|
134
143
|
import { clientManager } from "${clientManagerPath}"
|
|
135
144
|
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
136
145
|
|
|
@@ -142,11 +151,25 @@ export type ${serviceName} = _SERVICE
|
|
|
142
151
|
* Auto-generated by @ic-reactor/codegen \u2014 do not edit.
|
|
143
152
|
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
144
153
|
*/
|
|
145
|
-
export
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
154
|
+
export function ${rawFactoryName}() {
|
|
155
|
+
return new Reactor<${serviceName}>({
|
|
156
|
+
clientManager,
|
|
157
|
+
idlFactory,
|
|
158
|
+
name: "${canisterName}",
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function ${displayFactoryName}() {
|
|
163
|
+
return new DisplayReactor<${serviceName}>({
|
|
164
|
+
clientManager,
|
|
165
|
+
idlFactory,
|
|
166
|
+
name: "${canisterName}",
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const ${reactorName} = ${defaultFactoryName}()
|
|
171
|
+
|
|
172
|
+
export const ${pascalName}ReactorMode = "${reactorMode}" as const
|
|
150
173
|
|
|
151
174
|
export const {
|
|
152
175
|
useActorQuery: use${pascalName}Query,
|
|
@@ -158,8 +181,29 @@ export const {
|
|
|
158
181
|
} = createActorHooks(${reactorName})
|
|
159
182
|
`;
|
|
160
183
|
}
|
|
184
|
+
function generateReactorWrapperFile(options) {
|
|
185
|
+
const { canisterName } = options;
|
|
186
|
+
const pascalName = toPascalCase(canisterName);
|
|
187
|
+
return `/**
|
|
188
|
+
* ${pascalName} canister exports
|
|
189
|
+
*
|
|
190
|
+
* This wrapper is created once by @ic-reactor/codegen and is not overwritten.
|
|
191
|
+
* Customize it if you need to swap reactor implementations or compose custom exports.
|
|
192
|
+
*/
|
|
193
|
+
export * from "./index.generated"
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
161
196
|
|
|
162
197
|
// src/pipeline.ts
|
|
198
|
+
function resolveReactorMode(canisterName, globalConfig) {
|
|
199
|
+
return globalConfig.reactor?.canisters?.[canisterName] ?? globalConfig.reactor?.defaultMode ?? "display";
|
|
200
|
+
}
|
|
201
|
+
function isStableWrapperFile(content) {
|
|
202
|
+
return content.includes('export * from "./index.generated"');
|
|
203
|
+
}
|
|
204
|
+
function isLegacyGeneratedIndexFile(content) {
|
|
205
|
+
return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(") && !isStableWrapperFile(content);
|
|
206
|
+
}
|
|
163
207
|
async function runCanisterPipeline(options) {
|
|
164
208
|
const { canisterConfig, projectRoot, globalConfig } = options;
|
|
165
209
|
const { name, didFile, clientManagerPath } = canisterConfig;
|
|
@@ -198,20 +242,41 @@ async function runCanisterPipeline(options) {
|
|
|
198
242
|
error: `Declarations step failed: ${err instanceof Error ? err.message : String(err)}`
|
|
199
243
|
};
|
|
200
244
|
}
|
|
201
|
-
const
|
|
245
|
+
const generatedReactorPath = import_node_path3.default.join(canisterOutDir, "index.generated.ts");
|
|
246
|
+
const wrapperPath = import_node_path3.default.join(canisterOutDir, "index.ts");
|
|
247
|
+
const reactorMode = resolveReactorMode(name, globalConfig);
|
|
202
248
|
try {
|
|
203
249
|
const reactorContent = generateReactorFile({
|
|
204
250
|
canisterName: name,
|
|
205
251
|
didFile: resolvedDidFile,
|
|
206
|
-
clientManagerPath: resolvedClientManagerPath
|
|
252
|
+
clientManagerPath: resolvedClientManagerPath,
|
|
253
|
+
reactorMode
|
|
254
|
+
});
|
|
255
|
+
const wrapperContent = generateReactorWrapperFile({
|
|
256
|
+
canisterName: name,
|
|
257
|
+
didFile: resolvedDidFile,
|
|
258
|
+
clientManagerPath: resolvedClientManagerPath,
|
|
259
|
+
reactorMode
|
|
207
260
|
});
|
|
208
261
|
import_node_fs2.default.mkdirSync(canisterOutDir, { recursive: true });
|
|
209
|
-
import_node_fs2.default.writeFileSync(
|
|
210
|
-
files.push({ success: true, filePath:
|
|
262
|
+
import_node_fs2.default.writeFileSync(generatedReactorPath, reactorContent);
|
|
263
|
+
files.push({ success: true, filePath: generatedReactorPath });
|
|
264
|
+
if (!import_node_fs2.default.existsSync(wrapperPath)) {
|
|
265
|
+
import_node_fs2.default.writeFileSync(wrapperPath, wrapperContent);
|
|
266
|
+
files.push({ success: true, filePath: wrapperPath });
|
|
267
|
+
} else {
|
|
268
|
+
const existingWrapperContent = import_node_fs2.default.readFileSync(wrapperPath, "utf-8");
|
|
269
|
+
if (isLegacyGeneratedIndexFile(existingWrapperContent)) {
|
|
270
|
+
import_node_fs2.default.writeFileSync(wrapperPath, wrapperContent);
|
|
271
|
+
files.push({ success: true, filePath: wrapperPath });
|
|
272
|
+
} else {
|
|
273
|
+
files.push({ success: true, filePath: wrapperPath, skipped: true });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
211
276
|
} catch (err) {
|
|
212
277
|
files.push({
|
|
213
278
|
success: false,
|
|
214
|
-
filePath:
|
|
279
|
+
filePath: generatedReactorPath,
|
|
215
280
|
error: `Reactor generation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
216
281
|
});
|
|
217
282
|
return {
|
|
@@ -290,6 +355,7 @@ export const clientManager = new ClientManager({
|
|
|
290
355
|
generateClientFile,
|
|
291
356
|
generateDeclarations,
|
|
292
357
|
generateReactorFile,
|
|
358
|
+
generateReactorWrapperFile,
|
|
293
359
|
getReactorName,
|
|
294
360
|
getServiceTypeName,
|
|
295
361
|
parseDIDFile,
|
package/dist/index.d.cts
CHANGED
|
@@ -22,6 +22,19 @@ interface CanisterConfig {
|
|
|
22
22
|
/** Optional fixed canister ID */
|
|
23
23
|
canisterId?: string;
|
|
24
24
|
}
|
|
25
|
+
type ReactorMode = "raw" | "display";
|
|
26
|
+
interface ReactorGenerationConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Default reactor mode used for generated hook exports.
|
|
29
|
+
* `display` preserves current behavior.
|
|
30
|
+
*/
|
|
31
|
+
defaultMode?: ReactorMode;
|
|
32
|
+
/**
|
|
33
|
+
* Optional per-canister overrides keyed by canister name.
|
|
34
|
+
* Example: { workflow_engine: "raw" }
|
|
35
|
+
*/
|
|
36
|
+
canisters?: Record<string, ReactorMode>;
|
|
37
|
+
}
|
|
25
38
|
/**
|
|
26
39
|
* Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
|
|
27
40
|
*/
|
|
@@ -38,6 +51,8 @@ interface CodegenConfig {
|
|
|
38
51
|
* Individual canisters can override via `CanisterConfig.clientManagerPath`.
|
|
39
52
|
*/
|
|
40
53
|
clientManagerPath?: string;
|
|
54
|
+
/** Optional reactor mode generation settings */
|
|
55
|
+
reactor?: ReactorGenerationConfig;
|
|
41
56
|
/** Canister configurations, keyed by canister name */
|
|
42
57
|
canisters: Record<string, CanisterConfig>;
|
|
43
58
|
}
|
|
@@ -62,7 +77,7 @@ interface GeneratorResult {
|
|
|
62
77
|
* Pipeline steps (in order):
|
|
63
78
|
* 1. Resolve paths (didFile, outDir)
|
|
64
79
|
* 2. Generate declarations (JS + .d.ts + .did copy)
|
|
65
|
-
* 3. Generate reactor
|
|
80
|
+
* 3. Generate reactor implementation + stable wrapper (`index.generated.ts` + `index.ts`)
|
|
66
81
|
*/
|
|
67
82
|
|
|
68
83
|
interface PipelineOptions {
|
|
@@ -76,7 +91,7 @@ interface PipelineOptions {
|
|
|
76
91
|
/**
|
|
77
92
|
* Global codegen config (for fallback outDir and clientManagerPath).
|
|
78
93
|
*/
|
|
79
|
-
globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">;
|
|
94
|
+
globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath" | "reactor">;
|
|
80
95
|
}
|
|
81
96
|
interface PipelineResult {
|
|
82
97
|
canisterName: string;
|
|
@@ -204,6 +219,7 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
|
|
|
204
219
|
* ...
|
|
205
220
|
* } = createActorHooks(backendReactor)
|
|
206
221
|
*/
|
|
222
|
+
|
|
207
223
|
interface ReactorGeneratorOptions {
|
|
208
224
|
/** Canister name (e.g. "backend") */
|
|
209
225
|
canisterName: string;
|
|
@@ -217,11 +233,21 @@ interface ReactorGeneratorOptions {
|
|
|
217
233
|
* Default: "../../clients"
|
|
218
234
|
*/
|
|
219
235
|
clientManagerPath?: string;
|
|
236
|
+
/**
|
|
237
|
+
* Which reactor implementation should back the default exported hooks.
|
|
238
|
+
* Default: "display" (backward compatible)
|
|
239
|
+
*/
|
|
240
|
+
reactorMode?: ReactorMode;
|
|
220
241
|
}
|
|
221
242
|
/**
|
|
222
|
-
* Generate the content of a canister's `index.ts`
|
|
243
|
+
* Generate the content of a canister's `index.generated.ts` implementation file.
|
|
223
244
|
*/
|
|
224
245
|
declare function generateReactorFile(options: ReactorGeneratorOptions): string;
|
|
246
|
+
/**
|
|
247
|
+
* Generate the stable user wrapper `index.ts` for a canister.
|
|
248
|
+
* This file is intended to be created once and preserved across regenerations.
|
|
249
|
+
*/
|
|
250
|
+
declare function generateReactorWrapperFile(options: ReactorGeneratorOptions): string;
|
|
225
251
|
|
|
226
252
|
/**
|
|
227
253
|
* Client Manager Generator
|
|
@@ -246,4 +272,4 @@ interface ClientGeneratorOptions {
|
|
|
246
272
|
*/
|
|
247
273
|
declare function generateClientFile(options?: ClientGeneratorOptions): string;
|
|
248
274
|
|
|
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 };
|
|
275
|
+
export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorGenerationConfig, type ReactorGeneratorOptions, type ReactorMode, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorFile, generateReactorWrapperFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,19 @@ interface CanisterConfig {
|
|
|
22
22
|
/** Optional fixed canister ID */
|
|
23
23
|
canisterId?: string;
|
|
24
24
|
}
|
|
25
|
+
type ReactorMode = "raw" | "display";
|
|
26
|
+
interface ReactorGenerationConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Default reactor mode used for generated hook exports.
|
|
29
|
+
* `display` preserves current behavior.
|
|
30
|
+
*/
|
|
31
|
+
defaultMode?: ReactorMode;
|
|
32
|
+
/**
|
|
33
|
+
* Optional per-canister overrides keyed by canister name.
|
|
34
|
+
* Example: { workflow_engine: "raw" }
|
|
35
|
+
*/
|
|
36
|
+
canisters?: Record<string, ReactorMode>;
|
|
37
|
+
}
|
|
25
38
|
/**
|
|
26
39
|
* Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
|
|
27
40
|
*/
|
|
@@ -38,6 +51,8 @@ interface CodegenConfig {
|
|
|
38
51
|
* Individual canisters can override via `CanisterConfig.clientManagerPath`.
|
|
39
52
|
*/
|
|
40
53
|
clientManagerPath?: string;
|
|
54
|
+
/** Optional reactor mode generation settings */
|
|
55
|
+
reactor?: ReactorGenerationConfig;
|
|
41
56
|
/** Canister configurations, keyed by canister name */
|
|
42
57
|
canisters: Record<string, CanisterConfig>;
|
|
43
58
|
}
|
|
@@ -62,7 +77,7 @@ interface GeneratorResult {
|
|
|
62
77
|
* Pipeline steps (in order):
|
|
63
78
|
* 1. Resolve paths (didFile, outDir)
|
|
64
79
|
* 2. Generate declarations (JS + .d.ts + .did copy)
|
|
65
|
-
* 3. Generate reactor
|
|
80
|
+
* 3. Generate reactor implementation + stable wrapper (`index.generated.ts` + `index.ts`)
|
|
66
81
|
*/
|
|
67
82
|
|
|
68
83
|
interface PipelineOptions {
|
|
@@ -76,7 +91,7 @@ interface PipelineOptions {
|
|
|
76
91
|
/**
|
|
77
92
|
* Global codegen config (for fallback outDir and clientManagerPath).
|
|
78
93
|
*/
|
|
79
|
-
globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">;
|
|
94
|
+
globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath" | "reactor">;
|
|
80
95
|
}
|
|
81
96
|
interface PipelineResult {
|
|
82
97
|
canisterName: string;
|
|
@@ -204,6 +219,7 @@ declare function declarationsExist(outDir: string, canisterName: string): boolea
|
|
|
204
219
|
* ...
|
|
205
220
|
* } = createActorHooks(backendReactor)
|
|
206
221
|
*/
|
|
222
|
+
|
|
207
223
|
interface ReactorGeneratorOptions {
|
|
208
224
|
/** Canister name (e.g. "backend") */
|
|
209
225
|
canisterName: string;
|
|
@@ -217,11 +233,21 @@ interface ReactorGeneratorOptions {
|
|
|
217
233
|
* Default: "../../clients"
|
|
218
234
|
*/
|
|
219
235
|
clientManagerPath?: string;
|
|
236
|
+
/**
|
|
237
|
+
* Which reactor implementation should back the default exported hooks.
|
|
238
|
+
* Default: "display" (backward compatible)
|
|
239
|
+
*/
|
|
240
|
+
reactorMode?: ReactorMode;
|
|
220
241
|
}
|
|
221
242
|
/**
|
|
222
|
-
* Generate the content of a canister's `index.ts`
|
|
243
|
+
* Generate the content of a canister's `index.generated.ts` implementation file.
|
|
223
244
|
*/
|
|
224
245
|
declare function generateReactorFile(options: ReactorGeneratorOptions): string;
|
|
246
|
+
/**
|
|
247
|
+
* Generate the stable user wrapper `index.ts` for a canister.
|
|
248
|
+
* This file is intended to be created once and preserved across regenerations.
|
|
249
|
+
*/
|
|
250
|
+
declare function generateReactorWrapperFile(options: ReactorGeneratorOptions): string;
|
|
225
251
|
|
|
226
252
|
/**
|
|
227
253
|
* Client Manager Generator
|
|
@@ -246,4 +272,4 @@ interface ClientGeneratorOptions {
|
|
|
246
272
|
*/
|
|
247
273
|
declare function generateClientFile(options?: ClientGeneratorOptions): string;
|
|
248
274
|
|
|
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 };
|
|
275
|
+
export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorGenerationConfig, type ReactorGeneratorOptions, type ReactorMode, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorFile, generateReactorWrapperFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
|
package/dist/index.js
CHANGED
|
@@ -79,13 +79,21 @@ function getServiceTypeName(canisterName) {
|
|
|
79
79
|
|
|
80
80
|
// src/generators/reactor.ts
|
|
81
81
|
function generateReactorFile(options) {
|
|
82
|
-
const {
|
|
82
|
+
const {
|
|
83
|
+
canisterName,
|
|
84
|
+
didFile,
|
|
85
|
+
clientManagerPath = "../../clients",
|
|
86
|
+
reactorMode = "display"
|
|
87
|
+
} = options;
|
|
83
88
|
const pascalName = toPascalCase(canisterName);
|
|
84
89
|
const reactorName = getReactorName(canisterName);
|
|
85
90
|
const serviceName = getServiceTypeName(canisterName);
|
|
91
|
+
const rawFactoryName = `create${pascalName}RawReactor`;
|
|
92
|
+
const displayFactoryName = `create${pascalName}DisplayReactor`;
|
|
93
|
+
const defaultFactoryName = reactorMode === "raw" ? rawFactoryName : displayFactoryName;
|
|
86
94
|
const baseName = path2.basename(didFile, ".did");
|
|
87
95
|
const declarationsPath = `./declarations/${baseName}`;
|
|
88
|
-
return `import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
|
|
96
|
+
return `import { DisplayReactor, Reactor, createActorHooks } from "@ic-reactor/react"
|
|
89
97
|
import { clientManager } from "${clientManagerPath}"
|
|
90
98
|
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
91
99
|
|
|
@@ -97,11 +105,25 @@ export type ${serviceName} = _SERVICE
|
|
|
97
105
|
* Auto-generated by @ic-reactor/codegen \u2014 do not edit.
|
|
98
106
|
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
99
107
|
*/
|
|
100
|
-
export
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
108
|
+
export function ${rawFactoryName}() {
|
|
109
|
+
return new Reactor<${serviceName}>({
|
|
110
|
+
clientManager,
|
|
111
|
+
idlFactory,
|
|
112
|
+
name: "${canisterName}",
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function ${displayFactoryName}() {
|
|
117
|
+
return new DisplayReactor<${serviceName}>({
|
|
118
|
+
clientManager,
|
|
119
|
+
idlFactory,
|
|
120
|
+
name: "${canisterName}",
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const ${reactorName} = ${defaultFactoryName}()
|
|
125
|
+
|
|
126
|
+
export const ${pascalName}ReactorMode = "${reactorMode}" as const
|
|
105
127
|
|
|
106
128
|
export const {
|
|
107
129
|
useActorQuery: use${pascalName}Query,
|
|
@@ -113,8 +135,29 @@ export const {
|
|
|
113
135
|
} = createActorHooks(${reactorName})
|
|
114
136
|
`;
|
|
115
137
|
}
|
|
138
|
+
function generateReactorWrapperFile(options) {
|
|
139
|
+
const { canisterName } = options;
|
|
140
|
+
const pascalName = toPascalCase(canisterName);
|
|
141
|
+
return `/**
|
|
142
|
+
* ${pascalName} canister exports
|
|
143
|
+
*
|
|
144
|
+
* This wrapper is created once by @ic-reactor/codegen and is not overwritten.
|
|
145
|
+
* Customize it if you need to swap reactor implementations or compose custom exports.
|
|
146
|
+
*/
|
|
147
|
+
export * from "./index.generated"
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
116
150
|
|
|
117
151
|
// src/pipeline.ts
|
|
152
|
+
function resolveReactorMode(canisterName, globalConfig) {
|
|
153
|
+
return globalConfig.reactor?.canisters?.[canisterName] ?? globalConfig.reactor?.defaultMode ?? "display";
|
|
154
|
+
}
|
|
155
|
+
function isStableWrapperFile(content) {
|
|
156
|
+
return content.includes('export * from "./index.generated"');
|
|
157
|
+
}
|
|
158
|
+
function isLegacyGeneratedIndexFile(content) {
|
|
159
|
+
return content.includes("Auto-generated by @ic-reactor/codegen") && content.includes("createActorHooks(") && !isStableWrapperFile(content);
|
|
160
|
+
}
|
|
118
161
|
async function runCanisterPipeline(options) {
|
|
119
162
|
const { canisterConfig, projectRoot, globalConfig } = options;
|
|
120
163
|
const { name, didFile, clientManagerPath } = canisterConfig;
|
|
@@ -153,20 +196,41 @@ async function runCanisterPipeline(options) {
|
|
|
153
196
|
error: `Declarations step failed: ${err instanceof Error ? err.message : String(err)}`
|
|
154
197
|
};
|
|
155
198
|
}
|
|
156
|
-
const
|
|
199
|
+
const generatedReactorPath = path3.join(canisterOutDir, "index.generated.ts");
|
|
200
|
+
const wrapperPath = path3.join(canisterOutDir, "index.ts");
|
|
201
|
+
const reactorMode = resolveReactorMode(name, globalConfig);
|
|
157
202
|
try {
|
|
158
203
|
const reactorContent = generateReactorFile({
|
|
159
204
|
canisterName: name,
|
|
160
205
|
didFile: resolvedDidFile,
|
|
161
|
-
clientManagerPath: resolvedClientManagerPath
|
|
206
|
+
clientManagerPath: resolvedClientManagerPath,
|
|
207
|
+
reactorMode
|
|
208
|
+
});
|
|
209
|
+
const wrapperContent = generateReactorWrapperFile({
|
|
210
|
+
canisterName: name,
|
|
211
|
+
didFile: resolvedDidFile,
|
|
212
|
+
clientManagerPath: resolvedClientManagerPath,
|
|
213
|
+
reactorMode
|
|
162
214
|
});
|
|
163
215
|
fs2.mkdirSync(canisterOutDir, { recursive: true });
|
|
164
|
-
fs2.writeFileSync(
|
|
165
|
-
files.push({ success: true, filePath:
|
|
216
|
+
fs2.writeFileSync(generatedReactorPath, reactorContent);
|
|
217
|
+
files.push({ success: true, filePath: generatedReactorPath });
|
|
218
|
+
if (!fs2.existsSync(wrapperPath)) {
|
|
219
|
+
fs2.writeFileSync(wrapperPath, wrapperContent);
|
|
220
|
+
files.push({ success: true, filePath: wrapperPath });
|
|
221
|
+
} else {
|
|
222
|
+
const existingWrapperContent = fs2.readFileSync(wrapperPath, "utf-8");
|
|
223
|
+
if (isLegacyGeneratedIndexFile(existingWrapperContent)) {
|
|
224
|
+
fs2.writeFileSync(wrapperPath, wrapperContent);
|
|
225
|
+
files.push({ success: true, filePath: wrapperPath });
|
|
226
|
+
} else {
|
|
227
|
+
files.push({ success: true, filePath: wrapperPath, skipped: true });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
166
230
|
} catch (err) {
|
|
167
231
|
files.push({
|
|
168
232
|
success: false,
|
|
169
|
-
filePath:
|
|
233
|
+
filePath: generatedReactorPath,
|
|
170
234
|
error: `Reactor generation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
171
235
|
});
|
|
172
236
|
return {
|
|
@@ -244,6 +308,7 @@ export {
|
|
|
244
308
|
generateClientFile,
|
|
245
309
|
generateDeclarations,
|
|
246
310
|
generateReactorFile,
|
|
311
|
+
generateReactorWrapperFile,
|
|
247
312
|
getReactorName,
|
|
248
313
|
getServiceTypeName,
|
|
249
314
|
parseDIDFile,
|
package/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`Reactor generator > generates a stable wrapper file > stable-wrapper-index 1`] = `
|
|
4
|
+
"/**
|
|
5
|
+
* Backend canister exports
|
|
6
|
+
*
|
|
7
|
+
* This wrapper is created once by @ic-reactor/codegen and is not overwritten.
|
|
8
|
+
* Customize it if you need to swap reactor implementations or compose custom exports.
|
|
9
|
+
*/
|
|
10
|
+
export * from "./index.generated"
|
|
11
|
+
"
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
exports[`Reactor generator > keeps default behavior as display mode > display-mode-index-generated 1`] = `
|
|
15
|
+
"import { DisplayReactor, Reactor, createActorHooks } from "@ic-reactor/react"
|
|
16
|
+
import { clientManager } from "../../clients"
|
|
17
|
+
import { idlFactory, type _SERVICE } from "./declarations/backend"
|
|
18
|
+
|
|
19
|
+
export type BackendService = _SERVICE
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Backend Reactor
|
|
23
|
+
*
|
|
24
|
+
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
25
|
+
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
26
|
+
*/
|
|
27
|
+
export function createBackendRawReactor() {
|
|
28
|
+
return new Reactor<BackendService>({
|
|
29
|
+
clientManager,
|
|
30
|
+
idlFactory,
|
|
31
|
+
name: "backend",
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createBackendDisplayReactor() {
|
|
36
|
+
return new DisplayReactor<BackendService>({
|
|
37
|
+
clientManager,
|
|
38
|
+
idlFactory,
|
|
39
|
+
name: "backend",
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const backendReactor = createBackendDisplayReactor()
|
|
44
|
+
|
|
45
|
+
export const BackendReactorMode = "display" as const
|
|
46
|
+
|
|
47
|
+
export const {
|
|
48
|
+
useActorQuery: useBackendQuery,
|
|
49
|
+
useActorSuspenseQuery: useBackendSuspenseQuery,
|
|
50
|
+
useActorInfiniteQuery: useBackendInfiniteQuery,
|
|
51
|
+
useActorSuspenseInfiniteQuery: useBackendSuspenseInfiniteQuery,
|
|
52
|
+
useActorMutation: useBackendMutation,
|
|
53
|
+
useActorMethod: useBackendMethod,
|
|
54
|
+
} = createActorHooks(backendReactor)
|
|
55
|
+
"
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
exports[`Reactor generator > supports raw mode generation > raw-mode-index-generated 1`] = `
|
|
59
|
+
"import { DisplayReactor, Reactor, createActorHooks } from "@ic-reactor/react"
|
|
60
|
+
import { clientManager } from "../../clients"
|
|
61
|
+
import { idlFactory, type _SERVICE } from "./declarations/workflow_engine"
|
|
62
|
+
|
|
63
|
+
export type WorkflowEngineService = _SERVICE
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* WorkflowEngine Reactor
|
|
67
|
+
*
|
|
68
|
+
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
69
|
+
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
70
|
+
*/
|
|
71
|
+
export function createWorkflowEngineRawReactor() {
|
|
72
|
+
return new Reactor<WorkflowEngineService>({
|
|
73
|
+
clientManager,
|
|
74
|
+
idlFactory,
|
|
75
|
+
name: "workflow_engine",
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createWorkflowEngineDisplayReactor() {
|
|
80
|
+
return new DisplayReactor<WorkflowEngineService>({
|
|
81
|
+
clientManager,
|
|
82
|
+
idlFactory,
|
|
83
|
+
name: "workflow_engine",
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const workflowEngineReactor = createWorkflowEngineRawReactor()
|
|
88
|
+
|
|
89
|
+
export const WorkflowEngineReactorMode = "raw" as const
|
|
90
|
+
|
|
91
|
+
export const {
|
|
92
|
+
useActorQuery: useWorkflowEngineQuery,
|
|
93
|
+
useActorSuspenseQuery: useWorkflowEngineSuspenseQuery,
|
|
94
|
+
useActorInfiniteQuery: useWorkflowEngineInfiniteQuery,
|
|
95
|
+
useActorSuspenseInfiniteQuery: useWorkflowEngineSuspenseInfiniteQuery,
|
|
96
|
+
useActorMutation: useWorkflowEngineMutation,
|
|
97
|
+
useActorMethod: useWorkflowEngineMethod,
|
|
98
|
+
} = createActorHooks(workflowEngineReactor)
|
|
99
|
+
"
|
|
100
|
+
`;
|
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, generateReactorWrapperFile } from "./reactor.js"
|
|
15
15
|
export type { ReactorGeneratorOptions } from "./reactor.js"
|
|
16
16
|
|
|
17
17
|
export { generateClientFile } from "./client.js"
|
|
@@ -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 { ReactorMode } from "../types.js"
|
|
25
26
|
|
|
26
27
|
export interface ReactorGeneratorOptions {
|
|
27
28
|
/** Canister name (e.g. "backend") */
|
|
@@ -36,23 +37,37 @@ export interface ReactorGeneratorOptions {
|
|
|
36
37
|
* Default: "../../clients"
|
|
37
38
|
*/
|
|
38
39
|
clientManagerPath?: string
|
|
40
|
+
/**
|
|
41
|
+
* Which reactor implementation should back the default exported hooks.
|
|
42
|
+
* Default: "display" (backward compatible)
|
|
43
|
+
*/
|
|
44
|
+
reactorMode?: ReactorMode
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
/**
|
|
42
|
-
* Generate the content of a canister's `index.ts`
|
|
48
|
+
* Generate the content of a canister's `index.generated.ts` implementation file.
|
|
43
49
|
*/
|
|
44
50
|
export function generateReactorFile(options: ReactorGeneratorOptions): string {
|
|
45
|
-
const {
|
|
51
|
+
const {
|
|
52
|
+
canisterName,
|
|
53
|
+
didFile,
|
|
54
|
+
clientManagerPath = "../../clients",
|
|
55
|
+
reactorMode = "display",
|
|
56
|
+
} = options
|
|
46
57
|
|
|
47
58
|
const pascalName = toPascalCase(canisterName)
|
|
48
59
|
const reactorName = getReactorName(canisterName)
|
|
49
60
|
const serviceName = getServiceTypeName(canisterName)
|
|
61
|
+
const rawFactoryName = `create${pascalName}RawReactor`
|
|
62
|
+
const displayFactoryName = `create${pascalName}DisplayReactor`
|
|
63
|
+
const defaultFactoryName =
|
|
64
|
+
reactorMode === "raw" ? rawFactoryName : displayFactoryName
|
|
50
65
|
|
|
51
66
|
// Derive the declarations import path from the .did filename
|
|
52
67
|
const baseName = path.basename(didFile, ".did")
|
|
53
68
|
const declarationsPath = `./declarations/${baseName}`
|
|
54
69
|
|
|
55
|
-
return `import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
|
|
70
|
+
return `import { DisplayReactor, Reactor, createActorHooks } from "@ic-reactor/react"
|
|
56
71
|
import { clientManager } from "${clientManagerPath}"
|
|
57
72
|
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
58
73
|
|
|
@@ -64,11 +79,25 @@ export type ${serviceName} = _SERVICE
|
|
|
64
79
|
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
65
80
|
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
66
81
|
*/
|
|
67
|
-
export
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
82
|
+
export function ${rawFactoryName}() {
|
|
83
|
+
return new Reactor<${serviceName}>({
|
|
84
|
+
clientManager,
|
|
85
|
+
idlFactory,
|
|
86
|
+
name: "${canisterName}",
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function ${displayFactoryName}() {
|
|
91
|
+
return new DisplayReactor<${serviceName}>({
|
|
92
|
+
clientManager,
|
|
93
|
+
idlFactory,
|
|
94
|
+
name: "${canisterName}",
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const ${reactorName} = ${defaultFactoryName}()
|
|
99
|
+
|
|
100
|
+
export const ${pascalName}ReactorMode = "${reactorMode}" as const
|
|
72
101
|
|
|
73
102
|
export const {
|
|
74
103
|
useActorQuery: use${pascalName}Query,
|
|
@@ -80,3 +109,23 @@ export const {
|
|
|
80
109
|
} = createActorHooks(${reactorName})
|
|
81
110
|
`
|
|
82
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Generate the stable user wrapper `index.ts` for a canister.
|
|
115
|
+
* This file is intended to be created once and preserved across regenerations.
|
|
116
|
+
*/
|
|
117
|
+
export function generateReactorWrapperFile(
|
|
118
|
+
options: ReactorGeneratorOptions
|
|
119
|
+
): string {
|
|
120
|
+
const { canisterName } = options
|
|
121
|
+
const pascalName = toPascalCase(canisterName)
|
|
122
|
+
|
|
123
|
+
return `/**
|
|
124
|
+
* ${pascalName} canister exports
|
|
125
|
+
*
|
|
126
|
+
* This wrapper is created once by @ic-reactor/codegen and is not overwritten.
|
|
127
|
+
* Customize it if you need to swap reactor implementations or compose custom exports.
|
|
128
|
+
*/
|
|
129
|
+
export * from "./index.generated"
|
|
130
|
+
`
|
|
131
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// Core Types
|
|
9
|
-
export type {
|
|
9
|
+
export type {
|
|
10
|
+
CanisterConfig,
|
|
11
|
+
CodegenConfig,
|
|
12
|
+
GeneratorResult,
|
|
13
|
+
ReactorMode,
|
|
14
|
+
ReactorGenerationConfig,
|
|
15
|
+
} from "./types.js"
|
|
10
16
|
|
|
11
17
|
// Pipeline (Primary Entry Point)
|
|
12
18
|
export { runCanisterPipeline } from "./pipeline.js"
|
|
@@ -0,0 +1,178 @@
|
|
|
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("applies per-canister reactor mode overrides over the global default", 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
|
+
},
|
|
43
|
+
projectRoot,
|
|
44
|
+
globalConfig: {
|
|
45
|
+
outDir: "src/declarations",
|
|
46
|
+
clientManagerPath: "../../clients",
|
|
47
|
+
reactor: {
|
|
48
|
+
defaultMode: "display",
|
|
49
|
+
canisters: {
|
|
50
|
+
workflow_engine: "raw",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
expect(result.success).toBe(true)
|
|
57
|
+
|
|
58
|
+
const generatedPath = path.join(
|
|
59
|
+
projectRoot,
|
|
60
|
+
"src/declarations/workflow_engine/index.generated.ts"
|
|
61
|
+
)
|
|
62
|
+
const generated = fs.readFileSync(generatedPath, "utf-8")
|
|
63
|
+
|
|
64
|
+
expect(generated).toContain("new Reactor<WorkflowEngineService>")
|
|
65
|
+
expect(generated).toContain(
|
|
66
|
+
'export const WorkflowEngineReactorMode = "raw" as const'
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("does not overwrite the wrapper file on regenerate", async () => {
|
|
71
|
+
const projectRoot = createTempProject()
|
|
72
|
+
writeDid(projectRoot, "backend.did")
|
|
73
|
+
|
|
74
|
+
const options = {
|
|
75
|
+
canisterConfig: {
|
|
76
|
+
name: "backend",
|
|
77
|
+
didFile: "backend.did",
|
|
78
|
+
},
|
|
79
|
+
projectRoot,
|
|
80
|
+
globalConfig: {
|
|
81
|
+
outDir: "src/declarations",
|
|
82
|
+
clientManagerPath: "../../clients",
|
|
83
|
+
},
|
|
84
|
+
} as const
|
|
85
|
+
|
|
86
|
+
const first = await runCanisterPipeline(options)
|
|
87
|
+
expect(first.success).toBe(true)
|
|
88
|
+
|
|
89
|
+
const wrapperPath = path.join(
|
|
90
|
+
projectRoot,
|
|
91
|
+
"src/declarations/backend/index.ts"
|
|
92
|
+
)
|
|
93
|
+
fs.writeFileSync(
|
|
94
|
+
wrapperPath,
|
|
95
|
+
`// user wrapper
|
|
96
|
+
export const customBackendWrapper = true
|
|
97
|
+
`
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const second = await runCanisterPipeline(options)
|
|
101
|
+
expect(second.success).toBe(true)
|
|
102
|
+
|
|
103
|
+
const wrapper = fs.readFileSync(wrapperPath, "utf-8")
|
|
104
|
+
expect(wrapper).toContain("customBackendWrapper = true")
|
|
105
|
+
expect(second.files).toEqual(
|
|
106
|
+
expect.arrayContaining([
|
|
107
|
+
expect.objectContaining({
|
|
108
|
+
filePath: wrapperPath,
|
|
109
|
+
skipped: true,
|
|
110
|
+
success: true,
|
|
111
|
+
}),
|
|
112
|
+
])
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("migrates a legacy generated index.ts into a stable wrapper", async () => {
|
|
117
|
+
const projectRoot = createTempProject()
|
|
118
|
+
writeDid(projectRoot, "backend.did")
|
|
119
|
+
|
|
120
|
+
const canisterOutDir = path.join(projectRoot, "src/declarations/backend")
|
|
121
|
+
fs.mkdirSync(canisterOutDir, { recursive: true })
|
|
122
|
+
|
|
123
|
+
// Simulate the pre-wrapper generated index.ts content from older versions.
|
|
124
|
+
fs.writeFileSync(
|
|
125
|
+
path.join(canisterOutDir, "index.ts"),
|
|
126
|
+
`import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
|
|
127
|
+
import { clientManager } from "../../clients"
|
|
128
|
+
import { idlFactory, type _SERVICE } from "./declarations/backend"
|
|
129
|
+
|
|
130
|
+
export type BackendService = _SERVICE
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Backend Reactor
|
|
134
|
+
*
|
|
135
|
+
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
136
|
+
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
137
|
+
*/
|
|
138
|
+
export const backendReactor = new DisplayReactor<BackendService>({
|
|
139
|
+
clientManager,
|
|
140
|
+
idlFactory,
|
|
141
|
+
name: "backend",
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
export const {
|
|
145
|
+
useActorQuery: useBackendQuery,
|
|
146
|
+
} = createActorHooks(backendReactor)
|
|
147
|
+
`
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const result = await runCanisterPipeline({
|
|
151
|
+
canisterConfig: {
|
|
152
|
+
name: "backend",
|
|
153
|
+
didFile: "backend.did",
|
|
154
|
+
},
|
|
155
|
+
projectRoot,
|
|
156
|
+
globalConfig: {
|
|
157
|
+
outDir: "src/declarations",
|
|
158
|
+
clientManagerPath: "../../clients",
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
expect(result.success).toBe(true)
|
|
163
|
+
|
|
164
|
+
const wrapperPath = path.join(canisterOutDir, "index.ts")
|
|
165
|
+
const wrapper = fs.readFileSync(wrapperPath, "utf-8")
|
|
166
|
+
expect(wrapper).toContain('export * from "./index.generated"')
|
|
167
|
+
expect(wrapper).not.toContain("createActorHooks(")
|
|
168
|
+
|
|
169
|
+
const generated = fs.readFileSync(
|
|
170
|
+
path.join(canisterOutDir, "index.generated.ts"),
|
|
171
|
+
"utf-8"
|
|
172
|
+
)
|
|
173
|
+
expect(generated).toContain("createBackendDisplayReactor")
|
|
174
|
+
expect(generated).toContain(
|
|
175
|
+
'export const BackendReactorMode = "display" as const'
|
|
176
|
+
)
|
|
177
|
+
})
|
|
178
|
+
})
|
package/src/pipeline.ts
CHANGED
|
@@ -7,14 +7,22 @@
|
|
|
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 + stable wrapper (`index.generated.ts` + `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
|
+
ReactorMode,
|
|
20
|
+
} from "./types.js"
|
|
16
21
|
import { generateDeclarations } from "./generators/declarations.js"
|
|
17
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
generateReactorFile,
|
|
24
|
+
generateReactorWrapperFile,
|
|
25
|
+
} from "./generators/reactor.js"
|
|
18
26
|
|
|
19
27
|
export interface PipelineOptions {
|
|
20
28
|
/** Canister name and config */
|
|
@@ -27,7 +35,30 @@ export interface PipelineOptions {
|
|
|
27
35
|
/**
|
|
28
36
|
* Global codegen config (for fallback outDir and clientManagerPath).
|
|
29
37
|
*/
|
|
30
|
-
globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">
|
|
38
|
+
globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath" | "reactor">
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveReactorMode(
|
|
42
|
+
canisterName: string,
|
|
43
|
+
globalConfig: Pick<CodegenConfig, "reactor">
|
|
44
|
+
): ReactorMode {
|
|
45
|
+
return (
|
|
46
|
+
globalConfig.reactor?.canisters?.[canisterName] ??
|
|
47
|
+
globalConfig.reactor?.defaultMode ??
|
|
48
|
+
"display"
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isStableWrapperFile(content: string): boolean {
|
|
53
|
+
return content.includes('export * from "./index.generated"')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isLegacyGeneratedIndexFile(content: string): boolean {
|
|
57
|
+
return (
|
|
58
|
+
content.includes("Auto-generated by @ic-reactor/codegen") &&
|
|
59
|
+
content.includes("createActorHooks(") &&
|
|
60
|
+
!isStableWrapperFile(content)
|
|
61
|
+
)
|
|
31
62
|
}
|
|
32
63
|
|
|
33
64
|
export interface PipelineResult {
|
|
@@ -106,25 +137,48 @@ export async function runCanisterPipeline(
|
|
|
106
137
|
}
|
|
107
138
|
}
|
|
108
139
|
|
|
109
|
-
// ── Step 2: Reactor
|
|
140
|
+
// ── Step 2: Reactor implementation + stable wrapper ───────────────────────
|
|
110
141
|
|
|
111
|
-
const
|
|
142
|
+
const generatedReactorPath = path.join(canisterOutDir, "index.generated.ts")
|
|
143
|
+
const wrapperPath = path.join(canisterOutDir, "index.ts")
|
|
144
|
+
const reactorMode = resolveReactorMode(name, globalConfig)
|
|
112
145
|
|
|
113
146
|
try {
|
|
114
147
|
const reactorContent = generateReactorFile({
|
|
115
148
|
canisterName: name,
|
|
116
149
|
didFile: resolvedDidFile,
|
|
117
150
|
clientManagerPath: resolvedClientManagerPath,
|
|
151
|
+
reactorMode,
|
|
152
|
+
})
|
|
153
|
+
const wrapperContent = generateReactorWrapperFile({
|
|
154
|
+
canisterName: name,
|
|
155
|
+
didFile: resolvedDidFile,
|
|
156
|
+
clientManagerPath: resolvedClientManagerPath,
|
|
157
|
+
reactorMode,
|
|
118
158
|
})
|
|
119
159
|
|
|
120
160
|
fs.mkdirSync(canisterOutDir, { recursive: true })
|
|
121
|
-
fs.writeFileSync(
|
|
161
|
+
fs.writeFileSync(generatedReactorPath, reactorContent)
|
|
162
|
+
|
|
163
|
+
files.push({ success: true, filePath: generatedReactorPath })
|
|
122
164
|
|
|
123
|
-
|
|
165
|
+
if (!fs.existsSync(wrapperPath)) {
|
|
166
|
+
fs.writeFileSync(wrapperPath, wrapperContent)
|
|
167
|
+
files.push({ success: true, filePath: wrapperPath })
|
|
168
|
+
} else {
|
|
169
|
+
const existingWrapperContent = fs.readFileSync(wrapperPath, "utf-8")
|
|
170
|
+
|
|
171
|
+
if (isLegacyGeneratedIndexFile(existingWrapperContent)) {
|
|
172
|
+
fs.writeFileSync(wrapperPath, wrapperContent)
|
|
173
|
+
files.push({ success: true, filePath: wrapperPath })
|
|
174
|
+
} else {
|
|
175
|
+
files.push({ success: true, filePath: wrapperPath, skipped: true })
|
|
176
|
+
}
|
|
177
|
+
}
|
|
124
178
|
} catch (err) {
|
|
125
179
|
files.push({
|
|
126
180
|
success: false,
|
|
127
|
-
filePath:
|
|
181
|
+
filePath: generatedReactorPath,
|
|
128
182
|
error: `Reactor generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
129
183
|
})
|
|
130
184
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { generateReactorFile, generateReactorWrapperFile } from "./generators"
|
|
3
|
+
|
|
4
|
+
describe("Reactor generator", () => {
|
|
5
|
+
it("keeps default behavior as display mode", () => {
|
|
6
|
+
const content = generateReactorFile({
|
|
7
|
+
canisterName: "backend",
|
|
8
|
+
didFile: "mock/backend.did",
|
|
9
|
+
clientManagerPath: "../../clients",
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
expect(content).toMatchSnapshot("display-mode-index-generated")
|
|
13
|
+
expect(content).toContain("new DisplayReactor<BackendService>")
|
|
14
|
+
expect(content).toContain(
|
|
15
|
+
'export const BackendReactorMode = "display" as const'
|
|
16
|
+
)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("supports raw mode generation", () => {
|
|
20
|
+
const content = generateReactorFile({
|
|
21
|
+
canisterName: "workflow_engine",
|
|
22
|
+
didFile: "mock/workflow_engine.did",
|
|
23
|
+
reactorMode: "raw",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
expect(content).toMatchSnapshot("raw-mode-index-generated")
|
|
27
|
+
expect(content).toContain("new Reactor<WorkflowEngineService>")
|
|
28
|
+
expect(content).toContain(
|
|
29
|
+
'export const WorkflowEngineReactorMode = "raw" as const'
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("generates a stable wrapper file", () => {
|
|
34
|
+
const content = generateReactorWrapperFile({
|
|
35
|
+
canisterName: "backend",
|
|
36
|
+
didFile: "mock/backend.did",
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
expect(content).toMatchSnapshot("stable-wrapper-index")
|
|
40
|
+
expect(content).toContain('export * from "./index.generated"')
|
|
41
|
+
})
|
|
42
|
+
})
|
package/src/types.ts
CHANGED
|
@@ -28,6 +28,21 @@ export interface CanisterConfig {
|
|
|
28
28
|
canisterId?: string
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export type ReactorMode = "raw" | "display"
|
|
32
|
+
|
|
33
|
+
export interface ReactorGenerationConfig {
|
|
34
|
+
/**
|
|
35
|
+
* Default reactor mode used for generated hook exports.
|
|
36
|
+
* `display` preserves current behavior.
|
|
37
|
+
*/
|
|
38
|
+
defaultMode?: ReactorMode
|
|
39
|
+
/**
|
|
40
|
+
* Optional per-canister overrides keyed by canister name.
|
|
41
|
+
* Example: { workflow_engine: "raw" }
|
|
42
|
+
*/
|
|
43
|
+
canisters?: Record<string, ReactorMode>
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/**
|
|
32
47
|
* Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
|
|
33
48
|
*/
|
|
@@ -44,6 +59,8 @@ export interface CodegenConfig {
|
|
|
44
59
|
* Individual canisters can override via `CanisterConfig.clientManagerPath`.
|
|
45
60
|
*/
|
|
46
61
|
clientManagerPath?: string
|
|
62
|
+
/** Optional reactor mode generation settings */
|
|
63
|
+
reactor?: ReactorGenerationConfig
|
|
47
64
|
/** Canister configurations, keyed by canister name */
|
|
48
65
|
canisters: Record<string, CanisterConfig>
|
|
49
66
|
}
|