Source
Use Source when server code needs read-only content from files, globs, Markdown, GitHub, MCP resources, or a custom loader.
Source retrieves content but doesn't place it in a persistent file tree. Bind a Source to Workspace when the content needs paths, sync, snapshots, rules, or agent access.
Quick start
Install
pnpm add vite-hub
Configure
import { defineSources, registerSources } from 'vite-hub/source'
import { file } from 'vite-hub/source/file'
export const sources = defineSources({
readme: file('README.md'),
})
registerSources(sources)
Start using it
import '../sources'
import { useSource } from 'vite-hub/source'
export default defineEventHandler(() => {
return useSource('readme').read('README.md')
})
Public imports
| Import | Use |
|---|---|
defineSource, defineSources, createSource, combineSources, custom from vite-hub/source | Define Sources, create context-dependent readers, and combine keyed readers. |
defineCollection, table from vite-hub/source, useCollection from vite-hub/source/client | Turn a table or custom loader into a typed, paginated HTTP read model and consume it from Vue. |
useDatabase from vite-hub/database/drizzle | Access a discovered database and its generated schema. |
registerSource, registerSources, clearSources, getRegisteredSource, useSource from vite-hub/source | Manage and read the process-local Source registry. |
file, glob, github, markdown, mcpResources from the matching vite-hub/source/* subpath | Select one built-in loader and its private implementation closure. |
defineContent, contentSource from vite-hub/source/content | Define the Comark Content runtime from registered ViteHub Sources or adapt one reader explicitly. |
createContentClient from vite-hub/source/content/client | Use Comark Content's typed runtime client. |
getViteHubErrorShape from vite-hub/runtime | Inspect registry, path, and loader failures by SOURCE_* code. |
Source, Source Reader, Source Item, revision, cache, and error types are exported from vite-hub/source. Loader option types live beside their implementation subpath. Libraries that install the package directly can use the matching @vite-hub/source paths.
Register Sources
Use vite-hub/source when you want a direct retrieval registry.
import { defineSources, registerSources } from 'vite-hub/source'
import { file } from 'vite-hub/source/file'
import { github } from 'vite-hub/source/github'
export const sources = defineSources({
readme: file('README.md'),
docs: github({
repo: 'acme/docs',
ref: 'main',
root: 'docs',
include: ['**/*.md'],
}),
})
registerSources(sources)
Named Source Loader imports are the public authoring shape. Import the helpers you need directly.
Source has no discovery or Vite Integration by itself. Import the module that registers Sources before calling useSource() in a process.
Source loader options
| Loader | Key options | Nuance |
|---|---|---|
file(input) | A path string, { path, workspacePath?, mediaType? }, or inline { workspacePath, content, mediaType? }. | Reads one file from the Source Context root. workspacePath controls the Source key. |
markdown(options) | { path, workspacePath?, mediaType? } or inline { workspacePath, content, mediaType? }. | Uses the file() contract with text/markdown as the default media type. Unlike file(), it requires an options object. |
glob(options) | include, cwd, ignore, dot, followSymlinks, keyCache, prefix. | Expands local files with tinyglobby; keyCache: false refreshes keys on each read path. |
github(options) | repo, ref, root, auth, include, exclude, cache. | Retrieves repository archive content. auth can be a token string or a trusted callback. |
mcpResources(options) | server, include, exclude, path, request, cache. | Reads MCP Resource content. server can be a client, client config, or resolver. |
custom(source) | A Source object. | Use when the built-in loaders do not match the origin contract. |
Cache options
github(), mcpResources(), and custom Sources can expose a cache policy; false disables it. GitHub applies the policy to its own ref, archive, and metadata caches. Workspace can also consume the same policy when it decides whether materialized Source content is fresh.
| Option | Type | Default | Description |
|---|---|---|---|
maxAge | number | Consumer default | Maximum cache age in seconds. Workspace uses this value when deciding whether materialized Source content is still fresh. |
Source object contract
A custom Source implements the retrieval behavior directly.
| Field | Type | Description |
|---|---|---|
name | string | Loader name used in errors and metadata. |
cache | false or SourceCacheOptions | Optional cache policy. |
fingerprint | unknown | Cache identity for origin state. |
resolveRevision(ctx) | function | Optionally pins a mutable origin ref to one revision before any other operation. |
prepare(ctx) | function | Optional prefetch or validation hook. |
getKeys(ctx) | function | Returns all addressable Source keys. |
getItem(key, ctx) | function | Returns a SourceItem for one key. |
getItems(ctx) | function | Optional bulk item reader. |
getMeta(key, ctx) | function | Optional metadata reader. |
getKeys() and getItem() are required. resolveRevision() and prepare() each run at most once for every useSource() reader before its first operation. The resolved revision is added to the shared context, so preparation, keys, items, and metadata observe the same origin snapshot. getItems() lets a consumer load all items in one call; getMeta() can return origin metadata without loading content. |
Source context
The caller supplies SourceContext to every custom Source method.
| Field | Type | Default | Description |
|---|---|---|---|
rootDir | string | process.cwd() for useSource() | Base project directory. |
sourceRootDir | string | None | Optional Source-specific root. Built-in local file loaders fall back to rootDir when it is absent. |
source | string | Registered Source name | Identifies the active Source. |
workspace | string | None | Identifies the Workspace consuming the Source. |
abortSignal | AbortSignal | None | Cancels in-flight work. Custom loaders must forward it to fetches and other abortable operations. |
revision | SourceRevision | None | The revision pinned by resolveRevision() for every later operation in this reader or Workspace lifecycle. |
Use it at runtime
Read a Source by name with useSource().
import '../sources'
import { useSource } from 'vite-hub/source'
export default defineEventHandler(async () => {
const readme = useSource('readme')
return {
text: await readme.read('README.md'),
}
})
Source reader API
| Method | Returns |
|---|---|
source.revision() | The pinned origin revision, when supported. |
source.keys() | All Source keys. |
source.get(key) | A SourceItem with content, data, media type, and metadata. |
source.read(key, options?) | Text by default, or Uint8Array with { encoding: 'binary' }. |
source.meta(key) | Metadata for one key, when the loader supports it. |
source.exists(key) | Whether a key exists. |
source.list(prefix?) | Direct child files and directories below a prefix. |
import '../sources'
import { useSource } from 'vite-hub/source'
export default defineEventHandler(async () => {
const docs = useSource('docs')
return {
files: await docs.keys(),
root: await docs.list(''),
}
})
Parse, search, and serve content at runtime
Install comark-content when Source output is documentation or application
content that should become a parsed runtime API:
pnpm add vite-hub comark-content
import sqlite from 'comark-content/database/sqlite-node'
import sqliteFullTextSearch from 'comark-content/plugins/sqlite-full-text-search'
import { defineContent } from 'vite-hub/source/content'
export const content = defineContent({
plugins: [sqliteFullTextSearch({ database: sqlite() })],
sources: {
docs: 'docs',
},
})
await content.get('/guide')
await content.navigation(['docs'])
await content.search(['docs'], 'runtime')
ViteHub discovers server/content.ts and serves its exported content instance
at /api/content/** in Vite and Nuxt. defineContent() delegates the runtime
contract to Comark and preserves methods contributed by its server plugins. No
manual framework route or fetch() wrapper is required.
Registered Source names, explicit Source Readers, and native Comark Content Sources can coexist in one definition. The ViteHub adapter gives each Comark cache refresh a new Source Reader, so a runtime can discover a newer origin revision without mixing revisions within one load.
Use sqlite-wasm where Node SQLite is unavailable. Comark Content owns parsed
document cache entries and exposes refresh(source), invalidate(key), and
expire(key). ViteHub therefore does not duplicate content parsing or ranked
search inside Source.
import { createContentClient } from 'vite-hub/source/content/client'
import searchClient from 'comark-content/plugins/sqlite-full-text-search/client'
export const content = createContentClient({
plugins: [searchClient()],
})
await content.search(['docs'], 'runtime')
Workspace keeps its filesystem search because it searches every visible file, including generated and non-content files. Collections also remain distinct: they are typed, paginated application read models over records, while Comark Content exposes parsed document manifests and content APIs.
Combine keyed Source readers
Use combineSources() when several readers can return the same key. A
combined reader identifies each item with a [source, key] tuple, so the source
alias remains part of the runtime value and its inferred type.
import { combineSources, createSource, defineSource } from 'vite-hub/source'
const github = defineSource(context => ({
async get(month: `${number}-${number}`) {
return { month, rootDir: context.rootDir }
},
async items() {
return [{ key: '2026-07' as const }]
},
}))
export const recaps = combineSources({
sources: {
github: createSource(github, { rootDir: process.cwd() }),
},
})
await recaps.get(['github', '2026-07'])
await recaps.items()
// [{ key: '2026-07', source: 'github', identity: ['github', '2026-07'] }]
Source aliases must be strings. get() infers the accepted key and result
for each alias. items() is available on every combined reader, but it rejects a
partially enumerable reader before starting any work. When every reader
implements items(), each returned item includes source and identity.
defineSource(context => reader) declares a context-dependent keyed reader.
createSource() creates that reader with a SourceContext. Combined readers do not
change the process-local registry: defineSources(), registerSources(), and
useSource() keep their existing behavior.
Expose a typed Collection
A Source describes where data comes from. A Collection describes the paginated object shape an application exposes to a client. For a discovered Drizzle database, let the database adapter own the keyset query:
import { eq } from 'drizzle-orm'
import * as v from 'valibot'
import { useDatabase } from 'vite-hub/database/drizzle'
import { defineCollection, table } from 'vite-hub/source'
const { db, schema } = useDatabase('default')
export const articles = defineCollection({
source: table({
db,
table: schema.articles,
orderBy: {
column: schema.articles.createdAt,
direction: 'desc',
tieBreaker: schema.articles.id,
},
defaultLimit: 25,
maxLimit: 100,
querySchema: v.object({ author: v.optional(v.string()) }),
where: ({ query, table }) => query.author
? eq(table.author, query.author)
: undefined,
}),
transform: article => ({ id: article.id, title: article.title }),
})
column and tieBreaker must be non-null columns on the selected table, and the
tie-breaker must be unique. The table source applies where before its lexicographic
cursor predicate, orders both columns consistently, requests the extra row, and
keeps the cursor opaque to clients. Omit querySchema and where when the
Collection has no filters.
Use defineCollection directly when the origin is a Source reader, SDK, HTTP
API, joined query, or another loader whose pagination is not a single Drizzle
table. In that escape hatch, the loader owns its origin-specific cursor logic.
import { defineCollection } from 'vite-hub/source'
import * as v from 'valibot'
export const articles = defineCollection(async ({ cursor, limit, query }) => {
return db.listArticles({ after: cursor, author: query.author, limit })
}, {
cursor: article => [article.createdAt, article.id] as const,
cursorSchema: v.tuple([v.number(), v.string()]),
defaultLimit: 25,
maxLimit: 100,
querySchema: v.object({ author: v.optional(v.string()) }),
transform: article => ({ id: article.id, title: article.title }),
})
The generic Collection requests one extra row from the loader, enforces its configured
limits, and turns the last visible row into an opaque cursor. transform() is
the server-to-client boundary, so private columns and provider objects stay out
of the response while its return type becomes the client item type. Any Standard
Schema validator can provide cursorSchema and querySchema; their output types
flow into the loader without manual generic annotations.
<script setup lang="ts">
const author = ref<string>()
const { items, pending, error, hasMore, loadMore } = useCollection('articles', {
filter: computed(() => ({ author: author.value })),
})
</script>
ViteHub discovers modules in server/collections and generates their type
registry and read-only GET routes. Each module exports a Collection with the
same name as its filename, so articles.ts exports articles and maps to
/api/articles. The Nuxt module auto-imports useCollection; outside Nuxt,
import it from vite-hub/source/client. Everything in server/collections is
public through its transformed shape; keep private definitions elsewhere and do
not create a matching server/api handler. Restart Nuxt after adding, removing,
or renaming a Collection module so Nitro rebuilds its handler manifest. Use
filter for validated request input. It stays
fixed while loadMore() advances the opaque cursor. For a bounded Collection,
set all: true to fetch every page asynchronously. cursor and limit are
reserved route query parameters. Invalid limits, cursor encodings, and parsed
filters return HTTP 400.
Use Sources with Workspace
Use Workspace Source Bindings when retrieved content needs to appear inside a persistent Workspace file tree.
import { defineWorkspace, file, github } from 'vite-hub/workspace'
export default defineWorkspace({
sources: {
readme: file('README.md'),
docs: github({
repo: 'acme/docs',
root: 'docs',
mount: 'docs',
materialize: 'lazy',
}),
},
})
The same loader names appear in both packages. Import them from vite-hub/source/* for direct retrieval through useSource(). Import them from vite-hub/workspace when retrieved items need Workspace paths, materialization, sync, validation, resolution, or access rules.
Provider output
Source has no Vite integration. By itself, it doesn't generate host output, provider config, or discovered Definitions.
Workspace and other consuming packages can wrap Sources in discovered Definitions, runtime registries, generated metadata, or Provider Output when they need placement, persistence, or deployment wiring.
Production checks
Sources are read-only. Read secrets for private origins from Server Env or trusted callbacks, not from model-authored input.
Use Workspace when content needs durable sync, path-scoped rules, diffs, snapshots, or scoped agent visibility. Use Source directly when server code only needs to retrieve and inspect items.
Next steps
- Learn the shared model in Workspace and Sources.
- Persist retrieved content through Workspace.
- Expose visible Workspace content to agents through Official capabilities.