diff --git a/packages/config/package.json b/packages/config/package.json index 364ad15bea..b3669839bd 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -38,10 +38,12 @@ "esbuild": "^0.17.15", "ethers": "^5.7.2", "find-up": "^6.3.0", + "tapable": "^2.2.1", "zod": "^3.21.4", "zod-validation-error": "^1.3.0" }, "devDependencies": { + "@types/tapable": "^2.2.3", "tsup": "^6.7.0" }, "gitHead": "914a1e0ae4a573d685841ca2ea921435057deb8f" diff --git a/packages/config/src/library/core.ts b/packages/config/src/library/core.ts index e9e1fc990a..b42f2084e0 100644 --- a/packages/config/src/library/core.ts +++ b/packages/config/src/library/core.ts @@ -1,12 +1,17 @@ import { MergeReturnType, UnionToIntersection } from "@latticexyz/common/type-utils"; -import { Plugins } from "./types"; +import { MudPlugin, Plugins } from "./types"; // Helper type to infer the input types from a plugins config as union (InputA | InputB) -type PluginsInput

= Parameters[0]; +type PluginsInput

= Parameters[1]; // Infer the plugin input types as intersection (InputA & InputB) export type MergedPluginsInput

= UnionToIntersection>; +// Helper to strongly type a plugin definition +export function defineMUDPlugin

(plugin: P): P { + return plugin; +} + /** * Helper function to sequentially apply `expandConfig` of each plugin and strongly type the result. */ @@ -16,7 +21,7 @@ export function mudCoreConfig

