import type http from "node:http"; import type { IRouter } from "express"; import type { Plugin, UserConfig } from "vite"; import type { ViteDevServer } from "vite"; import { RESTART_EXIT_CODE } from "@superblocksteam/library-shared/restart-contract"; import type { ServerError } from "@superblocksteam/library-shared/types"; import { type ITokenManager } from "@superblocksteam/util"; import { type FileSyncPluginParams } from "@superblocksteam/vite-plugin-file-sync"; import type { AiService, SnapshotManager } from "@superblocksteam/vite-plugin-file-sync/ai-service"; import type { GitService } from "@superblocksteam/vite-plugin-file-sync/git-service"; import type { LockService } from "@superblocksteam/vite-plugin-file-sync/lock-service"; import type { OperationQueue } from "@superblocksteam/vite-plugin-file-sync/operation-queue"; import type { SyncService } from "@superblocksteam/vite-plugin-file-sync/sync-service"; import type { SuperblocksSdk } from "../sdk.js"; import { getLogger } from "../telemetry/logging.js"; import { type FatalProcessHandler } from "./fatal-process-barrier.mjs"; type BranchValidationResponse = { locals: Record; status: (statusCode: number) => { send: (body: string) => unknown; }; }; /** * Validates the `x-superblocks-branch` header on `/_sb_connect` against the * branch this dev server writes to (the runtime DBFS branch), not the git * checkout. `gitDisconnect` moves that branch back to `main` and git * activation moves it to `superblocks/live`, so it tracks transfers in both * directions without a git subprocess per request. * * `superblocks/live` is always accepted: on a reused pod, git activation runs * after the editor socket authenticates, so during a forward transfer the * client legitimately sends `superblocks/live` while the pod is still on the * source branch. */ export declare function validateConnectBranchHeader({ expectedBranch, incomingBranch, isLocal, response, }: { expectedBranch: string | undefined; incomingBranch: string | undefined; isLocal: boolean; response: BranchValidationResponse; }): boolean; export declare function getDevServerTracer(): import("@opentelemetry/api").Tracer; /** * True when a request must be answered even if Vite never initialized, so the * hold middleware lets it through. `DEV_SERVER_ENDPOINTS` says which endpoints * qualify and why. * * Compares the path, not the raw URL: the editor polls `/_sb_health?t=` * with a cache-buster, so a raw-URL comparison drops that poll into the hold. */ export declare function isDevServerControlPath(url: string | undefined): boolean; /** * Vite `resolve` config shared by both the activation and pre-warm servers. * * Applies the `react/compiler-runtime` → shim alias via `reactCompilerRuntimeAlias()`. * Must stay identical across both `createServer()` calls — Vite hashes this * config into the dep-optimization cache (see OPTIMIZE_DEPS_CONFIG note). */ export declare function buildResolveConfig(): UserConfig["resolve"]; /** * Builds the Vite plugin list shared by both pre-warm and activation. * * Vite hashes plugin names to validate the dep optimization cache, so * both paths must produce the same list. Call `fileSyncVitePlugin()` * with no args for the noop version during pre-warm. */ export declare function vitePlugins(root: string, opts: { fileSync: Plugin[]; ddRum?: { clientToken: string; applicationId: string; env: string; version: string; }; }): (import("vite").PluginOption[] | Plugin | { readonly name: "vite-tsconfig-paths"; readonly enforce: "pre"; readonly configResolved: (config: Pick) => void; readonly configureServer: (server: Pick) => void; readonly buildStart: () => void; readonly resolveId: (this: Pick, id: string, importer: string | undefined, options: {}) => Promise; })[]; export declare function viteCacheDir(root: string): string; /** * Deletes the Vite dependency cache so the next start rebuilds it. * * Call this when a workspace restore stops partway. tar may have written * `_metadata.json` but not every chunk it points to, and Vite only checks the * hash in `_metadata.json`, not that the chunks exist. A half-written cache * would keep failing dependency imports until the folder is gone. */ export declare function discardViteCache(root: string): void; /** * Build tar --exclude flags for .superblocks/ subdirectories that are NOT * in the safelist. If .superblocks/ doesn't exist we exclude it entirely. */ export declare function getSuperblocksTarExcludes(root: string): string[]; export declare function createWorkspacePersistArchive(root: string): Promise>; export declare function createWorkspacePersistUploadHeaders(archiveLength: number, uploadHeaders?: Record): { [k: string]: string; }; export declare function isWorkspacePersistUploadHeaders(uploadHeaders: unknown): uploadHeaders is Record | undefined; export { RESTART_EXIT_CODE }; export declare function createCheckAuthorization({ applicationId, logger, superblocksBaseUrl, }: { applicationId: string; logger: ReturnType; superblocksBaseUrl?: string; }): FileSyncPluginParams["checkAuthorization"]; interface CreateDevServerOptions { root: string; mode: "development" | "production"; port?: number; fsOperationQueue: OperationQueue; syncService?: SyncService; lockService?: LockService; tokenManager?: ITokenManager; aiService?: AiService; gitService?: GitService; gitBootstrapAuthFailed?: boolean; gitBootstrapFailed?: boolean; activateGitService?: () => Promise; refreshGitCredentials?: () => Promise; snapshotManager?: SnapshotManager; logger?: (...messages: unknown[]) => void; sdk?: SuperblocksSdk; /** Explicitly provided base URL from the CLI; preferred over auth-file reads. */ superblocksBaseUrl?: string; /** * Pre-existing HTTP server from warm standby mode. When provided, the dev * server attaches its Express app as a request handler on this server instead * of creating a new one and calling listen(). This avoids the port gap that * causes 502s from the gateway during the warm -> full server transition. */ existingServer?: http.Server; /** * Wall-clock timestamp (Date.now()) when the warm-standby /_sb_activate POST * was accepted. When provided, the dev server records the elapsed time from * activation acceptance to full Express handler attachment as the * `dev_server_warm_activation_handoff_duration_ms` histogram — the window * during which the gateway can return 502s if the warm server stops * responding before the full server takes over. */ warmActivationStart?: number; /** * Server-side errors (e.g. dependency-install failures) to surface to the UI * via `/_sb_connect` and `/_sb_status`. Optional and read at request time, so * the caller can mutate the referenced object after `createDevServer` returns * and have later requests reflect the change. Treated as `{ serverErrors: [] }` * when absent. */ devServerStatus?: { serverErrors: ServerError[]; }; } /** * Merges the live `serverErrors` list onto a status/health payload returned by * `/_sb_connect` and `/_sb_status`. `devServerStatus` is optional and may omit * `serverErrors`; both cases yield an empty array so the UI always receives a * well-formed `serverErrors` field. */ export declare function buildStatusPayload(base: T, devServerStatus?: { serverErrors?: ServerError[]; }): T & { serverErrors: ServerError[]; }; type ProcessErrorHandler = FatalProcessHandler; /** * What the dev server needs of the lock service on its way out. Structural so a * test does not have to stand up a real one. */ type ExitingLockService = Pick; /** * Give the AppBranchLock to whoever should have it next, then let the caller * exit. * * Which of the two things to do is the dev server's knowledge, not the lock * service's — it used to be a `serverInitiated` flag passed down into a * `shutdown()` that branched on it, which is how a lock service came to have an * opinion about why a process was stopping. * * Normally this process is the one stopping, so the lock goes back: the * heartbeat dies with the process, and anything not released here makes the * replacement sandbox's `acquireLock()` fail until the server-side TTL expires * — and the replacement is the entire point of exiting. * * When the server closed us, the lock is already gone. `/_sb_disconnect` is * usually a session moving across local↔cloud, so releasing would take the lock * from whoever is taking over. The editor is told instead, because it has to * distinguish "the server closed you" from a socket dropping. * * Extracted so both branches are testable: `runGracefulShutdown` is a closure * over a booted dev server, so a unit test cannot otherwise reach either one. */ export declare function handOverLockOnExit(lockService: ExitingLockService | undefined, { serverInitiated, switchingTo, initiatedByEmail, }: { serverInitiated: boolean; switchingTo?: "local" | "cloud" | "none"; initiatedByEmail?: string; }): Promise; /** * Decides whether a process-level error keeps the pod alive, and performs the * recording for the cases that do. Returns `true` when the caller must return * early without taking the fatal-exit path. * * Extracted from the `uncaughtException` / `unhandledRejection` listeners so * this decision is testable: registered listeners are effectively unreachable * from unit tests, so an early `return` could be deleted with the whole suite * staying green while the pod died on every recoverable build error. */ export declare function handleNonFatalProcessError(params: { devServerStatus?: { serverErrors: ServerError[]; }; devServerMetrics: { recordRecoverableBuildError: (labels: { category: string; handler: ProcessErrorHandler; }) => void; recordRecoverableShutdownError: (handler: ProcessErrorHandler, reason: unknown) => void; }; handler: ProcessErrorHandler; logger: { warn: (message: string, meta?: unknown) => void; }; reason: unknown; }): boolean; /** * Stable per-process identity for the dev-server runtime. `bootId` is generated * once at module load, so it changes on every process start (including an * in-place restart). Together with an unchanged sandbox/port, these let an e2e * spec prove "same sandbox, new process" after a post-flip restart without log * scraping (testability hook for the post-flip restart e2e spec). */ export declare const DEV_SERVER_BOOT_ID: `${string}-${string}-${string}-${string}-${string}`; export declare const DEV_SERVER_STARTED_AT: string; export declare function mountDevServerJsonParsers(app: IRouter): void; /** * Builds the `/_sb_health` payload, including the per-process boot identity. * Extracted as a pure function so the boot-identity contract can be unit * tested without booting the full dev server. */ export declare function buildHealthResponse(args: { applicationId: string; branch: string | null | undefined; cliVersion: string; }): { status: "healthy"; applicationId: string; branch: string | null | undefined; cliVersion: string; bootId: string; startedAt: string; }; export declare function shouldForwardAiServiceEvents(aiServiceUrl?: string): boolean; export declare function getDevServerSocketPath(aiServiceUrl?: string): string; export declare function createDevServer({ root, mode, fsOperationQueue, syncService, lockService, tokenManager, aiService, gitService, gitBootstrapAuthFailed, gitBootstrapFailed, activateGitService, refreshGitCredentials, snapshotManager, logger: loggerOverride, port, sdk, superblocksBaseUrl: explicitBaseUrl, existingServer, warmActivationStart, devServerStatus, }: CreateDevServerOptions): Promise>; export declare function preWarmViteCache(root: string): Promise; //# sourceMappingURL=dev-server.d.mts.map