@latticexyz/cli 1.40.0 → 1.41.1-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/LICENSE +21 -0
- package/dist/chunk-ATAWDHWC.js +67 -0
- package/dist/{chunk-6AQ6LFVZ.js → chunk-J4DJQNIC.js} +743 -103
- package/dist/chunk-KD354QKC.js +23039 -0
- package/dist/{chunk-S3V3XX7N.js → chunk-SLIMIO4Z.js} +1 -1
- package/dist/config/index.d.ts +746 -8
- package/dist/config/index.js +63 -17
- package/dist/index.d.ts +1 -2
- package/dist/index.js +14 -10
- package/dist/mud.js +1055 -49
- package/dist/utils/deprecated/index.js +2 -2
- package/dist/utils/index.d.ts +56 -7
- package/dist/utils/index.js +17 -3
- package/package.json +16 -11
- package/src/commands/deploy-v2.ts +96 -0
- package/src/commands/deprecated/call-system.ts +1 -1
- package/src/commands/deprecated/deploy-contracts.ts +1 -1
- package/src/commands/deprecated/test.ts +9 -6
- package/src/commands/deprecated/trace.ts +1 -1
- package/src/commands/gas-report.ts +1 -1
- package/src/commands/index.ts +4 -0
- package/src/commands/tablegen.ts +4 -18
- package/src/commands/worldgen.ts +55 -0
- package/src/config/commonSchemas.ts +19 -5
- package/src/config/dynamicResolution.ts +49 -0
- package/src/config/index.ts +20 -0
- package/src/config/loadStoreConfig.ts +3 -89
- package/src/config/parseStoreConfig.test-d.ts +40 -0
- package/src/config/parseStoreConfig.ts +314 -0
- package/src/config/validation.ts +71 -0
- package/src/config/world/index.ts +4 -0
- package/src/config/world/loadWorldConfig.test-d.ts +11 -0
- 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 +13 -5
- package/src/mud.ts +4 -0
- package/src/render-solidity/common.ts +138 -0
- package/src/render-solidity/field.ts +137 -0
- package/src/render-solidity/index.ts +10 -0
- package/src/render-solidity/record.ts +154 -0
- package/src/render-solidity/renderSystemInterface.ts +31 -0
- package/src/render-solidity/renderTable.ts +164 -0
- package/src/render-solidity/renderTypeHelpers.ts +99 -0
- package/src/render-solidity/renderTypes.ts +19 -0
- package/src/render-solidity/renderTypesFromConfig.ts +13 -0
- package/src/render-solidity/renderWorld.ts +24 -0
- package/src/{render-table/renderTablesFromConfig.ts → render-solidity/tableOptions.ts} +45 -37
- package/src/render-solidity/tablegen.ts +33 -0
- package/src/render-solidity/types.ts +110 -0
- package/src/render-solidity/userType.ts +132 -0
- package/src/render-solidity/worldgen.ts +60 -0
- package/src/utils/contractToInterface.ts +130 -0
- package/src/utils/deploy-v2.ts +512 -0
- package/src/utils/deprecated/build.ts +1 -1
- package/src/utils/deprecated/typegen.ts +1 -1
- package/src/utils/errors.ts +12 -2
- package/src/utils/execLog.ts +22 -0
- package/src/utils/formatAndWrite.ts +12 -0
- package/src/utils/foundry.ts +94 -0
- package/src/utils/getChainId.ts +10 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/typeUtils.ts +17 -0
- package/dist/chunk-B6VWCGHZ.js +0 -199
- package/dist/chunk-JKAA3WMC.js +0 -55
- package/dist/chunk-JNGSW4AP.js +0 -493
- package/dist/chunk-PJ6GS2R4.js +0 -22
- package/dist/chunk-UC3QPOON.js +0 -35
- package/dist/loadStoreConfig-37f99136.d.ts +0 -164
- package/dist/render-table/index.d.ts +0 -29
- package/dist/render-table/index.js +0 -24
- package/dist/renderTable-9e6410c5.d.ts +0 -72
- package/src/config/loadStoreConfig.test-d.ts +0 -11
- package/src/render-table/common.ts +0 -67
- package/src/render-table/field.ts +0 -132
- package/src/render-table/index.ts +0 -6
- package/src/render-table/record.ts +0 -176
- package/src/render-table/renderTable.ts +0 -109
- package/src/render-table/types.ts +0 -51
- package/src/utils/forgeConfig.ts +0 -45
|
@@ -1,95 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { z, ZodError } from "zod";
|
|
1
|
+
import { ZodError } from "zod";
|
|
3
2
|
import { fromZodErrorCustom } from "../utils/errors.js";
|
|
4
|
-
import { BaseRoute, ObjectName, OrdinaryRoute, ValueName } from "./commonSchemas.js";
|
|
5
3
|
import { loadConfig } from "./loadConfig.js";
|
|
6
|
-
|
|
7
|
-
const TableName = ObjectName;
|
|
8
|
-
const KeyName = ValueName;
|
|
9
|
-
const ColumnName = ValueName;
|
|
10
|
-
|
|
11
|
-
const PrimaryKey = z
|
|
12
|
-
.nativeEnum(SchemaType)
|
|
13
|
-
.refine((arg) => getStaticByteLength(arg) > 0, "Primary key must not use dynamic SchemaType");
|
|
14
|
-
const PrimaryKeys = z.record(KeyName, PrimaryKey).default({ key: SchemaType.BYTES32 });
|
|
15
|
-
|
|
16
|
-
const Schema = z
|
|
17
|
-
.record(ColumnName, z.nativeEnum(SchemaType))
|
|
18
|
-
.refine((arg) => Object.keys(arg).length > 0, "Table schema may not be empty");
|
|
19
|
-
|
|
20
|
-
const FullTable = z
|
|
21
|
-
.object({
|
|
22
|
-
route: OrdinaryRoute.default("/tables"),
|
|
23
|
-
tableIdArgument: z.boolean().default(false),
|
|
24
|
-
storeArgument: z.boolean().default(false),
|
|
25
|
-
primaryKeys: PrimaryKeys,
|
|
26
|
-
schema: Schema,
|
|
27
|
-
dataStruct: z.boolean().optional(),
|
|
28
|
-
})
|
|
29
|
-
.transform((arg) => {
|
|
30
|
-
// default dataStruct value depends on schema's length
|
|
31
|
-
if (Object.keys(arg.schema).length === 1) {
|
|
32
|
-
arg.dataStruct ??= false;
|
|
33
|
-
} else {
|
|
34
|
-
arg.dataStruct ??= true;
|
|
35
|
-
}
|
|
36
|
-
return arg as Omit<typeof arg, "dataStruct"> & Required<Pick<typeof arg, "dataStruct">>;
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const DefaultSingleValueTable = z.nativeEnum(SchemaType).transform((schemaType) => {
|
|
40
|
-
return FullTable.parse({
|
|
41
|
-
schema: {
|
|
42
|
-
value: schemaType,
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
export const StoreConfig = z.object({
|
|
48
|
-
baseRoute: BaseRoute.default(""),
|
|
49
|
-
storeImportPath: z.string().default("@latticexyz/store/src/"),
|
|
50
|
-
tables: z.record(TableName, z.union([DefaultSingleValueTable, FullTable])),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// zod doesn't preserve doc comments
|
|
54
|
-
export interface StoreUserConfig {
|
|
55
|
-
/** The base route prefix for table ids. Default is "" (empty string) */
|
|
56
|
-
baseRoute?: string;
|
|
57
|
-
/** Path for store package imports. Default is "@latticexyz/store/src/" */
|
|
58
|
-
storeImportPath?: string;
|
|
59
|
-
/**
|
|
60
|
-
* Configuration for each table.
|
|
61
|
-
*
|
|
62
|
-
* The key is the table name (capitalized).
|
|
63
|
-
*
|
|
64
|
-
* The value:
|
|
65
|
-
* - SchemaType for a single-value table (aka ECS component).
|
|
66
|
-
* - FullTableConfig object for multi-value tables (or for customizable options).
|
|
67
|
-
*/
|
|
68
|
-
tables: Record<string, SchemaType | FullTableConfig>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface FullTableConfig {
|
|
72
|
-
/** Output path for the file, and relevant for the table id. The table id will be keccak256(concat(baseRoute,route,tableName)). Default is "tables/" */
|
|
73
|
-
route?: string;
|
|
74
|
-
/** Make methods accept `tableId` argument instead of it being a hardcoded constant. Default is false */
|
|
75
|
-
tableIdArgument?: boolean;
|
|
76
|
-
/** Include methods that accept a manual `IStore` argument. Default is false. */
|
|
77
|
-
storeArgument?: boolean;
|
|
78
|
-
/** Include a data struct and methods for it. Default is false for 1-column tables; true for multi-column tables. */
|
|
79
|
-
dataStruct?: boolean;
|
|
80
|
-
/** Table's primary key names mapped to their types. Default is `{ key: SchemaType.BYTES32 }` */
|
|
81
|
-
primaryKeys?: Record<string, SchemaType>;
|
|
82
|
-
/** Table's column names mapped to their types. Table name's 1st letter should be lowercase. */
|
|
83
|
-
schema: Record<string, SchemaType>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export type StoreConfig = z.output<typeof StoreConfig>;
|
|
4
|
+
import { parseStoreConfig } from "./parseStoreConfig.js";
|
|
87
5
|
|
|
88
6
|
export async function loadStoreConfig(configPath?: string) {
|
|
89
7
|
const config = await loadConfig(configPath);
|
|
90
8
|
|
|
91
9
|
try {
|
|
92
|
-
return parseStoreConfig(config);
|
|
10
|
+
return parseStoreConfig(config as any);
|
|
93
11
|
} catch (error) {
|
|
94
12
|
if (error instanceof ZodError) {
|
|
95
13
|
throw fromZodErrorCustom(error, "StoreConfig Validation Error");
|
|
@@ -98,7 +16,3 @@ export async function loadStoreConfig(configPath?: string) {
|
|
|
98
16
|
}
|
|
99
17
|
}
|
|
100
18
|
}
|
|
101
|
-
|
|
102
|
-
export async function parseStoreConfig(config: unknown) {
|
|
103
|
-
return StoreConfig.parse(config);
|
|
104
|
-
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expectTypeOf } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { storeConfig, zStoreConfig, StoreUserConfig } from "./parseStoreConfig.js";
|
|
4
|
+
|
|
5
|
+
describe("StoreUserConfig", () => {
|
|
6
|
+
// Typecheck manual interfaces against zod
|
|
7
|
+
expectTypeOf<StoreUserConfig>().toEqualTypeOf<z.input<typeof zStoreConfig>>();
|
|
8
|
+
// type equality isn't deep for optionals
|
|
9
|
+
expectTypeOf<StoreUserConfig["tables"][string]>().toEqualTypeOf<z.input<typeof zStoreConfig>["tables"][string]>();
|
|
10
|
+
expectTypeOf<NonNullable<StoreUserConfig["enums"]>[string]>().toEqualTypeOf<
|
|
11
|
+
NonNullable<NonNullable<z.input<typeof zStoreConfig>>["enums"]>[string]
|
|
12
|
+
>();
|
|
13
|
+
// TODO If more nested schemas are added, provide separate tests for them
|
|
14
|
+
|
|
15
|
+
// Test possible inference confusion.
|
|
16
|
+
// This would fail if you remove `AsDependent` from `StoreUserConfig`
|
|
17
|
+
expectTypeOf(
|
|
18
|
+
storeConfig({
|
|
19
|
+
tables: {
|
|
20
|
+
Table1: {
|
|
21
|
+
primaryKeys: {
|
|
22
|
+
a: "Enum1",
|
|
23
|
+
},
|
|
24
|
+
schema: {
|
|
25
|
+
b: "Enum2",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
Table2: {
|
|
29
|
+
schema: {
|
|
30
|
+
a: "uint32",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
enums: {
|
|
35
|
+
Enum1: ["E1"],
|
|
36
|
+
Enum2: ["E1"],
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
).toEqualTypeOf<StoreUserConfig<"Enum1" | "Enum2", "Enum1" | "Enum2">>();
|
|
40
|
+
});
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { AbiType, AbiTypes, StaticAbiType, StaticAbiTypes } from "@latticexyz/schema-type";
|
|
2
|
+
import { RefinementCtx, z, ZodIssueCode } from "zod";
|
|
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";
|
|
6
|
+
|
|
7
|
+
const zTableName = zObjectName;
|
|
8
|
+
const zKeyName = zValueName;
|
|
9
|
+
const zColumnName = zValueName;
|
|
10
|
+
const zUserEnumName = zObjectName;
|
|
11
|
+
|
|
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();
|
|
15
|
+
|
|
16
|
+
type FieldData<UserTypes extends StringForUnion> = AbiType | StaticArray | UserTypes;
|
|
17
|
+
|
|
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)
|
|
39
|
+
.refine((arg) => Object.keys(arg).length > 0, "Table schema may not be empty");
|
|
40
|
+
|
|
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
|
|
80
|
+
.object({
|
|
81
|
+
directory: z.string().default("tables"),
|
|
82
|
+
fileSelector: zSelector.optional(),
|
|
83
|
+
tableIdArgument: z.boolean().default(false),
|
|
84
|
+
storeArgument: z.boolean().default(false),
|
|
85
|
+
primaryKeys: zPrimaryKeys,
|
|
86
|
+
schema: zSchemaConfig,
|
|
87
|
+
dataStruct: z.boolean().optional(),
|
|
88
|
+
})
|
|
89
|
+
.transform((arg) => {
|
|
90
|
+
// default dataStruct value depends on schema's length
|
|
91
|
+
if (Object.keys(arg.schema).length === 1) {
|
|
92
|
+
arg.dataStruct ??= false;
|
|
93
|
+
} else {
|
|
94
|
+
arg.dataStruct ??= true;
|
|
95
|
+
}
|
|
96
|
+
return arg as RequireKeys<typeof arg, "dataStruct">;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const zShorthandTableConfig = zFieldData.transform((fieldData) => {
|
|
100
|
+
return zFullTableConfig.parse({
|
|
101
|
+
schema: {
|
|
102
|
+
value: fieldData,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
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
|
|
122
|
+
for (const tableName of Object.keys(tables)) {
|
|
123
|
+
const table = tables[tableName];
|
|
124
|
+
table.fileSelector ??= tableName;
|
|
125
|
+
|
|
126
|
+
tables[tableName] = table;
|
|
127
|
+
}
|
|
128
|
+
return tables as Record<string, RequireKeys<(typeof tables)[string], "fileSelector">>;
|
|
129
|
+
});
|
|
130
|
+
|
|
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({}),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
/************************************************************************
|
|
169
|
+
*
|
|
170
|
+
* FINAL
|
|
171
|
+
*
|
|
172
|
+
************************************************************************/
|
|
173
|
+
|
|
174
|
+
// zod doesn't preserve doc comments
|
|
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;
|
|
181
|
+
/** Path for store package imports. Default is "@latticexyz/store/src/" */
|
|
182
|
+
storeImportPath?: string;
|
|
183
|
+
/**
|
|
184
|
+
* Configuration for each table.
|
|
185
|
+
*
|
|
186
|
+
* The key is the table name (capitalized).
|
|
187
|
+
*
|
|
188
|
+
* The value:
|
|
189
|
+
* - abi or user type for a single-value table.
|
|
190
|
+
* - FullTableConfig object for multi-value tables (or for customizable options).
|
|
191
|
+
*/
|
|
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
|
+
};
|
|
196
|
+
|
|
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;
|
|
204
|
+
}
|
|
205
|
+
|
|
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);
|
|
216
|
+
|
|
217
|
+
// finally validate global conditions
|
|
218
|
+
export const zStoreConfig = StoreConfigUnrefined.superRefine(validateStoreConfig);
|
|
219
|
+
|
|
220
|
+
export function parseStoreConfig(config: unknown) {
|
|
221
|
+
return zStoreConfig.parse(config);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/************************************************************************
|
|
225
|
+
*
|
|
226
|
+
* HELPERS
|
|
227
|
+
*
|
|
228
|
+
************************************************************************/
|
|
229
|
+
|
|
230
|
+
// Validate conditions that check multiple different config options simultaneously
|
|
231
|
+
function validateStoreConfig(config: z.output<typeof StoreConfigUnrefined>, ctx: RefinementCtx) {
|
|
232
|
+
// Local table variables must be unique within the table
|
|
233
|
+
for (const table of Object.values(config.tables)) {
|
|
234
|
+
const primaryKeyNames = Object.keys(table.primaryKeys);
|
|
235
|
+
const fieldNames = Object.keys(table.schema);
|
|
236
|
+
const duplicateVariableNames = getDuplicates([...primaryKeyNames, ...fieldNames]);
|
|
237
|
+
if (duplicateVariableNames.length > 0) {
|
|
238
|
+
ctx.addIssue({
|
|
239
|
+
code: ZodIssueCode.custom,
|
|
240
|
+
message: `Field and primary key names within one table must be unique: ${duplicateVariableNames.join(", ")}`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Global names must be unique
|
|
245
|
+
const tableNames = Object.keys(config.tables);
|
|
246
|
+
const staticUserTypeNames = Object.keys(config.enums);
|
|
247
|
+
const userTypeNames = staticUserTypeNames;
|
|
248
|
+
const globalNames = [...tableNames, ...userTypeNames];
|
|
249
|
+
const duplicateGlobalNames = getDuplicates(globalNames);
|
|
250
|
+
if (duplicateGlobalNames.length > 0) {
|
|
251
|
+
ctx.addIssue({
|
|
252
|
+
code: ZodIssueCode.custom,
|
|
253
|
+
message: `Table, enum names must be globally unique: ${duplicateGlobalNames.join(", ")}`,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
// User types must exist
|
|
257
|
+
for (const table of Object.values(config.tables)) {
|
|
258
|
+
for (const primaryKeyType of Object.values(table.primaryKeys)) {
|
|
259
|
+
validateStaticAbiOrUserType(staticUserTypeNames, primaryKeyType, ctx);
|
|
260
|
+
}
|
|
261
|
+
for (const fieldType of Object.values(table.schema)) {
|
|
262
|
+
validateAbiOrUserType(userTypeNames, staticUserTypeNames, fieldType, ctx);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function validateAbiOrUserType(
|
|
268
|
+
userTypeNames: string[],
|
|
269
|
+
staticUserTypeNames: string[],
|
|
270
|
+
type: string,
|
|
271
|
+
ctx: RefinementCtx
|
|
272
|
+
) {
|
|
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)) {
|
|
288
|
+
ctx.addIssue({
|
|
289
|
+
code: ZodIssueCode.custom,
|
|
290
|
+
message: `${type} is not a static type`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
1
2
|
import { ZodIssueCode, RefinementCtx } from "zod";
|
|
2
3
|
|
|
3
4
|
export function validateName(name: string, ctx: RefinementCtx) {
|
|
@@ -31,6 +32,30 @@ export function validateUncapitalizedName(name: string, ctx: RefinementCtx) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
// validates only the enum array, not the names of enum members
|
|
36
|
+
export function validateEnum(members: string[], ctx: RefinementCtx) {
|
|
37
|
+
if (members.length === 0) {
|
|
38
|
+
ctx.addIssue({
|
|
39
|
+
code: ZodIssueCode.custom,
|
|
40
|
+
message: `Enum must not be empty`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (members.length >= 256) {
|
|
44
|
+
ctx.addIssue({
|
|
45
|
+
code: ZodIssueCode.custom,
|
|
46
|
+
message: `Length of enum must be < 256`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const duplicates = getDuplicates(members);
|
|
51
|
+
if (duplicates.length > 0) {
|
|
52
|
+
ctx.addIssue({
|
|
53
|
+
code: ZodIssueCode.custom,
|
|
54
|
+
message: `Enum must not have duplicate names for: ${duplicates.join(", ")}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
function _factoryForValidateRoute(requireNonEmpty: boolean, requireSingleLevel: boolean) {
|
|
35
60
|
return (route: string, ctx: RefinementCtx) => {
|
|
36
61
|
if (route === "") {
|
|
@@ -90,3 +115,49 @@ export const validateRoute = _factoryForValidateRoute(true, false);
|
|
|
90
115
|
export const validateBaseRoute = _factoryForValidateRoute(false, false);
|
|
91
116
|
|
|
92
117
|
export const validateSingleLevelRoute = _factoryForValidateRoute(true, true);
|
|
118
|
+
|
|
119
|
+
export function validateEthereumAddress(address: string, ctx: RefinementCtx) {
|
|
120
|
+
if (!ethers.utils.isAddress(address)) {
|
|
121
|
+
ctx.addIssue({
|
|
122
|
+
code: ZodIssueCode.custom,
|
|
123
|
+
message: `Address must be a valid Ethereum address`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getDuplicates<T>(array: T[]) {
|
|
129
|
+
const checked = new Set<T>();
|
|
130
|
+
const duplicates = new Set<T>();
|
|
131
|
+
for (const element of array) {
|
|
132
|
+
if (checked.has(element)) {
|
|
133
|
+
duplicates.add(element);
|
|
134
|
+
}
|
|
135
|
+
checked.add(element);
|
|
136
|
+
}
|
|
137
|
+
return [...duplicates];
|
|
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,11 @@
|
|
|
1
|
+
import { describe, expectTypeOf } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { zWorldConfig, WorldUserConfig } from "./index.js";
|
|
4
|
+
|
|
5
|
+
describe("loadWorldConfig", () => {
|
|
6
|
+
// Typecheck manual interfaces against zod
|
|
7
|
+
expectTypeOf<WorldUserConfig>().toEqualTypeOf<z.input<typeof zWorldConfig>>();
|
|
8
|
+
// type equality isn't deep for optionals
|
|
9
|
+
expectTypeOf<WorldUserConfig["overrideSystems"]>().toEqualTypeOf<z.input<typeof zWorldConfig>["overrideSystems"]>();
|
|
10
|
+
// TODO If more nested schemas are added, provide separate tests for them
|
|
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>;
|