Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 76 additions & 2 deletions packages/devframe/src/adapters/_shared.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { DevframeDefinition, DevframeDeploymentKind } from '../types/devframe'
import { cleanDoubleSlashes, withLeadingSlash, withTrailingSlash } from 'ufo'
import type { ConnectionMeta } from '../types/context'
import type { DevframeDefinition, DevframeDeploymentKind, McpRouteOptions } from '../types/devframe'
import { getPort } from 'get-port-please'
import { cleanDoubleSlashes, withLeadingSlash, withoutLeadingSlash, withTrailingSlash } from 'ufo'
import { DEVFRAME_MCP_ROUTE } from '../constants'

const DEFAULT_PORT = 9999

/**
* Resolve the mount base path for a devframe's SPA. Hosted adapters
Expand All @@ -18,3 +23,72 @@ export function resolveBasePath(def: DevframeDefinition, kind: DevframeDeploymen
export function normalizeBasePath(base: string): string {
return cleanDoubleSlashes(withTrailingSlash(withLeadingSlash(base)))
}

export interface ResolveDevServerPortOptions {
/** Bind host (passed to `get-port-please` for in-use detection). */
host?: string
/** Override the preferred port. Default: `def.cli?.port ?? 9999`. */
defaultPort?: number
}

/**
* Resolve the listening port for `createDevServer` (and `createHandler`'s
* side-car tiers), honoring the definition's `cli.port` / `cli.portRange` /
* `cli.random` settings. Exposed separately so authors who run their own
* argv parsing can resolve a port up-front (to print it, log it, etc.)
* before starting the server.
*/
export async function resolveDevServerPort(
def: DevframeDefinition,
options: ResolveDevServerPortOptions = {},
): Promise<number> {
const host = options.host ?? def.cli?.host ?? 'localhost'
const port = options.defaultPort ?? def.cli?.port ?? DEFAULT_PORT
// Only include optional fields when set — `get-port-please` spreads
// user options over its defaults, so `portRange: undefined` would
// wipe out the internal `[]` and crash on iteration.
const portOptions: Parameters<typeof getPort>[0] = { port, host }
if (def.cli?.portRange)
portOptions.portRange = def.cli.portRange
if (def.cli?.random)
portOptions.random = def.cli.random
return getPort(portOptions)
}

/**
* Normalize the `cli.mcp` / `mcp` option (`boolean | McpRouteOptions`) into
* concrete options, or `undefined` when the MCP route is disabled.
*/
export function resolveMcpConfig(mcp: boolean | McpRouteOptions | undefined): McpRouteOptions | undefined {
if (!mcp)
return undefined
return mcp === true ? {} : mcp
}

/**
* Resolve the `mcp` entry a `__connection.json` should advertise for a dev
* server started with the given `mcp` option (falling back to `def.cli?.mcp`,
* exactly like `createDevServer`), or `undefined` when the route is
* disabled.
*
* Hosted bridges that hand-roll their connection meta pass the side-car
* `port`: the advertised path becomes absolute (the side-car mounts at `/`)
* and the client dials `<page-host>:<port><path>`. Without `port` the path
* stays relative, resolved against `__connection.json`'s own location (the
* same-server default).
*
* @experimental
*/
export function resolveMcpConnectionMeta(
def: DevframeDefinition,
mcp: boolean | McpRouteOptions | undefined,
port?: number,
): ConnectionMeta['mcp'] {
const config = resolveMcpConfig(mcp ?? def.cli?.mcp)
if (!config)
return undefined
const route = withoutLeadingSlash(config.path ?? DEVFRAME_MCP_ROUTE)
return port != null
? { path: withLeadingSlash(route), port }
: { path: route }
}
Loading
Loading