feat(console-ui): add TanStack management console
- provide Runtime overview, service lifecycle, invocation, and event views - add immutable WIT package listing, publication, and download workflows - connect all state to the Rust management API without direct database access - use TanStack Start with Node serving, Oxlint, Oxfmt, and SSR checks - include responsive operational layouts and empty Actor placeholders
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
/node_modules/
|
||||||
|
/dist/
|
||||||
|
/.tanstack/
|
||||||
|
*.tsbuildinfo
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"ignorePatterns": ["src/routeTree.gen.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["typescript", "unicorn", "oxc", "react", "jsx-a11y", "node"],
|
||||||
|
"categories": {
|
||||||
|
"correctness": "error"
|
||||||
|
},
|
||||||
|
"rules": {},
|
||||||
|
"ignorePatterns": ["src/routeTree.gen.ts"],
|
||||||
|
"env": {
|
||||||
|
"builtin": true,
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Wasmeld Console
|
||||||
|
|
||||||
|
WebAssembly Runtime 的管理控制台,使用 TanStack Start、TanStack Router、
|
||||||
|
React 和 srvx 构建,通过 Node.js 运行。页面只调用 `wasmeld-console` HTTP API,
|
||||||
|
不直接访问数据库或 Wasmeld Runtime。
|
||||||
|
|
||||||
|
运行概览可以启动、停止和重启整个 Runtime;服务版本与运行实例页面负责管理单个
|
||||||
|
Wasm Actor。停止 Runtime 不会断开管理 API。
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
默认访问 `http://localhost:3000`。
|
||||||
|
|
||||||
|
管理 API 默认使用 `http://127.0.0.1:8080`。可以在控制台的“运行设置”中修改,
|
||||||
|
或在构建时设置 `VITE_WASMELD_API_URL`。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run format:check
|
||||||
|
npm run lint
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
生产构建完成后可以运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm test` 会生成 Node.js 生产构建,并验证 srvx 服务端渲染和静态资源。
|
||||||
Generated
+3121
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "wasmeld-console",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build && tsc --noEmit",
|
||||||
|
"start": "srvx serve --prod --entry dist/server/server.js --static ../client",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "npm run build && node --test tests/rendered-html.test.mjs",
|
||||||
|
"lint": "oxlint . --deny-warnings",
|
||||||
|
"format": "oxfmt .",
|
||||||
|
"format:check": "oxfmt --check ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/react-router": "^1.170.18",
|
||||||
|
"@tanstack/react-start": "^1.168.32",
|
||||||
|
"lucide-react": "^1.27.0",
|
||||||
|
"react": "19.2.6",
|
||||||
|
"react-dom": "19.2.6",
|
||||||
|
"srvx": "^0.12.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "22.19.19",
|
||||||
|
"@types/react": "19.2.14",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@vitejs/plugin-react": "6.0.2",
|
||||||
|
"oxfmt": "^0.60.0",
|
||||||
|
"oxlint": "^1.75.0",
|
||||||
|
"typescript": "5.9.3",
|
||||||
|
"vite": "^8.1.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.13.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -0,0 +1,208 @@
|
|||||||
|
export const DEFAULT_API_BASE = import.meta.env.VITE_WASMELD_API_URL ?? "http://127.0.0.1:8080";
|
||||||
|
|
||||||
|
const API_BASE_STORAGE_KEY = "wasmeld-api-base";
|
||||||
|
|
||||||
|
export type BackendServiceStatus = "running" | "stopped" | "faulted";
|
||||||
|
|
||||||
|
export type BackendService = {
|
||||||
|
id: string;
|
||||||
|
revision: string;
|
||||||
|
artifact: string;
|
||||||
|
world: string;
|
||||||
|
status: BackendServiceStatus;
|
||||||
|
updated_at_ms: number;
|
||||||
|
limits: {
|
||||||
|
memory_bytes: number;
|
||||||
|
fuel_per_call: number;
|
||||||
|
deadline_ms: number;
|
||||||
|
mailbox_capacity: number;
|
||||||
|
max_input_bytes: number;
|
||||||
|
max_output_bytes: number;
|
||||||
|
};
|
||||||
|
calls: number;
|
||||||
|
errors: number;
|
||||||
|
last_latency_ms: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BackendEvent = {
|
||||||
|
id: number;
|
||||||
|
timestamp_ms: number;
|
||||||
|
kind: "registered" | "started" | "stopped" | "invoked" | "failed";
|
||||||
|
service_id: string | null;
|
||||||
|
revision: string | null;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BackendRuntime = {
|
||||||
|
status: "running" | "stopped";
|
||||||
|
uptime_ms: number;
|
||||||
|
managed_services: number;
|
||||||
|
registered_services: number;
|
||||||
|
running_services: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BackendWitDependency = {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BackendWitPackage = {
|
||||||
|
schema_version: number;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
sha256: string;
|
||||||
|
dependencies: BackendWitDependency[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegisterComponentInput = {
|
||||||
|
file: File;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InvokeResult = {
|
||||||
|
output: Uint8Array;
|
||||||
|
outputBytes: number;
|
||||||
|
latencyMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getApiBase(): string {
|
||||||
|
if (typeof window === "undefined") return DEFAULT_API_BASE;
|
||||||
|
return window.localStorage.getItem(API_BASE_STORAGE_KEY) ?? DEFAULT_API_BASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveApiBase(value: string): string {
|
||||||
|
const normalized = value.trim().replace(/\/+$/, "");
|
||||||
|
const parsed = new URL(normalized);
|
||||||
|
if (!["http:", "https:"].includes(parsed.protocol)) {
|
||||||
|
throw new Error("API 地址必须使用 http 或 https");
|
||||||
|
}
|
||||||
|
window.localStorage.setItem(API_BASE_STORAGE_KEY, normalized);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchSnapshot(apiBase: string) {
|
||||||
|
const [services, events, runtime, witPackages] = await Promise.all([
|
||||||
|
request<{ services: BackendService[] }>(apiBase, "/api/v1/services"),
|
||||||
|
request<{ events: BackendEvent[] }>(apiBase, "/api/v1/events"),
|
||||||
|
request<BackendRuntime>(apiBase, "/api/v1/runtime"),
|
||||||
|
request<{ packages: BackendWitPackage[] }>(apiBase, "/api/v1/wit/packages"),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
services: services.services,
|
||||||
|
events: events.events,
|
||||||
|
runtime,
|
||||||
|
witPackages: witPackages.packages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerComponent(
|
||||||
|
apiBase: string,
|
||||||
|
input: RegisterComponentInput,
|
||||||
|
): Promise<BackendService> {
|
||||||
|
const form = new FormData();
|
||||||
|
form.set("package", input.file, input.file.name);
|
||||||
|
return request(apiBase, "/api/v1/services", {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publishWitPackage(apiBase: string, file: File): Promise<BackendWitPackage> {
|
||||||
|
const form = new FormData();
|
||||||
|
form.set("package", file, file.name);
|
||||||
|
return request(apiBase, "/api/v1/wit/packages", {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function witPackageDownloadUrl(
|
||||||
|
apiBase: string,
|
||||||
|
packageMetadata: Pick<BackendWitPackage, "name" | "version">,
|
||||||
|
): string {
|
||||||
|
const [namespace, name] = packageMetadata.name.split(":");
|
||||||
|
return `${apiBase}/api/v1/wit/packages/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/${encodeURIComponent(packageMetadata.version)}/content`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changeServiceState(
|
||||||
|
apiBase: string,
|
||||||
|
service: Pick<BackendService, "id" | "revision">,
|
||||||
|
action: "start" | "stop" | "restart",
|
||||||
|
): Promise<BackendService> {
|
||||||
|
return request(
|
||||||
|
apiBase,
|
||||||
|
`/api/v1/services/${encodeURIComponent(service.id)}/${encodeURIComponent(service.revision)}/${action}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: "{}",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changeRuntimeState(
|
||||||
|
apiBase: string,
|
||||||
|
action: "start" | "stop" | "restart",
|
||||||
|
): Promise<BackendRuntime> {
|
||||||
|
return request(apiBase, `/api/v1/runtime/${action}`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invokeComponent(
|
||||||
|
apiBase: string,
|
||||||
|
service: Pick<BackendService, "id" | "revision">,
|
||||||
|
input: Uint8Array,
|
||||||
|
): Promise<InvokeResult> {
|
||||||
|
const response = await request<{
|
||||||
|
output_base64: string;
|
||||||
|
output_bytes: number;
|
||||||
|
latency_ms: number;
|
||||||
|
}>(
|
||||||
|
apiBase,
|
||||||
|
`/api/v1/services/${encodeURIComponent(service.id)}/${encodeURIComponent(service.revision)}/invoke`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ input_base64: bytesToBase64(input) }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
output: base64ToBytes(response.output_base64),
|
||||||
|
outputBytes: response.output_bytes,
|
||||||
|
latencyMs: response.latency_ms,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request<T>(apiBase: string, path: string, init?: RequestInit): Promise<T> {
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetch(`${apiBase}${path}`, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
accept: "application/json",
|
||||||
|
...init?.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
throw new Error(`无法连接 Wasmeld:${apiBase}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = (await response.json().catch(() => null)) as {
|
||||||
|
error?: { message?: string };
|
||||||
|
} | null;
|
||||||
|
throw new Error(body?.error?.message ?? `请求失败:HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
return (await response.json()) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesToBase64(bytes: Uint8Array): string {
|
||||||
|
let binary = "";
|
||||||
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
||||||
|
return window.btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64ToBytes(value: string): Uint8Array {
|
||||||
|
const binary = window.atob(value);
|
||||||
|
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/* oxlint-disable */
|
||||||
|
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
// This file was automatically generated by TanStack Router.
|
||||||
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
|
||||||
|
const IndexRoute = IndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
export interface FileRoutesByFullPath {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesByTo {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesById {
|
||||||
|
__root__: typeof rootRouteImport
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
}
|
||||||
|
export interface FileRouteTypes {
|
||||||
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
fullPaths: '/'
|
||||||
|
fileRoutesByTo: FileRoutesByTo
|
||||||
|
to: '/'
|
||||||
|
id: '__root__' | '/'
|
||||||
|
fileRoutesById: FileRoutesById
|
||||||
|
}
|
||||||
|
export interface RootRouteChildren {
|
||||||
|
IndexRoute: typeof IndexRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface FileRoutesByPath {
|
||||||
|
'/': {
|
||||||
|
id: '/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
|
IndexRoute: IndexRoute,
|
||||||
|
}
|
||||||
|
export const routeTree = rootRouteImport
|
||||||
|
._addFileChildren(rootRouteChildren)
|
||||||
|
._addFileTypes<FileRouteTypes>()
|
||||||
|
|
||||||
|
import type { getRouter } from './router.tsx'
|
||||||
|
import type { createStart } from '@tanstack/react-start'
|
||||||
|
declare module '@tanstack/react-start' {
|
||||||
|
interface Register {
|
||||||
|
ssr: true
|
||||||
|
router: Awaited<ReturnType<typeof getRouter>>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createRouter } from "@tanstack/react-router";
|
||||||
|
import { routeTree } from "./routeTree.gen";
|
||||||
|
|
||||||
|
export function getRouter() {
|
||||||
|
return createRouter({
|
||||||
|
routeTree,
|
||||||
|
defaultPreload: "intent",
|
||||||
|
scrollRestoration: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@tanstack/react-router" {
|
||||||
|
interface Register {
|
||||||
|
router: ReturnType<typeof getRouter>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
import { HeadContent, Outlet, Scripts, createRootRoute } from "@tanstack/react-router";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import appCss from "../styles/app.css?url";
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
head: () => ({
|
||||||
|
meta: [
|
||||||
|
{ charSet: "utf-8" },
|
||||||
|
{
|
||||||
|
name: "viewport",
|
||||||
|
content: "width=device-width, initial-scale=1",
|
||||||
|
},
|
||||||
|
{ title: "Wasmeld Console" },
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
content: "WebAssembly Runtime 管理控制台",
|
||||||
|
},
|
||||||
|
{ property: "og:title", content: "Wasmeld Console" },
|
||||||
|
{
|
||||||
|
property: "og:description",
|
||||||
|
content: "WebAssembly Runtime 管理控制台",
|
||||||
|
},
|
||||||
|
{ property: "og:type", content: "website" },
|
||||||
|
{ property: "og:image", content: "/og.png" },
|
||||||
|
{ name: "twitter:card", content: "summary_large_image" },
|
||||||
|
{ name: "twitter:title", content: "Wasmeld Console" },
|
||||||
|
{
|
||||||
|
name: "twitter:description",
|
||||||
|
content: "WebAssembly Runtime 管理控制台",
|
||||||
|
},
|
||||||
|
{ name: "twitter:image", content: "/og.png" },
|
||||||
|
],
|
||||||
|
links: [{ rel: "stylesheet", href: appCss }],
|
||||||
|
}),
|
||||||
|
component: RootComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RootComponent() {
|
||||||
|
return (
|
||||||
|
<RootDocument>
|
||||||
|
<Outlet />
|
||||||
|
</RootDocument>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
||||||
|
return (
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<HeadContent />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,106 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { access, readFile } from "node:fs/promises";
|
||||||
|
import { createServer } from "node:net";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import test, { after, before } from "node:test";
|
||||||
|
|
||||||
|
let serverProcess;
|
||||||
|
let serverOrigin;
|
||||||
|
|
||||||
|
async function reservePort() {
|
||||||
|
const server = createServer();
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
server.once("error", reject);
|
||||||
|
server.listen(0, "127.0.0.1", resolve);
|
||||||
|
});
|
||||||
|
const address = server.address();
|
||||||
|
assert(address && typeof address === "object");
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
server.close((error) => (error ? reject(error) : resolve()));
|
||||||
|
});
|
||||||
|
return address.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
const port = await reservePort();
|
||||||
|
serverOrigin = `http://127.0.0.1:${port}`;
|
||||||
|
serverProcess = spawn(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
fileURLToPath(new URL("../node_modules/srvx/bin/srvx.mjs", import.meta.url)),
|
||||||
|
"serve",
|
||||||
|
"--prod",
|
||||||
|
"--entry",
|
||||||
|
"dist/server/server.js",
|
||||||
|
"--static",
|
||||||
|
"../client",
|
||||||
|
],
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
HOST: "127.0.0.1",
|
||||||
|
PORT: String(port),
|
||||||
|
},
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 50; attempt += 1) {
|
||||||
|
if (serverProcess.exitCode !== null) {
|
||||||
|
throw new Error(`Node server exited with code ${serverProcess.exitCode}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(serverOrigin);
|
||||||
|
if (response.ok) return;
|
||||||
|
} catch {
|
||||||
|
// The server is still starting.
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
throw new Error("Node server did not become ready");
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
serverProcess?.kill("SIGTERM");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("server-renders the Wasm management console", async () => {
|
||||||
|
const response = await fetch(serverOrigin, {
|
||||||
|
headers: { accept: "text/html" },
|
||||||
|
});
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
assert.match(response.headers.get("content-type") ?? "", /^text\/html\b/i);
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
assert.match(html, /<title>Wasmeld Console<\/title>/i);
|
||||||
|
assert.match(html, /运行概览/);
|
||||||
|
assert.match(html, /WIT 包/);
|
||||||
|
assert.match(html, /连接中/);
|
||||||
|
assert.match(html, /来自 Wasmeld/);
|
||||||
|
assert.match(html, /Component Model/);
|
||||||
|
assert.doesNotMatch(html, /本地演示|codex-preview|vinext|next\.js/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses TanStack Start routing and produces Node artifacts", async () => {
|
||||||
|
const [rootRoute, indexRoute, router, packageJson, viteConfig] = await Promise.all([
|
||||||
|
readFile(new URL("../src/routes/__root.tsx", import.meta.url), "utf8"),
|
||||||
|
readFile(new URL("../src/routes/index.tsx", import.meta.url), "utf8"),
|
||||||
|
readFile(new URL("../src/router.tsx", import.meta.url), "utf8"),
|
||||||
|
readFile(new URL("../package.json", import.meta.url), "utf8"),
|
||||||
|
readFile(new URL("../vite.config.ts", import.meta.url), "utf8"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.match(rootRoute, /createRootRoute/);
|
||||||
|
assert.match(rootRoute, /<HeadContent \/>/);
|
||||||
|
assert.match(rootRoute, /<Scripts \/>/);
|
||||||
|
assert.match(indexRoute, /createFileRoute\("\/"\)/);
|
||||||
|
assert.match(router, /createRouter/);
|
||||||
|
assert.match(packageJson, /"@tanstack\/react-start"/);
|
||||||
|
assert.match(packageJson, /"srvx"/);
|
||||||
|
assert.doesNotMatch(packageJson, /"next"|"vinext"|"drizzle-orm"/);
|
||||||
|
assert.match(viteConfig, /tanstackStart\(\{/);
|
||||||
|
|
||||||
|
await access(new URL("../dist/server/server.js", import.meta.url));
|
||||||
|
await access(new URL("../dist/client/og.png", import.meta.url));
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
||||||
|
import viteReact from "@vitejs/plugin-react";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: { port: 3000 },
|
||||||
|
plugins: [
|
||||||
|
tanstackStart({
|
||||||
|
router: {
|
||||||
|
routeTreeFileHeader: [
|
||||||
|
"/* oxlint-disable */",
|
||||||
|
"// @ts-nocheck",
|
||||||
|
"// noinspection JSUnusedGlobalSymbols",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
viteReact(),
|
||||||
|
],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user