UNPKG

create-midnight-dapp

Version:

Initialize a React + Vite + Midnight dApp in the current directory

404 lines (356 loc) 15 kB
#!/usr/bin/env node import { execSync } from "node:child_process"; import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const log = (msg) => console.log(msg); const run = (cmd, opts = {}) => execSync(cmd, { stdio: "inherit", ...opts }); function detectPM() { if (existsSync("pnpm-lock.yaml")) return { name: "pnpm", dlx: "pnpm dlx", install: "pnpm install" }; if (existsSync("yarn.lock")) return { name: "yarn", dlx: "yarn dlx", install: "yarn install" }; return { name: "npm", dlx: "npx", install: "npm install" }; } function readJSON(path, fallback) { try { return JSON.parse(readFileSync(path, "utf8")); } catch { return fallback; } } function ensurePkgJSON() { if (!existsSync("package.json")) { log("→ No package.json found. Initializing…"); run("npm init -y"); } } function addScripts() { const pkg = readJSON("package.json", {}); pkg.scripts ||= {}; pkg.scripts.dev ||= "vite"; pkg.scripts.build ||= "tsc -b && vite build"; pkg.scripts.preview ||= "vite preview"; writeFileSync("package.json", JSON.stringify(pkg, null, 2)); } function addDeps(pm) { const devDeps = ["vite", "@vitejs/plugin-react", "typescript", "tslib"]; const deps = ["react", "react-dom"]; log("→ Installing dependencies…"); run(`${pm.install} ${deps.join(" ")}`); run(`${pm.install} -D ${devDeps.join(" ")}`); } function ensureTsConfig() { if (!existsSync("tsconfig.json")) { const ts = { "compilerOptions": { "target": "ES2020", "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "jsx": "react-jsx", "strict": true, "skipLibCheck": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "noEmit": true }, "include": ["src"] }; writeFileSync("tsconfig.json", JSON.stringify(ts, null, 2)); } } function ensureViteConfig() { if (!existsSync("vite.config.ts")) { const vite = `import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], })`; writeFileSync("vite.config.ts", vite); } } function ensureIndexHtml() { if (!existsSync("index.html")) { const html = `<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>Midnight dApp</title> </head> <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body> </html>`; writeFileSync("index.html", html); } } function ensureSrcTree() { if (!existsSync("src")) mkdirSync("src", { recursive: true }); if (!existsSync("src/components")) mkdirSync("src/components", { recursive: true }); if (!existsSync("src/styles")) mkdirSync("src/styles", { recursive: true }); // main.tsx if (!existsSync("src/main.tsx")) { const main = `import React from 'react' import { createRoot } from 'react-dom/client' import App from './App' import './styles/index.css' createRoot(document.getElementById('root')!).render( <React.StrictMode> <App /> </React.StrictMode> )`; writeFileSync("src/main.tsx", main); } // Plain CSS entry (NO Tailwind, NO PostCSS) if (!existsSync("src/styles/index.css")) { const css = `/* Basic starter styles (no Tailwind) */ :root { color-scheme: dark; } * { box-sizing: border-box; } html, body, #root { height: 100%; } body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', sans-serif; background-color: #0b1220; color: #e2e8f0; } button { font: inherit; }`; writeFileSync("src/styles/index.css", css); } // midnight-provider.ts if (!existsSync("src/midnight-provider.ts")) { const mp = `declare global { interface Window { cardano?: { midnight?: any }; midnight?: Record<string, any>; } } export function getMidnightProvider(): any | null { const m: any = (window as any)?.midnight; if (m && typeof m === "object") { if (m.mnLace) return m.mnLace; if (m.lace) return m.lace; const k = Object.keys(m).find((x) => m[x] && typeof m[x] === "object"); if (k) return m[k]; } return (window as any)?.cardano?.midnight ?? null; }`; writeFileSync("src/midnight-provider.ts", mp); } // Navbar.tsx if (!existsSync("src/components/Navbar.tsx")) { const nav = `import { useState } from "react"; import { getMidnightProvider } from "../midnight-provider"; export default function Navbar() { const [connected, setConnected] = useState(false); const connectWallet = async () => { try { const provider = getMidnightProvider(); if (!provider) throw new Error("Midnight provider not injected. Open Lace (Midnight testnet profile) and reload."); const api = typeof provider.enable === "function" ? await provider.enable() : provider; setConnected(true); window.dispatchEvent(new CustomEvent("midnight:connected", { detail: { api, provider } })); } catch (err:any) { alert(err?.message || String(err)); console.error("Wallet connection failed:", err); } }; return ( <nav style={{ position:"fixed", top:0, left:0, width:"100%", height:"3rem", display:"flex", alignItems:"center", justifyContent:"flex-end", paddingRight:"3rem", backgroundColor:"#111827", zIndex:50 }}> <button onClick={connectWallet} style={{ padding:"0.45rem 0.9rem", borderRadius:"0.55rem", border:"none", backgroundColor:"#4f46e5", color:"white", fontWeight:700, fontSize:"0.9rem", cursor:"pointer" }}> {connected ? "✅ Connected" : "Connect Midnight Lace Wallet"} </button> </nav> ); }`; writeFileSync("src/components/Navbar.tsx", nav); } // App.tsx (compact version that renders summary + keys) if (!existsSync("src/App.tsx")) { const app = `import { useEffect, useRef, useState } from "react"; import Navbar from "./components/Navbar"; import { getMidnightProvider } from "./midnight-provider"; type WalletState = { address?: string; addressLegacy?: string; coinPublicKey?: string; coinPublicKeyLegacy?: string; encryptionPublicKey?: string; encryptionPublicKeyLegacy?: string; balances?: any; [k: string]: any; }; function deriveTDustBalanceFromState(s:any): string { if (!s) return "—"; if (s?.balances?.tDUST != null) return String(s.balances.tDUST); if (s?.balances?.tdust != null) return String(s.balances.tdust); const arrays:any[]=[]; if (Array.isArray(s?.assets)) arrays.push(s.assets); if (Array.isArray(s?.balances)) arrays.push(s.balances); if (Array.isArray(s?.coins)) arrays.push(s.coins); for (const arr of arrays) { const hit = arr.find((x:any)=>x?.asset==="tDUST"||x?.ticker==="tDUST"||x?.symbol==="tDUST"||x?.denom==="tDUST"); if (hit?.amount!=null) return String(hit.amount); if (hit?.balance!=null) return String(hit.balance); if (hit?.quantity!=null) return String(hit.quantity); } return "—"; } function detectProviderLabel(api:any, provider:any): string { const m:any = (window as any)?.midnight; if (m && typeof m === "object") { for (const k of Object.keys(m)) if (m[k]===provider||m[k]===api) return k; } const c = (window as any)?.cardano?.midnight; if (c && (provider===c || api===c)) return "cardano.midnight"; return api?.providerName ?? provider?.providerName ?? "(auto-detected)"; } function detectWalletLabel(api:any, provider:any): string { return api?.walletName ?? provider?.walletName ?? api?.wallet?.name ?? provider?.wallet?.name ?? api?.name ?? provider?.name ?? provider?.constructor?.name ?? "—"; } async function detectApiVersion(api:any, provider:any): Promise<string> { const vals = [api?.apiVersion, api?.version, provider?.apiVersion, provider?.version]; for (const v of vals) if (typeof v === "string" && v) return v; const fns = [api?.getVersion, provider?.getVersion, api?.info, provider?.info]; for (const fn of fns) try { if (typeof fn==="function") { const v = await fn.call(api ?? provider); if (v) return String(v); } } catch {} return "—"; } export default function App() { const apiRef = useRef<any>(null); const [loading, setLoading] = useState(false); const [providerName, setProviderName] = useState("(auto-detected)"); const [walletName, setWalletName] = useState("—"); const [apiVersion, setApiVersion] = useState("—"); const [addr, setAddr] = useState("—"); const [tDustBalance, setTDustBalance] = useState("—"); const [capWalletTransfer, setCapWalletTransfer] = useState<boolean|null>(null); const [capCoinEnum, setCapCoinEnum] = useState<boolean|null>(null); const [shieldAddr, setShieldAddr] = useState("—"); const [shieldCPK, setShieldCPK] = useState("—"); const [shieldEPK, setShieldEPK] = useState("—"); const [legacyAddr, setLegacyAddr] = useState("—"); const [legacyCPK, setLegacyCPK] = useState("—"); const [legacyEPK, setLegacyEPK] = useState("—"); const readState = async (src:any) => { if (!src) return null; if (typeof src.serializeState === "function") { const s = await src.serializeState(); try { const parsed = typeof s === "string" ? JSON.parse(s) : s; return parsed?.state ?? parsed ?? null; } catch { return null; } } if (typeof src.state === "function") { const st = await src.state(); return st?.state ?? st ?? null; } return null; }; async function loadWalletInfoNonInteractive(ctx?: { api?: any; provider?: any }) { setLoading(true); try { const provider = ctx?.provider ?? getMidnightProvider(); const api = ctx?.api ?? apiRef.current ?? null; if (ctx?.api) apiRef.current = ctx.api; if (!provider && !api) return; setProviderName(detectProviderLabel(api, provider)); setWalletName(detectWalletLabel(api, provider)); setApiVersion(await detectApiVersion(api, provider)); const state: WalletState | null = (await readState(api)) ?? (await readState(provider)) ?? null; const address = state?.address ?? state?.addresses?.[0] ?? state?.account?.address ?? "—"; setAddr(address); setTDustBalance(deriveTDustBalanceFromState(state)); const w = api ?? provider; setCapWalletTransfer(typeof w?.balanceAndProveTransaction==="function" && typeof w?.submitTransaction==="function"); setCapCoinEnum(typeof w?.listCoins==="function" || typeof w?.getUtxos==="function" || typeof w?.coins==="function" || typeof w?.serializeState==="function" || typeof w?.state==="function"); setShieldAddr(state?.address ?? "—"); setShieldCPK(state?.coinPublicKey ?? "—"); setShieldEPK(state?.encryptionPublicKey ?? "—"); setLegacyAddr(state?.addressLegacy ?? "—"); setLegacyCPK(state?.coinPublicKeyLegacy ?? "—"); setLegacyEPK(state?.encryptionPublicKeyLegacy ?? "—"); } finally { setLoading(false); } } useEffect(() => { loadWalletInfoNonInteractive(); const onConnected = (e:Event) => { const { api, provider } = (e as CustomEvent).detail || {}; loadWalletInfoNonInteractive({ api, provider }); }; window.addEventListener("midnight:connected", onConnected); return () => window.removeEventListener("midnight:connected", onConnected); }, []); return ( <div> <Navbar /> <main style={{ paddingTop:"5rem", textAlign:"center" }}> <h1 style={{ fontSize:"2.25rem", marginBottom:"0.5rem" }}>🌙 Welcome to your Midnight dApp</h1> <p style={{ color:"#94a3b8", marginBottom:"1.25rem" }}>Start building with Vite + React + Midnight Wallet.</p> <Card title="Wallet Summary" onRefresh={() => loadWalletInfoNonInteractive()} loading={loading}> <Row label="Provider" value={providerName} /> <Row label="Wallet" value={walletName} /> <Row label="API version" value={apiVersion} /> <Row label="Address (heuristic)" value={addr} /> <Row label="tDUST Balance" value={tDustBalance} /> <Row label="Capabilities" value={\`walletTransfer=\${String(capWalletTransfer)} coinEnum=\${String(capCoinEnum)}\`} /> </Card> <Card title="Wallet Keys & Addresses" style={{ marginTop:16 }}> <Row label="Shield Address" value={shieldAddr} /> <Row label="Shield CPK" value={shieldCPK} /> <Row label="Shield EPK" value={shieldEPK} /> <Row label="Legacy Address" value={legacyAddr} /> <Row label="Legacy CPK" value={legacyCPK} /> <Row label="Legacy EPK" value={legacyEPK} /> </Card> </main> </div> ); } function Card({ title, children, onRefresh, loading, style }:{ title:string; children:React.ReactNode; onRefresh?:()=>void; loading?:boolean; style?:React.CSSProperties; }) { return ( <div style={{ background:"#0b1220", color:"#e2e8f0", padding:16, borderRadius:12, maxWidth:960, margin:"0 auto", textAlign:"left", ...style }}> <div style={{ display:"flex", justifyContent:"space-between", marginBottom:10 }}> <strong>{title}</strong> {onRefresh && ( <button onClick={onRefresh} disabled={!!loading} style={{ padding:"6px 12px", borderRadius:8, border:"1px solid #334155", background:"#1e293b", color:"#e2e8f0", cursor:"pointer", fontSize:13, opacity: loading ? 0.7 : 1 }}> {loading ? "Refreshing…" : "Refresh"} </button> )} </div> {children} </div> ); } function Row({ label, value }:{ label:string; value:string }) { return ( <div style={{ display:"grid", gridTemplateColumns:"180px 1fr", gap:8, alignItems:"center", margin:"6px 0" }}> <div style={{ color:"#93c5fd", fontSize:13 }}>{label}</div> <code style={{ background:"#0f172a", border:"1px solid #334155", borderRadius:6, padding:"6px 8px", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }} title={value}> {value} </code> </div> ); }`; writeFileSync("src/App.tsx", app); } } function main() { const inPlace = process.argv.includes("--in-place") || process.argv.includes("--here"); if (!inPlace) log("ℹ️ Tip: run with --in-place to initialize the current folder"); ensurePkgJSON(); const pm = detectPM(); addScripts(); ensureTsConfig(); ensureViteConfig(); ensureIndexHtml(); addDeps(pm); ensureSrcTree(); log("\n✅ Midnight dApp scaffold complete."); log(" Next steps:"); log(` 1) Start dev server: ${pm.name === "npm" ? "npm run dev" : `${pm.name} run dev`}`); log(" 2) Open: http://localhost:5173"); log(" 3) Click ‘Connect Midnight Lace Wallet’ and verify info shows up.\n"); } main();