@latticexyz/cli 1.41.0 → 2.0.0-alpha.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/dist/{chunk-GR245KYP.js → chunk-J4DJQNIC.js} +679 -133
- package/dist/chunk-O57QENJ6.js +23039 -0
- package/dist/config/index.d.ts +606 -292
- package/dist/config/index.js +49 -26
- package/dist/index.d.ts +2 -110
- package/dist/index.js +9 -50
- package/dist/mud.js +937 -57
- package/dist/utils/index.d.ts +92 -4
- package/dist/utils/index.js +2 -3
- package/package.json +10 -9
- package/src/commands/deploy-v2.ts +11 -15
- package/src/commands/index.ts +2 -0
- package/src/commands/worldgen.ts +55 -0
- package/src/config/commonSchemas.ts +11 -13
- package/src/config/dynamicResolution.ts +49 -0
- package/src/config/index.ts +15 -3
- package/src/config/loadStoreConfig.ts +1 -1
- package/src/config/parseStoreConfig.test-d.ts +31 -5
- package/src/config/parseStoreConfig.ts +218 -78
- package/src/config/validation.ts +25 -0
- package/src/config/world/index.ts +4 -0
- package/src/config/{loadWorldConfig.test-d.ts → world/loadWorldConfig.test-d.ts} +3 -3
- package/src/config/world/loadWorldConfig.ts +26 -0
- package/src/config/world/parseWorldConfig.ts +55 -0
- package/src/config/world/resolveWorldConfig.ts +80 -0
- package/src/config/world/userTypes.ts +72 -0
- package/src/index.ts +4 -6
- package/src/render-solidity/common.ts +51 -6
- package/src/render-solidity/field.ts +40 -44
- package/src/render-solidity/index.ts +5 -1
- package/src/render-solidity/record.ts +56 -73
- package/src/render-solidity/renderSystemInterface.ts +31 -0
- package/src/render-solidity/renderTable.ts +98 -70
- package/src/render-solidity/renderTypeHelpers.ts +99 -0
- package/src/render-solidity/renderTypesFromConfig.ts +2 -2
- package/src/render-solidity/renderWorld.ts +24 -0
- package/src/render-solidity/{renderTablesFromConfig.ts → tableOptions.ts} +28 -30
- package/src/render-solidity/tablegen.ts +20 -22
- package/src/render-solidity/types.ts +39 -5
- package/src/render-solidity/userType.ts +80 -48
- package/src/render-solidity/worldgen.ts +60 -0
- package/src/utils/contractToInterface.ts +130 -0
- package/src/utils/deploy-v2.ts +268 -101
- package/src/utils/formatAndWrite.ts +12 -0
- package/src/utils/getChainId.ts +10 -0
- package/src/utils/typeUtils.ts +17 -0
- package/dist/chunk-AER7UDD4.js +0 -0
- package/dist/chunk-XRS7KWBZ.js +0 -547
- package/dist/chunk-YZATC2M3.js +0 -397
- package/dist/chunk-ZYDMYSTH.js +0 -1178
- package/dist/deploy-v2-b7b3207d.d.ts +0 -92
- package/src/config/loadWorldConfig.ts +0 -178
- package/src/constants.ts +0 -1
|
@@ -1,32 +1,89 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AbiType, AbiTypes, StaticAbiType, StaticAbiTypes } from "@latticexyz/schema-type";
|
|
2
2
|
import { RefinementCtx, z, ZodIssueCode } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { AsDependent, ExtractUserTypes, RequireKeys, StaticArray, StringForUnion } from "../utils/typeUtils.js";
|
|
4
|
+
import { zObjectName, zSelector, zUserEnum, zValueName } from "./commonSchemas.js";
|
|
5
|
+
import { getDuplicates, parseStaticArray } from "./validation.js";
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
7
|
+
const zTableName = zObjectName;
|
|
8
|
+
const zKeyName = zValueName;
|
|
9
|
+
const zColumnName = zValueName;
|
|
10
|
+
const zUserEnumName = zObjectName;
|
|
10
11
|
|
|
11
|
-
// Fields can use
|
|
12
|
-
|
|
12
|
+
// Fields can use AbiType or one of user-defined wrapper types
|
|
13
|
+
// (user types are refined later, based on the appropriate config options)
|
|
14
|
+
const zFieldData = z.string();
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
const PrimaryKey = z.union([StaticSchemaType, UserEnumName]);
|
|
16
|
-
const PrimaryKeys = z.record(KeyName, PrimaryKey).default({ key: SchemaType.BYTES32 });
|
|
16
|
+
type FieldData<UserTypes extends StringForUnion> = AbiType | StaticArray | UserTypes;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// Primary keys allow only static types
|
|
19
|
+
// (user types are refined later, based on the appropriate config options)
|
|
20
|
+
const zPrimaryKey = z.string();
|
|
21
|
+
const zPrimaryKeys = z.record(zKeyName, zPrimaryKey).default({ key: "bytes32" });
|
|
22
|
+
|
|
23
|
+
type PrimaryKey<StaticUserTypes extends StringForUnion> = StaticAbiType | StaticUserTypes;
|
|
24
|
+
|
|
25
|
+
/************************************************************************
|
|
26
|
+
*
|
|
27
|
+
* TABLE SCHEMA
|
|
28
|
+
*
|
|
29
|
+
************************************************************************/
|
|
30
|
+
|
|
31
|
+
export type FullSchemaConfig<UserTypes extends StringForUnion = StringForUnion> = Record<string, FieldData<UserTypes>>;
|
|
32
|
+
export type ShorthandSchemaConfig<UserTypes extends StringForUnion = StringForUnion> = FieldData<UserTypes>;
|
|
33
|
+
export type SchemaConfig<UserTypes extends StringForUnion = StringForUnion> =
|
|
34
|
+
| FullSchemaConfig<UserTypes>
|
|
35
|
+
| ShorthandSchemaConfig<UserTypes>;
|
|
36
|
+
|
|
37
|
+
const zFullSchemaConfig = z
|
|
38
|
+
.record(zColumnName, zFieldData)
|
|
20
39
|
.refine((arg) => Object.keys(arg).length > 0, "Table schema may not be empty");
|
|
21
40
|
|
|
22
|
-
const
|
|
41
|
+
const zShorthandSchemaConfig = zFieldData.transform((fieldData) => {
|
|
42
|
+
return zFullSchemaConfig.parse({
|
|
43
|
+
value: fieldData,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const zSchemaConfig = zFullSchemaConfig.or(zShorthandSchemaConfig);
|
|
48
|
+
|
|
49
|
+
/************************************************************************
|
|
50
|
+
*
|
|
51
|
+
* TABLE
|
|
52
|
+
*
|
|
53
|
+
************************************************************************/
|
|
54
|
+
|
|
55
|
+
export interface TableConfig<
|
|
56
|
+
UserTypes extends StringForUnion = StringForUnion,
|
|
57
|
+
StaticUserTypes extends StringForUnion = StringForUnion
|
|
58
|
+
> {
|
|
59
|
+
/** Output directory path for the file. Default is "tables" */
|
|
60
|
+
directory?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The fileSelector is used with the namespace to register the table and construct its id.
|
|
63
|
+
* The table id will be uint256(bytes32(abi.encodePacked(bytes16(namespace), bytes16(fileSelector)))).
|
|
64
|
+
* Default is "<tableName>"
|
|
65
|
+
* */
|
|
66
|
+
fileSelector?: string;
|
|
67
|
+
/** Make methods accept `tableId` argument instead of it being a hardcoded constant. Default is false */
|
|
68
|
+
tableIdArgument?: boolean;
|
|
69
|
+
/** Include methods that accept a manual `IStore` argument. Default is false. */
|
|
70
|
+
storeArgument?: boolean;
|
|
71
|
+
/** Include a data struct and methods for it. Default is false for 1-column tables; true for multi-column tables. */
|
|
72
|
+
dataStruct?: boolean;
|
|
73
|
+
/** Table's primary key names mapped to their types. Default is `{ key: "bytes32" }` */
|
|
74
|
+
primaryKeys?: Record<string, PrimaryKey<StaticUserTypes>>;
|
|
75
|
+
/** Table's column names mapped to their types. Table name's 1st letter should be lowercase. */
|
|
76
|
+
schema: SchemaConfig<UserTypes>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const zFullTableConfig = z
|
|
23
80
|
.object({
|
|
24
|
-
directory:
|
|
25
|
-
|
|
81
|
+
directory: z.string().default("tables"),
|
|
82
|
+
fileSelector: zSelector.optional(),
|
|
26
83
|
tableIdArgument: z.boolean().default(false),
|
|
27
84
|
storeArgument: z.boolean().default(false),
|
|
28
|
-
primaryKeys:
|
|
29
|
-
schema:
|
|
85
|
+
primaryKeys: zPrimaryKeys,
|
|
86
|
+
schema: zSchemaConfig,
|
|
30
87
|
dataStruct: z.boolean().optional(),
|
|
31
88
|
})
|
|
32
89
|
.transform((arg) => {
|
|
@@ -39,43 +96,88 @@ const TableDataFull = z
|
|
|
39
96
|
return arg as RequireKeys<typeof arg, "dataStruct">;
|
|
40
97
|
});
|
|
41
98
|
|
|
42
|
-
const
|
|
43
|
-
return
|
|
99
|
+
const zShorthandTableConfig = zFieldData.transform((fieldData) => {
|
|
100
|
+
return zFullTableConfig.parse({
|
|
44
101
|
schema: {
|
|
45
102
|
value: fieldData,
|
|
46
103
|
},
|
|
47
104
|
});
|
|
48
105
|
});
|
|
49
106
|
|
|
50
|
-
const
|
|
51
|
-
|
|
107
|
+
export const zTableConfig = zFullTableConfig.or(zShorthandTableConfig);
|
|
108
|
+
|
|
109
|
+
/************************************************************************
|
|
110
|
+
*
|
|
111
|
+
* TABLES
|
|
112
|
+
*
|
|
113
|
+
************************************************************************/
|
|
114
|
+
|
|
115
|
+
export type TablesConfig<
|
|
116
|
+
UserTypes extends StringForUnion = StringForUnion,
|
|
117
|
+
StaticUserTypes extends StringForUnion = StringForUnion
|
|
118
|
+
> = Record<string, TableConfig<UserTypes, StaticUserTypes> | FieldData<UserTypes>>;
|
|
119
|
+
|
|
120
|
+
export const zTablesConfig = z.record(zTableName, zTableConfig).transform((tables) => {
|
|
121
|
+
// default fileSelector depends on tableName
|
|
52
122
|
for (const tableName of Object.keys(tables)) {
|
|
53
123
|
const table = tables[tableName];
|
|
54
|
-
table.
|
|
124
|
+
table.fileSelector ??= tableName;
|
|
55
125
|
|
|
56
126
|
tables[tableName] = table;
|
|
57
127
|
}
|
|
58
|
-
return tables as Record<string, RequireKeys<typeof tables[string], "
|
|
128
|
+
return tables as Record<string, RequireKeys<(typeof tables)[string], "fileSelector">>;
|
|
59
129
|
});
|
|
60
130
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
131
|
+
/************************************************************************
|
|
132
|
+
*
|
|
133
|
+
* USER TYPES
|
|
134
|
+
*
|
|
135
|
+
************************************************************************/
|
|
136
|
+
|
|
137
|
+
export type EnumsConfig<EnumNames extends StringForUnion> = never extends EnumNames
|
|
138
|
+
? {
|
|
139
|
+
/**
|
|
140
|
+
* Enum names mapped to lists of their member names
|
|
141
|
+
*
|
|
142
|
+
* (enums are inferred to be absent)
|
|
143
|
+
*/
|
|
144
|
+
enums?: Record<EnumNames, string[]>;
|
|
145
|
+
}
|
|
146
|
+
: StringForUnion extends EnumNames
|
|
147
|
+
? {
|
|
148
|
+
/**
|
|
149
|
+
* Enum names mapped to lists of their member names
|
|
150
|
+
*
|
|
151
|
+
* (enums aren't inferred - use `mudConfig` or `storeConfig` helper, and `as const` for variables)
|
|
152
|
+
*/
|
|
153
|
+
enums?: Record<EnumNames, string[]>;
|
|
154
|
+
}
|
|
155
|
+
: {
|
|
156
|
+
/**
|
|
157
|
+
* Enum names mapped to lists of their member names
|
|
158
|
+
*
|
|
159
|
+
* Enums defined here can be used as types in table schemas/keys
|
|
160
|
+
*/
|
|
161
|
+
enums: Record<EnumNames, string[]>;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const zEnumsConfig = z.object({
|
|
165
|
+
enums: z.record(zUserEnumName, zUserEnum).default({}),
|
|
71
166
|
});
|
|
72
|
-
|
|
73
|
-
|
|
167
|
+
|
|
168
|
+
/************************************************************************
|
|
169
|
+
*
|
|
170
|
+
* FINAL
|
|
171
|
+
*
|
|
172
|
+
************************************************************************/
|
|
74
173
|
|
|
75
174
|
// zod doesn't preserve doc comments
|
|
76
|
-
export
|
|
77
|
-
|
|
78
|
-
|
|
175
|
+
export type StoreUserConfig<
|
|
176
|
+
EnumNames extends StringForUnion = StringForUnion,
|
|
177
|
+
StaticUserTypes extends ExtractUserTypes<EnumNames> = ExtractUserTypes<EnumNames>
|
|
178
|
+
> = EnumsConfig<EnumNames> & {
|
|
179
|
+
/** The namespace for table ids. Default is "" (empty string) */
|
|
180
|
+
namespace?: string;
|
|
79
181
|
/** Path for store package imports. Default is "@latticexyz/store/src/" */
|
|
80
182
|
storeImportPath?: string;
|
|
81
183
|
/**
|
|
@@ -84,44 +186,47 @@ export interface StoreUserConfig {
|
|
|
84
186
|
* The key is the table name (capitalized).
|
|
85
187
|
*
|
|
86
188
|
* The value:
|
|
87
|
-
* -
|
|
189
|
+
* - abi or user type for a single-value table.
|
|
88
190
|
* - FullTableConfig object for multi-value tables (or for customizable options).
|
|
89
191
|
*/
|
|
90
|
-
tables:
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
}
|
|
192
|
+
tables: TablesConfig<AsDependent<StaticUserTypes>, AsDependent<StaticUserTypes>>;
|
|
193
|
+
/** Path to the file where common user types will be generated and imported from. Default is "Types" */
|
|
194
|
+
userTypesPath?: string;
|
|
195
|
+
};
|
|
94
196
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
/** Include methods that accept a manual `IStore` argument. Default is false. */
|
|
103
|
-
storeArgument?: boolean;
|
|
104
|
-
/** Include a data struct and methods for it. Default is false for 1-column tables; true for multi-column tables. */
|
|
105
|
-
dataStruct?: boolean;
|
|
106
|
-
/** Table's primary key names mapped to their types. Default is `{ key: SchemaType.BYTES32 }` */
|
|
107
|
-
primaryKeys?: Record<string, z.input<typeof PrimaryKey>>;
|
|
108
|
-
/** Table's column names mapped to their types. Table name's 1st letter should be lowercase. */
|
|
109
|
-
schema: Record<string, z.input<typeof FieldData>>;
|
|
197
|
+
/** Type helper for defining StoreUserConfig */
|
|
198
|
+
export function storeConfig<
|
|
199
|
+
// (`never` is overridden by inference, so only the defined enums can be used by default)
|
|
200
|
+
EnumNames extends StringForUnion = never,
|
|
201
|
+
StaticUserTypes extends ExtractUserTypes<EnumNames> = ExtractUserTypes<EnumNames>
|
|
202
|
+
>(config: StoreUserConfig<EnumNames, StaticUserTypes>) {
|
|
203
|
+
return config;
|
|
110
204
|
}
|
|
111
205
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
206
|
+
export type StoreConfig = z.output<typeof zStoreConfig>;
|
|
207
|
+
|
|
208
|
+
const StoreConfigUnrefined = z
|
|
209
|
+
.object({
|
|
210
|
+
namespace: zSelector.default(""),
|
|
211
|
+
storeImportPath: z.string().default("@latticexyz/store/src/"),
|
|
212
|
+
tables: zTablesConfig,
|
|
213
|
+
userTypesPath: z.string().default("Types"),
|
|
214
|
+
})
|
|
215
|
+
.merge(zEnumsConfig);
|
|
118
216
|
|
|
119
|
-
|
|
217
|
+
// finally validate global conditions
|
|
218
|
+
export const zStoreConfig = StoreConfigUnrefined.superRefine(validateStoreConfig);
|
|
120
219
|
|
|
121
|
-
export
|
|
122
|
-
return
|
|
220
|
+
export function parseStoreConfig(config: unknown) {
|
|
221
|
+
return zStoreConfig.parse(config);
|
|
123
222
|
}
|
|
124
223
|
|
|
224
|
+
/************************************************************************
|
|
225
|
+
*
|
|
226
|
+
* HELPERS
|
|
227
|
+
*
|
|
228
|
+
************************************************************************/
|
|
229
|
+
|
|
125
230
|
// Validate conditions that check multiple different config options simultaneously
|
|
126
231
|
function validateStoreConfig(config: z.output<typeof StoreConfigUnrefined>, ctx: RefinementCtx) {
|
|
127
232
|
// Local table variables must be unique within the table
|
|
@@ -138,37 +243,72 @@ function validateStoreConfig(config: z.output<typeof StoreConfigUnrefined>, ctx:
|
|
|
138
243
|
}
|
|
139
244
|
// Global names must be unique
|
|
140
245
|
const tableNames = Object.keys(config.tables);
|
|
141
|
-
const
|
|
246
|
+
const staticUserTypeNames = Object.keys(config.enums);
|
|
247
|
+
const userTypeNames = staticUserTypeNames;
|
|
142
248
|
const globalNames = [...tableNames, ...userTypeNames];
|
|
143
249
|
const duplicateGlobalNames = getDuplicates(globalNames);
|
|
144
250
|
if (duplicateGlobalNames.length > 0) {
|
|
145
251
|
ctx.addIssue({
|
|
146
252
|
code: ZodIssueCode.custom,
|
|
147
|
-
message: `Table
|
|
253
|
+
message: `Table, enum names must be globally unique: ${duplicateGlobalNames.join(", ")}`,
|
|
148
254
|
});
|
|
149
255
|
}
|
|
150
256
|
// User types must exist
|
|
151
257
|
for (const table of Object.values(config.tables)) {
|
|
152
258
|
for (const primaryKeyType of Object.values(table.primaryKeys)) {
|
|
153
|
-
|
|
259
|
+
validateStaticAbiOrUserType(staticUserTypeNames, primaryKeyType, ctx);
|
|
154
260
|
}
|
|
155
261
|
for (const fieldType of Object.values(table.schema)) {
|
|
156
|
-
|
|
262
|
+
validateAbiOrUserType(userTypeNames, staticUserTypeNames, fieldType, ctx);
|
|
157
263
|
}
|
|
158
264
|
}
|
|
159
265
|
}
|
|
160
266
|
|
|
161
|
-
function
|
|
267
|
+
function validateAbiOrUserType(
|
|
162
268
|
userTypeNames: string[],
|
|
163
|
-
|
|
269
|
+
staticUserTypeNames: string[],
|
|
270
|
+
type: string,
|
|
164
271
|
ctx: RefinementCtx
|
|
165
272
|
) {
|
|
166
|
-
if (
|
|
273
|
+
if (!(AbiTypes as string[]).includes(type) && !userTypeNames.includes(type)) {
|
|
274
|
+
const staticArray = parseStaticArray(type);
|
|
275
|
+
if (staticArray) {
|
|
276
|
+
validateStaticArray(staticUserTypeNames, staticArray.elementType, staticArray.staticLength, ctx);
|
|
277
|
+
} else {
|
|
278
|
+
ctx.addIssue({
|
|
279
|
+
code: ZodIssueCode.custom,
|
|
280
|
+
message: `${type} is not a valid abi type, and is not defined in userTypes`,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function validateStaticAbiOrUserType(staticUserTypeNames: string[], type: string, ctx: RefinementCtx) {
|
|
287
|
+
if (!(StaticAbiTypes as string[]).includes(type) && !staticUserTypeNames.includes(type)) {
|
|
167
288
|
ctx.addIssue({
|
|
168
289
|
code: ZodIssueCode.custom,
|
|
169
|
-
message:
|
|
290
|
+
message: `${type} is not a static type`,
|
|
170
291
|
});
|
|
171
292
|
}
|
|
172
293
|
}
|
|
173
294
|
|
|
174
|
-
|
|
295
|
+
function validateStaticArray(
|
|
296
|
+
staticUserTypeNames: string[],
|
|
297
|
+
elementType: string,
|
|
298
|
+
staticLength: number,
|
|
299
|
+
ctx: RefinementCtx
|
|
300
|
+
) {
|
|
301
|
+
validateStaticAbiOrUserType(staticUserTypeNames, elementType, ctx);
|
|
302
|
+
|
|
303
|
+
if (staticLength === 0) {
|
|
304
|
+
ctx.addIssue({
|
|
305
|
+
code: ZodIssueCode.custom,
|
|
306
|
+
message: `Static array length must not be 0`,
|
|
307
|
+
});
|
|
308
|
+
} else if (staticLength >= 2 ** 16) {
|
|
309
|
+
ctx.addIssue({
|
|
310
|
+
code: ZodIssueCode.custom,
|
|
311
|
+
message: `Static array length must be less than 2**16`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
package/src/config/validation.ts
CHANGED
|
@@ -136,3 +136,28 @@ export function getDuplicates<T>(array: T[]) {
|
|
|
136
136
|
}
|
|
137
137
|
return [...duplicates];
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
export function validateSelector(name: string, ctx: RefinementCtx) {
|
|
141
|
+
if (name.length > 16) {
|
|
142
|
+
ctx.addIssue({
|
|
143
|
+
code: ZodIssueCode.custom,
|
|
144
|
+
message: `Selector must be <= 16 characters`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (!/^\w*$/.test(name)) {
|
|
148
|
+
ctx.addIssue({
|
|
149
|
+
code: ZodIssueCode.custom,
|
|
150
|
+
message: `Selector must contain only alphanumeric & underscore characters`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Returns null if the type does not look like a static array, otherwise element and length data */
|
|
156
|
+
export function parseStaticArray(abiType: string) {
|
|
157
|
+
const matches = abiType.match(/^(\w+)\[(\d+)\]$/);
|
|
158
|
+
if (!matches) return null;
|
|
159
|
+
return {
|
|
160
|
+
elementType: matches[1],
|
|
161
|
+
staticLength: Number.parseInt(matches[2]),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, expectTypeOf } from "vitest";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { zWorldConfig, WorldUserConfig } from "./index.js";
|
|
4
4
|
|
|
5
5
|
describe("loadWorldConfig", () => {
|
|
6
6
|
// Typecheck manual interfaces against zod
|
|
7
|
-
expectTypeOf<WorldUserConfig>().toEqualTypeOf<z.input<typeof
|
|
7
|
+
expectTypeOf<WorldUserConfig>().toEqualTypeOf<z.input<typeof zWorldConfig>>();
|
|
8
8
|
// type equality isn't deep for optionals
|
|
9
|
-
expectTypeOf<WorldUserConfig["overrideSystems"]>().toEqualTypeOf<z.input<typeof
|
|
9
|
+
expectTypeOf<WorldUserConfig["overrideSystems"]>().toEqualTypeOf<z.input<typeof zWorldConfig>["overrideSystems"]>();
|
|
10
10
|
// TODO If more nested schemas are added, provide separate tests for them
|
|
11
11
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ZodError } from "zod";
|
|
2
|
+
import { fromZodErrorCustom } from "../../utils/errors.js";
|
|
3
|
+
import { loadConfig } from "../loadConfig.js";
|
|
4
|
+
import { zWorldConfig } from "./parseWorldConfig.js";
|
|
5
|
+
import { resolveWorldConfig } from "./resolveWorldConfig.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Loads and resolves the world config.
|
|
9
|
+
* @param configPath Path to load the config from. Defaults to "mud.config.mts" or "mud.config.ts"
|
|
10
|
+
* @param existingContracts Optional list of existing contract names to validate system names against. If not provided, no validation is performed. Contract names ending in `System` will be added to the config with default values.
|
|
11
|
+
* @returns Promise of ResolvedWorldConfig object
|
|
12
|
+
*/
|
|
13
|
+
export async function loadWorldConfig(configPath?: string, existingContracts?: string[]) {
|
|
14
|
+
const config = await loadConfig(configPath);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const parsedConfig = zWorldConfig.parse(config);
|
|
18
|
+
return resolveWorldConfig(parsedConfig, existingContracts);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (error instanceof ZodError) {
|
|
21
|
+
throw fromZodErrorCustom(error, "WorldConfig Validation Error");
|
|
22
|
+
} else {
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { zEthereumAddress, zObjectName, zSelector } from "../commonSchemas.js";
|
|
3
|
+
import { DynamicResolutionType } from "../dynamicResolution.js";
|
|
4
|
+
|
|
5
|
+
const zSystemName = zObjectName;
|
|
6
|
+
const zModuleName = zObjectName;
|
|
7
|
+
const zSystemAccessList = z.array(zSystemName.or(zEthereumAddress)).default([]);
|
|
8
|
+
|
|
9
|
+
// The system config is a combination of a fileSelector config and access config
|
|
10
|
+
const zSystemConfig = z.intersection(
|
|
11
|
+
z.object({
|
|
12
|
+
fileSelector: zSelector,
|
|
13
|
+
registerFunctionSelectors: z.boolean().default(true),
|
|
14
|
+
}),
|
|
15
|
+
z.discriminatedUnion("openAccess", [
|
|
16
|
+
z.object({
|
|
17
|
+
openAccess: z.literal(true),
|
|
18
|
+
}),
|
|
19
|
+
z.object({
|
|
20
|
+
openAccess: z.literal(false),
|
|
21
|
+
accessList: zSystemAccessList,
|
|
22
|
+
}),
|
|
23
|
+
])
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const zValueWithType = z.object({
|
|
27
|
+
value: z.union([z.string(), z.number(), z.instanceof(Uint8Array)]),
|
|
28
|
+
type: z.string(),
|
|
29
|
+
});
|
|
30
|
+
const zDynamicResolution = z.object({ type: z.nativeEnum(DynamicResolutionType), input: z.string() });
|
|
31
|
+
|
|
32
|
+
const zModuleConfig = z.object({
|
|
33
|
+
name: zModuleName,
|
|
34
|
+
root: z.boolean().default(false),
|
|
35
|
+
args: z.array(z.union([zValueWithType, zDynamicResolution])).default([]),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// The parsed world config is the result of parsing the user config
|
|
39
|
+
export const zWorldConfig = z.object({
|
|
40
|
+
namespace: zSelector.default(""),
|
|
41
|
+
worldContractName: z.string().optional(),
|
|
42
|
+
overrideSystems: z.record(zSystemName, zSystemConfig).default({}),
|
|
43
|
+
excludeSystems: z.array(zSystemName).default([]),
|
|
44
|
+
postDeployScript: z.string().default("PostDeploy"),
|
|
45
|
+
deploysDirectory: z.string().default("./deploys"),
|
|
46
|
+
worldgenDirectory: z.string().default("world"),
|
|
47
|
+
worldImportPath: z.string().default("@latticexyz/world/src/"),
|
|
48
|
+
modules: z.array(zModuleConfig).default([]),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export async function parseWorldConfig(config: unknown) {
|
|
52
|
+
return zWorldConfig.parse(config);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ParsedWorldConfig = z.output<typeof zWorldConfig>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { UnrecognizedSystemErrorFactory } from "../../utils/errors.js";
|
|
2
|
+
import { ParsedWorldConfig } from "./parseWorldConfig.js";
|
|
3
|
+
import { SystemUserConfig } from "./userTypes.js";
|
|
4
|
+
|
|
5
|
+
export type ResolvedSystemConfig = ReturnType<typeof resolveSystemConfig>;
|
|
6
|
+
|
|
7
|
+
export type ResolvedWorldConfig = ReturnType<typeof resolveWorldConfig>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the world config by combining the default and overridden system configs,
|
|
11
|
+
* filtering out excluded systems, validate system names refer to existing contracts, and
|
|
12
|
+
* splitting the access list into addresses and system names.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveWorldConfig(config: ParsedWorldConfig, existingContracts?: string[]) {
|
|
15
|
+
// Include contract names ending in "System", but not the base "System" contract, and not Interfaces
|
|
16
|
+
const defaultSystemNames =
|
|
17
|
+
existingContracts?.filter((name) => name.endsWith("System") && name !== "System" && !name.match(/^I[A-Z]/)) ?? [];
|
|
18
|
+
const overriddenSystemNames = Object.keys(config.overrideSystems);
|
|
19
|
+
|
|
20
|
+
// Validate every key in overrideSystems refers to an existing system contract (and is not called "World")
|
|
21
|
+
if (existingContracts) {
|
|
22
|
+
for (const systemName of overriddenSystemNames) {
|
|
23
|
+
if (!existingContracts.includes(systemName) || systemName === "World") {
|
|
24
|
+
throw UnrecognizedSystemErrorFactory(["overrideSystems", systemName], systemName);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Combine the default and overridden system names and filter out excluded systems
|
|
30
|
+
const systemNames = [...new Set([...defaultSystemNames, ...overriddenSystemNames])].filter(
|
|
31
|
+
(name) => !config.excludeSystems.includes(name)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Resolve the config
|
|
35
|
+
const resolvedSystems: Record<string, ResolvedSystemConfig> = systemNames.reduce((acc, systemName) => {
|
|
36
|
+
return {
|
|
37
|
+
...acc,
|
|
38
|
+
[systemName]: resolveSystemConfig(systemName, config.overrideSystems[systemName], existingContracts),
|
|
39
|
+
};
|
|
40
|
+
}, {});
|
|
41
|
+
|
|
42
|
+
const { overrideSystems, excludeSystems, ...otherConfig } = config;
|
|
43
|
+
return { ...otherConfig, systems: resolvedSystems };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolves the system config by combining the default and overridden system configs,
|
|
48
|
+
* @param systemName name of the system
|
|
49
|
+
* @param config optional SystemConfig object, if none is provided the default config is used
|
|
50
|
+
* @param existingContracts optional list of existing contract names, used to validate system names in the access list. If not provided, no validation is performed.
|
|
51
|
+
* @returns ResolvedSystemConfig object
|
|
52
|
+
* Default value for fileSelector is `systemName`
|
|
53
|
+
* Default value for registerFunctionSelectors is true
|
|
54
|
+
* Default value for openAccess is true
|
|
55
|
+
* Default value for accessListAddresses is []
|
|
56
|
+
* Default value for accessListSystems is []
|
|
57
|
+
*/
|
|
58
|
+
export function resolveSystemConfig(systemName: string, config?: SystemUserConfig, existingContracts?: string[]) {
|
|
59
|
+
const fileSelector = config?.fileSelector ?? systemName;
|
|
60
|
+
const registerFunctionSelectors = config?.registerFunctionSelectors ?? true;
|
|
61
|
+
const openAccess = config?.openAccess ?? true;
|
|
62
|
+
const accessListAddresses: string[] = [];
|
|
63
|
+
const accessListSystems: string[] = [];
|
|
64
|
+
const accessList = config && !config.openAccess ? config.accessList : [];
|
|
65
|
+
|
|
66
|
+
// Split the access list into addresses and system names
|
|
67
|
+
for (const accessListItem of accessList) {
|
|
68
|
+
if (accessListItem.startsWith("0x")) {
|
|
69
|
+
accessListAddresses.push(accessListItem);
|
|
70
|
+
} else {
|
|
71
|
+
// Validate every system refers to an existing system contract
|
|
72
|
+
if (existingContracts && !existingContracts.includes(accessListItem)) {
|
|
73
|
+
throw UnrecognizedSystemErrorFactory(["overrideSystems", systemName, "accessList"], accessListItem);
|
|
74
|
+
}
|
|
75
|
+
accessListSystems.push(accessListItem);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { fileSelector, registerFunctionSelectors, openAccess, accessListAddresses, accessListSystems };
|
|
80
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { DynamicResolution } from "../dynamicResolution.js";
|
|
2
|
+
|
|
3
|
+
// zod doesn't preserve doc comments
|
|
4
|
+
export type SystemUserConfig =
|
|
5
|
+
| {
|
|
6
|
+
/** The full resource selector consists of namespace and fileSelector */
|
|
7
|
+
fileSelector?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Register function selectors for the system in the World.
|
|
10
|
+
* Defaults to true.
|
|
11
|
+
* Note:
|
|
12
|
+
* - For root systems all World function selectors will correspond to the system's function selectors.
|
|
13
|
+
* - For non-root systems, the World function selectors will be <namespace>_<system>_<function>.
|
|
14
|
+
*/
|
|
15
|
+
registerFunctionSelectors?: boolean;
|
|
16
|
+
} & (
|
|
17
|
+
| {
|
|
18
|
+
/** If openAccess is true, any address can call the system */
|
|
19
|
+
openAccess: true;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
/** If openAccess is false, only the addresses or systems in `access` can call the system */
|
|
23
|
+
openAccess: false;
|
|
24
|
+
/** An array of addresses or system names that can access the system */
|
|
25
|
+
accessList: string[];
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export type ValueWithType = {
|
|
30
|
+
value: string | number | Uint8Array;
|
|
31
|
+
type: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ModuleConfig = {
|
|
35
|
+
/** The name of the module */
|
|
36
|
+
name: string;
|
|
37
|
+
/** Should this module be installed as a root module? */
|
|
38
|
+
root?: boolean;
|
|
39
|
+
/** Arguments to be passed to the module's install method */
|
|
40
|
+
args?: (ValueWithType | DynamicResolution)[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// zod doesn't preserve doc comments
|
|
44
|
+
export interface WorldUserConfig {
|
|
45
|
+
/** The namespace to register tables and systems at. Defaults to the root namespace (empty string) */
|
|
46
|
+
namespace?: string;
|
|
47
|
+
/** The name of the World contract to deploy. If no name is provided, a vanilla World is deployed */
|
|
48
|
+
worldContractName?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Contracts named *System will be deployed by default
|
|
51
|
+
* as public systems at `namespace/ContractName`, unless overridden
|
|
52
|
+
*
|
|
53
|
+
* The key is the system name (capitalized).
|
|
54
|
+
* The value is a SystemConfig object.
|
|
55
|
+
*/
|
|
56
|
+
overrideSystems?: Record<string, SystemUserConfig>;
|
|
57
|
+
/** Systems to exclude from automatic deployment */
|
|
58
|
+
excludeSystems?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Script to execute after the deployment is complete (Default "PostDeploy").
|
|
61
|
+
* Script must be placed in the forge scripts directory (see foundry.toml) and have a ".s.sol" extension.
|
|
62
|
+
*/
|
|
63
|
+
postDeployScript?: string;
|
|
64
|
+
/** Directory to write the deployment info to (Default "./deploys") */
|
|
65
|
+
deploysDirectory?: string;
|
|
66
|
+
/** Directory to output system and world interfaces of `worldgen` (Default "world") */
|
|
67
|
+
worldgenDirectory?: string;
|
|
68
|
+
/** Path for world package imports. Default is "@latticexyz/world/src/" */
|
|
69
|
+
worldImportPath?: string;
|
|
70
|
+
/** Modules to in the World */
|
|
71
|
+
modules?: ModuleConfig[];
|
|
72
|
+
}
|