mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
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:
@@ -286,12 +286,14 @@
|
||||
"VEVENT",
|
||||
"viem",
|
||||
"vitalik",
|
||||
"vite",
|
||||
"vitest",
|
||||
"wagmi",
|
||||
"warpcast",
|
||||
"watchlist",
|
||||
"webacy",
|
||||
"Winky",
|
||||
"wouter",
|
||||
"WOWG",
|
||||
"WPOA",
|
||||
"xdai",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.vite
|
||||
*.log
|
||||
@@ -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
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>,
|
||||
);
|
||||
@@ -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 }`);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user