Add wiki-explorer-server example (#122)

Interactive Wikipedia link graph visualization using force-graph with:
- Force-directed graph layout for exploring page connections
- Node expansion to reveal first-degree Wikipedia links
- Visual state tracking (blue=default, green=expanded, red=error)
- Direct browser access to Wikipedia pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jonathan Hefner 2025-12-11 06:58:05 -06:00 • committed by GitHub
commit 366c5d5a6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 3772 additions and 2820 deletions

View file

@ -0,0 +1,66 @@
# Example: Wiki Explorer
Visualizes Wikipedia link graphs using a force-directed layout. Explore how Wikipedia pages are connected by expanding nodes to reveal first-degree links.
<table>
<tr>
<td><a href="https://modelcontextprotocol.github.io/ext-apps/screenshots/wiki-explorer-server/01-zoomed.png"><img src="https://modelcontextprotocol.github.io/ext-apps/screenshots/wiki-explorer-server/01-zoomed.png" alt="Zoomed" width="100%"></a></td>
<td><a href="https://modelcontextprotocol.github.io/ext-apps/screenshots/wiki-explorer-server/02-pop-up.png"><img src="https://modelcontextprotocol.github.io/ext-apps/screenshots/wiki-explorer-server/02-pop-up.png" alt="Pop-up" width="100%"></a></td>
<td><a href="https://modelcontextprotocol.github.io/ext-apps/screenshots/wiki-explorer-server/03-expanded-graph.png"><img src="https://modelcontextprotocol.github.io/ext-apps/screenshots/wiki-explorer-server/03-expanded-graph.png" alt="Expanded graph" width="100%"></a></td>
</tr>
</table>
## Features
- **Force-directed graph visualization**: Interactive graph powered by [`force-graph`](https://github.com/vasturiano/force-graph)
- **Node expansion**: Click any node to expand and see all pages it links to
- **Visual state tracking**: Nodes change color based on state (blue = default, green = expanded, red = error)
- **Direct page access**: Open any Wikipedia page in your browser
## Running
1. Install dependencies:
```bash
npm install
```
2. Build and start the server:
```bash
npm run start:http # for Streamable HTTP transport
# OR
npm run start:stdio # for stdio transport
```
3. View using the [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example or another MCP Apps-compatible host.
### Tool Input
To test the example, call the `get-first-degree-links` tool with a Wikipedia URL:
```json
{
"url": "https://en.wikipedia.org/wiki/Graph_theory"
}
```
Click nodes in the graph to **Open** (view in browser) or **Expand** (visualize linked pages).
## Architecture
### Server (`server.ts`)
MCP server that fetches Wikipedia pages and extracts internal links.
Exposes one tool:
- `get-first-degree-links` - Returns links to other Wikipedia pages from a given page
### App (`src/mcp-app.ts`)
Vanilla TypeScript app using force-graph for visualization that:
- Receives tool inputs via the MCP App SDK
- Renders an interactive force-directed graph
- Supports node expansion to explore link relationships

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wiki Explorer</title>
</head>
<body>
<div id="graph"></div>
<div id="popup">
<div class="popup-title"></div>
<div class="popup-error"></div>
<div class="popup-buttons">
<button id="open-btn">Open</button>
<button id="expand-btn">Expand</button>
</div>
</div>
<div id="controls">
<button id="reset-graph" title="Reset graph">&#x21BA;</button>
<div id="zoom-controls">
<button id="zoom-in" title="Zoom in">+</button>
<button id="zoom-out" title="Zoom out">−</button>
</div>
</div>
<script type="module" src="/src/mcp-app.ts"></script>
</body>
</html>

View file

@ -0,0 +1,35 @@
{
"name": "wiki-explorer-server",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "INPUT=mcp-app.html vite build",
"watch": "INPUT=mcp-app.html vite build --watch",
"serve:http": "bun server.ts",
"serve:stdio": "bun server.ts --stdio",
"start": "npm run start:http",
"start:http": "NODE_ENV=development npm run build && npm run serve:http",
"start:stdio": "NODE_ENV=development npm run build && npm run serve:stdio",
"dev": "NODE_ENV=development concurrently 'npm run watch' 'npm run serve:http'"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "../..",
"@modelcontextprotocol/sdk": "^1.22.0",
"cheerio": "^1.0.0",
"zod": "^3.25.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"bun": "^1.3.2",
"concurrently": "^9.2.1",
"cors": "^2.8.5",
"express": "^5.1.0",
"force-graph": "^1.49.0",
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0"
}
}

View file

@ -0,0 +1,199 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import type {
CallToolResult,
ReadResourceResult,
} from "@modelcontextprotocol/sdk/types.js";
import * as cheerio from "cheerio";
import cors from "cors";
import express, { type Request, type Response } from "express";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
const DIST_DIR = path.join(import.meta.dirname, "dist");
type PageInfo = { url: string; title: string };
// Helper to derive title from Wikipedia URL
function extractTitleFromUrl(url: string): string {
try {
const urlObj = new URL(url);
const path = urlObj.pathname;
const title = path.replace("/wiki/", "");
return decodeURIComponent(title).replace(/_/g, " ");
} catch {
return url; // Fallback to URL if parsing fails
}
}
// Wikipedia namespace prefixes to exclude from link extraction
const EXCLUDED_PREFIXES = [
"Wikipedia:",
"Help:",
"File:",
"Special:",
"Talk:",
"Template:",
"Category:",
"Portal:",
"Draft:",
"Module:",
"MediaWiki:",
"User:",
"Main_Page",
];
// Extract wiki links from HTML, filtering out special pages and self-links
function extractWikiLinks(pageUrl: URL, html: string): PageInfo[] {
const $ = cheerio.load(html);
return [
...new Set(
$('a[href^="/wiki/"]')
.map((_, el) => $(el).attr("href"))
.get()
.filter(
(href): href is string =>
href !== undefined &&
href !== pageUrl.pathname &&
!href.includes("#") &&
!EXCLUDED_PREFIXES.some((prefix) => href.includes(prefix)),
),
),
].map((href) => ({
url: `${pageUrl.origin}${href}`,
title: extractTitleFromUrl(`${pageUrl.origin}${href}`),
}));
}
const server = new McpServer({
name: "Wiki Explorer",
version: "1.0.0",
});
// Register the get-first-degree-links tool and its associated UI resource
{
const resourceUri = "ui://wiki-explorer/mcp-app.html";
server.registerTool(
"get-first-degree-links",
{
title: "Get First-Degree Links",
description:
"Returns all Wikipedia pages that the given page links to directly.",
inputSchema: z.object({
url: z.string().url().describe("Wikipedia page URL"),
}),
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
},
async ({ url }): Promise<CallToolResult> => {
let title = url;
try {
if (!url.match(/^https?:\/\/[a-z]+\.wikipedia\.org\/wiki\//)) {
throw new Error("Not a valid Wikipedia URL");
}
title = extractTitleFromUrl(url);
const response = await fetch(url);
if (!response.ok) {
throw new Error(
response.status === 404
? "Page not found"
: `Fetch failed: ${response.status}`,
);
}
const html = await response.text();
const links = extractWikiLinks(new URL(url), html);
const result = { page: { url, title }, links, error: null };
return { content: [{ type: "text", text: JSON.stringify(result) }] };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
const result = { page: { url, title }, links: [], error };
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
},
);
server.registerResource(
resourceUri,
resourceUri,
{},
async (): Promise<ReadResourceResult> => {
const html = await fs.readFile(
path.join(DIST_DIR, "mcp-app.html"),
"utf-8",
);
return {
contents: [
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
],
};
},
);
}
async function main() {
if (process.argv.includes("--stdio")) {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Wiki Explorer server running in stdio mode");
} else {
const app = express();
app.use(cors());
app.use(express.json());
app.post("/mcp", async (req: Request, res: Response) => {
try {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
});
res.on("close", () => {
transport.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("Error handling MCP request:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
});
const httpServer = app.listen(PORT, () => {
console.log(
`Wiki Explorer server listening on http://localhost:${PORT}/mcp`,
);
});
function shutdown() {
console.log("\nShutting down...");
httpServer.close(() => {
console.log("Server closed");
process.exit(0);
});
}
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}
}
main().catch(console.error);

View file

@ -0,0 +1,28 @@
declare module "d3-force-3d" {
interface Force {
(alpha: number): void;
initialize?: (nodes: unknown[], ...args: unknown[]) => void;
}
interface ManyBodyForce extends Force {
strength(value: number): ManyBodyForce;
}
interface LinkForce extends Force {
distance(value: number): LinkForce;
}
interface CollideForce extends Force {
radius(value: number): CollideForce;
}
interface CenterForce extends Force {
x(value: number): CenterForce;
y(value: number): CenterForce;
}
export function forceManyBody(): ManyBodyForce;
export function forceLink(): LinkForce;
export function forceCollide(radius?: number): CollideForce;
export function forceCenter(x?: number, y?: number): CenterForce;
}

View file

@ -0,0 +1,36 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
font-family: system-ui, -apple-system, sans-serif;
font-size: 1rem;
width: 100%;
height: 100%;
overflow: hidden;
}
:root {
--bg-color: #ffffff;
--text-color: #333333;
--border-color: #dddddd;
--node-default: #4a90d9;
--node-expanded: #2ecc71;
--node-error: #e74c3c;
--link-color: rgba(128, 128, 128, 0.5);
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1e1e1e;
--text-color: #e0e0e0;
--border-color: #444444;
}
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}

View file

@ -0,0 +1,147 @@
#graph {
width: 100%;
height: 100vh;
}
#popup {
display: none;
position: fixed;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 100;
min-width: 200px;
max-width: 400px;
}
.popup-title {
font-weight: 600;
margin-bottom: 12px;
word-wrap: break-word;
color: var(--text-color);
}
.popup-error {
display: none;
color: var(--node-error);
font-size: 14px;
padding: 8px 0;
border-top: 1px solid var(--border-color);
margin-bottom: 8px;
}
.popup-buttons {
display: flex;
gap: 8px;
}
.popup-buttons button {
flex: 1;
padding: 8px 16px;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.popup-buttons button:hover {
opacity: 0.9;
}
#open-btn {
background-color: var(--node-default);
color: white;
}
#expand-btn {
background-color: var(--node-expanded);
color: white;
}
/* Disabled state for buttons */
.popup-buttons button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Controls container */
#controls {
position: fixed;
bottom: 24px;
right: 24px;
display: flex;
flex-direction: row;
align-items: flex-end;
gap: 8px;
z-index: 100;
}
/* Reset button */
#reset-graph {
width: 40px;
height: 40px;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-color);
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
transition: background-color 0.15s;
}
#reset-graph:hover {
background: var(--border-color);
}
#reset-graph:active {
background: var(--border-color);
opacity: 0.8;
}
/* Zoom controls */
#zoom-controls {
display: flex;
flex-direction: column;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
#zoom-controls button {
width: 40px;
height: 40px;
background: var(--bg-color);
border: 1px solid var(--border-color);
color: var(--text-color);
font-size: 20px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s;
}
#zoom-controls button:hover {
background: var(--border-color);
}
#zoom-controls button:active {
background: var(--border-color);
opacity: 0.8;
}
#zoom-in {
border-radius: 4px 4px 0 0;
border-bottom: none;
}
#zoom-out {
border-radius: 0 0 4px 4px;
}

