Skip to content

Commit c10c9fb

Browse files
authored
feat(store,world): add option to codegen tables into namespace dirs (#2840)
1 parent 1e43543 commit c10c9fb

35 files changed

+365
-139
lines changed

.changeset/eleven-lobsters-play.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@latticexyz/store": patch
3+
---
4+
5+
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.

.changeset/great-ducks-search.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@latticexyz/store": patch
3+
"@latticexyz/world": patch
4+
---
5+
6+
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`.
7+
8+
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.

packages/cli/scripts/generate-test-tables.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import path from "path";
21
import { tablegen } from "@latticexyz/store/codegen";
32
import { defineStore } from "@latticexyz/store";
4-
import { getRemappings, getSrcDirectory } from "@latticexyz/common/foundry";
3+
import { getRemappings } from "@latticexyz/common/foundry";
4+
import { fileURLToPath } from "node:url";
5+
6+
const configPath = fileURLToPath(import.meta.url);
57

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

95-
const srcDirectory = await getSrcDirectory();
9698
const remappings = await getRemappings();
9799

98-
await tablegen(config, path.join(srcDirectory, config.codegen.outputDirectory), remappings);
100+
await tablegen({ configPath, config, remappings });

packages/cli/src/build.ts

+11-6
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,34 @@ import path from "node:path";
22
import { tablegen } from "@latticexyz/store/codegen";
33
import { worldgen } from "@latticexyz/world/node";
44
import { World as WorldConfig } from "@latticexyz/world";
5-
import { worldToV1 } from "@latticexyz/world/config/v2";
65
import { forge, getRemappings } from "@latticexyz/common/foundry";
76
import { getExistingContracts } from "./utils/getExistingContracts";
87
import { execa } from "execa";
98

109
type BuildOptions = {
1110
foundryProfile?: string;
1211
srcDir: string;
12+
/**
13+
* Path to `mud.config.ts`. We use this as the "project root" to resolve other relative paths.
14+
*
15+
* Defaults to finding the nearest `mud.config.ts`, looking in `process.cwd()` and moving up the directory tree.
16+
*/
17+
configPath: string;
1318
config: WorldConfig;
1419
};
1520

1621
export async function build({
17-
config: configV2,
22+
configPath,
23+
config,
1824
srcDir,
1925
foundryProfile = process.env.FOUNDRY_PROFILE,
2026
}: BuildOptions): Promise<void> {
21-
const config = worldToV1(configV2);
22-
const outPath = path.join(srcDir, config.codegenDirectory);
27+
const outPath = path.join(srcDir, config.codegen.outputDirectory);
2328
const remappings = await getRemappings(foundryProfile);
2429

2530
await Promise.all([
26-
tablegen(configV2, outPath, remappings),
27-
worldgen(configV2, getExistingContracts(srcDir), outPath),
31+
tablegen({ configPath, config, remappings }),
32+
worldgen(config, getExistingContracts(srcDir), outPath),
2833
]);
2934

3035
await forge(["build"], { profile: foundryProfile });

packages/cli/src/commands/build.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { CommandModule } from "yargs";
2-
import { loadConfig } from "@latticexyz/config/node";
2+
import { loadConfig, resolveConfigPath } from "@latticexyz/config/node";
33
import { World as WorldConfig } from "@latticexyz/world";
44

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

25-
async handler({ configPath, profile }) {
25+
async handler(opts) {
26+
const configPath = await resolveConfigPath(opts.configPath);
2627
const config = (await loadConfig(configPath)) as WorldConfig;
2728
const srcDir = await getSrcDirectory();
2829

29-
await build({ config, srcDir, foundryProfile: profile });
30+
await build({ configPath, config, srcDir, foundryProfile: opts.profile });
3031

3132
process.exit(0);
3233
},

packages/cli/src/commands/tablegen.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import path from "path";
21
import type { CommandModule } from "yargs";
3-
import { loadConfig } from "@latticexyz/config/node";
2+
import { loadConfig, resolveConfigPath } from "@latticexyz/config/node";
43
import { Store as StoreConfig } from "@latticexyz/store";
54
import { tablegen } from "@latticexyz/store/codegen";
6-
import { getRemappings, getSrcDirectory } from "@latticexyz/common/foundry";
5+
import { getRemappings } from "@latticexyz/common/foundry";
76

87
type Options = {
98
configPath?: string;
@@ -20,12 +19,12 @@ const commandModule: CommandModule<Options, Options> = {
2019
});
2120
},
2221

23-
async handler({ configPath }) {
22+
async handler(opts) {
23+
const configPath = await resolveConfigPath(opts.configPath);
2424
const config = (await loadConfig(configPath)) as StoreConfig;
25-
const srcDir = await getSrcDirectory();
2625
const remappings = await getRemappings();
2726

28-
await tablegen(config, path.join(srcDir, config.codegen.outputDirectory), remappings);
27+
await tablegen({ configPath, config, remappings });
2928

3029
process.exit(0);
3130
},

packages/cli/src/runDeploy.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { InferredOptionTypes, Options } from "yargs";
44
import { deploy } from "./deploy/deploy";
55
import { createWalletClient, http, Hex, isHex } from "viem";
66
import { privateKeyToAccount } from "viem/accounts";
7-
import { loadConfig } from "@latticexyz/config/node";
7+
import { loadConfig, resolveConfigPath } from "@latticexyz/config/node";
88
import { World as WorldConfig } from "@latticexyz/world";
99
import { worldToV1 } from "@latticexyz/world/config/v2";
1010
import { getOutDirectory, getRpcUrl, getSrcDirectory } from "@latticexyz/common/foundry";
@@ -64,7 +64,8 @@ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
6464

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

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

8384
// Run build
8485
if (!opts.skipBuild) {
85-
await build({ config: configV2, srcDir, foundryProfile: profile });
86+
await build({ configPath, config: configV2, srcDir, foundryProfile: profile });
8687
}
8788

8889
const resolvedConfig = resolveConfig({ config, forgeSourceDir: srcDir, forgeOutDir: outDir });

packages/store/mud.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default defineStore({
44
codegen: {
55
storeImportPath: "../../",
66
},
7-
namespace: "store" as const,
7+
namespace: "store",
88
userTypes: {
99
ResourceId: { filePath: "./src/ResourceId.sol", type: "bytes32" },
1010
FieldLayout: { filePath: "./src/FieldLayout.sol", type: "bytes32" },

packages/store/test/codegen/tables/Callbacks.sol

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/store/test/codegen/tables/KeyEncoding.sol

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/store/test/codegen/tables/Mixed.sol

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/store/test/codegen/tables/Vector2.sol

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/store/ts/codegen/tableOptions.ts

+46-37
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import {
99
SolidityUserDefinedType,
1010
} from "@latticexyz/common/codegen";
1111
import { RenderTableOptions } from "./types";
12-
import { StoreConfig } from "../config";
1312
import { getSchemaTypeInfo, importForAbiOrUserType, resolveAbiOrUserType } from "./userType";
13+
import { Store as StoreConfig } from "../config/v2/output";
14+
import { storeToV1 } from "../config/v2/compat";
15+
import { getKeySchema, getValueSchema } from "@latticexyz/protocol-parser/internal";
1416

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

33-
const options = [];
34-
for (const tableName of Object.keys(config.tables)) {
35-
const tableData = config.tables[tableName];
35+
const options = Object.values(config.tables).map((table): TableOptions => {
36+
const keySchema = getKeySchema(table);
37+
const valueSchema = getValueSchema(table);
3638

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

46-
const keyTuple = Object.keys(tableData.keySchema).map((name) => {
47-
const abiOrUserType = tableData.keySchema[name];
48-
const { renderType } = resolveAbiOrUserType(abiOrUserType, config, solidityUserTypes);
48+
const keyTuple = Object.entries(keySchema).map(([name, field]): RenderKeyTuple => {
49+
const abiOrUserType = field.internalType;
50+
const { renderType } = resolveAbiOrUserType(abiOrUserType, configV1, solidityUserTypes);
4951

50-
const importDatum = importForAbiOrUserType(abiOrUserType, tableData.directory, config, solidityUserTypes);
52+
const importDatum = importForAbiOrUserType(
53+
abiOrUserType,
54+
table.codegen.outputDirectory,
55+
configV1,
56+
solidityUserTypes,
57+
);
5158
if (importDatum) imports.push(importDatum);
5259

53-
if (renderType.isDynamic) throw new Error(`Parsing error: found dynamic key ${name} in table ${tableName}`);
54-
55-
const keyTuple: RenderKeyTuple = {
60+
return {
5661
...renderType,
5762
name,
5863
isDynamic: false,
5964
};
60-
return keyTuple;
6165
});
6266

63-
const fields = Object.keys(tableData.valueSchema).map((name) => {
64-
const abiOrUserType = tableData.valueSchema[name];
65-
const { renderType, schemaType } = resolveAbiOrUserType(abiOrUserType, config, solidityUserTypes);
67+
const fields = Object.entries(valueSchema).map(([name, field]): RenderField => {
68+
const abiOrUserType = field.internalType;
69+
const { renderType, schemaType } = resolveAbiOrUserType(abiOrUserType, configV1, solidityUserTypes);
6670

67-
const importDatum = importForAbiOrUserType(abiOrUserType, tableData.directory, config, solidityUserTypes);
71+
const importDatum = importForAbiOrUserType(
72+
abiOrUserType,
73+
table.codegen.outputDirectory,
74+
configV1,
75+
solidityUserTypes,
76+
);
6877
if (importDatum) imports.push(importDatum);
6978

7079
const elementType = SchemaTypeArrayToElement[schemaType];
71-
const field: RenderField = {
80+
return {
7281
...renderType,
7382
arrayElement: elementType !== undefined ? getSchemaTypeInfo(elementType) : undefined,
7483
name,
7584
};
76-
return field;
7785
});
7886

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

8290
// With tableIdArgument: tableId is a dynamic argument for each method
8391
// Without tableIdArgument: tableId is a file-level constant generated from `staticResourceData`
84-
const staticResourceData = tableData.tableIdArgument
92+
const staticResourceData = table.codegen.tableIdArgument
8593
? undefined
8694
: {
87-
namespace: config.namespace,
88-
name: tableData.name,
89-
offchainOnly: tableData.offchainOnly,
95+
namespace: table.namespace,
96+
name: table.name,
97+
offchainOnly: table.type === "offchainTable",
9098
};
9199

92-
options.push({
93-
outputPath: path.join(tableData.directory, `${tableName}.sol`),
94-
tableName,
100+
return {
101+
outputPath: path.join(table.codegen.outputDirectory, `${table.name}.sol`),
102+
tableName: table.name,
95103
renderOptions: {
96104
imports,
97-
libraryName: tableName,
98-
structName: withStruct ? tableName + "Data" : undefined,
105+
libraryName: table.name,
106+
structName: withStruct ? table.name + "Data" : undefined,
99107
staticResourceData,
100-
storeImportPath,
108+
storeImportPath: config.codegen.storeImportPath,
101109
keyTuple,
102110
fields,
103111
staticFields,
104112
dynamicFields,
105-
withGetters: !tableData.offchainOnly,
113+
withGetters: table.type === "table",
106114
withRecordMethods,
107-
withDynamicFieldMethods: !tableData.offchainOnly,
115+
withDynamicFieldMethods: table.type === "table",
108116
withSuffixlessFieldMethods,
109-
storeArgument: tableData.storeArgument,
117+
storeArgument: table.codegen.storeArgument,
110118
},
111-
});
112-
}
119+
};
120+
});
121+
113122
return options;
114123
}

0 commit comments

Comments
 (0)