diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 8436aeea..2269b8a7 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -46,6 +46,9 @@ jobs: - name: Verify package (publint) run: pnpm verify + - name: Build sample app + run: pnpm sample:build + - name: Pack and inspect contents run: | pnpm pack --pack-destination /tmp/web-pack diff --git a/platforms/web/README.md b/platforms/web/README.md index d42149b4..a2d42523 100644 --- a/platforms/web/README.md +++ b/platforms/web/README.md @@ -36,6 +36,9 @@ pnpm test:watch pnpm lint # typecheck + oxlint + oxfmt --check pnpm format # oxfmt (writes in place) pnpm verify # publint + +pnpm sample # serve the playground at http://localhost:5173 +pnpm sample:build # build the playground (sample/dist/) ``` ## Tooling diff --git a/platforms/web/package.json b/platforms/web/package.json index de766be2..6d73e554 100644 --- a/platforms/web/package.json +++ b/platforms/web/package.json @@ -55,12 +55,16 @@ "dev": "vite build --watch", "test": "vitest run --coverage", "test:watch": "vitest", - "lint": "pnpm run typecheck && pnpm run lint:js && pnpm run format:check", - "lint:js": "oxlint --report-unused-disable-directives --max-warnings 0 src", - "lint:js:fix": "oxlint --fix src", - "format": "oxfmt src", - "format:check": "oxfmt --check src", + "lint": "pnpm run typecheck && pnpm run sample:typecheck && pnpm run lint:js && pnpm run format:check", + "lint:js": "oxlint --report-unused-disable-directives --max-warnings 0 src sample", + "lint:js:fix": "oxlint --fix src sample", + "format": "oxfmt src sample", + "format:check": "oxfmt --check src sample", "typecheck": "tsc --noEmit", + "sample": "vite --config sample/vite.config.ts", + "sample:build": "vite build --config sample/vite.config.ts", + "sample:preview": "vite preview --config sample/vite.config.ts", + "sample:typecheck": "tsc --noEmit -p sample/tsconfig.json", "verify": "publint", "prepack": "pnpm run build" }, diff --git a/platforms/web/sample/README.md b/platforms/web/sample/README.md new file mode 100644 index 00000000..e85b4952 --- /dev/null +++ b/platforms/web/sample/README.md @@ -0,0 +1,55 @@ +# Web Component Playground + +A development harness for the `` web component. Renders the +component with adjustable options and logs all dispatched `checkout:*` events +in real time. + +## Run locally + +```bash +cd platforms/web +pnpm sample +``` + +Vite serves at `http://localhost:5173`. The page has three panels: + +- **Options** — form for setting the component's attributes (`src`, + `target`) plus a small panel of manual method + buttons (`open()`, `close()`, `focus()`) for ad-hoc debugging. +- **Demo Storefront** — a mocked merchant product card with a **Buy now** + button that calls `checkout.open()`. The button is disabled until you + enter a checkout URL in the Options panel. Below the card, a collapsible + readout shows the component's read-only state (`cart`, `locale`, + `orderConfirmation`, `error`, `sessionId`). +- **Events** — a chronological log of every `checkout:*` event the component + dispatches, with a snapshot of component state at the moment the event + fired. Respondable events are tagged with a badge. + +The `` element is appended to `` rather than placed +inside the storefront panel — for `popup` and `auto` targets, the element +has no visible footprint of its own; only its internal dialog scrim appears +when `open()` is called. `target="inline"` is intentionally not supported +in the v1 of the component. + +## Status + +The `` component implementation has not yet landed in +`../src`. Until it does, the element renders as an unknown HTML element and +dispatches no events — the playground is wired up against the component's +eventual API surface but is **non-functional at runtime**. + +The forward-looking API surface is declared in [`./types.d.ts`](./types.d.ts). +Delete that file once `@shopify/checkout-kit` exports the real `ShopifyCheckout` +types from `../src`. + +## Build + +```bash +pnpm sample:build # outputs to sample/dist/ +``` + +CI runs this on every PR (see `.github/workflows/web.yml`) so the sample stays +buildable as the package evolves. + +The sample is **not** published to npm — it's excluded by the `files` +allowlist in `platforms/web/package.json`. diff --git a/platforms/web/sample/index.html b/platforms/web/sample/index.html new file mode 100644 index 00000000..b10767c0 --- /dev/null +++ b/platforms/web/sample/index.html @@ -0,0 +1,141 @@ + + + + + + Checkout Kit — Web Component Playground + + + +
+
+

