wip: scaffold Vite 8 + React 19 SPA under vite/ — Next.js port blocked

The existing Next.js app (99 pages, 427 next/* imports, 177 files using
next/router, 154 using nextjs-routes types, 105 using getServerSideProps) is
far too coupled to rewrite in one pass. A full migration is ~2–4 weeks of
mechanical work.

This commit adds the scaffold only:

  vite/package.json      — Vite 8 + React 19.2 + wouter, no Next deps
  vite/vite.config.ts    — aliases at ../lib, ../ui, ../configs, … so source
                           migrates file-by-file instead of moving
  vite/tsconfig.json     — mirrors the aliases for TypeScript
  vite/index.html        — security headers as <meta> (was nextjs/headers.js)
  vite/src/main.tsx      — React 19 root bootstrap
  vite/src/env.ts        — runtime envs: window.__envs first, import.meta.env
                           fallback, maps NEXT_PUBLIC_API_HOST → VITE_API_BASE_URL
  vite/src/redirects.ts  — full port of nextjs/redirects.js (69 client rules)
  vite/src/App.tsx       — wouter router with CatchAll redirect resolver
  vite/Dockerfile        — 2-stage → ghcr.io/hanzoai/spa
  vite/LLM.md            — what's done, what's left, how to finish

The production Dockerfile at ../Dockerfile still builds the Next.js app. This
scaffold is independent; nothing in the existing tree is deleted or moved.

See vite/LLM.md for the full migration plan and 9 critical TODOs before this
can replace the Next.js build.
This commit is contained in:
2026-04-19 00:05:37 -07:00
parent bb2623fe98
commit b383c07020
13 changed files with 546 additions and 0 deletions
+2
View File
@@ -286,12 +286,14 @@
"VEVENT",
"viem",
"vitalik",
"vite",
"vitest",
"wagmi",
"warpcast",
"watchlist",
"webacy",
"Winky",
"wouter",
"WOWG",
"WPOA",
"xdai",
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
.vite
*.log
+19
View File
@@ -0,0 +1,19 @@
# WIP Vite 8 SPA build — see vite/LLM.md for migration plan.
# The production image (../Dockerfile) still builds the Next.js app; this
# Dockerfile will replace it once the SPA rewrite is complete.
FROM node:22-alpine AS build
RUN corepack enable
WORKDIR /app
# Copy the whole repo because the vite/ project aliases resolve into
# ../configs, ../lib, ../ui, etc. (see vite.config.ts). Once migration
# finalizes these paths will live inside vite/src and the context can shrink.
COPY . .
WORKDIR /app/vite
RUN pnpm install --frozen-lockfile || pnpm install
RUN pnpm build
FROM ghcr.io/hanzoai/spa
COPY --from=build /app/vite/dist /public
+116
View File
@@ -0,0 +1,116 @@
# lux/explore — Vite 8 SPA migration (WIP scaffold)
**Status**: Scaffold only. The Next.js app under `../pages` is still the production build.
## Why a scaffold, not a rewrite
A full Next.js → Vite conversion of this codebase is a multi-week project, not
a one-shot:
| Coupling point | Count |
|---|---|
| Total `next/*` imports | 427 |
| Files using `next/router` | 177 |
| Files using `nextjs-routes` generated types | 154 |
| Pages with `getServerSideProps` | 105 |
| `pages/` file-system routes | 99 |
| `pages/api/` server routes | 9 |
Plus: the Next.js runtime container has a custom `entrypoint.sh` that generates
`/assets/envs.js`, `envs-validator`, `sitemap-generator`, `favicon-generator`,
and `og-image-generator` — all tied to `next start`. `ghcr.io/hanzoai/spa` is
pure static serving; none of those runtime helpers run inside it.
## What this scaffold provides
- `vite/package.json` — Vite 8 + React 19.2 + wouter, no Next deps
- `vite/vite.config.ts` — aliases point at the *existing* `../lib`, `../ui`,
`../configs`, `../toolkit`, `../nextjs` trees so source can be migrated file
by file instead of moved
- `vite/tsconfig.json` — mirrors the aliases for TypeScript
- `vite/index.html` — security headers as `<meta>` tags (was `nextjs/headers.js`)
- `vite/src/env.ts` — runtime env resolution, preserves the `window.__envs`
contract from `/assets/envs.js` and adds `VITE_API_BASE_URL` fallback
- `vite/src/redirects.ts` — port of `nextjs/redirects.js` as a client-side
match + substitute table (69 rules, 4 deprecated permanent)
- `vite/src/App.tsx` — wouter router with redirect resolver + landing page
- `vite/Dockerfile` — 2-stage build → `ghcr.io/hanzoai/spa`
## What's NOT migrated
These are the tasks the next engineer must complete **before** this replaces
the production Next.js app:
1. **99 pages.** Every file in `../pages/*.tsx` needs to become a wouter
`<Route>` in `src/App.tsx`. The component bodies *usually* transfer as-is
because Blockscout components are already client-heavy — the hard part is:
- `next/router` (177 files) → `useLocation` + `useSearch` from wouter
- `nextjs-routes` (154 files) → a hand-maintained `Route` type union
- `getServerSideProps` (105 files) → React Query `useQuery` hooks against
the public API. The existing `nextjs/getServerSideProps/main.ts` helpers
mostly just pre-fetched data that's also available client-side.
- `next/dynamic` (heavy use in `_app.tsx`) → `React.lazy` + `Suspense`
2. **9 API routes.** These run server-side in Next.js; none can run in a
static SPA. Options per route:
- `/api/config` — runtime env injection. Replace with the SPA's existing
`/assets/envs.js` pattern (the `hanzoai/spa` image can template this at
container boot).
- `/api/pchain` — CORS-avoiding proxy for JSON-RPC. Either (a) enable CORS
on the gateway for `/ext/bc/P/*`, or (b) deploy a separate tiny Go proxy
alongside the SPA. Option (a) is cleaner.
- `/api/csrf`, `/api/proxy` — Blockscout account API auth. Must move to the
same pattern: gateway-side, or the account API itself exposes them.
- `/api/metrics` — Prometheus. Drop for frontend, or scrape the pod directly.
- `/api/log`, `/api/healthz`, `/api/monitoring/*`, `/api/tokens/*` — re-home
case by case. Most can be deleted — they were convenience endpoints.
3. **Runtime container tooling.** The existing `deploy/scripts/entrypoint.sh`
wires together envs-validator, sitemap-generator, favicon-generator. These
need either:
- Moved to **build-time** (preferred — bake favicons + sitemap into `dist/`
during Vite build), or
- Rewritten as init-containers in K8s that populate the shared static
volume before `hanzoai/spa` reads it.
4. **`next/image` usage.** There are ~60 imports. Replace with `<img>` + Vite's
static asset imports, or add `vite-imagetools` if per-size variants matter.
5. **`next-themes`.** Replace with `@hanzo/gui` theme provider used by cloud/web.
6. **Tailwind.** Already configured via `postcss.config.mjs` — should Just Work
once `vite/src/styles.css` pulls in the same `@tailwind` directives.
## Estimated migration effort
- **% of UI preserved at full completion**: ~85%. Blockscout's components are
framework-agnostic React; only route wiring changes. The remaining 15% is
`_app.tsx` providers, `_document.tsx` (replaced by `index.html`), and pages
that lean on `getServerSideProps` for SEO-critical data.
- **Time to ship**: ~2–4 weeks for one engineer. The bulk is mechanical
transform of `router.push(url)` → `navigate(url)` and deleting
`getServerSideProps` exports.
- **Bail-out threshold**: If upstream Blockscout merges come in, re-evaluate.
A permanent fork from Blockscout means owning all feature work forever.
## Critical TODOs before going live
1. `vite build` must produce a working `dist/` (currently builds an empty
landing page — not the explorer)
2. Smoke-test every route group: `/block/:h`, `/tx/:h`, `/address/:h`,
`/token/:h`, `/stats`, `/chains`, `/bridge`, `/account/*`, `/api-docs`
3. Wire React Query + replace `getServerSideProps` with `useQuery`
4. Validate runtime env injection via `window.__envs` from SPA container
5. Configure CSP for the SPA — current Next.js CSP in `nextjs/csp/` must port
6. Replace `/api/pchain` — enable gateway CORS OR ship a sidecar proxy
7. Decide favicon/sitemap generation strategy (build-time vs init-container)
8. `universe/` K8s manifests must switch image from `@luxfi/explore` (Next)
to `@luxfi/explore-vite` (SPA) once parity is reached
9. Preserve `NEXT_PUBLIC_*` env names for a release cycle to avoid breaking
operator deployments — map them inside `vite/src/env.ts` (already done)
## Reference implementation
`~/work/lux/cloud/apps/web/` is the canonical Vite 8 + wouter + `hanzoai/spa`
pattern. That app is ~2k LOC; `explore` is ~176k. The patterns copy over,
the volume does not.
+20
View File
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-Frame-Options" content="SAMEORIGIN" />
<meta http-equiv="X-Content-Type-Options" content="nosniff" />
<meta name="referrer" content="origin-when-cross-origin" />
<link rel="icon" type="image/x-icon" href="/lux-favicon.ico" />
<title>Lux Explorer</title>
<meta name="description" content="Lux Network blockchain explorer — blocks, transactions, addresses, tokens." />
<meta property="og:title" content="Lux Explorer" />
<meta property="og:site_name" content="Lux Explorer" />
<meta property="og:type" content="website" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+26
View File
@@ -0,0 +1,26 @@
{
"name": "@luxfi/explore-vite",
"version": "0.1.0-wip",
"private": true,
"type": "module",
"description": "WIP Vite 8 + React 19 SPA rewrite of @luxfi/explore. Scaffold only — see LLM.md.",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview --port 3000",
"typecheck": "tsc -b --noEmit"
},
"dependencies": {
"react": "19.2.4",
"react-dom": "19.2.4",
"wouter": "^3.7.1"
},
"devDependencies": {
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "^5.1.0",
"typescript": "5.9.3",
"vite": "^8.0.0",
"vite-plugin-svgr": "^4.5.0"
}
}
+56
View File
@@ -0,0 +1,56 @@
import { Route, Switch, Redirect, useLocation } from 'wouter';
import { apiBaseUrl } from './env';
import { matchRedirect, redirects } from './redirects';
// WIP landing page for the Vite SPA scaffold.
// The Next.js app at ../pages/*.tsx remains the production surface. Once
// components are decoupled from next/router + nextjs-routes, individual
// routes below will replace their Next equivalents one at a time.
function Home() {
return (
<main style={{ maxWidth: 720, margin: '80px auto', padding: 24, fontFamily: 'system-ui' }}>
<h1 style={{ fontSize: 32, fontWeight: 600 }}>Lux Explorer — Vite SPA (WIP)</h1>
<p style={{ marginTop: 16, color: '#666' }}>
This is the Vite 8 + React 19 scaffold that will replace the Next.js app
under <code>../pages</code>. See <code>vite/LLM.md</code> for the migration plan.
</p>
<dl style={{ marginTop: 24, display: 'grid', gridTemplateColumns: 'auto 1fr', gap: 8 }}>
<dt style={{ fontWeight: 600 }}>API base:</dt>
<dd><code>{ apiBaseUrl() || '(not configured)' }</code></dd>
<dt style={{ fontWeight: 600 }}>Redirects loaded:</dt>
<dd>{ redirects.length }</dd>
</dl>
</main>
);
}
function NotFound() {
const [ path ] = useLocation();
return (
<main style={{ maxWidth: 720, margin: '80px auto', padding: 24, fontFamily: 'system-ui' }}>
<h1 style={{ fontSize: 48, fontWeight: 700 }}>404</h1>
<p style={{ marginTop: 16, color: '#666' }}>
Not found: <code>{ path }</code>
</p>
</main>
);
}
function CatchAll() {
const [ path ] = useLocation();
const dest = matchRedirect(path);
if (dest) return <Redirect to={ dest } replace/>;
return <NotFound/>;
}
export function App() {
return (
<Switch>
<Route path="/" component={ Home }/>
{ /* Every unmatched path is tested against the redirect table. */ }
<Route component={ CatchAll }/>
</Switch>
);
}
+48
View File
@@ -0,0 +1,48 @@
// Runtime env resolution for the Vite SPA.
//
// The Next.js app injected envs via /assets/envs.js (generated at container
// startup by deploy/scripts/make_envs_script.sh). For the SPA we keep the same
// shape — envs.js sets window.__envs — and fall back to Vite build-time
// import.meta.env so local `pnpm dev` works without the shell script.
declare global {
interface Window {
__envs?: Record<string, string>;
}
}
const runtime = (): Record<string, string> => window.__envs ?? {};
export function env(key: string, fallback = ''): string {
const r = runtime()[key];
if (r !== undefined && r !== '') return r;
const b = (import.meta.env as Record<string, string | undefined>)[key];
return b ?? fallback;
}
// Derive the backend REST base URL. In the Next.js app this was set via
// NEXT_PUBLIC_API_HOST + NEXT_PUBLIC_API_BASE_PATH and proxied through
// /node-api/* by next.config.js rewrites. For the SPA we hit it directly with
// CORS, so the gateway must enable it.
export function apiBaseUrl(): string {
const explicit = env('VITE_API_BASE_URL');
if (explicit) return explicit.replace(/\/$/, '');
const host = env('NEXT_PUBLIC_API_HOST') || env('VITE_API_HOST');
const proto = env('NEXT_PUBLIC_API_PROTOCOL') || env('VITE_API_PROTOCOL') || 'https';
const basePath = env('NEXT_PUBLIC_API_BASE_PATH') || '';
if (!host) return '';
return `${ proto }://${ host }${ basePath }`.replace(/\/$/, '');
}
// RPC URL for direct wallet/chain calls (e.g. viem/wagmi).
export function rpcUrl(): string {
return env('VITE_RPC_URL') || env('NEXT_PUBLIC_NETWORK_RPC_URL') || '';
}
// Chain id for wallet connect + signature binding.
export function chainId(): number {
const raw = env('VITE_CHAIN_ID') || env('NEXT_PUBLIC_NETWORK_ID') || '0';
const n = Number(raw);
return Number.isFinite(n) ? n : 0;
}
+14
View File
@@ -0,0 +1,14 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import './styles.css';
const root = document.getElementById('root');
if (!root) throw new Error('root element missing');
createRoot(root).render(
<StrictMode>
<App/>
</StrictMode>,
);
+135
View File
@@ -0,0 +1,135 @@
// Client-side redirect table. Port of nextjs/redirects.js.
//
// Next.js did these server-side via next.config.js `redirects`. The SPA does
// them in the router at mount time — first match wins. `:param` placeholders
// carry forward into the destination. Query params survive as-is.
export interface Redirect {
source: string;
destination: string;
permanent: boolean;
}
const OLD_UI_URLS: Array<Omit<Redirect, 'permanent'>> = [
// ACCOUNT
{ source: '/account/tag_address', destination: '/account/tag-address' },
{ source: '/account/tag_address/new', destination: '/account/tag-address' },
{ source: '/account/tag_transaction', destination: '/account/tag-address?tab=tx' },
{ source: '/account/tag_transaction/new', destination: '/account/tag-address?tab=tx' },
{ source: '/account/watchlist_address/:id/edit', destination: '/account/watchlist' },
{ source: '/account/watchlist_address/new', destination: '/account/watchlist' },
{ source: '/account/api_key', destination: '/account/api-key' },
{ source: '/account/api_key/:id/edit', destination: '/account/api-key' },
{ source: '/account/api_key/new', destination: '/account/api-key' },
{ source: '/account/custom_abi', destination: '/account/custom-abi' },
{ source: '/account/custom_abi/:id/edit', destination: '/account/custom-abi' },
{ source: '/account/custom_abi/new', destination: '/account/custom-abi' },
{ source: '/account/public-tags-request', destination: '/public-tags/submit' },
{ source: '/account/rewards', destination: '/account/merits' },
// TRANSACTIONS
{ source: '/pending-transactions', destination: '/txs?tab=pending' },
{ source: '/tx/:hash/internal-transactions', destination: '/tx/:hash?tab=internal' },
{ source: '/tx/:hash/logs', destination: '/tx/:hash?tab=logs' },
{ source: '/tx/:hash/raw-trace', destination: '/tx/:hash?tab=raw_trace' },
{ source: '/tx/:hash/state', destination: '/tx/:hash?tab=state' },
{ source: '/tx/:hash/token-transfers', destination: '/tx/:hash?tab=token_transfers' },
// BLOCKS
{ source: '/blocks/:height', destination: '/block/:height' },
{ source: '/uncles', destination: '/blocks?tab=uncles' },
{ source: '/reorgs', destination: '/blocks?tab=reorgs' },
{ source: '/block/:height/transactions', destination: '/block/:height?tab=txs' },
{ source: '/block/:height/withdrawals', destination: '/block/:height?tab=withdrawals' },
// ADDRESS
{ source: '/address/:hash/transactions', destination: '/address/:hash' },
{ source: '/address/:hash/token-transfers', destination: '/address/:hash?tab=token_transfers' },
{ source: '/address/:hash/tokens', destination: '/address/:hash?tab=tokens' },
{ source: '/address/:hash/internal-transactions', destination: '/address/:hash?tab=internal_txns' },
{ source: '/address/:hash/coin-balances', destination: '/address/:hash?tab=coin_balance_history' },
{ source: '/address/:hash/logs', destination: '/address/:hash?tab=logs' },
{ source: '/address/:hash/validations', destination: '/address/:hash?tab=blocks_validated' },
{ source: '/address/:hash/contracts', destination: '/address/:hash?tab=contract' },
{ source: '/address/:hash/read-contract', destination: '/address/:hash?tab=read_contract' },
{ source: '/address/:hash/read-proxy', destination: '/address/:hash?tab=read_proxy' },
{ source: '/address/:hash/write-contract', destination: '/address/:hash?tab=write_contract' },
{ source: '/address/:hash/write-proxy', destination: '/address/:hash?tab=write_proxy' },
// TOKENS
{ source: '/bridged-tokens', destination: '/tokens?tab=bridged' },
{ source: '/bridged-tokens/:chain_name', destination: '/tokens?tab=bridged' },
{ source: '/token/:hash/token-transfers', destination: '/token/:hash?tab=token_transfers' },
{ source: '/token/:hash/token-holders', destination: '/token/:hash?tab=holders' },
{ source: '/token/:hash/inventory', destination: '/token/:hash?tab=inventory' },
{ source: '/token/:hash/instance/:id/token-transfers', destination: '/token/:hash/instance/:id' },
{ source: '/token/:hash/instance/:id/token-holders', destination: '/token/:hash/instance/:id?tab=holders' },
{ source: '/token/:hash/instance/:id/metadata', destination: '/token/:hash/instance/:id?tab=metadata' },
{ source: '/token/:hash/read-contract', destination: '/token/:hash?tab=read_contract' },
{ source: '/token/:hash/read-proxy', destination: '/token/:hash?tab=read_proxy' },
{ source: '/token/:hash/write-contract', destination: '/token/:hash?tab=write_contract' },
{ source: '/token/:hash/write-proxy', destination: '/token/:hash?tab=write_proxy' },
// ROLLUPS
{ source: '/l2-txn-batches', destination: '/batches' },
{ source: '/zkevm-l2-txn-batches', destination: '/batches' },
{ source: '/l2-deposits', destination: '/deposits' },
{ source: '/l2-withdrawals', destination: '/withdrawals' },
{ source: '/l2-output-roots', destination: '/output-roots' },
];
const ETHERSCAN_URLS: Array<Omit<Redirect, 'permanent'>> = [
{ source: '/txsAA', destination: '/ops' },
{ source: '/blocks_forked', destination: '/blocks?tab=reorgs' },
{ source: '/contractsVerified', destination: '/verified-contracts' },
{ source: '/tokentxns', destination: '/token-transfers' },
{ source: '/nft/:hash/:id', destination: '/token/:hash/instance/:id' },
{ source: '/charts', destination: '/stats' },
{ source: '/name-lookup-search', destination: '/name-domains' },
{ source: '/txsExit', destination: '/withdrawals' },
{ source: '/txsEnqueued', destination: '/deposits' },
{ source: '/cc/txs', destination: '/txs?tab=cctx' },
];
const DEPRECATED_ROUTES: Array<Redirect> = [
{ source: '/graphiql', destination: '/api-docs?tab=graphql_api', permanent: true },
{ source: '/name-domains', destination: '/name-services', permanent: true },
{ source: '/name-domains/:name', destination: '/name-services/domains/:name', permanent: true },
];
export const redirects: Array<Redirect> = [
...OLD_UI_URLS.map((r) => ({ ...r, permanent: false })),
...ETHERSCAN_URLS.map((r) => ({ ...r, permanent: true })),
...DEPRECATED_ROUTES,
];
// Match a path against `:param` placeholders and substitute into destination.
// Returns the destination path+query, or null if no match.
export function matchRedirect(pathname: string): string | null {
for (const r of redirects) {
const params = matchPath(r.source, pathname);
if (params) return substitute(r.destination, params);
}
return null;
}
function matchPath(source: string, pathname: string): Record<string, string> | null {
const sParts = source.split('/').filter(Boolean);
const pParts = pathname.split('/').filter(Boolean);
if (sParts.length !== pParts.length) return null;
const params: Record<string, string> = {};
for (let i = 0; i < sParts.length; i++) {
const s = sParts[i];
const p = pParts[i];
if (s.startsWith(':')) {
params[s.slice(1)] = decodeURIComponent(p);
} else if (s !== p) {
return null;
}
}
return params;
}
function substitute(dest: string, params: Record<string, string>): string {
return dest.replace(/:([a-z_]+)/gi, (_, k) => params[k] ?? `:${ k }`);
}
+38
View File
@@ -0,0 +1,38 @@
/* Minimal baseline. The production app will import toolkit/theme tokens and
* Tailwind layers from ../styles once components migrate. */
:root {
color-scheme: light dark;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.5;
}
html, body, #root {
margin: 0;
padding: 0;
height: 100%;
}
body {
background: #fff;
color: #111;
}
@media (prefers-color-scheme: dark) {
body {
background: #0b0b0b;
color: #f5f5f5;
}
}
code {
font-family: "SF Mono", Consolas, Menlo, monospace;
font-size: 0.9em;
background: rgba(128, 128, 128, 0.12);
padding: 1px 4px;
border-radius: 3px;
}
a {
color: inherit;
}
+31
View File
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"baseUrl": ".",
"paths": {
"configs/*": ["../configs/*"],
"lib/*": ["../lib/*"],
"mocks/*": ["../mocks/*"],
"nextjs/*": ["../nextjs/*"],
"stubs/*": ["../stubs/*"],
"toolkit/*": ["../toolkit/*"],
"tools/*": ["../tools/*"],
"types/*": ["../types/*"],
"ui/*": ["../ui/*"],
"icons/*": ["../icons/*"]
}
},
"include": ["src", "vite.config.ts"]
}
+37
View File
@@ -0,0 +1,37 @@
import react from '@vitejs/plugin-react';
import path from 'node:path';
import { defineConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
// Mirror the Next.js alias root so we can reuse existing source folders as the
// migration progresses. Each alias points at ../<dir>, not vite/src/<dir>.
const aliasRoot = path.resolve(__dirname, '..');
export default defineConfig({
plugins: [ react(), svgr() ],
resolve: {
alias: {
configs: path.join(aliasRoot, 'configs'),
lib: path.join(aliasRoot, 'lib'),
mocks: path.join(aliasRoot, 'mocks'),
nextjs: path.join(aliasRoot, 'nextjs'),
stubs: path.join(aliasRoot, 'stubs'),
toolkit: path.join(aliasRoot, 'toolkit'),
tools: path.join(aliasRoot, 'tools'),
types: path.join(aliasRoot, 'types'),
ui: path.join(aliasRoot, 'ui'),
icons: path.join(aliasRoot, 'icons'),
'public': path.join(aliasRoot, 'public'),
},
},
build: {
outDir: 'dist',
emptyOutDir: true,
target: 'es2022',
sourcemap: false,
},
server: {
port: 3000,
host: '0.0.0.0',
},
});