Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] local config types #1025

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/common/src/type-utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ export type OrDefault<T, Default> = T extends undefined ? Default : T;
export type OrDefaults<T extends object, Defaults> = {
[key in keyof Defaults]: key extends keyof T ? OrDefault<T[key], Defaults[key]> : Defaults[key];
};

// Helper type to turn `A | B` into `A & B`
export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

// Helper type to extract and merge the return types of a given union of functions
export type MergeReturnType<T extends (...args: any) => any> = UnionToIntersection<ReturnType<T>>;
6 changes: 2 additions & 4 deletions packages/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,13 @@
"type": "module",
"exports": {
".": "./dist/library/index.js",
"./register": "./dist/register/index.js",
"./node": "./dist/node/index.js"
},
"typesVersions": {
"*": {
"index": [
"./src/library/index.ts"
],
"register": [
"./src/register/index.ts"
],
"node": [
"./src/node/index.ts"
]
Expand All @@ -42,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"
Expand Down
36 changes: 0 additions & 36 deletions packages/config/src/library/context.ts

This file was deleted.

58 changes: 38 additions & 20 deletions packages/config/src/library/core.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
import { MUDCoreContext } from "./context";
import { UnionToIntersection } from "@latticexyz/common/type-utils";
import { MudPlugin, Plugins } from "./types";

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface MUDCoreUserConfig {}
// Helper type to infer the input types from a plugins config as union (InputA | InputB)
type PluginsInput<P extends Plugins> = Parameters<P[keyof P]["expandConfig"]>[0];

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface MUDCoreConfig {}
/**
* Infer the plugin input types as intersection (InputA & InputB)
*/
export type MergedPluginsInput<P extends Plugins> = UnionToIntersection<PluginsInput<P>>;

export type MUDConfigExtender = (config: MUDCoreConfig) => Record<string, unknown>;
/**
* Helper function to typecheck a plugin definition.
*/
export function defineMUDPlugin<P extends MudPlugin>(plugin: P): P {
return plugin;
}

/** Resolver that sequentially passes the config through all the plugins */
export function mudCoreConfig(config: MUDCoreUserConfig): MUDCoreConfig {
// config types can change with plugins, `any` helps avoid errors when typechecking dependencies
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let configAsAny = config as any;
const context = MUDCoreContext.getContext();
for (const extender of context.configExtenders) {
configAsAny = extender(configAsAny);
}
return configAsAny;
/**
* Helper function to typecheck a config.
*/
export function mudCoreConfig<P extends Plugins, C extends MergedPluginsInput<P>>(config: { plugins: P } & C) {
return config;
}

/** Utility for plugin developers to extend the core config */
export function extendMUDCoreConfig(extender: MUDConfigExtender) {
const context = MUDCoreContext.getContext();
context.configExtenders.push(extender);
/**
* Helper function to sequentially apply `expandConfig` of each plugin.
* Use ExpandConfig to strongly type the result.
*
* Usage:
* ```
* const _typedExpandConfig = expandConfig as ExpandConfig<typeof config>;
* type ExpandedConfig = MergeReturnType<typeof _typedExpandConfig<typeof config>>;
* const expandedConfig = expandConfig(config) as ExpandedConfig;
* ```
*
* TODO explain HKTs and why this can't just return `MergeReturnType<ExpandConfig<C><C>>`
*/
export function expandConfig<C extends { plugins: Plugins }>(config: C): Record<string, unknown> {
let expanded = config;
for (const plugin of Object.values(config.plugins)) {
expanded = { ...expanded, ...plugin.expandConfig(config) };
}
return expanded;
}
4 changes: 1 addition & 3 deletions packages/config/src/library/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Importing library has no side-effects, unlike register
// (library does not create MUDCoreContext when imported)
export * from "./commonSchemas";
export * from "./context";
export * from "./core";
export * from "./dynamicResolution";
export * from "./errors";
export * from "./types";
export * from "./validation";
36 changes: 36 additions & 0 deletions packages/config/src/library/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Hook } from "tapable";

/*
* Every plugin defines an `Input`, an `Expanded<T = Input>` type,
* and a `expandConfig` function to map from `Input` to `Expanded`.
* To distinguish plugins from each other in TypeScript, they also define a unique `id` string.
*/
export interface MudPlugin<Input = any, Expanded = any> {
id: string;
expandConfig: <C extends Input>(config: C) => Expanded;
hooks: Record<string, Hook<unknown, unknown>>;
}

/**
* The core config only expects a map of plugins.
* 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
* https://www.stefanjudis.com/today-i-learned/property-order-is-predictable-in-javascript-objects-since-es2015/
*/
export type Config = { plugins: Plugins };
export type Plugins = Record<string, MudPlugin>;

/*
* Helper type to turn a strongly typed config into a union of
* all `expandConfig` functions defined in the config
*
* Usage:
* ```
* const _typedExpandConfig = expandConfig as ExpandConfig<typeof config>;
* type ExpandedConfig = MergeReturnType<typeof _typedExpandConfig<typeof config>>;
* const expandedConfig = expandConfig(config) as ExpandedConfig;
* ```
*/
export type ExpandConfig<C extends { plugins: Plugins }> = C["plugins"][keyof C["plugins"]]["expandConfig"];
11 changes: 0 additions & 11 deletions packages/config/src/register/index.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/config/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/library/index.ts", "src/register/index.ts", "src/node/index.ts"],
entry: ["src/library/index.ts", "src/node/index.ts"],
target: "esnext",
format: ["esm"],
dts: false,
Expand Down
11 changes: 9 additions & 2 deletions packages/store/mud.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { mudConfig } from "./ts/register";
import { MergeReturnType } from "@latticexyz/common/type-utils";
import { ExpandConfig, expandConfig } from "@latticexyz/config";
import { mudConfig, storePlugin } from "./ts";

export default mudConfig({
const config = mudConfig({
plugins: { storePlugin },
storeImportPath: "../../",
namespace: "mudstore",
enums: {
Expand Down Expand Up @@ -46,3 +49,7 @@ export default mudConfig({
},
},
});

const _typedExpandConfig = expandConfig as ExpandConfig<typeof config>;
type ExpandedConfig = MergeReturnType<typeof _typedExpandConfig<typeof config>>;
export default expandConfig(config) as ExpandedConfig;
2 changes: 2 additions & 0 deletions packages/store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,15 @@
"@latticexyz/schema-type": "workspace:*",
"abitype": "0.8.7",
"ethers": "^5.7.2",
"tapable": "^2.2.1",
"zod": "^3.21.4"
},
"devDependencies": {
"@typechain/ethers-v5": "^10.2.0",
"@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",
Expand Down
19 changes: 19 additions & 0 deletions packages/store/ts/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
export * from "./defaults";
export * from "./plugin";
export * from "./storeConfig";

import { Plugins, MergedPluginsInput } from "@latticexyz/config";
import { ExtractUserTypes, StringForUnion } from "@latticexyz/common/type-utils";
import { MUDUserConfig } from "./storeConfig";

/**
* Helper function to typecheck a config.
* This is an alternative to mudCoreConfig that uses more generics for type inference of user-defined types.
*/
export function mudConfig<
P extends Plugins,
C extends MergedPluginsInput<P>,
// (`never` is overridden by inference, so only the defined enums can be used by default)
EnumNames extends StringForUnion = never,
StaticUserTypes extends ExtractUserTypes<EnumNames> = ExtractUserTypes<EnumNames>
>(config: { plugins: P } & MUDUserConfig<P, C, EnumNames, StaticUserTypes>): { plugins: P } & C {
return config as any;
}
86 changes: 86 additions & 0 deletions packages/store/ts/config/mudConfig.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { MergeReturnType } from "@latticexyz/common/type-utils";
import { expandConfig, ExpandConfig } from "@latticexyz/config";
import { describe, expectTypeOf } from "vitest";
import { mudConfig, TABLE_DEFAULTS } from ".";
import { storePlugin } from "./plugin";

type DefinedConfig = ReturnType<
typeof mudConfig<
{ storePlugin: typeof storePlugin },
{
tables: {
Table1: {
keySchema: {
a: "Enum1";
};
schema: {
b: "Enum2";
};
};
Table2: {
schema: {
a: "uint32";
};
};
};
enums: {
Enum1: ["E1"];
Enum2: ["E1"];
};
}
>
>;

const typedExpandConfig = expandConfig as ExpandConfig<DefinedConfig>;
type AutoExpandedConfig = MergeReturnType<typeof typedExpandConfig<DefinedConfig>>;

type ManuallyExpandedConfig = {
enums: {
Enum1: ["E1"];
Enum2: ["E1"];
};
tables: {
Table1: {
keySchema: {
a: "Enum1";
};
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;
};
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";
};

describe("mudConfig", () => {
// Test possible inference confusion.
// This would fail if you remove `AsDependent` from `MUDUserConfig`
expectTypeOf<AutoExpandedConfig>().toEqualTypeOf<ManuallyExpandedConfig>();
});

// An extra test to be sure of type equality
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _test1: AutoExpandedConfig = {} as ManuallyExpandedConfig;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _test2: ManuallyExpandedConfig = {} as AutoExpandedConfig;
28 changes: 28 additions & 0 deletions packages/store/ts/config/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { defineMUDPlugin, fromZodErrorCustom } from "@latticexyz/config";
import { SyncHook } from "tapable";
import { ZodError } from "zod";
import { ExpandStoreUserConfig, StoreUserConfig, zPluginStoreConfig } from "./storeConfig";

export function expandConfig<C extends StoreUserConfig>(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.
try {
return zPluginStoreConfig.parse(config) as ExpandStoreUserConfig<C>;
} catch (error) {
if (error instanceof ZodError) {
throw fromZodErrorCustom(error, "StoreConfig Validation Error");
} else {
throw error;
}
}
}

export const storePlugin = defineMUDPlugin({
id: "mud-store-plugin",
expandConfig,
hooks: {
preTablegen: new SyncHook(["mudConfig"]),
postTablegen: new SyncHook(["mudConfig"]),
},
} as const);
Loading