@ic-reactor/codegen 0.11.1 → 0.12.1
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 +4 -4
- package/dist/index.cjs +238 -22
- package/dist/index.d.cts +111 -1
- package/dist/index.d.ts +111 -1
- package/dist/index.js +228 -22
- package/llms.txt +28 -0
- package/package.json +21 -12
- package/src/__snapshots__/reactor.test.ts.snap +12 -0
- package/src/bindgen.test.ts +1 -1
- package/src/generators/client.ts +0 -1
- package/src/generators/reactor.ts +27 -4
- package/src/index.ts +19 -0
- package/src/naming.test.ts +1 -1
- package/src/pipeline.test.ts +1 -1
- package/src/pipeline.ts +36 -9
- package/src/reactor.test.ts +4 -1
- package/src/validate.test.ts +393 -0
- package/src/validate.ts +369 -0
package/README.md
CHANGED
|
@@ -54,16 +54,16 @@ need to skip `index.generated.ts` and `index.ts`.
|
|
|
54
54
|
|
|
55
55
|
You can also use individual generators if you need more granular control:
|
|
56
56
|
|
|
57
|
-
- **`generateDeclarations`**:
|
|
58
|
-
- **`generateReactorFile`**: Generates the managed `index.generated.ts` implementation using
|
|
57
|
+
- **`generateDeclarations`**: Writes `declarations/<did-basename>.js` (factory), `.d.ts` (types), and a `.did` copy. The `declarations/` directory is wiped and recreated on every run.
|
|
58
|
+
- **`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
59
|
- **`generateReactorEntryFile`**: Generates the stable `index.ts` wrapper that re-exports from `index.generated.ts`.
|
|
60
60
|
- **`generateClientFile`**: Generates a `ClientManager` boilerplate file that
|
|
61
61
|
imports `ClientManager` from `@ic-reactor/react`.
|
|
62
62
|
|
|
63
63
|
## Utilities
|
|
64
64
|
|
|
65
|
-
- **`parseDIDFile`**:
|
|
66
|
-
- **`toPascalCase` / `
|
|
65
|
+
- **`parseDIDFile` / `extractMethods`**: Parse a `.did` file and extract method signatures.
|
|
66
|
+
- **`toPascalCase` / `getReactorName` / `getServiceTypeName`**: Naming helpers.
|
|
67
67
|
|
|
68
68
|
## License
|
|
69
69
|
|
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
CANISTER_NAME_PATTERN: () => CANISTER_NAME_PATTERN,
|
|
34
|
+
CODEGEN_TARGETS: () => CODEGEN_TARGETS,
|
|
35
|
+
CodegenConfigError: () => CodegenConfigError,
|
|
36
|
+
REACTOR_CLASS_NAMES: () => REACTOR_CLASS_NAMES,
|
|
37
|
+
assertContainedPath: () => assertContainedPath,
|
|
38
|
+
assertOneOf: () => assertOneOf,
|
|
39
|
+
assertSafeCanisterConfig: () => assertSafeCanisterConfig,
|
|
40
|
+
assertSafeCanisterName: () => assertSafeCanisterName,
|
|
41
|
+
assertSafeModuleSpecifier: () => assertSafeModuleSpecifier,
|
|
33
42
|
declarationsExist: () => declarationsExist,
|
|
34
43
|
extractMethods: () => extractMethods,
|
|
35
44
|
generateClientFile: () => generateClientFile,
|
|
@@ -39,14 +48,15 @@ __export(index_exports, {
|
|
|
39
48
|
getReactorName: () => getReactorName,
|
|
40
49
|
getServiceTypeName: () => getServiceTypeName,
|
|
41
50
|
parseDIDFile: () => parseDIDFile,
|
|
51
|
+
resolveContainedOutDir: () => resolveContainedOutDir,
|
|
42
52
|
runCanisterPipeline: () => runCanisterPipeline,
|
|
43
53
|
toPascalCase: () => toPascalCase
|
|
44
54
|
});
|
|
45
55
|
module.exports = __toCommonJS(index_exports);
|
|
46
56
|
|
|
47
57
|
// src/pipeline.ts
|
|
48
|
-
var
|
|
49
|
-
var
|
|
58
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
59
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
50
60
|
|
|
51
61
|
// src/generators/declarations.ts
|
|
52
62
|
var import_parser = require("@ic-reactor/parser");
|
|
@@ -122,6 +132,9 @@ function getReactorName(canisterName) {
|
|
|
122
132
|
function getServiceTypeName(canisterName) {
|
|
123
133
|
return `${toPascalCase(canisterName)}Service`;
|
|
124
134
|
}
|
|
135
|
+
function getHookPrefix(canisterName) {
|
|
136
|
+
return toPascalCase(canisterName);
|
|
137
|
+
}
|
|
125
138
|
|
|
126
139
|
// src/generators/reactor.ts
|
|
127
140
|
function getReactorClassImportSource(reactorClass, runtimeTarget) {
|
|
@@ -133,6 +146,10 @@ function getReactorClassImportSource(reactorClass, runtimeTarget) {
|
|
|
133
146
|
case "CandidDisplayReactor":
|
|
134
147
|
case "MetadataDisplayReactor":
|
|
135
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
|
+
);
|
|
136
153
|
}
|
|
137
154
|
}
|
|
138
155
|
function generateReactorFile(options) {
|
|
@@ -166,9 +183,9 @@ export const {
|
|
|
166
183
|
useActorMethod: use${pascalName}Method,
|
|
167
184
|
} = createActorHooks(${reactorName})
|
|
168
185
|
` : "";
|
|
169
|
-
return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from
|
|
170
|
-
import { clientManager } from
|
|
171
|
-
import { idlFactory, type _SERVICE } from
|
|
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)}
|
|
172
189
|
|
|
173
190
|
export type ${serviceName} = _SERVICE
|
|
174
191
|
|
|
@@ -177,11 +194,14 @@ export type ${serviceName} = _SERVICE
|
|
|
177
194
|
*
|
|
178
195
|
* Auto-generated by @ic-reactor/codegen \u2014 do not edit.
|
|
179
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.
|
|
180
200
|
*/
|
|
181
201
|
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
182
202
|
clientManager,
|
|
183
203
|
idlFactory,
|
|
184
|
-
${canisterIdLine} name:
|
|
204
|
+
${canisterIdLine} name: ${JSON.stringify(canisterName)},
|
|
185
205
|
})${hookExports || "\n"}`;
|
|
186
206
|
}
|
|
187
207
|
function generateReactorEntryFile() {
|
|
@@ -190,11 +210,176 @@ function generateReactorEntryFile() {
|
|
|
190
210
|
*
|
|
191
211
|
* Created once by @ic-reactor/codegen and safe to customize.
|
|
192
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
|
|
193
222
|
*/
|
|
194
223
|
export * from "./index.generated"
|
|
195
224
|
`;
|
|
196
225
|
}
|
|
197
226
|
|
|
227
|
+
// src/validate.ts
|
|
228
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
229
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
230
|
+
var CodegenConfigError = class extends Error {
|
|
231
|
+
name = "CodegenConfigError";
|
|
232
|
+
constructor(message) {
|
|
233
|
+
super(message);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
var CANISTER_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
237
|
+
var IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
238
|
+
var MAX_CANISTER_NAME_LENGTH = 64;
|
|
239
|
+
var HAS_URI_SCHEME = /^(?:[A-Za-z][A-Za-z0-9+.-]*:|\/\/)/;
|
|
240
|
+
var BREAKS_OUT_OF_LITERAL = /["'`\\\u0000-\u001f\u2028\u2029]/;
|
|
241
|
+
function assertSafeCanisterName(name) {
|
|
242
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
243
|
+
throw new CodegenConfigError(
|
|
244
|
+
`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).`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (name.length > MAX_CANISTER_NAME_LENGTH) {
|
|
248
|
+
throw new CodegenConfigError(
|
|
249
|
+
`Invalid canister name ${JSON.stringify(name)}: must be at most ${MAX_CANISTER_NAME_LENGTH} characters.`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (!CANISTER_NAME_PATTERN.test(name)) {
|
|
253
|
+
throw new CodegenConfigError(
|
|
254
|
+
`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.`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (name === "." || name === "..") {
|
|
258
|
+
throw new CodegenConfigError(
|
|
259
|
+
`Invalid canister name ${JSON.stringify(name)}: refers to a directory, not a canister.`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (toPascalCase(name).length === 0) {
|
|
263
|
+
throw new CodegenConfigError(
|
|
264
|
+
`Invalid canister name ${JSON.stringify(name)}: contains no letters or digits, so it collapses to an empty identifier in generated code.`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const derived = [
|
|
268
|
+
["reactor constant", getReactorName(name)],
|
|
269
|
+
["service type", getServiceTypeName(name)],
|
|
270
|
+
["hook name", `use${getHookPrefix(name)}Query`]
|
|
271
|
+
];
|
|
272
|
+
for (const [what, identifier] of derived) {
|
|
273
|
+
if (!IDENTIFIER_PATTERN.test(identifier)) {
|
|
274
|
+
throw new CodegenConfigError(
|
|
275
|
+
`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.`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function assertSafeModuleSpecifier(label, specifier) {
|
|
281
|
+
if (typeof specifier !== "string" || specifier.length === 0) {
|
|
282
|
+
throw new CodegenConfigError(
|
|
283
|
+
`Invalid ${label}: expected a non-empty string, received ${specifier === void 0 ? "undefined" : JSON.stringify(specifier)}.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (BREAKS_OUT_OF_LITERAL.test(specifier)) {
|
|
287
|
+
throw new CodegenConfigError(
|
|
288
|
+
`Invalid ${label} ${JSON.stringify(specifier)}: must not contain quotes, backslashes or control characters.`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (/^\s|\s$/.test(specifier)) {
|
|
292
|
+
throw new CodegenConfigError(
|
|
293
|
+
`Invalid ${label} ${JSON.stringify(specifier)}: must not begin or end with whitespace.`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (HAS_URI_SCHEME.test(specifier.trim())) {
|
|
297
|
+
throw new CodegenConfigError(
|
|
298
|
+
`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.`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
var REACTOR_CLASS_NAMES = [
|
|
303
|
+
"Reactor",
|
|
304
|
+
"DisplayReactor",
|
|
305
|
+
"CandidReactor",
|
|
306
|
+
"CandidDisplayReactor",
|
|
307
|
+
"MetadataDisplayReactor"
|
|
308
|
+
];
|
|
309
|
+
var CODEGEN_TARGETS = ["react", "core"];
|
|
310
|
+
function assertOneOf(label, value, allowed) {
|
|
311
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
312
|
+
throw new CodegenConfigError(
|
|
313
|
+
`Invalid ${label} ${JSON.stringify(value)}: must be one of ${allowed.map((a) => JSON.stringify(a)).join(", ")}.`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function realpathAllowingMissing(target) {
|
|
318
|
+
let current = import_node_path3.default.resolve(target);
|
|
319
|
+
const missing = [];
|
|
320
|
+
for (; ; ) {
|
|
321
|
+
try {
|
|
322
|
+
return import_node_path3.default.join(import_node_fs2.default.realpathSync(current), ...missing);
|
|
323
|
+
} catch {
|
|
324
|
+
const parent = import_node_path3.default.dirname(current);
|
|
325
|
+
if (parent === current) return import_node_path3.default.resolve(target);
|
|
326
|
+
missing.unshift(import_node_path3.default.basename(current));
|
|
327
|
+
current = parent;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function assertContainedPath(label, resolved, projectRoot, original = resolved) {
|
|
332
|
+
const relative = import_node_path3.default.relative(
|
|
333
|
+
realpathAllowingMissing(projectRoot),
|
|
334
|
+
realpathAllowingMissing(resolved)
|
|
335
|
+
);
|
|
336
|
+
const [firstSegment] = relative.split(/[\\/]/);
|
|
337
|
+
if (firstSegment === ".." || import_node_path3.default.isAbsolute(relative)) {
|
|
338
|
+
throw new CodegenConfigError(
|
|
339
|
+
`Invalid ${label} ${JSON.stringify(original)}: resolves to ${JSON.stringify(resolved)}, which is outside the project root ${JSON.stringify(import_node_path3.default.resolve(projectRoot))}. Generated output must stay inside the project \u2014 generated directories are deleted and rewritten on every run.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function resolveContainedOutDir(label, outDir, projectRoot) {
|
|
344
|
+
if (typeof outDir !== "string" || outDir.length === 0) {
|
|
345
|
+
throw new CodegenConfigError(
|
|
346
|
+
`Invalid ${label}: expected a non-empty string, received ${outDir === void 0 ? "undefined" : JSON.stringify(outDir)}.`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const resolved = import_node_path3.default.isAbsolute(outDir) ? import_node_path3.default.resolve(outDir) : import_node_path3.default.resolve(projectRoot, outDir);
|
|
350
|
+
assertContainedPath(label, resolved, projectRoot, outDir);
|
|
351
|
+
return resolved;
|
|
352
|
+
}
|
|
353
|
+
function assertSafeCanisterConfig(options) {
|
|
354
|
+
const {
|
|
355
|
+
name,
|
|
356
|
+
canisterOutDir,
|
|
357
|
+
globalOutDir,
|
|
358
|
+
clientManagerPath,
|
|
359
|
+
projectRoot,
|
|
360
|
+
mode,
|
|
361
|
+
target
|
|
362
|
+
} = options;
|
|
363
|
+
assertSafeCanisterName(name);
|
|
364
|
+
assertSafeModuleSpecifier("clientManagerPath", clientManagerPath);
|
|
365
|
+
if (mode != null) assertOneOf("mode", mode, REACTOR_CLASS_NAMES);
|
|
366
|
+
if (target != null) assertOneOf("target", target, CODEGEN_TARGETS);
|
|
367
|
+
const outDir = canisterOutDir != null ? resolveContainedOutDir(
|
|
368
|
+
`outDir for canister ${JSON.stringify(name)}`,
|
|
369
|
+
canisterOutDir,
|
|
370
|
+
projectRoot
|
|
371
|
+
) : import_node_path3.default.join(
|
|
372
|
+
resolveContainedOutDir("outDir", globalOutDir, projectRoot),
|
|
373
|
+
name
|
|
374
|
+
);
|
|
375
|
+
assertContainedPath(
|
|
376
|
+
`output directory for canister ${JSON.stringify(name)}`,
|
|
377
|
+
outDir,
|
|
378
|
+
projectRoot
|
|
379
|
+
);
|
|
380
|
+
return { name, outDir, clientManagerPath };
|
|
381
|
+
}
|
|
382
|
+
|
|
198
383
|
// src/pipeline.ts
|
|
199
384
|
function resolveReactorClass(canisterConfig) {
|
|
200
385
|
return canisterConfig.mode ?? "DisplayReactor";
|
|
@@ -220,8 +405,30 @@ async function runCanisterPipeline(options) {
|
|
|
220
405
|
} = options;
|
|
221
406
|
const { name, didFile, clientManagerPath } = canisterConfig;
|
|
222
407
|
const files = [];
|
|
223
|
-
|
|
224
|
-
|
|
408
|
+
let validated;
|
|
409
|
+
try {
|
|
410
|
+
validated = assertSafeCanisterConfig({
|
|
411
|
+
name,
|
|
412
|
+
canisterOutDir: canisterConfig.outDir,
|
|
413
|
+
globalOutDir: globalConfig.outDir,
|
|
414
|
+
clientManagerPath: clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients",
|
|
415
|
+
projectRoot,
|
|
416
|
+
mode: canisterConfig.mode,
|
|
417
|
+
target: canisterConfig.target ?? globalConfig.target
|
|
418
|
+
});
|
|
419
|
+
} catch (err) {
|
|
420
|
+
if (err instanceof CodegenConfigError) {
|
|
421
|
+
return {
|
|
422
|
+
canisterName: typeof name === "string" ? name : String(name),
|
|
423
|
+
success: false,
|
|
424
|
+
files,
|
|
425
|
+
error: err.message
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
throw err;
|
|
429
|
+
}
|
|
430
|
+
const resolvedDidFile = import_node_path4.default.isAbsolute(didFile) ? didFile : import_node_path4.default.resolve(projectRoot, didFile);
|
|
431
|
+
if (!import_node_fs3.default.existsSync(resolvedDidFile)) {
|
|
225
432
|
return {
|
|
226
433
|
canisterName: name,
|
|
227
434
|
success: false,
|
|
@@ -229,8 +436,8 @@ async function runCanisterPipeline(options) {
|
|
|
229
436
|
error: `DID file not found: ${resolvedDidFile}`
|
|
230
437
|
};
|
|
231
438
|
}
|
|
232
|
-
const canisterOutDir =
|
|
233
|
-
const resolvedClientManagerPath =
|
|
439
|
+
const canisterOutDir = validated.outDir;
|
|
440
|
+
const resolvedClientManagerPath = validated.clientManagerPath;
|
|
234
441
|
try {
|
|
235
442
|
const declResult = await generateDeclarations({
|
|
236
443
|
didFile: resolvedDidFile,
|
|
@@ -261,8 +468,8 @@ async function runCanisterPipeline(options) {
|
|
|
261
468
|
files
|
|
262
469
|
};
|
|
263
470
|
}
|
|
264
|
-
const reactorPath =
|
|
265
|
-
const entryPath =
|
|
471
|
+
const reactorPath = import_node_path4.default.join(canisterOutDir, "index.generated.ts");
|
|
472
|
+
const entryPath = import_node_path4.default.join(canisterOutDir, "index.ts");
|
|
266
473
|
const reactorClass = resolveReactorClass(canisterConfig);
|
|
267
474
|
const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
|
|
268
475
|
try {
|
|
@@ -275,16 +482,16 @@ async function runCanisterPipeline(options) {
|
|
|
275
482
|
reactorClass
|
|
276
483
|
});
|
|
277
484
|
const entryContent = generateReactorEntryFile();
|
|
278
|
-
|
|
279
|
-
|
|
485
|
+
import_node_fs3.default.mkdirSync(canisterOutDir, { recursive: true });
|
|
486
|
+
import_node_fs3.default.writeFileSync(reactorPath, reactorContent);
|
|
280
487
|
files.push({ success: true, filePath: reactorPath });
|
|
281
|
-
if (!
|
|
282
|
-
|
|
488
|
+
if (!import_node_fs3.default.existsSync(entryPath)) {
|
|
489
|
+
import_node_fs3.default.writeFileSync(entryPath, entryContent);
|
|
283
490
|
files.push({ success: true, filePath: entryPath });
|
|
284
491
|
} else {
|
|
285
|
-
const existingEntryContent =
|
|
492
|
+
const existingEntryContent = import_node_fs3.default.readFileSync(entryPath, "utf-8");
|
|
286
493
|
if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
|
|
287
|
-
|
|
494
|
+
import_node_fs3.default.writeFileSync(entryPath, entryContent);
|
|
288
495
|
files.push({ success: true, filePath: entryPath });
|
|
289
496
|
} else {
|
|
290
497
|
files.push({ success: true, filePath: entryPath, skipped: true });
|
|
@@ -312,7 +519,7 @@ async function runCanisterPipeline(options) {
|
|
|
312
519
|
|
|
313
520
|
// src/parser.ts
|
|
314
521
|
var import_parser2 = require("@ic-reactor/parser");
|
|
315
|
-
var
|
|
522
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
316
523
|
function extractMethods(didContent) {
|
|
317
524
|
try {
|
|
318
525
|
const jsContent = (0, import_parser2.didToJs)(didContent);
|
|
@@ -337,10 +544,10 @@ function extractMethods(didContent) {
|
|
|
337
544
|
}
|
|
338
545
|
}
|
|
339
546
|
function parseDIDFile(didFilePath) {
|
|
340
|
-
if (!
|
|
547
|
+
if (!import_node_fs4.default.existsSync(didFilePath)) {
|
|
341
548
|
throw new Error(`DID file not found: ${didFilePath}`);
|
|
342
549
|
}
|
|
343
|
-
const content =
|
|
550
|
+
const content = import_node_fs4.default.readFileSync(didFilePath, "utf-8");
|
|
344
551
|
return extractMethods(content);
|
|
345
552
|
}
|
|
346
553
|
|
|
@@ -361,12 +568,20 @@ ${queryClientImport}
|
|
|
361
568
|
*/
|
|
362
569
|
export const clientManager = new ClientManager({
|
|
363
570
|
queryClient,
|
|
364
|
-
withCanisterEnv: true,
|
|
365
571
|
})
|
|
366
572
|
`;
|
|
367
573
|
}
|
|
368
574
|
// Annotate the CommonJS export names for ESM import in node:
|
|
369
575
|
0 && (module.exports = {
|
|
576
|
+
CANISTER_NAME_PATTERN,
|
|
577
|
+
CODEGEN_TARGETS,
|
|
578
|
+
CodegenConfigError,
|
|
579
|
+
REACTOR_CLASS_NAMES,
|
|
580
|
+
assertContainedPath,
|
|
581
|
+
assertOneOf,
|
|
582
|
+
assertSafeCanisterConfig,
|
|
583
|
+
assertSafeCanisterName,
|
|
584
|
+
assertSafeModuleSpecifier,
|
|
370
585
|
declarationsExist,
|
|
371
586
|
extractMethods,
|
|
372
587
|
generateClientFile,
|
|
@@ -376,6 +591,7 @@ export const clientManager = new ClientManager({
|
|
|
376
591
|
getReactorName,
|
|
377
592
|
getServiceTypeName,
|
|
378
593
|
parseDIDFile,
|
|
594
|
+
resolveContainedOutDir,
|
|
379
595
|
runCanisterPipeline,
|
|
380
596
|
toPascalCase
|
|
381
597
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -140,6 +140,116 @@ declare function getReactorName(canisterName: string): string;
|
|
|
140
140
|
*/
|
|
141
141
|
declare function getServiceTypeName(canisterName: string): string;
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Config validation for the codegen pipeline.
|
|
145
|
+
*
|
|
146
|
+
* Every value validated here arrives from a project's `ic-reactor.json` or from
|
|
147
|
+
* `@ic-reactor/vite-plugin` options — that is, from a file in a repository the
|
|
148
|
+
* user may have merely cloned. The pipeline turns those values into filesystem
|
|
149
|
+
* paths it recursively deletes and into source text it writes into the user's
|
|
150
|
+
* bundle, so they are treated as untrusted input rather than as configuration.
|
|
151
|
+
*
|
|
152
|
+
* Validation lives here, at the pipeline's own entry point, rather than in the
|
|
153
|
+
* CLI's interactive prompt: the prompt only covers one of the three entry paths
|
|
154
|
+
* (hand-edited config and plugin options bypass it entirely).
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Thrown when a canister config would produce an unsafe path or unsafe
|
|
159
|
+
* generated source. Callers convert this into a `PipelineResult` error.
|
|
160
|
+
*/
|
|
161
|
+
declare class CodegenConfigError extends Error {
|
|
162
|
+
readonly name = "CodegenConfigError";
|
|
163
|
+
constructor(message: string);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Characters allowed in a canister name.
|
|
167
|
+
*
|
|
168
|
+
* A canister name becomes a directory segment, so this excludes path
|
|
169
|
+
* separators, quotes, whitespace and control characters. It deliberately does
|
|
170
|
+
* NOT require a leading letter: `dfx` places no restriction on canister names,
|
|
171
|
+
* and real projects use `_private` and `my.canister`, both of which derive
|
|
172
|
+
* perfectly good identifiers. Identifier validity is enforced separately, on
|
|
173
|
+
* the *derived* names, by {@link assertSafeCanisterName}.
|
|
174
|
+
*/
|
|
175
|
+
declare const CANISTER_NAME_PATTERN: RegExp;
|
|
176
|
+
/**
|
|
177
|
+
* Assert that a canister name is safe to use as a path segment and as the stem
|
|
178
|
+
* of a generated identifier.
|
|
179
|
+
*
|
|
180
|
+
* Rejecting a leading digit here is what stops `2048_game` from reaching the
|
|
181
|
+
* generator, where it would become `export const 2048GameReactor` — invalid
|
|
182
|
+
* TypeScript emitted with a success status.
|
|
183
|
+
*/
|
|
184
|
+
declare function assertSafeCanisterName(name: unknown): asserts name is string;
|
|
185
|
+
/**
|
|
186
|
+
* Assert that a module specifier we interpolate into an `import` statement is a
|
|
187
|
+
* relative path or a bare package name — never a URL.
|
|
188
|
+
*
|
|
189
|
+
* A `https://…` specifier here would make the generated module pull code from a
|
|
190
|
+
* remote host at import time, inside the user's own bundle.
|
|
191
|
+
*/
|
|
192
|
+
declare function assertSafeModuleSpecifier(label: string, specifier: unknown): asserts specifier is string;
|
|
193
|
+
/** Reactor classes the generator knows how to emit an import for. */
|
|
194
|
+
declare const REACTOR_CLASS_NAMES: readonly ReactorClassName[];
|
|
195
|
+
/** Runtime targets the generator knows how to emit. */
|
|
196
|
+
declare const CODEGEN_TARGETS: readonly CodegenTarget[];
|
|
197
|
+
/**
|
|
198
|
+
* Assert a config value is one of a closed set.
|
|
199
|
+
*
|
|
200
|
+
* `mode` and `target` are interpolated into emitted source as bare identifiers
|
|
201
|
+
* and module specifiers, so an unrecognized value is not merely unsupported —
|
|
202
|
+
* it is injected.
|
|
203
|
+
*/
|
|
204
|
+
declare function assertOneOf<T extends string>(label: string, value: unknown, allowed: readonly T[]): asserts value is T;
|
|
205
|
+
/**
|
|
206
|
+
* Assert that an already-resolved absolute path lies inside `projectRoot`.
|
|
207
|
+
*
|
|
208
|
+
* Containment is decided on the real on-disk locations: `path.resolve` does not
|
|
209
|
+
* follow symlinks, so a purely lexical comparison can be walked out of via a
|
|
210
|
+
* symlink placed inside the project root.
|
|
211
|
+
*
|
|
212
|
+
* @param original - the path as the user wrote it, for the error message
|
|
213
|
+
*/
|
|
214
|
+
declare function assertContainedPath(label: string, resolved: string, projectRoot: string, original?: string): void;
|
|
215
|
+
/**
|
|
216
|
+
* Resolve `outDir` against `projectRoot` and assert the result stays inside it.
|
|
217
|
+
*
|
|
218
|
+
* The pipeline recursively deletes `<outDir>/declarations` before every
|
|
219
|
+
* generation, so an `outDir` that escapes the project root turns a config file
|
|
220
|
+
* into an arbitrary-directory delete.
|
|
221
|
+
*/
|
|
222
|
+
declare function resolveContainedOutDir(label: string, outDir: unknown, projectRoot: string): string;
|
|
223
|
+
interface ValidatedCanisterPaths {
|
|
224
|
+
/** The validated canister name. */
|
|
225
|
+
name: string;
|
|
226
|
+
/** Absolute output directory, guaranteed to be inside `projectRoot`. */
|
|
227
|
+
outDir: string;
|
|
228
|
+
/** Validated module specifier for the client manager import. */
|
|
229
|
+
clientManagerPath: string;
|
|
230
|
+
}
|
|
231
|
+
interface ValidateCanisterConfigOptions {
|
|
232
|
+
name: unknown;
|
|
233
|
+
/** Per-canister `outDir`, if the config sets one. */
|
|
234
|
+
canisterOutDir?: unknown;
|
|
235
|
+
/** Global `outDir`; the canister name is appended to it when used. */
|
|
236
|
+
globalOutDir: unknown;
|
|
237
|
+
clientManagerPath: unknown;
|
|
238
|
+
projectRoot: string;
|
|
239
|
+
/** Resolved reactor class (`canisterConfig.mode`), if the config sets one. */
|
|
240
|
+
mode?: unknown;
|
|
241
|
+
/** Resolved runtime target (`canisterConfig.target` / global `target`). */
|
|
242
|
+
target?: unknown;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Validate one canister's config and return the resolved, contained paths the
|
|
246
|
+
* pipeline should use.
|
|
247
|
+
*
|
|
248
|
+
* Call this before any filesystem work: it is the single choke point that all
|
|
249
|
+
* three entry paths (CLI, vite plugin, direct API) share.
|
|
250
|
+
*/
|
|
251
|
+
declare function assertSafeCanisterConfig(options: ValidateCanisterConfigOptions): ValidatedCanisterPaths;
|
|
252
|
+
|
|
143
253
|
/**
|
|
144
254
|
* Candid Parser Utilities
|
|
145
255
|
*
|
|
@@ -283,4 +393,4 @@ interface ClientGeneratorOptions {
|
|
|
283
393
|
*/
|
|
284
394
|
declare function generateClientFile(options?: ClientGeneratorOptions): string;
|
|
285
395
|
|
|
286
|
-
export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorClassName, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
|
|
396
|
+
export { CANISTER_NAME_PATTERN, CODEGEN_TARGETS, type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, CodegenConfigError, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, REACTOR_CLASS_NAMES, type ReactorClassName, type ReactorGeneratorOptions, type ValidateCanisterConfigOptions, type ValidatedCanisterPaths, assertContainedPath, assertOneOf, assertSafeCanisterConfig, assertSafeCanisterName, assertSafeModuleSpecifier, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, resolveContainedOutDir, runCanisterPipeline, toPascalCase };
|
package/dist/index.d.ts
CHANGED
|
@@ -140,6 +140,116 @@ declare function getReactorName(canisterName: string): string;
|
|
|
140
140
|
*/
|
|
141
141
|
declare function getServiceTypeName(canisterName: string): string;
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Config validation for the codegen pipeline.
|
|
145
|
+
*
|
|
146
|
+
* Every value validated here arrives from a project's `ic-reactor.json` or from
|
|
147
|
+
* `@ic-reactor/vite-plugin` options — that is, from a file in a repository the
|
|
148
|
+
* user may have merely cloned. The pipeline turns those values into filesystem
|
|
149
|
+
* paths it recursively deletes and into source text it writes into the user's
|
|
150
|
+
* bundle, so they are treated as untrusted input rather than as configuration.
|
|
151
|
+
*
|
|
152
|
+
* Validation lives here, at the pipeline's own entry point, rather than in the
|
|
153
|
+
* CLI's interactive prompt: the prompt only covers one of the three entry paths
|
|
154
|
+
* (hand-edited config and plugin options bypass it entirely).
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Thrown when a canister config would produce an unsafe path or unsafe
|
|
159
|
+
* generated source. Callers convert this into a `PipelineResult` error.
|
|
160
|
+
*/
|
|
161
|
+
declare class CodegenConfigError extends Error {
|
|
162
|
+
readonly name = "CodegenConfigError";
|
|
163
|
+
constructor(message: string);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Characters allowed in a canister name.
|
|
167
|
+
*
|
|
168
|
+
* A canister name becomes a directory segment, so this excludes path
|
|
169
|
+
* separators, quotes, whitespace and control characters. It deliberately does
|
|
170
|
+
* NOT require a leading letter: `dfx` places no restriction on canister names,
|
|
171
|
+
* and real projects use `_private` and `my.canister`, both of which derive
|
|
172
|
+
* perfectly good identifiers. Identifier validity is enforced separately, on
|
|
173
|
+
* the *derived* names, by {@link assertSafeCanisterName}.
|
|
174
|
+
*/
|
|
175
|
+
declare const CANISTER_NAME_PATTERN: RegExp;
|
|
176
|
+
/**
|
|
177
|
+
* Assert that a canister name is safe to use as a path segment and as the stem
|
|
178
|
+
* of a generated identifier.
|
|
179
|
+
*
|
|
180
|
+
* Rejecting a leading digit here is what stops `2048_game` from reaching the
|
|
181
|
+
* generator, where it would become `export const 2048GameReactor` — invalid
|
|
182
|
+
* TypeScript emitted with a success status.
|
|
183
|
+
*/
|
|
184
|
+
declare function assertSafeCanisterName(name: unknown): asserts name is string;
|
|
185
|
+
/**
|
|
186
|
+
* Assert that a module specifier we interpolate into an `import` statement is a
|
|
187
|
+
* relative path or a bare package name — never a URL.
|
|
188
|
+
*
|
|
189
|
+
* A `https://…` specifier here would make the generated module pull code from a
|
|
190
|
+
* remote host at import time, inside the user's own bundle.
|
|
191
|
+
*/
|
|
192
|
+
declare function assertSafeModuleSpecifier(label: string, specifier: unknown): asserts specifier is string;
|
|
193
|
+
/** Reactor classes the generator knows how to emit an import for. */
|
|
194
|
+
declare const REACTOR_CLASS_NAMES: readonly ReactorClassName[];
|
|
195
|
+
/** Runtime targets the generator knows how to emit. */
|
|
196
|
+
declare const CODEGEN_TARGETS: readonly CodegenTarget[];
|
|
197
|
+
/**
|
|
198
|
+
* Assert a config value is one of a closed set.
|
|
199
|
+
*
|
|
200
|
+
* `mode` and `target` are interpolated into emitted source as bare identifiers
|
|
201
|
+
* and module specifiers, so an unrecognized value is not merely unsupported —
|
|
202
|
+
* it is injected.
|
|
203
|
+
*/
|
|
204
|
+
declare function assertOneOf<T extends string>(label: string, value: unknown, allowed: readonly T[]): asserts value is T;
|
|
205
|
+
/**
|
|
206
|
+
* Assert that an already-resolved absolute path lies inside `projectRoot`.
|
|
207
|
+
*
|
|
208
|
+
* Containment is decided on the real on-disk locations: `path.resolve` does not
|
|
209
|
+
* follow symlinks, so a purely lexical comparison can be walked out of via a
|
|
210
|
+
* symlink placed inside the project root.
|
|
211
|
+
*
|
|
212
|
+
* @param original - the path as the user wrote it, for the error message
|
|
213
|
+
*/
|
|
214
|
+
declare function assertContainedPath(label: string, resolved: string, projectRoot: string, original?: string): void;
|
|
215
|
+
/**
|
|
216
|
+
* Resolve `outDir` against `projectRoot` and assert the result stays inside it.
|
|
217
|
+
*
|
|
218
|
+
* The pipeline recursively deletes `<outDir>/declarations` before every
|
|
219
|
+
* generation, so an `outDir` that escapes the project root turns a config file
|
|
220
|
+
* into an arbitrary-directory delete.
|
|
221
|
+
*/
|
|
222
|
+
declare function resolveContainedOutDir(label: string, outDir: unknown, projectRoot: string): string;
|
|
223
|
+
interface ValidatedCanisterPaths {
|
|
224
|
+
/** The validated canister name. */
|
|
225
|
+
name: string;
|
|
226
|
+
/** Absolute output directory, guaranteed to be inside `projectRoot`. */
|
|
227
|
+
outDir: string;
|
|
228
|
+
/** Validated module specifier for the client manager import. */
|
|
229
|
+
clientManagerPath: string;
|
|
230
|
+
}
|
|
231
|
+
interface ValidateCanisterConfigOptions {
|
|
232
|
+
name: unknown;
|
|
233
|
+
/** Per-canister `outDir`, if the config sets one. */
|
|
234
|
+
canisterOutDir?: unknown;
|
|
235
|
+
/** Global `outDir`; the canister name is appended to it when used. */
|
|
236
|
+
globalOutDir: unknown;
|
|
237
|
+
clientManagerPath: unknown;
|
|
238
|
+
projectRoot: string;
|
|
239
|
+
/** Resolved reactor class (`canisterConfig.mode`), if the config sets one. */
|
|
240
|
+
mode?: unknown;
|
|
241
|
+
/** Resolved runtime target (`canisterConfig.target` / global `target`). */
|
|
242
|
+
target?: unknown;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Validate one canister's config and return the resolved, contained paths the
|
|
246
|
+
* pipeline should use.
|
|
247
|
+
*
|
|
248
|
+
* Call this before any filesystem work: it is the single choke point that all
|
|
249
|
+
* three entry paths (CLI, vite plugin, direct API) share.
|
|
250
|
+
*/
|
|
251
|
+
declare function assertSafeCanisterConfig(options: ValidateCanisterConfigOptions): ValidatedCanisterPaths;
|
|
252
|
+
|
|
143
253
|
/**
|
|
144
254
|
* Candid Parser Utilities
|
|
145
255
|
*
|
|
@@ -283,4 +393,4 @@ interface ClientGeneratorOptions {
|
|
|
283
393
|
*/
|
|
284
394
|
declare function generateClientFile(options?: ClientGeneratorOptions): string;
|
|
285
395
|
|
|
286
|
-
export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorClassName, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
|
|
396
|
+
export { CANISTER_NAME_PATTERN, CODEGEN_TARGETS, type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, CodegenConfigError, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, REACTOR_CLASS_NAMES, type ReactorClassName, type ReactorGeneratorOptions, type ValidateCanisterConfigOptions, type ValidatedCanisterPaths, assertContainedPath, assertOneOf, assertSafeCanisterConfig, assertSafeCanisterName, assertSafeModuleSpecifier, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, resolveContainedOutDir, runCanisterPipeline, toPascalCase };
|