Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
14 changes: 7 additions & 7 deletions examples/basic/velite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { exec } from 'node:child_process'
import { promisify } from 'node:util'
import { defineConfig, s } from 'velite'
import { context, defineConfig, s } from 'velite'

const slugify = input =>
input
Expand All @@ -25,11 +25,11 @@ const execAsync = promisify(exec)
const timestamp = () =>
s
.custom(i => i === undefined || typeof i === 'string')
.transform(async (value, { meta, addIssue }) => {
.transform(async (value, { addIssue }) => {
if (value != null) {
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 { stdout } = await execAsync(`git log -1 --format=%cd ${context().file.path}`)
return new Date(stdout || Date.now()).toISOString()
})

Expand Down Expand Up @@ -63,7 +63,7 @@ export default defineConfig({
schema: s
.object({
name: s.unique('categories'),
slug: s.slug('global'),
slug: s.slug(),
cover: s.image().optional(),
description: s.string().max(999).optional(),
count
Expand All @@ -76,7 +76,7 @@ export default defineConfig({
schema: s
.object({
name: s.string().max(20),
slug: s.slug('global'),
slug: s.slug(),
cover: s.image().optional(),
description: s.string().max(999).optional(),
count
Expand All @@ -89,11 +89,11 @@ export default defineConfig({
schema: s
.object({
title: s.string().max(99),
slug: s.slug('global'),
slug: s.slug(),
body: s.mdx(),
raw: s.raw()
})
.transform((data, { meta }) => ({ ...data, permalink: `/${data.slug}`, basename: meta.basename }))
.transform(data => ({ ...data, permalink: `/${data.slug}`, basename: context().file.basename }))
},
posts: {
name: 'Post',
Expand Down
15 changes: 2 additions & 13 deletions examples/nextjs/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
import type { NextConfig } from 'next'
import { withVelite } from '@velite/plugin-next'

const isDev = process.argv.indexOf('dev') !== -1
const isBuild = process.argv.indexOf('build') !== -1
if (!process.env.VELITE_STARTED && (isDev || isBuild)) {
process.env.VELITE_STARTED = '1'
import('velite').then(m => m.build({ watch: isDev, clean: !isDev }))
}

const nextConfig: NextConfig = {
/* config options here */
}

export default nextConfig
export default withVelite()

// legacy next.config.js ↓ (not support turbopack)

Expand Down
19 changes: 10 additions & 9 deletions examples/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,21 @@
"content": "velite --clean"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.12",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^24.3.0",
"@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^25.0.2",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"next": "16.0.7",
"@velite/plugin-next": "workspace:*",
"next": "16.0.10",
"next-themes": "^0.4.6",
"postcss": "^8.5.6",
"react": "19.2.1",
"react-dom": "19.2.1",
"react": "19.2.3",
"react-dom": "19.2.3",
"rehype-pretty-code": "^0.14.1",
"shiki": "^3.11.0",
"tailwindcss": "^4.1.12",
"typescript": "^5.9.2",
"shiki": "^3.20.0",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"velite": "workspace:*"
}
}
6 changes: 3 additions & 3 deletions examples/nextjs/velite.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
import rehypePrettyCode from 'rehype-pretty-code'
import { defineCollection, defineConfig, s } from 'velite'
import { context, defineCollection, defineConfig, s } from 'velite'

const slugify = (input: string) =>
input
Expand All @@ -26,11 +26,11 @@ const execAsync = promisify(exec)
const timestamp = () =>
s
.custom<string | undefined>(i => i === undefined || typeof i === 'string')
.transform<string>(async (value, { meta, addIssue }) => {
.transform<string>(async (value, { addIssue }) => {
if (value != null) {
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 { stdout } = await execAsync(`git log -1 --format=%cd ${context().file.path}`)
return new Date(stdout || Date.now()).toISOString()
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same command injection risk as in the Vite example.

This timestamp() helper has the same vulnerability where the file path is passed directly to a shell command without escaping.

Apply the same fix using execFile instead of exec:

+import { execFile } from 'node:child_process'
+
+const execFileAsync = promisify(execFile)
+
 const timestamp = () =>
   s
     .custom<string | undefined>(i => i === undefined || typeof i === 'string')
     .transform<string>(async (value, { addIssue }) => {
       if (value != null) {
         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 ${context().file.path}`)
+      const { stdout } = await execFileAsync('git', ['log', '-1', '--format=%cd', context().file.path])
       return new Date(stdout || Date.now()).toISOString()
     })

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In examples/nextjs/velite.config.ts around lines 29 to 35, the transform uses
execAsync with a file path interpolated into a shell command causing a command
injection risk; replace this with a safe execFile-based call (or promisified
execFile) that passes the git and format args plus context().file.path as an
argument (do not interpolate into a shell string), await the execFile result,
and return the ISO timestamp from stdout (falling back to Date.now() if stdout
is empty); ensure any error is handled similarly to current behavior and that
addIssue logic remains unchanged.


Expand Down
28 changes: 14 additions & 14 deletions examples/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@
"preview": "vite preview"
},
"dependencies": {
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react": "^19.2.3",
"react-dom": "^19.2.3"
},
"devDependencies": {
"@eslint/js": "^9.34.0",
"@types/react": "^19.1.11",
"@types/react-dom": "^19.1.7",
"@eslint/js": "^9.39.2",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@velite/plugin-vite": "workspace:*",
"@vitejs/plugin-react": "^5.0.1",
"eslint": "^9.34.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"@vitejs/plugin-react": "^5.1.2",
"eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.25",
"globals": "^16.5.0",
"rehype-pretty-code": "^0.14.1",
"shiki": "^3.11.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.40.0",
"shiki": "^3.20.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.49.0",
"velite": "workspace:*",
"vite": "^7.1.3"
"vite": "^7.2.7"
}
}
6 changes: 3 additions & 3 deletions examples/vite/velite.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
import rehypePrettyCode from 'rehype-pretty-code'
import { defineCollection, defineConfig, s } from 'velite'
import { context, defineCollection, defineConfig, s } from 'velite'

const slugify = (input: string) =>
input
Expand All @@ -26,11 +26,11 @@ const execAsync = promisify(exec)
const timestamp = () =>
s
.custom<string | undefined>(i => i === undefined || typeof i === 'string')
.transform<string>(async (value, { meta, addIssue }) => {
.transform<string>(async (value, { addIssue }) => {
if (value != null) {
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 { stdout } = await execAsync(`git log -1 --format=%cd ${context().file.path}`)
return new Date(stdout || Date.now()).toISOString()
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential command injection via unescaped file path.

The file path is interpolated directly into a shell command without escaping. If a file path contains shell metacharacters (spaces, quotes, $, ;, etc.), it could cause command failures or injection vulnerabilities.

Consider escaping the path or using execFile which doesn't invoke a shell:

+import { execFile } from 'node:child_process'
+
+const execFileAsync = promisify(execFile)
+
 const timestamp = () =>
   s
     .custom<string | undefined>(i => i === undefined || typeof i === 'string')
     .transform<string>(async (value, { addIssue }) => {
       if (value != null) {
         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 ${context().file.path}`)
+      const { stdout } = await execFileAsync('git', ['log', '-1', '--format=%cd', context().file.path])
       return new Date(stdout || Date.now()).toISOString()
     })

Committable suggestion skipped: line range outside the PR's diff.


Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,18 @@
},
"dependencies": {
"@mdx-js/mdx": "^3.1.1",
"esbuild": "^0.25.12",
"esbuild": "^0.27.1",
"sharp": "^0.34.5",
"terser": "^5.44.1"
"terser": "^5.44.1",
"zod": "^4.1.13"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"@types/node": "^22.19.1",
"@types/node": "^25.0.2",
"@types/picomatch": "^4.0.2",
"chokidar": "^4.0.3",
"chokidar": "^5.0.0",
"fast-glob": "^3.3.3",
"hast-util-raw": "^9.1.0",
"hast-util-to-string": "^3.0.1",
Expand Down
19 changes: 19 additions & 0 deletions packages/next/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { NextConfig } from 'next'
import type { Options as VeliteOptions } from 'velite'

export type Options = Omit<VeliteOptions, 'watch' | 'clean'>

type NextConfigFunction = (phase: string, options: { defaultConfig: NextConfig }) => Promise<NextConfig> | NextConfig
type NextConfigInput = NextConfig | NextConfigFunction

/**
* Create a Next.js plugin for integrating Velite
*/
declare const createNextPlugin: (options?: Options) => <T extends NextConfigInput>(nextConfig?: T) => Promise<T>

/**
* Next.js plugin for integrating Velite
*/
declare const withVelite: ReturnType<typeof createNextPlugin>
Comment on lines +12 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat packages/next/index.js | head -80

Repository: zce/velite

Length of output: 1673


🏁 Script executed:

cat packages/next/index.d.ts

Repository: zce/velite

Length of output: 722


Correct the type signature to reflect actual behavior.

The generic <T extends NextConfigInput> with return type Promise<T> creates a type mismatch when T is NextConfigFunction. The implementation returns nextConfig as-is without invoking it, which means if a function-style config is passed, the return type Promise<NextConfigFunction> represents a Promise resolving to a function—not a valid Next.js config. Either restrict the generic to exclude NextConfigFunction, or invoke the function before returning to ensure the resolved value is always a plain NextConfig object.

🤖 Prompt for AI Agents
In packages/next/index.d.ts around lines 12 to 17, the exported createNextPlugin
signature allows T to be NextConfigFunction but returns Promise<T>, which would
make the Promise resolve to a function (invalid as a Next.js config); fix by
narrowing the generic so T excludes NextConfigFunction (e.g. T extends
NextConfigInput but not a function) or change the implementation/signature to
detect and call a function-style nextConfig and return Promise<NextConfig> (i.e.
always resolve to a plain NextConfig object) — pick one approach and update the
type declaration accordingly so the declared return type matches actual runtime
behavior.


export { createNextPlugin, withVelite, type Options }
50 changes: 50 additions & 0 deletions packages/next/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Thanks to `https://github.com/sdorra/content-collections` for Next.js v16+ Velite integration.
*/

/**
* Create a Next.js plugin for integrating Velite
* @param {Omit<import('velite').Options, 'watch' | 'clean'>} pluginOptions
* @returns {import('next').NextConfig} Next.js plugin
*/
export const createNextPlugin = (pluginOptions = {}) => {
const [command] = process.argv.slice(2).filter(i => !i.startsWith('-'))

// typegen loads next.config.js
const isTypegen = command === 'typegen'

// the build step loads next.config.js
const isBuild = command === 'build'

// starting with v16 next dev doesn't load next.config.js
// next dev - calls next-start in a forked process
// next-start loads next.config.js
// process.argv are not visible by next-start
const isDev =
// to make this compatible with previous versions
// check if command is NOT set (next-start) and we are in development mode
typeof command === 'undefined' && process.env.NODE_ENV === 'development'

/**
* @param {import('next').NextConfig} nextConfig
* @returns {Promise<import('next').NextConfig>}
*/
return async (nextConfig = {}) => {
// prevent multiple calls
if (process.env.__VELITE_STARTED) return nextConfig

// if not dev, build, or typegen, return the next config
if (!isDev && !isBuild && !isTypegen) return nextConfig

// start velite
process.env.__VELITE_STARTED = '1'

const { build } = await import('velite')

await build({ ...pluginOptions, watch: isDev, clean: !isDev })

return nextConfig
}
}

export const withVelite = createNextPlugin()
24 changes: 24 additions & 0 deletions packages/next/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@velite/plugin-next",
"version": "0.0.1",
"description": "Next.js plugin for integrating Velite",
"type": "module",
"exports": "./index.js",
"types": "./index.d.ts",
"files": [
"index.js",
"index.d.ts"
],
"keywords": [
"next-plugin",
"velite",
"content"
],
"peerDependencies": {
"next": "^16.0.0",
"velite": "workspace:*"
},
"publishConfig": {
"access": "public"
}
}
24 changes: 24 additions & 0 deletions packages/next/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# @velite/plugin-next

A Next.js plugin for integrating Velite content processing.

## Installation

```bash
npm install -D velite @velite/plugin-next
```

## Usage

```ts
// next.config.ts
import { withVelite } from '@velite/plugin-next'

export default withVelite({
// other next config here...
})
```

## License

[MIT](../../license) &copy; [zce](https://zce.me)
Comment on lines +1 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Verify license link path/casing

README content looks good. Please double-check that ../../license matches the actual filename in the repo (often LICENSE), otherwise this link may 404 on case‑sensitive paths.

🤖 Prompt for AI Agents
In packages/next/readme.md around lines 1 to 24, the README links to
../../license which may not match the repository filename/casing (commonly
LICENSE) and can 404 on case-sensitive filesystems; update the link to the exact
filename and path used in the repo (for example ../../LICENSE) or use an
absolute/root-relative link to the repo license, then verify the link works in a
case-sensitive environment.

Loading