Skip to content

Commit

Permalink
feat(cli): add module config to CLI (#494)
Browse files Browse the repository at this point in the history
* refactor(cli): split up loadWorldConfig into multiple files

* feat(cli): add dynamicResolution

* feat(cli): add support for custom modules to CLI

* feat(cli): register modules
  • Loading branch information
alvrs authored Mar 16, 2023
1 parent 564604c commit 263c828
Show file tree
Hide file tree
Showing 16 changed files with 403 additions and 238 deletions.
2 changes: 1 addition & 1 deletion packages/cli/src/commands/deploy-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import chalk from "chalk";
import glob from "glob";
import path, { basename } from "path";
import type { CommandModule } from "yargs";
import { loadWorldConfig } from "../config/loadWorldConfig.js";
import { loadWorldConfig } from "../config/world/index.js";
import { deploy } from "../utils/deploy-v2.js";
import { logError, MUDError } from "../utils/errors.js";
import { forge, getRpcUrl, getSrcDirectory } from "../utils/foundry.js";
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/worldgen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CommandModule } from "yargs";
import { loadStoreConfig } from "../config/loadStoreConfig.js";
import { loadWorldConfig } from "../config/loadWorldConfig.js";
import { loadWorldConfig } from "../config/index.js";
import { getSrcDirectory } from "../utils/foundry.js";
import glob from "glob";
import path, { basename } from "path";
Expand Down
49 changes: 49 additions & 0 deletions packages/cli/src/config/dynamicResolution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { MUDError } from "../index.js";
import { ValueWithType } from "./world/userTypes.js";

export enum DynamicResolutionType {
TABLE_ID,
SYSTEM_ADDRESS,
}

export type DynamicResolution = {
type: DynamicResolutionType;
input: string;
};

/**
* Dynamically resolve a table name to a table id at deploy time
*/
export function resolveTableId(tableName: string) {
return {
type: DynamicResolutionType.TABLE_ID,
input: tableName,
};
}

/** Type guard for DynamicResolution */
export function isDynamicResolution(value: unknown): value is DynamicResolution {
return typeof value === "object" && value !== null && "type" in value && "input" in value;
}

/**
* Turn a DynamicResolution object into a ValueWithType based on the provided context
*/
export async function resolveWithContext(
unresolved: any,
context: { systemAddresses?: Record<string, Promise<string>>; tableIds?: Record<string, Uint8Array> }
): Promise<ValueWithType> {
if (!isDynamicResolution(unresolved)) return unresolved;
let resolved: ValueWithType | undefined = undefined;

if (unresolved.type === DynamicResolutionType.TABLE_ID) {
const tableId = context.tableIds?.[unresolved.input];
resolved = tableId && { value: tableId, type: "bytes32" };
}

if (resolved === undefined) {
throw new MUDError(`Could not resolve dynamic resolution: \n${JSON.stringify(unresolved, null, 2)}`);
}

return resolved;
}
5 changes: 3 additions & 2 deletions packages/cli/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StoreUserConfig, StoreConfig } from "./parseStoreConfig.js";
import { WorldUserConfig, ResolvedWorldConfig } from "./loadWorldConfig.js";
import { WorldUserConfig, ResolvedWorldConfig } from "./world/index.js";

export type MUDUserConfig = StoreUserConfig & WorldUserConfig;
export type MUDConfig = StoreConfig & ResolvedWorldConfig;
Expand All @@ -8,5 +8,6 @@ export * from "./commonSchemas.js";
export * from "./loadConfig.js";
export * from "./loadStoreConfig.js";
export * from "./parseStoreConfig.js";
export * from "./loadWorldConfig.js";
export * from "./world/index.js";
export * from "./validation.js";
export * from "./dynamicResolution.js";
184 changes: 0 additions & 184 deletions packages/cli/src/config/loadWorldConfig.ts

This file was deleted.

4 changes: 4 additions & 0 deletions packages/cli/src/config/world/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./loadWorldConfig.js";
export * from "./parseWorldConfig.js";
export * from "./resolveWorldConfig.js";
export * from "./userTypes.js";
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expectTypeOf } from "vitest";
import { z } from "zod";
import { WorldConfig, WorldUserConfig } from "./loadWorldConfig.js";
import { WorldConfig, WorldUserConfig } from "./index.js";

describe("loadWorldConfig", () => {
// Typecheck manual interfaces against zod
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/config/world/loadWorldConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ZodError } from "zod";
import { fromZodErrorCustom } from "../../utils/errors.js";
import { loadConfig } from "../loadConfig.js";
import { WorldConfig } from "./parseWorldConfig.js";
import { resolveWorldConfig } from "./resolveWorldConfig.js";

/**
* Loads and resolves the world config.
* @param configPath Path to load the config from. Defaults to "mud.config.mts" or "mud.config.ts"
* @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.
* @returns Promise of ResolvedWorldConfig object
*/
export async function loadWorldConfig(configPath?: string, existingContracts?: string[]) {
const config = await loadConfig(configPath);

try {
const parsedConfig = WorldConfig.parse(config);
return resolveWorldConfig(parsedConfig, existingContracts);
} catch (error) {
if (error instanceof ZodError) {
throw fromZodErrorCustom(error, "WorldConfig Validation Error");
} else {
throw error;
}
}
}
54 changes: 54 additions & 0 deletions packages/cli/src/config/world/parseWorldConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { z } from "zod";
import { EthereumAddress, ObjectName, Selector } from "../commonSchemas.js";
import { DynamicResolutionType } from "../dynamicResolution.js";

const SystemName = ObjectName;
const ModuleName = ObjectName;
const SystemAccessList = z.array(SystemName.or(EthereumAddress)).default([]);

// The system config is a combination of a fileSelector config and access config
const SystemConfig = z.intersection(
z.object({
fileSelector: Selector,
}),
z.discriminatedUnion("openAccess", [
z.object({
openAccess: z.literal(true),
}),
z.object({
openAccess: z.literal(false),
accessList: SystemAccessList,
}),
])
);

const ValueWithType = z.object({
value: z.union([z.string(), z.number(), z.instanceof(Uint8Array)]),
type: z.string(),
});
const DynamicResolution = z.object({ type: z.nativeEnum(DynamicResolutionType), input: z.string() });

const ModuleConfig = z.object({
name: ModuleName,
root: z.boolean().default(false),
args: z.array(z.union([ValueWithType, DynamicResolution])).default([]),
});

// The parsed world config is the result of parsing the user config
export const WorldConfig = z.object({
namespace: Selector.default(""),
worldContractName: z.string().optional(),
overrideSystems: z.record(SystemName, SystemConfig).default({}),
excludeSystems: z.array(SystemName).default([]),
postDeployScript: z.string().default("PostDeploy"),
deploymentInfoDirectory: z.string().default("."),
worldgenDirectory: z.string().default("world"),
worldImportPath: z.string().default("@latticexyz/world/src/"),
modules: z.array(ModuleConfig).default([]),
});

export async function parseWorldConfig(config: unknown) {
return WorldConfig.parse(config);
}

export type ParsedWorldConfig = z.output<typeof WorldConfig>;
Loading

0 comments on commit 263c828

Please sign in to comment.