@latticexyz/cli 1.41.1-alpha.41 → 1.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/chunk-4STWSICF.js +26139 -0
  2. package/dist/chunk-6V563IAZ.js +283 -0
  3. package/dist/chunk-7KQJTK2K.js +3842 -0
  4. package/dist/{chunk-ATAWDHWC.js → chunk-FPG73MVN.js} +5 -1
  5. package/dist/{chunk-O6HOO6WA.js → chunk-L4YLJHLJ.js} +1 -9
  6. package/dist/{chunk-J4DJQNIC.js → chunk-SKNB74MT.js} +129 -568
  7. package/dist/chunk-VQTZJIFF.js +353 -0
  8. package/dist/chunk-WZFXLDPK.js +761 -0
  9. package/dist/index.d.ts +2 -3
  10. package/dist/index.js +0 -21
  11. package/dist/mud.d.ts +1 -1
  12. package/dist/mud.js +326 -4452
  13. package/dist/mud2.d.ts +1 -0
  14. package/dist/mud2.js +25 -0
  15. package/dist/render-solidity/index.d.ts +171 -0
  16. package/dist/render-solidity/index.js +49 -0
  17. package/dist/render-ts/index.d.ts +26 -0
  18. package/dist/render-ts/index.js +14 -0
  19. package/dist/utils/deprecated/index.js +3 -3
  20. package/dist/utils/index.d.ts +13 -18
  21. package/dist/utils/index.js +14 -16
  22. package/package.json +21 -21
  23. package/src/commands/deploy-v2.ts +80 -64
  24. package/src/commands/deprecated/index.ts +22 -0
  25. package/src/commands/deprecated/test.ts +1 -1
  26. package/src/commands/gas-report.ts +54 -55
  27. package/src/commands/index.ts +6 -17
  28. package/src/commands/set-version.ts +172 -0
  29. package/src/commands/tablegen.ts +5 -5
  30. package/src/commands/test-v2.ts +71 -0
  31. package/src/commands/tsgen.ts +33 -0
  32. package/src/commands/worldgen.ts +5 -4
  33. package/src/contracts/BulkUpload.sol +13 -20
  34. package/src/contracts/Deploy.sol +3 -3
  35. package/src/contracts/LibDeploy.sol +1 -1
  36. package/src/contracts/LibDeployStub.sol +1 -1
  37. package/src/index.ts +1 -15
  38. package/src/mud.ts +4 -3
  39. package/src/mud2.ts +29 -0
  40. package/src/render-solidity/common.ts +4 -4
  41. package/src/render-solidity/field.ts +25 -2
  42. package/src/render-solidity/record.ts +14 -10
  43. package/src/render-solidity/renderSystemInterface.ts +4 -4
  44. package/src/render-solidity/renderTableIndex.ts +15 -0
  45. package/src/render-solidity/renderTypesFromConfig.ts +1 -1
  46. package/src/render-solidity/tableOptions.ts +2 -2
  47. package/src/render-solidity/tablegen.ts +15 -13
  48. package/src/render-solidity/types.ts +2 -1
  49. package/src/render-solidity/userType.ts +1 -2
  50. package/src/render-solidity/worldgen.ts +8 -9
  51. package/src/render-ts/index.ts +5 -0
  52. package/src/render-ts/recsV1TableOptions.ts +39 -0
  53. package/src/render-ts/renderRecsV1Tables.ts +31 -0
  54. package/src/render-ts/schemaTypesToRecsTypeStrings.ts +202 -0
  55. package/src/render-ts/tsgen.ts +12 -0
  56. package/src/render-ts/types.ts +13 -0
  57. package/src/utils/contractToInterface.ts +5 -3
  58. package/src/utils/deploy-v2.ts +90 -84
  59. package/src/utils/errors.ts +3 -34
  60. package/src/utils/format.ts +6 -0
  61. package/src/utils/formatAndWrite.ts +11 -2
  62. package/src/utils/foundry.ts +9 -0
  63. package/src/utils/index.ts +1 -0
  64. package/dist/chunk-O57QENJ6.js +0 -23039
  65. package/dist/chunk-SLIMIO4Z.js +0 -14358
  66. package/dist/config/index.d.ts +0 -763
  67. package/dist/config/index.js +0 -83
  68. package/src/config/commonSchemas.ts +0 -34
  69. package/src/config/dynamicResolution.ts +0 -49
  70. package/src/config/index.ts +0 -24
  71. package/src/config/loadConfig.ts +0 -39
  72. package/src/config/loadStoreConfig.ts +0 -18
  73. package/src/config/parseStoreConfig.test-d.ts +0 -40
  74. package/src/config/parseStoreConfig.ts +0 -314
  75. package/src/config/validation.ts +0 -163
  76. package/src/config/world/index.ts +0 -4
  77. package/src/config/world/loadWorldConfig.test-d.ts +0 -11
  78. package/src/config/world/loadWorldConfig.ts +0 -26
  79. package/src/config/world/parseWorldConfig.ts +0 -55
  80. package/src/config/world/resolveWorldConfig.ts +0 -80
  81. package/src/config/world/userTypes.ts +0 -72
  82. package/src/utils/typeUtils.ts +0 -17
