@ic-reactor/codegen 0.12.0 → 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 -4
- package/dist/index.cjs +415 -699
- package/dist/index.d.cts +119 -28
- package/dist/index.d.ts +119 -28
- package/dist/index.js +404 -98
- package/package.json +9 -15
- package/src/generators/declarations.ts +164 -21
- package/src/generators/reactor.ts +20 -7
- package/src/index.ts +19 -5
- package/src/pipeline.ts +190 -18
- package/src/validate.ts +407 -0
- package/dist/chunk-VBCR5IVT.js +0 -609
- package/dist/renderer.cjs +0 -639
- package/dist/renderer.d.cts +0 -33
- package/dist/renderer.d.ts +0 -33
- package/dist/renderer.js +0 -14
- 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/metadata-rules.json +0 -144
- package/src/metadata.ts +0 -157
- 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/renderer.test.ts +0 -573
- package/src/renderer.ts +0 -515
package/dist/index.js
CHANGED
|
@@ -1,18 +1,248 @@
|
|
|
1
|
-
import {
|
|
2
|
-
generateCodecDeclarations
|
|
3
|
-
} from "./chunk-VBCR5IVT.js";
|
|
4
|
-
|
|
5
1
|
// src/pipeline.ts
|
|
6
|
-
import
|
|
2
|
+
import fs3 from "fs";
|
|
7
3
|
import path3 from "path";
|
|
8
4
|
|
|
9
5
|
// src/generators/declarations.ts
|
|
10
6
|
import { didToJs, didToTs } from "@ic-reactor/parser";
|
|
11
|
-
import
|
|
7
|
+
import path2 from "path";
|
|
8
|
+
import fs2 from "fs";
|
|
9
|
+
|
|
10
|
+
// src/validate.ts
|
|
12
11
|
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
|
|
14
|
+
// src/naming.ts
|
|
15
|
+
import { camelCase, pascalCase } from "change-case";
|
|
16
|
+
function toPascalCase(str) {
|
|
17
|
+
return pascalCase(str);
|
|
18
|
+
}
|
|
19
|
+
function toCamelCase(str) {
|
|
20
|
+
return camelCase(str);
|
|
21
|
+
}
|
|
22
|
+
function getReactorName(canisterName) {
|
|
23
|
+
return `${toCamelCase(canisterName)}Reactor`;
|
|
24
|
+
}
|
|
25
|
+
function getServiceTypeName(canisterName) {
|
|
26
|
+
return `${toPascalCase(canisterName)}Service`;
|
|
27
|
+
}
|
|
28
|
+
function getHookPrefix(canisterName) {
|
|
29
|
+
return toPascalCase(canisterName);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/validate.ts
|
|
33
|
+
var CodegenConfigError = class extends Error {
|
|
34
|
+
name = "CodegenConfigError";
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var CANISTER_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
40
|
+
var IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
41
|
+
var MAX_CANISTER_NAME_LENGTH = 64;
|
|
42
|
+
var HAS_URI_SCHEME = /^(?:[A-Za-z][A-Za-z0-9+.-]*:|\/\/)/;
|
|
43
|
+
var BREAKS_OUT_OF_LITERAL = /["'`\\\u0000-\u001f\u2028\u2029]/;
|
|
44
|
+
function assertSafeCanisterName(name) {
|
|
45
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
46
|
+
throw new CodegenConfigError(
|
|
47
|
+
`Invalid canister name: expected a non-empty string, received ${name === void 0 ? "undefined" : JSON.stringify(name)}. Check the "canisters" entries in your ic-reactor.json (or the plugin's "canisters" option).`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (name.length > MAX_CANISTER_NAME_LENGTH) {
|
|
51
|
+
throw new CodegenConfigError(
|
|
52
|
+
`Invalid canister name ${JSON.stringify(name)}: must be at most ${MAX_CANISTER_NAME_LENGTH} characters.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (!CANISTER_NAME_PATTERN.test(name)) {
|
|
56
|
+
throw new CodegenConfigError(
|
|
57
|
+
`Invalid canister name ${JSON.stringify(name)}: may contain only letters, digits, "_", "." and "-" (${CANISTER_NAME_PATTERN.source}). Canister names become directory names, so path separators, quotes and whitespace are not allowed.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (name === "." || name === "..") {
|
|
61
|
+
throw new CodegenConfigError(
|
|
62
|
+
`Invalid canister name ${JSON.stringify(name)}: refers to a directory, not a canister.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (toPascalCase(name).length === 0) {
|
|
66
|
+
throw new CodegenConfigError(
|
|
67
|
+
`Invalid canister name ${JSON.stringify(name)}: contains no letters or digits, so it collapses to an empty identifier in generated code.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const derived = [
|
|
71
|
+
["reactor constant", getReactorName(name)],
|
|
72
|
+
["service type", getServiceTypeName(name)],
|
|
73
|
+
["hook name", `use${getHookPrefix(name)}Query`]
|
|
74
|
+
];
|
|
75
|
+
for (const [what, identifier] of derived) {
|
|
76
|
+
if (!IDENTIFIER_PATTERN.test(identifier)) {
|
|
77
|
+
throw new CodegenConfigError(
|
|
78
|
+
`Invalid canister name ${JSON.stringify(name)}: it derives the ${what} ${JSON.stringify(identifier)}, which is not a valid TypeScript identifier. Rename the canister so it does not begin with a digit.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function assertSafeModuleSpecifier(label, specifier) {
|
|
84
|
+
if (typeof specifier !== "string" || specifier.length === 0) {
|
|
85
|
+
throw new CodegenConfigError(
|
|
86
|
+
`Invalid ${label}: expected a non-empty string, received ${specifier === void 0 ? "undefined" : JSON.stringify(specifier)}.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (BREAKS_OUT_OF_LITERAL.test(specifier)) {
|
|
90
|
+
throw new CodegenConfigError(
|
|
91
|
+
`Invalid ${label} ${JSON.stringify(specifier)}: must not contain quotes, backslashes or control characters.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (/^\s|\s$/.test(specifier)) {
|
|
95
|
+
throw new CodegenConfigError(
|
|
96
|
+
`Invalid ${label} ${JSON.stringify(specifier)}: must not begin or end with whitespace.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (HAS_URI_SCHEME.test(specifier.trim())) {
|
|
100
|
+
throw new CodegenConfigError(
|
|
101
|
+
`Invalid ${label} ${JSON.stringify(specifier)}: must be a relative path ("./\u2026", "../\u2026") or a bare package name. URLs are not allowed \u2014 the generated file imports from this specifier.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
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
|
+
}
|
|
123
|
+
var REACTOR_CLASS_NAMES = [
|
|
124
|
+
"Reactor",
|
|
125
|
+
"DisplayReactor",
|
|
126
|
+
"CandidReactor",
|
|
127
|
+
"CandidDisplayReactor",
|
|
128
|
+
"MetadataDisplayReactor"
|
|
129
|
+
];
|
|
130
|
+
var CODEGEN_TARGETS = ["react", "core"];
|
|
131
|
+
function assertOneOf(label, value, allowed) {
|
|
132
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
133
|
+
throw new CodegenConfigError(
|
|
134
|
+
`Invalid ${label} ${JSON.stringify(value)}: must be one of ${allowed.map((a) => JSON.stringify(a)).join(", ")}.`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function realpathAllowingMissing(target) {
|
|
139
|
+
let current = path.resolve(target);
|
|
140
|
+
const missing = [];
|
|
141
|
+
for (; ; ) {
|
|
142
|
+
try {
|
|
143
|
+
return path.join(fs.realpathSync(current), ...missing);
|
|
144
|
+
} catch {
|
|
145
|
+
const parent = path.dirname(current);
|
|
146
|
+
if (parent === current) return path.resolve(target);
|
|
147
|
+
missing.unshift(path.basename(current));
|
|
148
|
+
current = parent;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function assertContainedPath(label, resolved, projectRoot, original = resolved) {
|
|
153
|
+
const relative = path.relative(
|
|
154
|
+
realpathAllowingMissing(projectRoot),
|
|
155
|
+
realpathAllowingMissing(resolved)
|
|
156
|
+
);
|
|
157
|
+
const [firstSegment] = relative.split(/[\\/]/);
|
|
158
|
+
if (firstSegment === ".." || path.isAbsolute(relative)) {
|
|
159
|
+
throw new CodegenConfigError(
|
|
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.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function resolveContainedOutDir(label, outDir, projectRoot) {
|
|
165
|
+
if (typeof outDir !== "string" || outDir.length === 0) {
|
|
166
|
+
throw new CodegenConfigError(
|
|
167
|
+
`Invalid ${label}: expected a non-empty string, received ${outDir === void 0 ? "undefined" : JSON.stringify(outDir)}.`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const resolved = path.isAbsolute(outDir) ? path.resolve(outDir) : path.resolve(projectRoot, outDir);
|
|
171
|
+
assertContainedPath(label, resolved, projectRoot, outDir);
|
|
172
|
+
return resolved;
|
|
173
|
+
}
|
|
174
|
+
function assertSafeCanisterConfig(options) {
|
|
175
|
+
const {
|
|
176
|
+
name,
|
|
177
|
+
canisterOutDir,
|
|
178
|
+
globalOutDir,
|
|
179
|
+
clientManagerPath,
|
|
180
|
+
projectRoot,
|
|
181
|
+
mode,
|
|
182
|
+
target
|
|
183
|
+
} = options;
|
|
184
|
+
assertSafeCanisterName(name);
|
|
185
|
+
assertSafeModuleSpecifier("clientManagerPath", clientManagerPath);
|
|
186
|
+
if (mode != null) assertOneOf("mode", mode, REACTOR_CLASS_NAMES);
|
|
187
|
+
if (target != null) assertOneOf("target", target, CODEGEN_TARGETS);
|
|
188
|
+
const outDir = canisterOutDir != null ? resolveContainedOutDir(
|
|
189
|
+
`outDir for canister ${JSON.stringify(name)}`,
|
|
190
|
+
canisterOutDir,
|
|
191
|
+
projectRoot
|
|
192
|
+
) : path.join(
|
|
193
|
+
resolveContainedOutDir("outDir", globalOutDir, projectRoot),
|
|
194
|
+
name
|
|
195
|
+
);
|
|
196
|
+
assertContainedPath(
|
|
197
|
+
`output directory for canister ${JSON.stringify(name)}`,
|
|
198
|
+
outDir,
|
|
199
|
+
projectRoot
|
|
200
|
+
);
|
|
201
|
+
return { name, outDir, clientManagerPath };
|
|
202
|
+
}
|
|
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";
|
|
13
225
|
async function generateDeclarations(options) {
|
|
14
226
|
const { didFile, outDir, canisterName } = options;
|
|
15
|
-
|
|
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 {
|
|
16
246
|
return {
|
|
17
247
|
success: false,
|
|
18
248
|
declarationsDir: "",
|
|
@@ -20,25 +250,39 @@ async function generateDeclarations(options) {
|
|
|
20
250
|
error: `DID file not found: ${didFile}`
|
|
21
251
|
};
|
|
22
252
|
}
|
|
23
|
-
|
|
24
|
-
|
|
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;
|
|
25
262
|
try {
|
|
26
|
-
const didContent =
|
|
27
|
-
if (!fs.existsSync(outDir)) {
|
|
28
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
29
|
-
}
|
|
30
|
-
if (fs.existsSync(declarationsDir)) {
|
|
31
|
-
fs.rmSync(declarationsDir, { recursive: true, force: true });
|
|
32
|
-
}
|
|
33
|
-
fs.mkdirSync(declarationsDir, { recursive: true });
|
|
263
|
+
const didContent = fs2.readFileSync(didFile, "utf-8");
|
|
34
264
|
const jsContent = didToJs(didContent);
|
|
35
265
|
const tsContent = didToTs(didContent);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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;
|
|
42
286
|
return {
|
|
43
287
|
success: true,
|
|
44
288
|
declarationsDir,
|
|
@@ -56,29 +300,24 @@ async function generateDeclarations(options) {
|
|
|
56
300
|
files: [],
|
|
57
301
|
error: `[${canisterName}] Failed to generate declarations: ${message}`
|
|
58
302
|
};
|
|
303
|
+
} finally {
|
|
304
|
+
if (staging) fs2.rmSync(staging, { recursive: true, force: true });
|
|
59
305
|
}
|
|
60
306
|
}
|
|
61
|
-
function declarationsExist(outDir, canisterName) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return camelCase(str);
|
|
76
|
-
}
|
|
77
|
-
function getReactorName(canisterName) {
|
|
78
|
-
return `${toCamelCase(canisterName)}Reactor`;
|
|
79
|
-
}
|
|
80
|
-
function getServiceTypeName(canisterName) {
|
|
81
|
-
return `${toPascalCase(canisterName)}Service`;
|
|
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
|
+
}
|
|
82
321
|
}
|
|
83
322
|
|
|
84
323
|
// src/generators/reactor.ts
|
|
@@ -91,6 +330,10 @@ function getReactorClassImportSource(reactorClass, runtimeTarget) {
|
|
|
91
330
|
case "CandidDisplayReactor":
|
|
92
331
|
case "MetadataDisplayReactor":
|
|
93
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
|
+
);
|
|
94
337
|
}
|
|
95
338
|
}
|
|
96
339
|
function generateReactorFile(options) {
|
|
@@ -105,7 +348,7 @@ function generateReactorFile(options) {
|
|
|
105
348
|
const pascalName = toPascalCase(canisterName);
|
|
106
349
|
const reactorName = getReactorName(canisterName);
|
|
107
350
|
const serviceName = getServiceTypeName(canisterName);
|
|
108
|
-
const baseName =
|
|
351
|
+
const baseName = resolveDeclarationsBaseName(didFile);
|
|
109
352
|
const declarationsPath = `./declarations/${baseName}`;
|
|
110
353
|
const reactorImportSource = getReactorClassImportSource(
|
|
111
354
|
reactorClass,
|
|
@@ -124,9 +367,9 @@ export const {
|
|
|
124
367
|
useActorMethod: use${pascalName}Method,
|
|
125
368
|
} = createActorHooks(${reactorName})
|
|
126
369
|
` : "";
|
|
127
|
-
return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from
|
|
128
|
-
import { clientManager } from
|
|
129
|
-
import { idlFactory, type _SERVICE } from
|
|
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)}
|
|
130
373
|
|
|
131
374
|
export type ${serviceName} = _SERVICE
|
|
132
375
|
|
|
@@ -142,7 +385,7 @@ export type ${serviceName} = _SERVICE
|
|
|
142
385
|
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
143
386
|
clientManager,
|
|
144
387
|
idlFactory,
|
|
145
|
-
${canisterIdLine} name:
|
|
388
|
+
${canisterIdLine} name: ${JSON.stringify(canisterName)},
|
|
146
389
|
})${hookExports || "\n"}`;
|
|
147
390
|
}
|
|
148
391
|
function generateReactorEntryFile() {
|
|
@@ -175,8 +418,48 @@ function resolveRuntimeTarget(canisterConfig, globalConfig) {
|
|
|
175
418
|
function normalizeFileContent(content) {
|
|
176
419
|
return content.replace(/\r\n/g, "\n").trim();
|
|
177
420
|
}
|
|
178
|
-
|
|
179
|
-
|
|
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];
|
|
180
463
|
}
|
|
181
464
|
function isManagedEntryWrapper(content, expectedEntryContent) {
|
|
182
465
|
return normalizeFileContent(content) === normalizeFileContent(expectedEntryContent);
|
|
@@ -190,8 +473,30 @@ async function runCanisterPipeline(options) {
|
|
|
190
473
|
} = options;
|
|
191
474
|
const { name, didFile, clientManagerPath } = canisterConfig;
|
|
192
475
|
const files = [];
|
|
476
|
+
let validated;
|
|
477
|
+
try {
|
|
478
|
+
validated = assertSafeCanisterConfig({
|
|
479
|
+
name,
|
|
480
|
+
canisterOutDir: canisterConfig.outDir,
|
|
481
|
+
globalOutDir: globalConfig.outDir,
|
|
482
|
+
clientManagerPath: clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients",
|
|
483
|
+
projectRoot,
|
|
484
|
+
mode: canisterConfig.mode,
|
|
485
|
+
target: canisterConfig.target ?? globalConfig.target
|
|
486
|
+
});
|
|
487
|
+
} catch (err) {
|
|
488
|
+
if (err instanceof CodegenConfigError) {
|
|
489
|
+
return {
|
|
490
|
+
canisterName: typeof name === "string" ? name : String(name),
|
|
491
|
+
success: false,
|
|
492
|
+
files,
|
|
493
|
+
error: err.message
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
throw err;
|
|
497
|
+
}
|
|
193
498
|
const resolvedDidFile = path3.isAbsolute(didFile) ? didFile : path3.resolve(projectRoot, didFile);
|
|
194
|
-
if (!
|
|
499
|
+
if (!fs3.existsSync(resolvedDidFile)) {
|
|
195
500
|
return {
|
|
196
501
|
canisterName: name,
|
|
197
502
|
success: false,
|
|
@@ -199,8 +504,30 @@ async function runCanisterPipeline(options) {
|
|
|
199
504
|
error: `DID file not found: ${resolvedDidFile}`
|
|
200
505
|
};
|
|
201
506
|
}
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
530
|
+
const resolvedClientManagerPath = validated.clientManagerPath;
|
|
204
531
|
try {
|
|
205
532
|
const declResult = await generateDeclarations({
|
|
206
533
|
didFile: resolvedDidFile,
|
|
@@ -245,16 +572,21 @@ async function runCanisterPipeline(options) {
|
|
|
245
572
|
reactorClass
|
|
246
573
|
});
|
|
247
574
|
const entryContent = generateReactorEntryFile();
|
|
248
|
-
|
|
249
|
-
|
|
575
|
+
fs3.mkdirSync(canisterOutDir, { recursive: true });
|
|
576
|
+
fs3.writeFileSync(reactorPath, reactorContent);
|
|
250
577
|
files.push({ success: true, filePath: reactorPath });
|
|
251
|
-
if (!
|
|
252
|
-
|
|
578
|
+
if (!fs3.existsSync(entryPath)) {
|
|
579
|
+
fs3.writeFileSync(entryPath, entryContent);
|
|
253
580
|
files.push({ success: true, filePath: entryPath });
|
|
254
581
|
} else {
|
|
255
|
-
const existingEntryContent =
|
|
256
|
-
if (
|
|
257
|
-
|
|
582
|
+
const existingEntryContent = fs3.readFileSync(entryPath, "utf-8");
|
|
583
|
+
if (isManagedEntryWrapper(existingEntryContent, entryContent)) {
|
|
584
|
+
fs3.writeFileSync(entryPath, entryContent);
|
|
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 });
|
|
258
590
|
files.push({ success: true, filePath: entryPath });
|
|
259
591
|
} else {
|
|
260
592
|
files.push({ success: true, filePath: entryPath, skipped: true });
|
|
@@ -280,40 +612,6 @@ async function runCanisterPipeline(options) {
|
|
|
280
612
|
};
|
|
281
613
|
}
|
|
282
614
|
|
|
283
|
-
// src/parser.ts
|
|
284
|
-
import { didToJs as didToJs2 } from "@ic-reactor/parser";
|
|
285
|
-
import fs3 from "fs";
|
|
286
|
-
function extractMethods(didContent) {
|
|
287
|
-
try {
|
|
288
|
-
const jsContent = didToJs2(didContent);
|
|
289
|
-
const methods = [];
|
|
290
|
-
const serviceMatch = /IDL\.Service\(\{([\s\S]*?)\}\)/.exec(jsContent);
|
|
291
|
-
if (!serviceMatch) return methods;
|
|
292
|
-
const serviceBody = serviceMatch[1];
|
|
293
|
-
const methodRegex = /['"]([\w]+)['"]\s*:\s*IDL\.Func\(\s*\[(.*?)\],\s*\[.*?\],\s*\[(.*?)\]\)/g;
|
|
294
|
-
let match;
|
|
295
|
-
while ((match = methodRegex.exec(serviceBody)) !== null) {
|
|
296
|
-
const [, name, args, annotations] = match;
|
|
297
|
-
methods.push({
|
|
298
|
-
name,
|
|
299
|
-
type: annotations.includes("'query'") ? "query" : "mutation",
|
|
300
|
-
hasArgs: args.trim().length > 0
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
return methods;
|
|
304
|
-
} catch (error) {
|
|
305
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
306
|
-
throw new Error(`Failed to parse Candid: ${msg}`);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
function parseDIDFile(didFilePath) {
|
|
310
|
-
if (!fs3.existsSync(didFilePath)) {
|
|
311
|
-
throw new Error(`DID file not found: ${didFilePath}`);
|
|
312
|
-
}
|
|
313
|
-
const content = fs3.readFileSync(didFilePath, "utf-8");
|
|
314
|
-
return extractMethods(content);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
615
|
// src/generators/client.ts
|
|
318
616
|
function generateClientFile(options = {}) {
|
|
319
617
|
const { queryClientPath } = options;
|
|
@@ -335,16 +633,24 @@ export const clientManager = new ClientManager({
|
|
|
335
633
|
`;
|
|
336
634
|
}
|
|
337
635
|
export {
|
|
636
|
+
CANISTER_NAME_PATTERN,
|
|
637
|
+
CODEGEN_TARGETS,
|
|
638
|
+
CodegenConfigError,
|
|
639
|
+
REACTOR_CLASS_NAMES,
|
|
640
|
+
assertContainedPath,
|
|
641
|
+
assertOneOf,
|
|
642
|
+
assertSafeCanisterConfig,
|
|
643
|
+
assertSafeCanisterName,
|
|
644
|
+
assertSafeModuleSpecifier,
|
|
338
645
|
declarationsExist,
|
|
339
|
-
extractMethods,
|
|
340
646
|
generateClientFile,
|
|
341
|
-
generateCodecDeclarations,
|
|
342
647
|
generateDeclarations,
|
|
343
648
|
generateReactorEntryFile,
|
|
344
649
|
generateReactorFile,
|
|
345
650
|
getReactorName,
|
|
346
651
|
getServiceTypeName,
|
|
347
|
-
|
|
652
|
+
resolveContainedOutDir,
|
|
653
|
+
resolveDeclarationsBaseName,
|
|
348
654
|
runCanisterPipeline,
|
|
349
655
|
toPascalCase
|
|
350
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",
|
|
@@ -17,21 +17,15 @@
|
|
|
17
17
|
"default": "./dist/index.cjs"
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
|
-
"./renderer": {
|
|
21
|
-
"import": {
|
|
22
|
-
"types": "./dist/renderer.d.ts",
|
|
23
|
-
"default": "./dist/renderer.js"
|
|
24
|
-
},
|
|
25
|
-
"require": {
|
|
26
|
-
"types": "./dist/renderer.d.cts",
|
|
27
|
-
"default": "./dist/renderer.cjs"
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
20
|
"./package.json": "./package.json"
|
|
31
21
|
},
|
|
32
22
|
"files": [
|
|
33
23
|
"dist",
|
|
34
24
|
"src",
|
|
25
|
+
"!src/**/*.test.*",
|
|
26
|
+
"!src/**/*.spec.*",
|
|
27
|
+
"!src/**/__snapshots__/**",
|
|
28
|
+
"!**/*.tsbuildinfo",
|
|
35
29
|
"README.md",
|
|
36
30
|
"llms.txt"
|
|
37
31
|
],
|
|
@@ -53,17 +47,17 @@
|
|
|
53
47
|
"homepage": "https://ic-reactor.b3pay.net/v3/packages/codegen",
|
|
54
48
|
"dependencies": {
|
|
55
49
|
"change-case": "^5.4.4",
|
|
56
|
-
"@ic-reactor/parser": "0.4.
|
|
50
|
+
"@ic-reactor/parser": "0.4.8"
|
|
57
51
|
},
|
|
58
52
|
"devDependencies": {
|
|
59
|
-
"@types/node": "^26.
|
|
53
|
+
"@types/node": "^26.2.0",
|
|
60
54
|
"tsup": "^8.5.1",
|
|
61
55
|
"typescript": "^6.0.3",
|
|
62
56
|
"vitest": "^4.1.10"
|
|
63
57
|
},
|
|
64
58
|
"scripts": {
|
|
65
|
-
"build": "tsup src/index.ts
|
|
66
|
-
"dev": "tsup src/index.ts
|
|
59
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean --tsconfig tsconfig.json",
|
|
60
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch --tsconfig tsconfig.json",
|
|
67
61
|
"test": "vitest run",
|
|
68
62
|
"test:watch": "vitest",
|
|
69
63
|
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
|