@ic-reactor/codegen 0.12.1 → 0.13.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 +14 -3
- package/dist/index.cjs +316 -201
- package/dist/index.d.cts +20 -36
- package/dist/index.d.ts +20 -36
- package/dist/index.js +315 -199
- package/package.json +7 -3
- package/src/generators/declarations.ts +164 -21
- package/src/generators/reactor.ts +5 -3
- package/src/index.ts +1 -3
- package/src/pipeline.ts +155 -10
- package/src/validate.ts +38 -0
- package/src/__snapshots__/bindgen.test.ts.snap +0 -18
- package/src/__snapshots__/reactor.test.ts.snap +0 -127
- package/src/bindgen.test.ts +0 -85
- package/src/naming.test.ts +0 -45
- package/src/parser.ts +0 -79
- package/src/pipeline.test.ts +0 -355
- package/src/reactor.test.ts +0 -84
- package/src/validate.test.ts +0 -393
package/dist/index.js
CHANGED
|
@@ -1,66 +1,15 @@
|
|
|
1
1
|
// src/pipeline.ts
|
|
2
2
|
import fs3 from "fs";
|
|
3
|
-
import
|
|
3
|
+
import path3 from "path";
|
|
4
4
|
|
|
5
5
|
// src/generators/declarations.ts
|
|
6
6
|
import { didToJs, didToTs } from "@ic-reactor/parser";
|
|
7
|
-
import path from "path";
|
|
8
|
-
import fs from "fs";
|
|
9
|
-
async function generateDeclarations(options) {
|
|
10
|
-
const { didFile, outDir, canisterName } = options;
|
|
11
|
-
if (!fs.existsSync(didFile)) {
|
|
12
|
-
return {
|
|
13
|
-
success: false,
|
|
14
|
-
declarationsDir: "",
|
|
15
|
-
files: [],
|
|
16
|
-
error: `DID file not found: ${didFile}`
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
const declarationsDir = path.join(outDir, "declarations");
|
|
20
|
-
const baseName = path.basename(didFile, ".did");
|
|
21
|
-
try {
|
|
22
|
-
const didContent = fs.readFileSync(didFile, "utf-8");
|
|
23
|
-
if (!fs.existsSync(outDir)) {
|
|
24
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
25
|
-
}
|
|
26
|
-
if (fs.existsSync(declarationsDir)) {
|
|
27
|
-
fs.rmSync(declarationsDir, { recursive: true, force: true });
|
|
28
|
-
}
|
|
29
|
-
fs.mkdirSync(declarationsDir, { recursive: true });
|
|
30
|
-
const jsContent = didToJs(didContent);
|
|
31
|
-
const tsContent = didToTs(didContent);
|
|
32
|
-
const jsPath = path.join(declarationsDir, `${baseName}.js`);
|
|
33
|
-
const dtsPath = path.join(declarationsDir, `${baseName}.d.ts`);
|
|
34
|
-
const didCopyPath = path.join(declarationsDir, `${baseName}.did`);
|
|
35
|
-
fs.writeFileSync(jsPath, jsContent);
|
|
36
|
-
fs.writeFileSync(dtsPath, tsContent);
|
|
37
|
-
fs.writeFileSync(didCopyPath, didContent);
|
|
38
|
-
return {
|
|
39
|
-
success: true,
|
|
40
|
-
declarationsDir,
|
|
41
|
-
files: [
|
|
42
|
-
{ success: true, filePath: jsPath },
|
|
43
|
-
{ success: true, filePath: dtsPath },
|
|
44
|
-
{ success: true, filePath: didCopyPath }
|
|
45
|
-
]
|
|
46
|
-
};
|
|
47
|
-
} catch (error) {
|
|
48
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
49
|
-
return {
|
|
50
|
-
success: false,
|
|
51
|
-
declarationsDir,
|
|
52
|
-
files: [],
|
|
53
|
-
error: `[${canisterName}] Failed to generate declarations: ${message}`
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function declarationsExist(outDir, canisterName) {
|
|
58
|
-
const dtsPath = path.join(outDir, "declarations", `${canisterName}.d.ts`);
|
|
59
|
-
return fs.existsSync(dtsPath);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// src/generators/reactor.ts
|
|
63
7
|
import path2 from "path";
|
|
8
|
+
import fs2 from "fs";
|
|
9
|
+
|
|
10
|
+
// src/validate.ts
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
64
13
|
|
|
65
14
|
// src/naming.ts
|
|
66
15
|
import { camelCase, pascalCase } from "change-case";
|
|
@@ -80,97 +29,7 @@ function getHookPrefix(canisterName) {
|
|
|
80
29
|
return toPascalCase(canisterName);
|
|
81
30
|
}
|
|
82
31
|
|
|
83
|
-
// src/generators/reactor.ts
|
|
84
|
-
function getReactorClassImportSource(reactorClass, runtimeTarget) {
|
|
85
|
-
switch (reactorClass) {
|
|
86
|
-
case "Reactor":
|
|
87
|
-
case "DisplayReactor":
|
|
88
|
-
return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react";
|
|
89
|
-
case "CandidReactor":
|
|
90
|
-
case "CandidDisplayReactor":
|
|
91
|
-
case "MetadataDisplayReactor":
|
|
92
|
-
return "@ic-reactor/candid";
|
|
93
|
-
default:
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function generateReactorFile(options) {
|
|
100
|
-
const {
|
|
101
|
-
canisterName,
|
|
102
|
-
didFile,
|
|
103
|
-
clientManagerPath = "../../clients",
|
|
104
|
-
canisterId,
|
|
105
|
-
runtimeTarget = "react",
|
|
106
|
-
reactorClass = "DisplayReactor"
|
|
107
|
-
} = options;
|
|
108
|
-
const pascalName = toPascalCase(canisterName);
|
|
109
|
-
const reactorName = getReactorName(canisterName);
|
|
110
|
-
const serviceName = getServiceTypeName(canisterName);
|
|
111
|
-
const baseName = path2.basename(didFile, ".did");
|
|
112
|
-
const declarationsPath = `./declarations/${baseName}`;
|
|
113
|
-
const reactorImportSource = getReactorClassImportSource(
|
|
114
|
-
reactorClass,
|
|
115
|
-
runtimeTarget
|
|
116
|
-
);
|
|
117
|
-
const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
|
|
118
|
-
` : "";
|
|
119
|
-
const hookExports = runtimeTarget === "react" ? `
|
|
120
|
-
|
|
121
|
-
export const {
|
|
122
|
-
useActorQuery: use${pascalName}Query,
|
|
123
|
-
useActorSuspenseQuery: use${pascalName}SuspenseQuery,
|
|
124
|
-
useActorInfiniteQuery: use${pascalName}InfiniteQuery,
|
|
125
|
-
useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
|
|
126
|
-
useActorMutation: use${pascalName}Mutation,
|
|
127
|
-
useActorMethod: use${pascalName}Method,
|
|
128
|
-
} = createActorHooks(${reactorName})
|
|
129
|
-
` : "";
|
|
130
|
-
return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
|
|
131
|
-
import { clientManager } from ${JSON.stringify(clientManagerPath)}
|
|
132
|
-
import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
|
|
133
|
-
|
|
134
|
-
export type ${serviceName} = _SERVICE
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* ${pascalName} Reactor
|
|
138
|
-
*
|
|
139
|
-
* Auto-generated by @ic-reactor/codegen \u2014 do not edit.
|
|
140
|
-
* This file is overwritten whenever generation runs.
|
|
141
|
-
*
|
|
142
|
-
* Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
|
|
143
|
-
* factory modules). Avoid editing this managed file directly.
|
|
144
|
-
*/
|
|
145
|
-
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
146
|
-
clientManager,
|
|
147
|
-
idlFactory,
|
|
148
|
-
${canisterIdLine} name: ${JSON.stringify(canisterName)},
|
|
149
|
-
})${hookExports || "\n"}`;
|
|
150
|
-
}
|
|
151
|
-
function generateReactorEntryFile() {
|
|
152
|
-
return `/**
|
|
153
|
-
* Canister entrypoint.
|
|
154
|
-
*
|
|
155
|
-
* Created once by @ic-reactor/codegen and safe to customize.
|
|
156
|
-
* Keep the re-export below if you want generated exports and types to stay in sync.
|
|
157
|
-
*
|
|
158
|
-
* Recommended customization points:
|
|
159
|
-
* - define reusable query/mutation factories
|
|
160
|
-
* - add app-specific hooks and cache invalidation wiring
|
|
161
|
-
* - compose generated APIs into route loaders/actions
|
|
162
|
-
*
|
|
163
|
-
* Do not edit \`index.generated.ts\`; it is regenerated on each codegen run.
|
|
164
|
-
* AI guide: https://ic-reactor.b3pay.net/llms-full.txt
|
|
165
|
-
* Skill install: npx skills add B3Pay/ic-reactor-skills --full-depth --skill ic-reactor-hooks
|
|
166
|
-
*/
|
|
167
|
-
export * from "./index.generated"
|
|
168
|
-
`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
32
|
// src/validate.ts
|
|
172
|
-
import fs2 from "fs";
|
|
173
|
-
import path3 from "path";
|
|
174
33
|
var CodegenConfigError = class extends Error {
|
|
175
34
|
name = "CodegenConfigError";
|
|
176
35
|
constructor(message) {
|
|
@@ -243,6 +102,24 @@ function assertSafeModuleSpecifier(label, specifier) {
|
|
|
243
102
|
);
|
|
244
103
|
}
|
|
245
104
|
}
|
|
105
|
+
function resolveDeclarationsBaseName(didFile) {
|
|
106
|
+
if (typeof didFile !== "string" || didFile.length === 0) {
|
|
107
|
+
throw new CodegenConfigError(
|
|
108
|
+
`Invalid didFile: expected a non-empty string, received ${didFile === void 0 ? "undefined" : JSON.stringify(didFile)}.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const baseName = path.basename(didFile, ".did");
|
|
112
|
+
if (baseName === "" || baseName === "." || baseName === "..") {
|
|
113
|
+
throw new CodegenConfigError(
|
|
114
|
+
`Invalid didFile ${JSON.stringify(didFile)}: its file name ${JSON.stringify(baseName)} refers to a directory, not a Candid file. Generated declarations are named after the .did file, so this would emit an import of a directory.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
assertSafeModuleSpecifier(
|
|
118
|
+
`declarations import derived from didFile ${JSON.stringify(didFile)}`,
|
|
119
|
+
`./declarations/${baseName}`
|
|
120
|
+
);
|
|
121
|
+
return baseName;
|
|
122
|
+
}
|
|
246
123
|
var REACTOR_CLASS_NAMES = [
|
|
247
124
|
"Reactor",
|
|
248
125
|
"DisplayReactor",
|
|
@@ -259,28 +136,28 @@ function assertOneOf(label, value, allowed) {
|
|
|
259
136
|
}
|
|
260
137
|
}
|
|
261
138
|
function realpathAllowingMissing(target) {
|
|
262
|
-
let current =
|
|
139
|
+
let current = path.resolve(target);
|
|
263
140
|
const missing = [];
|
|
264
141
|
for (; ; ) {
|
|
265
142
|
try {
|
|
266
|
-
return
|
|
143
|
+
return path.join(fs.realpathSync(current), ...missing);
|
|
267
144
|
} catch {
|
|
268
|
-
const parent =
|
|
269
|
-
if (parent === current) return
|
|
270
|
-
missing.unshift(
|
|
145
|
+
const parent = path.dirname(current);
|
|
146
|
+
if (parent === current) return path.resolve(target);
|
|
147
|
+
missing.unshift(path.basename(current));
|
|
271
148
|
current = parent;
|
|
272
149
|
}
|
|
273
150
|
}
|
|
274
151
|
}
|
|
275
152
|
function assertContainedPath(label, resolved, projectRoot, original = resolved) {
|
|
276
|
-
const relative =
|
|
153
|
+
const relative = path.relative(
|
|
277
154
|
realpathAllowingMissing(projectRoot),
|
|
278
155
|
realpathAllowingMissing(resolved)
|
|
279
156
|
);
|
|
280
157
|
const [firstSegment] = relative.split(/[\\/]/);
|
|
281
|
-
if (firstSegment === ".." ||
|
|
158
|
+
if (firstSegment === ".." || path.isAbsolute(relative)) {
|
|
282
159
|
throw new CodegenConfigError(
|
|
283
|
-
`Invalid ${label} ${JSON.stringify(original)}: resolves to ${JSON.stringify(resolved)}, which is outside the project root ${JSON.stringify(
|
|
160
|
+
`Invalid ${label} ${JSON.stringify(original)}: resolves to ${JSON.stringify(resolved)}, which is outside the project root ${JSON.stringify(path.resolve(projectRoot))}. Generated output must stay inside the project \u2014 generated directories are deleted and rewritten on every run.`
|
|
284
161
|
);
|
|
285
162
|
}
|
|
286
163
|
}
|
|
@@ -290,7 +167,7 @@ function resolveContainedOutDir(label, outDir, projectRoot) {
|
|
|
290
167
|
`Invalid ${label}: expected a non-empty string, received ${outDir === void 0 ? "undefined" : JSON.stringify(outDir)}.`
|
|
291
168
|
);
|
|
292
169
|
}
|
|
293
|
-
const resolved =
|
|
170
|
+
const resolved = path.isAbsolute(outDir) ? path.resolve(outDir) : path.resolve(projectRoot, outDir);
|
|
294
171
|
assertContainedPath(label, resolved, projectRoot, outDir);
|
|
295
172
|
return resolved;
|
|
296
173
|
}
|
|
@@ -312,7 +189,7 @@ function assertSafeCanisterConfig(options) {
|
|
|
312
189
|
`outDir for canister ${JSON.stringify(name)}`,
|
|
313
190
|
canisterOutDir,
|
|
314
191
|
projectRoot
|
|
315
|
-
) :
|
|
192
|
+
) : path.join(
|
|
316
193
|
resolveContainedOutDir("outDir", globalOutDir, projectRoot),
|
|
317
194
|
name
|
|
318
195
|
);
|
|
@@ -324,6 +201,213 @@ function assertSafeCanisterConfig(options) {
|
|
|
324
201
|
return { name, outDir, clientManagerPath };
|
|
325
202
|
}
|
|
326
203
|
|
|
204
|
+
// src/generators/declarations.ts
|
|
205
|
+
function replaceDirectory(from, to) {
|
|
206
|
+
if (!fs2.existsSync(to)) {
|
|
207
|
+
fs2.renameSync(from, to);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const displaced = fs2.mkdtempSync(
|
|
211
|
+
path2.join(path2.dirname(to), `.${path2.basename(to)}.old-`)
|
|
212
|
+
);
|
|
213
|
+
fs2.rmdirSync(displaced);
|
|
214
|
+
fs2.renameSync(to, displaced);
|
|
215
|
+
try {
|
|
216
|
+
fs2.renameSync(from, to);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
fs2.renameSync(displaced, to);
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
fs2.rmSync(displaced, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
var HAS_IDL_FACTORY = /\bexport\s+const\s+idlFactory\b/;
|
|
224
|
+
var OWNER_FILE = ".ic-reactor-owner";
|
|
225
|
+
async function generateDeclarations(options) {
|
|
226
|
+
const { didFile, outDir, canisterName } = options;
|
|
227
|
+
const declarationsDir = path2.join(outDir, "declarations");
|
|
228
|
+
let baseName;
|
|
229
|
+
try {
|
|
230
|
+
baseName = resolveDeclarationsBaseName(didFile);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error instanceof CodegenConfigError) {
|
|
233
|
+
return {
|
|
234
|
+
success: false,
|
|
235
|
+
declarationsDir: "",
|
|
236
|
+
files: [],
|
|
237
|
+
error: `[${canisterName}] ${error.message}`
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
let didStat;
|
|
243
|
+
try {
|
|
244
|
+
didStat = fs2.statSync(didFile);
|
|
245
|
+
} catch {
|
|
246
|
+
return {
|
|
247
|
+
success: false,
|
|
248
|
+
declarationsDir: "",
|
|
249
|
+
files: [],
|
|
250
|
+
error: `DID file not found: ${didFile}`
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (!didStat.isFile()) {
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
declarationsDir: "",
|
|
257
|
+
files: [],
|
|
258
|
+
error: `[${canisterName}] DID path is not a regular file: ${didFile}`
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
let staging;
|
|
262
|
+
try {
|
|
263
|
+
const didContent = fs2.readFileSync(didFile, "utf-8");
|
|
264
|
+
const jsContent = didToJs(didContent);
|
|
265
|
+
const tsContent = didToTs(didContent);
|
|
266
|
+
if (!HAS_IDL_FACTORY.test(jsContent)) {
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
declarationsDir,
|
|
270
|
+
files: [],
|
|
271
|
+
error: `[${canisterName}] ${didFile} produces no idlFactory, so there is no service to generate against. Point this canister at the .did file that declares its service.`
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
fs2.mkdirSync(outDir, { recursive: true });
|
|
275
|
+
fs2.writeFileSync(path2.join(outDir, OWNER_FILE), `${canisterName}
|
|
276
|
+
`);
|
|
277
|
+
staging = fs2.mkdtempSync(path2.join(outDir, ".declarations.tmp-"));
|
|
278
|
+
const jsPath = path2.join(declarationsDir, `${baseName}.js`);
|
|
279
|
+
const dtsPath = path2.join(declarationsDir, `${baseName}.d.ts`);
|
|
280
|
+
const didCopyPath = path2.join(declarationsDir, `${baseName}.did`);
|
|
281
|
+
fs2.writeFileSync(path2.join(staging, `${baseName}.js`), jsContent);
|
|
282
|
+
fs2.writeFileSync(path2.join(staging, `${baseName}.d.ts`), tsContent);
|
|
283
|
+
fs2.writeFileSync(path2.join(staging, `${baseName}.did`), didContent);
|
|
284
|
+
replaceDirectory(staging, declarationsDir);
|
|
285
|
+
staging = void 0;
|
|
286
|
+
return {
|
|
287
|
+
success: true,
|
|
288
|
+
declarationsDir,
|
|
289
|
+
files: [
|
|
290
|
+
{ success: true, filePath: jsPath },
|
|
291
|
+
{ success: true, filePath: dtsPath },
|
|
292
|
+
{ success: true, filePath: didCopyPath }
|
|
293
|
+
]
|
|
294
|
+
};
|
|
295
|
+
} catch (error) {
|
|
296
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
297
|
+
return {
|
|
298
|
+
success: false,
|
|
299
|
+
declarationsDir,
|
|
300
|
+
files: [],
|
|
301
|
+
error: `[${canisterName}] Failed to generate declarations: ${message}`
|
|
302
|
+
};
|
|
303
|
+
} finally {
|
|
304
|
+
if (staging) fs2.rmSync(staging, { recursive: true, force: true });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function declarationsExist(outDir, canisterName, didFile) {
|
|
308
|
+
const declarationsDir = path2.join(outDir, "declarations");
|
|
309
|
+
if (didFile !== void 0) {
|
|
310
|
+
const baseName = path2.basename(didFile, ".did");
|
|
311
|
+
return fs2.existsSync(path2.join(declarationsDir, `${baseName}.d.ts`));
|
|
312
|
+
}
|
|
313
|
+
if (fs2.existsSync(path2.join(declarationsDir, `${canisterName}.d.ts`))) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
return fs2.readdirSync(declarationsDir).some((entry) => entry.endsWith(".d.ts"));
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/generators/reactor.ts
|
|
324
|
+
function getReactorClassImportSource(reactorClass, runtimeTarget) {
|
|
325
|
+
switch (reactorClass) {
|
|
326
|
+
case "Reactor":
|
|
327
|
+
case "DisplayReactor":
|
|
328
|
+
return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react";
|
|
329
|
+
case "CandidReactor":
|
|
330
|
+
case "CandidDisplayReactor":
|
|
331
|
+
case "MetadataDisplayReactor":
|
|
332
|
+
return "@ic-reactor/candid";
|
|
333
|
+
default:
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function generateReactorFile(options) {
|
|
340
|
+
const {
|
|
341
|
+
canisterName,
|
|
342
|
+
didFile,
|
|
343
|
+
clientManagerPath = "../../clients",
|
|
344
|
+
canisterId,
|
|
345
|
+
runtimeTarget = "react",
|
|
346
|
+
reactorClass = "DisplayReactor"
|
|
347
|
+
} = options;
|
|
348
|
+
const pascalName = toPascalCase(canisterName);
|
|
349
|
+
const reactorName = getReactorName(canisterName);
|
|
350
|
+
const serviceName = getServiceTypeName(canisterName);
|
|
351
|
+
const baseName = resolveDeclarationsBaseName(didFile);
|
|
352
|
+
const declarationsPath = `./declarations/${baseName}`;
|
|
353
|
+
const reactorImportSource = getReactorClassImportSource(
|
|
354
|
+
reactorClass,
|
|
355
|
+
runtimeTarget
|
|
356
|
+
);
|
|
357
|
+
const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
|
|
358
|
+
` : "";
|
|
359
|
+
const hookExports = runtimeTarget === "react" ? `
|
|
360
|
+
|
|
361
|
+
export const {
|
|
362
|
+
useActorQuery: use${pascalName}Query,
|
|
363
|
+
useActorSuspenseQuery: use${pascalName}SuspenseQuery,
|
|
364
|
+
useActorInfiniteQuery: use${pascalName}InfiniteQuery,
|
|
365
|
+
useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
|
|
366
|
+
useActorMutation: use${pascalName}Mutation,
|
|
367
|
+
useActorMethod: use${pascalName}Method,
|
|
368
|
+
} = createActorHooks(${reactorName})
|
|
369
|
+
` : "";
|
|
370
|
+
return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
|
|
371
|
+
import { clientManager } from ${JSON.stringify(clientManagerPath)}
|
|
372
|
+
import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
|
|
373
|
+
|
|
374
|
+
export type ${serviceName} = _SERVICE
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* ${pascalName} Reactor
|
|
378
|
+
*
|
|
379
|
+
* Auto-generated by @ic-reactor/codegen \u2014 do not edit.
|
|
380
|
+
* This file is overwritten whenever generation runs.
|
|
381
|
+
*
|
|
382
|
+
* Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
|
|
383
|
+
* factory modules). Avoid editing this managed file directly.
|
|
384
|
+
*/
|
|
385
|
+
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
386
|
+
clientManager,
|
|
387
|
+
idlFactory,
|
|
388
|
+
${canisterIdLine} name: ${JSON.stringify(canisterName)},
|
|
389
|
+
})${hookExports || "\n"}`;
|
|
390
|
+
}
|
|
391
|
+
function generateReactorEntryFile() {
|
|
392
|
+
return `/**
|
|
393
|
+
* Canister entrypoint.
|
|
394
|
+
*
|
|
395
|
+
* Created once by @ic-reactor/codegen and safe to customize.
|
|
396
|
+
* Keep the re-export below if you want generated exports and types to stay in sync.
|
|
397
|
+
*
|
|
398
|
+
* Recommended customization points:
|
|
399
|
+
* - define reusable query/mutation factories
|
|
400
|
+
* - add app-specific hooks and cache invalidation wiring
|
|
401
|
+
* - compose generated APIs into route loaders/actions
|
|
402
|
+
*
|
|
403
|
+
* Do not edit \`index.generated.ts\`; it is regenerated on each codegen run.
|
|
404
|
+
* AI guide: https://ic-reactor.b3pay.net/llms-full.txt
|
|
405
|
+
* Skill install: npx skills add B3Pay/ic-reactor-skills --full-depth --skill ic-reactor-hooks
|
|
406
|
+
*/
|
|
407
|
+
export * from "./index.generated"
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
|
|
327
411
|
// src/pipeline.ts
|
|
328
412
|
function resolveReactorClass(canisterConfig) {
|
|
329
413
|
return canisterConfig.mode ?? "DisplayReactor";
|
|
@@ -334,8 +418,48 @@ function resolveRuntimeTarget(canisterConfig, globalConfig) {
|
|
|
334
418
|
function normalizeFileContent(content) {
|
|
335
419
|
return content.replace(/\r\n/g, "\n").trim();
|
|
336
420
|
}
|
|
337
|
-
|
|
338
|
-
|
|
421
|
+
var GENERATED_MARKER = "Auto-generated by @ic-reactor/codegen";
|
|
422
|
+
var GENERATED_CANISTER_NAME = /^ {2}name: "([A-Za-z0-9_.-]+)",$/m;
|
|
423
|
+
var EXPORT_STATEMENT = /^export\b.*$/gm;
|
|
424
|
+
function isLegacyGeneratedIndexFile(content, canisterName) {
|
|
425
|
+
if (!content.includes(GENERATED_MARKER)) return false;
|
|
426
|
+
const reactorName = getReactorName(canisterName);
|
|
427
|
+
const serviceName = getServiceTypeName(canisterName);
|
|
428
|
+
if (!content.includes(`export const ${reactorName} = new `)) return false;
|
|
429
|
+
if (!content.includes(`= createActorHooks(${reactorName})`)) return false;
|
|
430
|
+
const allowed = [
|
|
431
|
+
`export type ${serviceName} = _SERVICE`,
|
|
432
|
+
`export const ${reactorName} = new `,
|
|
433
|
+
// Opens the destructured hook block; its members are not export statements.
|
|
434
|
+
"export const {"
|
|
435
|
+
];
|
|
436
|
+
const statements = normalizeFileContent(content).match(EXPORT_STATEMENT) ?? [];
|
|
437
|
+
return statements.every(
|
|
438
|
+
(statement) => allowed.some((prefix) => statement.startsWith(prefix))
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
function backUpFile(filePath) {
|
|
442
|
+
let backupPath = `${filePath}.bak`;
|
|
443
|
+
for (let n = 2; fs3.existsSync(backupPath); n += 1) {
|
|
444
|
+
backupPath = `${filePath}.bak.${n}`;
|
|
445
|
+
}
|
|
446
|
+
fs3.copyFileSync(filePath, backupPath);
|
|
447
|
+
return backupPath;
|
|
448
|
+
}
|
|
449
|
+
function findOutDirOwner(outDir) {
|
|
450
|
+
try {
|
|
451
|
+
const owner = fs3.readFileSync(path3.join(outDir, OWNER_FILE), "utf-8").trim();
|
|
452
|
+
if (owner) return owner;
|
|
453
|
+
} catch {
|
|
454
|
+
}
|
|
455
|
+
let content;
|
|
456
|
+
try {
|
|
457
|
+
content = fs3.readFileSync(path3.join(outDir, "index.generated.ts"), "utf-8");
|
|
458
|
+
} catch {
|
|
459
|
+
return void 0;
|
|
460
|
+
}
|
|
461
|
+
if (!content.includes(GENERATED_MARKER)) return void 0;
|
|
462
|
+
return GENERATED_CANISTER_NAME.exec(content)?.[1];
|
|
339
463
|
}
|
|
340
464
|
function isManagedEntryWrapper(content, expectedEntryContent) {
|
|
341
465
|
return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
|
|
@@ -371,7 +495,7 @@ async function runCanisterPipeline(options) {
|
|
|
371
495
|
}
|
|
372
496
|
throw err;
|
|
373
497
|
}
|
|
374
|
-
const resolvedDidFile =
|
|
498
|
+
const resolvedDidFile = path3.isAbsolute(didFile) ? didFile : path3.resolve(projectRoot, didFile);
|
|
375
499
|
if (!fs3.existsSync(resolvedDidFile)) {
|
|
376
500
|
return {
|
|
377
501
|
canisterName: name,
|
|
@@ -380,7 +504,29 @@ async function runCanisterPipeline(options) {
|
|
|
380
504
|
error: `DID file not found: ${resolvedDidFile}`
|
|
381
505
|
};
|
|
382
506
|
}
|
|
507
|
+
try {
|
|
508
|
+
resolveDeclarationsBaseName(resolvedDidFile);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
if (err instanceof CodegenConfigError) {
|
|
511
|
+
return {
|
|
512
|
+
canisterName: name,
|
|
513
|
+
success: false,
|
|
514
|
+
files,
|
|
515
|
+
error: `[${name}] ${err.message}`
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
throw err;
|
|
519
|
+
}
|
|
383
520
|
const canisterOutDir = validated.outDir;
|
|
521
|
+
const owner = findOutDirOwner(canisterOutDir);
|
|
522
|
+
if (owner !== void 0 && owner !== name) {
|
|
523
|
+
return {
|
|
524
|
+
canisterName: name,
|
|
525
|
+
success: false,
|
|
526
|
+
files,
|
|
527
|
+
error: `[${name}] Output directory ${canisterOutDir} was generated for canister "${owner}". Two canisters cannot share an outDir \u2014 each run replaces the declarations directory and index.generated.ts, so they would overwrite each other. Give each canister its own "outDir". If you renamed "${owner}" to "${name}", delete that directory and regenerate.`
|
|
528
|
+
};
|
|
529
|
+
}
|
|
384
530
|
const resolvedClientManagerPath = validated.clientManagerPath;
|
|
385
531
|
try {
|
|
386
532
|
const declResult = await generateDeclarations({
|
|
@@ -412,8 +558,8 @@ async function runCanisterPipeline(options) {
|
|
|
412
558
|
files
|
|
413
559
|
};
|
|
414
560
|
}
|
|
415
|
-
const reactorPath =
|
|
416
|
-
const entryPath =
|
|
561
|
+
const reactorPath = path3.join(canisterOutDir, "index.generated.ts");
|
|
562
|
+
const entryPath = path3.join(canisterOutDir, "index.ts");
|
|
417
563
|
const reactorClass = resolveReactorClass(canisterConfig);
|
|
418
564
|
const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
|
|
419
565
|
try {
|
|
@@ -434,9 +580,14 @@ async function runCanisterPipeline(options) {
|
|
|
434
580
|
files.push({ success: true, filePath: entryPath });
|
|
435
581
|
} else {
|
|
436
582
|
const existingEntryContent = fs3.readFileSync(entryPath, "utf-8");
|
|
437
|
-
if (
|
|
583
|
+
if (isManagedEntryWrapper(existingEntryContent, entryContent)) {
|
|
438
584
|
fs3.writeFileSync(entryPath, entryContent);
|
|
439
585
|
files.push({ success: true, filePath: entryPath });
|
|
586
|
+
} else if (isLegacyGeneratedIndexFile(existingEntryContent, name)) {
|
|
587
|
+
const backupPath = backUpFile(entryPath);
|
|
588
|
+
fs3.writeFileSync(entryPath, entryContent);
|
|
589
|
+
files.push({ success: true, filePath: backupPath });
|
|
590
|
+
files.push({ success: true, filePath: entryPath });
|
|
440
591
|
} else {
|
|
441
592
|
files.push({ success: true, filePath: entryPath, skipped: true });
|
|
442
593
|
}
|
|
@@ -461,40 +612,6 @@ async function runCanisterPipeline(options) {
|
|
|
461
612
|
};
|
|
462
613
|
}
|
|
463
614
|
|
|
464
|
-
// src/parser.ts
|
|
465
|
-
import { didToJs as didToJs2 } from "@ic-reactor/parser";
|
|
466
|
-
import fs4 from "fs";
|
|
467
|
-
function extractMethods(didContent) {
|
|
468
|
-
try {
|
|
469
|
-
const jsContent = didToJs2(didContent);
|
|
470
|
-
const methods = [];
|
|
471
|
-
const serviceMatch = /IDL\.Service\(\{([\s\S]*?)\}\)/.exec(jsContent);
|
|
472
|
-
if (!serviceMatch) return methods;
|
|
473
|
-
const serviceBody = serviceMatch[1];
|
|
474
|
-
const methodRegex = /['"]([\w]+)['"]\s*:\s*IDL\.Func\(\s*\[(.*?)\],\s*\[.*?\],\s*\[(.*?)\]\)/g;
|
|
475
|
-
let match;
|
|
476
|
-
while ((match = methodRegex.exec(serviceBody)) !== null) {
|
|
477
|
-
const [, name, args, annotations] = match;
|
|
478
|
-
methods.push({
|
|
479
|
-
name,
|
|
480
|
-
type: annotations.includes("'query'") ? "query" : "mutation",
|
|
481
|
-
hasArgs: args.trim().length > 0
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
return methods;
|
|
485
|
-
} catch (error) {
|
|
486
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
487
|
-
throw new Error(`Failed to parse Candid: ${msg}`);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
function parseDIDFile(didFilePath) {
|
|
491
|
-
if (!fs4.existsSync(didFilePath)) {
|
|
492
|
-
throw new Error(`DID file not found: ${didFilePath}`);
|
|
493
|
-
}
|
|
494
|
-
const content = fs4.readFileSync(didFilePath, "utf-8");
|
|
495
|
-
return extractMethods(content);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
615
|
// src/generators/client.ts
|
|
499
616
|
function generateClientFile(options = {}) {
|
|
500
617
|
const { queryClientPath } = options;
|
|
@@ -526,15 +643,14 @@ export {
|
|
|
526
643
|
assertSafeCanisterName,
|
|
527
644
|
assertSafeModuleSpecifier,
|
|
528
645
|
declarationsExist,
|
|
529
|
-
extractMethods,
|
|
530
646
|
generateClientFile,
|
|
531
647
|
generateDeclarations,
|
|
532
648
|
generateReactorEntryFile,
|
|
533
649
|
generateReactorFile,
|
|
534
650
|
getReactorName,
|
|
535
651
|
getServiceTypeName,
|
|
536
|
-
parseDIDFile,
|
|
537
652
|
resolveContainedOutDir,
|
|
653
|
+
resolveDeclarationsBaseName,
|
|
538
654
|
runCanisterPipeline,
|
|
539
655
|
toPascalCase
|
|
540
656
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ic-reactor/codegen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Shared code generation utilities for IC Reactor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"dist",
|
|
24
24
|
"src",
|
|
25
|
+
"!src/**/*.test.*",
|
|
26
|
+
"!src/**/*.spec.*",
|
|
27
|
+
"!src/**/__snapshots__/**",
|
|
28
|
+
"!**/*.tsbuildinfo",
|
|
25
29
|
"README.md",
|
|
26
30
|
"llms.txt"
|
|
27
31
|
],
|
|
@@ -43,10 +47,10 @@
|
|
|
43
47
|
"homepage": "https://ic-reactor.b3pay.net/v3/packages/codegen",
|
|
44
48
|
"dependencies": {
|
|
45
49
|
"change-case": "^5.4.4",
|
|
46
|
-
"@ic-reactor/parser": "0.4.
|
|
50
|
+
"@ic-reactor/parser": "0.4.8"
|
|
47
51
|
},
|
|
48
52
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^26.
|
|
53
|
+
"@types/node": "^26.2.0",
|
|
50
54
|
"tsup": "^8.5.1",
|
|
51
55
|
"typescript": "^6.0.3",
|
|
52
56
|
"vitest": "^4.1.10"
|