@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/README.md
CHANGED
|
@@ -45,7 +45,9 @@ Set `target` to control whether generated files include React hooks:
|
|
|
45
45
|
- `react` (default): generates the reactor plus bound `createActorHooks` exports
|
|
46
46
|
- `core`: generates only the typed reactor exports with no `@ic-reactor/react` dependency
|
|
47
47
|
|
|
48
|
-
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.
|
|
48
|
+
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. A scaffold that carries any export the old generator never wrote counts as user-owned and is left alone; one that is migrated is copied to `index.ts.bak` first.
|
|
49
|
+
|
|
50
|
+
Each canister needs its own `outDir`. Generating into a directory that already holds another canister's `index.generated.ts` fails rather than overwriting it, because every run replaces `declarations/` wholesale.
|
|
49
51
|
|
|
50
52
|
Set `generateReactor: false` if you only want the bindgen/declaration output and
|
|
51
53
|
need to skip `index.generated.ts` and `index.ts`.
|
|
@@ -54,7 +56,7 @@ need to skip `index.generated.ts` and `index.ts`.
|
|
|
54
56
|
|
|
55
57
|
You can also use individual generators if you need more granular control:
|
|
56
58
|
|
|
57
|
-
- **`generateDeclarations`**: Writes `declarations/<did-basename>.js` (factory), `.d.ts` (types), and a `.did` copy.
|
|
59
|
+
- **`generateDeclarations`**: Writes `declarations/<did-basename>.js` (factory), `.d.ts` (types), and a `.did` copy. Generation runs in a staging directory that replaces `declarations/` only once every file is written, so a `.did` that fails to parse leaves the previous declarations untouched. A `.did` that parses but declares no `service` is rejected — it would produce no `idlFactory` and no `_SERVICE`.
|
|
58
60
|
- **`generateReactorFile`**: Generates the managed `index.generated.ts` implementation using any `ReactorClassName` — `Reactor`, `DisplayReactor` (default), `CandidReactor`, `CandidDisplayReactor`, or `MetadataDisplayReactor`. With `target: "react"` it also emits the six `createActorHooks` exports (`use<Canister>Query`, `use<Canister>SuspenseQuery`, `use<Canister>InfiniteQuery`, `use<Canister>SuspenseInfiniteQuery`, `use<Canister>Mutation`, `use<Canister>Method`). No `createQuery` / `createMutation` objects are generated.
|
|
59
61
|
- **`generateReactorEntryFile`**: Generates the stable `index.ts` wrapper that re-exports from `index.generated.ts`.
|
|
60
62
|
- **`generateClientFile`**: Generates a `ClientManager` boilerplate file that
|
|
@@ -62,8 +64,17 @@ You can also use individual generators if you need more granular control:
|
|
|
62
64
|
|
|
63
65
|
## Utilities
|
|
64
66
|
|
|
65
|
-
- **`parseDIDFile` / `extractMethods`**: Parse a `.did` file and extract method signatures.
|
|
66
67
|
- **`toPascalCase` / `getReactorName` / `getServiceTypeName`**: Naming helpers.
|
|
68
|
+
- **`assertSafeCanisterConfig` and friends**: Validate a canister config before generating; the pipeline runs these itself.
|
|
69
|
+
|
|
70
|
+
`parseDIDFile` / `extractMethods` (and the `MethodInfo` / `MethodType` types)
|
|
71
|
+
have been removed. They regex-scraped the pretty-printed JS from
|
|
72
|
+
`didToJs`, which silently returned no methods for any service whose first
|
|
73
|
+
method took an inline record, dropped every method whose signature wrapped over
|
|
74
|
+
80 columns, and reported `composite_query` as a mutation. Read a `.did` with
|
|
75
|
+
[`parseDid()`](https://ic-reactor.b3pay.net/v3/packages/parser) from
|
|
76
|
+
`@ic-reactor/parser` instead — it returns `{ name, mode, args, returns }` per
|
|
77
|
+
method, straight from the Rust parser.
|
|
67
78
|
|
|
68
79
|
## License
|
|
69
80
|
|
package/dist/index.cjs
CHANGED
|
@@ -40,15 +40,14 @@ __export(index_exports, {
|
|
|
40
40
|
assertSafeCanisterName: () => assertSafeCanisterName,
|
|
41
41
|
assertSafeModuleSpecifier: () => assertSafeModuleSpecifier,
|
|
42
42
|
declarationsExist: () => declarationsExist,
|
|
43
|
-
extractMethods: () => extractMethods,
|
|
44
43
|
generateClientFile: () => generateClientFile,
|
|
45
44
|
generateDeclarations: () => generateDeclarations,
|
|
46
45
|
generateReactorEntryFile: () => generateReactorEntryFile,
|
|
47
46
|
generateReactorFile: () => generateReactorFile,
|
|
48
47
|
getReactorName: () => getReactorName,
|
|
49
48
|
getServiceTypeName: () => getServiceTypeName,
|
|
50
|
-
parseDIDFile: () => parseDIDFile,
|
|
51
49
|
resolveContainedOutDir: () => resolveContainedOutDir,
|
|
50
|
+
resolveDeclarationsBaseName: () => resolveDeclarationsBaseName,
|
|
52
51
|
runCanisterPipeline: () => runCanisterPipeline,
|
|
53
52
|
toPascalCase: () => toPascalCase
|
|
54
53
|
});
|
|
@@ -56,67 +55,16 @@ module.exports = __toCommonJS(index_exports);
|
|
|
56
55
|
|
|
57
56
|
// src/pipeline.ts
|
|
58
57
|
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
59
|
-
var
|
|
58
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
60
59
|
|
|
61
60
|
// src/generators/declarations.ts
|
|
62
61
|
var import_parser = require("@ic-reactor/parser");
|
|
63
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
64
|
-
var import_node_fs = __toESM(require("fs"), 1);
|
|
65
|
-
async function generateDeclarations(options) {
|
|
66
|
-
const { didFile, outDir, canisterName } = options;
|
|
67
|
-
if (!import_node_fs.default.existsSync(didFile)) {
|
|
68
|
-
return {
|
|
69
|
-
success: false,
|
|
70
|
-
declarationsDir: "",
|
|
71
|
-
files: [],
|
|
72
|
-
error: `DID file not found: ${didFile}`
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
const declarationsDir = import_node_path.default.join(outDir, "declarations");
|
|
76
|
-
const baseName = import_node_path.default.basename(didFile, ".did");
|
|
77
|
-
try {
|
|
78
|
-
const didContent = import_node_fs.default.readFileSync(didFile, "utf-8");
|
|
79
|
-
if (!import_node_fs.default.existsSync(outDir)) {
|
|
80
|
-
import_node_fs.default.mkdirSync(outDir, { recursive: true });
|
|
81
|
-
}
|
|
82
|
-
if (import_node_fs.default.existsSync(declarationsDir)) {
|
|
83
|
-
import_node_fs.default.rmSync(declarationsDir, { recursive: true, force: true });
|
|
84
|
-
}
|
|
85
|
-
import_node_fs.default.mkdirSync(declarationsDir, { recursive: true });
|
|
86
|
-
const jsContent = (0, import_parser.didToJs)(didContent);
|
|
87
|
-
const tsContent = (0, import_parser.didToTs)(didContent);
|
|
88
|
-
const jsPath = import_node_path.default.join(declarationsDir, `${baseName}.js`);
|
|
89
|
-
const dtsPath = import_node_path.default.join(declarationsDir, `${baseName}.d.ts`);
|
|
90
|
-
const didCopyPath = import_node_path.default.join(declarationsDir, `${baseName}.did`);
|
|
91
|
-
import_node_fs.default.writeFileSync(jsPath, jsContent);
|
|
92
|
-
import_node_fs.default.writeFileSync(dtsPath, tsContent);
|
|
93
|
-
import_node_fs.default.writeFileSync(didCopyPath, didContent);
|
|
94
|
-
return {
|
|
95
|
-
success: true,
|
|
96
|
-
declarationsDir,
|
|
97
|
-
files: [
|
|
98
|
-
{ success: true, filePath: jsPath },
|
|
99
|
-
{ success: true, filePath: dtsPath },
|
|
100
|
-
{ success: true, filePath: didCopyPath }
|
|
101
|
-
]
|
|
102
|
-
};
|
|
103
|
-
} catch (error) {
|
|
104
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
-
return {
|
|
106
|
-
success: false,
|
|
107
|
-
declarationsDir,
|
|
108
|
-
files: [],
|
|
109
|
-
error: `[${canisterName}] Failed to generate declarations: ${message}`
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
function declarationsExist(outDir, canisterName) {
|
|
114
|
-
const dtsPath = import_node_path.default.join(outDir, "declarations", `${canisterName}.d.ts`);
|
|
115
|
-
return import_node_fs.default.existsSync(dtsPath);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// src/generators/reactor.ts
|
|
119
62
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
63
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
64
|
+
|
|
65
|
+
// src/validate.ts
|
|
66
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
67
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
120
68
|
|
|
121
69
|
// src/naming.ts
|
|
122
70
|
var import_change_case = require("change-case");
|
|
@@ -136,97 +84,7 @@ function getHookPrefix(canisterName) {
|
|
|
136
84
|
return toPascalCase(canisterName);
|
|
137
85
|
}
|
|
138
86
|
|
|
139
|
-
// src/generators/reactor.ts
|
|
140
|
-
function getReactorClassImportSource(reactorClass, runtimeTarget) {
|
|
141
|
-
switch (reactorClass) {
|
|
142
|
-
case "Reactor":
|
|
143
|
-
case "DisplayReactor":
|
|
144
|
-
return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react";
|
|
145
|
-
case "CandidReactor":
|
|
146
|
-
case "CandidDisplayReactor":
|
|
147
|
-
case "MetadataDisplayReactor":
|
|
148
|
-
return "@ic-reactor/candid";
|
|
149
|
-
default:
|
|
150
|
-
throw new Error(
|
|
151
|
-
`Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
function generateReactorFile(options) {
|
|
156
|
-
const {
|
|
157
|
-
canisterName,
|
|
158
|
-
didFile,
|
|
159
|
-
clientManagerPath = "../../clients",
|
|
160
|
-
canisterId,
|
|
161
|
-
runtimeTarget = "react",
|
|
162
|
-
reactorClass = "DisplayReactor"
|
|
163
|
-
} = options;
|
|
164
|
-
const pascalName = toPascalCase(canisterName);
|
|
165
|
-
const reactorName = getReactorName(canisterName);
|
|
166
|
-
const serviceName = getServiceTypeName(canisterName);
|
|
167
|
-
const baseName = import_node_path2.default.basename(didFile, ".did");
|
|
168
|
-
const declarationsPath = `./declarations/${baseName}`;
|
|
169
|
-
const reactorImportSource = getReactorClassImportSource(
|
|
170
|
-
reactorClass,
|
|
171
|
-
runtimeTarget
|
|
172
|
-
);
|
|
173
|
-
const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
|
|
174
|
-
` : "";
|
|
175
|
-
const hookExports = runtimeTarget === "react" ? `
|
|
176
|
-
|
|
177
|
-
export const {
|
|
178
|
-
useActorQuery: use${pascalName}Query,
|
|
179
|
-
useActorSuspenseQuery: use${pascalName}SuspenseQuery,
|
|
180
|
-
useActorInfiniteQuery: use${pascalName}InfiniteQuery,
|
|
181
|
-
useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
|
|
182
|
-
useActorMutation: use${pascalName}Mutation,
|
|
183
|
-
useActorMethod: use${pascalName}Method,
|
|
184
|
-
} = createActorHooks(${reactorName})
|
|
185
|
-
` : "";
|
|
186
|
-
return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
|
|
187
|
-
import { clientManager } from ${JSON.stringify(clientManagerPath)}
|
|
188
|
-
import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
|
|
189
|
-
|
|
190
|
-
export type ${serviceName} = _SERVICE
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* ${pascalName} Reactor
|
|
194
|
-
*
|
|
195
|
-
* Auto-generated by @ic-reactor/codegen \u2014 do not edit.
|
|
196
|
-
* This file is overwritten whenever generation runs.
|
|
197
|
-
*
|
|
198
|
-
* Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
|
|
199
|
-
* factory modules). Avoid editing this managed file directly.
|
|
200
|
-
*/
|
|
201
|
-
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
202
|
-
clientManager,
|
|
203
|
-
idlFactory,
|
|
204
|
-
${canisterIdLine} name: ${JSON.stringify(canisterName)},
|
|
205
|
-
})${hookExports || "\n"}`;
|
|
206
|
-
}
|
|
207
|
-
function generateReactorEntryFile() {
|
|
208
|
-
return `/**
|
|
209
|
-
* Canister entrypoint.
|
|
210
|
-
*
|
|
211
|
-
* Created once by @ic-reactor/codegen and safe to customize.
|
|
212
|
-
* Keep the re-export below if you want generated exports and types to stay in sync.
|
|
213
|
-
*
|
|
214
|
-
* Recommended customization points:
|
|
215
|
-
* - define reusable query/mutation factories
|
|
216
|
-
* - add app-specific hooks and cache invalidation wiring
|
|
217
|
-
* - compose generated APIs into route loaders/actions
|
|
218
|
-
*
|
|
219
|
-
* Do not edit \`index.generated.ts\`; it is regenerated on each codegen run.
|
|
220
|
-
* AI guide: https://ic-reactor.b3pay.net/llms-full.txt
|
|
221
|
-
* Skill install: npx skills add B3Pay/ic-reactor-skills --full-depth --skill ic-reactor-hooks
|
|
222
|
-
*/
|
|
223
|
-
export * from "./index.generated"
|
|
224
|
-
`;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
87
|
// src/validate.ts
|
|
228
|
-
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
229
|
-
var import_node_path3 = __toESM(require("path"), 1);
|
|
230
88
|
var CodegenConfigError = class extends Error {
|
|
231
89
|
name = "CodegenConfigError";
|
|
232
90
|
constructor(message) {
|
|
@@ -299,6 +157,24 @@ function assertSafeModuleSpecifier(label, specifier) {
|
|
|
299
157
|
);
|
|
300
158
|
}
|
|
301
159
|
}
|
|
160
|
+
function resolveDeclarationsBaseName(didFile) {
|
|
161
|
+
if (typeof didFile !== "string" || didFile.length === 0) {
|
|
162
|
+
throw new CodegenConfigError(
|
|
163
|
+
`Invalid didFile: expected a non-empty string, received ${didFile === void 0 ? "undefined" : JSON.stringify(didFile)}.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const baseName = import_node_path.default.basename(didFile, ".did");
|
|
167
|
+
if (baseName === "" || baseName === "." || baseName === "..") {
|
|
168
|
+
throw new CodegenConfigError(
|
|
169
|
+
`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.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
assertSafeModuleSpecifier(
|
|
173
|
+
`declarations import derived from didFile ${JSON.stringify(didFile)}`,
|
|
174
|
+
`./declarations/${baseName}`
|
|
175
|
+
);
|
|
176
|
+
return baseName;
|
|
177
|
+
}
|
|
302
178
|
var REACTOR_CLASS_NAMES = [
|
|
303
179
|
"Reactor",
|
|
304
180
|
"DisplayReactor",
|
|
@@ -315,28 +191,28 @@ function assertOneOf(label, value, allowed) {
|
|
|
315
191
|
}
|
|
316
192
|
}
|
|
317
193
|
function realpathAllowingMissing(target) {
|
|
318
|
-
let current =
|
|
194
|
+
let current = import_node_path.default.resolve(target);
|
|
319
195
|
const missing = [];
|
|
320
196
|
for (; ; ) {
|
|
321
197
|
try {
|
|
322
|
-
return
|
|
198
|
+
return import_node_path.default.join(import_node_fs.default.realpathSync(current), ...missing);
|
|
323
199
|
} catch {
|
|
324
|
-
const parent =
|
|
325
|
-
if (parent === current) return
|
|
326
|
-
missing.unshift(
|
|
200
|
+
const parent = import_node_path.default.dirname(current);
|
|
201
|
+
if (parent === current) return import_node_path.default.resolve(target);
|
|
202
|
+
missing.unshift(import_node_path.default.basename(current));
|
|
327
203
|
current = parent;
|
|
328
204
|
}
|
|
329
205
|
}
|
|
330
206
|
}
|
|
331
207
|
function assertContainedPath(label, resolved, projectRoot, original = resolved) {
|
|
332
|
-
const relative =
|
|
208
|
+
const relative = import_node_path.default.relative(
|
|
333
209
|
realpathAllowingMissing(projectRoot),
|
|
334
210
|
realpathAllowingMissing(resolved)
|
|
335
211
|
);
|
|
336
212
|
const [firstSegment] = relative.split(/[\\/]/);
|
|
337
|
-
if (firstSegment === ".." ||
|
|
213
|
+
if (firstSegment === ".." || import_node_path.default.isAbsolute(relative)) {
|
|
338
214
|
throw new CodegenConfigError(
|
|
339
|
-
`Invalid ${label} ${JSON.stringify(original)}: resolves to ${JSON.stringify(resolved)}, which is outside the project root ${JSON.stringify(
|
|
215
|
+
`Invalid ${label} ${JSON.stringify(original)}: resolves to ${JSON.stringify(resolved)}, which is outside the project root ${JSON.stringify(import_node_path.default.resolve(projectRoot))}. Generated output must stay inside the project \u2014 generated directories are deleted and rewritten on every run.`
|
|
340
216
|
);
|
|
341
217
|
}
|
|
342
218
|
}
|
|
@@ -346,7 +222,7 @@ function resolveContainedOutDir(label, outDir, projectRoot) {
|
|
|
346
222
|
`Invalid ${label}: expected a non-empty string, received ${outDir === void 0 ? "undefined" : JSON.stringify(outDir)}.`
|
|
347
223
|
);
|
|
348
224
|
}
|
|
349
|
-
const resolved =
|
|
225
|
+
const resolved = import_node_path.default.isAbsolute(outDir) ? import_node_path.default.resolve(outDir) : import_node_path.default.resolve(projectRoot, outDir);
|
|
350
226
|
assertContainedPath(label, resolved, projectRoot, outDir);
|
|
351
227
|
return resolved;
|
|
352
228
|
}
|
|
@@ -368,7 +244,7 @@ function assertSafeCanisterConfig(options) {
|
|
|
368
244
|
`outDir for canister ${JSON.stringify(name)}`,
|
|
369
245
|
canisterOutDir,
|
|
370
246
|
projectRoot
|
|
371
|
-
) :
|
|
247
|
+
) : import_node_path.default.join(
|
|
372
248
|
resolveContainedOutDir("outDir", globalOutDir, projectRoot),
|
|
373
249
|
name
|
|
374
250
|
);
|
|
@@ -380,6 +256,213 @@ function assertSafeCanisterConfig(options) {
|
|
|
380
256
|
return { name, outDir, clientManagerPath };
|
|
381
257
|
}
|
|
382
258
|
|
|
259
|
+
// src/generators/declarations.ts
|
|
260
|
+
function replaceDirectory(from, to) {
|
|
261
|
+
if (!import_node_fs2.default.existsSync(to)) {
|
|
262
|
+
import_node_fs2.default.renameSync(from, to);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const displaced = import_node_fs2.default.mkdtempSync(
|
|
266
|
+
import_node_path2.default.join(import_node_path2.default.dirname(to), `.${import_node_path2.default.basename(to)}.old-`)
|
|
267
|
+
);
|
|
268
|
+
import_node_fs2.default.rmdirSync(displaced);
|
|
269
|
+
import_node_fs2.default.renameSync(to, displaced);
|
|
270
|
+
try {
|
|
271
|
+
import_node_fs2.default.renameSync(from, to);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
import_node_fs2.default.renameSync(displaced, to);
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
import_node_fs2.default.rmSync(displaced, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
var HAS_IDL_FACTORY = /\bexport\s+const\s+idlFactory\b/;
|
|
279
|
+
var OWNER_FILE = ".ic-reactor-owner";
|
|
280
|
+
async function generateDeclarations(options) {
|
|
281
|
+
const { didFile, outDir, canisterName } = options;
|
|
282
|
+
const declarationsDir = import_node_path2.default.join(outDir, "declarations");
|
|
283
|
+
let baseName;
|
|
284
|
+
try {
|
|
285
|
+
baseName = resolveDeclarationsBaseName(didFile);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (error instanceof CodegenConfigError) {
|
|
288
|
+
return {
|
|
289
|
+
success: false,
|
|
290
|
+
declarationsDir: "",
|
|
291
|
+
files: [],
|
|
292
|
+
error: `[${canisterName}] ${error.message}`
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
let didStat;
|
|
298
|
+
try {
|
|
299
|
+
didStat = import_node_fs2.default.statSync(didFile);
|
|
300
|
+
} catch {
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
declarationsDir: "",
|
|
304
|
+
files: [],
|
|
305
|
+
error: `DID file not found: ${didFile}`
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (!didStat.isFile()) {
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
declarationsDir: "",
|
|
312
|
+
files: [],
|
|
313
|
+
error: `[${canisterName}] DID path is not a regular file: ${didFile}`
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
let staging;
|
|
317
|
+
try {
|
|
318
|
+
const didContent = import_node_fs2.default.readFileSync(didFile, "utf-8");
|
|
319
|
+
const jsContent = (0, import_parser.didToJs)(didContent);
|
|
320
|
+
const tsContent = (0, import_parser.didToTs)(didContent);
|
|
321
|
+
if (!HAS_IDL_FACTORY.test(jsContent)) {
|
|
322
|
+
return {
|
|
323
|
+
success: false,
|
|
324
|
+
declarationsDir,
|
|
325
|
+
files: [],
|
|
326
|
+
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.`
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
import_node_fs2.default.mkdirSync(outDir, { recursive: true });
|
|
330
|
+
import_node_fs2.default.writeFileSync(import_node_path2.default.join(outDir, OWNER_FILE), `${canisterName}
|
|
331
|
+
`);
|
|
332
|
+
staging = import_node_fs2.default.mkdtempSync(import_node_path2.default.join(outDir, ".declarations.tmp-"));
|
|
333
|
+
const jsPath = import_node_path2.default.join(declarationsDir, `${baseName}.js`);
|
|
334
|
+
const dtsPath = import_node_path2.default.join(declarationsDir, `${baseName}.d.ts`);
|
|
335
|
+
const didCopyPath = import_node_path2.default.join(declarationsDir, `${baseName}.did`);
|
|
336
|
+
import_node_fs2.default.writeFileSync(import_node_path2.default.join(staging, `${baseName}.js`), jsContent);
|
|
337
|
+
import_node_fs2.default.writeFileSync(import_node_path2.default.join(staging, `${baseName}.d.ts`), tsContent);
|
|
338
|
+
import_node_fs2.default.writeFileSync(import_node_path2.default.join(staging, `${baseName}.did`), didContent);
|
|
339
|
+
replaceDirectory(staging, declarationsDir);
|
|
340
|
+
staging = void 0;
|
|
341
|
+
return {
|
|
342
|
+
success: true,
|
|
343
|
+
declarationsDir,
|
|
344
|
+
files: [
|
|
345
|
+
{ success: true, filePath: jsPath },
|
|
346
|
+
{ success: true, filePath: dtsPath },
|
|
347
|
+
{ success: true, filePath: didCopyPath }
|
|
348
|
+
]
|
|
349
|
+
};
|
|
350
|
+
} catch (error) {
|
|
351
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
declarationsDir,
|
|
355
|
+
files: [],
|
|
356
|
+
error: `[${canisterName}] Failed to generate declarations: ${message}`
|
|
357
|
+
};
|
|
358
|
+
} finally {
|
|
359
|
+
if (staging) import_node_fs2.default.rmSync(staging, { recursive: true, force: true });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function declarationsExist(outDir, canisterName, didFile) {
|
|
363
|
+
const declarationsDir = import_node_path2.default.join(outDir, "declarations");
|
|
364
|
+
if (didFile !== void 0) {
|
|
365
|
+
const baseName = import_node_path2.default.basename(didFile, ".did");
|
|
366
|
+
return import_node_fs2.default.existsSync(import_node_path2.default.join(declarationsDir, `${baseName}.d.ts`));
|
|
367
|
+
}
|
|
368
|
+
if (import_node_fs2.default.existsSync(import_node_path2.default.join(declarationsDir, `${canisterName}.d.ts`))) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
return import_node_fs2.default.readdirSync(declarationsDir).some((entry) => entry.endsWith(".d.ts"));
|
|
373
|
+
} catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/generators/reactor.ts
|
|
379
|
+
function getReactorClassImportSource(reactorClass, runtimeTarget) {
|
|
380
|
+
switch (reactorClass) {
|
|
381
|
+
case "Reactor":
|
|
382
|
+
case "DisplayReactor":
|
|
383
|
+
return runtimeTarget === "core" ? "@ic-reactor/core" : "@ic-reactor/react";
|
|
384
|
+
case "CandidReactor":
|
|
385
|
+
case "CandidDisplayReactor":
|
|
386
|
+
case "MetadataDisplayReactor":
|
|
387
|
+
return "@ic-reactor/candid";
|
|
388
|
+
default:
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function generateReactorFile(options) {
|
|
395
|
+
const {
|
|
396
|
+
canisterName,
|
|
397
|
+
didFile,
|
|
398
|
+
clientManagerPath = "../../clients",
|
|
399
|
+
canisterId,
|
|
400
|
+
runtimeTarget = "react",
|
|
401
|
+
reactorClass = "DisplayReactor"
|
|
402
|
+
} = options;
|
|
403
|
+
const pascalName = toPascalCase(canisterName);
|
|
404
|
+
const reactorName = getReactorName(canisterName);
|
|
405
|
+
const serviceName = getServiceTypeName(canisterName);
|
|
406
|
+
const baseName = resolveDeclarationsBaseName(didFile);
|
|
407
|
+
const declarationsPath = `./declarations/${baseName}`;
|
|
408
|
+
const reactorImportSource = getReactorClassImportSource(
|
|
409
|
+
reactorClass,
|
|
410
|
+
runtimeTarget
|
|
411
|
+
);
|
|
412
|
+
const canisterIdLine = canisterId ? ` canisterId: ${JSON.stringify(canisterId)},
|
|
413
|
+
` : "";
|
|
414
|
+
const hookExports = runtimeTarget === "react" ? `
|
|
415
|
+
|
|
416
|
+
export const {
|
|
417
|
+
useActorQuery: use${pascalName}Query,
|
|
418
|
+
useActorSuspenseQuery: use${pascalName}SuspenseQuery,
|
|
419
|
+
useActorInfiniteQuery: use${pascalName}InfiniteQuery,
|
|
420
|
+
useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
|
|
421
|
+
useActorMutation: use${pascalName}Mutation,
|
|
422
|
+
useActorMethod: use${pascalName}Method,
|
|
423
|
+
} = createActorHooks(${reactorName})
|
|
424
|
+
` : "";
|
|
425
|
+
return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
|
|
426
|
+
import { clientManager } from ${JSON.stringify(clientManagerPath)}
|
|
427
|
+
import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
|
|
428
|
+
|
|
429
|
+
export type ${serviceName} = _SERVICE
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* ${pascalName} Reactor
|
|
433
|
+
*
|
|
434
|
+
* Auto-generated by @ic-reactor/codegen \u2014 do not edit.
|
|
435
|
+
* This file is overwritten whenever generation runs.
|
|
436
|
+
*
|
|
437
|
+
* Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
|
|
438
|
+
* factory modules). Avoid editing this managed file directly.
|
|
439
|
+
*/
|
|
440
|
+
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
441
|
+
clientManager,
|
|
442
|
+
idlFactory,
|
|
443
|
+
${canisterIdLine} name: ${JSON.stringify(canisterName)},
|
|
444
|
+
})${hookExports || "\n"}`;
|
|
445
|
+
}
|
|
446
|
+
function generateReactorEntryFile() {
|
|
447
|
+
return `/**
|
|
448
|
+
* Canister entrypoint.
|
|
449
|
+
*
|
|
450
|
+
* Created once by @ic-reactor/codegen and safe to customize.
|
|
451
|
+
* Keep the re-export below if you want generated exports and types to stay in sync.
|
|
452
|
+
*
|
|
453
|
+
* Recommended customization points:
|
|
454
|
+
* - define reusable query/mutation factories
|
|
455
|
+
* - add app-specific hooks and cache invalidation wiring
|
|
456
|
+
* - compose generated APIs into route loaders/actions
|
|
457
|
+
*
|
|
458
|
+
* Do not edit \`index.generated.ts\`; it is regenerated on each codegen run.
|
|
459
|
+
* AI guide: https://ic-reactor.b3pay.net/llms-full.txt
|
|
460
|
+
* Skill install: npx skills add B3Pay/ic-reactor-skills --full-depth --skill ic-reactor-hooks
|
|
461
|
+
*/
|
|
462
|
+
export * from "./index.generated"
|
|
463
|
+
`;
|
|
464
|
+
}
|
|
465
|
+
|
|
383
466
|
// src/pipeline.ts
|
|
384
467
|
function resolveReactorClass(canisterConfig) {
|
|
385
468
|
return canisterConfig.mode ?? "DisplayReactor";
|
|
@@ -390,8 +473,48 @@ function resolveRuntimeTarget(canisterConfig, globalConfig) {
|
|
|
390
473
|
function normalizeFileContent(content) {
|
|
391
474
|
return content.replace(/\r\n/g, "\n").trim();
|
|
392
475
|
}
|
|
393
|
-
|
|
394
|
-
|
|
476
|
+
var GENERATED_MARKER = "Auto-generated by @ic-reactor/codegen";
|
|
477
|
+
var GENERATED_CANISTER_NAME = /^ {2}name: "([A-Za-z0-9_.-]+)",$/m;
|
|
478
|
+
var EXPORT_STATEMENT = /^export\b.*$/gm;
|
|
479
|
+
function isLegacyGeneratedIndexFile(content, canisterName) {
|
|
480
|
+
if (!content.includes(GENERATED_MARKER)) return false;
|
|
481
|
+
const reactorName = getReactorName(canisterName);
|
|
482
|
+
const serviceName = getServiceTypeName(canisterName);
|
|
483
|
+
if (!content.includes(`export const ${reactorName} = new `)) return false;
|
|
484
|
+
if (!content.includes(`= createActorHooks(${reactorName})`)) return false;
|
|
485
|
+
const allowed = [
|
|
486
|
+
`export type ${serviceName} = _SERVICE`,
|
|
487
|
+
`export const ${reactorName} = new `,
|
|
488
|
+
// Opens the destructured hook block; its members are not export statements.
|
|
489
|
+
"export const {"
|
|
490
|
+
];
|
|
491
|
+
const statements = normalizeFileContent(content).match(EXPORT_STATEMENT) ?? [];
|
|
492
|
+
return statements.every(
|
|
493
|
+
(statement) => allowed.some((prefix) => statement.startsWith(prefix))
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
function backUpFile(filePath) {
|
|
497
|
+
let backupPath = `${filePath}.bak`;
|
|
498
|
+
for (let n = 2; import_node_fs3.default.existsSync(backupPath); n += 1) {
|
|
499
|
+
backupPath = `${filePath}.bak.${n}`;
|
|
500
|
+
}
|
|
501
|
+
import_node_fs3.default.copyFileSync(filePath, backupPath);
|
|
502
|
+
return backupPath;
|
|
503
|
+
}
|
|
504
|
+
function findOutDirOwner(outDir) {
|
|
505
|
+
try {
|
|
506
|
+
const owner = import_node_fs3.default.readFileSync(import_node_path3.default.join(outDir, OWNER_FILE), "utf-8").trim();
|
|
507
|
+
if (owner) return owner;
|
|
508
|
+
} catch {
|
|
509
|
+
}
|
|
510
|
+
let content;
|
|
511
|
+
try {
|
|
512
|
+
content = import_node_fs3.default.readFileSync(import_node_path3.default.join(outDir, "index.generated.ts"), "utf-8");
|
|
513
|
+
} catch {
|
|
514
|
+
return void 0;
|
|
515
|
+
}
|
|
516
|
+
if (!content.includes(GENERATED_MARKER)) return void 0;
|
|
517
|
+
return GENERATED_CANISTER_NAME.exec(content)?.[1];
|
|
395
518
|
}
|
|
396
519
|
function isManagedEntryWrapper(content, expectedEntryContent) {
|
|
397
520
|
return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
|
|
@@ -427,7 +550,7 @@ async function runCanisterPipeline(options) {
|
|
|
427
550
|
}
|
|
428
551
|
throw err;
|
|
429
552
|
}
|
|
430
|
-
const resolvedDidFile =
|
|
553
|
+
const resolvedDidFile = import_node_path3.default.isAbsolute(didFile) ? didFile : import_node_path3.default.resolve(projectRoot, didFile);
|
|
431
554
|
if (!import_node_fs3.default.existsSync(resolvedDidFile)) {
|
|
432
555
|
return {
|
|
433
556
|
canisterName: name,
|
|
@@ -436,7 +559,29 @@ async function runCanisterPipeline(options) {
|
|
|
436
559
|
error: `DID file not found: ${resolvedDidFile}`
|
|
437
560
|
};
|
|
438
561
|
}
|
|
562
|
+
try {
|
|
563
|
+
resolveDeclarationsBaseName(resolvedDidFile);
|
|
564
|
+
} catch (err) {
|
|
565
|
+
if (err instanceof CodegenConfigError) {
|
|
566
|
+
return {
|
|
567
|
+
canisterName: name,
|
|
568
|
+
success: false,
|
|
569
|
+
files,
|
|
570
|
+
error: `[${name}] ${err.message}`
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
throw err;
|
|
574
|
+
}
|
|
439
575
|
const canisterOutDir = validated.outDir;
|
|
576
|
+
const owner = findOutDirOwner(canisterOutDir);
|
|
577
|
+
if (owner !== void 0 && owner !== name) {
|
|
578
|
+
return {
|
|
579
|
+
canisterName: name,
|
|
580
|
+
success: false,
|
|
581
|
+
files,
|
|
582
|
+
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.`
|
|
583
|
+
};
|
|
584
|
+
}
|
|
440
585
|
const resolvedClientManagerPath = validated.clientManagerPath;
|
|
441
586
|
try {
|
|
442
587
|
const declResult = await generateDeclarations({
|
|
@@ -468,8 +613,8 @@ async function runCanisterPipeline(options) {
|
|
|
468
613
|
files
|
|
469
614
|
};
|
|
470
615
|
}
|
|
471
|
-
const reactorPath =
|
|
472
|
-
const entryPath =
|
|
616
|
+
const reactorPath = import_node_path3.default.join(canisterOutDir, "index.generated.ts");
|
|
617
|
+
const entryPath = import_node_path3.default.join(canisterOutDir, "index.ts");
|
|
473
618
|
const reactorClass = resolveReactorClass(canisterConfig);
|
|
474
619
|
const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
|
|
475
620
|
try {
|
|
@@ -490,9 +635,14 @@ async function runCanisterPipeline(options) {
|
|
|
490
635
|
files.push({ success: true, filePath: entryPath });
|
|
491
636
|
} else {
|
|
492
637
|
const existingEntryContent = import_node_fs3.default.readFileSync(entryPath, "utf-8");
|
|
493
|
-
if (
|
|
638
|
+
if (isManagedEntryWrapper(existingEntryContent, entryContent)) {
|
|
494
639
|
import_node_fs3.default.writeFileSync(entryPath, entryContent);
|
|
495
640
|
files.push({ success: true, filePath: entryPath });
|
|
641
|
+
} else if (isLegacyGeneratedIndexFile(existingEntryContent, name)) {
|
|
642
|
+
const backupPath = backUpFile(entryPath);
|
|
643
|
+
import_node_fs3.default.writeFileSync(entryPath, entryContent);
|
|
644
|
+
files.push({ success: true, filePath: backupPath });
|
|
645
|
+
files.push({ success: true, filePath: entryPath });
|
|
496
646
|
} else {
|
|
497
647
|
files.push({ success: true, filePath: entryPath, skipped: true });
|
|
498
648
|
}
|
|
@@ -517,40 +667,6 @@ async function runCanisterPipeline(options) {
|
|
|
517
667
|
};
|
|
518
668
|
}
|
|
519
669
|
|
|
520
|
-
// src/parser.ts
|
|
521
|
-
var import_parser2 = require("@ic-reactor/parser");
|
|
522
|
-
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
523
|
-
function extractMethods(didContent) {
|
|
524
|
-
try {
|
|
525
|
-
const jsContent = (0, import_parser2.didToJs)(didContent);
|
|
526
|
-
const methods = [];
|
|
527
|
-
const serviceMatch = /IDL\.Service\(\{([\s\S]*?)\}\)/.exec(jsContent);
|
|
528
|
-
if (!serviceMatch) return methods;
|
|
529
|
-
const serviceBody = serviceMatch[1];
|
|
530
|
-
const methodRegex = /['"]([\w]+)['"]\s*:\s*IDL\.Func\(\s*\[(.*?)\],\s*\[.*?\],\s*\[(.*?)\]\)/g;
|
|
531
|
-
let match;
|
|
532
|
-
while ((match = methodRegex.exec(serviceBody)) !== null) {
|
|
533
|
-
const [, name, args, annotations] = match;
|
|
534
|
-
methods.push({
|
|
535
|
-
name,
|
|
536
|
-
type: annotations.includes("'query'") ? "query" : "mutation",
|
|
537
|
-
hasArgs: args.trim().length > 0
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
return methods;
|
|
541
|
-
} catch (error) {
|
|
542
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
543
|
-
throw new Error(`Failed to parse Candid: ${msg}`);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
function parseDIDFile(didFilePath) {
|
|
547
|
-
if (!import_node_fs4.default.existsSync(didFilePath)) {
|
|
548
|
-
throw new Error(`DID file not found: ${didFilePath}`);
|
|
549
|
-
}
|
|
550
|
-
const content = import_node_fs4.default.readFileSync(didFilePath, "utf-8");
|
|
551
|
-
return extractMethods(content);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
670
|
// src/generators/client.ts
|
|
555
671
|
function generateClientFile(options = {}) {
|
|
556
672
|
const { queryClientPath } = options;
|
|
@@ -583,15 +699,14 @@ export const clientManager = new ClientManager({
|
|
|
583
699
|
assertSafeCanisterName,
|
|
584
700
|
assertSafeModuleSpecifier,
|
|
585
701
|
declarationsExist,
|
|
586
|
-
extractMethods,
|
|
587
702
|
generateClientFile,
|
|
588
703
|
generateDeclarations,
|
|
589
704
|
generateReactorEntryFile,
|
|
590
705
|
generateReactorFile,
|
|
591
706
|
getReactorName,
|
|
592
707
|
getServiceTypeName,
|
|
593
|
-
parseDIDFile,
|
|
594
708
|
resolveContainedOutDir,
|
|
709
|
+
resolveDeclarationsBaseName,
|
|
595
710
|
runCanisterPipeline,
|
|
596
711
|
toPascalCase
|
|
597
712
|
});
|