Skip to content

Commit

Permalink
Init command (#5)
Browse files Browse the repository at this point in the history
* rename cli package

* rename cli

* creates init command

* cleanup

* adds changeset

* fix missing types

* fix dependencies

* test different bun version

* tries disabling macro

* tries with bun canary

* hardcode template in a string because of bun's macro bug
julia-script authored Feb 16, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 5fdeead commit 52bf8c3
Showing 26 changed files with 447 additions and 139 deletions.
7 changes: 7 additions & 0 deletions .changeset/proud-dingos-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"dappstore": patch
"@evmos/dappstore-sdk": patch
"@evmos/dev-wrapper": patch
---

Creates init command
5 changes: 5 additions & 0 deletions .changeset/smooth-mails-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"dappstore": patch
---

Rename dappstore cli package
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
build:
name: Release
name: CI
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
143 changes: 73 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,81 +1,84 @@
# Turborepo starter
# DAppStore SDK client

This is an official starter Turborepo.
## Standalone usage:

## Using this example
```ts
import { createDAppStoreClient } from "@evmos/dappstore-sdk";

Run the following command:
// Waits for the client to establish a connection to the DAppStore
await dappstore.initialized;

```sh
npx create-turbo@latest
console.log(
`DAppStore client initialized.`,
`Chain ID: ${dappstore.chainId}, Accounts: ${dappstore.accounts}`
); // -> DAppStore client initialized. Chain ID: evmos:1, Accounts: ["0x..."]
```

## What's inside?

This Turborepo includes the following packages/apps:

### Apps and Packages

- `docs`: a [Next.js](https://nextjs.org/) app
- `web`: another [Next.js](https://nextjs.org/) app
- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications
- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo

Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).

### Utilities

This Turborepo has some additional tools already setup for you:

- [TypeScript](https://www.typescriptlang.org/) for static type checking
- [ESLint](https://eslint.org/) for code linting
- [Prettier](https://prettier.io) for code formatting

### Build

To build all apps and packages, run the following command:

```
cd my-turborepo
pnpm build
```

### Develop

To develop all apps and packages, run the following command:

## Subscribe to account and chain id changes:

```ts
// Shorthand for dappstore.provider.on("accountsChanged", (accounts) => { ... })
dappstore.onAccountsChange((accounts) => {
console.log(`Accounts changed: ${accounts}`); // -> Accounts changed: ["0x..."]
});
*
// Shorthand for dappstore.provider.on("chainChanged", (chainId) => { ... })
dappstore.onChainChange((chainId) => {
console.log(`Chain changed: ${chainId}`); // -> Chain changed: evmos:1
});

// Or interact directly with the provider
dappstore.provider.request({ method: "eth_requestAccounts" }).then((accounts) => {
console.log(`Accounts: ${accounts}`); // -> Accounts: ["0x..."]
});
```
cd my-turborepo
pnpm dev
```

### Remote Caching

Turborepo can use a technique known as [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.

By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup), then enter the following commands:

```
cd my-turborepo
npx turbo login
## Usage with React:

```tsx
const dappstore = createDAppStoreClient();
import { useEffect, useState } from "react";

const useAccounts = () => {
const [accounts, setAccounts] = useState<`0x${string}`[]>(dappstore.accounts);
useEffect(() => {
return dappstore.onAccountsChange(setAccounts); // <- returns cleanup function
}, []);
return acccounts;
};

const useChainId = () => {
const [chainId, setChainId] = useState(dappstore.chainId);
useEffect(() => {
return dappstore.onChainChange(setChainId);
}, []);
return chainId;
};

const App = () => {
const accounts = useAccounts();
return <div>Accounts: {accounts.join(", ")}</div>;
};
```

This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).

Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo:

```
npx turbo link
## Send a transaction:

```ts
const sendTransaction = async (to: `0x${string}`) => {
const [from] = dappstore.accounts;
if (!from) {
throw new Error("No account connected");
}

return await dappstore.provider.request({
method: "eth_sendTransaction",
params: [
{
from,
to,
value: "0x1", // We recommend using a library like ethers.js or viem to handle amounts
},
],
});
};
```

## Useful Links

Learn more about the power of Turborepo:

- [Tasks](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks)
- [Caching](https://turbo.build/repo/docs/core-concepts/caching)
- [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching)
- [Filtering](https://turbo.build/repo/docs/core-concepts/monorepos/filtering)
- [Configuration Options](https://turbo.build/repo/docs/reference/configuration)
- [CLI Usage](https://turbo.build/repo/docs/reference/command-line-reference)
Binary file modified bun.lockb
Binary file not shown.
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -10,14 +10,16 @@
"release": "bun run build && changeset publish"
},
"devDependencies": {
"dappstore": "workspace:*",
"@types/bun": "^1.0.4",
"@types/node": "^20.11.19",
"autoprefixer": "^10.4.17",
"dappstore": "workspace:*",
"knip": "^5.0.1",
"postcss": "^8.4.35",
"prettier": "^3.1.1",
"tailwindcss": "^3.4.1",
"turbo": "latest",
"@evmos/config": "workspace:*"
"typescript": "^5.3.3"
},
"engines": {
"node": ">=18"
@@ -29,6 +31,7 @@
"packages/*"
],
"dependencies": {
"@changesets/cli": "^2.27.1"
"@changesets/cli": "^2.27.1",
"cross-spawn": "^7.0.3"
}
}
17 changes: 11 additions & 6 deletions packages/dappstore-cli/build.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
// @ts-nocheck
import Bun from "bun";
import { dependencies, peerDependencies } from "./package.json";
import { version } from "./package.json";
import { watch } from "node:fs";
export const build = async () =>
await Bun.build({
export const build = async () => {
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
target: "node",
format: "esm",
external: [...Object.keys(dependencies), ...Object.keys(peerDependencies)],
outdir: "./dist",
});
const test = await build();

if (result.success === false) {
result.logs.forEach((log) => console.error(log));
return;
}
console.log("Build successful");
};
await build();

if (process.argv.includes("--watch")) {
const srcWatcher = watch(
`${import.meta.dir}/src`,
{ recursive: true },
async (event, filename) => {
await build();

console.log(`Detected ${event} in ${filename} (src)`);
await build();
}
);
process.on("SIGINT", () => {
26 changes: 7 additions & 19 deletions packages/dappstore-cli/package.json
Original file line number Diff line number Diff line change
@@ -5,39 +5,27 @@
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "bun run ./build.ts"
"build": "bun run ./build.ts",
"dev": "bun run build -- --watch"
},
"bin": {
"dappstore": "./dist/index.js"
},
"peerDependencies": {},
"devDependencies": {
"@commander-js/extra-typings": "^11.1.0",
"@evmos/dappstore-sdk": "workspace:*",
"@types/eslint": "^8.56.1",
"@types/node": "^20.10.6",
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"eslint": "^8.56.0",
"react": "^18.2.0",
"typescript": "^5.3.3",
"viem": "^2.7.9"
"typescript": "^5.3.3"
},
"dependencies": {
"@evmos/dev-wrapper": "workspace:*",
"@types/express": "^4.17.21",
"@vitejs/plugin-react": "^4.2.1",
"@types/cross-spawn": "^6.0.6",
"autoprefixer": "^10.4.17",
"chalk": "^5.3.0",
"clsx": "^2.1.0",
"express": "^4.18.2",
"glob": "^10.3.10",
"inquirer": "^9.2.14",
"lodash-es": "^4.17.21",
"postcss": "^8.4.33",
"postcss-scopify": "^0.1.10",
"react-dom": "^18.2.0",
"tailwindcss": "^3.4.1",
"ts-dedent": "^2.2.0",
"vite": "^5.0.12"
"@commander-js/extra-typings": "^12.0.0"
},
"publishConfig": {
"access": "public"
16 changes: 14 additions & 2 deletions packages/dappstore-cli/src/dev.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { program } from "@commander-js/extra-typings";
import { serve } from "@evmos/dev-wrapper/serve/serve.js";
import chalk from "chalk";

program
.command("dev")
.description("Start a development server")
.option("-p, --port <port>", "Development server port", "1337")
.option(
"-t, --target <port>",
@@ -11,10 +13,20 @@ program
)

.action(async ({ port, target }) => {
const targetUrl = target.startsWith("http")
? target
: `http://localhost:${target}`;
const app = serve({
target,
target: targetUrl,
});
app.listen(port, () => {
console.log(`Example app listening on port http://localhost:${port}`);
console.log(
"\n\n",
`${chalk.hex("#FF8C5C")(`☄️ Evmos DAppStore Widget`)} environment running on http://localhost:${port}`,
"\n\n",
chalk.dim(
`Note: Expects your widget server to be running on ${targetUrl}`
)
);
});
});
4 changes: 0 additions & 4 deletions packages/dappstore-cli/src/index.d.ts

This file was deleted.

5 changes: 3 additions & 2 deletions packages/dappstore-cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node
import "./dev";

import "./init/init";
import { program } from "@commander-js/extra-typings";
program.parseAsync(process.argv);

program.parse(process.argv);
43 changes: 43 additions & 0 deletions packages/dappstore-cli/src/init/detect-port.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { readPackageJson } from "./read-package-json.js";

export const detectRunningPort = async (dir: string = ".") => {
const pkg = await readPackageJson(dir);

const startScript = pkg.scripts?.start ?? "";
const devScript = pkg.scripts?.dev ?? "";
let explicitPort: null | number = parseInt(
startScript.match(/(--port|-p) (\d+)/)?.[1] ?? ""
);

if (isNaN(explicitPort)) {
explicitPort = null;
}

if (startScript.includes("next") || devScript.includes("next")) {
return {
framework: "next" as const,
port: explicitPort ?? 3000,
};
}
if (startScript.includes("vite") || devScript.includes("vite")) {
return {
framework: "vite" as const,
port: explicitPort ?? 5173,
};
}

if (
startScript.includes("react-scripts") ||
devScript.includes("react-scripts")
) {
return {
framework: "react-scripts" as const,
port: explicitPort ?? 3000,
};
}

return {
framework: "unknown" as const,
port: explicitPort ?? null,
};
};
19 changes: 19 additions & 0 deletions packages/dappstore-cli/src/init/get-package-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";

export function getPkgManager(): PackageManager {
const userAgent = process.env.npm_config_user_agent || "";

if (userAgent.startsWith("yarn")) {
return "yarn";
}

if (userAgent.startsWith("pnpm")) {
return "pnpm";
}

if (userAgent.startsWith("bun")) {
return "bun";
}

return "npm";
}
26 changes: 26 additions & 0 deletions packages/dappstore-cli/src/init/get-template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const TEMPLATE = `
import { createDAppStoreClient } from "@evmos/dappstore-sdk";
export const dappstore = createDAppStoreClient();
/**
* EIP-1193 provider
*/
export const provider = dappstore.provider;
`;
// const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const readTemplate = () => {
// try {
// return await readFile(
// path.join(__dirname, `./templates/dappstore-client.ts`),
// "utf-8"
// );
// } catch (e) {
// console.error("Template not found");
// process.exit(1);
// }

// TODO: I was actually reading this file with a Bun macro but it's not working on github CI
// let's try again in the future
return TEMPLATE;
};
92 changes: 92 additions & 0 deletions packages/dappstore-cli/src/init/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { program } from "@commander-js/extra-typings";
import inquirer from "inquirer";
import { PackageManager, getPkgManager } from "./get-package-manager.js";
import { installDependencies } from "./install-dependencies.js";
import { readPackageJson, writePackageJson } from "./read-package-json.js";
import chalk from "chalk";
import { detectRunningPort } from "./detect-port.js";
import { stat } from "fs/promises";
import { writeTemplate } from "./write-template.js";

program
.command("init")
.description("Initialize DAppStore widget in your project")

.option("--skip-install", "Skip installing dependencies")

.action(async ({ skipInstall }) => {
const detectedPackageManager = getPkgManager();
let pkgJson: Awaited<ReturnType<typeof readPackageJson>>;
try {
pkgJson = await readPackageJson();
} catch (e) {
console.log(
"\n",
chalk.bgRed("package.json not found"),
"\n\n",
"Make sure you run this command in the root of your project,",
"\n",
`or you initialize a new project with ${chalk.yellow(
`'${detectedPackageManager} init'`
)} first.`,
"\n"
);
process.exit(1);
}

const defaultPort = await detectRunningPort();
const { packageManager, port } = await inquirer.prompt<{
packageManager: PackageManager;
port: number;
}>([
{
type: "list",
name: "packageManager",
message: `Which package manager do you want to use?`,

choices: ["npm", "pnpm", "bun", "yarn"].map((pm) => ({
name: pm === detectedPackageManager ? `${pm} (detected)` : pm,
value: pm,
})),
default: detectedPackageManager,
},
{
type: "number",
name: "port",

message: `What port or url of your development server will run on?`,
default: defaultPort.port ?? 3000,
},
]);
if (!skipInstall) {
await installDependencies(packageManager, ["@evmos/dappstore-sdk"]);
await installDependencies(packageManager, ["dappstore"], "dev");
}
const packageJson = await readPackageJson();
packageJson.scripts = {
...packageJson.scripts,
"dev:dappstore": `dappstore dev --target ${port}`,
};
await writePackageJson(".", packageJson);
let templateDest = "./dappstore-client.ts";
try {
const srcDir = await stat("src");
if (srcDir.isDirectory()) {
templateDest = "./src/dappstore-client.ts";
}
} catch (e) {
// noop
}

await writeTemplate(templateDest);

console.log(
"\n",
chalk.green(`🚀 DAppStore Widget Setup is completed`),
"\n",
`To start the development, start your development server, and then run:`,
"\n\n",

chalk.yellow(`\t${packageManager} run dev:dappstore`)
);
});
49 changes: 49 additions & 0 deletions packages/dappstore-cli/src/init/install-dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import spawn from "cross-spawn";
import { PackageManager } from "./get-package-manager.js";
import { readPackageJson } from "./read-package-json.js";

/**
* Spawn a package manager installation based on user preference.
*
* @returns A Promise that resolves once the installation is finished.
*/
export async function installDependencies(
/** Indicate which package manager to use. */
packageManager: PackageManager,
dependencies: string[] = [],
as: "dev" | "prod" = "dev"
): Promise<void> {
readPackageJson.cache.clear?.();
const args: string[] = ["install"];
if (as === "dev") {
args.push("--save-dev");
}
args.push(...dependencies);

/**
* Return a Promise that resolves once the installation is finished.
*/
return new Promise((resolve, reject) => {
/**
* Spawn the installation process.
*/
const child = spawn(packageManager, args, {
stdio: "inherit",
env: {
...process.env,
ADBLOCK: "1",
// we set NODE_ENV to development as pnpm skips dev
// dependencies when production
NODE_ENV: "development",
DISABLE_OPENCOLLECTIVE: "1",
},
});
child.on("close", (code) => {
if (code !== 0) {
reject({ command: `${packageManager} ${args.join(" ")}` });
return;
}
resolve();
});
});
}
33 changes: 33 additions & 0 deletions packages/dappstore-cli/src/init/read-package-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { readFile, writeFile } from "fs/promises";
import { join } from "path";
import { memoize } from "lodash-es";
const readJson = async <T>(path: string): Promise<T> => {
const content = await readFile(path, "utf-8");
return JSON.parse(content);
};

export const readPackageJson = memoize(async (dir: string = ".") => {
const pkgPath = join(process.cwd(), dir, "package.json");

return await readJson<{
name?: string;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
scripts?: Record<string, string>;
}>(pkgPath);
});

export const writePackageJson = async (
dir: string = ".",
pkg: {
name?: string;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
scripts?: Record<string, string>;
}
) => {
readPackageJson.cache.clear?.();
const pkgPath = join(process.cwd(), dir, "package.json");

await writeFile(pkgPath, JSON.stringify(pkg, null, 2));
};
8 changes: 8 additions & 0 deletions packages/dappstore-cli/src/init/templates/dappstore-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createDAppStoreClient } from "@evmos/dappstore-sdk";

export const dappstore = createDAppStoreClient();

/**
* EIP-1193 provider
*/
export const provider = dappstore.provider;
8 changes: 8 additions & 0 deletions packages/dappstore-cli/src/init/write-template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { writeFile } from "fs/promises";
import { readTemplate } from "./get-template" with { type: "macro" };

export const writeTemplate = async (destination: string) => {
const templatePath = await readTemplate();

await writeFile(destination, templatePath, "utf-8");
};
6 changes: 1 addition & 5 deletions packages/dappstore-cli/tsconfig.json
Original file line number Diff line number Diff line change
@@ -2,11 +2,7 @@
"extends": "@evmos/config/base.json",
"compilerOptions": {
"outDir": "./dist",
"emitDeclarationOnly": true,

"target": "ESNext",
"moduleResolution": "NodeNext",
"module": "NodeNext"
"target": "ESNext"
},
"include": ["./src/**/*", "./src/**/*.d.ts"],
"exclude": ["node_modules", "dist"]
10 changes: 4 additions & 6 deletions packages/dappstore-sdk/package.json
Original file line number Diff line number Diff line change
@@ -10,12 +10,12 @@
"dev": "bun run --watch ./build.ts --watch & bun run dev:types",
"build": "bun run ./build.ts & tsc"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./internal/host": {
"types": "./dist/host.d.ts",
@@ -27,8 +27,6 @@
"@trpc/server": "next",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.10.6",
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"@types/uuid": "^9.0.8",
"lodash-es": "^4.17.21",
"typescript": "^5.3.3",
46 changes: 32 additions & 14 deletions packages/dappstore-sdk/src/client.ts
Original file line number Diff line number Diff line change
@@ -68,13 +68,22 @@ class SDKProvider implements StronglyTypedEIP1193Provider {
}

class Client {
private _host: null | ReturnType<typeof createHost> = null;
private _listeners: Record<string, Set<Function>> = {};
private _provider = new SDKProvider();
private _ready = false;
private _chainId: Hex | null = null;
private _accounts: Hex[] = [];
private _state: "uninitialized" | "ready" | "error" = "uninitialized";
// @internal
_host: null | ReturnType<typeof createHost> = null;
// @internal
_listeners: Record<string, Set<Function>> = {};
// @internal
_provider = new SDKProvider();
// @internal
_ready = false;
// @internal
_chainId: Hex | null = null;
// @internal
_accounts: Hex[] = [];
// @internal
_state: "uninitialized" | "ready" | "error" = "uninitialized";

initialized: Promise<() => void> = Promise.resolve(() => {});
get ready() {
return this._ready;
}
@@ -88,18 +97,25 @@ class Client {
get provider() {
return this._provider;
}
private get isInsideIframe() {
// @internal
get _isInsideIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
init() {
if (this.isInsideIframe === false) {
throw new Error("Cannot use DAppStore SDK outside of an iframe");

constructor(autoInit = true) {
if (autoInit) this.initialized = this.init();
}
async init() {
if (this._isInsideIframe === false) {
throw new Error(
"Cannot use DAppStore SDK outside of the DAppStore iframe"
);
}
this.ack();
await this.ack();
const unsubAccounts = trpcClient.provider.on.accountsChanged.subscribe(
undefined,
{
@@ -162,7 +178,9 @@ class Client {
}

export const createDAppStoreClient = ({ autoInit = true } = {}) => {
const client = new Client();
if (autoInit) client.init();
const client = new Client(autoInit);

return client;
};

export type DAppStoreClient = Client;
2 changes: 1 addition & 1 deletion packages/dappstore-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { createDAppStoreClient as createDAppstoreClient } from "./client";
export { createDAppStoreClient, type DAppStoreClient } from "./client";
export * from "./types/EIP1193Provider";
2 changes: 2 additions & 0 deletions packages/dev-wrapper/package.json
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.20.5",
"@types/express": "^4.17.21",
"clsx": "^2.1.0",
"express": "^4.18.2",
"react": "^18.2.0",
6 changes: 1 addition & 5 deletions packages/dev-wrapper/serve.ts
Original file line number Diff line number Diff line change
@@ -5,12 +5,8 @@ import { readFileSync } from "fs";
const __dirname = fileURLToPath(new URL("./", import.meta.url));

export const serve = ({ target }: { target: string }) => {
const targetUrl = target.startsWith("http")
? target
: `http://localhost:${target}`;

const app = express();
const envScript = `<script>__ENV__ = ${JSON.stringify({ TARGET: targetUrl })}</script>`;
const envScript = `<script>__ENV__ = ${JSON.stringify({ TARGET: target })}</script>`;
const index = readFileSync(
path.join(__dirname, "../app/index.html"),
"utf-8"
2 changes: 1 addition & 1 deletion packages/dev-wrapper/src/App.tsx
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ const Header = () => {
</header>
);
};
export function WalletOptions() {
function WalletOptions() {
const { connectors, connect, isPending } = useConnect();
const { disconnect } = useDisconnect();
const { isConnected, address } = useAccount();

0 comments on commit 52bf8c3

Please sign in to comment.