@@ -1,80 +1,81 @@
1
1
  import chalk from "chalk";
2
2
  import glob from "glob";
3
3
  import path, { basename } from "path";
4
- import type { CommandModule } from "yargs";
5
- import { loadWorldConfig } from "../config/world/index.js";
4
+ import type { CommandModule, Options } from "yargs";
5
+ import { loadStoreConfig, loadWorldConfig } from "@latticexyz/config";
6
+ import { MUDError } from "@latticexyz/config";
6
7
  import { deploy } from "../utils/deploy-v2.js";
7
- import { logError, MUDError } from "../utils/errors.js";
8
+ import { logError } from "../utils/errors.js";
8
9
  import { forge, getRpcUrl, getSrcDirectory } from "../utils/foundry.js";
9
10
  import { mkdirSync, writeFileSync } from "fs";
10
- import { loadStoreConfig } from "../config/loadStoreConfig.js";
11
11
  import { getChainId } from "../utils/getChainId.js";
12
12
 
13
- type Options = {
13
+ export type DeployOptions = {
14
14
  configPath?: string;
15
15
  printConfig?: boolean;
16
16
  profile?: string;
17
- privateKey: string;
18
17
  priorityFeeMultiplier: number;
19
18
  clean?: boolean;
20
19
  debug?: boolean;
20
+ saveDeployment?: boolean;
21
+ rpc?: string;
22
+ worldAddress?: string;
23
+ srcDir?: string;
21
24
  };
22
25
 
23
- const commandModule: CommandModule<Options, Options> = {
24
- command: "deploy-v2",
25
-
26
- describe: "Deploy MUD v2 contracts",
27
-
28
- builder(yargs) {
29
- return yargs.options({
30
- configPath: { type: "string", desc: "Path to the config file" },
31
- clean: { type: "boolean", desc: "Remove the build forge artifacts and cache directories before building" },
32
- printConfig: { type: "boolean", desc: "Print the resolved config" },
33
- profile: { type: "string", desc: "The foundry profile to use" },
34
- debug: { type: "boolean", desc: "Print debug logs, like full error messages" },
35
- priorityFeeMultiplier: {
36
- type: "number",
37
- desc: "Multiply the estimated priority fee by the provided factor",
38
- default: 1,
39
- },
40
- });
26
+ export const yDeployOptions = {
27
+ configPath: { type: "string", desc: "Path to the config file" },
28
+ clean: { type: "boolean", desc: "Remove the build forge artifacts and cache directories before building" },
29
+ printConfig: { type: "boolean", desc: "Print the resolved config" },
30
+ profile: { type: "string", desc: "The foundry profile to use" },
31
+ debug: { type: "boolean", desc: "Print debug logs, like full error messages" },
32
+ priorityFeeMultiplier: {
33
+ type: "number",
34
+ desc: "Multiply the estimated priority fee by the provided factor",
35
+ default: 1,
41
36
  },
42
-
43
- async handler(args) {
44
- args.profile = args.profile ?? process.env.FOUNDRY_PROFILE;
45
- const { configPath, printConfig, profile, clean } = args;
46
-
47
- const rpc = await getRpcUrl(profile);
48
- console.log(
49
- chalk.bgBlue(
50
- chalk.whiteBright(`\n Deploying MUD v2 contracts${profile ? " with profile " + profile : ""} to RPC ${rpc} \n`)
51
- )
52
- );
53
-
54
- if (clean) await forge(["clean"], { profile });
55
-
56
- // Run forge build
57
- await forge(["build"], { profile });
58
-
59
- // Get a list of all contract names
60
- const srcDir = await getSrcDirectory();
61
- const existingContracts = glob
62
- .sync(`${srcDir}/**/*.sol`)
63
- // Get the basename of the file
64
- .map((path) => basename(path, ".sol"));
65
-
66
- // Load and resolve the config
67
- const worldConfig = await loadWorldConfig(configPath, existingContracts);
68
- const storeConfig = await loadStoreConfig(configPath);
69
- const mudConfig = { ...worldConfig, ...storeConfig };
70
-
71
- if (printConfig) console.log(chalk.green("\nResolved config:\n"), JSON.stringify(mudConfig, null, 2));
72
-
73
- try {
74
- const privateKey = process.env.PRIVATE_KEY;
75
- if (!privateKey) throw new MUDError("Missing PRIVATE_KEY environment variable");
76
- const deploymentInfo = await deploy(mudConfig, { ...args, rpc, privateKey });
77
-
37
+ saveDeployment: { type: "boolean", desc: "Save the deployment info to a file", default: true },
38
+ rpc: { type: "string", desc: "The RPC URL to use. Defaults to the RPC url from the local foundry.toml" },
39
+ worldAddress: { type: "string", desc: "Deploy to an existing World at the given address" },
40
+ srcDir: { type: "string", desc: "Source directory. Defaults to foundry src directory." },
41
+ } satisfies Record<keyof DeployOptions, Options>;
42
+
43
+ export async function deployHandler(args: Parameters<(typeof commandModule)["handler"]>[0]) {
44
+ args.profile = args.profile ?? process.env.FOUNDRY_PROFILE;
45
+ const { configPath, printConfig, profile, clean } = args;
46
+
47
+ const rpc = args.rpc ?? (await getRpcUrl(profile));
48
+ console.log(
49
+ chalk.bgBlue(
50
+ chalk.whiteBright(`\n Deploying MUD v2 contracts${profile ? " with profile " + profile : ""} to RPC ${rpc} \n`)
51
+ )
52
+ );
53
+
54
+ if (clean) await forge(["clean"], { profile });
55
+
56
+ // Run forge build
57
+ await forge(["build"], { profile });
58
+
59
+ // Get a list of all contract names
60
+ const srcDir = args?.srcDir ?? (await getSrcDirectory());
61
+ const existingContracts = glob
62
+ .sync(`${srcDir}/**/*.sol`)
63
+ // Get the basename of the file
64
+ .map((path) => basename(path, ".sol"));
65
+
66
+ // Load and resolve the config
67
+ const worldConfig = await loadWorldConfig(configPath, existingContracts);
68
+ const storeConfig = await loadStoreConfig(configPath);
69
+ const mudConfig = { ...worldConfig, ...storeConfig };
70
+
71
+ if (printConfig) console.log(chalk.green("\nResolved config:\n"), JSON.stringify(mudConfig, null, 2));
72
+
73
+ try {
74
+ const privateKey = process.env.PRIVATE_KEY;
75
+ if (!privateKey) throw new MUDError("Missing PRIVATE_KEY environment variable");
76
+ const deploymentInfo = await deploy(mudConfig, { ...args, rpc, privateKey });
77
+
78
+ if (args.saveDeployment) {
78
79
  // Write deployment result to file (latest and timestamp)
79
80
  const chainId = await getChainId(rpc);
80
81
  const outputDir = path.join(mudConfig.deploysDirectory, chainId.toString());
@@ -83,12 +84,27 @@ const commandModule: CommandModule<Options, Options> = {
83
84
  writeFileSync(path.join(outputDir, Date.now() + ".json"), JSON.stringify(deploymentInfo, null, 2));
84
85
 
85
86
  console.log(chalk.bgGreen(chalk.whiteBright(`\n Deployment result (written to ${outputDir}): \n`)));
86
- console.log(deploymentInfo);
87
- } catch (error: any) {
88
- logError(error);
89
- process.exit(1);
90
87
  }
91
88
 
89
+ console.log(deploymentInfo);
90
+ return deploymentInfo;
91
+ } catch (error: any) {
92
+ logError(error);
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ const commandModule: CommandModule<DeployOptions, DeployOptions> = {
98
+ command: "deploy-v2",
99
+
100
+ describe: "Deploy MUD v2 contracts",
101
+
102
+ builder(yargs) {
103
+ return yargs.options(yDeployOptions);
104
+ },
105
+
106
+ async handler(args) {
107
+ await deployHandler(args);
92
108
  process.exit(0);
93
109
  },
94
110
  };
@@ -0,0 +1,22 @@
1
+ import { CommandModule } from "yargs";
2
+
3
+ import bulkupload from "./bulkupload.js";
4
+ import callSystem from "./call-system.js";
5
+ import codegenLibdeploy from "./codegen-libdeploy.js";
6
+ import deployContracts from "./deploy-contracts.js";
7
+ import systemTypes from "./system-types.js";
8
+ import test from "./test.js";
9
+ import trace from "./trace.js";
10
+ import types from "./types.js";
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Each command has different options
13
+ export const commands: CommandModule<any, any>[] = [
14
+ bulkupload,
15
+ callSystem,
16
+ deployContracts,
17
+ codegenLibdeploy,
18
+ systemTypes,
19
+ test,
20
+ trace,
21
+ types,
22
+ ];
@@ -43,7 +43,7 @@ const commandModule: CommandModule<Options, Options> = {
43
43
  });
44
44
 
45
45
  await child;
46
-
46
+
47
47
  // Reset LibDeploy.sol
48
48
  console.log("Reset LibDeploy.sol");
49
49
  await resetLibDeploy(testDir);
@@ -39,6 +39,8 @@ type GasReportEntry = {
39
39
 
40
40
  type GasReport = GasReportEntry[];
41
41
 
42
+ const tempFileSuffix = "MudGasReport";
43
+
42
44
  const commandModule: CommandModule<Options, Options> = {
43
45
  command: "gas-report",
44
46
 
@@ -52,43 +54,43 @@ const commandModule: CommandModule<Options, Options> = {
52
54
  });
53
55
  },
54
56
 
55
- async handler({ path, save, compare }) {
56
- let gasReport: GasReport = [];
57
+ async handler({ path: files, save, compare }) {
58
+ const validFiles = files.filter((file) => file.endsWith(".t.sol"));
59
+ const tempFiles = await Promise.all(validFiles.map((file) => createGasReport(file)));
60
+
61
+ process.once("SIGINT", () => {
62
+ console.log("caught sigint, deleting temp files");
63
+ tempFiles.forEach((file) => rmSync(file));
64
+ });
57
65
 
58
- // Iterate through all files provided in the path
59
- for (const file of path) {
60
- gasReport = gasReport.concat(await runGasReport(file));
66
+ let gasReport: GasReport;
67
+ try {
68
+ gasReport = await runGasReport();
69
+ } catch {
70
+ setTimeout(() => process.exit());
71
+ return;
72
+ } finally {
73
+ // Delete the temporary files
74
+ tempFiles.forEach((file) => rmSync(file));
61
75
  }
62
76
 
63
77
  // If this gas report should be compared to an existing one, load the existing one
64
- const compareGasReport: GasReport = [];
65
78
  if (compare) {
66
79
  try {
67
- const compareFileContents = readFileSync(compare, "utf8");
68
- // Create a regex to extract the name, function call and gas used
69
- const compareGasReportRegex = new RegExp(/\((.*)\) \| (.*) \[(.*)\]: (.*)/g);
70
- // Loop through the matches and add the resuls to the compareGasReport
71
- let compareGasReportMatch;
72
- while ((compareGasReportMatch = compareGasReportRegex.exec(compareFileContents)) !== null) {
73
- const source = compareGasReportMatch[1];
74
- const name = compareGasReportMatch[2];
75
- const functionCall = compareGasReportMatch[3];
76
- const gasUsed = compareGasReportMatch[4];
77
-
78
- compareGasReport.push({ source, name, functionCall, gasUsed: parseInt(gasUsed) });
79
- }
80
+ const compareGasReport: GasReport = JSON.parse(readFileSync(compare, "utf8"));
81
+ // Merge the previous gas report with the new one
82
+ gasReport = gasReport.map((entry) => {
83
+ const prevEntry = compareGasReport.find(
84
+ (e) => e.name === entry.name && e.functionCall === entry.functionCall
85
+ );
86
+ return { ...entry, prevGasUsed: prevEntry?.gasUsed };
87
+ });
80
88
  } catch {
81
89
  console.log(chalk.red(`Gas report to compare not found: ${compare}`));
82
90
  compare = undefined;
83
91
  }
84
92
  }
85
93
 
86
- // Merge the previous gas report with the new one
87
- gasReport = gasReport.map((entry) => {
88
- const prevEntry = compareGasReport.find((e) => e.name === entry.name && e.functionCall === entry.functionCall);
89
- return { ...entry, prevGasUsed: prevEntry?.gasUsed };
90
- });
91
-
92
94
  // Print gas report
93
95
  printGasReport(gasReport, compare);
94
96
 
@@ -101,16 +103,11 @@ const commandModule: CommandModule<Options, Options> = {
101
103
 
102
104
  export default commandModule;
103
105
 
104
- async function runGasReport(path: string): Promise<GasReport> {
105
- if (!path.endsWith(".t.sol")) {
106
- console.log("Skipping gas report for", chalk.bold(path), "(not a test file)");
107
- return [];
108
- }
109
- console.log("Running gas report for", chalk.bold(path));
110
- const gasReport: GasReport = [];
106
+ async function createGasReport(filename: string): Promise<string> {
107
+ console.log("Creating gas report for", chalk.bold(filename));
111
108
 
112
109
  // Parse the given test file, and add gas reporting wherever requested by a `// !gasreport` comment
113
- const fileContents = readFileSync(path, "utf8");
110
+ const fileContents = readFileSync(filename, "utf8");
114
111
  let newFile = fileContents;
115
112
 
116
113
  // Use a regex to find first line of each function
@@ -139,7 +136,7 @@ async function runGasReport(path: string): Promise<GasReport> {
139
136
  _gasreport = gasleft();
140
137
  ${functionCall}
141
138
  _gasreport = _gasreport - gasleft();
142
- console.log("GAS REPORT: ${name} [${functionCall.replaceAll('"', '\\"')}]:", _gasreport);`
139
+ console.log("GAS REPORT(${filename}): ${name} [${functionCall.replaceAll('"', '\\"')}]:", _gasreport);`
143
140
  );
144
141
  }
145
142
 
@@ -148,41 +145,47 @@ console.log("GAS REPORT: ${name} [${functionCall.replaceAll('"', '\\"')}]:", _ga
148
145
 
149
146
  // Write the new file to disk (temporarily)
150
147
  // Create the temp file by replacing the previous file name with MudGasReport
151
- const tempFileName = path.replace(/\.t\.sol$/, "MudGasReport.t.sol");
148
+ const tempFileName = filename.replace(/\.t\.sol$/, `${tempFileSuffix}.t.sol`);
152
149
  writeFileSync(tempFileName, newFile);
153
150
 
154
- // Run the generated file using forge
155
- const child = execa("forge", ["test", "--match-path", tempFileName, "-vvv"], {
156
- stdio: ["inherit", "pipe", "inherit"],
157
- });
151
+ return tempFileName;
152
+ }
153
+
154
+ async function runGasReport(): Promise<GasReport> {
155
+ console.log("Running gas report");
156
+ const gasReport: GasReport = [];
158
157
 
159
158
  // Extract the logs from the child process
160
159
  let logs = "";
161
160
  try {
161
+ // Run the generated file using forge
162
+ const child = execa("forge", ["test", "--match-path", `*${tempFileSuffix}*`, "-vvv"], {
163
+ stdio: ["inherit", "pipe", "inherit"],
164
+ });
162
165
  logs = (await child).stdout;
163
- rmSync(tempFileName);
164
- } catch (e: any) {
165
- console.log(e.stdout ?? e);
166
+ } catch (error: any) {
167
+ console.log(error.stdout ?? error);
166
168
  console.log(chalk.red("\n-----------\nError while running the gas report (see above)"));
167
- rmSync(tempFileName);
168
- process.exit();
169
+ throw error;
169
170
  }
170
171
 
171
172
  // Extract the gas reports from the logs
172
173
 
173
174
  // Create a regex to find all lines starting with `GAS REPORT:` and extract the name, function call and gas used
174
- const gasReportRegex = new RegExp(/GAS REPORT: (.*) \[(.*)\]: (.*)/g);
175
+ const gasReportRegex = new RegExp(/GAS REPORT\((.*)\): (.*) \[(.*)\]: (.*)/g);
175
176
 
176
177
  // Loop through the matches and print the gas report
177
178
  let gasReportMatch;
178
179
  while ((gasReportMatch = gasReportRegex.exec(logs)) !== null) {
179
- const name = gasReportMatch[1];
180
- const functionCall = gasReportMatch[2].replace(";", "");
181
- const gasUsed = gasReportMatch[3];
182
-
183
- gasReport.push({ source: path, name, functionCall, gasUsed: parseInt(gasUsed) });
180
+ const source = gasReportMatch[1];
181
+ const name = gasReportMatch[2];
182
+ const functionCall = gasReportMatch[3].replace(";", "");
183
+ const gasUsed = parseInt(gasReportMatch[4]);
184
+ gasReport.push({ source, name, functionCall, gasUsed });
184
185
  }
185
186
 
187
+ gasReport.sort((a, b) => a.source.localeCompare(b.source));
188
+
186
189
  return gasReport;
187
190
  }
188
191
 
@@ -212,9 +215,5 @@ function printGasReport(gasReport: GasReport, compare?: string) {
212
215
 
213
216
  function saveGasReport(gasReport: GasReport, path: string) {
214
217
  console.log(chalk.bold(`Saving gas report to ${path}`));
215
- const serializedGasReport = gasReport
216
- .map((entry) => `(${entry.source}) | ${entry.name} [${entry.functionCall}]: ${entry.gasUsed}`)
217
- .join("\n");
218
-
219
- writeFileSync(path, serializedGasReport);
218
+ writeFileSync(path, `${JSON.stringify(gasReport, null, 2)}\n`);
220
219
  }
@@ -1,37 +1,26 @@
1
1
  import { CommandModule } from "yargs";
2
2
 
3
- import bulkupload from "./deprecated/bulkupload.js";
4
- import callSystem from "./deprecated/call-system.js";
5
- import codegenLibdeploy from "./deprecated/codegen-libdeploy.js";
6
- import deployContracts from "./deprecated/deploy-contracts.js";
7
- import systemTypes from "./deprecated/system-types.js";
8
- import test from "./deprecated/test.js";
9
- import trace from "./deprecated/trace.js";
10
- import types from "./deprecated/types.js";
11
-
12
3
  import devnode from "./devnode.js";
13
4
  import faucet from "./faucet.js";
14
5
  import gasReport from "./gas-report.js";
15
6
  import hello from "./hello.js";
16
7
  import tablegen from "./tablegen.js";
8
+ import tsgen from "./tsgen.js";
17
9
  import deployV2 from "./deploy-v2.js";
18
10
  import worldgen from "./worldgen.js";
11
+ import setVersion from "./set-version.js";
12
+ import testV2 from "./test-v2.js";
19
13
 
20
14
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Each command has different options
21
15
  export const commands: CommandModule<any, any>[] = [
22
- bulkupload,
23
- callSystem,
24
- codegenLibdeploy,
25
- deployContracts,
26
16
  deployV2,
27
17
  devnode,
28
18
  faucet,
29
19
  gasReport,
30
20
  hello,
31
- systemTypes,
32
21
  tablegen,
33
- test,
34
- trace,
35
- types,
22
+ tsgen,
36
23
  worldgen,
24
+ setVersion,
25
+ testV2,
37
26
  ];
@@ -0,0 +1,172 @@
1
+ import chalk from "chalk";
2
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
3
+ import path from "path";
4
+ import type { CommandModule } from "yargs";
5
+ import { MUDError } from "@latticexyz/config";
6
+ import { logError } from "../utils/errors.js";
7
+ import localPackageJson from "../../package.json" assert { type: "json" };
8
+
9
+ type Options = {
10
+ backup?: boolean;
11
+ force?: boolean;
12
+ restore?: boolean;
13
+ mudVersion?: string;
14
+ };
15
+
16
+ const BACKUP_FILE = ".mudbackup";
17
+ const MUD_PREFIX = "@latticexyz";
18
+
19
+ const commandModule: CommandModule<Options, Options> = {
20
+ command: "set-version",
21
+
22
+ describe: "Install a custom MUD version and optionally backup the previously installed version",
23
+
24
+ builder(yargs) {
25
+ return yargs.options({
26
+ backup: { type: "boolean", description: `Back up the current MUD versions to "${BACKUP_FILE}"` },
27
+ force: {
28
+ type: "boolean",
29
+ description: `Backup fails if a "${BACKUP_FILE}" file is found, unless --force is provided`,
30
+ },
31
+ restore: { type: "boolean", description: `Restore the previous MUD versions from "${BACKUP_FILE}"` },
32
+ mudVersion: { alias: "v", type: "string", description: "The MUD version to install" },
33
+ });
34
+ },
35
+
36
+ async handler(options) {
37
+ try {
38
+ if (!options.mudVersion && !options.restore) {
39
+ throw new MUDError(`Version parameter is required unless --restore is provided.`);
40
+ }
41
+
42
+ // Resolve the `canary` version number if needed
43
+ options.mudVersion =
44
+ options.mudVersion === "canary" ? await getCanaryVersion(localPackageJson.name) : options.mudVersion;
45
+
46
+ // Read the current package.json
47
+ const rootPath = "./package.json";
48
+ const { workspaces } = updatePackageJson(rootPath, options);
49
+
50
+ // Load the package.json of each workspace
51
+ if (workspaces) {
52
+ for (const workspace of workspaces) {
53
+ const filePath = path.join(workspace, "/package.json");
54
+ updatePackageJson(filePath, options);
55
+ }
56
+ }
57
+ } catch (e) {
58
+ logError(e);
59
+ } finally {
60
+ process.exit(0);
61
+ }
62
+ },
63
+ };
64
+
65
+ function updatePackageJson(filePath: string, options: Options): { workspaces?: string[] } {
66
+ const { backup, restore, force, mudVersion } = options;
67
+ const backupFilePath = path.join(path.dirname(filePath), BACKUP_FILE);
68
+
69
+ // If `backup` is true and force not set, check if a backup file already exists and throw an error if it does
70
+ if (backup && !force && existsSync(backupFilePath)) {
71
+ throw new MUDError(
72
+ `A backup file already exists at ${backupFilePath}.\nUse --force to overwrite it or --restore to restore it.`
73
+ );
74
+ }
75
+
76
+ const packageJson = readPackageJson(filePath);
77
+
78
+ // Load .mudbackup if `restore` is true
79
+ const backupJson = restore ? readPackageJson(backupFilePath) : undefined;
80
+
81
+ // Find all MUD dependencies
82
+ const mudDependencies: Record<string, string> = {};
83
+ for (const key in packageJson.dependencies) {
84
+ if (key.startsWith(MUD_PREFIX)) {
85
+ mudDependencies[key] = packageJson.dependencies[key];
86
+ }
87
+ }
88
+
89
+ // Find all MUD devDependencies
90
+ const mudDevDependencies: Record<string, string> = {};
91
+ for (const key in packageJson.devDependencies) {
92
+ if (key.startsWith(MUD_PREFIX)) {
93
+ mudDevDependencies[key] = packageJson.devDependencies[key];
94
+ }
95
+ }
96
+
97
+ // Back up the current dependencies if `backup` is true
98
+ if (backup) {
99
+ writeFileSync(
100
+ backupFilePath,
101
+ JSON.stringify({ dependencies: mudDependencies, devDependencies: mudDevDependencies }, null, 2)
102
+ );
103
+ console.log(chalk.green(`Backed up MUD dependencies from ${filePath} to ${backupFilePath}`));
104
+ }
105
+
106
+ // Update the dependencies
107
+ for (const key in packageJson.dependencies) {
108
+ if (key.startsWith(MUD_PREFIX)) {
109
+ packageJson.dependencies[key] =
110
+ restore && backupJson ? backupJson.dependencies[key] : mudVersion || packageJson.dependencies[key];
111
+ }
112
+ }
113
+
114
+ // Update the devDependencies
115
+ for (const key in packageJson.devDependencies) {
116
+ if (key.startsWith(MUD_PREFIX)) {
117
+ packageJson.devDependencies[key] =
118
+ restore && backupJson ? backupJson.devDependencies[key] : mudVersion || packageJson.devDependencies[key];
119
+ }
120
+ }
121
+
122
+ // Write the updated package.json
123
+ writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + "\n");
124
+
125
+ console.log(`Updating ${filePath}`);
126
+ logComparison(mudDependencies, packageJson.dependencies);
127
+ logComparison(mudDevDependencies, packageJson.devDependencies);
128
+
129
+ // Remove the backup file if `restore` is true and `backup` is false
130
+ // because the old backup file is no longer needed
131
+ if (restore && !backup) {
132
+ rmSync(backupFilePath);
133
+ console.log(chalk.green(`Cleaned up ${backupFilePath}`));
134
+ }
135
+
136
+ return packageJson;
137
+ }
138
+
139
+ function readPackageJson(path: string): {
140
+ workspaces?: string[];
141
+ dependencies: Record<string, string>;
142
+ devDependencies: Record<string, string>;
143
+ } {
144
+ try {
145
+ const jsonString = readFileSync(path, "utf8");
146
+ return JSON.parse(jsonString);
147
+ } catch {
148
+ throw new MUDError("Could not read JSON at " + path);
149
+ }
150
+ }
151
+
152
+ async function getCanaryVersion(pkg: string) {
153
+ try {
154
+ console.log(chalk.blue("fetching MUD canary version..."));
155
+ const result = await (await fetch(`https://registry.npmjs.org/${pkg}`)).json();
156
+ const canary = result["dist-tags"].canary;
157
+ console.log(chalk.green("MUD canary version:", canary));
158
+ return canary;
159
+ } catch (e) {
160
+ throw new MUDError(`Could not fetch canary version of ${pkg}`);
161
+ }
162
+ }
163
+
164
+ function logComparison(prev: Record<string, string>, curr: Record<string, string>) {
165
+ for (const key in prev) {
166
+ if (prev[key] !== curr[key]) {
167
+ console.log(`${key}: ${chalk.red(prev[key])} -> ${chalk.green(curr[key])}`);
168
+ }
169
+ }
170
+ }
171
+
172
+ export default commandModule;
@@ -1,7 +1,8 @@
1
+ import path from "path";
1
2
  import type { CommandModule } from "yargs";
2
- import { loadStoreConfig } from "../config/loadStoreConfig.js";
3
- import { getSrcDirectory } from "../utils/foundry.js";
3
+ import { loadStoreConfig } from "@latticexyz/config";
4
4
  import { tablegen } from "../render-solidity/tablegen.js";
5
+ import { getSrcDirectory } from "../utils/index.js";
5
6
 
6
7
  type Options = {
7
8
  configPath?: string;
@@ -19,11 +20,10 @@ const commandModule: CommandModule<Options, Options> = {
19
20
  },
20
21
 
21
22
  async handler({ configPath }) {
22
- const srcDirectory = await getSrcDirectory();
23
-
24
23
  const config = await loadStoreConfig(configPath);
24
+ const srcDir = await getSrcDirectory();
25
25
 
26
- await tablegen(config, srcDirectory);
26
+ await tablegen(config, path.join(srcDir, config.codegenDirectory));
27
27
 
28
28
  process.exit(0);
29
29
  },