@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.
Files changed (53) hide show
  1. package/dist/{chunk-GR245KYP.js → chunk-J4DJQNIC.js} +679 -133
  2. package/dist/chunk-O57QENJ6.js +23039 -0
  3. package/dist/config/index.d.ts +606 -292
  4. package/dist/config/index.js +49 -26
  5. package/dist/index.d.ts +2 -110
  6. package/dist/index.js +9 -50
  7. package/dist/mud.js +937 -57
  8. package/dist/utils/index.d.ts +92 -4
  9. package/dist/utils/index.js +2 -3
  10. package/package.json +10 -9
  11. package/src/commands/deploy-v2.ts +11 -15
  12. package/src/commands/index.ts +2 -0
  13. package/src/commands/worldgen.ts +55 -0
  14. package/src/config/commonSchemas.ts +11 -13
  15. package/src/config/dynamicResolution.ts +49 -0
  16. package/src/config/index.ts +15 -3
  17. package/src/config/loadStoreConfig.ts +1 -1
  18. package/src/config/parseStoreConfig.test-d.ts +31 -5
  19. package/src/config/parseStoreConfig.ts +218 -78
  20. package/src/config/validation.ts +25 -0
  21. package/src/config/world/index.ts +4 -0
  22. package/src/config/{loadWorldConfig.test-d.ts → world/loadWorldConfig.test-d.ts} +3 -3
  23. package/src/config/world/loadWorldConfig.ts +26 -0
  24. package/src/config/world/parseWorldConfig.ts +55 -0
  25. package/src/config/world/resolveWorldConfig.ts +80 -0
  26. package/src/config/world/userTypes.ts +72 -0
  27. package/src/index.ts +4 -6
  28. package/src/render-solidity/common.ts +51 -6
  29. package/src/render-solidity/field.ts +40 -44
  30. package/src/render-solidity/index.ts +5 -1
  31. package/src/render-solidity/record.ts +56 -73
  32. package/src/render-solidity/renderSystemInterface.ts +31 -0
  33. package/src/render-solidity/renderTable.ts +98 -70
  34. package/src/render-solidity/renderTypeHelpers.ts +99 -0
  35. package/src/render-solidity/renderTypesFromConfig.ts +2 -2
  36. package/src/render-solidity/renderWorld.ts +24 -0
  37. package/src/render-solidity/{renderTablesFromConfig.ts → tableOptions.ts} +28 -30
  38. package/src/render-solidity/tablegen.ts +20 -22
  39. package/src/render-solidity/types.ts +39 -5
  40. package/src/render-solidity/userType.ts +80 -48
  41. package/src/render-solidity/worldgen.ts +60 -0
  42. package/src/utils/contractToInterface.ts +130 -0
  43. package/src/utils/deploy-v2.ts +268 -101
  44. package/src/utils/formatAndWrite.ts +12 -0
  45. package/src/utils/getChainId.ts +10 -0
  46. package/src/utils/typeUtils.ts +17 -0
  47. package/dist/chunk-AER7UDD4.js +0 -0
  48. package/dist/chunk-XRS7KWBZ.js +0 -547
  49. package/dist/chunk-YZATC2M3.js +0 -397
  50. package/dist/chunk-ZYDMYSTH.js +0 -1178
  51. package/dist/deploy-v2-b7b3207d.d.ts +0 -92
  52. package/src/config/loadWorldConfig.ts +0 -178
  53. package/src/constants.ts +0 -1
@@ -1,32 +1,89 @@
1
- import { SchemaType } from "@latticexyz/schema-type";
1
+ import { AbiType, AbiTypes, StaticAbiType, StaticAbiTypes } from "@latticexyz/schema-type";
2
2
  import { RefinementCtx, z, ZodIssueCode } from "zod";
3
- import { BaseRoute, ObjectName, OrdinaryRoute, StaticSchemaType, UserEnum, ValueName } from "./commonSchemas.js";
4
- import { getDuplicates } from "./validation.js";
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 TableName = ObjectName;
7
- const KeyName = ValueName;
8
- const ColumnName = ValueName;
9
- const UserEnumName = ObjectName;
7
+ const zTableName = zObjectName;
8
+ const zKeyName = zValueName;
9
+ const zColumnName = zValueName;
10
+ const zUserEnumName = zObjectName;
10
11
 