View file

@ -0,0 +1,370 @@
/**
* Wiki Explorer - Force-directed graph visualization of Wikipedia link networks
*/
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import {
forceCenter,
forceCollide,
forceLink,
forceManyBody,
} from "d3-force-3d";
import ForceGraph, { type LinkObject, type NodeObject } from "force-graph";
import "./global.css";
import "./mcp-app.css";
// Helper to resolve CSS variables for canvas rendering
function getCSSColor(varName: string): string {
return (
getComputedStyle(document.documentElement)
.getPropertyValue(varName)
.trim() || "#000"
);
}
// Types
type NodeState = "default" | "expanded" | "error";
interface NodeData extends NodeObject {
url: string;
title: string;
state: NodeState;
errorMessage?: string;
}
interface LinkData extends LinkObject {
source: string | NodeData;
target: string | NodeData;
}
interface GraphData {
nodes: NodeData[];
links: LinkData[];
}
type PageInfo = { url: string; title: string };
type ToolResponse = {
page: PageInfo;
links: PageInfo[];
error: string | null;
};
// Graph state
const graphData: GraphData = { nodes: [], links: [] };
let selectedNodeUrl: string | null = null;
let initialUrl: string | null = null;
// DOM elements
const container = document.getElementById("graph")!;
const popup = document.getElementById("popup")!;
const popupTitle = popup.querySelector(".popup-title")!;
const popupError = popup.querySelector(".popup-error")! as HTMLElement;
const openBtn = document.getElementById("open-btn")!;
const expandBtn = document.getElementById("expand-btn")!;
const zoomInBtn = document.getElementById("zoom-in")!;
const zoomOutBtn = document.getElementById("zoom-out")!;
const resetBtn = document.getElementById("reset-graph")!;
// Initialize force-graph
const graph = new ForceGraph<NodeData, LinkData>(container)
.nodeId("url")
.nodeLabel("title")
.nodeColor((node: NodeData) => {
switch (node.state) {
case "expanded":
return getCSSColor("--node-expanded");
case "error":
return getCSSColor("--node-error");
default:
return getCSSColor("--node-default");
}
})
.nodeVal(8)
.linkDirectionalArrowLength(6)
.linkDirectionalArrowRelPos(1)
.linkColor(() => getCSSColor("--link-color"))
.onNodeClick(handleNodeClick)
.onBackgroundClick(() => hidePopup())
// Configure forces for better node spreading
.d3Force("charge", forceManyBody().strength(-80))
.d3Force("link", forceLink().distance(60))
.d3Force("collide", forceCollide(12))
.d3Force("center", forceCenter())
.d3VelocityDecay(0.3)
.cooldownTime(Infinity)
.d3AlphaMin(0)
.d3Force("ambient", () => {
for (const node of graphData.nodes) {
if (node.vx !== undefined && node.vy !== undefined) {
node.vx += (Math.random() - 0.5) * 0.1;
node.vy += (Math.random() - 0.5) * 0.1;
}
}
})
.graphData(graphData);
// Handle window resize
function handleResize() {
const { width, height } = container.getBoundingClientRect();
graph.width(width).height(height);
}
window.addEventListener("resize", handleResize);
handleResize();
// Node management functions
function addNode(
url: string,
title: string,
state: NodeState = "default",
initialPos?: { x: number; y: number },
): boolean {
const existing = graphData.nodes.find((n) => n.url === url);
if (existing) {
return false;
}
const node: NodeData = { url, title, state };
if (initialPos) {
// Small random jitter so nodes don't stack exactly
node.x = initialPos.x + (Math.random() - 0.5) * 20;
node.y = initialPos.y + (Math.random() - 0.5) * 20;
}
graphData.nodes.push(node);
return true;
}
function updateNodeTitle(url: string, title: string): void {
const node = graphData.nodes.find((n) => n.url === url);
if (node) {
node.title = title;
}
}
function setNodeState(
url: string,
state: NodeState,
errorMessage?: string,
): void {
const node = graphData.nodes.find((n) => n.url === url);
if (node) {
node.state = state;
node.errorMessage = errorMessage;
}
}
function addEdge(sourceUrl: string, targetUrl: string): boolean {
const existing = graphData.links.find((l) => {
const src =
typeof l.source === "string" ? l.source : (l.source as NodeData).url;
const tgt =
typeof l.target === "string" ? l.target : (l.target as NodeData).url;
return src === sourceUrl && tgt === targetUrl;
});
if (existing) {
return false;
}
graphData.links.push({ source: sourceUrl, target: targetUrl });
return true;
}
function updateGraph(): void {
graph.graphData({ nodes: [...graphData.nodes], links: [...graphData.links] });
}
// Popup management
function showPopup(node: NodeData, x: number, y: number): void {
popupTitle.textContent = node.title;
if (node.state === "error") {
popupError.textContent = node.errorMessage || "Failed to load page";
popupError.style.display = "block";
expandBtn.style.display = "none";
} else {
popupError.style.display = "none";
expandBtn.style.display = "inline-block";
if (node.state === "expanded") {
expandBtn.setAttribute("disabled", "true");
expandBtn.textContent = "Expanded";
} else {
expandBtn.removeAttribute("disabled");
expandBtn.textContent = "Expand";
}
}
popup.style.display = "block";
const rect = popup.getBoundingClientRect();
const gap = 15;
// Place popup on opposite side of cursor from screen center
const left =
x < window.innerWidth / 2
? x + gap // cursor on left half → popup to right
: x - rect.width - gap; // cursor on right half → popup to left
const top =
y < window.innerHeight / 2
? y + gap // cursor on top half → popup below
: y - rect.height - gap; // cursor on bottom half → popup above
popup.style.left = `${left}px`;
popup.style.top = `${top}px`;
}
function hidePopup(): void {
popup.style.display = "none";
selectedNodeUrl = null;
}
// Event handlers
function handleNodeClick(node: NodeData, event: MouseEvent): void {
// Toggle popup if clicking same node
if (selectedNodeUrl === node.url) {
hidePopup();
return;
}
selectedNodeUrl = node.url;
showPopup(node, event.clientX, event.clientY);
}
// Close popup on Escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && popup.style.display === "block") {
hidePopup();
}
});
// Zoom controls
const ZOOM_FACTOR = 1.5;
zoomInBtn.addEventListener("click", () => {
const currentZoom = graph.zoom();
graph.zoom(currentZoom * ZOOM_FACTOR, 200);
});
zoomOutBtn.addEventListener("click", () => {
const currentZoom = graph.zoom();
graph.zoom(currentZoom / ZOOM_FACTOR, 200);
});
// Initialize App SDK
const app = new App({ name: "Wiki Explorer", version: "1.0.0" });
// Reset button - clears graph and reloads from initial URL
resetBtn.addEventListener("click", async () => {
if (!initialUrl) return;
// Fetch fresh data first, then clear and repopulate
const result = await app.callServerTool({
name: "get-first-degree-links",
arguments: { url: initialUrl },
});
// Clear current graph and repopulate with result
graphData.nodes = [];
graphData.links = [];
addNode(initialUrl, initialUrl, "default", { x: 0, y: 0 });
graph.warmupTicks(100);
handleToolResultData(result);
graph.centerAt(0, 0, 500);
});
// Open button - opens the Wikipedia page in browser
openBtn.addEventListener("click", async () => {
if (selectedNodeUrl) {
await app.sendOpenLink({ url: selectedNodeUrl });
hidePopup();
}
});
// Expand button - fetches and displays linked pages
expandBtn.addEventListener("click", async () => {
if (!selectedNodeUrl) return;
const sourceUrl = selectedNodeUrl;
expandBtn.setAttribute("disabled", "true");
expandBtn.textContent = "Loading...";
try {
const result = await app.callServerTool({
name: "get-first-degree-links",
arguments: { url: sourceUrl },
});
graph.warmupTicks(0);
handleToolResultData(result);
} catch (e) {
console.error("Expand error:", e);
setNodeState(sourceUrl, "error", "Request failed");
updateGraph();
} finally {
expandBtn.removeAttribute("disabled");
expandBtn.textContent = "Expand";
hidePopup();
}
});
// Handle tool input - create initial node with URL as placeholder title
app.ontoolinput = (params) => {
const args = params.arguments as { url?: string } | undefined;
if (args?.url) {
initialUrl = args.url; // Store for reset functionality
addNode(args.url, args.url, "default", { x: 0, y: 0 });
graph.warmupTicks(100);
updateGraph();
// Center on the new node
graph.centerAt(0, 0, 500);
}
};
// Handle tool result - update node and add linked pages (host-initiated, initial load)
app.ontoolresult = (result) => {
graph.warmupTicks(100);
handleToolResultData(result);
};
function handleToolResultData(result: CallToolResult): void {
if (
result.isError ||
!result.content?.[0] ||
result.content[0].type !== "text"
) {
console.error("Tool result error:", result);
return;
}
try {
const response: ToolResponse = JSON.parse(result.content[0].text);
const { page, links, error } = response;
// Ensure the source node exists
addNode(page.url, page.title);
updateNodeTitle(page.url, page.title);
if (error) {
setNodeState(page.url, "error", error);
} else {
// Get source node position so new nodes appear nearby
const sourceNode = graphData.nodes.find((n) => n.url === page.url);
const sourcePos = sourceNode
? { x: sourceNode.x ?? 0, y: sourceNode.y ?? 0 }
: undefined;
// Add all linked nodes and edges
for (const link of links) {
addNode(link.url, link.title, "default", sourcePos);
addEdge(page.url, link.url);
}
setNodeState(page.url, "expanded");
}
updateGraph();
} catch (e) {
console.error("Failed to parse tool result:", e);
}
}
app.onerror = (err) => {
console.error("[Wiki Explorer] App error:", err);
};
// Connect to host
app.connect(new PostMessageTransport(window.parent));

View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "server.ts"]
}

View file

@ -0,0 +1,24 @@
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
const INPUT = process.env.INPUT;
if (!INPUT) {
throw new Error("INPUT environment variable is not set");
}
const isDevelopment = process.env.NODE_ENV === "development";
export default defineConfig({
plugins: [viteSingleFile()],
build: {
sourcemap: isDevelopment ? "inline" : undefined,
cssMinify: !isDevelopment,
minify: !isDevelopment,
rollupOptions: {
input: INPUT,
},
outDir: "dist",
emptyOutDir: false,
},
});

2913
package-lock.json generated

File diff suppressed because it is too large Load diff