From 303532c0ae67e2bcfe55bbe8075690d528061a0f Mon Sep 17 00:00:00 2001 From: Ditadi Date: Wed, 10 Dec 2025 23:11:24 +0000 Subject: [PATCH 1/9] refactor(server): extract server plugin into focused modules --- apps/dev-playground/package.json | 2 +- packages/app-kit/src/server/index.ts | 185 +++++---- .../{dev-mode.ts => remote-tunnel-manager.ts} | 97 ++--- packages/app-kit/src/server/static-server.ts | 70 ++++ .../server/tests/server.integration.test.ts | 219 +++++++++++ .../app-kit/src/server/tests/server.test.ts | 365 ++++++++++++++++++ .../src/server/tests/static-server.test.ts | 182 +++++++++ .../src/server/tests/vite-dev-server.test.ts | 273 +++++++++++++ packages/app-kit/src/server/types.ts | 1 - .../app-kit/src/server/vite-dev-server.ts | 132 +++++++ 10 files changed, 1371 insertions(+), 155 deletions(-) rename packages/app-kit/src/server/{dev-mode.ts => remote-tunnel-manager.ts} (88%) create mode 100644 packages/app-kit/src/server/static-server.ts create mode 100644 packages/app-kit/src/server/tests/server.integration.test.ts create mode 100644 packages/app-kit/src/server/tests/server.test.ts create mode 100644 packages/app-kit/src/server/tests/static-server.test.ts create mode 100644 packages/app-kit/src/server/tests/vite-dev-server.test.ts create mode 100644 packages/app-kit/src/server/vite-dev-server.ts diff --git a/apps/dev-playground/package.json b/apps/dev-playground/package.json index 96a4a82..21022ed 100644 --- a/apps/dev-playground/package.json +++ b/apps/dev-playground/package.json @@ -5,7 +5,7 @@ "scripts": { "start": "node build/index.mjs", "start:local": "NODE_ENV=production node --env-file=./server/.env build/index.mjs", - "dev": "tsx watch ./server --exclude '**/.vite-temp/**' --tsconfig ./tsconfig.json", + "dev": "NODE_ENV=development tsx watch server/index.ts", "dev:inspect": "tsx --inspect --tsconfig ./tsconfig.json ./server", "build": "npm run build:app", "build:app": "tsdown --out-dir build server/index.ts && cd client && npm run build", diff --git a/packages/app-kit/src/server/index.ts b/packages/app-kit/src/server/index.ts index 679e548..10d6a12 100644 --- a/packages/app-kit/src/server/index.ts +++ b/packages/app-kit/src/server/index.ts @@ -7,26 +7,41 @@ import type { PluginPhase } from "shared"; import { Plugin, toPlugin } from "../plugin"; import { instrumentations } from "../telemetry"; import { databricksClientMiddleware, isRemoteServerEnabled } from "../utils"; -import { DevModeManager } from "./dev-mode"; +import { RemoteTunnelManager } from "./remote-tunnel-manager"; +import { StaticServer } from "./static-server"; import type { ServerConfig } from "./types"; -import { getQueries, getRoutes } from "./utils"; +import { getRoutes } from "./utils"; +import { ViteDevServer } from "./vite-dev-server"; dotenv.config({ path: path.resolve(process.cwd(), "./server/.env") }); +/** + * Server plugin for the App Kit. + * + * This plugin is responsible for starting the server and serving the static files. + * It also handles the remote tunneling for development purposes. + * + * @example + * ```ts + * createApp({ + * plugins: [server(), telemetryExamples(), analytics({})], + * }); + * ``` + * + */ export class ServerPlugin extends Plugin { public static DEFAULT_CONFIG = { autoStart: true, - staticPath: path.resolve(process.cwd(), "client", "dist"), host: process.env.FLASK_RUN_HOST || "0.0.0.0", port: Number(process.env.DATABRICKS_APP_PORT) || 8000, - watch: process.env.NODE_ENV === "development", }; public name = "server" as const; public envVars = ["DATABRICKS_APP_PORT", "FLASK_RUN_HOST"]; private serverApplication: express.Application; private server: HTTPServer | null; - private devModeManager?: DevModeManager; + private viteDevServer?: ViteDevServer; + private remoteTunnelManager?: RemoteTunnelManager; protected declare config: ServerConfig; private serverExtensions: ((app: express.Application) => void)[] = []; static phase: PluginPhase = "deferred"; @@ -43,26 +58,38 @@ export class ServerPlugin extends Plugin { ]); } + /** Setup the server plugin. */ async setup() { if (this.shouldAutoStart()) { await this.start(); } } + /** Get the server configuration. */ getConfig() { const { plugins: _plugins, ...config } = this.config; return config; } + /** Check if the server should auto start. */ shouldAutoStart() { return this.config.autoStart; } + /** Check if the remote serving is enabled. */ isRemoteServingEnabled() { return isRemoteServerEnabled(); } + /** + * Start the server. + * + * This method starts the server and sets up the frontend. + * It also sets up the remote tunneling if enabled. + * + * @returns The express application. + */ async start(): Promise { this.serverApplication.use(express.json()); this.serverApplication.use(await databricksClientMiddleware()); @@ -73,55 +100,27 @@ export class ServerPlugin extends Plugin { extension(this.serverApplication); } - const isRemoteDevModeEnabled = this.isRemoteServingEnabled(); - if (isRemoteDevModeEnabled) { - this.devModeManager = new DevModeManager(this.devFileReader); - - if (this.config.watch) { - await this.devModeManager.setupViteWatching(this.serverApplication); - } - - this.serverApplication.use(this.devModeManager.devModeMiddleware()); - this.serverApplication.use( - DevModeManager.ASSETS_MIDDLEWARE_PATHS, - this.devModeManager.assetMiddleware(), - ); - } - - if (this.config.staticPath && !this.config.watch) { - this._setupStaticServing(); - } + await this.setupFrontend(); const server = this.serverApplication.listen( this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port, this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host, - () => { - console.log(`Server is running on port ${this.config.port}`); - if (this.config.staticPath && !this.config.watch) { - console.log(`Serving static files from: ${this.config.staticPath}`); - } - if (this.config.watch) { - console.log("Vite is watching for changes..."); - } - }, + () => this.logStartupInfo(), ); this.server = server; - if (isRemoteDevModeEnabled && this.devModeManager) { - this.devModeManager.setServer(server); - this.devModeManager.setupWebSocket(); + if (this.isRemoteServingEnabled()) { + this.setupRemoteTunnels(); } process.on("SIGTERM", () => this._gracefulShutdown()); process.on("SIGINT", () => this._gracefulShutdown()); if (process.env.NODE_ENV === "development") { - // TODO: improve this const allRoutes = getRoutes(this.serverApplication._router.stack); console.dir(allRoutes, { depth: null }); } - return this.serverApplication; } @@ -147,15 +146,11 @@ export class ServerPlugin extends Plugin { return this.server; } - extend(fn: (app: express.Application) => void) { - if (this.shouldAutoStart()) { - throw new Error("Cannot extend server when autoStart is true."); - } - - this.serverExtensions.push(fn); - return this; - } - + /** + * Setup the routes with the plugins. + * + * This method goes through all the plugins and injects the routes into the server application. + */ private extendRoutes() { if (!this.config.plugins) return; @@ -176,57 +171,79 @@ export class ServerPlugin extends Plugin { } } - private _setupStaticServing() { - if (!this.config.staticPath) return; - - this.serverApplication.use( - express.static(this.config.staticPath, { - index: false, - }), - ); - - this.serverApplication.get("*", (req, res) => { - if (!req.path.startsWith("/api") && !req.path.startsWith("/query")) { - this._renderFE(res); + /** + * Setup the frontend. + * + * This method sets up the frontend using Vite for development and static files for production. + */ + private async setupFrontend() { + const isDev = process.env.NODE_ENV === "development"; + if (isDev) { + this.viteDevServer = new ViteDevServer(this.serverApplication); + await this.viteDevServer.setup(); + } else { + const staticPath = this.config.staticPath ?? this.findStaticPath(); + if (staticPath) { + const staticServer = new StaticServer( + this.serverApplication, + staticPath, + ); + staticServer.setup(); } - }); - } - - private _renderFE(res: express.Response) { - if (!this.config.staticPath) { - return res.status(500).json({ error: "No static path configured" }); } + } - const indexPath = path.join(this.config.staticPath, "index.html"); - const configObject = this._configInjection(); - const configScript = ` - - `; - let html = fs.readFileSync(indexPath, "utf-8"); + /** + * Setup the remote tunnels. + * + * This method sets up the remote tunnels for the development server. + */ + private setupRemoteTunnels() { + if (!this.server) return; - html = html.replace("", `${configScript}`); + this.remoteTunnelManager = new RemoteTunnelManager(this.devFileReader); + this.remoteTunnelManager.setServer(this.server); + this.remoteTunnelManager.setup(this.serverApplication); + this.remoteTunnelManager.setupWebSocket(); + } - res.send(html); + private findStaticPath() { + const staticPaths = ["dist", "client/dist", "build", "public", "out"]; + const cwd = process.cwd(); + for (const p of staticPaths) { + const fullPath = path.resolve(cwd, p); + if (fs.existsSync(path.resolve(fullPath, "index.html"))) { + console.log(`Static files: serving from ${fullPath}`); + return fullPath; + } + } + return undefined; } - private _configInjection() { - const configFolder = path.join(process.cwd(), "config"); + private logStartupInfo() { + const isDev = process.env.NODE_ENV === "development"; + const port = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port; + const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host; - const configObject = { - appName: process.env.DATABRICKS_APP_NAME || "", - queries: getQueries(configFolder), - }; + console.log(`Server running on http://${host}:${port}`); + console.log( + `Mode: ${isDev ? "development (Vite HMR)" : "production (static)"}`, + ); - return configObject; + if (this.isRemoteServingEnabled()) { + console.log("Remote tunnel support: enabled"); + } } - private _gracefulShutdown() { + private async _gracefulShutdown() { console.log("Starting graceful shutdown..."); - if (this.devModeManager) { - this.devModeManager.cleanup(); + if (this.viteDevServer) { + await this.viteDevServer.close(); + } + + if (this.remoteTunnelManager) { + this.remoteTunnelManager.cleanup(); } // 1. abort active operations from plugins diff --git a/packages/app-kit/src/server/dev-mode.ts b/packages/app-kit/src/server/remote-tunnel-manager.ts similarity index 88% rename from packages/app-kit/src/server/dev-mode.ts rename to packages/app-kit/src/server/remote-tunnel-manager.ts index 490a417..1c3cf14 100644 --- a/packages/app-kit/src/server/dev-mode.ts +++ b/packages/app-kit/src/server/remote-tunnel-manager.ts @@ -1,13 +1,12 @@ -import type { TunnelConnection } from "shared"; -import type express from "express"; -import { generateTunnelIdFromEmail, getQueries, parseCookies } from "./utils"; -import path from "node:path"; import { randomUUID } from "node:crypto"; import fs from "node:fs"; import type { Server as HTTPServer } from "node:http"; +import path from "node:path"; import { fileURLToPath } from "node:url"; +import type express from "express"; +import type { TunnelConnection } from "shared"; import { WebSocketServer } from "ws"; -import { mergeConfigDedup } from "../utils"; +import { generateTunnelIdFromEmail, getQueries, parseCookies } from "./utils"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -19,7 +18,19 @@ interface DevFileReader { ): void; } -export class DevModeManager { +/** + * Remote tunnel manager for the App Kit. + * + * This class is responsible for managing the remote tunnels for the development server. + * It also handles the asset fetching and the HMR for the development server. + * + * @example + * ```ts + * const remoteTunnelManager = new RemoteTunnelManager(devFileReader); + * remoteTunnelManager.setup(app); + * ``` + */ +export class RemoteTunnelManager { private tunnels = new Map(); private wss: WebSocketServer; private hmrWss: WebSocketServer; @@ -47,6 +58,7 @@ export class DevModeManager { this.server = server; } + /** Asset middleware for the development server. */ assetMiddleware() { return async (req: express.Request, res: express.Response) => { const email = req.headers["x-forwarded-email"] as string; @@ -107,6 +119,7 @@ export class DevModeManager { }; } + /** Dev mode middleware for the development server. */ devModeMiddleware() { return async ( req: express.Request, @@ -172,6 +185,15 @@ export class DevModeManager { }; } + /** Setup the dev mode middleware. */ + setup(app: express.Application) { + app.use(this.devModeMiddleware()); + app.use( + RemoteTunnelManager.ASSETS_MIDDLEWARE_PATHS, + this.assetMiddleware(), + ); + } + private loadHtmlTemplate( filename: string, replacements: Record, @@ -445,69 +467,6 @@ export class DevModeManager { ); } - async setupViteWatching(serverApplication: express.Application) { - const { - createServer: createViteServer, - loadConfigFromFile, - mergeConfig, - } = require("vite"); - const { default: react } = require("@vitejs/plugin-react"); - - const clientRoot = path.resolve(process.cwd(), "client"); - - const loadedConfig = await loadConfigFromFile( - { - mode: "development", - command: "serve", - }, - undefined, - clientRoot, - ); - const userConfig = loadedConfig?.config ?? {}; - const coreConfig = { - configFile: false, - root: clientRoot, - server: { - middlewareMode: true, - watch: { - useFsEvents: true, - ignored: ["**/node_modules/**", "!**/node_modules/@databricks/**"], - }, - }, - plugins: [react()], - }; - const mergedConfigs = mergeConfigDedup(userConfig, coreConfig, mergeConfig); - const vite = await createViteServer(mergedConfigs); - - serverApplication.use(vite.middlewares); - - serverApplication.use( - "*", - async ( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ) => { - try { - if (!req.path.startsWith("/api")) { - const url = req.originalUrl; - const indexHtmlPath = path.resolve(clientRoot, "index.html"); - let template = fs.readFileSync(indexHtmlPath, "utf-8"); - - template = await vite.transformIndexHtml(url, template); - - res.status(200).set({ "Content-Type": "text/html" }).end(template); - } else { - next(); - } - } catch (e) { - vite.ssrFixStacktrace(e as Error); - next(e); - } - }, - ); - } - getTunnelForRequest(req: express.Request) { const email = req.headers["x-forwarded-email"] as string; const cookieHeader = req.headers.cookie; diff --git a/packages/app-kit/src/server/static-server.ts b/packages/app-kit/src/server/static-server.ts new file mode 100644 index 0000000..fe7faca --- /dev/null +++ b/packages/app-kit/src/server/static-server.ts @@ -0,0 +1,70 @@ +import fs from "node:fs"; +import path from "node:path"; +import express from "express"; +import { getQueries } from "./utils"; + +/** + * Static server for the App Kit. + * + * Serves pre-built static files in production mode. Handles SPA routing + * by serving index.html for non-API routes and injects runtime configuration. + * + * @example + * ```ts + * const staticServer = new StaticServer(app, staticPath); + * staticServer.setup(); + * ``` + */ +export class StaticServer { + private app: express.Application; + private staticPath: string; + + constructor(app: express.Application, staticPath: string) { + this.app = app; + this.staticPath = staticPath; + } + + /** Setup the static server. */ + setup() { + this.app.use( + express.static(this.staticPath, { + index: false, + }), + ); + + this.app.get("*", (req, res, next) => { + if (req.path.startsWith("/api") || req.path.startsWith("/query")) { + return next(); + } + this.serveIndex(res); + }); + } + + /** Serve the index.html file. */ + private serveIndex(res: express.Response) { + const indexPath = path.join(this.staticPath, "index.html"); + + if (!fs.existsSync(indexPath)) { + res.status(404).send("index.html not found"); + return; + } + + let html = fs.readFileSync(indexPath, "utf-8"); + const config = this.getRuntimeConfig(); + const configScript = ` + + `; + html = html.replace("", `${configScript}`); + res.send(html); + } + + private getRuntimeConfig() { + const configFolder = path.join(process.cwd(), "config"); + return { + appName: process.env.DATABRICKS_APP_NAME || "", + queries: getQueries(configFolder), + }; + } +} diff --git a/packages/app-kit/src/server/tests/server.integration.test.ts b/packages/app-kit/src/server/tests/server.integration.test.ts new file mode 100644 index 0000000..a3955d8 --- /dev/null +++ b/packages/app-kit/src/server/tests/server.integration.test.ts @@ -0,0 +1,219 @@ +import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; +import type { Server } from "node:http"; + +// Set required env vars BEFORE imports that use them +process.env.DATABRICKS_APP_PORT = "8000"; +process.env.FLASK_RUN_HOST = "0.0.0.0"; + +// Mock databricks middleware to avoid auth requirements +vi.mock("../../utils", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + databricksClientMiddleware: vi + .fn() + .mockResolvedValue((_req: any, _res: any, next: any) => next()), + }; +}); + +import { createApp } from "../../core"; +import { server as serverPlugin } from "../index"; +import { Plugin, toPlugin } from "../../plugin"; + +// Integration tests - actually start server and make HTTP requests +describe("ServerPlugin Integration", () => { + let server: Server; + let baseUrl: string; + const TEST_PORT = 9876; // Use non-standard port to avoid conflicts + + beforeAll(async () => { + const app = await createApp({ + plugins: [ + serverPlugin({ + port: TEST_PORT, + host: "127.0.0.1", + autoStart: false, + }), + ], + }); + + // Start server manually + const expressApp = await app.server.start(); + server = app.server.getServer(); + baseUrl = `http://127.0.0.1:${TEST_PORT}`; + + // Wait a bit for server to be ready + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + + afterAll(async () => { + if (server) { + await new Promise((resolve, reject) => { + server.close((err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + }); + + describe("health endpoint", () => { + test("GET /health returns 200 with status ok", async () => { + const response = await fetch(`${baseUrl}/health`); + + expect(response.status).toBe(200); + + const data = await response.json(); + expect(data).toEqual({ status: "ok" }); + }); + }); + + describe("API routing", () => { + test("unknown API route returns 404", async () => { + const response = await fetch(`${baseUrl}/api/nonexistent`); + + expect(response.status).toBe(404); + }); + }); + + describe("server lifecycle", () => { + test("server is listening on correct port", () => { + const address = server.address(); + + expect(address).not.toBeNull(); + if (typeof address === "object" && address !== null) { + expect(address.port).toBe(TEST_PORT); + } + }); + }); +}); + +describe("ServerPlugin with custom plugin", () => { + let server: Server; + let baseUrl: string; + const TEST_PORT = 9877; + + beforeAll(async () => { + // Create a simple test plugin + class TestPlugin extends Plugin { + name = "test-plugin" as const; + envVars: string[] = []; + + injectRoutes(router: any) { + router.get("/echo", (_req: any, res: any) => { + res.json({ message: "hello from test plugin" }); + }); + + router.post("/echo", (req: any, res: any) => { + res.json({ received: req.body }); + }); + } + } + + const testPlugin = toPlugin( + TestPlugin, + "test-plugin", + ); + + const app = await createApp({ + plugins: [ + serverPlugin({ + port: TEST_PORT, + host: "127.0.0.1", + autoStart: false, + }), + testPlugin({}), + ], + }); + + await app.server.start(); + server = app.server.getServer(); + baseUrl = `http://127.0.0.1:${TEST_PORT}`; + + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + + afterAll(async () => { + if (server) { + await new Promise((resolve, reject) => { + server.close((err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + }); + + test("GET /api/test-plugin/echo returns plugin response", async () => { + const response = await fetch(`${baseUrl}/api/test-plugin/echo`); + + expect(response.status).toBe(200); + + const data = await response.json(); + expect(data).toEqual({ message: "hello from test plugin" }); + }); + + test("POST /api/test-plugin/echo returns posted body", async () => { + const response = await fetch(`${baseUrl}/api/test-plugin/echo`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ foo: "bar" }), + }); + + expect(response.status).toBe(200); + + const data = await response.json(); + expect(data).toEqual({ received: { foo: "bar" } }); + }); +}); + +describe("ServerPlugin with extend()", () => { + let server: Server; + let baseUrl: string; + const TEST_PORT = 9878; + + beforeAll(async () => { + const app = await createApp({ + plugins: [ + serverPlugin({ + port: TEST_PORT, + host: "127.0.0.1", + autoStart: false, + }), + ], + }); + + // Add custom route via extend() + app.server.extend((expressApp) => { + expressApp.get("/custom", (_req, res) => { + res.json({ custom: true }); + }); + }); + + await app.server.start(); + server = app.server.getServer(); + baseUrl = `http://127.0.0.1:${TEST_PORT}`; + + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + + afterAll(async () => { + if (server) { + await new Promise((resolve, reject) => { + server.close((err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + }); + + test("custom route via extend() works", async () => { + const response = await fetch(`${baseUrl}/custom`); + + expect(response.status).toBe(200); + + const data = await response.json(); + expect(data).toEqual({ custom: true }); + }); +}); diff --git a/packages/app-kit/src/server/tests/server.test.ts b/packages/app-kit/src/server/tests/server.test.ts new file mode 100644 index 0000000..ac11048 --- /dev/null +++ b/packages/app-kit/src/server/tests/server.test.ts @@ -0,0 +1,365 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; + +// Use vi.hoisted for mocks that need to be available before module loading +const { mockHttpServer, mockExpressApp } = vi.hoisted(() => { + const httpServer = { + close: vi.fn((cb: any) => cb?.()), + on: vi.fn(), + address: vi.fn().mockReturnValue({ port: 8000 }), + }; + + const expressApp = { + use: vi.fn().mockReturnThis(), + get: vi.fn().mockReturnThis(), + listen: vi.fn((_port: any, _host: any, cb: any) => { + cb?.(); + return httpServer; + }), + _router: { + stack: [] as any[], + }, + }; + + return { mockHttpServer: httpServer, mockExpressApp: expressApp }; +}); + +// Mock express +vi.mock("express", () => { + const jsonMiddleware = vi.fn(); + const staticMiddleware = vi.fn(); + + const expressFn: any = vi.fn(() => mockExpressApp); + expressFn.json = vi.fn(() => jsonMiddleware); + expressFn.static = vi.fn(() => staticMiddleware); + expressFn.Router = vi.fn(() => ({ + get: vi.fn(), + post: vi.fn(), + use: vi.fn(), + })); + + return { default: expressFn }; +}); + +// Mock dependencies before imports +vi.mock("../../telemetry", () => ({ + TelemetryManager: { + getProvider: vi.fn().mockReturnValue({ + getTracer: vi.fn().mockReturnValue({ startActiveSpan: vi.fn() }), + getMeter: vi.fn().mockReturnValue({ + createCounter: vi.fn().mockReturnValue({ add: vi.fn() }), + createHistogram: vi.fn().mockReturnValue({ record: vi.fn() }), + }), + getLogger: vi.fn().mockReturnValue({ emit: vi.fn() }), + registerInstrumentations: vi.fn(), + }), + }, + instrumentations: { + http: {}, + express: {}, + }, +})); + +vi.mock("../../cache", () => ({ + CacheManager: { + getInstanceSync: vi.fn().mockReturnValue({ + get: vi.fn(), + set: vi.fn(), + delete: vi.fn(), + }), + }, +})); + +vi.mock("../../utils", () => ({ + databricksClientMiddleware: vi + .fn() + .mockResolvedValue((_req: any, _res: any, next: any) => next()), + isRemoteServerEnabled: vi.fn().mockReturnValue(false), + validateEnv: vi.fn(), + deepMerge: vi.fn((a, b) => ({ ...a, ...b })), +})); + +vi.mock("../vite-dev-server", () => ({ + ViteDevServer: vi.fn().mockImplementation(() => ({ + setup: vi.fn().mockResolvedValue(undefined), + close: vi.fn().mockResolvedValue(undefined), + })), +})); + +vi.mock("../static-server", () => ({ + StaticServer: vi.fn().mockImplementation(() => ({ + setup: vi.fn(), + })), +})); + +vi.mock("../remote-tunnel-manager", () => ({ + RemoteTunnelManager: vi.fn().mockImplementation(() => ({ + setServer: vi.fn(), + setup: vi.fn(), + setupWebSocket: vi.fn(), + cleanup: vi.fn(), + })), +})); + +vi.mock("dotenv", () => ({ + default: { config: vi.fn() }, +})); + +// Mock fs for findStaticPath +vi.mock("node:fs", () => ({ + default: { + existsSync: vi.fn().mockReturnValue(false), + readFileSync: vi.fn(), + }, +})); + +vi.mock("../utils", () => ({ + getRoutes: vi.fn().mockReturnValue([]), +})); + +import fs from "node:fs"; +import { isRemoteServerEnabled } from "../../utils"; +import { RemoteTunnelManager } from "../remote-tunnel-manager"; +import { StaticServer } from "../static-server"; +import { ViteDevServer } from "../vite-dev-server"; +import { ServerPlugin } from "../index"; + +describe("ServerPlugin", () => { + let originalEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + originalEnv = { ...process.env }; + vi.clearAllMocks(); + + // Reset mock router stack for health endpoint test + mockExpressApp._router.stack = []; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + describe("constructor", () => { + test("should initialize with default config", () => { + const plugin = new ServerPlugin({}); + + expect(plugin.name).toBe("server"); + expect(plugin.envVars).toEqual(["DATABRICKS_APP_PORT", "FLASK_RUN_HOST"]); + }); + + test("should use provided config values", () => { + const plugin = new ServerPlugin({ + port: 3000, + host: "127.0.0.1", + autoStart: false, + }); + + const config = plugin.getConfig(); + expect(config.port).toBe(3000); + expect(config.host).toBe("127.0.0.1"); + expect(config.autoStart).toBe(false); + }); + }); + + describe("DEFAULT_CONFIG", () => { + test("should have correct default values", () => { + expect(ServerPlugin.DEFAULT_CONFIG.autoStart).toBe(true); + expect(ServerPlugin.DEFAULT_CONFIG.host).toBe("0.0.0.0"); + expect(ServerPlugin.DEFAULT_CONFIG.port).toBe(8000); + }); + + test("should use env vars when available", () => { + expect(typeof ServerPlugin.DEFAULT_CONFIG.port).toBe("number"); + expect(typeof ServerPlugin.DEFAULT_CONFIG.host).toBe("string"); + }); + }); + + describe("shouldAutoStart", () => { + test("should return true when autoStart is true", () => { + const plugin = new ServerPlugin({ autoStart: true }); + expect(plugin.shouldAutoStart()).toBe(true); + }); + + test("should return false when autoStart is false", () => { + const plugin = new ServerPlugin({ autoStart: false }); + expect(plugin.shouldAutoStart()).toBe(false); + }); + }); + + describe("setup", () => { + test("should call start when autoStart is true", async () => { + const plugin = new ServerPlugin({ autoStart: true }); + const startSpy = vi.spyOn(plugin, "start").mockResolvedValue({} as any); + + await plugin.setup(); + + expect(startSpy).toHaveBeenCalled(); + }); + + test("should not call start when autoStart is false", async () => { + const plugin = new ServerPlugin({ autoStart: false }); + const startSpy = vi.spyOn(plugin, "start").mockResolvedValue({} as any); + + await plugin.setup(); + + expect(startSpy).not.toHaveBeenCalled(); + }); + }); + + describe("start", () => { + test("should call listen on express app", async () => { + const plugin = new ServerPlugin({ autoStart: false, port: 3000 }); + + await plugin.start(); + + expect(mockExpressApp.listen).toHaveBeenCalledWith( + 3000, + expect.any(String), + expect.any(Function), + ); + }); + + test("should setup ViteDevServer in development mode", async () => { + process.env.NODE_ENV = "development"; + const plugin = new ServerPlugin({ autoStart: false }); + + await plugin.start(); + + expect(ViteDevServer).toHaveBeenCalled(); + const viteInstance = vi.mocked(ViteDevServer).mock.results[0].value; + expect(viteInstance.setup).toHaveBeenCalled(); + }); + + test("should setup StaticServer in production mode with valid static path", async () => { + process.env.NODE_ENV = "production"; + vi.mocked(fs.existsSync).mockReturnValue(true); + + const plugin = new ServerPlugin({ autoStart: false }); + + await plugin.start(); + + expect(StaticServer).toHaveBeenCalled(); + const staticInstance = vi.mocked(StaticServer).mock.results[0].value; + expect(staticInstance.setup).toHaveBeenCalled(); + }); + + test("should not setup StaticServer when no static path found", async () => { + process.env.NODE_ENV = "production"; + vi.mocked(fs.existsSync).mockReturnValue(false); + + const plugin = new ServerPlugin({ autoStart: false }); + + await plugin.start(); + + expect(StaticServer).not.toHaveBeenCalled(); + }); + + test("should setup RemoteTunnelManager when remote serving is enabled", async () => { + vi.mocked(isRemoteServerEnabled).mockReturnValue(true); + + const plugin = new ServerPlugin({ autoStart: false }); + + await plugin.start(); + + expect(RemoteTunnelManager).toHaveBeenCalled(); + const tunnelInstance = + vi.mocked(RemoteTunnelManager).mock.results[0].value; + expect(tunnelInstance.setServer).toHaveBeenCalled(); + expect(tunnelInstance.setup).toHaveBeenCalled(); + expect(tunnelInstance.setupWebSocket).toHaveBeenCalled(); + }); + + test("should not setup RemoteTunnelManager when remote serving is disabled", async () => { + vi.mocked(isRemoteServerEnabled).mockReturnValue(false); + + const plugin = new ServerPlugin({ autoStart: false }); + + await plugin.start(); + + expect(RemoteTunnelManager).not.toHaveBeenCalled(); + }); + }); + + describe("extend", () => { + test("should add extension function when autoStart is false", () => { + const plugin = new ServerPlugin({ autoStart: false }); + const extensionFn = vi.fn(); + + const result = plugin.extend(extensionFn); + + expect(result).toBe(plugin); + }); + + test("should throw when autoStart is true", () => { + const plugin = new ServerPlugin({ autoStart: true }); + const extensionFn = vi.fn(); + + expect(() => plugin.extend(extensionFn)).toThrow( + "Cannot extend server when autoStart is true.", + ); + }); + + test("should call extension functions during start", async () => { + const plugin = new ServerPlugin({ autoStart: false }); + const extensionFn = vi.fn(); + + plugin.extend(extensionFn); + await plugin.start(); + + expect(extensionFn).toHaveBeenCalled(); + }); + }); + + describe("getServer", () => { + test("should throw when autoStart is true", () => { + const plugin = new ServerPlugin({ autoStart: true }); + + expect(() => plugin.getServer()).toThrow( + "Cannot get server when autoStart is true.", + ); + }); + + test("should throw when server not started", () => { + const plugin = new ServerPlugin({ autoStart: false }); + + expect(() => plugin.getServer()).toThrow( + "Server not started. Please start the server first by calling the start() method.", + ); + }); + + test("should return server after start", async () => { + const plugin = new ServerPlugin({ autoStart: false }); + + await plugin.start(); + const server = plugin.getServer(); + + expect(server).toBe(mockHttpServer); + }); + }); + + describe("getConfig", () => { + test("should return config without plugins", () => { + const mockPlugin = { name: "test" } as any; + const plugin = new ServerPlugin({ + port: 3000, + plugins: { test: mockPlugin }, + }); + + const config = plugin.getConfig(); + + expect(config.port).toBe(3000); + expect(config.plugins).toBeUndefined(); + }); + }); + + describe("isRemoteServingEnabled", () => { + test("should return value from isRemoteServerEnabled util", () => { + vi.mocked(isRemoteServerEnabled).mockReturnValue(true); + const plugin = new ServerPlugin({ autoStart: false }); + + expect(plugin.isRemoteServingEnabled()).toBe(true); + + vi.mocked(isRemoteServerEnabled).mockReturnValue(false); + expect(plugin.isRemoteServingEnabled()).toBe(false); + }); + }); +}); diff --git a/packages/app-kit/src/server/tests/static-server.test.ts b/packages/app-kit/src/server/tests/static-server.test.ts new file mode 100644 index 0000000..df221dd --- /dev/null +++ b/packages/app-kit/src/server/tests/static-server.test.ts @@ -0,0 +1,182 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import path from "node:path"; + +// Mock fs +vi.mock("node:fs", () => ({ + default: { + readFileSync: vi + .fn() + .mockReturnValue( + "
", + ), + existsSync: vi.fn().mockReturnValue(true), + }, +})); + +// Mock express.static +vi.mock("express", () => ({ + default: { + static: vi + .fn() + .mockReturnValue((_req: any, _res: any, next: any) => next()), + }, +})); + +// Mock getQueries +vi.mock("../utils", () => ({ + getQueries: vi.fn().mockReturnValue({ query1: "SELECT 1" }), +})); + +import fs from "node:fs"; +import express from "express"; +import { getQueries } from "../utils"; +import { StaticServer } from "../static-server"; + +describe("StaticServer", () => { + let mockApp: any; + let mockRes: any; + let mockNext: any; + + beforeEach(() => { + vi.clearAllMocks(); + + mockApp = { + use: vi.fn(), + get: vi.fn(), + }; + + mockRes = { + send: vi.fn(), + }; + + mockNext = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("constructor", () => { + test("should store app and staticPath", () => { + const staticPath = "/path/to/static"; + const server = new StaticServer(mockApp, staticPath); + + expect(server).toBeDefined(); + }); + }); + + describe("setup", () => { + test("should register express.static middleware", () => { + const staticPath = "/path/to/static"; + const server = new StaticServer(mockApp, staticPath); + + server.setup(); + + expect(express.static).toHaveBeenCalledWith(staticPath, { index: false }); + expect(mockApp.use).toHaveBeenCalled(); + }); + + test("should register catch-all GET route for SPA fallback", () => { + const server = new StaticServer(mockApp, "/static"); + + server.setup(); + + expect(mockApp.get).toHaveBeenCalledWith("*", expect.any(Function)); + }); + + test("should skip API routes in SPA fallback", () => { + const server = new StaticServer(mockApp, "/static"); + + server.setup(); + + const handler = mockApp.get.mock.calls[0][1]; + + handler({ path: "/api/test" }, mockRes, mockNext); + expect(mockNext).toHaveBeenCalled(); + expect(mockRes.send).not.toHaveBeenCalled(); + }); + + test("should skip query routes in SPA fallback", () => { + const server = new StaticServer(mockApp, "/static"); + + server.setup(); + + const handler = mockApp.get.mock.calls[0][1]; + + handler({ path: "/query/test" }, mockRes, mockNext); + expect(mockNext).toHaveBeenCalled(); + expect(mockRes.send).not.toHaveBeenCalled(); + }); + + test("should serve index.html for non-API routes", () => { + const server = new StaticServer(mockApp, "/static"); + + server.setup(); + + const handler = mockApp.get.mock.calls[0][1]; + + handler({ path: "/some/page" }, mockRes, mockNext); + + expect(mockRes.send).toHaveBeenCalled(); + expect(mockNext).not.toHaveBeenCalled(); + }); + }); + + describe("serveIndex (via setup handler)", () => { + test("should read index.html from static path", () => { + const staticPath = "/my/static/path"; + const server = new StaticServer(mockApp, staticPath); + + server.setup(); + + const handler = mockApp.get.mock.calls[0][1]; + handler({ path: "/" }, mockRes, mockNext); + + expect(fs.readFileSync).toHaveBeenCalledWith( + path.join(staticPath, "index.html"), + "utf-8", + ); + }); + + test("should inject config script into HTML", () => { + const server = new StaticServer(mockApp, "/static"); + + server.setup(); + + const handler = mockApp.get.mock.calls[0][1]; + handler({ path: "/" }, mockRes, mockNext); + + const sentHtml = mockRes.send.mock.calls[0][0]; + expect(sentHtml).toContain("window.__CONFIG__"); + expect(sentHtml).toContain(" + + diff --git a/apps/clean-app/package-lock.json b/apps/clean-app/package-lock.json new file mode 100644 index 0000000..f034d6d --- /dev/null +++ b/apps/clean-app/package-lock.json @@ -0,0 +1,3212 @@ +{ + "name": "clean-app", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "clean-app", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.3.tgz", + "integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", + "integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/type-utils": "8.49.0", + "@typescript-eslint/utils": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.49.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz", + "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz", + "integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.49.0", + "@typescript-eslint/types": "^8.49.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz", + "integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz", + "integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz", + "integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz", + "integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz", + "integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.49.0", + "@typescript-eslint/tsconfig-utils": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz", + "integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz", + "integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.49.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", + "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.1" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.49.0.tgz", + "integrity": "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.49.0", + "@typescript-eslint/parser": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", + "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/apps/clean-app/package.json b/apps/clean-app/package.json new file mode 100644 index 0000000..e31fb90 --- /dev/null +++ b/apps/clean-app/package.json @@ -0,0 +1,31 @@ +{ + "name": "clean-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "NODE_ENV=development tsx watch server.ts", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@databricks/app-kit": "workspace:*", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/apps/clean-app/public/vite.svg b/apps/clean-app/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/apps/clean-app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/clean-app/server.ts b/apps/clean-app/server.ts new file mode 100644 index 0000000..cc279cb --- /dev/null +++ b/apps/clean-app/server.ts @@ -0,0 +1,5 @@ +import { createApp, server } from "@databricks/app-kit"; + +createApp({ + plugins: [server()], +}); diff --git a/apps/clean-app/src/App.css b/apps/clean-app/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/apps/clean-app/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/apps/clean-app/src/App.tsx b/apps/clean-app/src/App.tsx new file mode 100644 index 0000000..5457236 --- /dev/null +++ b/apps/clean-app/src/App.tsx @@ -0,0 +1,35 @@ +import { useState } from "react"; +import viteLogo from "/vite.svg"; +import reactLogo from "./assets/react.svg"; +import "./App.css"; + +function App() { + const [count, setCount] = useState(0); + + return ( + <> + +

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ); +} + +export default App; diff --git a/apps/clean-app/src/assets/react.svg b/apps/clean-app/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/apps/clean-app/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/clean-app/src/index.css b/apps/clean-app/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/apps/clean-app/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/apps/clean-app/src/main.tsx b/apps/clean-app/src/main.tsx new file mode 100644 index 0000000..eff7ccc --- /dev/null +++ b/apps/clean-app/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App.tsx"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/apps/clean-app/tsconfig.app.json b/apps/clean-app/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/apps/clean-app/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/apps/clean-app/tsconfig.json b/apps/clean-app/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/apps/clean-app/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/clean-app/tsconfig.node.json b/apps/clean-app/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/apps/clean-app/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/clean-app/vite.config.ts b/apps/clean-app/vite.config.ts new file mode 100644 index 0000000..0e43ae8 --- /dev/null +++ b/apps/clean-app/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/packages/app-kit/src/server/index.ts b/packages/app-kit/src/server/index.ts index 10d6a12..151842b 100644 --- a/packages/app-kit/src/server/index.ts +++ b/packages/app-kit/src/server/index.ts @@ -13,7 +13,7 @@ import type { ServerConfig } from "./types"; import { getRoutes } from "./utils"; import { ViteDevServer } from "./vite-dev-server"; -dotenv.config({ path: path.resolve(process.cwd(), "./server/.env") }); +dotenv.config({ path: path.resolve(process.cwd(), "./.env") }); /** * Server plugin for the App Kit. @@ -146,6 +146,22 @@ export class ServerPlugin extends Plugin { return this.server; } + /** + * Extend the server with custom routes or middleware. + * + * @param fn - A function that receives the express application. + * @returns The server plugin instance for chaining. + * @throws {Error} If autoStart is true. + */ + extend(fn: (app: express.Application) => void) { + if (this.shouldAutoStart()) { + throw new Error("Cannot extend server when autoStart is true."); + } + + this.serverExtensions.push(fn); + return this; + } + /** * Setup the routes with the plugins. * diff --git a/packages/app-kit/src/server/tests/server.integration.test.ts b/packages/app-kit/src/server/tests/server.integration.test.ts index a3955d8..494d20f 100644 --- a/packages/app-kit/src/server/tests/server.integration.test.ts +++ b/packages/app-kit/src/server/tests/server.integration.test.ts @@ -38,7 +38,7 @@ describe("ServerPlugin Integration", () => { }); // Start server manually - const expressApp = await app.server.start(); + await app.server.start(); server = app.server.getServer(); baseUrl = `http://127.0.0.1:${TEST_PORT}`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8a3172..ad32bd0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 10.4.1 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@types/node': specifier: ^24.7.2 version: 24.7.2 @@ -69,6 +69,55 @@ importers: specifier: ^3.2.4 version: 3.2.4(@types/node@24.7.2)(jiti@2.6.1)(jsdom@27.0.0(bufferutil@4.0.9)(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + apps/clean-app: + dependencies: + '@databricks/app-kit': + specifier: workspace:* + version: link:../../packages/app-kit + react: + specifier: ^19.2.0 + version: 19.2.0 + react-dom: + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) + devDependencies: + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.1 + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 + '@types/react': + specifier: ^19.2.5 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.7) + '@vitejs/plugin-react': + specifier: ^5.1.1 + version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) + eslint: + specifier: ^9.39.1 + version: 9.39.1(jiti@2.6.1) + eslint-plugin-react-hooks: + specifier: ^7.0.1 + version: 7.0.1(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: ^0.4.24 + version: 0.4.24(eslint@9.39.1(jiti@2.6.1)) + globals: + specifier: ^16.5.0 + version: 16.5.0 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.46.4 + version: 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + vite: + specifier: ^7.2.4 + version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + apps/dev-playground: dependencies: '@databricks/app-kit': @@ -816,6 +865,44 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.1': + resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -845,6 +932,22 @@ packages: peerDependencies: react-hook-form: ^7.55.0 + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@inquirer/external-editor@1.0.2': resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==} engines: {node: '>=18'} @@ -2467,6 +2570,9 @@ packages: '@types/inquirer@9.0.9': resolution: {integrity: sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/liftoff@4.0.3': resolution: {integrity: sha512-UgbL2kR5pLrWICvr8+fuSg0u43LY250q7ZMkC+XKC3E+rs/YBDEnQIzsnhU5dYsLlwMi3R75UvCL87pObP1sxw==} @@ -2511,12 +2617,20 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + '@types/react-syntax-highlighter@15.5.13': resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} '@types/react@19.2.2': resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/send@0.17.5': resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} @@ -2541,18 +2655,44 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.49.0': + resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.49.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.49.0': + resolution: {integrity: sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.49.0': resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@8.49.0': + resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.49.0': resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.49.0': + resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/types@8.49.0': resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2563,6 +2703,13 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.49.0': + resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@8.49.0': resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2641,6 +2788,11 @@ packages: peerDependencies: acorn: ^8 + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -2650,6 +2802,9 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -2955,6 +3110,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} @@ -3044,6 +3202,9 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -3251,20 +3412,65 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} hasBin: true + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.24: + resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} + peerDependencies: + eslint: '>=8.40' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@4.2.1: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@9.39.1: + resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -3319,6 +3525,12 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -3338,6 +3550,10 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + filing-cabinet@5.0.3: resolution: {integrity: sha512-PlPcMwVWg60NQkhvfoxZs4wEHjhlOO/y7OAm4sKM60o1Z9nttRY4mcdQxp/iZ+kg/Vv6Hw1OAaTbYVM9DA9pYg==} engines: {node: '>=18'} @@ -3351,6 +3567,10 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + find-up@7.0.0: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} @@ -3367,6 +3587,13 @@ packages: resolution: {integrity: sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==} engines: {node: '>= 10.13.0'} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + for-in@1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} engines: {node: '>=0.10.0'} @@ -3468,6 +3695,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true @@ -3492,6 +3723,14 @@ packages: resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} engines: {node: '>=0.10.0'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} @@ -3540,6 +3779,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + homedir-polyfill@1.0.3: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} engines: {node: '>=0.10.0'} @@ -3594,6 +3839,14 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3604,6 +3857,10 @@ packages: import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -3785,6 +4042,10 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + jsdom@27.0.0: resolution: {integrity: sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==} engines: {node: '>=20'} @@ -3802,12 +4063,21 @@ packages: json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -3823,6 +4093,13 @@ packages: jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + liftoff@5.0.1: resolution: {integrity: sha512-wwLXMbuxSF8gMvubFcFRp56lkFV69twvbU5vDPbaw+Q+/rF8j0HKjGbIdlSi+LuJm9jf7k9PB+nTxnsLMPcv2Q==} engines: {node: '>=10.13.0'} @@ -3913,6 +4190,10 @@ packages: resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} engines: {node: '>=18.0.0'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + locate-path@7.2.0: resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4108,6 +4389,9 @@ packages: nanospinner@1.2.2: resolution: {integrity: sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==} + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -4193,14 +4477,26 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + p-limit@4.0.0: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + p-locate@6.0.0: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4234,6 +4530,10 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + path-exists@5.0.0: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4361,6 +4661,10 @@ packages: engines: {node: '>=18'} hasBin: true + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -4827,6 +5131,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -5015,6 +5323,10 @@ packages: tw-animate-css@1.4.0: resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -5023,6 +5335,13 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + typescript-eslint@8.49.0: + resolution: {integrity: sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -5063,6 +5382,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -5244,6 +5566,10 @@ packages: engines: {node: '>=8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -5309,6 +5635,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + yocto-queue@1.2.1: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} @@ -5323,6 +5653,12 @@ packages: typescript: ^5.0.0 zod: ^3.25.0 || ^4.0.0 + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + zod@4.1.13: resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} @@ -5809,6 +6145,52 @@ snapshots: '@esbuild/win32-x64@0.25.10': optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': + dependencies: + eslint: 9.39.1(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.1': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -5843,6 +6225,17 @@ snapshots: '@standard-schema/utils': 0.3.0 react-hook-form: 7.68.0(react@19.2.0) + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@inquirer/external-editor@1.0.2(@types/node@24.7.2)': dependencies: chardet: 2.1.0 @@ -7479,15 +7872,15 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@testing-library/dom': 10.4.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) optionalDependencies: - '@types/react': 19.2.2 - '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) '@tybys/wasm-util@0.10.1': dependencies: @@ -7538,7 +7931,7 @@ snapshots: '@types/conventional-commits-parser@5.0.1': dependencies: - '@types/node': 24.7.2 + '@types/node': 24.10.1 '@types/d3-array@3.2.2': {} @@ -7604,10 +7997,12 @@ snapshots: '@types/through': 0.0.33 rxjs: 7.8.2 + '@types/json-schema@7.0.15': {} + '@types/liftoff@4.0.3': dependencies: '@types/fined': 1.1.5 - '@types/node': 24.7.2 + '@types/node': 24.10.1 '@types/memcached@2.2.10': dependencies: @@ -7655,14 +8050,22 @@ snapshots: dependencies: '@types/react': 19.2.2 + '@types/react-dom@19.2.3(@types/react@19.2.7)': + dependencies: + '@types/react': 19.2.7 + '@types/react-syntax-highlighter@15.5.13': dependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.7 '@types/react@19.2.2': dependencies: csstype: 3.1.3 + '@types/react@19.2.7': + dependencies: + csstype: 3.2.3 + '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 @@ -7695,12 +8098,40 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 24.7.2 + '@types/node': 24.10.1 '@types/ws@8.18.1': dependencies: '@types/node': 24.10.1 + '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.49.0 + eslint: 9.39.1(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.49.0 + debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.49.0(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) @@ -7710,10 +8141,27 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/scope-manager@8.49.0': + dependencies: + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 + '@typescript-eslint/type-utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@8.49.0': {} '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)': @@ -7731,6 +8179,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.49.0': dependencies: '@typescript-eslint/types': 8.49.0 @@ -7772,6 +8231,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-react@5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.47 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/node@24.7.2)(jiti@2.6.1)(jsdom@27.0.0(bufferutil@4.0.9)(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@istanbuljs/schema': 0.1.3 @@ -7876,10 +8347,21 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn@8.15.0: {} agent-base@7.1.4: {} + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -7933,7 +8415,7 @@ snapshots: ast-kit@2.1.3: dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 pathe: 2.0.3 ast-module-types@6.0.1: {} @@ -8179,6 +8661,8 @@ snapshots: csstype@3.1.3: {} + csstype@3.2.3: {} + d3-array@3.2.4: dependencies: internmap: 2.0.3 @@ -8244,6 +8728,8 @@ snapshots: deep-eql@5.0.2: {} + deep-is@0.1.4: {} + defaults@1.0.4: dependencies: clone: 1.0.4 @@ -8446,6 +8932,8 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} + escodegen@2.1.0: dependencies: esprima: 4.0.1 @@ -8454,10 +8942,87 @@ snapshots: optionalDependencies: source-map: 0.6.1 + eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + eslint: 9.39.1(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 4.1.13 + zod-validation-error: 4.0.2(zod@4.1.13) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.4.24(eslint@9.39.1(jiti@2.6.1)): + dependencies: + eslint: 9.39.1(jiti@2.6.1) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + eslint-visitor-keys@4.2.1: {} + eslint@9.39.1(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.1 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + esprima@4.0.1: {} + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + estraverse@5.3.0: {} estree-walker@2.0.2: {} @@ -8542,6 +9107,10 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} fastq@1.19.1: @@ -8557,6 +9126,10 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + filing-cabinet@5.0.3: dependencies: app-module-path: 2.2.0 @@ -8587,6 +9160,11 @@ snapshots: transitivePeerDependencies: - supports-color + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + find-up@7.0.0: dependencies: locate-path: 7.2.0 @@ -8610,6 +9188,13 @@ snapshots: flagged-respawn@2.0.0: {} + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + for-in@1.0.2: {} for-own@1.0.0: @@ -8724,6 +9309,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + glob@10.4.5: dependencies: foreground-child: 3.3.1 @@ -8769,6 +9358,10 @@ snapshots: is-windows: 1.0.2 which: 1.3.1 + globals@14.0.0: {} + + globals@16.5.0: {} + globrex@0.1.2: {} gonzales-pe@4.3.0: @@ -8819,6 +9412,12 @@ snapshots: dependencies: function-bind: 1.1.2 + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + homedir-polyfill@1.0.3: dependencies: parse-passwd: 1.0.0 @@ -8879,6 +9478,10 @@ snapshots: ieee754@1.2.1: {} + ignore@5.3.2: {} + + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -8893,6 +9496,8 @@ snapshots: import-meta-resolve@4.2.0: {} + imurmurhash@0.1.4: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -9050,6 +9655,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsdom@27.0.0(bufferutil@4.0.9)(postcss@8.5.6): dependencies: '@asamuzakjp/dom-selector': 6.6.2 @@ -9084,10 +9693,16 @@ snapshots: dependencies: bignumber.js: 9.3.1 + json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} jsonparse@1.3.1: {} @@ -9103,6 +9718,15 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + liftoff@5.0.1: dependencies: extend: 3.0.2 @@ -9190,6 +9814,10 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.2 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + locate-path@7.2.0: dependencies: p-locate: 6.0.0 @@ -9344,6 +9972,8 @@ snapshots: dependencies: picocolors: 1.1.1 + natural-compare@1.4.0: {} + negotiator@0.6.3: {} neo-async@2.6.2: {} @@ -9428,6 +10058,15 @@ snapshots: dependencies: mimic-function: 5.0.1 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + ora@5.4.1: dependencies: bl: 4.1.0 @@ -9440,10 +10079,18 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + p-limit@4.0.0: dependencies: yocto-queue: 1.2.1 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + p-locate@6.0.0: dependencies: p-limit: 4.0.0 @@ -9477,6 +10124,8 @@ snapshots: parseurl@1.3.3: {} + path-exists@4.0.0: {} + path-exists@5.0.0: {} path-is-absolute@1.0.1: {} @@ -9603,6 +10252,8 @@ snapshots: transitivePeerDependencies: - supports-color + prelude-ls@1.2.1: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -9829,7 +10480,7 @@ snapshots: rolldown-plugin-dts@0.16.11(rolldown@1.0.0-beta.53)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.3 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/types': 7.28.4 ast-kit: 2.1.3 birpc: 2.6.1 @@ -10139,6 +10790,8 @@ snapshots: strip-final-newline@3.0.0: {} + strip-json-comments@3.1.1: {} + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -10294,6 +10947,10 @@ snapshots: tw-animate-css@1.4.0: {} + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-fest@0.21.3: {} type-is@1.6.18: @@ -10301,6 +10958,17 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + typescript-eslint@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} uglify-js@3.19.3: @@ -10331,6 +10999,10 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + use-callback-ref@1.3.3(@types/react@19.2.2)(react@19.2.0): dependencies: react: 19.2.0 @@ -10418,6 +11090,22 @@ snapshots: - supports-color - typescript + vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.4 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + tsx: 4.20.6 + yaml: 2.8.1 + vite@7.2.4(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.10 @@ -10519,6 +11207,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + word-wrap@1.2.5: {} + wordwrap@1.0.0: {} wrap-ansi@6.2.0: @@ -10575,6 +11265,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} + yocto-queue@1.2.1: {} yoctocolors-cjs@2.1.3: {} @@ -10584,4 +11276,8 @@ snapshots: typescript: 5.9.3 zod: 4.1.13 + zod-validation-error@4.0.2(zod@4.1.13): + dependencies: + zod: 4.1.13 + zod@4.1.13: {} From f1e1b1c2dad84aa600794566be0244d23f5e092a Mon Sep 17 00:00:00 2001 From: Ditadi Date: Thu, 11 Dec 2025 12:37:52 +0000 Subject: [PATCH 3/9] fix(server): dev mode add middleware before --- packages/app-kit/src/server/index.ts | 36 +++++++++++++--------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/app-kit/src/server/index.ts b/packages/app-kit/src/server/index.ts index 151842b..c2e1321 100644 --- a/packages/app-kit/src/server/index.ts +++ b/packages/app-kit/src/server/index.ts @@ -100,6 +100,12 @@ export class ServerPlugin extends Plugin { extension(this.serverApplication); } + // tunnel middlewared needs to be registered before the static/vite handlers + if (this.isRemoteServingEnabled()) { + this.remoteTunnelManager = new RemoteTunnelManager(this.devFileReader); + this.remoteTunnelManager.setup(this.serverApplication); + } + await this.setupFrontend(); const server = this.serverApplication.listen( @@ -110,8 +116,10 @@ export class ServerPlugin extends Plugin { this.server = server; - if (this.isRemoteServingEnabled()) { - this.setupRemoteTunnels(); + // ws needs the server instance to handle upgrades + if (this.isRemoteServingEnabled() && this.remoteTunnelManager) { + this.remoteTunnelManager.setServer(server); + this.remoteTunnelManager.setupWebSocket(); } process.on("SIGTERM", () => this._gracefulShutdown()); @@ -188,16 +196,18 @@ export class ServerPlugin extends Plugin { } /** - * Setup the frontend. - * - * This method sets up the frontend using Vite for development and static files for production. + * Setup frontend serving based on environment: + * - Dev mode: Vite for HMR + * - Production: Static files (unless remote tunnel handles it) */ private async setupFrontend() { const isDev = process.env.NODE_ENV === "development"; + const isRemote = this.isRemoteServingEnabled(); + if (isDev) { this.viteDevServer = new ViteDevServer(this.serverApplication); await this.viteDevServer.setup(); - } else { + } else if (!isRemote) { const staticPath = this.config.staticPath ?? this.findStaticPath(); if (staticPath) { const staticServer = new StaticServer( @@ -209,20 +219,6 @@ export class ServerPlugin extends Plugin { } } - /** - * Setup the remote tunnels. - * - * This method sets up the remote tunnels for the development server. - */ - private setupRemoteTunnels() { - if (!this.server) return; - - this.remoteTunnelManager = new RemoteTunnelManager(this.devFileReader); - this.remoteTunnelManager.setServer(this.server); - this.remoteTunnelManager.setup(this.serverApplication); - this.remoteTunnelManager.setupWebSocket(); - } - private findStaticPath() { const staticPaths = ["dist", "client/dist", "build", "public", "out"]; const cwd = process.cwd(); From 6ee69ea5831187e68a6da351c6d9f3497b5bcfea Mon Sep 17 00:00:00 2001 From: Ditadi Date: Thu, 11 Dec 2025 13:17:25 +0000 Subject: [PATCH 4/9] fix(server): handles dedicated static path --- packages/app-kit/src/server/index.ts | 33 ++++++++++++++----- .../src/server/remote-tunnel-manager.ts | 12 +++++-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/packages/app-kit/src/server/index.ts b/packages/app-kit/src/server/index.ts index 1e792cf..9abca78 100644 --- a/packages/app-kit/src/server/index.ts +++ b/packages/app-kit/src/server/index.ts @@ -32,7 +32,6 @@ dotenv.config({ path: path.resolve(process.cwd(), "./.env") }); export class ServerPlugin extends Plugin { public static DEFAULT_CONFIG = { autoStart: true, - staticPath: ServerPlugin.findStaticPath(), host: process.env.FLASK_RUN_HOST || "0.0.0.0", port: Number(process.env.DATABRICKS_APP_PORT) || 8000, }; @@ -201,19 +200,31 @@ export class ServerPlugin extends Plugin { /** * Setup frontend serving based on environment: - * - Dev mode: Vite for HMR - * - Production: Static files (unless remote tunnel handles it) + * - If staticPath is explicitly provided: use static server + * - Dev mode (no staticPath): Vite for HMR + * - Production (no staticPath): Static files auto-detected */ private async setupFrontend() { const isDev = process.env.NODE_ENV === "development"; const isRemote = this.isRemoteServingEnabled(); + const hasExplicitStaticPath = this.config.staticPath !== undefined; + // explict static path provided + if (hasExplicitStaticPath) { + const staticServer = new StaticServer( + this.serverApplication, + this.config.staticPath as string, + ); + staticServer.setup(); + return; + } + + // auto-detection based on environment if (isDev) { this.viteDevServer = new ViteDevServer(this.serverApplication); await this.viteDevServer.setup(); } else if (!isRemote) { - const staticPath = - this.config.staticPath ?? ServerPlugin.findStaticPath(); + const staticPath = ServerPlugin.findStaticPath(); if (staticPath) { const staticServer = new StaticServer( this.serverApplication, @@ -239,13 +250,19 @@ export class ServerPlugin extends Plugin { private logStartupInfo() { const isDev = process.env.NODE_ENV === "development"; + const hasExplicitStaticPath = this.config.staticPath !== undefined; const port = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port; const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host; console.log(`Server running on http://${host}:${port}`); - console.log( - `Mode: ${isDev ? "development (Vite HMR)" : "production (static)"}`, - ); + + if (hasExplicitStaticPath) { + console.log(`Mode: static (${this.config.staticPath})`); + } else if (isDev) { + console.log("Mode: development (Vite HMR)"); + } else { + console.log("Mode: production (static)"); + } if (this.isRemoteServingEnabled()) { console.log("Remote tunnel support: enabled"); diff --git a/packages/app-kit/src/server/remote-tunnel-manager.ts b/packages/app-kit/src/server/remote-tunnel-manager.ts index 1c3cf14..bb9eb1c 100644 --- a/packages/app-kit/src/server/remote-tunnel-manager.ts +++ b/packages/app-kit/src/server/remote-tunnel-manager.ts @@ -60,7 +60,11 @@ export class RemoteTunnelManager { /** Asset middleware for the development server. */ assetMiddleware() { - return async (req: express.Request, res: express.Response) => { + return async ( + req: express.Request, + res: express.Response, + next: express.NextFunction, + ) => { const email = req.headers["x-forwarded-email"] as string; // Try cookie first, then generate from email @@ -79,11 +83,13 @@ export class RemoteTunnelManager { tunnelId = generateTunnelIdFromEmail(email); } - if (!tunnelId) return res.status(404).send("Tunnel not ready"); + // no tunnelID means local dev mode, let vite handle assets + if (!tunnelId) return next(); const tunnel = this.tunnels.get(tunnelId); - if (!tunnel) return res.status(404).send("Tunnel not found"); + // no active tunnel connection, let vite handle assets + if (!tunnel) return next(); const { ws, approvedViewers, pendingFetches } = tunnel; From f5157d36b5959097a4ffb7e8e53ee1362bcc7a5c Mon Sep 17 00:00:00 2001 From: Ditadi Date: Thu, 11 Dec 2025 13:20:28 +0000 Subject: [PATCH 5/9] chore: fix biome/format command --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e88abd2..64e9f00 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "dev": "pnpm build && NODE_ENV=development turbo watch build:watch dev", "dev:inspect": "NODE_ENV=development pnpm --filter=sdk-playground dev:inspect", "format:check": "biome format .", - "format": "biome format --write .", - "lint:fix": "biome lint --write .", + "format": "biome format --write", + "lint:fix": "biome lint --write", "lint": "biome lint .", "pack:sdk": "pnpm build && pnpm -r dist", "prepare": "husky", From 44c844413f31276636f7424c90227e2a50b1b1a2 Mon Sep 17 00:00:00 2001 From: Ditadi Date: Thu, 11 Dec 2025 13:29:20 +0000 Subject: [PATCH 6/9] chore: typecheck ignore some ui files --- packages/app-kit-ui/src/react/ui/calendar.tsx | 1 + packages/app-kit-ui/src/react/ui/sonner.tsx | 1 + packages/app-kit-ui/src/react/ui/spinner.tsx | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/app-kit-ui/src/react/ui/calendar.tsx b/packages/app-kit-ui/src/react/ui/calendar.tsx index 5447f8e..dd0e5dc 100644 --- a/packages/app-kit-ui/src/react/ui/calendar.tsx +++ b/packages/app-kit-ui/src/react/ui/calendar.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck "use client"; import * as React from "react"; diff --git a/packages/app-kit-ui/src/react/ui/sonner.tsx b/packages/app-kit-ui/src/react/ui/sonner.tsx index c9cd094..7fdb606 100644 --- a/packages/app-kit-ui/src/react/ui/sonner.tsx +++ b/packages/app-kit-ui/src/react/ui/sonner.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import { CircleCheckIcon, InfoIcon, diff --git a/packages/app-kit-ui/src/react/ui/spinner.tsx b/packages/app-kit-ui/src/react/ui/spinner.tsx index 9d88982..c4e05d5 100644 --- a/packages/app-kit-ui/src/react/ui/spinner.tsx +++ b/packages/app-kit-ui/src/react/ui/spinner.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import { Loader2Icon } from "lucide-react"; import { cn } from "../lib/utils"; From 0e7c59523b62c1860448ad07353abce1c5057e76 Mon Sep 17 00:00:00 2001 From: Ditadi Date: Thu, 11 Dec 2025 18:17:13 +0000 Subject: [PATCH 7/9] chore: add biome write with dot path --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 64e9f00..e88abd2 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "dev": "pnpm build && NODE_ENV=development turbo watch build:watch dev", "dev:inspect": "NODE_ENV=development pnpm --filter=sdk-playground dev:inspect", "format:check": "biome format .", - "format": "biome format --write", - "lint:fix": "biome lint --write", + "format": "biome format --write .", + "lint:fix": "biome lint --write .", "lint": "biome lint .", "pack:sdk": "pnpm build && pnpm -r dist", "prepare": "husky", From e496d873ebd2ba447605b1d864583c1f551f6ef5 Mon Sep 17 00:00:00 2001 From: Ditadi Date: Fri, 12 Dec 2025 10:43:35 +0000 Subject: [PATCH 8/9] refactor(server): move isRemoteTunnelEnabled to be static on RemoteTunnelManager --- packages/app-kit/src/plugin/dev-reader.ts | 4 +- packages/app-kit/src/server/index.ts | 8 +- .../src/server/remote-tunnel-manager.ts | 21 ++--- .../app-kit/src/server/tests/server.test.ts | 76 ++++++++++--------- packages/app-kit/src/utils/index.ts | 1 - .../src/utils/remote-server-enabled.ts | 6 -- 6 files changed, 61 insertions(+), 55 deletions(-) delete mode 100644 packages/app-kit/src/utils/remote-server-enabled.ts diff --git a/packages/app-kit/src/plugin/dev-reader.ts b/packages/app-kit/src/plugin/dev-reader.ts index 8e5a730..361c6cf 100644 --- a/packages/app-kit/src/plugin/dev-reader.ts +++ b/packages/app-kit/src/plugin/dev-reader.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import type { TunnelConnection } from "shared"; -import { isRemoteServerEnabled } from "../utils"; +import { RemoteTunnelManager } from "@/server/remote-tunnel-manager"; type TunnelConnectionGetter = ( req: import("express").Request, @@ -23,7 +23,7 @@ export class DevFileReader { * We proxy the reader to return a noop function if the remote server is disabled. */ get(target, prop, receiver) { - if (isRemoteServerEnabled()) { + if (RemoteTunnelManager.isRemoteServerEnabled()) { return Reflect.get(target, prop, receiver); } diff --git a/packages/app-kit/src/server/index.ts b/packages/app-kit/src/server/index.ts index 9abca78..e821844 100644 --- a/packages/app-kit/src/server/index.ts +++ b/packages/app-kit/src/server/index.ts @@ -6,7 +6,7 @@ import express from "express"; import type { PluginPhase } from "shared"; import { Plugin, toPlugin } from "../plugin"; import { instrumentations } from "../telemetry"; -import { databricksClientMiddleware, isRemoteServerEnabled } from "../utils"; +import { databricksClientMiddleware } from "../utils"; import { RemoteTunnelManager } from "./remote-tunnel-manager"; import { StaticServer } from "./static-server"; import type { ServerConfig } from "./types"; @@ -79,7 +79,7 @@ export class ServerPlugin extends Plugin { /** Check if the remote serving is enabled. */ isRemoteServingEnabled() { - return isRemoteServerEnabled(); + return RemoteTunnelManager.isRemoteServerEnabled(); } /** @@ -265,7 +265,9 @@ export class ServerPlugin extends Plugin { } if (this.isRemoteServingEnabled()) { - console.log("Remote tunnel support: enabled"); + console.log("Remote tunnel: enabled"); + } else { + console.log("Remote tunnel: disabled"); } } diff --git a/packages/app-kit/src/server/remote-tunnel-manager.ts b/packages/app-kit/src/server/remote-tunnel-manager.ts index bb9eb1c..0358e82 100644 --- a/packages/app-kit/src/server/remote-tunnel-manager.ts +++ b/packages/app-kit/src/server/remote-tunnel-manager.ts @@ -60,11 +60,7 @@ export class RemoteTunnelManager { /** Asset middleware for the development server. */ assetMiddleware() { - return async ( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ) => { + return async (req: express.Request, res: express.Response) => { const email = req.headers["x-forwarded-email"] as string; // Try cookie first, then generate from email @@ -83,13 +79,11 @@ export class RemoteTunnelManager { tunnelId = generateTunnelIdFromEmail(email); } - // no tunnelID means local dev mode, let vite handle assets - if (!tunnelId) return next(); + if (!tunnelId) return res.status(404).send("Tunnel not ready"); const tunnel = this.tunnels.get(tunnelId); - // no active tunnel connection, let vite handle assets - if (!tunnel) return next(); + if (!tunnel) return res.status(404).send("Tunnel not found"); const { ws, approvedViewers, pendingFetches } = tunnel; @@ -200,6 +194,15 @@ export class RemoteTunnelManager { ); } + static isRemoteServerEnabled() { + return ( + process.env.NODE_ENV !== "production" && + process.env.DISABLE_REMOTE_SERVING !== "true" && + // DATABRICKS_CLIENT_SECRET is set in the .env file for deployed environments + Boolean(process.env.DATABRICKS_CLIENT_SECRET) + ); + } + private loadHtmlTemplate( filename: string, replacements: Record, diff --git a/packages/app-kit/src/server/tests/server.test.ts b/packages/app-kit/src/server/tests/server.test.ts index 5403858..9fc9f90 100644 --- a/packages/app-kit/src/server/tests/server.test.ts +++ b/packages/app-kit/src/server/tests/server.test.ts @@ -1,27 +1,34 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; // Use vi.hoisted for mocks that need to be available before module loading -const { mockHttpServer, mockExpressApp } = vi.hoisted(() => { - const httpServer = { - close: vi.fn((cb: any) => cb?.()), - on: vi.fn(), - address: vi.fn().mockReturnValue({ port: 8000 }), - }; - - const expressApp = { - use: vi.fn().mockReturnThis(), - get: vi.fn().mockReturnThis(), - listen: vi.fn((_port: any, _host: any, cb: any) => { - cb?.(); - return httpServer; - }), - _router: { - stack: [] as any[], - }, - }; - - return { mockHttpServer: httpServer, mockExpressApp: expressApp }; -}); +const { mockHttpServer, mockExpressApp, mockIsRemoteServerEnabled } = + vi.hoisted(() => { + const httpServer = { + close: vi.fn((cb: any) => cb?.()), + on: vi.fn(), + address: vi.fn().mockReturnValue({ port: 8000 }), + }; + + const expressApp = { + use: vi.fn().mockReturnThis(), + get: vi.fn().mockReturnThis(), + listen: vi.fn((_port: any, _host: any, cb: any) => { + cb?.(); + return httpServer; + }), + _router: { + stack: [] as any[], + }, + }; + + const isRemoteServerEnabled = vi.fn().mockReturnValue(false); + + return { + mockHttpServer: httpServer, + mockExpressApp: expressApp, + mockIsRemoteServerEnabled: isRemoteServerEnabled, + }; + }); // Mock express vi.mock("express", () => { @@ -73,7 +80,6 @@ vi.mock("../../utils", () => ({ databricksClientMiddleware: vi .fn() .mockResolvedValue((_req: any, _res: any, next: any) => next()), - isRemoteServerEnabled: vi.fn().mockReturnValue(false), validateEnv: vi.fn(), deepMerge: vi.fn((a, b) => ({ ...a, ...b })), })); @@ -92,12 +98,15 @@ vi.mock("../static-server", () => ({ })); vi.mock("../remote-tunnel-manager", () => ({ - RemoteTunnelManager: vi.fn().mockImplementation(() => ({ - setServer: vi.fn(), - setup: vi.fn(), - setupWebSocket: vi.fn(), - cleanup: vi.fn(), - })), + RemoteTunnelManager: Object.assign( + vi.fn().mockImplementation(() => ({ + setServer: vi.fn(), + setup: vi.fn(), + setupWebSocket: vi.fn(), + cleanup: vi.fn(), + })), + { isRemoteServerEnabled: mockIsRemoteServerEnabled }, + ), })); vi.mock("dotenv", () => ({ @@ -117,11 +126,10 @@ vi.mock("../utils", () => ({ })); import fs from "node:fs"; -import { isRemoteServerEnabled } from "../../utils"; +import { ServerPlugin } from "../index"; import { RemoteTunnelManager } from "../remote-tunnel-manager"; import { StaticServer } from "../static-server"; import { ViteDevServer } from "../vite-dev-server"; -import { ServerPlugin } from "../index"; describe("ServerPlugin", () => { let originalEnv: NodeJS.ProcessEnv; @@ -254,7 +262,7 @@ describe("ServerPlugin", () => { }); test("should setup RemoteTunnelManager when remote serving is enabled", async () => { - vi.mocked(isRemoteServerEnabled).mockReturnValue(true); + mockIsRemoteServerEnabled.mockReturnValue(true); const plugin = new ServerPlugin({ autoStart: false }); @@ -269,7 +277,7 @@ describe("ServerPlugin", () => { }); test("should not setup RemoteTunnelManager when remote serving is disabled", async () => { - vi.mocked(isRemoteServerEnabled).mockReturnValue(false); + mockIsRemoteServerEnabled.mockReturnValue(false); const plugin = new ServerPlugin({ autoStart: false }); @@ -353,12 +361,12 @@ describe("ServerPlugin", () => { describe("isRemoteServingEnabled", () => { test("should return value from isRemoteServerEnabled util", () => { - vi.mocked(isRemoteServerEnabled).mockReturnValue(true); + mockIsRemoteServerEnabled.mockReturnValue(true); const plugin = new ServerPlugin({ autoStart: false }); expect(plugin.isRemoteServingEnabled()).toBe(true); - vi.mocked(isRemoteServerEnabled).mockReturnValue(false); + mockIsRemoteServerEnabled.mockReturnValue(false); expect(plugin.isRemoteServingEnabled()).toBe(false); }); }); diff --git a/packages/app-kit/src/utils/index.ts b/packages/app-kit/src/utils/index.ts index 50d5229..bd004f4 100644 --- a/packages/app-kit/src/utils/index.ts +++ b/packages/app-kit/src/utils/index.ts @@ -1,5 +1,4 @@ export * from "./databricks-client-middleware"; export * from "./env-validator"; export * from "./merge"; -export * from "./remote-server-enabled"; export * from "./vite-config-merge"; diff --git a/packages/app-kit/src/utils/remote-server-enabled.ts b/packages/app-kit/src/utils/remote-server-enabled.ts deleted file mode 100644 index 5da9b00..0000000 --- a/packages/app-kit/src/utils/remote-server-enabled.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function isRemoteServerEnabled() { - return ( - process.env.NODE_ENV !== "production" && - process.env.DISABLE_REMOTE_SERVING !== "true" - ); -} From 60563287e88a72128bf1efdd75f4cfdf57d05883 Mon Sep 17 00:00:00 2001 From: Ditadi Date: Fri, 12 Dec 2025 16:17:39 +0000 Subject: [PATCH 9/9] refactor(server): isolated remote tunnel permissions to controller --- packages/app-kit/src/plugin/dev-reader.ts | 4 +- packages/app-kit/src/server/index.ts | 57 ++--- .../server/{ => remote-tunnel}/denied.html | 0 .../app-kit/src/server/remote-tunnel/gate.ts | 60 +++++ .../src/server/{ => remote-tunnel}/index.html | 0 .../remote-tunnel-controller.test.ts | 175 +++++++++++++ .../remote-tunnel/remote-tunnel-controller.ts | 152 +++++++++++ .../remote-tunnel-manager.test.ts | 130 ++++++++++ .../remote-tunnel-manager.ts | 23 +- .../src/server/{ => remote-tunnel}/wait.html | 0 .../server/tests/remote-tunnel-gate.test.ts | 71 ++++++ .../app-kit/src/server/tests/server.test.ts | 238 +++++++++++++----- .../app-kit/src/server/tests/utils.test.ts | 93 +++++++ packages/app-kit/tsdown.config.ts | 12 +- 14 files changed, 891 insertions(+), 124 deletions(-) rename packages/app-kit/src/server/{ => remote-tunnel}/denied.html (100%) create mode 100644 packages/app-kit/src/server/remote-tunnel/gate.ts rename packages/app-kit/src/server/{ => remote-tunnel}/index.html (100%) create mode 100644 packages/app-kit/src/server/remote-tunnel/remote-tunnel-controller.test.ts create mode 100644 packages/app-kit/src/server/remote-tunnel/remote-tunnel-controller.ts create mode 100644 packages/app-kit/src/server/remote-tunnel/remote-tunnel-manager.test.ts rename packages/app-kit/src/server/{ => remote-tunnel}/remote-tunnel-manager.ts (97%) rename packages/app-kit/src/server/{ => remote-tunnel}/wait.html (100%) create mode 100644 packages/app-kit/src/server/tests/remote-tunnel-gate.test.ts create mode 100644 packages/app-kit/src/server/tests/utils.test.ts diff --git a/packages/app-kit/src/plugin/dev-reader.ts b/packages/app-kit/src/plugin/dev-reader.ts index 361c6cf..f546475 100644 --- a/packages/app-kit/src/plugin/dev-reader.ts +++ b/packages/app-kit/src/plugin/dev-reader.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import type { TunnelConnection } from "shared"; -import { RemoteTunnelManager } from "@/server/remote-tunnel-manager"; +import { isRemoteTunnelAllowedByEnv } from "@/server/remote-tunnel/gate"; type TunnelConnectionGetter = ( req: import("express").Request, @@ -23,7 +23,7 @@ export class DevFileReader { * We proxy the reader to return a noop function if the remote server is disabled. */ get(target, prop, receiver) { - if (RemoteTunnelManager.isRemoteServerEnabled()) { + if (isRemoteTunnelAllowedByEnv()) { return Reflect.get(target, prop, receiver); } diff --git a/packages/app-kit/src/server/index.ts b/packages/app-kit/src/server/index.ts index e821844..88d1403 100644 --- a/packages/app-kit/src/server/index.ts +++ b/packages/app-kit/src/server/index.ts @@ -7,7 +7,7 @@ import type { PluginPhase } from "shared"; import { Plugin, toPlugin } from "../plugin"; import { instrumentations } from "../telemetry"; import { databricksClientMiddleware } from "../utils"; -import { RemoteTunnelManager } from "./remote-tunnel-manager"; +import { RemoteTunnelController } from "./remote-tunnel/remote-tunnel-controller"; import { StaticServer } from "./static-server"; import type { ServerConfig } from "./types"; import { getRoutes } from "./utils"; @@ -41,7 +41,7 @@ export class ServerPlugin extends Plugin { private serverApplication: express.Application; private server: HTTPServer | null; private viteDevServer?: ViteDevServer; - private remoteTunnelManager?: RemoteTunnelManager; + private remoteTunnelController?: RemoteTunnelController; protected declare config: ServerConfig; private serverExtensions: ((app: express.Application) => void)[] = []; static phase: PluginPhase = "deferred"; @@ -77,11 +77,6 @@ export class ServerPlugin extends Plugin { return this.config.autoStart; } - /** Check if the remote serving is enabled. */ - isRemoteServingEnabled() { - return RemoteTunnelManager.isRemoteServerEnabled(); - } - /** * Start the server. * @@ -99,11 +94,11 @@ export class ServerPlugin extends Plugin { extension(this.serverApplication); } - // tunnel middlewared needs to be registered before the static/vite handlers - if (this.isRemoteServingEnabled()) { - this.remoteTunnelManager = new RemoteTunnelManager(this.devFileReader); - this.remoteTunnelManager.setup(this.serverApplication); - } + // register remote tunnel controller (before static/vite) + this.remoteTunnelController = new RemoteTunnelController( + this.devFileReader, + ); + this.serverApplication.use(this.remoteTunnelController.middleware); await this.setupFrontend(); @@ -115,11 +110,8 @@ export class ServerPlugin extends Plugin { this.server = server; - // ws needs the server instance to handle upgrades - if (this.isRemoteServingEnabled() && this.remoteTunnelManager) { - this.remoteTunnelManager.setServer(server); - this.remoteTunnelManager.setupWebSocket(); - } + // attach server to remote tunnel controller + this.remoteTunnelController.setServer(server); process.on("SIGTERM", () => this._gracefulShutdown()); process.on("SIGINT", () => this._gracefulShutdown()); @@ -206,7 +198,6 @@ export class ServerPlugin extends Plugin { */ private async setupFrontend() { const isDev = process.env.NODE_ENV === "development"; - const isRemote = this.isRemoteServingEnabled(); const hasExplicitStaticPath = this.config.staticPath !== undefined; // explict static path provided @@ -223,15 +214,14 @@ export class ServerPlugin extends Plugin { if (isDev) { this.viteDevServer = new ViteDevServer(this.serverApplication); await this.viteDevServer.setup(); - } else if (!isRemote) { - const staticPath = ServerPlugin.findStaticPath(); - if (staticPath) { - const staticServer = new StaticServer( - this.serverApplication, - staticPath, - ); - staticServer.setup(); - } + return; + } + + // auto-detection based on static path + const staticPath = ServerPlugin.findStaticPath(); + if (staticPath) { + const staticServer = new StaticServer(this.serverApplication, staticPath); + staticServer.setup(); } } @@ -264,10 +254,13 @@ export class ServerPlugin extends Plugin { console.log("Mode: production (static)"); } - if (this.isRemoteServingEnabled()) { - console.log("Remote tunnel: enabled"); + const remoteServerController = this.remoteTunnelController; + if (!remoteServerController) { + console.log("Remote tunnel: disabled (controller not initialized)"); } else { - console.log("Remote tunnel: disabled"); + console.log( + `Remote tunnel: ${remoteServerController.isAllowedByEnv() ? "allowed" : "blocked"}; ${remoteServerController.isActive() ? "active" : "inactive"}`, + ); } } @@ -278,8 +271,8 @@ export class ServerPlugin extends Plugin { await this.viteDevServer.close(); } - if (this.remoteTunnelManager) { - this.remoteTunnelManager.cleanup(); + if (this.remoteTunnelController) { + this.remoteTunnelController.cleanup(); } // 1. abort active operations from plugins diff --git a/packages/app-kit/src/server/denied.html b/packages/app-kit/src/server/remote-tunnel/denied.html similarity index 100% rename from packages/app-kit/src/server/denied.html rename to packages/app-kit/src/server/remote-tunnel/denied.html diff --git a/packages/app-kit/src/server/remote-tunnel/gate.ts b/packages/app-kit/src/server/remote-tunnel/gate.ts new file mode 100644 index 0000000..4e8c5f6 --- /dev/null +++ b/packages/app-kit/src/server/remote-tunnel/gate.ts @@ -0,0 +1,60 @@ +import type express from "express"; + +/** Assets prefixes that are served through the remote tunnel */ +export const REMOTE_TUNNEL_ASSET_PREFIXES = [ + "/@vite/", + "/@fs/", + "/node_modules/.vite/deps/", + "/node_modules/vite/", + "/src/", + "/@react-refresh", +]; + +/** + * Check if the server is running in local development mode + * - NODE_ENV is set to "development" + * @param env - the environment variables + * @returns true if the server is running in local development mode + */ +export function isLocalDev(env: NodeJS.ProcessEnv = process.env): boolean { + return env.NODE_ENV === "development"; +} + +/** + * Check if the environment allows the remote tunnel + * - not in local development mode + * - DISABLE_REMOTE_SERVING is not set to "true" + * - DATABRICKS_CLIENT_SECRET is set + * @param env - the environment variables + * @returns true if the environment allows the remote tunnel + */ +export function isRemoteTunnelAllowedByEnv( + env: NodeJS.ProcessEnv = process.env, +): boolean { + return ( + !isLocalDev(env) && + env.DISABLE_REMOTE_SERVING !== "true" && + Boolean(env.DATABRICKS_CLIENT_SECRET) + ); +} + +/** + * Check if the request has a dev query parameter + * @param req - the request + * @returns true if the request has a dev query parameter + */ +export function hasDevQuery(req: express.Request): boolean { + const queryParams = req.query; + + return "dev" in queryParams; +} + +/** + * Check if the request is an asset request + * @param req - the request + * @returns true if the request is an asset request + */ +export function isRemoteTunnelAssetRequest(req: express.Request): boolean { + const path = req.originalUrl; + return REMOTE_TUNNEL_ASSET_PREFIXES.some((prefix) => path.startsWith(prefix)); +} diff --git a/packages/app-kit/src/server/index.html b/packages/app-kit/src/server/remote-tunnel/index.html similarity index 100% rename from packages/app-kit/src/server/index.html rename to packages/app-kit/src/server/remote-tunnel/index.html diff --git a/packages/app-kit/src/server/remote-tunnel/remote-tunnel-controller.test.ts b/packages/app-kit/src/server/remote-tunnel/remote-tunnel-controller.test.ts new file mode 100644 index 0000000..01cabb0 --- /dev/null +++ b/packages/app-kit/src/server/remote-tunnel/remote-tunnel-controller.test.ts @@ -0,0 +1,175 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; + +const { mockDevHandler, mockAssetHandler, mockManagerInstance } = vi.hoisted( + () => { + const devHandler = vi.fn((_req: any, _res: any, next: any) => next()); + const assetHandler = vi.fn(async (_req: any, _res: any) => {}); + const managerInstance = { + devModeMiddleware: vi.fn(() => devHandler), + assetMiddleware: vi.fn(() => assetHandler), + setServer: vi.fn(), + setupWebSocket: vi.fn(), + cleanup: vi.fn(), + }; + return { + mockDevHandler: devHandler, + mockAssetHandler: assetHandler, + mockManagerInstance: managerInstance, + }; + }, +); + +vi.mock("./remote-tunnel-manager", () => ({ + RemoteTunnelManager: vi.fn().mockImplementation(() => mockManagerInstance), +})); + +import { RemoteTunnelController } from "./remote-tunnel-controller"; +import { RemoteTunnelManager } from "./remote-tunnel-manager"; + +describe("RemoteTunnelController", () => { + const originalEnv = { ...process.env }; + const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + beforeEach(() => { + vi.clearAllMocks(); + process.env = { ...originalEnv }; + delete process.env.DEBUG_REMOTE_TUNNEL; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + test("middleware hard-blocks in local dev (never initializes manager)", async () => { + process.env.NODE_ENV = "development"; + process.env.DATABRICKS_CLIENT_SECRET = "x"; + + const ctrl = new RemoteTunnelController({} as any); + const next = vi.fn(); + + await ctrl.middleware( + { query: { dev: "" }, originalUrl: "/?dev", method: "GET" } as any, + {} as any, + next, + ); + + expect(next).toHaveBeenCalled(); + expect(RemoteTunnelManager).not.toHaveBeenCalled(); + expect(ctrl.isActive()).toBe(false); + }); + + test("middleware blocks when env disallows remote tunnel", async () => { + process.env.NODE_ENV = "production"; + delete process.env.DATABRICKS_CLIENT_SECRET; + + const ctrl = new RemoteTunnelController({} as any); + const next = vi.fn(); + + await ctrl.middleware( + { query: { dev: "" }, originalUrl: "/?dev", method: "GET" } as any, + {} as any, + next, + ); + + expect(next).toHaveBeenCalled(); + expect(RemoteTunnelManager).not.toHaveBeenCalled(); + }); + + test("initializes manager on demand for dev query and calls devModeMiddleware", async () => { + process.env.NODE_ENV = "production"; + process.env.DATABRICKS_CLIENT_SECRET = "x"; + + const ctrl = new RemoteTunnelController({} as any); + const next = vi.fn(); + + await ctrl.middleware( + { + query: { dev: "" }, + originalUrl: "/?dev", + path: "/", + method: "GET", + } as any, + {} as any, + next, + ); + + expect(RemoteTunnelManager).toHaveBeenCalledTimes(1); + expect(mockManagerInstance.devModeMiddleware).toHaveBeenCalledTimes(1); + expect(mockDevHandler).toHaveBeenCalled(); + expect(ctrl.isActive()).toBe(true); + }); + + test("initializes manager on demand for asset request and calls assetMiddleware", async () => { + process.env.NODE_ENV = "production"; + process.env.DATABRICKS_CLIENT_SECRET = "x"; + + const ctrl = new RemoteTunnelController({} as any); + const next = vi.fn(); + + await ctrl.middleware( + { + query: {}, + originalUrl: "/@vite/client", + path: "/@vite/client", + method: "GET", + } as any, + {} as any, + next, + ); + + expect(RemoteTunnelManager).toHaveBeenCalledTimes(1); + expect(mockManagerInstance.assetMiddleware).toHaveBeenCalledTimes(1); + expect(mockAssetHandler).toHaveBeenCalled(); + }); + + test("setServer before init hooks websocket after init", async () => { + process.env.NODE_ENV = "production"; + process.env.DATABRICKS_CLIENT_SECRET = "x"; + + const ctrl = new RemoteTunnelController({} as any); + const server = {} as any; + ctrl.setServer(server); + + await ctrl.middleware( + { + query: { dev: "" }, + originalUrl: "/?dev", + path: "/", + method: "GET", + } as any, + {} as any, + vi.fn(), + ); + + expect(mockManagerInstance.setServer).toHaveBeenCalledWith(server); + expect(mockManagerInstance.setupWebSocket).toHaveBeenCalledTimes(1); + }); + + test("cleanup calls manager.cleanup and resets state", async () => { + process.env.NODE_ENV = "production"; + process.env.DATABRICKS_CLIENT_SECRET = "x"; + + const ctrl = new RemoteTunnelController({} as any); + + await ctrl.middleware( + { + query: { dev: "" }, + originalUrl: "/?dev", + path: "/", + method: "GET", + } as any, + {} as any, + vi.fn(), + ); + + expect(ctrl.isActive()).toBe(true); + + ctrl.cleanup(); + expect(mockManagerInstance.cleanup).toHaveBeenCalledTimes(1); + expect(ctrl.isActive()).toBe(false); + }); + + afterEach(() => { + consoleLogSpy.mockClear(); + }); +}); diff --git a/packages/app-kit/src/server/remote-tunnel/remote-tunnel-controller.ts b/packages/app-kit/src/server/remote-tunnel/remote-tunnel-controller.ts new file mode 100644 index 0000000..5c79911 --- /dev/null +++ b/packages/app-kit/src/server/remote-tunnel/remote-tunnel-controller.ts @@ -0,0 +1,152 @@ +import type { Server as HTTPServer } from "node:http"; +import type express from "express"; +import type { DevFileReader } from "../../plugin/dev-reader"; +import { + hasDevQuery, + isLocalDev, + isRemoteTunnelAllowedByEnv, + isRemoteTunnelAssetRequest, +} from "./gate"; +import type { RemoteTunnelManager } from "./remote-tunnel-manager"; + +/** + * Controller for the remote tunnel + * + * - Reads files from the dev file reader + * - Manages the remote tunnel manager + * - Sets up the web socket + * - Cleans up the remote tunnel + */ +export class RemoteTunnelController { + private devFileReader: DevFileReader; + private server?: HTTPServer; + private manager: RemoteTunnelManager | null; + private initPromise: Promise | null; + private wsReady: boolean; + + constructor(devFileReader: DevFileReader) { + this.devFileReader = devFileReader; + this.manager = null; + this.initPromise = null; + this.wsReady = false; + } + + /** + * Set the server instance + * @param server + */ + setServer(server: HTTPServer) { + this.server = server; + this.maybeSetupWebSocket(); + } + + /** Check if the remote tunnel is active */ + isActive(): boolean { + return this.manager != null; + } + + /** Check if the remote tunnel is allowed by the environment */ + isAllowedByEnv(): boolean { + return isRemoteTunnelAllowedByEnv(); + } + + /** + * Middleware for the remote tunnel + * - Hard blocks in local dev + * - Blocks when not allowed by env + * - Handles dev query and asset requests + * @param req - the request + * @param res - the response + * @param next - the next function + * @returns the next function + */ + middleware: express.RequestHandler = async (req, res, next) => { + // hard blocker in local dev + if (isLocalDev()) return next(); + + // if not allowed by env, block + if (!this.isAllowedByEnv()) return next(); + + const wantsDev = hasDevQuery(req); + const wantsRemoteAssets = isRemoteTunnelAssetRequest(req); + + // if not wants dev and not wants remote assets, skip + if (!wantsDev && !wantsRemoteAssets) return next(); + + const manager = await this.getOrInitManager(); + // if no manager, skip + if (!manager) return next(); + + // dev query present, let manager handle it + if (wantsDev) { + return manager.devModeMiddleware()(req, res, next); + } + + // otherwise, handle vite asset fetch paths through the tunnel + try { + await manager.assetMiddleware()(req, res); + } catch (error) { + next(error); + } + }; + + /** Cleanup the remote tunnel */ + cleanup() { + try { + this.manager?.cleanup(); + } finally { + this.manager = null; + this.initPromise = null; + this.wsReady = false; + } + } + + /** + * Get or initialize the remote tunnel manager + * - If the manager is already initialized, return it + * - If the manager is not initialized, initialize it + * - If the manager is not allowed by the environment, return null + * @returns the remote tunnel manager + */ + private async getOrInitManager(): Promise { + if (this.manager) return this.manager; + if (this.initPromise) return await this.initPromise; + + this.initPromise = (async () => { + // double check gate + if (isLocalDev() || !isRemoteTunnelAllowedByEnv()) return null; + const mod = await import("./remote-tunnel-manager"); + const remoteTunnelManager = new mod.RemoteTunnelManager( + this.devFileReader, + ); + this.manager = remoteTunnelManager; + + // attach server + ws setup + this.maybeSetupWebSocket(); + + console.log("RemoteTunnel: initialized (on-demand)"); + return remoteTunnelManager; + })(); + + return this.initPromise; + } + + /** + * Setup the web socket + * - If the server is not set, return + * - If the manager is not set, return + * - If the web socket is already setup, return + * - Setup the web socket + */ + private maybeSetupWebSocket() { + if (!this.server) return; + if (!this.manager) return; + if (this.wsReady) return; + + this.manager.setServer(this.server); + this.manager.setupWebSocket(); + this.wsReady = true; + + console.log("RemoteTunnel: web socket setup complete"); + } +} diff --git a/packages/app-kit/src/server/remote-tunnel/remote-tunnel-manager.test.ts b/packages/app-kit/src/server/remote-tunnel/remote-tunnel-manager.test.ts new file mode 100644 index 0000000..ace41ca --- /dev/null +++ b/packages/app-kit/src/server/remote-tunnel/remote-tunnel-manager.test.ts @@ -0,0 +1,130 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; + +const { mockReadFileSync, mockExistsSync } = vi.hoisted(() => ({ + mockReadFileSync: vi.fn(), + mockExistsSync: vi.fn(), +})); + +vi.mock("node:fs", () => ({ + default: { + readFileSync: mockReadFileSync, + existsSync: mockExistsSync, + readdirSync: vi.fn(), + }, +})); + +const { mockWebSocketServerCtor } = vi.hoisted(() => ({ + mockWebSocketServerCtor: vi.fn(), +})); + +vi.mock("ws", () => ({ + WebSocketServer: mockWebSocketServerCtor.mockImplementation((_opts: any) => ({ + on: vi.fn(), + emit: vi.fn(), + handleUpgrade: vi.fn((_req: any, _socket: any, _head: any, cb: any) => + cb({}), + ), + close: vi.fn(), + })), +})); + +import { REMOTE_TUNNEL_ASSET_PREFIXES } from "./gate"; +import { RemoteTunnelManager } from "./remote-tunnel-manager"; + +describe("RemoteTunnelManager (light unit tests)", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockReadFileSync.mockReturnValue("OK"); + mockExistsSync.mockReturnValue(false); + }); + + test("constructor registers tunnel getter and creates ws servers", () => { + const devFileReader = { registerTunnelGetter: vi.fn() }; + new RemoteTunnelManager(devFileReader as any); + + expect(devFileReader.registerTunnelGetter).toHaveBeenCalledTimes(1); + expect(mockWebSocketServerCtor).toHaveBeenCalledTimes(2); + }); + + test("setup registers dev middleware and asset middleware", () => { + const mgr = new RemoteTunnelManager({ + registerTunnelGetter: vi.fn(), + } as any); + const app = { use: vi.fn() } as any; + + mgr.setup(app); + + expect(app.use).toHaveBeenCalledTimes(2); + expect(app.use.mock.calls[0]).toHaveLength(1); + expect(app.use.mock.calls[1][0]).toBe(REMOTE_TUNNEL_ASSET_PREFIXES); + expect(typeof app.use.mock.calls[1][1]).toBe("function"); + }); + + test("devModeMiddleware passes through when dev is undefined", async () => { + const mgr = new RemoteTunnelManager({ + registerTunnelGetter: vi.fn(), + } as any); + const mw = mgr.devModeMiddleware(); + const next = vi.fn(); + + await mw({ query: {}, path: "/", headers: {} } as any, {} as any, next); + + expect(next).toHaveBeenCalledTimes(1); + }); + + test("devModeMiddleware serves HTML + cookie for ?dev (blank)", async () => { + const mgr = new RemoteTunnelManager({ + registerTunnelGetter: vi.fn(), + } as any); + const mw = mgr.devModeMiddleware(); + + const res = { + cookie: vi.fn(), + send: vi.fn(), + status: vi.fn().mockReturnThis(), + } as any; + const next = vi.fn(); + + await mw( + { + query: { dev: "" }, + path: "/", + originalUrl: "/?dev", + headers: { "x-forwarded-email": "x@y.com" }, + } as any, + res, + next, + ); + + expect(next).not.toHaveBeenCalled(); + expect(res.cookie).toHaveBeenCalledWith( + "dev-tunnel-id", + expect.any(String), + expect.objectContaining({ sameSite: "lax" }), + ); + expect(res.send).toHaveBeenCalledWith( + expect.stringContaining("window.__CONFIG__"), + ); + }); + + test("assetMiddleware returns 404 when tunnelId cannot be derived", async () => { + const mgr = new RemoteTunnelManager({ + registerTunnelGetter: vi.fn(), + } as any); + const mw = mgr.assetMiddleware(); + + const res = { + status: vi.fn().mockReturnThis(), + set: vi.fn().mockReturnThis(), + send: vi.fn(), + } as any; + + await mw( + { headers: {}, originalUrl: "/@vite/client", method: "GET" } as any, + res, + ); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.send).toHaveBeenCalledWith("Tunnel not ready"); + }); +}); diff --git a/packages/app-kit/src/server/remote-tunnel-manager.ts b/packages/app-kit/src/server/remote-tunnel/remote-tunnel-manager.ts similarity index 97% rename from packages/app-kit/src/server/remote-tunnel-manager.ts rename to packages/app-kit/src/server/remote-tunnel/remote-tunnel-manager.ts index 0358e82..bf5c1ff 100644 --- a/packages/app-kit/src/server/remote-tunnel-manager.ts +++ b/packages/app-kit/src/server/remote-tunnel/remote-tunnel-manager.ts @@ -6,11 +6,12 @@ import { fileURLToPath } from "node:url"; import type express from "express"; import type { TunnelConnection } from "shared"; import { WebSocketServer } from "ws"; -import { generateTunnelIdFromEmail, getQueries, parseCookies } from "./utils"; +import { generateTunnelIdFromEmail, getQueries, parseCookies } from "../utils"; +import { REMOTE_TUNNEL_ASSET_PREFIXES } from "./gate"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const MAX_ASSET_FETCH_TIMEOUT = 30_000; +const MAX_ASSET_FETCH_TIMEOUT = 60_000; interface DevFileReader { registerTunnelGetter( @@ -37,15 +38,6 @@ export class RemoteTunnelManager { private server?: HTTPServer; private devFileReader: DevFileReader; - public static ASSETS_MIDDLEWARE_PATHS: string[] = [ - "/@vite/*", - "/@fs/*", - "/node_modules/.vite/deps/*", - "/node_modules/vite/*", - "/src/*", - "/@react-refresh", - ]; - constructor(devFileReader: DevFileReader) { this.devFileReader = devFileReader; this.wss = new WebSocketServer({ noServer: true, path: "/dev-tunnel" }); @@ -126,9 +118,9 @@ export class RemoteTunnelManager { res: express.Response, next: express.NextFunction, ) => { - const dev = req.query.dev; + const dev = req.query?.dev; - if (!dev) { + if (dev === undefined) { return next(); } @@ -188,10 +180,7 @@ export class RemoteTunnelManager { /** Setup the dev mode middleware. */ setup(app: express.Application) { app.use(this.devModeMiddleware()); - app.use( - RemoteTunnelManager.ASSETS_MIDDLEWARE_PATHS, - this.assetMiddleware(), - ); + app.use(REMOTE_TUNNEL_ASSET_PREFIXES, this.assetMiddleware()); } static isRemoteServerEnabled() { diff --git a/packages/app-kit/src/server/wait.html b/packages/app-kit/src/server/remote-tunnel/wait.html similarity index 100% rename from packages/app-kit/src/server/wait.html rename to packages/app-kit/src/server/remote-tunnel/wait.html diff --git a/packages/app-kit/src/server/tests/remote-tunnel-gate.test.ts b/packages/app-kit/src/server/tests/remote-tunnel-gate.test.ts new file mode 100644 index 0000000..8ff260c --- /dev/null +++ b/packages/app-kit/src/server/tests/remote-tunnel-gate.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from "vitest"; +import { + hasDevQuery, + isLocalDev, + isRemoteTunnelAllowedByEnv, + isRemoteTunnelAssetRequest, +} from "../remote-tunnel/gate"; + +describe("remote-tunnel/gate", () => { + test("isLocalDev returns true when NODE_ENV=development", () => { + expect(isLocalDev({ NODE_ENV: "development" } as any)).toBe(true); + expect(isLocalDev({ NODE_ENV: "production" } as any)).toBe(false); + }); + + test("isRemoteTunnelAllowedByEnv requires secret, not disabled, and not local dev", () => { + expect( + isRemoteTunnelAllowedByEnv({ + NODE_ENV: "development", + DATABRICKS_CLIENT_SECRET: "x", + } as any), + ).toBe(false); + + expect( + isRemoteTunnelAllowedByEnv({ + NODE_ENV: "production", + DATABRICKS_CLIENT_SECRET: "", + } as any), + ).toBe(false); + + expect( + isRemoteTunnelAllowedByEnv({ + NODE_ENV: "production", + DATABRICKS_CLIENT_SECRET: "x", + DISABLE_REMOTE_SERVING: "true", + } as any), + ).toBe(false); + + expect( + isRemoteTunnelAllowedByEnv({ + NODE_ENV: "production", + DATABRICKS_CLIENT_SECRET: "x", + DISABLE_REMOTE_SERVING: "false", + } as any), + ).toBe(true); + }); + + test("hasDevQuery returns true for ?dev (blank) and ?dev=true and ?dev=", () => { + expect(hasDevQuery({ query: {} } as any)).toBe(false); + expect(hasDevQuery({ query: { dev: "" } } as any)).toBe(true); + expect(hasDevQuery({ query: { dev: "true" } } as any)).toBe(true); + expect(hasDevQuery({ query: { dev: "abcd1234" } } as any)).toBe(true); + }); + + test("isRemoteTunnelAssetRequest matches known vite prefixes", () => { + expect( + isRemoteTunnelAssetRequest({ + originalUrl: "/@vite/client", + } as any), + ).toBe(true); + expect( + isRemoteTunnelAssetRequest({ + originalUrl: "/node_modules/.vite/deps/react.js?v=1", + } as any), + ).toBe(true); + expect( + isRemoteTunnelAssetRequest({ + originalUrl: "/api/health", + } as any), + ).toBe(false); + }); +}); diff --git a/packages/app-kit/src/server/tests/server.test.ts b/packages/app-kit/src/server/tests/server.test.ts index 9fc9f90..601d17b 100644 --- a/packages/app-kit/src/server/tests/server.test.ts +++ b/packages/app-kit/src/server/tests/server.test.ts @@ -1,34 +1,48 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; // Use vi.hoisted for mocks that need to be available before module loading -const { mockHttpServer, mockExpressApp, mockIsRemoteServerEnabled } = - vi.hoisted(() => { - const httpServer = { - close: vi.fn((cb: any) => cb?.()), - on: vi.fn(), - address: vi.fn().mockReturnValue({ port: 8000 }), - }; - - const expressApp = { - use: vi.fn().mockReturnThis(), - get: vi.fn().mockReturnThis(), - listen: vi.fn((_port: any, _host: any, cb: any) => { - cb?.(); - return httpServer; - }), - _router: { - stack: [] as any[], - }, - }; - - const isRemoteServerEnabled = vi.fn().mockReturnValue(false); - - return { - mockHttpServer: httpServer, - mockExpressApp: expressApp, - mockIsRemoteServerEnabled: isRemoteServerEnabled, - }; - }); +const { + mockHttpServer, + mockExpressApp, + mockRemoteTunnelControllerMiddleware, + mockRemoteTunnelControllerInstance, +} = vi.hoisted(() => { + const httpServer = { + close: vi.fn((cb: any) => cb?.()), + on: vi.fn(), + address: vi.fn().mockReturnValue({ port: 8000 }), + }; + + const expressApp = { + use: vi.fn().mockReturnThis(), + get: vi.fn().mockReturnThis(), + listen: vi.fn((_port: any, _host: any, cb: any) => { + cb?.(); + return httpServer; + }), + _router: { + stack: [] as any[], + }, + }; + + const remoteTunnelControllerMiddleware = vi.fn( + (_req: any, _res: any, next: any) => next(), + ); + const remoteTunnelControllerInstance = { + middleware: remoteTunnelControllerMiddleware, + setServer: vi.fn(), + cleanup: vi.fn(), + isAllowedByEnv: vi.fn().mockReturnValue(false), + isActive: vi.fn().mockReturnValue(false), + }; + + return { + mockHttpServer: httpServer, + mockExpressApp: expressApp, + mockRemoteTunnelControllerMiddleware: remoteTunnelControllerMiddleware, + mockRemoteTunnelControllerInstance: remoteTunnelControllerInstance, + }; +}); // Mock express vi.mock("express", () => { @@ -97,16 +111,10 @@ vi.mock("../static-server", () => ({ })), })); -vi.mock("../remote-tunnel-manager", () => ({ - RemoteTunnelManager: Object.assign( - vi.fn().mockImplementation(() => ({ - setServer: vi.fn(), - setup: vi.fn(), - setupWebSocket: vi.fn(), - cleanup: vi.fn(), - })), - { isRemoteServerEnabled: mockIsRemoteServerEnabled }, - ), +vi.mock("../remote-tunnel/remote-tunnel-controller", () => ({ + RemoteTunnelController: vi.fn().mockImplementation(() => { + return mockRemoteTunnelControllerInstance; + }), })); vi.mock("dotenv", () => ({ @@ -126,8 +134,9 @@ vi.mock("../utils", () => ({ })); import fs from "node:fs"; +import express from "express"; import { ServerPlugin } from "../index"; -import { RemoteTunnelManager } from "../remote-tunnel-manager"; +import { RemoteTunnelController } from "../remote-tunnel/remote-tunnel-controller"; import { StaticServer } from "../static-server"; import { ViteDevServer } from "../vite-dev-server"; @@ -237,53 +246,69 @@ describe("ServerPlugin", () => { expect(viteInstance.setup).toHaveBeenCalled(); }); - test("should setup StaticServer in production mode with valid static path", async () => { - process.env.NODE_ENV = "production"; - vi.mocked(fs.existsSync).mockReturnValue(true); - + test("should register RemoteTunnelController middleware and set server", async () => { const plugin = new ServerPlugin({ autoStart: false }); await plugin.start(); - expect(StaticServer).toHaveBeenCalled(); - const staticInstance = vi.mocked(StaticServer).mock.results[0].value; - expect(staticInstance.setup).toHaveBeenCalled(); + expect(RemoteTunnelController).toHaveBeenCalledTimes(1); + expect(mockExpressApp.use).toHaveBeenCalledWith( + mockRemoteTunnelControllerMiddleware, + ); + expect(mockRemoteTunnelControllerInstance.setServer).toHaveBeenCalledWith( + mockHttpServer, + ); }); - test("should not setup StaticServer when no static path found", async () => { + test("extendRoutes registers databricksClientMiddleware when plugin.requiresDatabricksClient=true", async () => { process.env.NODE_ENV = "production"; - vi.mocked(fs.existsSync).mockReturnValue(false); - const plugin = new ServerPlugin({ autoStart: false }); + const injectRoutes = vi.fn(); + const plugins: any = { + "needs-client": { + name: "needs-client", + requiresDatabricksClient: true, + injectRoutes, + }, + }; + const plugin = new ServerPlugin({ autoStart: false, plugins }); await plugin.start(); - expect(StaticServer).not.toHaveBeenCalled(); + const routerFn = (express as any).Router as ReturnType; + expect(routerFn).toHaveBeenCalledTimes(1); + const routerInstance = routerFn.mock.results[0].value; + + expect(routerInstance.use).toHaveBeenCalledWith(expect.any(Function)); + expect(injectRoutes).toHaveBeenCalledWith(routerInstance); + expect(mockExpressApp.use).toHaveBeenCalledWith( + "/api/needs-client", + routerInstance, + ); }); - test("should setup RemoteTunnelManager when remote serving is enabled", async () => { - mockIsRemoteServerEnabled.mockReturnValue(true); + test("should setup StaticServer in production mode with valid static path", async () => { + process.env.NODE_ENV = "production"; + vi.mocked(fs.existsSync).mockReturnValue(true); const plugin = new ServerPlugin({ autoStart: false }); await plugin.start(); - expect(RemoteTunnelManager).toHaveBeenCalled(); - const tunnelInstance = - vi.mocked(RemoteTunnelManager).mock.results[0].value; - expect(tunnelInstance.setServer).toHaveBeenCalled(); - expect(tunnelInstance.setup).toHaveBeenCalled(); - expect(tunnelInstance.setupWebSocket).toHaveBeenCalled(); + expect(StaticServer).toHaveBeenCalled(); + const staticInstance = vi.mocked(StaticServer).mock.results[0].value; + expect(staticInstance.setup).toHaveBeenCalled(); }); - test("should not setup RemoteTunnelManager when remote serving is disabled", async () => { - mockIsRemoteServerEnabled.mockReturnValue(false); + test("should not setup StaticServer when no static path found", async () => { + process.env.NODE_ENV = "production"; + vi.mocked(fs.existsSync).mockReturnValue(false); const plugin = new ServerPlugin({ autoStart: false }); await plugin.start(); - expect(RemoteTunnelManager).not.toHaveBeenCalled(); + expect(StaticServer).not.toHaveBeenCalled(); }); }); @@ -359,15 +384,94 @@ describe("ServerPlugin", () => { }); }); - describe("isRemoteServingEnabled", () => { - test("should return value from isRemoteServerEnabled util", () => { - mockIsRemoteServerEnabled.mockReturnValue(true); + describe("logStartupInfo", () => { + test("logs remote tunnel controller disabled when missing", () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const plugin = new ServerPlugin({ autoStart: false }); + (plugin as any).remoteTunnelController = undefined; + + (plugin as any).logStartupInfo(); + + expect(logSpy).toHaveBeenCalledWith( + "Remote tunnel: disabled (controller not initialized)", + ); + logSpy.mockRestore(); + }); + + test("logs remote tunnel allowed/active when controller present", () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); const plugin = new ServerPlugin({ autoStart: false }); + (plugin as any).remoteTunnelController = { + isAllowedByEnv: () => true, + isActive: () => true, + }; + + (plugin as any).logStartupInfo(); + + expect( + logSpy.mock.calls.some((c) => + String(c[0]).includes("Remote tunnel: allowed; active"), + ), + ).toBe(true); + logSpy.mockRestore(); + }); + }); + + describe("findStaticPath", () => { + test("returns first matching static path and logs it", () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + vi.mocked(fs.existsSync).mockImplementation((p: any) => { + return String(p).endsWith("dist/index.html"); + }); + + const p = (ServerPlugin as any).findStaticPath(); + expect(String(p)).toContain("dist"); + expect( + logSpy.mock.calls.some((c) => + String(c[0]).includes("Static files: serving from"), + ), + ).toBe(true); + logSpy.mockRestore(); + }); + }); + + describe("_gracefulShutdown", () => { + test("aborts plugin operations (with error isolation) and closes server", async () => { + vi.useFakeTimers(); + const exitSpy = vi + .spyOn(process, "exit") + .mockImplementation(((_code?: number) => undefined) as any); + const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + const plugin = new ServerPlugin({ + autoStart: false, + plugins: { + ok: { + name: "ok", + abortActiveOperations: vi.fn(), + } as any, + bad: { + name: "bad", + abortActiveOperations: vi.fn(() => { + throw new Error("boom"); + }), + } as any, + }, + }); + + // pretend started + (plugin as any).server = mockHttpServer; + + await (plugin as any)._gracefulShutdown(); + vi.runAllTimers(); - expect(plugin.isRemoteServingEnabled()).toBe(true); + expect(errSpy).toHaveBeenCalled(); + expect(mockHttpServer.close).toHaveBeenCalled(); + expect(exitSpy).toHaveBeenCalled(); - mockIsRemoteServerEnabled.mockReturnValue(false); - expect(plugin.isRemoteServingEnabled()).toBe(false); + errSpy.mockRestore(); + exitSpy.mockRestore(); + vi.useRealTimers(); }); }); }); diff --git a/packages/app-kit/src/server/tests/utils.test.ts b/packages/app-kit/src/server/tests/utils.test.ts new file mode 100644 index 0000000..ffcaa60 --- /dev/null +++ b/packages/app-kit/src/server/tests/utils.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, test, vi } from "vitest"; + +const { mockExistsSync, mockReaddirSync } = vi.hoisted(() => ({ + mockExistsSync: vi.fn(), + mockReaddirSync: vi.fn(), +})); + +vi.mock("node:fs", () => ({ + default: { + existsSync: mockExistsSync, + readdirSync: mockReaddirSync, + }, +})); + +import { + generateTunnelIdFromEmail, + getQueries, + getRoutes, + parseCookies, +} from "../utils"; + +describe("server/utils", () => { + test("parseCookies returns {} when no cookie header", () => { + const req = { headers: {} } as any; + expect(parseCookies(req)).toEqual({}); + }); + + test("parseCookies parses a single cookie (fast path)", () => { + const req = { headers: { cookie: "a=b" } } as any; + expect(parseCookies(req)).toEqual({ a: "b" }); + }); + + test("parseCookies parses multiple cookies", () => { + const req = { headers: { cookie: "a=b; c=d; e=f" } } as any; + expect(parseCookies(req)).toEqual({ a: "b", c: "d", e: "f" }); + }); + + test("generateTunnelIdFromEmail is deterministic and 8 chars", () => { + const id1 = generateTunnelIdFromEmail("x@y.com"); + const id2 = generateTunnelIdFromEmail("x@y.com"); + expect(id1).toBe(id2); + expect(id1).toHaveLength(8); + }); + + test("generateTunnelIdFromEmail returns undefined for empty input", () => { + expect(generateTunnelIdFromEmail(undefined)).toBeUndefined(); + }); + + test("getRoutes returns flat + nested router routes with proper base path", () => { + const stack: any[] = [ + { + route: { + path: "/health", + methods: { get: true }, + }, + }, + { + name: "router", + handle: { + stack: [ + { + route: { + path: "/echo", + methods: { post: true }, + }, + }, + ], + }, + regexp: { + // Express-style source is usually "^\\/api\\/?(?=\\/|$)" + source: "^\\/api\\/?(?=\\/|$)", + }, + }, + ]; + + expect(getRoutes(stack)).toEqual([ + { path: "/health", methods: ["GET"] }, + { path: "/api/echo", methods: ["POST"] }, + ]); + }); + + test("getQueries returns {} when queries folder does not exist", () => { + mockExistsSync.mockReturnValue(false); + expect(getQueries("/cfg")).toEqual({}); + }); + + test("getQueries returns sql basenames", () => { + mockExistsSync.mockReturnValue(true); + mockReaddirSync.mockReturnValue(["a.sql", "b.txt", "c.sql"]); + + expect(getQueries("/cfg")).toEqual({ a: "a", c: "c" }); + }); +}); diff --git a/packages/app-kit/tsdown.config.ts b/packages/app-kit/tsdown.config.ts index 6c51e27..be6aa68 100644 --- a/packages/app-kit/tsdown.config.ts +++ b/packages/app-kit/tsdown.config.ts @@ -26,16 +26,16 @@ export default defineConfig([ tsconfig: "./tsconfig.json", copy: [ { - from: "src/server/index.html", - to: "dist/server/index.html", + from: "src/server/remote-tunnel/index.html", + to: "dist/server/remote-tunnel/index.html", }, { - from: "src/server/wait.html", - to: "dist/server/wait.html", + from: "src/server/remote-tunnel/wait.html", + to: "dist/server/remote-tunnel/wait.html", }, { - from: "src/server/denied.html", - to: "dist/server/denied.html", + from: "src/server/remote-tunnel/denied.html", + to: "dist/server/remote-tunnel/denied.html", }, ], },