11
- // Fields can use SchemaType or one of user defined wrapper types
12
- const FieldData = z.union([z.nativeEnum(SchemaType), UserEnumName]);
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
- // Primary keys allow only static types, but allow static user defined types
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
- const Schema = z
19
- .record(ColumnName, FieldData)
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 TableDataFull = z
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: OrdinaryRoute.default("/tables"),
25
- route: BaseRoute.optional(),
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: PrimaryKeys,
29
- schema: 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 TableDataShorthand = FieldData.transform((fieldData) => {
43
- return TableDataFull.parse({
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 TablesRecord = z.record(TableName, z.union([TableDataShorthand, TableDataFull])).transform((tables) => {
51
- // default route depends on tableName
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.route ??= `/${tableName}`;
124
+ table.fileSelector ??= tableName;
55
125
 
56
126
  tables[tableName] = table;
57
127
  }
58
- return tables as Record<string, RequireKeys<typeof tables[string], "route">>;
128
+ return tables as Record<string, RequireKeys<(typeof tables)[string], "fileSelector">>;
59
129
  });
60
130
 
61
- const StoreConfigUnrefined = z.object({
62
- baseRoute: BaseRoute.default(""),
63
- storeImportPath: z.string().default("@latticexyz/store/src/"),
64
- tables: TablesRecord,
65
- userTypes: z
66
- .object({
67
- path: OrdinaryRoute.default("/types"),
68
- enums: z.record(UserEnumName, UserEnum).default({}),
69
- })
70
- .default({}),
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
- // finally validate global conditions
73
- export const StoreConfig = StoreConfigUnrefined.superRefine(validateStoreConfig);
167
+
168
+ /************************************************************************
169
+ *
170
+ * FINAL
171
+ *
172
+ ************************************************************************/
74
173
 
75
174
  // zod doesn't preserve doc comments
76
- export interface StoreUserConfig {
77
- /** The base route prefix for table ids. Default is "" (empty string) */
78
- baseRoute?: string;
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
- * - `SchemaType | userType` for a single-value table (aka ECS component).
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: Record<string, z.input<typeof FieldData> | FullTableConfig>;
91
- /** User-defined types that will be generated and may be used in table schemas instead of `SchemaType` */
92
- userTypes?: UserTypesConfig;
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
- interface FullTableConfig {
96
- /** Output directory path for the file. Default is "/tables" */
97
- directory?: string;
98
- /** Route is used to register the table and construct its id. The table id will be keccak256(concat(baseRoute,route)). Default is "/<tableName>" */
99
- route?: string;
100
- /** Make methods accept `tableId` argument instead of it being a hardcoded constant. Default is false */
101
- tableIdArgument?: boolean;
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
- interface UserTypesConfig {
113
- /** Path to the file where common types will be generated and imported from. Default is "/types" */
114
- path?: string;
115
- /** Enum names mapped to lists of their member names */
116
- enums?: Record<string, string[]>;
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
- export type StoreConfig = z.output<typeof StoreConfig>;
217
+ // finally validate global conditions
218
+ export const zStoreConfig = StoreConfigUnrefined.superRefine(validateStoreConfig);
120
219
 
121
- export async function parseStoreConfig(config: unknown) {
122
- return StoreConfig.parse(config);
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 userTypeNames = Object.keys(config.userTypes.enums);
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 and enum names must be globally unique: ${duplicateGlobalNames.join(", ")}`,
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
- validateIfUserType(userTypeNames, primaryKeyType, ctx);
259
+ validateStaticAbiOrUserType(staticUserTypeNames, primaryKeyType, ctx);
154
260
  }
155
261
  for (const fieldType of Object.values(table.schema)) {
156
- validateIfUserType(userTypeNames, fieldType, ctx);
262
+ validateAbiOrUserType(userTypeNames, staticUserTypeNames, fieldType, ctx);
157
263
  }
158
264
  }
159
265
  }
160
266
 
161
- function validateIfUserType(
267
+ function validateAbiOrUserType(
162
268
  userTypeNames: string[],
163
- type: z.output<typeof FieldData> | z.output<typeof PrimaryKey>,
269
+ staticUserTypeNames: string[],
270
+ type: string,
164
271
  ctx: RefinementCtx
165
272
  ) {
166
- if (typeof type === "string" && !userTypeNames.includes(type)) {
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: `User type ${type} is not defined in userTypes`,
290
+ message: `${type} is not a static type`,
170
291
  });
171
292
  }
172
293
  }
173
294
 
174
- type RequireKeys<T extends Record<string, unknown>, P extends string> = T & Required<Pick<T, P>>;
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
+ }
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./loadWorldConfig.js";
2
+ export * from "./parseWorldConfig.js";
3
+ export * from "./resolveWorldConfig.js";
4
+ export * from "./userTypes.js";
@@ -1,11 +1,11 @@
1
1
  import { describe, expectTypeOf } from "vitest";
2
2
  import { z } from "zod";
3
- import { WorldConfig, WorldUserConfig } from "./loadWorldConfig.js";
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 WorldConfig>>();
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 WorldConfig>["overrideSystems"]>();
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
+ }