Skip to content

Commit 17df0bb

Browse files
authored
feat(types): extend ABI tooling for schema generation (#491)
1 parent df38391 commit 17df0bb

File tree

6 files changed

+489
-136
lines changed

6 files changed

+489
-136
lines changed

.changeset/mighty-feet-move.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@dojoengine/core": patch
3+
---
4+
5+
feat(core): add ABI tooling for schema generation

packages/core/src/cli/compile-abi.ts

Lines changed: 194 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
#!/usr/bin/env node
22

3-
import { readFileSync, writeFileSync, readdirSync, existsSync } from "fs";
4-
import { join } from "path";
3+
import {
4+
readFileSync,
5+
writeFileSync,
6+
readdirSync,
7+
existsSync,
8+
mkdirSync,
9+
} from "fs";
10+
import { join, isAbsolute, dirname, resolve } from "path";
11+
import { fileURLToPath } from "url";
12+
import type { Dirent } from "fs";
513

614
type AbiEntry = {
715
[key: string]: any;
@@ -26,13 +34,41 @@ type TargetFile = {
2634
[key: string]: any;
2735
};
2836

37+
type OutputPaths = {
38+
json: string;
39+
ts: string;
40+
};
41+
42+
type CollectOptions = {
43+
generateTypes: boolean;
44+
outputPath?: string;
45+
};
46+
2947
/**
3048
* Generate a TypeScript file from compiled-abi.json with proper const assertions
3149
* This allows TypeScript to extract literal types from the ABI
3250
*/
33-
function generateAbiTypes(dojoRoot: string): void {
34-
const inputPath = join(dojoRoot, "compiled-abi.json");
35-
const outputPath = join(dojoRoot, "compiled-abi.ts");
51+
function ensureDirectory(path: string): void {
52+
const directory = dirname(path);
53+
mkdirSync(directory, { recursive: true });
54+
}
55+
56+
export function resolveOutputPaths(outputOption?: string): OutputPaths {
57+
const jsonPath = outputOption
58+
? isAbsolute(outputOption)
59+
? outputOption
60+
: join(process.cwd(), outputOption)
61+
: join(process.cwd(), "compiled-abi.json");
62+
63+
const tsPath = jsonPath.endsWith(".json")
64+
? `${jsonPath.slice(0, -5)}.ts`
65+
: `${jsonPath}.ts`;
66+
67+
return { json: jsonPath, ts: tsPath };
68+
}
69+
70+
function generateAbiTypes(paths: OutputPaths): void {
71+
const { json: inputPath, ts: outputPath } = paths;
3672

3773
try {
3874
// Read the compiled ABI
@@ -49,14 +85,18 @@ export type CompiledAbi = typeof compiledAbi;
4985
`;
5086

5187
// Write the TypeScript file
88+
ensureDirectory(outputPath);
5289
writeFileSync(outputPath, tsContent);
5390

5491
console.log(`✅ Generated TypeScript types!`);
55-
console.log(`📄 Output written to: ${outputPath}`);
56-
console.log(`\nUsage in your code:`);
57-
console.log(`\nimport { compiledAbi } from './compiled-abi';`);
92+
console.log(`📄 Type output written to: ${outputPath}`);
93+
console.log(`
94+
Usage in your code:`);
95+
console.log(`
96+
import { compiledAbi } from './compiled-abi';`);
5897
console.log(`import { ExtractAbiTypes } from '@dojoengine/core';`);
59-
console.log(`\ntype MyAbi = ExtractAbiTypes<typeof compiledAbi>;`);
98+
console.log(`
99+
type MyAbi = ExtractAbiTypes<typeof compiledAbi>;`);
60100
console.log(
61101
`type Position = MyAbi["structs"]["dojo_starter::models::Position"];`
62102
);
@@ -66,14 +106,35 @@ export type CompiledAbi = typeof compiledAbi;
66106
}
67107
}
68108

69-
function collectAbis(generateTypes: boolean): void {
109+
function walkJsonFiles(root: string, entries: Dirent[] = []): string[] {
110+
const collected: string[] = [];
111+
112+
for (const entry of entries) {
113+
const fullPath = join(root, entry.name);
114+
115+
if (entry.isDirectory()) {
116+
const childEntries = readdirSync(fullPath, { withFileTypes: true });
117+
collected.push(...walkJsonFiles(fullPath, childEntries));
118+
continue;
119+
}
120+
121+
if (entry.isFile() && entry.name.endsWith(".json")) {
122+
collected.push(fullPath);
123+
}
124+
}
125+
126+
return collected;
127+
}
128+
129+
function collectAbis(options: CollectOptions): void {
70130
const dojoRoot = process.env.DOJO_ROOT || process.cwd();
71131
const dojoEnv = process.env.DOJO_ENV || "dev";
72132

73133
const manifestPath = join(dojoRoot, `manifest_${dojoEnv}.json`);
74134
const targetDir = join(dojoRoot, "target", dojoEnv);
75135

76136
const allAbis: AbiEntry[] = [];
137+
let manifest: Manifest | null = null;
77138

78139
// Read manifest file
79140
if (!existsSync(manifestPath)) {
@@ -83,7 +144,7 @@ function collectAbis(generateTypes: boolean): void {
83144

84145
try {
85146
const manifestContent = readFileSync(manifestPath, "utf-8");
86-
const manifest: Manifest = JSON.parse(manifestContent);
147+
manifest = JSON.parse(manifestContent) as Manifest;
87148

88149
// Extract ABIs from world
89150
if (manifest.world?.abi) {
@@ -108,12 +169,10 @@ function collectAbis(generateTypes: boolean): void {
108169
console.warn(`Target directory not found: ${targetDir}`);
109170
} else {
110171
try {
111-
const files = readdirSync(targetDir).filter((file) =>
112-
file.endsWith(".json")
113-
);
172+
const dirEntries = readdirSync(targetDir, { withFileTypes: true });
173+
const files = walkJsonFiles(targetDir, dirEntries);
114174

115-
for (const file of files) {
116-
const filePath = join(targetDir, file);
175+
for (const filePath of files) {
117176
try {
118177
const fileContent = readFileSync(filePath, "utf-8");
119178
const targetFile: TargetFile = JSON.parse(fileContent);
@@ -123,40 +182,142 @@ function collectAbis(generateTypes: boolean): void {
123182
allAbis.push(...targetFile.abi);
124183
}
125184
} catch (error) {
126-
console.error(`Error reading file ${file}: ${error}`);
185+
console.error(`Error reading file ${filePath}: ${error}`);
127186
}
128187
}
129188
} catch (error) {
130189
console.error(`Error reading target directory: ${error}`);
131190
}
132191
}
133192

193+
const dedupedAbis = new Map<string, AbiEntry>();
194+
const duplicateCounts: Record<string, number> = {};
195+
196+
for (const entry of allAbis) {
197+
const type = typeof entry.type === "string" ? entry.type : "unknown";
198+
const name =
199+
typeof (entry as { name?: string }).name === "string"
200+
? (entry as { name: string }).name
201+
: "";
202+
const interfaceName =
203+
typeof (entry as { interface_name?: string }).interface_name ===
204+
"string"
205+
? (entry as { interface_name: string }).interface_name
206+
: "";
207+
208+
const key = `${type}::${name}::${interfaceName}`;
209+
210+
if (dedupedAbis.has(key)) {
211+
duplicateCounts[key] = (duplicateCounts[key] ?? 1) + 1;
212+
continue;
213+
}
214+
215+
dedupedAbis.set(key, entry);
216+
}
217+
218+
const mergedAbis = Array.from(dedupedAbis.entries())
219+
.sort((a, b) => a[0].localeCompare(b[0]))
220+
.map(([, value]) => value);
221+
222+
if (Object.keys(duplicateCounts).length > 0) {
223+
console.warn("! Duplicate ABI entries detected and ignored:");
224+
for (const [key, count] of Object.entries(duplicateCounts)) {
225+
console.warn(` • ${key} (${count} occurrences)`);
226+
}
227+
}
228+
134229
// Write output
135230
const output = {
136-
abi: allAbis,
231+
abi: mergedAbis,
232+
manifest: manifest && {
233+
world: manifest.world,
234+
base: manifest.base,
235+
contracts: manifest.contracts ?? [],
236+
models: manifest.models ?? [],
237+
},
137238
};
138239

139-
const outputPath = join(dojoRoot, "compiled-abi.json");
140-
writeFileSync(outputPath, JSON.stringify(output, null, 2));
240+
const paths = resolveOutputPaths(options.outputPath);
241+
ensureDirectory(paths.json);
242+
writeFileSync(paths.json, JSON.stringify(output, null, 2));
141243

142244
console.log(` ABI compilation complete!`);
143-
console.log(`📄 Output written to: ${outputPath}`);
144-
console.log(`📊 Total ABI entries: ${allAbis.length}`);
245+
console.log(`📄 Output written to: ${paths.json}`);
246+
console.log(`📊 Total ABI entries: ${mergedAbis.length}`);
247+
248+
const typeStats = mergedAbis.reduce<Record<string, number>>((acc, item) => {
249+
const key = typeof item.type === "string" ? item.type : "unknown";
250+
acc[key] = (acc[key] ?? 0) + 1;
251+
return acc;
252+
}, {});
253+
254+
for (const [abiType, count] of Object.entries(typeStats)) {
255+
console.log(` • ${abiType}: ${count}`);
256+
}
145257

146258
// Generate TypeScript types if requested
147-
if (generateTypes) {
148-
generateAbiTypes(dojoRoot);
259+
if (options.generateTypes) {
260+
generateAbiTypes(paths);
261+
}
262+
}
263+
264+
function parseArgs(argv: string[]): CollectOptions {
265+
let generateTypes = false;
266+
let outputPath: string | undefined;
267+
let index = 0;
268+
269+
while (index < argv.length) {
270+
const arg = argv[index];
271+
272+
if (arg === "--generate-types") {
273+
generateTypes = true;
274+
index += 1;
275+
continue;
276+
}
277+
278+
if (arg === "--output") {
279+
const value = argv[index + 1];
280+
if (!value || value.startsWith("--")) {
281+
console.error("Missing value for --output option");
282+
process.exit(1);
283+
}
284+
outputPath = value;
285+
index += 2;
286+
continue;
287+
}
288+
289+
if (arg.startsWith("--output=")) {
290+
const value = arg.slice("--output=".length);
291+
if (!value) {
292+
console.error("Missing value for --output option");
293+
process.exit(1);
294+
}
295+
outputPath = value;
296+
index += 1;
297+
continue;
298+
}
299+
300+
console.warn(`! Unknown argument ignored: ${arg}`);
301+
index += 1;
149302
}
303+
304+
return {
305+
generateTypes,
306+
outputPath,
307+
};
150308
}
151309

152-
// Parse command line arguments
153-
const args = process.argv.slice(2);
154-
const generateTypes = args.includes("--generate-types");
310+
const __filename = resolve(fileURLToPath(import.meta.url));
311+
const entryPoint = process.argv[1] ? resolve(process.argv[1]) : undefined;
312+
const isDirectExecution = entryPoint === __filename;
313+
314+
if (isDirectExecution) {
315+
const options = parseArgs(process.argv.slice(2));
155316

156-
// Run the compilation
157-
try {
158-
collectAbis(generateTypes);
159-
} catch (error) {
160-
console.error(`Unexpected error: ${error}`);
161-
process.exit(1);
317+
try {
318+
collectAbis(options);
319+
} catch (error) {
320+
console.error(`Unexpected error: ${error}`);
321+
process.exit(1);
322+
}
162323
}

0 commit comments

Comments
 (0)