{ let expanded = config as any; for (const { expandConfig } of Object.values(plugins)) { - expanded = { ...expanded, ...expandConfig(config) }; + expanded = { ...expanded, ...expandConfig(plugins, config) }; } return expanded; } diff --git a/packages/config/src/library/types.ts b/packages/config/src/library/types.ts index 7acbc65db0..7eb629cc87 100644 --- a/packages/config/src/library/types.ts +++ b/packages/config/src/library/types.ts @@ -1,11 +1,14 @@ +import { Hook } from "tapable"; + /* * Every plugin defines an `Input`, an `Expanded` type, * and a `expandConfig` function to map from `Input` to `Expanded`. - * Do distinguish plugins from each other in TypeScript, they also define a unique `id` string. + * To distinguish plugins from each other in TypeScript, they also define a unique `id` string. */ export interface MudPlugin { id: string; - expandConfig: (config: C) => Expanded; + expandConfig: (plugins: Plugins, config: C) => Expanded; + hooks: Record>; } /** @@ -13,7 +16,7 @@ export interface MudPlugin { * The config is later expanded by calling the expandConfig method of each * plugin in order of appearance in the map. We use a map instead of an array, * because it makes it easier to type check for the existence of expected - * plugins in the map. (Object keys order is guaranteed since ES2015, see + * plugins in the map. Object keys order is guaranteed since ES2015, see * https://www.stefanjudis.com/today-i-learned/property-order-is-predictable-in-javascript-objects-since-es2015/ */ export type Config = { plugins: Plugins }; diff --git a/packages/store/package.json b/packages/store/package.json index 9abca9b640..5e15c5828a 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -53,6 +53,7 @@ "@latticexyz/config": "workspace:*", "@latticexyz/schema-type": "workspace:*", "ethers": "^5.7.2", + "tapable": "^2.2.1", "zod": "^3.21.4" }, "devDependencies": { @@ -60,6 +61,7 @@ "@types/ejs": "^3.1.1", "@types/mocha": "^9.1.1", "@types/node": "^18.15.11", + "@types/tapable": "^2.2.3", "ds-test": "https://github.com/dapphub/ds-test.git#c9ce3f25bde29fc5eb9901842bf02850dfd2d084", "ejs": "^3.1.8", "forge-std": "https://github.com/foundry-rs/forge-std.git#b4f121555729b3afb3c5ffccb62ff4b6e2818fd3", diff --git a/packages/store/ts/library/config/mudConfig.test-d.ts b/packages/store/ts/library/config/mudConfig.test-d.ts index a1cf0bb45b..e54a4dd1fc 100644 --- a/packages/store/ts/library/config/mudConfig.test-d.ts +++ b/packages/store/ts/library/config/mudConfig.test-d.ts @@ -1,17 +1,11 @@ import { describe, expectTypeOf } from "vitest"; -import { mudConfig } from "."; +import { mudConfig, TABLE_DEFAULTS } from "."; import { storePlugin } from "./plugin"; -/* -describe("mudConfig", () => { - // Test possible inference confusion. - // This would fail if you remove `AsDependent` from `MUDUserConfig` - expectTypeOf< - ReturnType< - typeof mudConfig< +type AutoExpandedConfig = ReturnType< + typeof mudConfig< { storePlugin: typeof storePlugin }, { - plugins: { storePlugin: typeof storePlugin }, tables: { Table1: { keySchema: { @@ -33,32 +27,53 @@ describe("mudConfig", () => { }; } > - > - >().toEqualTypeOf<{ - plugins: { storePlugin: typeof storePlugin }, - enums: { - Enum1: ["E1"]; - Enum2: ["E1"]; - }; - tables: { - Table1: { - keySchema: { - a: "Enum1"; - }; - schema: { - b: "Enum2"; - }; +>; + +type ManuallyExpandedConfig = { + enums: { + Enum1: ["E1"]; + Enum2: ["E1"]; + }; + tables: { + Table1: { + keySchema: { + a: "Enum1"; }; - Table2: { - schema: { - a: "uint32"; - }; + schema: { + b: "Enum2"; }; + directory: typeof TABLE_DEFAULTS.directory; + name: "Table1"; + tableIdArgument: typeof TABLE_DEFAULTS.tableIdArgument; + storeArgument: typeof TABLE_DEFAULTS.storeArgument; + dataStruct: boolean; + ephemeral: typeof TABLE_DEFAULTS.ephemeral; }; - namespace: ""; - storeImportPath: "@latticexyz/store/src/"; - userTypesPath: "Types"; - codegenDirectory: "codegen"; - }>(); + Table2: { + schema: { + a: "uint32"; + }; + directory: typeof TABLE_DEFAULTS.directory; + name: "Table2"; + tableIdArgument: typeof TABLE_DEFAULTS.tableIdArgument; + storeArgument: typeof TABLE_DEFAULTS.storeArgument; + dataStruct: boolean; + keySchema: typeof TABLE_DEFAULTS.keySchema; + ephemeral: typeof TABLE_DEFAULTS.ephemeral; + }; + }; + namespace: ""; + storeImportPath: "@latticexyz/store/src/"; + userTypesPath: "Types"; + codegenDirectory: "codegen"; +}; + +const _test1: AutoExpandedConfig = {} as ManuallyExpandedConfig; +// TODO fix weak types +const _test2: ManuallyExpandedConfig = {} as AutoExpandedConfig; + +describe("mudConfig", () => { + // Test possible inference confusion. + // This would fail if you remove `AsDependent` from `MUDUserConfig` + expectTypeOf().toEqualTypeOf(); }); -*/ diff --git a/packages/store/ts/library/config/plugin.ts b/packages/store/ts/library/config/plugin.ts index 5d5cbcf260..845d109a9d 100644 --- a/packages/store/ts/library/config/plugin.ts +++ b/packages/store/ts/library/config/plugin.ts @@ -1,8 +1,9 @@ -import { fromZodErrorCustom, MudPlugin } from "@latticexyz/config"; +import { defineMUDPlugin, fromZodErrorCustom, Plugins } from "@latticexyz/config"; +import { SyncHook } from "tapable"; import { ZodError } from "zod"; import { ExpandStoreUserConfig, StoreUserConfig, zPluginStoreConfig } from "./storeConfig"; -export function expandConfig(config: C) { +export function expandConfig(plugins: Plugins, config: C) { // This function gets called within mudConfig. // The call order of config extenders depends on the order of their imports. // Any config validation and transformation should be placed here. @@ -17,7 +18,11 @@ export function expandConfig(config: C) { } } -export const storePlugin = { +export const storePlugin = defineMUDPlugin({ id: "mud-store-plugin", expandConfig, -} as const satisfies MudPlugin; + hooks: { + preTablegen: new SyncHook(["mudConfig"]), + postTablegen: new SyncHook(["mudConfig"]), + }, +}); diff --git a/packages/store/ts/library/config/storeConfig.test-d.ts b/packages/store/ts/library/config/storeConfig.test-d.ts index 5f7e020b9d..836e0f3b5d 100644 --- a/packages/store/ts/library/config/storeConfig.test-d.ts +++ b/packages/store/ts/library/config/storeConfig.test-d.ts @@ -1,15 +1,22 @@ +import { MergedPluginsInput } from "@latticexyz/config"; import { describe, expectTypeOf } from "vitest"; import { z } from "zod"; -import { zStoreConfig, MUDUserConfig } from "./storeConfig"; -/* +import { storePlugin } from "./plugin"; +import { zStoreConfig, StoreUserConfig } from "./storeConfig"; + describe("StoreUserConfig", () => { + // Check that the plugin uses the correct type + expectTypeOf< + Omit, "plugins"> + >().toEqualTypeOf(); + // Typecheck manual interfaces against zod - expectTypeOf().toEqualTypeOf>(); + expectTypeOf().toEqualTypeOf>(); // type equality isn't deep for optionals - expectTypeOf().toEqualTypeOf["tables"][string]>(); - expectTypeOf[string]>().toEqualTypeOf< + expectTypeOf().toEqualTypeOf["tables"][string]>(); + expectTypeOf[string]>().toEqualTypeOf< NonNullable>["enums"]>[string] >(); // TODO If more nested schemas are added, provide separate tests for them -});*/ +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be8b39f32b..8db78ad3a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -238,6 +238,9 @@ importers: find-up: specifier: ^6.3.0 version: 6.3.0 + tapable: + specifier: ^2.2.1 + version: 2.2.1 zod: specifier: ^3.21.4 version: 3.21.4 @@ -245,6 +248,9 @@ importers: specifier: ^1.3.0 version: 1.3.0(zod@3.21.4) devDependencies: + '@types/tapable': + specifier: ^2.2.3 + version: 2.2.3 tsup: specifier: ^6.7.0 version: 6.7.0(typescript@4.9.5) @@ -928,6 +934,9 @@ importers: ethers: specifier: ^5.7.2 version: 5.7.2 + tapable: + specifier: ^2.2.1 + version: 2.2.1 zod: specifier: ^3.21.4 version: 3.21.4 @@ -944,6 +953,9 @@ importers: '@types/node': specifier: ^18.15.11 version: 18.15.11 + '@types/tapable': + specifier: ^2.2.3 + version: 2.2.3 ds-test: specifier: https://github.com/dapphub/ds-test.git#c9ce3f25bde29fc5eb9901842bf02850dfd2d084 version: github.com/dapphub/ds-test/c9ce3f25bde29fc5eb9901842bf02850dfd2d084 @@ -3795,6 +3807,12 @@ packages: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true + /@types/tapable@2.2.3: + resolution: {integrity: sha512-WA0xhgs1wenECj1psxojjYOIc1Zkn+4gNtuWmF+dcFs5UfsQa8WhWs+8WG4D2RuaX7DufNPU+KsOEF7KqlByuw==} + dependencies: + tapable: 2.2.1 + dev: true + /@types/throttle-debounce@5.0.0: resolution: {integrity: sha512-Pb7k35iCGFcGPECoNE4DYp3Oyf2xcTd3FbFQxXUI9hEYKUl6YX+KLf7HrBmgVcD05nl50LIH6i+80js4iYmWbw==} dev: true @@ -13542,6 +13560,10 @@ packages: - ts-node dev: true + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + /tar-stream@1.6.2: resolution: {integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==} engines: {node: '>= 0.8.0'}