1
1
#!/usr/bin/env node
2
2
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" ;
5
13
6
14
type AbiEntry = {
7
15
[ key : string ] : any ;
@@ -26,13 +34,41 @@ type TargetFile = {
26
34
[ key : string ] : any ;
27
35
} ;
28
36
37
+ type OutputPaths = {
38
+ json : string ;
39
+ ts : string ;
40
+ } ;
41
+
42
+ type CollectOptions = {
43
+ generateTypes : boolean ;
44
+ outputPath ?: string ;
45
+ } ;
46
+
29
47
/**
30
48
* Generate a TypeScript file from compiled-abi.json with proper const assertions
31
49
* This allows TypeScript to extract literal types from the ABI
32
50
*/
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 ;
36
72
37
73
try {
38
74
// Read the compiled ABI
@@ -49,14 +85,18 @@ export type CompiledAbi = typeof compiledAbi;
49
85
` ;
50
86
51
87
// Write the TypeScript file
88
+ ensureDirectory ( outputPath ) ;
52
89
writeFileSync ( outputPath , tsContent ) ;
53
90
54
91
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';` ) ;
58
97
console . log ( `import { ExtractAbiTypes } from '@dojoengine/core';` ) ;
59
- console . log ( `\ntype MyAbi = ExtractAbiTypes<typeof compiledAbi>;` ) ;
98
+ console . log ( `
99
+ type MyAbi = ExtractAbiTypes<typeof compiledAbi>;` ) ;
60
100
console . log (
61
101
`type Position = MyAbi["structs"]["dojo_starter::models::Position"];`
62
102
) ;
@@ -66,14 +106,35 @@ export type CompiledAbi = typeof compiledAbi;
66
106
}
67
107
}
68
108
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 {
70
130
const dojoRoot = process . env . DOJO_ROOT || process . cwd ( ) ;
71
131
const dojoEnv = process . env . DOJO_ENV || "dev" ;
72
132
73
133
const manifestPath = join ( dojoRoot , `manifest_${ dojoEnv } .json` ) ;
74
134
const targetDir = join ( dojoRoot , "target" , dojoEnv ) ;
75
135
76
136
const allAbis : AbiEntry [ ] = [ ] ;
137
+ let manifest : Manifest | null = null ;
77
138
78
139
// Read manifest file
79
140
if ( ! existsSync ( manifestPath ) ) {
@@ -83,7 +144,7 @@ function collectAbis(generateTypes: boolean): void {
83
144
84
145
try {
85
146
const manifestContent = readFileSync ( manifestPath , "utf-8" ) ;
86
- const manifest : Manifest = JSON . parse ( manifestContent ) ;
147
+ manifest = JSON . parse ( manifestContent ) as Manifest ;
87
148
88
149
// Extract ABIs from world
89
150
if ( manifest . world ?. abi ) {
@@ -108,12 +169,10 @@ function collectAbis(generateTypes: boolean): void {
108
169
console . warn ( `Target directory not found: ${ targetDir } ` ) ;
109
170
} else {
110
171
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 ) ;
114
174
115
- for ( const file of files ) {
116
- const filePath = join ( targetDir , file ) ;
175
+ for ( const filePath of files ) {
117
176
try {
118
177
const fileContent = readFileSync ( filePath , "utf-8" ) ;
119
178
const targetFile : TargetFile = JSON . parse ( fileContent ) ;
@@ -123,40 +182,142 @@ function collectAbis(generateTypes: boolean): void {
123
182
allAbis . push ( ...targetFile . abi ) ;
124
183
}
125
184
} catch ( error ) {
126
- console . error ( `Error reading file ${ file } : ${ error } ` ) ;
185
+ console . error ( `Error reading file ${ filePath } : ${ error } ` ) ;
127
186
}
128
187
}
129
188
} catch ( error ) {
130
189
console . error ( `Error reading target directory: ${ error } ` ) ;
131
190
}
132
191
}
133
192
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
+
134
229
// Write output
135
230
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
+ } ,
137
238
} ;
138
239
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 ) ) ;
141
243
142
244
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
+ }
145
257
146
258
// 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 ;
149
302
}
303
+
304
+ return {
305
+ generateTypes,
306
+ outputPath,
307
+ } ;
150
308
}
151
309
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 ) ) ;
155
316
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
+ }
162
323
}
0 commit comments