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