Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
3fb3ba8
feat: next demo
zce Dec 7, 2025
6da0174
feat: next demo
zce Dec 7, 2025
882a4da
feat: next demo
zce Dec 7, 2025
7f49e0d
feat: refactor file handling and parser integration
zce Dec 8, 2025
11f2012
feat: refactor file handling and parser integration
zce Dec 15, 2025
ad3f1d6
feat: refactor file handling and parser integration
zce Dec 15, 2025
468f391
feat: refactor file handling and parser integration
zce Dec 15, 2025
677bb4f
feat: integrate @velite/plugin-next into Next.js example
zce Dec 15, 2025
4d75ae0
feat: refactor file handling and parser integration
zce Dec 15, 2025
aa7c8dd
feat: refactor file handling and parser integration
zce Dec 15, 2025
0bf4f75
feat: refactor file handling and parser integration
zce Dec 15, 2025
d405ef2
fix: align zod 4 schema parsing
zce May 16, 2026
5104c7f
docs: add alpha migration guide
zce May 16, 2026
eddff30
chore: update zod format helpers
zce May 16, 2026
349bdc1
chore: update package version
zce May 16, 2026
047f6dd
Merge branch 'main' into next
zce Jun 17, 2026
b645b91
feat: enhance schema definitions and improve file handling in build p…
zce Jun 17, 2026
7a17fd5
feat: add initial documentation for AI workspace and session management
zce Jun 17, 2026
29dbb0c
feat: Add core architecture design and specs review documents
zce Jun 17, 2026
67b2ae1
Refactor core architecture (#385)
zce Jun 19, 2026
89820fc
Merge remote-tracking branch 'refs/remotes/origin/next' into next
zce Jun 19, 2026
5c32466
Implement feature X to enhance user experience and optimize performance
zce Jun 19, 2026
99025f1
refactor: simplify AssetStore by removing publicUrl and ownerFiles pr…
zce Jun 19, 2026
d164247
refactor: remove architecture review documents and implementation plan
zce Jun 19, 2026
f3df73d
feat: add lifecycle documentation and update schema handling
zce Jun 19, 2026
e94409f
test: use built CLI for basic fixture
zce Jun 19, 2026
18a1196
chore: release 1.0.0-alpha.3
zce Jun 19, 2026
b98456e
fix: support provenance publishing for plugins
zce Jun 19, 2026
4cffc47
feat: update type imports to use BuildOptions and improve cache handling
zce Jun 19, 2026
555a9fb
refactor: allow injected rebuild caches
zce Jun 19, 2026
4a492c2
refactor: tighten RebuildChange types
zce Jun 19, 2026
8ed1fc4
feat: reuse unaffected collections on rebuild
zce Jun 19, 2026
373a12d
fix: treat empty change paths as full resolve
zce Jun 19, 2026
e7e0c40
feat: engine scoped incremental rebuild
zce Jun 19, 2026
04e2997
refactor: keep incremental state binding const
zce Jun 19, 2026
ce8ad60
feat: cache asset processing results
zce Jun 19, 2026
c59834f
fix: register asset cache source synchronously
zce Jun 19, 2026
42551ff
feat: thread asset cache through parsing
zce Jun 19, 2026
93ed235
refactor: drop unused internal build context helper
zce Jun 19, 2026
a774135
feat: rebuild asset owners incrementally
zce Jun 19, 2026
eacee5f
chore: add incremental rebuild benchmark
zce Jun 19, 2026
e838738
refactor: simplify writer closure and drop dead re-export
zce Jun 20, 2026
60e8c43
refactor: simplify watcher and fix logger docs
zce Jun 20, 2026
4582253
test: adapt to simplified discover/state/logger APIs
zce Jun 20, 2026
606bfd3
refactor: simplify discover, state, logger, and engine
zce Jun 20, 2026
ca50479
refactor: move discover and path helpers to utils
zce Jun 20, 2026
8c9e32d
refactor: remove duplicate utils files
zce Jun 20, 2026
fa3d310
refactor: merge utils into single patterns module
zce Jun 20, 2026
2d550a2
fix: engine-scoped unique store, clear vfile messages, validate colle…
zce Jun 20, 2026
acf05e0
feat: implement unique value invalidation for incremental rebuilds
zce Jun 20, 2026
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
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
"explorer.fileNesting.enabled": true,
"files.exclude": {
"**/node_modules": true
},
Expand Down
95 changes: 79 additions & 16 deletions docs/guide/custom-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ export const title = defineSchema(() => s.string().min(1).max(100))
// for validating email
export const email = defineSchema(() => s.string().email({ message: 'Invalid email address' }))

// custom validation logic
export const hello = defineSchema(() =>
s.string().refine(value => {
if (value !== 'hello') {
return 'Value must be "hello"'
// custom validation logic using refine
export const hello = defineSchema(() => s.string().refine(value => value === 'hello', 'Value must be "hello"'))

// custom validation logic using superRefine (for more control)
export const customValidation = defineSchema(() =>
s.string().superRefine((value, ctx) => {
if (value.length < 5) {
ctx.addIssue({ code: 'custom', message: 'Value must be at least 5 characters' })
}
if (!value.includes('@')) {
ctx.addIssue({ code: 'custom', message: 'Value must contain @ symbol' })
}
return true
})
)
```
Expand All @@ -40,10 +45,28 @@ Refer to [Zod documentation](https://zod.dev) for more information about Zod.
```ts
import { defineSchema, s } from 'velite'

// for transforming title
// for transforming title (simple transform)
export const title = defineSchema(() => s.string().transform(value => value.toUpperCase()))

// ...
// for transforming with error handling (using ctx.addIssue)
export const safeTransform = defineSchema(() =>
s.string().transform((value, ctx) => {
try {
return value.toUpperCase()
} catch (err) {
ctx.addIssue({ fatal: true, code: 'custom', message: 'Transform failed' })
return value
}
})
)

// async transform (zod 4 supports async transforms)
export const asyncTransform = defineSchema(() =>
s.string().transform(async (value, ctx) => {
// async operations...
return processedValue
})
)
```

### Example
Expand All @@ -59,7 +82,7 @@ import type { Image } from 'velite'
* Remote Image with metadata schema
*/
export const remoteImage = () =>
s.string().transform<Image>(async (value, { addIssue }) => {
s.string().transform<Image>(async (value, ctx) => {
try {
const response = await fetch(value)
const blob = await response.blob()
Expand All @@ -69,7 +92,7 @@ export const remoteImage = () =>
return { src: value, ...metadata }
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
addIssue({ fatal: true, code: 'custom', message })
ctx.addIssue({ fatal: true, code: 'custom', message })
return null as never
}
})
Expand All @@ -78,22 +101,62 @@ export const remoteImage = () =>
## Schema Context

> [!TIP]
> Considering that Velite's scenario often needs to obtain metadata information about the current file in the schema, Velite does not use the original Zod package. Instead, it uses a custom Zod package that provides a `meta` member in the schema context.
> In Zod 4, the context object (`ctx`) in `refine`, `superRefine`, and `transform` provides an `addIssue()` method for adding validation errors. Velite extends this context to provide access to file metadata through `context()` function.

### Using Context API

```ts
import { defineSchema, s } from 'velite'
import { context, defineSchema, s } from 'velite'

// convert a nonexistent field
// Access file context in transform
export const path = defineSchema(() =>
s.custom<string>().transform((value, ctx) => {
if (ctx.meta.path) {
return ctx.meta.path
// Use context() to access current file information
const { file, config } = context()

if (value == null) {
// Use ctx.addIssue() to add validation errors (Zod 4 API)
ctx.addIssue({ fatal: false, code: 'custom', message: 'Using file path as fallback' })
return file.path
}
return value
})
)
```

### Context API Reference

The `context()` function returns an object with:

- `config`: The resolved Velite configuration
- `file`: The current [`VeliteFile`](../reference/types.md#velitefile) being processed

### Error Handling in Transforms

```ts
import { defineSchema, s } from 'velite'

export const safeTransform = defineSchema(() =>
s.string().transform(async (value, ctx) => {
try {
// async operation
const result = await processValue(value)
return result
} catch (err) {
// Add error issue using Zod 4 API
ctx.addIssue({
fatal: true, // Set to true to stop processing
code: 'custom', // Error code
message: err.message // Error message
})
return null as never // Type assertion for TypeScript
}
})
)
```

### Reference

the type of `meta` is `ZodMeta`, which extends [`VeliteFile`](../reference/types.md#velitefile).
- `context()` returns `{ config: Config, file: VeliteFile }`
- `ctx.addIssue()` accepts `{ fatal?: boolean, code: string, message: string }`
- See [`VeliteFile`](../reference/types.md#velitefile) for file metadata structure
36 changes: 25 additions & 11 deletions docs/guide/define-collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,35 +156,49 @@ const posts = defineCollection({

### Transform Context Metadata

The `transform()` function can receive a second argument, which is the context object. This is useful for adding computed fields to the content items in a collection.
The `transform()` function can receive a second argument, which is the context object (in Zod 4). To access file metadata, use the `context()` function from Velite.

```js
import { context, s } from 'velite'

const posts = defineCollection({
schema: s
.object({
// fields
})
.transform((data, { meta }) => ({
...data,
// computed fields
path: meta.path // or parse to filename based slug
}))
.transform(data => {
const { file } = context()
return {
...data,
// computed fields
path: file.path, // or parse to filename based slug
basename: file.basename
}
})
})
```

the type of `meta` is `ZodMeta`, which extends [`VeliteFile`](../reference/types.md#velitefile). for more information, see [Custom Schema](custom-schema.md).
For more information about accessing file context, see [Custom Schema](custom-schema.md).

## Content Body

Velite's built-in loader keeps content's raw body in `meta.content`, and the plain text body in `meta.plain`.
Velite's built-in loader keeps content's raw body in `file.content`, and the plain text body in `file.plain`.

To add them as a field, you can use a custom schema.
To add them as a field, you can use a custom schema with the `context()` function.

```js
import { context, s } from 'velite'

const posts = defineCollection({
schema: s.object({
content: s.custom().transform((data, { meta }) => meta.content),
plain: s.custom().transform((data, { meta }) => meta.plain)
content: s.custom().transform(() => {
const { file } = context()
return file.content
}),
plain: s.custom().transform(() => {
const { file } = context()
return file.plain
})
})
})
```
Expand Down
18 changes: 10 additions & 8 deletions docs/guide/last-modified.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@ Create a timestamp schema based on file stat.

```ts
import { stat } from 'fs/promises'
import { defineSchema } from 'velite'
import { context, defineSchema } from 'velite'

const timestamp = defineSchema(() =>
s
.custom<string | undefined>(i => i === undefined || typeof i === 'string')
.transform<string>(async (value, { meta, addIssue }) => {
.transform<string>(async (value, ctx) => {
if (value != null) {
addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the file modified timestamp' })
ctx.addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the file modified timestamp' })
}

const stats = await stat(meta.path)
const { file } = context()
const stats = await stat(file.path)
return stats.mtime.toISOString()
})
)
Expand All @@ -45,18 +46,19 @@ const posts = defineCollection({
```ts
import { exec } from 'child_process'
import { promisify } from 'util'
import { defineSchema } from 'velite'
import { context, defineSchema } from 'velite'

const execAsync = promisify(exec)

const timestamp = defineSchema(() =>
s
.custom<string | undefined>(i => i === undefined || typeof i === 'string')
.transform<string>(async (value, { meta, addIssue }) => {
.transform<string>(async (value, ctx) => {
if (value != null) {
addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the value from `git log -1 --format=%cd`' })
ctx.addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the value from `git log -1 --format=%cd`' })
}
const { stdout } = await execAsync(`git log -1 --format=%cd ${meta.path}`)
const { file } = context()
const { stdout } = await execAsync(`git log -1 --format=%cd ${file.path}`)
return new Date(stdout || Date.now()).toISOString()
})
)
Expand Down
11 changes: 6 additions & 5 deletions docs/guide/using-mdx.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,11 @@ const compileMdx = async (source: string, path: string, options: CompileOptions)
}

export const mdxBundle = (options: MdxOptions = {}) =>
custom<string>().transform<string>(async (value, { meta: { path, content, config }, addIssue }) => {
value = value ?? content
custom<string>().transform<string>(async (value, ctx) => {
const { file, config } = context()
value = value ?? file.content
if (value == null) {
addIssue({ fatal: true, code: 'custom', message: 'The content is empty' })
ctx.addIssue({ fatal: true, code: 'custom', message: 'The content is empty' })
return null as never
}

Expand All @@ -339,9 +340,9 @@ export const mdxBundle = (options: MdxOptions = {}) =>
const compilerOptions = { ...config.mdx, ...options, outputFormat, remarkPlugins, rehypePlugins }

try {
return await compileMdx(value, path, compilerOptions)
return await compileMdx(value, file.path, compilerOptions)
} catch (err: any) {
addIssue({ fatal: true, code: 'custom', message: err.message })
ctx.addIssue({ fatal: true, code: 'custom', message: err.message })
return null as never
}
})
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/velite-schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ name: s.unique('taxonomies')
// 'foo' => 'foo'

// case 2. non-unique value (in all unique by 'taxonomies')
// 'foo' => issue 'Already exists'
// 'foo' => issue 'Duplicate 'foo' with '/path/to/existing/file.yml''
```

### Parameters
Expand Down
27 changes: 15 additions & 12 deletions docs/other/snippets.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import { defineSchema } from 'velite'
const timestamp = defineSchema(() =>
s
.custom<string | undefined>(i => i === undefined || typeof i === 'string')
.transform<string>(async (value, { meta, addIssue }) => {
.transform<string>(async (value, ctx) => {
if (value != null) {
addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the file modified timestamp' })
ctx.addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the file modified timestamp' })
}

const stats = await stat(meta.path)
const { file } = context()
const stats = await stat(file.path)
return stats.mtime.toISOString()
})
)
Expand All @@ -43,11 +44,12 @@ const execAsync = promisify(exec)
const timestamp = defineSchema(() =>
s
.custom<string | undefined>(i => i === undefined || typeof i === 'string')
.transform<string>(async (value, { meta, addIssue }) => {
.transform<string>(async (value, ctx) => {
if (value != null) {
addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the value from `git log -1 --format=%cd`' })
ctx.addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the value from `git log -1 --format=%cd`' })
}
const { stdout } = await execAsync(`git log -1 --format=%cd ${meta.path}`)
const { file } = context()
const { stdout } = await execAsync(`git log -1 --format=%cd ${file.path}`)
return new Date(stdout || Date.now()).toISOString()
})
)
Expand All @@ -73,7 +75,7 @@ import type { Image } from 'velite'
* Remote Image with metadata schema
*/
export const remoteImage = () =>
s.string().transform<Image>(async (value, { addIssue }) => {
s.string().transform<Image>(async (value, ctx) => {
try {
const response = await fetch(value)
const blob = await response.blob()
Expand All @@ -83,7 +85,7 @@ export const remoteImage = () =>
return { src: value, ...metadata }
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
addIssue({ fatal: true, code: 'custom', message })
ctx.addIssue({ fatal: true, code: 'custom', message })
return null as never
}
})
Expand Down Expand Up @@ -273,16 +275,17 @@ export interface ExcerptOptions {
}

export const excerpt = ({ separator = 'more', length = 300 }: ExcerptOptions = {}) =>
custom<string>().transform(async (value, { meta: { path, content, config } }) => {
if (value == null && content != null) {
value = content
custom<string>().transform(async (value, ctx) => {
const { file, config } = context()
if (value == null && file.content != null) {
value = file.content
}
try {
const mdast = fromMarkdown(value)
const hast = raw(toHast(mdast, { allowDangerousHtml: true }))
const exHast = hastExcerpt(hast, { comment: separator, maxSearchSize: 1024 })
const output = exHast ?? truncate(hast, { size: length, ellipsis: '…' })
await rehypeCopyLinkedFiles(config.output)(output, { path })
await rehypeCopyLinkedFiles(config.output)(output, { path: file.path })
return toHtml(output)
} catch (err: any) {
ctx.addIssue({ fatal: true, code: 'custom', message: err.message })
Expand Down
Loading