From 9d2800eb7eb77472866ee93a5139bec30f791e66 Mon Sep 17 00:00:00 2001 From: Gorka Date: Tue, 12 May 2026 18:37:54 -0300 Subject: [PATCH 1/3] chore(build): fetch @moonlight/ui CSS at build, write to public/styles.css MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds UI_LIB_TAG = "v0.3.1" pin + buildStyles() step in src/build.ts that fetches the lib's tokens.css + base-styles.css + nav.css from raw.githubusercontent.com and concatenates them with src/app-styles.css into the served public/styles.css. raw.githubusercontent.com serves CSS as text/plain with nosniff so browser @import would be rejected — fetch happens at build time and the served file is same-origin text/css. network-dashboard does not consume the lib's stepper or invite-waitlist (no onboarding flow, no auth-gated views), so those CSS files aren't fetched. tokens + base-styles + nav are enough. public/styles.css is now a build artifact: gitignored, tracked-deletion committed here. src/app-styles.css (new) holds network-unique selectors only: .nav-brand:hover variant, table CSS, .text-muted utility, .loading, .clickable-row, map (.map-container/.world-map/.council-dot/.council-label), council grid (.council-card and friends), transaction feed (.feed-list/ .feed-item and friends), error banner. deno.json adds check/lint/fmt/fmt:check tasks to mirror moonlight-pay's shape (uniform across the 4 apps). Imports add @moonlight/ui/nav pinned at v0.3.1. --- .gitignore | 1 + deno.json | 9 +- deno.lock | 10 +- public/styles.css | 394 --------------------------------------------- src/app-styles.css | 179 ++++++++++++++++++++ src/build.ts | 35 ++++ 6 files changed, 230 insertions(+), 398 deletions(-) delete mode 100644 public/styles.css create mode 100644 src/app-styles.css diff --git a/.gitignore b/.gitignore index fc4373e..db8c92a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ public/app.js public/app.js.map +public/styles.css diff --git a/deno.json b/deno.json index a4ff423..6ca1763 100644 --- a/deno.json +++ b/deno.json @@ -5,7 +5,11 @@ "dev": "deno run --allow-all --watch src/server.ts", "build": "deno run --allow-all src/build.ts", "serve": "deno run --allow-all src/server.ts", - "test": "deno test --allow-all src/" + "test": "deno test --allow-all src/", + "check": "deno check src/server.ts src/build.ts src/app.ts src/lib/*.ts src/views/*.ts src/components/*.ts", + "lint": "deno lint src/", + "fmt": "deno fmt src/", + "fmt:check": "deno fmt --check src/" }, "compilerOptions": { "strict": true, @@ -14,6 +18,7 @@ "imports": { "@std/path": "jsr:@std/path@^1.0.0", "@std/assert": "jsr:@std/assert@^1.0.0", - "stellar-sdk": "npm:@stellar/stellar-sdk@^15.0.1" + "stellar-sdk": "npm:@stellar/stellar-sdk@^15.0.1", + "@moonlight/ui/nav": "https://raw.githubusercontent.com/Moonlight-Protocol/ui/v0.3.1/src/nav/mod.ts" } } diff --git a/deno.lock b/deno.lock index d025d86..696d64f 100644 --- a/deno.lock +++ b/deno.lock @@ -6,6 +6,7 @@ "jsr:@std/assert@~0.213.1": "0.213.1", "jsr:@std/encoding@0.213": "0.213.1", "jsr:@std/internal@^1.0.12": "1.0.13", + "jsr:@std/json@~0.213.1": "0.213.1", "jsr:@std/jsonc@0.213": "0.213.1", "jsr:@std/path@0.213": "0.213.1", "jsr:@std/path@1": "1.1.4", @@ -35,10 +36,14 @@ "@std/internal@1.0.13": { "integrity": "2f9546691d4ac2d32859c82dff284aaeac980ddeca38430d07941e7e288725c0" }, + "@std/json@0.213.1": { + "integrity": "f572b1de605d07c4a5602445dac54bfc51b1fb87a3710a17aed2608bfca54e68" + }, "@std/jsonc@0.213.1": { "integrity": "5578f21aa583b7eb7317eed077ffcde47b294f1056bdbb9aacec407758637bfe", "dependencies": [ - "jsr:@std/assert@~0.213.1" + "jsr:@std/assert@~0.213.1", + "jsr:@std/json" ] }, "@std/path@0.213.1": { @@ -382,7 +387,8 @@ "https://deno.land/x/denoflate@1.2.1/mod.ts": "f5628e44b80b3d80ed525afa2ba0f12408e3849db817d47a883b801f9ce69dd6", "https://deno.land/x/denoflate@1.2.1/pkg/denoflate.js": "b9f9ad9457d3f12f28b1fb35c555f57443427f74decb403113d67364e4f2caf4", "https://deno.land/x/denoflate@1.2.1/pkg/denoflate_bg.wasm.js": "d581956245407a2115a3d7e8d85a9641c032940a8e810acbd59ca86afd34d44d", - "https://deno.land/x/esbuild@v0.20.1/mod.js": "d50e500b53ce67e31116beba3916b0f9275c0e1cc20bc5cadc0fc1b7a3b06fd9" + "https://deno.land/x/esbuild@v0.20.1/mod.js": "d50e500b53ce67e31116beba3916b0f9275c0e1cc20bc5cadc0fc1b7a3b06fd9", + "https://raw.githubusercontent.com/Moonlight-Protocol/ui/v0.3.1/src/nav/mod.ts": "4bce92a8e347e12953f2fc7e4329e0b58e3dcb8d1c872054e2d30beeba4373e0" }, "workspace": { "dependencies": [ diff --git a/public/styles.css b/public/styles.css deleted file mode 100644 index 6f3518e..0000000 --- a/public/styles.css +++ /dev/null @@ -1,394 +0,0 @@ -:root { - --bg: #0f1117; - --surface: #1a1d27; - --border: #2a2d3a; - --text: #e1e4ed; - --text-muted: #8b8fa3; - --primary: #6366f1; - --primary-hover: #818cf8; - --active: #22c55e; - --pending: #f59e0b; - --inactive: #ef4444; - --font-mono: "SF Mono", "Fira Code", monospace; - --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: var(--font-sans); - background: var(--bg); - color: var(--text); - line-height: 1.6; -} - -nav { - background: var(--surface); - border-bottom: 1px solid var(--border); - padding: 0.75rem 1.5rem; -} - -.nav-inner { - max-width: 1200px; - margin: 0 auto; - display: flex; - align-items: center; - justify-content: space-between; -} - -.nav-brand { - font-weight: 700; - font-size: 1rem; - color: var(--primary); - text-decoration: none; -} - -.nav-brand:hover { - opacity: 0.9; -} - -.nav-links { - display: flex; - gap: 1.25rem; - align-items: center; -} - -.nav-links a { - color: var(--text-muted); - text-decoration: none; - font-size: 0.875rem; - transition: color 0.15s; -} - -.nav-links a:hover { - color: var(--text); -} - -.container { - max-width: 1200px; - margin: 2rem auto; - padding: 0 1.5rem; -} - -.stats-row { - display: flex; - gap: 1rem; - flex-wrap: wrap; - margin: 1rem 0 2rem; -} - -.stat-card { - background: var(--surface); - border: 1px solid var(--border); - border-radius: 8px; - padding: 1rem 1.5rem; - min-width: 120px; - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.stat-card.active { - border-color: var(--active); -} -.stat-card.pending { - border-color: var(--pending); -} -.stat-card.inactive { - border-color: var(--inactive); -} - -.stat-value { - font-size: 1.5rem; - font-weight: 700; -} - -.stat-label { - font-size: 0.75rem; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -table { - width: 100%; - border-collapse: collapse; - margin: 1rem 0; -} - -th, td { - padding: 0.75rem 1rem; - text-align: left; - border-bottom: 1px solid var(--border); -} - -th { - color: var(--text-muted); - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.mono { - font-family: var(--font-mono); - font-size: 0.8rem; - word-break: break-all; -} - -.text-muted { - color: var(--text-muted); -} - -.badge { - display: inline-block; - padding: 0.2rem 0.6rem; - border-radius: 4px; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; -} - -.badge-active { - background: rgba(34, 197, 94, 0.15); - color: var(--active); -} -.badge-pending { - background: rgba(245, 158, 11, 0.15); - color: var(--pending); -} -.badge-inactive { - background: rgba(239, 68, 68, 0.15); - color: var(--inactive); -} - -.btn-link { - background: none; - border: none; - color: var(--text-muted); - font-size: 0.875rem; - cursor: pointer; - padding: 0; - text-decoration: none; -} - -.btn-link:hover { - color: var(--text); -} - -.login-container { - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; -} - -.login-card { - background: var(--surface); - border: 1px solid var(--border); - border-radius: 12px; - padding: 2.5rem; - width: 100%; - max-width: 420px; -} - -.login-card h1 { - margin-bottom: 0.5rem; - color: var(--primary); -} - -.login-card p { - color: var(--text-muted); - font-size: 0.875rem; - margin-bottom: 1.5rem; -} - -.error-text { - color: var(--inactive); - font-size: 0.875rem; -} - -h2 { - margin-bottom: 0.5rem; -} -h3 { - margin: 1.5rem 0 0.5rem; - color: var(--text-muted); - font-size: 0.875rem; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.empty-state { - background: var(--surface); - border: 1px solid var(--border); - border-radius: 8px; - padding: 2rem; - margin-top: 1rem; -} - -.empty-state p { - margin-bottom: 0.75rem; -} - -.version-badge { - font-size: 0.7rem; - font-weight: 500; - color: var(--text-muted); - background: var(--border); - padding: 0.15rem 0.5rem; - border-radius: 4px; - vertical-align: middle; -} - -.loading { - color: var(--text-muted); - padding: 2rem 0; -} - -.clickable-row { - cursor: pointer; - transition: background 0.15s; -} - -.clickable-row:hover { - background: var(--surface); -} - -/* Map */ -.map-container { - background: var(--surface); - border: 1px solid var(--border); - border-radius: 8px; - padding: 1rem; - margin: 1rem 0 2rem; - overflow: hidden; -} - -.world-map { - width: 100%; - height: auto; -} - -.council-dot { - fill: var(--primary); - opacity: 0.8; - cursor: pointer; - transition: opacity 0.15s, r 0.15s; -} - -.council-dot:hover { - opacity: 1; - fill: var(--primary-hover); -} - -.council-label { - fill: var(--text-muted); - font-size: 10px; - text-anchor: middle; - pointer-events: none; -} - -/* Council grid */ -.council-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 1rem; - margin-top: 1rem; -} - -.council-card { - background: var(--surface); - border: 1px solid var(--border); - border-radius: 8px; - padding: 1.25rem; - text-decoration: none; - color: var(--text); - transition: border-color 0.15s; -} - -.council-card:hover { - border-color: var(--primary); -} - -.council-card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.5rem; -} - -.council-name { - font-weight: 600; -} - -.council-card-meta { - font-size: 0.875rem; - color: var(--text-muted); - margin-bottom: 0.25rem; -} - -.council-card-id { - font-size: 0.7rem; - color: var(--text-muted); -} - -/* Transaction feed */ -.feed-list { - display: flex; - flex-direction: column; - gap: 0.5rem; - margin-top: 1rem; -} - -.feed-item { - background: var(--surface); - border: 1px solid var(--border); - border-radius: 8px; - padding: 0.75rem 1rem; -} - -.feed-item-header { - display: flex; - align-items: center; - gap: 0.75rem; - margin-bottom: 0.25rem; -} - -.feed-event-type { - font-weight: 500; - font-size: 0.875rem; -} - -.feed-item-details { - font-size: 0.8rem; - margin-bottom: 0.25rem; -} - -.feed-item-id { - font-size: 0.7rem; - color: var(--text-muted); -} - -/* Error banner */ -.error-banner { - background: rgba(239, 68, 68, 0.1); - border: 1px solid var(--inactive); - border-radius: 8px; - padding: 0.75rem 1rem; - font-size: 0.875rem; - color: var(--inactive); - margin-bottom: 1rem; -} - -/* Dev version mismatch banner */ -.version-mismatch-banner { - background: rgba(255, 255, 255, 0.03); - border-bottom: 1px solid var(--border); - padding: 0.4rem 1.5rem; - font-size: 0.75rem; - text-align: center; -} diff --git a/src/app-styles.css b/src/app-styles.css new file mode 100644 index 0000000..9b09f21 --- /dev/null +++ b/src/app-styles.css @@ -0,0 +1,179 @@ +/* + * App-specific styles for network-dashboard. + * + * Tokens + base-styles + nav now come from @moonlight/ui at the pinned + * tag — fetched and concatenated into public/styles.css by src/build.ts. + * This file contains ONLY selectors the lib does not ship. + * (network-dashboard does not consume the lib's stepper or invite-waitlist.) + */ + +/* Nav-brand hover (lib's nav.css doesn't ship this variant) */ +.nav-brand:hover { + opacity: 0.9; +} + +/* Tables */ +table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0; +} + +th, td { + padding: 0.75rem 1rem; + text-align: left; + border-bottom: 1px solid var(--border); +} + +th { + color: var(--text-muted); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* Utility */ +.text-muted { + color: var(--text-muted); +} + +/* Loading + clickable row */ +.loading { + color: var(--text-muted); + padding: 2rem 0; +} + +.clickable-row { + cursor: pointer; + transition: background 0.15s; +} + +.clickable-row:hover { + background: var(--surface); +} + +/* Map */ +.map-container { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + margin: 1rem 0 2rem; + overflow: hidden; +} + +.world-map { + width: 100%; + height: auto; +} + +.council-dot { + fill: var(--primary); + opacity: 0.8; + cursor: pointer; + transition: opacity 0.15s, r 0.15s; +} + +.council-dot:hover { + opacity: 1; + fill: var(--primary-hover); +} + +.council-label { + fill: var(--text-muted); + font-size: 10px; + text-anchor: middle; + pointer-events: none; +} + +/* Council grid */ +.council-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.council-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1.25rem; + text-decoration: none; + color: var(--text); + transition: border-color 0.15s; +} + +.council-card:hover { + border-color: var(--primary); +} + +.council-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.council-name { + font-weight: 600; +} + +.council-card-meta { + font-size: 0.875rem; + color: var(--text-muted); + margin-bottom: 0.25rem; +} + +.council-card-id { + font-size: 0.7rem; + color: var(--text-muted); +} + +/* Transaction feed */ +.feed-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 1rem; +} + +.feed-item { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.75rem 1rem; +} + +.feed-item-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.25rem; +} + +.feed-event-type { + font-weight: 500; + font-size: 0.875rem; +} + +.feed-item-details { + font-size: 0.8rem; + margin-bottom: 0.25rem; +} + +.feed-item-id { + font-size: 0.7rem; + color: var(--text-muted); +} + +/* Error banner */ +.error-banner { + background: rgba(239, 68, 68, 0.1); + border: 1px solid var(--inactive); + border-radius: 8px; + padding: 0.75rem 1rem; + font-size: 0.875rem; + color: var(--inactive); + margin-bottom: 1rem; +} diff --git a/src/build.ts b/src/build.ts index e8392e9..229244c 100644 --- a/src/build.ts +++ b/src/build.ts @@ -7,10 +7,45 @@ import * as esbuild from "https://deno.land/x/esbuild@v0.20.1/mod.js"; // deno-lint-ignore no-import-prefix -- build script intentionally pins the version import { denoPlugins } from "jsr:@luca/esbuild-deno-loader@0.10"; +// Pinned @moonlight/ui tag. raw.githubusercontent.com serves CSS as +// text/plain with nosniff so browsers refuse @import of these URLs; we +// fetch + concatenate at build time and write the result to public/styles.css. +// Do not change without bumping the consumer-side deps explicitly. +const UI_LIB_TAG = "v0.3.1"; +const UI_LIB_CSS_FILES = [ + "tokens/tokens.css", + "base-styles/base-styles.css", + "nav/nav.css", +]; + +async function buildStyles(): Promise { + const parts: string[] = []; + for (const path of UI_LIB_CSS_FILES) { + const url = + `https://raw.githubusercontent.com/Moonlight-Protocol/ui/${UI_LIB_TAG}/src/${path}`; + const res = await fetch(url); + if (!res.ok) { + throw new Error( + `Failed to fetch ${url}: ${res.status} ${res.statusText}`, + ); + } + const body = await res.text(); + parts.push(`/* @moonlight/ui ${UI_LIB_TAG} — ${path} */\n${body}`); + } + const appStyles = await Deno.readTextFile("src/app-styles.css"); + parts.push(`/* network-dashboard app-styles */\n${appStyles}`); + await Deno.writeTextFile("public/styles.css", parts.join("\n")); + console.log( + `Built public/styles.css from @moonlight/ui@${UI_LIB_TAG} + src/app-styles.css`, + ); +} + const isProduction = Deno.args.includes("--production"); const denoJson = JSON.parse(await Deno.readTextFile("deno.json")); const version = denoJson.version ?? "0.0.0"; +await buildStyles(); + async function resolveSorobanCoreVersion(): Promise { try { const res = await fetch( From f5600fa2c5bc3d4a05d8c07fc52a06bb23b28e7e Mon Sep 17 00:00:00 2001 From: Gorka Date: Tue, 12 May 2026 18:37:54 -0300 Subject: [PATCH 2/3] refactor: replace local nav with @moonlight/ui - src/components/nav.ts deleted. - src/lib/nav.ts new: getNav() helper that calls renderNav from @moonlight/ui/nav with the network-dashboard brand ("Moonlight Network") and the three top-level links (Map / Councils / Transactions). Helper exists so the 4 view files don't each duplicate the brand + links arrays. - src/views/{transactions,councils,council-detail,map}.ts: switched from importing renderNav from the deleted local file to importing getNav from the new lib/nav.ts helper. Call site shape (el.appendChild(getNav())) preserved. --- src/components/nav.ts | 21 --------------------- src/lib/nav.ts | 20 ++++++++++++++++++++ src/views/council-detail.ts | 4 ++-- src/views/councils.ts | 4 ++-- src/views/map.ts | 4 ++-- src/views/transactions.ts | 4 ++-- 6 files changed, 28 insertions(+), 29 deletions(-) delete mode 100644 src/components/nav.ts create mode 100644 src/lib/nav.ts diff --git a/src/components/nav.ts b/src/components/nav.ts deleted file mode 100644 index 0c0e244..0000000 --- a/src/components/nav.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { escapeHtml } from "../lib/dom.ts"; - -declare const __APP_VERSION__: string; -const appVersion: string = __APP_VERSION__; - -export function renderNav(): HTMLElement { - const nav = document.createElement("nav"); - nav.innerHTML = ` - - `; - return nav; -} diff --git a/src/lib/nav.ts b/src/lib/nav.ts new file mode 100644 index 0000000..3ac5c4c --- /dev/null +++ b/src/lib/nav.ts @@ -0,0 +1,20 @@ +import { renderNav } from "@moonlight/ui/nav"; + +declare const __APP_VERSION__: string; + +/** + * Network-dashboard nav with the fixed brand + top-level links shared + * across every view. Each view calls this helper instead of the lib's + * renderNav directly to avoid duplicating the brand/links arrays. + */ +export function getNav(): HTMLElement { + return renderNav({ + brand: "Moonlight Network", + version: __APP_VERSION__, + links: [ + { href: "#/map", label: "Map" }, + { href: "#/councils", label: "Councils" }, + { href: "#/transactions", label: "Transactions" }, + ], + }); +} diff --git a/src/views/council-detail.ts b/src/views/council-detail.ts index 49934be..e19b457 100644 --- a/src/views/council-detail.ts +++ b/src/views/council-detail.ts @@ -1,7 +1,7 @@ /** * Council detail view — drill into a council's channels, PPs, and activity. */ -import { renderNav } from "../components/nav.ts"; +import { getNav } from "../lib/nav.ts"; import { getCouncils } from "../lib/config.ts"; import { clearQueryErrors, @@ -27,7 +27,7 @@ export async function councilDetailView( params?: Record, ): Promise { const el = document.createElement("div"); - el.appendChild(renderNav()); + el.appendChild(getNav()); const main = document.createElement("main"); main.className = "container"; diff --git a/src/views/councils.ts b/src/views/councils.ts index e001292..94ec22e 100644 --- a/src/views/councils.ts +++ b/src/views/councils.ts @@ -1,7 +1,7 @@ /** * Council list view — all registered councils with on-chain state. */ -import { renderNav } from "../components/nav.ts"; +import { getNav } from "../lib/nav.ts"; import { getCouncils } from "../lib/config.ts"; import { clearQueryErrors, @@ -31,7 +31,7 @@ interface CouncilState { // deno-lint-ignore require-await -- view fn satisfies router's Promise contract export async function councilsView(): Promise { const el = document.createElement("div"); - el.appendChild(renderNav()); + el.appendChild(getNav()); const main = document.createElement("main"); main.className = "container"; diff --git a/src/views/map.ts b/src/views/map.ts index 15f476e..c8c5a04 100644 --- a/src/views/map.ts +++ b/src/views/map.ts @@ -2,7 +2,7 @@ * Map view — world map with councils plotted by jurisdiction. * Uses a static SVG map (simple-world-map, CC BY-SA 3.0). */ -import { renderNav } from "../components/nav.ts"; +import { getNav } from "../lib/nav.ts"; import { type CouncilConfig, getCouncils } from "../lib/config.ts"; import { fetchWorldSvg, @@ -14,7 +14,7 @@ import { onCleanup } from "../lib/router.ts"; export async function mapView(): Promise { const el = document.createElement("div"); - el.appendChild(renderNav()); + el.appendChild(getNav()); const main = document.createElement("main"); main.className = "container"; diff --git a/src/views/transactions.ts b/src/views/transactions.ts index 2d93ebb..81fc628 100644 --- a/src/views/transactions.ts +++ b/src/views/transactions.ts @@ -1,7 +1,7 @@ /** * Transaction feed — recent transactions across all channels. */ -import { renderNav } from "../components/nav.ts"; +import { getNav } from "../lib/nav.ts"; import { getCouncils } from "../lib/config.ts"; import { clearQueryErrors, @@ -22,7 +22,7 @@ interface FeedEntry { // deno-lint-ignore require-await -- view fn satisfies router's Promise contract export async function transactionsView(): Promise { const el = document.createElement("div"); - el.appendChild(renderNav()); + el.appendChild(getNav()); const main = document.createElement("main"); main.className = "container"; From d923c9cd7041a756f91e4621ba1453c719ee2975 Mon Sep 17 00:00:00 2001 From: Gorka Date: Tue, 12 May 2026 18:37:54 -0300 Subject: [PATCH 3/3] chore: bump version to 0.2.9 --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 6ca1763..62d3f94 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,5 @@ { - "version": "0.2.8", + "version": "0.2.9", "license": "MIT", "tasks": { "dev": "deno run --allow-all --watch src/server.ts",