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
2 changes: 2 additions & 0 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const alias = {
'devframe/utils/agent-tool-name': r('devframe/src/utils/agent-tool-name.ts'),
'devframe/utils/colors': r('devframe/src/utils/colors.ts'),
'devframe/utils/crypto-token': r('devframe/src/utils/crypto-token.ts'),
'devframe/utils/debounce': r('devframe/src/utils/debounce.ts'),
'devframe/utils/events': r('devframe/src/utils/events.ts'),
'devframe/utils/hash': r('devframe/src/utils/hash.ts'),
'devframe/utils/launch-editor': r('devframe/src/utils/launch-editor.ts'),
Expand All @@ -41,6 +42,7 @@ export const alias = {
'devframe/utils/shared-state': r('devframe/src/utils/shared-state.ts'),
'devframe/utils/streaming-channel': r('devframe/src/utils/streaming-channel.ts'),
'devframe/utils/structured-clone': r('devframe/src/utils/structured-clone.ts'),
'devframe/utils/url': r('devframe/src/utils/url.ts'),
'devframe/utils/when': r('devframe/src/utils/when.ts'),
'devframe/adapters/cac': r('devframe/src/adapters/cac.ts'),
'devframe/adapters/dev': r('devframe/src/adapters/dev.ts'),
Expand Down
3 changes: 1 addition & 2 deletions docs/app/pages/logos.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script setup lang="ts">
// Shadows the comark-docs layer's brand-assets page with devframe's assets.
import { joinURL } from 'ufo'
import logoSvg from '../../public/logo.svg?raw'

definePageMeta({
Expand All @@ -11,7 +10,7 @@ const title = 'Brand assets'
const description = 'Logos and assets for Devframe. Download SVG or PNG, or copy the raw SVG source directly.'

const site = useSiteConfig()
const canonicalUrl = computed(() => joinURL(site.url, '/logos'))
const canonicalUrl = computed(() => new URL('/logos', site.url).href)

useRobotsRule('index, follow')

Expand Down
25 changes: 25 additions & 0 deletions docs/content/8.references/8.utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ randomDigits(6) // '047204', uniform, leading zeros preserved
timingSafeEqual(input, secret) // constant-time string comparison
```

### `devframe/utils/debounce`

Promise-aware debounce, bundled in so integrations reuse it without adding a `perfect-debounce` dependency.

```ts
import { debounce } from 'devframe/utils/debounce'

const save = debounce((state: State) => write(state), 100)
save(next) // coalesces bursts; returns a promise, plus `.cancel()` / `.flush()` / `.isPending()`
```

### `devframe/utils/events`

Typed event emitter.
Expand Down Expand Up @@ -123,6 +134,20 @@ state.mutate((draft) => {
state.value() // => count is 1
```

### `devframe/utils/url`

URL path-joining and slash-normalization helpers (browser-safe, zero-dep).

```ts
import { joinURL, withBase, withProtocol, withTrailingSlash } from 'devframe/utils/url'

joinURL('/__devframe', '__renderers', 'vue.mjs') // '/__devframe/__renderers/vue.mjs'
withBase('meta.json', '/__devframe/') // '/__devframe/meta.json'
withProtocol('http://localhost:7777/ws', 'ws://') // 'ws://localhost:7777/ws'
```

Also exports `withLeadingSlash`, `withoutLeadingSlash`, `withoutTrailingSlash`, and `cleanDoubleSlashes`.

### `devframe/utils/streaming-channel`

Sink/reader primitives for streamed RPC payloads, via `ctx.rpc.streaming`; see [Streaming](/guide/streaming).
Expand Down
3 changes: 1 addition & 2 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
"comark-docs": "catalog:docs",
"d3-shape": "catalog:frontend",
"nuxt": "catalog:build",
"shiki": "catalog:deps",
"ufo": "catalog:deps"
"shiki": "catalog:deps"
},
"devDependencies": {
"@types/d3-shape": "catalog:types"
Expand Down
5 changes: 2 additions & 3 deletions packages/agentic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,11 @@
"dependencies": {
"@modelcontextprotocol/client": "catalog:deps",
"@modelcontextprotocol/server": "catalog:deps",
"@standard-schema/spec": "catalog:deps",
"h3": "catalog:deps",
"pathe": "catalog:deps",
"ufo": "catalog:deps"
"pathe": "catalog:deps"
},
"devDependencies": {
"@standard-schema/spec": "catalog:deps",
"@types/node": "catalog:types",
"devframe": "workspace:*",
"tsdown": "catalog:build",
Expand Down
2 changes: 1 addition & 1 deletion packages/agentic/src/connect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'
import { diagnostics, listLiveDevframeInstances, probeDevframeOrigin } from 'devframe/internal'
import { toAgentToolName } from 'devframe/utils/agent-tool-name'
import { Diagnostic } from 'devframe/utils/nostics'
import { joinURL } from 'ufo'
import { joinURL } from 'devframe/utils/url'

export interface ConnectServerOptions {
/**
Expand Down
3 changes: 3 additions & 0 deletions packages/agentic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ throw new Error(
+ ' • import from "devframe/adapters/mcp" to serve a devframe over MCP\n'
+ ' • run "devframe connect" for the stdio discovery gateway\n',
)

// An explicit empty module so dts emit has something to say about this entry.
export {}
11 changes: 6 additions & 5 deletions packages/devframe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"./utils/agent-tool-name": "./dist/utils/agent-tool-name.mjs",
"./utils/colors": "./dist/utils/colors.mjs",
"./utils/crypto-token": "./dist/utils/crypto-token.mjs",
"./utils/debounce": "./dist/utils/debounce.mjs",
"./utils/events": "./dist/utils/events.mjs",
"./utils/get-port": "./dist/utils/get-port.mjs",
"./utils/hash": "./dist/utils/hash.mjs",
Expand All @@ -62,6 +63,7 @@
"./utils/shared-state": "./dist/utils/shared-state.mjs",
"./utils/streaming-channel": "./dist/utils/streaming-channel.mjs",
"./utils/structured-clone": "./dist/utils/structured-clone.mjs",
"./utils/url": "./dist/utils/url.mjs",
"./utils/when": "./dist/utils/when.mjs",
"./package.json": "./package.json"
},
Expand Down Expand Up @@ -93,22 +95,21 @@
}
},
"dependencies": {
"@standard-schema/spec": "catalog:deps",
"birpc": "catalog:deps",
"crossws": "catalog:deps",
"h3": "catalog:deps",
"mrmime": "catalog:deps",
"nostics": "catalog:deps",
"pathe": "catalog:deps",
"ufo": "catalog:deps"
"pathe": "catalog:deps"
},
"devDependencies": {
"@devframes/agentic": "workspace:*",
"@standard-schema/spec": "catalog:deps",
"birpc": "catalog:deps",
"cac": "catalog:deps",
"get-port-please": "catalog:deps",
"immer": "catalog:deps",
"launch-editor": "catalog:deps",
"mlly": "catalog:build",
"mrmime": "catalog:deps",
"obug": "catalog:deps",
"ohash": "catalog:deps",
"p-limit": "catalog:deps",
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/adapters/_shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { AgenticMcpModule } from '../node/agentic'
import type { DevframeAgentHost } from '../types/agent'
import type { ConnectionMeta } from '../types/context'
import type { DevframeDefinition, DevframeDeploymentKind, McpAuthorization, McpSetting } from '../types/devframe'
import { cleanDoubleSlashes, withLeadingSlash, withoutLeadingSlash, withTrailingSlash } from 'devframe/utils/url'
import { getPort } from 'get-port-please'
import { cleanDoubleSlashes, withLeadingSlash, withoutLeadingSlash, withTrailingSlash } from 'ufo'
import { DEVFRAME_MCP_ROUTE } from '../constants'
import { importAgenticMcp, isAgenticInstalled, warnAgenticMcpMissingOnce } from '../node/agentic'

Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/adapters/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import type { StaticAssetsSource } from '../types/remote-assets'
import type { DevframeNodeRpcSession, DevframeNodeRpcSessionMeta } from '../types/rpc'
import { createServer } from 'node:http'
import { open } from 'devframe/utils/open'
import { withBase } from 'devframe/utils/url'
import { H3, toNodeHandler } from 'h3'
import { withBase } from 'ufo'
import { diagnostics } from '../node/diagnostics'
import { normalizeHttpServerUrl } from '../node/utils'
import { normalizeBasePath, resolveBasePath, resolveDevServerPort } from './_shared'
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/adapters/initiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import type { ResolvedMcpConfig } from './_shared'
import process from 'node:process'
import { resolveStaticAssetsSource } from 'devframe/utils/remote-assets'
import { mountStaticHandler } from 'devframe/utils/serve-static'
import { joinURL, withoutLeadingSlash } from 'devframe/utils/url'
import { H3 } from 'h3'
import { resolve } from 'pathe'
import { joinURL, withoutLeadingSlash } from 'ufo'
import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_MCP_ROUTE } from '../constants'
import { importAgenticMcp } from '../node/agentic'
import { createHostContext } from '../node/context'
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/client/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
DEVFRAME_VIEWER_ORIGIN_QUERY_PARAM,
DEVFRAME_VIEWER_ORIGIN_TOKEN_QUERY_PARAM,
} from 'devframe/constants'
import { withBase } from 'ufo'
import { withBase } from 'devframe/utils/url'
import {
readStoredAuthToken,
readStoredConnection,
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/client/rpc-ws.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ConnectionMeta, EventEmitter } from 'devframe/types'
import type { DevframeClientRpcHost, DevframeRpcClientMode, DevframeRpcClientOptions, RpcClientEvents } from './rpc'
import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client'
import { withProtocol } from 'ufo'
import { withProtocol } from 'devframe/utils/url'
import { createLiveRpcClientMode } from './rpc-live'

export interface CreateWsRpcClientModeOptions {
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/client/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { DevframeScopedClientContext } from './scope'
import { DEVFRAME_OTP_URL_PARAM } from 'devframe/constants'
import { RpcCacheManager, RpcFunctionsCollectorBase } from 'devframe/rpc'
import { createEventEmitter } from 'devframe/utils/events'
import { withBase } from 'ufo'
import { withBase } from 'devframe/utils/url'
import { setupDevframeConnection } from './connection'
import { storeAuthToken } from './connection-storage'
import { authenticateWithUrlOtp } from './otp'
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/node/instance-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import type { ContextRpcServer } from './rpc-core'
import { createServer } from 'node:http'
import process from 'node:process'
import { validateOriginCandidate } from 'devframe/utils/origin'
import { joinURL, withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from 'devframe/utils/url'
import { defineHandler, H3 as H3App, toNodeHandler } from 'h3'
import { joinURL, withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from 'ufo'
import { DEVFRAME_SSE_ROUTE, DEVFRAME_WS_ROUTE } from '../constants'
import { createInteractiveAuth } from '../recipes/interactive-auth'
import { diagnostics } from './diagnostics'
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/node/storage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import fs from 'node:fs'
import process from 'node:process'
import { debounce } from 'devframe/utils/debounce'
import { createSharedState } from 'devframe/utils/shared-state'
import { dirname } from 'pathe'
import { debounce } from 'perfect-debounce'
import { diagnostics } from './diagnostics'

export interface CreateStorageOptions<T extends object> {
Expand Down
8 changes: 8 additions & 0 deletions packages/devframe/src/utils/debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Promise-aware debounce, bundled in so any integration can reuse it through
* `devframe/utils/debounce` without adding its own `perfect-debounce`
* dependency. Thin re-export of `perfect-debounce`'s `debounce`; the bytes
* live once in devframe's dist.
*/
export type { DebouncedReturn, DebounceOptions } from 'perfect-debounce'
export { debounce } from 'perfect-debounce'
17 changes: 15 additions & 2 deletions packages/devframe/src/utils/nostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,26 @@ export function defineDiagnostics<
docsBase?: string | ((code: keyof Codes) => string | undefined)
codes: Codes
reporters?: Reporters
}): Diagnostics<Codes, readonly [typeof devframeReporter, ...Reporters]> {
}): DevframeDiagnostics<Codes, Reporters> {
return defineNosticsDiagnostics({
...options,
reporters: [devframeReporter, ...(options.reporters ?? [])],
}) as Diagnostics<Codes, readonly [typeof devframeReporter, ...Reporters]>
}) as DevframeDiagnostics<Codes, Reporters>
}

/**
* The `Diagnostics` object returned by devframe's {@link defineDiagnostics}
* (devframe's ANSI console reporter is always prepended). Integrations that
* export their own `defineDiagnostics(...)` result as public API annotate it
* with this type, so the reference resolves through `devframe/utils/nostics`
* rather than inlining `nostics`'s types or taking a direct `nostics`
* dependency.
*/
export type DevframeDiagnostics<
Codes extends Record<string, DiagnosticDefinition>,
Reporters extends readonly AnyDiagnosticReporter[] = [],
> = Diagnostics<Codes, readonly [typeof devframeReporter, ...Reporters]>

export {
createConsoleReporter,
defineProdDiagnostics,
Expand Down
8 changes: 8 additions & 0 deletions packages/devframe/src/utils/simple-schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import type { StandardSchemaV1 } from '@standard-schema/spec'

/**
* Re-exported [Standard Schema](https://standardschema.dev/) contract types so
* integrations can type `args`/`returns`/view schemas against Standard Schema
* through the devframe peer, without taking their own `@standard-schema/spec`
* dependency (devframe inlines the declarations into its own `.d.mts`).
*/
export type { StandardSchemaV1 } from '@standard-schema/spec'

/**
* A tiny, zero-dependency [Standard Schema](https://standardschema.dev/)
* builder.
Expand Down
63 changes: 63 additions & 0 deletions packages/devframe/src/utils/url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, it } from 'vitest'
import {
cleanDoubleSlashes,
joinURL,
withBase,
withLeadingSlash,
withoutLeadingSlash,
withoutTrailingSlash,
withProtocol,
withTrailingSlash,
} from './url'

describe('utils/url', () => {
it('adds and removes leading slashes', () => {
expect(withLeadingSlash('base')).toBe('/base')
expect(withLeadingSlash('/base')).toBe('/base')
expect(withLeadingSlash('')).toBe('/')
expect(withoutLeadingSlash('/base')).toBe('base')
expect(withoutLeadingSlash('base')).toBe('base')
expect(withoutLeadingSlash('/')).toBe('/')
})

it('adds and removes trailing slashes', () => {
expect(withTrailingSlash('base')).toBe('base/')
expect(withTrailingSlash('base/')).toBe('base/')
expect(withoutTrailingSlash('base/')).toBe('base')
expect(withoutTrailingSlash('base')).toBe('base')
expect(withoutTrailingSlash('/')).toBe('/')
})

it('cleans double slashes but keeps the protocol seam', () => {
expect(cleanDoubleSlashes('//base//sub//')).toBe('/base/sub/')
expect(cleanDoubleSlashes('http://example.com//base//')).toBe('http://example.com/base/')
expect(cleanDoubleSlashes('/base/')).toBe('/base/')
})

it('joins segments with single slashes', () => {
expect(joinURL('/', '__mcp')).toBe('/__mcp')
expect(joinURL('/base', 'route')).toBe('/base/route')
expect(joinURL('/base/', '/route')).toBe('/base/route')
expect(joinURL('/base', './route')).toBe('/base/route')
expect(joinURL('/base', '__renderers', 'vue.mjs')).toBe('/base/__renderers/vue.mjs')
expect(joinURL('', 'route')).toBe('route')
expect(joinURL('/base', '')).toBe('/base')
expect(joinURL('/base', '/')).toBe('/base')
expect(joinURL('http://localhost:3000', '/__devframe/')).toBe('http://localhost:3000/__devframe/')
})

it('prefixes with a base', () => {
expect(withBase('meta.json', '/__devframe/')).toBe('/__devframe/meta.json')
expect(withBase('/__devframe/meta.json', '/__devframe/')).toBe('/__devframe/meta.json')
expect(withBase('meta.json', '/')).toBe('meta.json')
expect(withBase('meta.json', '')).toBe('meta.json')
expect(withBase('http://example.com/x', '/base')).toBe('http://example.com/x')
expect(withBase('/__devframe/', 'http://localhost:5173')).toBe('http://localhost:5173/__devframe/')
})

it('swaps protocols', () => {
expect(withProtocol('http://localhost:3000/ws', 'ws://')).toBe('ws://localhost:3000/ws')
expect(withProtocol('https://example.com/ws', 'wss://')).toBe('wss://example.com/ws')
expect(withProtocol('localhost:3000/ws', 'ws://')).toBe('ws://localhost:3000/ws')
})
})
Loading
Loading