Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(create-mud): new react template with stash/entrykit #3478

Merged
merged 2 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cuddly-pugs-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/entrykit": patch
---

Bumped react-error-boundary dependency.
5 changes: 5 additions & 0 deletions .changeset/five-bats-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-mud": patch
---

Updated React template with Stash client state library, EntryKit for wallet support, and a cleaned up app structure.
18 changes: 10 additions & 8 deletions packages/create-mud/scripts/copy-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const __dirname = path.dirname(__filename);
const packageDir = path.resolve(__dirname, "..");
const rootDir = path.resolve(packageDir, "../..");

// TODO: could swap this with `pnpm m ls --json --depth=-1`
const mudPackageNames = await (async () => {
const files = await glob("packages/*/package.json", { cwd: rootDir });
const packages = await Promise.all(
Expand All @@ -36,16 +37,17 @@ const __dirname = path.dirname(__filename);

await fs.mkdir(path.dirname(destPath), { recursive: true });

// Replace all MUD package links with mustache placeholder used by create-create-app
// that will be replaced with the latest MUD version number when the template is used.
if (/package\.json$/.test(destPath)) {
const source = await fs.readFile(sourcePath, "utf-8");
await fs.writeFile(
destPath,
source.replace(/"([^"]+)":\s*"(link|file):[^"]+"/g, (match, packageName) =>
mudPackageNames.includes(packageName) ? `"${packageName}": "{{mud-version}}"` : match,
),
let source = await fs.readFile(sourcePath, "utf-8");
// Replace all MUD package links with mustache placeholder used by create-create-app
// that will be replaced with the latest MUD version number when the template is used.
source = source.replace(/"([^"]+)":\s*"(link|file):[^"]+"/g, (match, packageName) =>
mudPackageNames.includes(packageName) ? `"${packageName}": "{{mud-version}}"` : match,
);
const json = JSON.parse(source);
// Strip out pnpm overrides
delete json.pnpm;
await fs.writeFile(destPath, JSON.stringify(json, null, 2) + "\n");
}
// Replace template workspace root `tsconfig.json` files (which have paths relative to monorepo)
// with one that inherits our base tsconfig.
Expand Down
2 changes: 1 addition & 1 deletion packages/entrykit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"debug": "^4.3.4",
"dotenv": "^16.0.3",
"permissionless": "0.2.25",
"react-error-boundary": "^4.0.13",
"react-error-boundary": "5.0.0",
"react-merge-refs": "^2.1.1",
"tailwind-merge": "^1.12.0",
"usehooks-ts": "^3.1.0",
Expand Down
20 changes: 6 additions & 14 deletions pnpm-lock.yaml

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

19 changes: 14 additions & 5 deletions templates/react/.gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
.DS_Store
logs
*.log

node_modules

# mud artifacts
.mud
# sqlite indexer data
*.db
*.db-journal
.env.*

# foundry
cache
broadcast
out/*
!out/IWorld.sol
out/IWorld.sol/*
!out/IWorld.sol/IWorld.abi.json
!out/IWorld.sol/IWorld.abi.d.json.ts
10 changes: 9 additions & 1 deletion templates/react/mprocs.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
scrollback: 10000
procs:
client:
cwd: packages/client
shell: pnpm run dev
contracts:
cwd: packages/contracts
shell: pnpm mud dev-contracts --rpc http://127.0.0.1:8545
deploy-prereqs:
cwd: packages/contracts
shell: pnpm deploy-local-prereqs
env:
DEBUG: "mud:*"
# Anvil default account (0x70997970C51812dc3A010C7d01b50e0d17dc79C8)
PRIVATE_KEY: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
anvil:
cwd: packages/contracts
shell: anvil --base-fee 0 --block-time 2
shell: anvil --block-time 2
explorer:
cwd: packages/contracts
shell: pnpm explorer
7 changes: 7 additions & 0 deletions templates/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,12 @@
"engines": {
"node": "^18",
"pnpm": "^8 || ^9"
},
"pnpm": {
"overrides": {
"@tanstack/react-query": "link:../../packages/entrykit/node_modules/@tanstack/react-query",
"@types/react": "link:../../packages/entrykit/node_modules/@types/react",
"wagmi": "link:../../packages/entrykit/node_modules/wagmi"
}
}
}
2 changes: 0 additions & 2 deletions templates/react/packages/client/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
node_modules
dist
.DS_Store
4 changes: 2 additions & 2 deletions templates/react/packages/client/index.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>a minimal MUD client</title>
<title>a MUD app</title>
</head>
<body>
<div id="react-root"></div>
Expand Down
26 changes: 17 additions & 9 deletions templates/react/packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,39 @@
"type": "module",
"scripts": {
"build": "vite build",
"dev": "wait-port localhost:8545 && vite",
"dev": "vite",
"preview": "vite preview",
"test": "tsc --noEmit"
},
"dependencies": {
"@latticexyz/common": "link:../../../../packages/common",
"@latticexyz/dev-tools": "link:../../../../packages/dev-tools",
"@latticexyz/entrykit": "link:../../../../packages/entrykit",
"@latticexyz/explorer": "link:../../../../packages/explorer",
"@latticexyz/react": "link:../../../../packages/react",
"@latticexyz/schema-type": "link:../../../../packages/schema-type",
"@latticexyz/stash": "link:../../../../packages/stash",
"@latticexyz/store-sync": "link:../../../../packages/store-sync",
"@latticexyz/utils": "link:../../../../packages/utils",
"@latticexyz/world": "link:../../../../packages/world",
"@tanstack/react-query": "^5.63.0",
"contracts": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rxjs": "7.5.5",
"viem": "2.21.19"
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "5.0.0",
"tailwind-merge": "^2.6.0",
"viem": "2.21.19",
"wagmi": "2.12.11"
},
"devDependencies": {
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
"@vitejs/plugin-react": "^3.1.0",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0",
"vite": "^4.2.1",
"wait-port": "^1.0.4"
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"vite": "^6.0.7",
"vite-plugin-mud": "link:../../../../packages/vite-plugin-mud"
}
}
6 changes: 6 additions & 0 deletions templates/react/packages/client/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
141 changes: 41 additions & 100 deletions templates/react/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,105 +1,46 @@
import { useMUD } from "./MUDContext";

const styleUnset = { all: "unset" } as const;

export const App = () => {
const {
network: { tables, useStore },
systemCalls: { addTask, toggleTask, deleteTask },
} = useMUD();

const tasks = useStore((state) => {
const records = Object.values(state.getRecords(tables.Tasks));
records.sort((a, b) => Number(a.value.createdAt - b.value.createdAt));
return records;
});
import { stash } from "./mud/stash";
import { useRecords } from "@latticexyz/stash/react";
import { AccountButton } from "@latticexyz/entrykit/internal";
import { Direction } from "./common";
import mudConfig from "contracts/mud.config";
import { useMemo } from "react";
import { GameMap } from "./game/GameMap";
import { useWorldContract } from "./mud/useWorldContract";
import { Synced } from "./mud/Synced";
import { useSync } from "@latticexyz/store-sync/react";

export function App() {
const players = useRecords({ stash, table: mudConfig.tables.app__Position });

const sync = useSync();
const worldContract = useWorldContract();
const onMove = useMemo(
() =>
sync.data && worldContract
? async (direction: Direction) => {
const tx = await worldContract.write.app__move([mudConfig.enums.Direction.indexOf(direction)]);
await sync.data.waitForTransaction(tx);
}
: undefined,
[sync.data, worldContract],
);

return (
<>
<table>
<tbody>
{tasks.map((task) => (
<tr key={task.id}>
<td align="right">
<input
type="checkbox"
checked={task.value.completedAt > 0n}
title={task.value.completedAt === 0n ? "Mark task as completed" : "Mark task as incomplete"}
onChange={async (event) => {
event.preventDefault();
const checkbox = event.currentTarget;

checkbox.disabled = true;
try {
await toggleTask(task.key.id);
} finally {
checkbox.disabled = false;
}
}}
/>
</td>
<td>{task.value.completedAt > 0n ? <s>{task.value.description}</s> : <>{task.value.description}</>}</td>
<td align="right">
<button
type="button"
title="Delete task"
style={styleUnset}
onClick={async (event) => {
event.preventDefault();
if (!window.confirm("Are you sure you want to delete this task?")) return;

const button = event.currentTarget;
button.disabled = true;
try {
await deleteTask(task.key.id);
} finally {
button.disabled = false;
}
}}
>
&times;
</button>
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td>
<input type="checkbox" disabled />
</td>
<td colSpan={2}>
<form
onSubmit={async (event) => {
event.preventDefault();
const form = event.currentTarget;
const fieldset = form.querySelector("fieldset");
if (!(fieldset instanceof HTMLFieldSetElement)) return;

const formData = new FormData(form);
const desc = formData.get("description");
if (typeof desc !== "string") return;

fieldset.disabled = true;
try {
await addTask(desc);
form.reset();
} finally {
fieldset.disabled = false;
}
}}
>
<fieldset style={styleUnset}>
<input type="text" name="description" />{" "}
<button type="submit" title="Add task">
Add
</button>
</fieldset>
</form>
</td>
</tr>
</tfoot>
</table>
<div className="fixed inset-0 grid place-items-center p-4">
<Synced
fallback={({ message, percentage }) => (
<div className="tabular-nums">
{message} ({percentage.toFixed(1)}%)…
</div>
)}
>
<GameMap players={players} onMove={onMove} />
</Synced>
</div>
<div className="fixed top-2 right-2">
<AccountButton />
</div>
</>
);
};
}
Loading
Loading