-
Notifications
You must be signed in to change notification settings - Fork 189
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): add store config parser and basic schema generator (#402)
* build(store): add store config to store for testing * feat(cli): add store config and basic schema generator * refactor(store): use some autogenerated schemas * refactor(cli): simplify shebang * build: update some typescript versions * refactor(store): use local mud.js for tablegen * build(cli): change dependencies * feat(cli): use fancy custom errors for config * feat(cli): use tagged template for conditional rendering * refactor(cli): rename config to storeConfig * chore(cli): add tablegen description Co-authored-by: alvarius <[email protected]> * feat(cli): separate config logically, improve storeConfig validation * fix(cli): make shebang work on mac --------- Co-authored-by: alvarius <[email protected]>
- Loading branch information
Showing
23 changed files
with
1,044 additions
and
194 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import type { CommandModule } from "yargs"; | ||
import { writeFileSync } from "fs"; | ||
import path from "path"; | ||
import { loadStoreConfig } from "../config/loadStoreConfig.js"; | ||
import { renderTables } from "../utils/tablegen.js"; | ||
import { getSrcDirectory } from "../utils/forgeConfig.js"; | ||
|
||
type Options = { | ||
configPath?: string; | ||
}; | ||
|
||
const commandModule: CommandModule<Options, Options> = { | ||
command: "tablegen", | ||
|
||
describe: "Autogenerate MUD Store table libraries based on the config file", | ||
|
||
builder(yargs) { | ||
return yargs.options({ | ||
configPath: { type: "string", desc: "Path to the config file" }, | ||
}); | ||
}, | ||
|
||
async handler({ configPath }) { | ||
const srcDir = await getSrcDirectory(); | ||
|
||
const config = await loadStoreConfig(configPath); | ||
const renderedTables = renderTables(config); | ||
|
||
for (const { output, tableName } of renderedTables) { | ||
const basePath = config.tables[tableName].path; | ||
const outputPath = path.join(srcDir, basePath, `${tableName}.sol`); | ||
writeFileSync(outputPath, output); | ||
console.log(`Generated schema: ${outputPath}`); | ||
} | ||
|
||
process.exit(0); | ||
}, | ||
}; | ||
|
||
export default commandModule; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { findUpSync } from "find-up"; | ||
import path from "path"; | ||
import { ERRORS, MUDError } from "../utils/errors.js"; | ||
|
||
// TODO require may or may not be needed, finish exploring the various configurations | ||
import { createRequire } from "module"; | ||
import { fileURLToPath } from "url"; | ||
const require = createRequire(fileURLToPath(import.meta.url)); | ||
|
||
// Based on hardhat's config (MIT) | ||
// https://github.com/NomicFoundation/hardhat/tree/main/packages/hardhat-core | ||
|
||
const TS_CONFIG_FILENAME = "mud.config.mts"; | ||
|
||
export async function loadConfig(configPath?: string): Promise<unknown> { | ||
configPath = resolveConfigPath(configPath); | ||
|
||
const config = (await import(configPath)).default; | ||
console.log("Config loaded:", config); | ||
return config; | ||
} | ||
|
||
function resolveConfigPath(configPath: string | undefined) { | ||
if (configPath === undefined) { | ||
configPath = getUserConfigPath(); | ||
} else { | ||
if (!path.isAbsolute(configPath)) { | ||
configPath = path.join(process.cwd(), configPath); | ||
configPath = path.normalize(configPath); | ||
} | ||
} | ||
return configPath; | ||
} | ||
|
||
function getUserConfigPath() { | ||
const tsConfigPath = findUpSync(TS_CONFIG_FILENAME); | ||
if (tsConfigPath === undefined) { | ||
throw new MUDError(ERRORS.NOT_INSIDE_PROJECT); | ||
} | ||
return tsConfigPath; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import path from "path"; | ||
import { SchemaType } from "@latticexyz/schema-type"; | ||
import { defaultKeyTuple, defaultStoreImportPath, defaultTablePath } from "../utils/constants.js"; | ||
import { ERRORS, MUDError } from "../utils/errors.js"; | ||
import { loadConfig } from "./loadConfig.js"; | ||
import { isArray, isRecord, isString } from "./utils.js"; | ||
|
||
// Based on hardhat's config (MIT) | ||
// https://github.com/NomicFoundation/hardhat/tree/main/packages/hardhat-core | ||
|
||
export interface StoreUserConfig { | ||
/** Path for store package imports. Default is "@latticexyz/store/src/" */ | ||
storeImportPath?: string; | ||
/** Configuration for each table. The keys are table names, 1st letter should be uppercase. */ | ||
tables: { | ||
[tableName: string]: { | ||
/** Output path for the file. Default is "tables/" */ | ||
path?: string; | ||
/** List of names for the table's keys. Default is ["key"] */ | ||
keyTuple?: string[]; | ||
/** Table's columns. The keys are column names, 1st letter should be lowercase. */ | ||
schema: { | ||
[columnName: string]: SchemaType; | ||
}; | ||
}; | ||
}; | ||
} | ||
|
||
export interface StoreConfig extends StoreUserConfig { | ||
storeImportPath: string; | ||
tables: { | ||
[tableName: string]: { | ||
path: string; | ||
keyTuple: string[]; | ||
schema: { | ||
[columnName: string]: SchemaType; | ||
}; | ||
}; | ||
}; | ||
} | ||
|
||
export async function loadStoreConfig(configPath?: string) { | ||
const config = await loadConfig(configPath); | ||
|
||
const validatedConfig = validateConfig(config); | ||
|
||
return resolveConfig(validatedConfig); | ||
} | ||
|
||
function validateConfig(config: unknown) { | ||
if (!isRecord(config)) { | ||
throw new MUDError(ERRORS.INVALID_CONFIG, ["Config file does not default export an object"]); | ||
} | ||
if (!isRecord(config.tables)) { | ||
throw new MUDError(ERRORS.INVALID_CONFIG, ['Config does not have a "tables" property object']); | ||
} | ||
|
||
// Collect all table-related config errors | ||
let errors: string[] = []; | ||
|
||
for (const tableName of Object.keys(config.tables)) { | ||
// validate table name | ||
if (!/^\w+$/.test(tableName)) { | ||
errors.push(`Table name "${tableName}" must contain only alphanumeric & underscore characters`); | ||
} | ||
if (!/^[A-Z]/.test(tableName)) { | ||
errors.push(`Table name "${tableName}" must start with a capital letter`); | ||
} | ||
|
||
const tableData = config.tables[tableName]; | ||
if (!isRecord(tableData)) { | ||
errors.push(`Table "${tableName}" is not a valid object`); | ||
continue; | ||
} | ||
|
||
const { keyTuple, schema } = tableData; | ||
if (!isRecord(schema)) { | ||
errors.push(`Table "${tableName}" must have a "schema" property object`); | ||
continue; | ||
} | ||
|
||
// validate schema | ||
for (const [columnName] of Object.entries(schema)) { | ||
if (!/^\w+$/.test(columnName)) { | ||
errors.push( | ||
`In table "${tableName}" schema column "${columnName}" must contain only alphanumeric & underscore characters` | ||
); | ||
} | ||
if (!/^[a-z]/.test(columnName)) { | ||
errors.push(`In table "${tableName}" schema column "${columnName}" must start with a lowercase letter`); | ||
} | ||
} | ||
// validate key names | ||
errors = errors.concat(validateKeyTuple(tableName, keyTuple)); | ||
} | ||
|
||
// Display all collected errors at once | ||
if (errors.length > 0) { | ||
throw new MUDError(ERRORS.INVALID_CONFIG, errors); | ||
} | ||
|
||
return config as unknown as StoreUserConfig; | ||
} | ||
|
||
function validateKeyTuple(tableName: string, keyTuple: unknown) { | ||
const errors: string[] = []; | ||
// keyTuple is optional, absence is valid | ||
if (keyTuple === undefined) return errors; | ||
// if present, it must be an array of strings | ||
if (!isArray(keyTuple)) { | ||
errors.push(`In table "${tableName}" property "keyTuple" must be an array of strings`); | ||
return errors; | ||
} | ||
for (const key of keyTuple) { | ||
// check that keys are strings | ||
if (!isString(key)) { | ||
errors.push(`In table "${tableName}" property "keyTuple" must be an array of strings`); | ||
continue; | ||
} | ||
// and that they are correctly formatted | ||
if (!/^\w+$/.test(key)) { | ||
errors.push(`In table "${tableName}" key "${key}" must contain only alphanumeric & underscore characters`); | ||
} | ||
if (!/^[a-z]/.test(key)) { | ||
errors.push(`In table "${tableName}" key "${key}" must start with a lowercase letter`); | ||
} | ||
} | ||
return errors; | ||
} | ||
|
||
function resolveConfig(config: StoreUserConfig) { | ||
config.storeImportPath ??= defaultStoreImportPath; | ||
// ensure the path has a trailing slash | ||
config.storeImportPath = path.join(config.storeImportPath, "/"); | ||
|
||
for (const tableName of Object.keys(config.tables)) { | ||
const table = config.tables[tableName]; | ||
table.path ??= defaultTablePath; | ||
table.keyTuple ??= defaultKeyTuple; | ||
} | ||
return config as StoreConfig; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export function isRecord(value: unknown): value is Record<string, unknown> { | ||
return typeof value === "object" && value !== null; | ||
} | ||
|
||
export function isArray(value: unknown): value is Array<unknown> { | ||
return Array.isArray(value); | ||
} | ||
|
||
export function isString(value: unknown): value is string { | ||
return typeof value === "string"; | ||
} |
Oops, something went wrong.