Checkout Kit

+

Web Component Playground

+
+

+ Component not yet implemented in ../src — clicking + Buy now is a no-op until the element is registered. +

+
+ +
+ +
+

Options

+ +
+ + + + +
+ Methods +
+ + + +
+
+
+
+ + +
+

Demo Storefront

+ +
+ + +
+

Acme Goods

+

Studio Plant Pot

+

$29.00 USD

+ +

+ A hand-thrown ceramic pot for your favorite green friend. Glazed inside, raw outside. + Fits a 4″ nursery pot. +

+ +
+ + + +
+ +
+ + +
+ +

+ Set a checkout URL in the Options panel to enable Buy now. +

+
+
+ +
+ Component state +
+
cart
+
+
locale
+
+
orderConfirmation
+
+
error
+
+
sessionId
+
+
+
+
+ + +
+
+

Events

+ +
+
    +

    + Click Buy now to open checkout. Events the component dispatches will + appear here. +

    +
    +
    + + + + diff --git a/platforms/web/sample/main.ts b/platforms/web/sample/main.ts new file mode 100644 index 00000000..f5918b5f --- /dev/null +++ b/platforms/web/sample/main.ts @@ -0,0 +1,200 @@ +// Importing the package registers the custom element +// as a side effect — once the component implementation lands. Today the +// package only exports `VERSION`, so the element below renders as an +// unknown HTML element and the playground produces no events at runtime. +import "../src"; + +import "./styles.css"; + +// ───── Helpers ───────────────────────────────────────────────────────────── + +function $(selector: string): T { + const el = document.querySelector(selector); + if (!el) { + throw new Error(`[playground] element not found: ${selector}`); + } + return el; +} + +function formatValue(value: unknown): string { + if (value === undefined || value === null) return "—"; + if (typeof value === "string") return value; + return JSON.stringify(value, null, 2); +} + +function timestamp(): string { + const now = new Date(); + const hh = String(now.getHours()).padStart(2, "0"); + const mm = String(now.getMinutes()).padStart(2, "0"); + const ss = String(now.getSeconds()).padStart(2, "0"); + const ms = String(now.getMilliseconds()).padStart(3, "0"); + return `${hh}:${mm}:${ss}.${ms}`; +} + +// ───── DOM references ────────────────────────────────────────────────────── + +const form = $("#options-form"); +const eventLog = $("#event-log"); +const clearLogButton = $("#clear-log"); +const buyNowButton = $("#buy-now"); +const buyHint = $("#buy-hint"); + +const stateNodes = { + cart: $("#state-cart"), + locale: $("#state-locale"), + orderConfirmation: $("#state-order-confirmation"), + error: $("#state-error"), + sessionId: $("#state-session-id"), +}; + +// ───── Mount the component (off-layout) ─────────────────────────────────── +// +// In real merchant integrations the element lives wherever +// it makes sense in the page. For popup / auto targets it has no visible UI +// of its own beyond a transient dialog scrim that appears when open() is +// called, so we attach it to and leave the storefront panel free for +// the merchant's product UI. + +const checkout = document.createElement("shopify-checkout"); +document.body.append(checkout); + +// ───── Form ↔ attributes ────────────────────────────────────────────────── + +function setStringAttribute(el: HTMLElement, name: string, value: FormDataEntryValue | null): void { + if (typeof value === "string" && value.length > 0) { + el.setAttribute(name, value); + } else { + el.removeAttribute(name); + } +} + +function syncAttributes(): void { + const data = new FormData(form); + setStringAttribute(checkout, "src", data.get("src")); + setStringAttribute(checkout, "target", data.get("target")); + + refreshBuyButton(data.get("src")); +} + +function refreshBuyButton(src: FormDataEntryValue | null): void { + const hasSrc = typeof src === "string" && src.length > 0; + buyNowButton.disabled = !hasSrc; + buyHint.style.display = hasSrc ? "none" : ""; +} + +form.addEventListener("input", syncAttributes); +form.addEventListener("change", syncAttributes); +syncAttributes(); + +// ───── Methods (Buy now + manual debug buttons) ─────────────────────────── + +document.addEventListener("click", (event) => { + const target = event.target; + if (!(target instanceof Element)) return; + const button = target.closest("button[data-method]"); + if (!button || button.disabled) return; + + switch (button.dataset["method"]) { + case "open": + checkout.open(); + break; + case "close": + checkout.close(); + break; + case "focus": + checkout.focus(); + break; + default: + break; + } +}); + +// ───── Variant swatches (visual only) ───────────────────────────────────── + +const swatches = document.querySelectorAll(".swatch"); +for (const swatch of swatches) { + swatch.addEventListener("click", () => { + for (const other of swatches) { + other.setAttribute("aria-pressed", "false"); + } + swatch.setAttribute("aria-pressed", "true"); + }); +} + +// ───── Event log ────────────────────────────────────────────────────────── + +const EVENT_TYPES = [ + "checkout:start", + "checkout:complete", + "checkout:close", + "checkout:error", + "checkout:addressChangeStart", + "checkout:paymentMethodChangeStart", + "checkout:submitStart", +] as const; + +const RESPONDABLE_EVENTS = new Set([ + "checkout:addressChangeStart", + "checkout:paymentMethodChangeStart", + "checkout:submitStart", +]); + +for (const type of EVENT_TYPES) { + checkout.addEventListener(type, () => { + appendLog(type); + refreshState(); + }); +} + +clearLogButton.addEventListener("click", () => { + eventLog.replaceChildren(); +}); + +function snapshotState(): Record { + return { + cart: checkout.cart, + locale: checkout.locale, + orderConfirmation: checkout.orderConfirmation, + error: checkout.error, + sessionId: checkout.sessionId, + }; +} + +function refreshState(): void { + stateNodes.cart.textContent = formatValue(checkout.cart); + stateNodes.locale.textContent = formatValue(checkout.locale); + stateNodes.orderConfirmation.textContent = formatValue(checkout.orderConfirmation); + stateNodes.error.textContent = formatValue(checkout.error); + stateNodes.sessionId.textContent = formatValue(checkout.sessionId); +} + +function appendLog(type: string): void { + const li = document.createElement("li"); + li.className = "event-entry"; + + const header = document.createElement("header"); + header.className = "event-entry-header"; + + const name = document.createElement("span"); + name.className = "event-entry-name"; + name.textContent = type; + header.append(name); + + if (RESPONDABLE_EVENTS.has(type)) { + const badge = document.createElement("span"); + badge.className = "event-entry-badge"; + badge.textContent = "respondable"; + header.append(badge); + } + + const time = document.createElement("time"); + time.className = "event-entry-time"; + time.textContent = timestamp(); + header.append(time); + + const pre = document.createElement("pre"); + pre.textContent = JSON.stringify(snapshotState(), null, 2); + + li.append(header, pre); + eventLog.prepend(li); +} diff --git a/platforms/web/sample/styles.css b/platforms/web/sample/styles.css new file mode 100644 index 00000000..6556ba9d --- /dev/null +++ b/platforms/web/sample/styles.css @@ -0,0 +1,519 @@ +:root { + color-scheme: light dark; + --bg: #fafaf9; + --surface: #ffffff; + --border: #e5e7eb; + --text: #0f172a; + --text-muted: #64748b; + --accent: #5a31f4; + --accent-text: #ffffff; + --code-bg: #f1f5f9; + --warn-bg: #fef3c7; + --warn-text: #78350f; + --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.05); + --shadow-md: 0 1px 3px rgba(15, 23, 42, 0.06), 0 4px 12px rgba(15, 23, 42, 0.04); + --radius: 8px; + --gap: 16px; + --pad: 20px; + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0f172a; + --surface: #1e293b; + --border: #334155; + --text: #f1f5f9; + --text-muted: #94a3b8; + --code-bg: #0b1220; + --warn-bg: #422006; + --warn-text: #fcd34d; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 1px 3px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(0, 0, 0, 0.25); + } +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100dvh; + display: flex; + flex-direction: column; + font-family: var(--font-sans); + background: var(--bg); + color: var(--text); + -webkit-font-smoothing: antialiased; +} + +/* ───── Topbar ────────────────────────────────────────────── */ +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 16px 24px; + border-bottom: 1px solid var(--border); + background: var(--surface); +} + +.topbar h1 { + margin: 0; + font-size: 16px; + font-weight: 600; +} + +.topbar .subtitle { + margin: 2px 0 0; + color: var(--text-muted); + font-size: 13px; +} + +.topbar .status { + margin: 0; + padding: 8px 12px; + font-size: 12px; + background: var(--warn-bg); + color: var(--warn-text); + border-radius: 6px; + max-width: 480px; + line-height: 1.45; +} + +.topbar .status code { + font-family: var(--font-mono); + background: rgba(0, 0, 0, 0.08); + padding: 1px 4px; + border-radius: 3px; +} + +/* ───── Layout ────────────────────────────────────────────── */ +main { + display: grid; + grid-template-columns: 300px minmax(360px, 1fr) 380px; + gap: var(--gap); + padding: var(--gap); + flex: 1; + min-height: 0; +} + +@media (max-width: 1100px) { + main { + grid-template-columns: 1fr; + } +} + +.panel { + display: flex; + flex-direction: column; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--pad); + min-height: 0; +} + +.panel h2 { + margin: 0 0 12px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-muted); +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.panel-header h2 { + margin: 0; +} + +.muted { + color: var(--text-muted); + font-size: 13px; +} + +/* ───── Form ──────────────────────────────────────────────── */ +#options-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +#options-form label { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 13px; + font-weight: 500; +} + +#options-form label.checkbox { + flex-direction: row; + align-items: center; +} + +#options-form input[type="text"], +#options-form input[type="url"], +#options-form select { + padding: 8px 10px; + font: inherit; + font-size: 13px; + color: var(--text); + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + outline: none; +} + +#options-form input:focus, +#options-form select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); +} + +fieldset { + margin: 0; + padding: 12px; + border: 1px solid var(--border); + border-radius: 6px; +} + +legend { + padding: 0 6px; + font-size: 11px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-muted); +} + +.button-row { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +button { + padding: 7px 12px; + font: inherit; + font-size: 13px; + color: var(--text); + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + cursor: pointer; + transition: + background 0.15s, + transform 0.05s; +} + +button:hover:not(:disabled) { + background: var(--code-bg); +} + +button:active:not(:disabled) { + transform: translateY(1px); +} + +button:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +/* ───── Storefront / product card ─────────────────────────── */ +.storefront { + align-items: center; + justify-content: flex-start; +} + +.storefront > h2 { + align-self: stretch; +} + +.product-card { + width: 100%; + max-width: 380px; + display: flex; + flex-direction: column; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + overflow: hidden; + box-shadow: var(--shadow-md); +} + +.product-image { + aspect-ratio: 4 / 3; + display: grid; + place-items: center; + background: + radial-gradient(circle at 28% 32%, #f9a8d4 0%, transparent 55%), + radial-gradient(circle at 72% 68%, #a5b4fc 0%, transparent 55%), + linear-gradient(135deg, #fce7f3 0%, #ede9fe 50%, #dbeafe 100%); +} + +@media (prefers-color-scheme: dark) { + .product-image { + background: + radial-gradient(circle at 28% 32%, #831843 0%, transparent 55%), + radial-gradient(circle at 72% 68%, #312e81 0%, transparent 55%), + linear-gradient(135deg, #4c1d95 0%, #1e1b4b 50%, #0c4a6e 100%); + } +} + +.product-emoji { + font-size: 80px; + line-height: 1; + filter: drop-shadow(0 4px 12px rgba(15, 23, 42, 0.18)); +} + +.product-info { + padding: 18px 20px 20px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.product-vendor { + margin: 0; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); +} + +.product-title { + margin: 0; + font-size: 18px; + font-weight: 600; + letter-spacing: -0.01em; +} + +.product-price { + margin: 2px 0 0; + font-size: 15px; + font-weight: 500; +} + +.product-description { + margin: 8px 0 4px; + font-size: 13px; + line-height: 1.5; + color: var(--text-muted); +} + +.product-variants { + display: flex; + gap: 8px; + margin: 6px 0 8px; +} + +.swatch { + width: 24px; + height: 24px; + padding: 0; + border-radius: 50%; + border: 2px solid transparent; + cursor: pointer; + transition: transform 0.15s; + position: relative; +} + +.swatch::after { + content: ""; + position: absolute; + inset: -4px; + border-radius: 50%; + border: 2px solid transparent; + transition: border-color 0.15s; +} + +.swatch:hover { + transform: scale(1.08); +} + +.swatch[aria-pressed="true"]::after { + border-color: var(--accent); +} + +.swatch--clay { + background: #c2410c; +} +.swatch--moss { + background: #4d7c0f; +} +.swatch--sand { + background: #d6d3d1; +} + +.product-actions { + display: flex; + gap: 8px; + margin-top: 10px; +} + +.product-actions button { + flex: 1; + padding: 11px 16px; + font-size: 14px; + font-weight: 600; + border-radius: 8px; + transition: + background 0.15s, + transform 0.05s, + opacity 0.15s; +} + +.product-actions .primary { + background: var(--accent); + color: var(--accent-text); + border-color: var(--accent); +} + +.product-actions .primary:hover:not(:disabled) { + background: color-mix(in srgb, var(--accent) 88%, black); +} + +.product-actions .secondary { + background: var(--surface); + color: var(--text); + border-color: var(--border); +} + +#buy-hint { + margin: 8px 0 0; + font-size: 12px; + text-align: center; +} + +/* ───── Component state ───────────────────────────────────── */ +#component-state { + width: 100%; + max-width: 380px; + margin-top: 16px; + padding: 10px 12px; + background: var(--code-bg); + border-radius: 6px; +} + +#component-state summary { + cursor: pointer; + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; + list-style: none; +} + +#component-state summary::before { + content: "▸"; + display: inline-block; + margin-right: 6px; + transition: transform 0.15s; +} + +#component-state[open] summary::before { + transform: rotate(90deg); +} + +#component-state dl { + margin: 12px 0 0; + display: grid; + grid-template-columns: 130px minmax(0, 1fr); + gap: 6px 10px; + font-family: var(--font-mono); + font-size: 11px; +} + +#component-state dt { + color: var(--text-muted); +} + +#component-state dd { + margin: 0; + word-break: break-word; + white-space: pre-wrap; +} + +/* ───── Events panel ──────────────────────────────────────── */ +#event-log { + list-style: none; + margin: 0; + padding: 0; + flex: 1; + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 8px; +} + +#event-empty { + display: none; +} + +#event-log:empty + #event-empty { + display: block; + padding: 12px 0; +} + +.event-entry { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + padding: 10px 12px; + font-size: 12px; +} + +.event-entry-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; +} + +.event-entry-name { + font-family: var(--font-mono); + font-weight: 600; + color: var(--accent); +} + +.event-entry-badge { + font-size: 10px; + font-weight: 600; + padding: 2px 6px; + background: var(--accent); + color: var(--accent-text); + border-radius: 3px; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.event-entry-time { + margin-left: auto; + color: var(--text-muted); + font-family: var(--font-mono); + font-size: 11px; +} + +.event-entry pre { + margin: 0; + padding: 8px 10px; + background: var(--code-bg); + border-radius: 4px; + font-family: var(--font-mono); + font-size: 11px; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; +} diff --git a/platforms/web/sample/tsconfig.json b/platforms/web/sample/tsconfig.json new file mode 100644 index 00000000..92a0e93f --- /dev/null +++ b/platforms/web/sample/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + // The sample uses vanilla DOM patterns and a forward-looking shim + // (types.d.ts) for the not-yet-implemented component. Don't enforce the + // library-grade isolatedDeclarations rule here. + "isolatedDeclarations": false, + "declaration": false, + "noEmit": true, + // vite/client provides the `*.css` module declaration so `import "./styles.css"` typechecks. + "types": ["vite/client"] + }, + "include": ["./**/*.ts", "./**/*.d.ts"] +} diff --git a/platforms/web/sample/types.d.ts b/platforms/web/sample/types.d.ts new file mode 100644 index 00000000..ad569659 --- /dev/null +++ b/platforms/web/sample/types.d.ts @@ -0,0 +1,51 @@ +// TODO: delete this file once `@shopify/checkout-kit` exports the real +// `ShopifyCheckout` types from `../src`. +// +// Forward-looking type declarations describing the API surface that the +// `` component will expose once its implementation lands +// in `../src`. Lets the playground compile and read as if the component +// were already in place. At runtime, the element is not yet registered +// and renders as an unknown HTML element. + +// `inline` is intentionally omitted from the initial release — only popup +// (window.open with explicit features) and auto (window.open new tab) are +// supported in v1. Add `"inline"` back here when iframe rendering lands. +type CheckoutTarget = "auto" | "popup"; + +interface ShopifyCheckoutCart { + [key: string]: unknown; +} + +interface ShopifyCheckoutOrderConfirmation { + [key: string]: unknown; +} + +interface ShopifyCheckoutError { + code: string; + message: string; +} + +interface ShopifyCheckoutElement extends HTMLElement { + // ── Read/write attributes (reflected) ── + src: string; + target: CheckoutTarget | string; + + // ── Read-only state populated by checkout protocol events ── + readonly cart?: ShopifyCheckoutCart; + readonly locale?: string; + readonly orderConfirmation?: ShopifyCheckoutOrderConfirmation; + readonly error?: ShopifyCheckoutError; + readonly sessionId?: string; + + // ── Methods ── + open(): void; + close(): void; + focus(): void; +} + +// In an ambient .d.ts (no top-level imports/exports), interface declarations +// are global, so `HTMLElementTagNameMap` is augmented directly without +// needing a `declare global` wrapper. +interface HTMLElementTagNameMap { + "shopify-checkout": ShopifyCheckoutElement; +} diff --git a/platforms/web/sample/vite.config.ts b/platforms/web/sample/vite.config.ts new file mode 100644 index 00000000..70bc45a1 --- /dev/null +++ b/platforms/web/sample/vite.config.ts @@ -0,0 +1,21 @@ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { defineConfig } from "vite"; + +const here = dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + // Treat `sample/` as the project root so vite serves `index.html` from here. + root: here, + build: { + outDir: resolve(here, "dist"), + emptyOutDir: true, + target: "es2022", + sourcemap: true, + }, + server: { + port: 5173, + open: true, + }, +});