Skip to content

Commit

Permalink
feat(store,world): add option to codegen tables into namespace dirs (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
holic authored May 23, 2024
1 parent 1e43543 commit c10c9fb
Show file tree
Hide file tree
Showing 35 changed files with 365 additions and 139 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-lobsters-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/store": patch
---

Internal `tablegen` function (exported from `@latticexyz/store/codegen`) now expects an object of options with a `configPath` to use as a base path to resolve other relative paths from.
8 changes: 8 additions & 0 deletions .changeset/great-ducks-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@latticexyz/store": patch
"@latticexyz/world": patch
---

Added `sourceDirectory` as a top-level config option for specifying contracts source (i.e. Solidity) directory relative to the MUD config. This is used to resolve other paths in the config, like codegen and user types. Like `foundry.toml`, this defaults to `src` and should be kept in sync with `foundry.toml`.

Also added a `codegen.namespaceDirectories` option to organize codegen output (table libraries, etc.) into directories by namespace. For example, a `Counter` table in the `app` namespace will have codegen at `codegen/app/tables/Counter.sol`. If not set, defaults to `true` when using top-level `namespaces` key, `false` otherwise.
10 changes: 6 additions & 4 deletions packages/cli/scripts/generate-test-tables.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import path from "path";
import { tablegen } from "@latticexyz/store/codegen";
import { defineStore } from "@latticexyz/store";
import { getRemappings, getSrcDirectory } from "@latticexyz/common/foundry";
import { getRemappings } from "@latticexyz/common/foundry";
import { fileURLToPath } from "node:url";

const configPath = fileURLToPath(import.meta.url);

// This config is used only for tests.
// Aside from avoiding `mud.config.ts` in cli package (could cause issues),
// this also tests that mudConfig and tablegen can work as standalone functions
const config = defineStore({
sourceDirectory: "../contracts/src",
enums: {
Enum1: ["E1", "E2", "E3"],
Enum2: ["E1"],
Expand Down Expand Up @@ -92,7 +95,6 @@ const config = defineStore({
},
});

const srcDirectory = await getSrcDirectory();
const remappings = await getRemappings();

await tablegen(config, path.join(srcDirectory, config.codegen.outputDirectory), remappings);
await tablegen({ configPath, config, remappings });
17 changes: 11 additions & 6 deletions packages/cli/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,34 @@ import path from "node:path";
import { tablegen } from "@latticexyz/store/codegen";
import { worldgen } from "@latticexyz/world/node";
import { World as WorldConfig } from "@latticexyz/world";
import { worldToV1 } from "@latticexyz/world/config/v2";
import { forge, getRemappings } from "@latticexyz/common/foundry";
import { getExistingContracts } from "./utils/getExistingContracts";
import { execa } from "execa";

type BuildOptions = {
foundryProfile?: string;
srcDir: string;
/**
* Path to `mud.config.ts`. We use this as the "project root" to resolve other relative paths.
*
* Defaults to finding the nearest `mud.config.ts`, looking in `process.cwd()` and moving up the directory tree.
*/
configPath: string;
config: WorldConfig;
};

export async function build({
config: configV2,
configPath,
config,
srcDir,
foundryProfile = process.env.FOUNDRY_PROFILE,
}: BuildOptions): Promise<void> {
const config = worldToV1(configV2);
const outPath = path.join(srcDir, config.codegenDirectory);
const outPath = path.join(srcDir, config.codegen.outputDirectory);
const remappings = await getRemappings(foundryProfile);

await Promise.all([
tablegen(configV2, outPath, remappings),
worldgen(configV2, getExistingContracts(srcDir), outPath),
tablegen({ configPath, config, remappings }),
worldgen(config, getExistingContracts(srcDir), outPath),
]);

await forge(["build"], { profile: foundryProfile });
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CommandModule } from "yargs";
import { loadConfig } from "@latticexyz/config/node";
import { loadConfig, resolveConfigPath } from "@latticexyz/config/node";
import { World as WorldConfig } from "@latticexyz/world";

import { getSrcDirectory } from "@latticexyz/common/foundry";
Expand All @@ -22,11 +22,12 @@ const commandModule: CommandModule<Options, Options> = {
});
},

async handler({ configPath, profile }) {
async handler(opts) {
const configPath = await resolveConfigPath(opts.configPath);
const config = (await loadConfig(configPath)) as WorldConfig;
const srcDir = await getSrcDirectory();

await build({ config, srcDir, foundryProfile: profile });
await build({ configPath, config, srcDir, foundryProfile: opts.profile });

process.exit(0);
},
Expand Down
11 changes: 5 additions & 6 deletions packages/cli/src/commands/tablegen.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import path from "path";
import type { CommandModule } from "yargs";
import { loadConfig } from "@latticexyz/config/node";
import { loadConfig, resolveConfigPath } from "@latticexyz/config/node";
import { Store as StoreConfig } from "@latticexyz/store";
import { tablegen } from "@latticexyz/store/codegen";
import { getRemappings, getSrcDirectory } from "@latticexyz/common/foundry";
import { getRemappings } from "@latticexyz/common/foundry";

type Options = {
configPath?: string;
Expand All @@ -20,12 +19,12 @@ const commandModule: CommandModule<Options, Options> = {
});
},

async handler({ configPath }) {
async handler(opts) {
const configPath = await resolveConfigPath(opts.configPath);
const config = (await loadConfig(configPath)) as StoreConfig;
const srcDir = await getSrcDirectory();
const remappings = await getRemappings();

await tablegen(config, path.join(srcDir, config.codegen.outputDirectory), remappings);
await tablegen({ configPath, config, remappings });

process.exit(0);
},
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/runDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { InferredOptionTypes, Options } from "yargs";
import { deploy } from "./deploy/deploy";
import { createWalletClient, http, Hex, isHex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { loadConfig } from "@latticexyz/config/node";
import { loadConfig, resolveConfigPath } from "@latticexyz/config/node";
import { World as WorldConfig } from "@latticexyz/world";
import { worldToV1 } from "@latticexyz/world/config/v2";
import { getOutDirectory, getRpcUrl, getSrcDirectory } from "@latticexyz/common/foundry";
Expand Down Expand Up @@ -64,7 +64,8 @@ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {

const profile = opts.profile ?? process.env.FOUNDRY_PROFILE;

const configV2 = (await loadConfig(opts.configPath)) as WorldConfig;
const configPath = await resolveConfigPath(opts.configPath);
const configV2 = (await loadConfig(configPath)) as WorldConfig;
const config = worldToV1(configV2);
if (opts.printConfig) {
console.log(chalk.green("\nResolved config:\n"), JSON.stringify(config, null, 2));
Expand All @@ -82,7 +83,7 @@ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {

// Run build
if (!opts.skipBuild) {
await build({ config: configV2, srcDir, foundryProfile: profile });
await build({ configPath, config: configV2, srcDir, foundryProfile: profile });
}

const resolvedConfig = resolveConfig({ config, forgeSourceDir: srcDir, forgeOutDir: outDir });
Expand Down
2 changes: 1 addition & 1 deletion packages/store/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default defineStore({
codegen: {
storeImportPath: "../../",
},
namespace: "store" as const,
namespace: "store",
userTypes: {
ResourceId: { filePath: "./src/ResourceId.sol", type: "bytes32" },
FieldLayout: { filePath: "./src/FieldLayout.sol", type: "bytes32" },
Expand Down
4 changes: 2 additions & 2 deletions packages/store/test/codegen/tables/Callbacks.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/store/test/codegen/tables/KeyEncoding.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/store/test/codegen/tables/Mixed.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/store/test/codegen/tables/Vector2.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 46 additions & 37 deletions packages/store/ts/codegen/tableOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
SolidityUserDefinedType,
} from "@latticexyz/common/codegen";
import { RenderTableOptions } from "./types";
import { StoreConfig } from "../config";
import { getSchemaTypeInfo, importForAbiOrUserType, resolveAbiOrUserType } from "./userType";
import { Store as StoreConfig } from "../config/v2/output";
import { storeToV1 } from "../config/v2/compat";
import { getKeySchema, getValueSchema } from "@latticexyz/protocol-parser/internal";

export interface TableOptions {
/** Path where the file is expected to be written (relative to project root) */
Expand All @@ -28,87 +30,94 @@ export function getTableOptions(
config: StoreConfig,
solidityUserTypes: Record<string, SolidityUserDefinedType>,
): TableOptions[] {
const storeImportPath = config.storeImportPath;
const configV1 = storeToV1(config);

const options = [];
for (const tableName of Object.keys(config.tables)) {
const tableData = config.tables[tableName];
const options = Object.values(config.tables).map((table): TableOptions => {
const keySchema = getKeySchema(table);
const valueSchema = getValueSchema(table);

// struct adds methods to get/set all values at once
const withStruct = tableData.dataStruct;
const withStruct = table.codegen.dataStruct;
// operate on all fields at once; always render for offchain tables; for only 1 field keep them if struct is also kept
const withRecordMethods = withStruct || tableData.offchainOnly || Object.keys(tableData.valueSchema).length > 1;
const withRecordMethods = withStruct || table.type === "offchainTable" || Object.keys(valueSchema).length > 1;
// field methods can include simply get/set if there's only 1 field and no record methods
const withSuffixlessFieldMethods = !withRecordMethods && Object.keys(tableData.valueSchema).length === 1;
const withSuffixlessFieldMethods = !withRecordMethods && Object.keys(valueSchema).length === 1;
// list of any symbols that need to be imported
const imports: ImportDatum[] = [];

const keyTuple = Object.keys(tableData.keySchema).map((name) => {
const abiOrUserType = tableData.keySchema[name];
const { renderType } = resolveAbiOrUserType(abiOrUserType, config, solidityUserTypes);
const keyTuple = Object.entries(keySchema).map(([name, field]): RenderKeyTuple => {
const abiOrUserType = field.internalType;
const { renderType } = resolveAbiOrUserType(abiOrUserType, configV1, solidityUserTypes);

const importDatum = importForAbiOrUserType(abiOrUserType, tableData.directory, config, solidityUserTypes);
const importDatum = importForAbiOrUserType(
abiOrUserType,
table.codegen.outputDirectory,
configV1,
solidityUserTypes,
);
if (importDatum) imports.push(importDatum);

if (renderType.isDynamic) throw new Error(`Parsing error: found dynamic key ${name} in table ${tableName}`);

const keyTuple: RenderKeyTuple = {
return {
...renderType,
name,
isDynamic: false,
};
return keyTuple;
});

const fields = Object.keys(tableData.valueSchema).map((name) => {
const abiOrUserType = tableData.valueSchema[name];
const { renderType, schemaType } = resolveAbiOrUserType(abiOrUserType, config, solidityUserTypes);
const fields = Object.entries(valueSchema).map(([name, field]): RenderField => {
const abiOrUserType = field.internalType;
const { renderType, schemaType } = resolveAbiOrUserType(abiOrUserType, configV1, solidityUserTypes);

const importDatum = importForAbiOrUserType(abiOrUserType, tableData.directory, config, solidityUserTypes);
const importDatum = importForAbiOrUserType(
abiOrUserType,
table.codegen.outputDirectory,
configV1,
solidityUserTypes,
);
if (importDatum) imports.push(importDatum);

const elementType = SchemaTypeArrayToElement[schemaType];
const field: RenderField = {
return {
...renderType,
arrayElement: elementType !== undefined ? getSchemaTypeInfo(elementType) : undefined,
name,
};
return field;
});

const staticFields = fields.filter(({ isDynamic }) => !isDynamic) as RenderStaticField[];
const dynamicFields = fields.filter(({ isDynamic }) => isDynamic) as RenderDynamicField[];

// With tableIdArgument: tableId is a dynamic argument for each method
// Without tableIdArgument: tableId is a file-level constant generated from `staticResourceData`
const staticResourceData = tableData.tableIdArgument
const staticResourceData = table.codegen.tableIdArgument
? undefined
: {
namespace: config.namespace,
name: tableData.name,
offchainOnly: tableData.offchainOnly,
namespace: table.namespace,
name: table.name,
offchainOnly: table.type === "offchainTable",
};

options.push({
outputPath: path.join(tableData.directory, `${tableName}.sol`),
tableName,
return {
outputPath: path.join(table.codegen.outputDirectory, `${table.name}.sol`),
tableName: table.name,
renderOptions: {
imports,
libraryName: tableName,
structName: withStruct ? tableName + "Data" : undefined,
libraryName: table.name,
structName: withStruct ? table.name + "Data" : undefined,
staticResourceData,
storeImportPath,
storeImportPath: config.codegen.storeImportPath,
keyTuple,
fields,
staticFields,
dynamicFields,
withGetters: !tableData.offchainOnly,
withGetters: table.type === "table",
withRecordMethods,
withDynamicFieldMethods: !tableData.offchainOnly,
withDynamicFieldMethods: table.type === "table",
withSuffixlessFieldMethods,
storeArgument: tableData.storeArgument,
storeArgument: table.codegen.storeArgument,
},
});
}
};
});

return options;
}
Loading

0 comments on commit c10c9fb

Please sign in to comment.