diff --git a/packages/store/ts/codegen/tableOptions.ts b/packages/store/ts/codegen/tableOptions.ts index fe66118099..ab26f98a83 100644 --- a/packages/store/ts/codegen/tableOptions.ts +++ b/packages/store/ts/codegen/tableOptions.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "fs"; import path from "path"; import { SchemaTypeArrayToElement } from "@latticexyz/schema-type/deprecated"; import { @@ -6,6 +7,8 @@ import { RenderField, RenderKeyTuple, RenderStaticField, + SolidityUserDefinedType, + extractUserTypes, } from "@latticexyz/common/codegen"; import { RenderTableOptions } from "./types"; import { StoreConfig } from "../config"; @@ -19,6 +22,7 @@ export interface TableOptions { export function getTableOptions(config: StoreConfig): TableOptions[] { const storeImportPath = config.storeImportPath; + const solidityUserTypes = loadAndExtractUserTypes(config.userTypes); const options = []; for (const tableName of Object.keys(config.tables)) { @@ -35,9 +39,9 @@ export function getTableOptions(config: StoreConfig): TableOptions[] { const keyTuple = Object.keys(tableData.keySchema).map((name) => { const abiOrUserType = tableData.keySchema[name]; - const { renderType } = resolveAbiOrUserType(abiOrUserType, config); + const { renderType } = resolveAbiOrUserType(abiOrUserType, config, solidityUserTypes); - const importDatum = importForAbiOrUserType(abiOrUserType, tableData.directory, config); + const importDatum = importForAbiOrUserType(abiOrUserType, tableData.directory, config, solidityUserTypes); if (importDatum) imports.push(importDatum); if (renderType.isDynamic) throw new Error(`Parsing error: found dynamic key ${name} in table ${tableName}`); @@ -52,9 +56,9 @@ export function getTableOptions(config: StoreConfig): TableOptions[] { const fields = Object.keys(tableData.valueSchema).map((name) => { const abiOrUserType = tableData.valueSchema[name]; - const { renderType, schemaType } = resolveAbiOrUserType(abiOrUserType, config); + const { renderType, schemaType } = resolveAbiOrUserType(abiOrUserType, config, solidityUserTypes); - const importDatum = importForAbiOrUserType(abiOrUserType, tableData.directory, config); + const importDatum = importForAbiOrUserType(abiOrUserType, tableData.directory, config, solidityUserTypes); if (importDatum) imports.push(importDatum); const elementType = SchemaTypeArrayToElement[schemaType]; @@ -106,3 +110,19 @@ export function getTableOptions(config: StoreConfig): TableOptions[] { } return options; } + +function loadAndExtractUserTypes(userTypes: StoreConfig["userTypes"]) { + const userTypesPerFile: Record = {}; + for (const [userTypeName, filePath] of Object.entries(userTypes)) { + if (!(filePath in userTypesPerFile)) { + userTypesPerFile[filePath] = []; + } + userTypesPerFile[filePath].push(userTypeName); + } + let extractedUserTypes: Record = {}; + for (const [filePath, userTypeNames] of Object.entries(userTypesPerFile)) { + const data = readFileSync(filePath, "utf8"); + extractedUserTypes = Object.assign(userTypes, extractUserTypes(data, userTypeNames, filePath)); + } + return extractedUserTypes; +} diff --git a/packages/store/ts/codegen/userType.ts b/packages/store/ts/codegen/userType.ts index f22f2664de..db95f3c949 100644 --- a/packages/store/ts/codegen/userType.ts +++ b/packages/store/ts/codegen/userType.ts @@ -5,7 +5,7 @@ import { SchemaTypeToAbiType, } from "@latticexyz/schema-type/deprecated"; import { parseStaticArray } from "@latticexyz/config"; -import { RelativeImportDatum, RenderType } from "@latticexyz/common/codegen"; +import { RelativeImportDatum, RenderType, SolidityUserDefinedType } from "@latticexyz/common/codegen"; import { StoreConfig } from "../config"; export type UserTypeInfo = ReturnType; @@ -15,7 +15,8 @@ export type UserTypeInfo = ReturnType; */ export function resolveAbiOrUserType( abiOrUserType: string, - config: StoreConfig + config: StoreConfig, + solidityUserTypes: Record ): { schemaType: SchemaType; renderType: RenderType; @@ -38,7 +39,7 @@ export function resolveAbiOrUserType( } } // user types - return getUserTypeInfo(abiOrUserType, config); + return getUserTypeInfo(abiOrUserType, config, solidityUserTypes); } /** @@ -47,7 +48,8 @@ export function resolveAbiOrUserType( export function importForAbiOrUserType( abiOrUserType: string, usedInDirectory: string, - config: StoreConfig + config: StoreConfig, + solidityUserTypes: Record ): RelativeImportDatum | undefined { // abi types which directly mirror a SchemaType if (abiOrUserType in AbiTypeToSchemaType) { @@ -58,7 +60,17 @@ export function importForAbiOrUserType( if (staticArray) { return undefined; } - // user types + // user-defined types in a user-provided file + if (abiOrUserType in solidityUserTypes) { + // these types can have a library name as their import symbol + const solidityUserType = solidityUserTypes[abiOrUserType]; + return { + symbol: solidityUserType.importSymbol, + fromPath: solidityUserType.fromPath, + usedInPath: usedInDirectory, + }; + } + // other user types return { symbol: abiOrUserType, fromPath: config.userTypesFilename, @@ -84,7 +96,8 @@ export function getSchemaTypeInfo(schemaType: SchemaType): RenderType { export function getUserTypeInfo( userType: string, - config: StoreConfig + config: StoreConfig, + solidityUserTypes: Record ): { schemaType: SchemaType; renderType: RenderType; @@ -109,6 +122,27 @@ export function getUserTypeInfo( }, }; } + // user-defined types + if (userType in solidityUserTypes) { + if (!(userType in solidityUserTypes)) { + throw new Error(`User type "${userType}" not found in MUD config`); + } + const solidityUserType = solidityUserTypes[userType]; + const schemaType = AbiTypeToSchemaType[solidityUserType.typeId]; + return { + schemaType, + renderType: { + typeId: solidityUserType.typeId, + typeWithLocation: solidityUserType.typeId, + enumName: SchemaType[schemaType], + staticByteLength: getStaticByteLength(schemaType), + isDynamic: false, + typeWrap: `${userType}.wrap`, + typeUnwrap: `${userType}.unwrap`, + internalTypeId: `${solidityUserType.internalTypeId}`, + }, + }; + } // invalid throw new Error(`User type "${userType}" does not exist`); } diff --git a/packages/store/ts/config/defaults.ts b/packages/store/ts/config/defaults.ts index 0393830547..0a00b2361f 100644 --- a/packages/store/ts/config/defaults.ts +++ b/packages/store/ts/config/defaults.ts @@ -8,6 +8,7 @@ export const PATH_DEFAULTS = { export const DEFAULTS = { namespace: "", enums: {} as Record, + userTypes: {} as Record, } as const; export const TABLE_DEFAULTS = { diff --git a/packages/store/ts/config/storeConfig.test-d.ts b/packages/store/ts/config/storeConfig.test-d.ts index f31cd59f21..be99051fec 100644 --- a/packages/store/ts/config/storeConfig.test-d.ts +++ b/packages/store/ts/config/storeConfig.test-d.ts @@ -11,5 +11,8 @@ describe("StoreUserConfig", () => { expectTypeOf[string]>().toEqualTypeOf< NonNullable>["enums"]>[string] >(); + expectTypeOf[string]>().toEqualTypeOf< + NonNullable>["userTypes"]>[string] + >(); // TODO If more nested schemas are added, provide separate tests for them }); diff --git a/packages/store/ts/config/storeConfig.ts b/packages/store/ts/config/storeConfig.ts index 2795a266ff..3dd4fe6e46 100644 --- a/packages/store/ts/config/storeConfig.ts +++ b/packages/store/ts/config/storeConfig.ts @@ -26,6 +26,7 @@ const zTableName = zObjectName; const zKeyName = zValueName; const zColumnName = zValueName; const zUserEnumName = zObjectName; +const zUserTypeName = zObjectName; // Fields can use AbiType or one of user-defined wrapper types // (user types are refined later, based on the appropriate config options) @@ -194,7 +195,7 @@ export type ExpandTablesConfig> = { /************************************************************************ * - * USER TYPES + * ENUMS * ************************************************************************/ @@ -233,6 +234,47 @@ export const zEnumsConfig = z.object({ enums: z.record(zUserEnumName, zUserEnum).default(DEFAULTS.enums), }); +/************************************************************************ + * + * USER TYPES + * + ************************************************************************/ + +export type UserTypesConfig = never extends UserTypeNames + ? { + /** + * User types mapped to file paths from which to import them + * + * (user types are inferred to be absent) + */ + userTypes?: Record; + } + : StringForUnion extends UserTypeNames + ? { + /** + * User types mapped to file paths from which to import them + * + * (user types aren't inferred - use `mudConfig` or `storeConfig` helper, and `as const` for variables) + */ + userTypes?: Record; + } + : { + /** + * User types mapped to file paths from which to import them + * + * User types defined here can be used as types in table schemas/keys + */ + userTypes: Record; + }; + +export type FullUserTypesConfig = { + userTypes: Record; +}; + +export const zUserTypesConfig = z.object({ + userTypes: z.record(zUserTypeName, z.string()).default(DEFAULTS.userTypes), +}); + /************************************************************************ * * FINAL @@ -244,9 +286,11 @@ export const zEnumsConfig = z.object({ export type MUDUserConfig< T extends MUDCoreUserConfig = MUDCoreUserConfig, EnumNames extends StringForUnion = StringForUnion, - StaticUserTypes extends ExtractUserTypes = ExtractUserTypes + UserTypeNames extends StringForUnion = StringForUnion, + StaticUserTypes extends ExtractUserTypes = ExtractUserTypes > = T & - EnumsConfig & { + EnumsConfig & + UserTypesConfig & { /** * Configuration for each table. * @@ -278,7 +322,8 @@ const StoreConfigUnrefined = z codegenDirectory: z.string().default(PATH_DEFAULTS.codegenDirectory), codegenIndexFilename: z.string().default(PATH_DEFAULTS.codegenIndexFilename), }) - .merge(zEnumsConfig); + .merge(zEnumsConfig) + .merge(zUserTypesConfig); // finally validate global conditions export const zStoreConfig = StoreConfigUnrefined.superRefine(validateStoreConfig); @@ -311,14 +356,16 @@ function validateStoreConfig(config: z.output, ctx: } // Global names must be unique const tableLibraryNames = Object.keys(config.tables); - const staticUserTypeNames = Object.keys(config.enums); + const staticUserTypeNames = [...Object.keys(config.enums), ...Object.keys(config.userTypes)]; const userTypeNames = staticUserTypeNames; const globalNames = [...tableLibraryNames, ...userTypeNames]; const duplicateGlobalNames = getDuplicates(globalNames); if (duplicateGlobalNames.length > 0) { ctx.addIssue({ code: ZodIssueCode.custom, - message: `Table library names, enum names must be globally unique: ${duplicateGlobalNames.join(", ")}`, + message: `Table library names, enum names, user type names must be globally unique: ${duplicateGlobalNames.join( + ", " + )}`, }); } // Table names used for tableId must be unique diff --git a/packages/store/ts/register/mudConfig.test-d.ts b/packages/store/ts/register/mudConfig.test-d.ts index e310c93804..3252b86fa0 100644 --- a/packages/store/ts/register/mudConfig.test-d.ts +++ b/packages/store/ts/register/mudConfig.test-d.ts @@ -33,6 +33,7 @@ describe("mudConfig", () => { Enum1: ["E1"]; Enum2: ["E1"]; }; + userTypes: Record; tables: { Table1: { keySchema: { diff --git a/packages/store/ts/register/mudConfig.ts b/packages/store/ts/register/mudConfig.ts index afc08f9dd5..907ef5fb1c 100644 --- a/packages/store/ts/register/mudConfig.ts +++ b/packages/store/ts/register/mudConfig.ts @@ -8,8 +8,9 @@ export function mudConfig< T extends MUDCoreUserConfig, // (`never` is overridden by inference, so only the defined enums can be used by default) EnumNames extends StringForUnion = never, - StaticUserTypes extends ExtractUserTypes = ExtractUserTypes ->(config: MUDUserConfig): ExpandMUDUserConfig { + UserTypeNames extends StringForUnion = never, + StaticUserTypes extends ExtractUserTypes = ExtractUserTypes +>(config: MUDUserConfig): ExpandMUDUserConfig { // eslint-disable-next-line @typescript-eslint/no-explicit-any return mudCoreConfig(config) as any; } diff --git a/packages/store/ts/register/typeExtensions.ts b/packages/store/ts/register/typeExtensions.ts index e0bb8d20cb..f425ab4113 100644 --- a/packages/store/ts/register/typeExtensions.ts +++ b/packages/store/ts/register/typeExtensions.ts @@ -25,6 +25,7 @@ export interface ExpandMUDUserConfig T, { enums: typeof DEFAULTS.enums; + userTypes: typeof DEFAULTS.userTypes; namespace: typeof DEFAULTS.namespace; storeImportPath: typeof PATH_DEFAULTS.storeImportPath; userTypesFilename: typeof PATH_DEFAULTS.userTypesFilename;