Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions .changeset/fix-cli-diff-ref.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@zpress/cli': minor
---

Add `--ref` option to `zpress diff` for comparing between commits, enabling use as a Vercel `ignoreCommand`
5 changes: 5 additions & 0 deletions .changeset/fix-ui-filetree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@zpress/ui': patch
---

Fix missing FileTree component by pinning rspress-plugin-file-tree to 1.0.3 and copying its component files into the published dist
3 changes: 2 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@
{
"files": ["**/MermaidRenderer.tsx"],
"rules": {
"unicorn/filename-case": "off"
"unicorn/filename-case": "off",
"prefer-destructuring": "off"
}
}
],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@iconify-json/material-icon-theme": "^1.2.56",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/pixelarticons": "^1.2.4",
"@iconify-json/simple-icons": "^1.2.74",
"@iconify-json/simple-icons": "^1.2.75",
"@iconify-json/skill-icons": "^1.2.4",
"@iconify-json/vscode-icons": "^1.2.45",
"@microsoft/api-extractor": "^7.57.7",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@zpress/templates": "workspace:*",
"@zpress/ui": "workspace:*",
"es-toolkit": "catalog:",
"get-port": "^7.1.0",
"get-port": "^7.2.0",
"ts-pattern": "catalog:",
"zod": "catalog:"
},
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ export const buildCommand = command({
quiet: z.boolean().optional().default(false),
clean: z.boolean().optional().default(false),
check: z.boolean().optional().default(true),
verbose: z.boolean().optional().default(false),
}),
handler: async (ctx) => {
const { quiet, check } = ctx.args
const { quiet, check, verbose } = ctx.args
const paths = createPaths(process.cwd())
ctx.logger.intro('zpress build')

Expand Down Expand Up @@ -53,7 +54,7 @@ export const buildCommand = command({
await sync(config, { paths, quiet: true })

ctx.logger.step('Building & checking for broken links...')
const buildResult = await runBuildCheck({ config, paths })
const buildResult = await runBuildCheck({ config, paths, verbose })

const passed = presentResults({ configResult, buildResult, logger: ctx.logger })
if (!passed) {
Expand Down
83 changes: 77 additions & 6 deletions packages/cli/src/commands/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { command } from '@kidd-cli/core'
import type { Section, ZpressConfig, Result } from '@zpress/core'
import { createPaths, hasGlobChars, loadConfig, normalizeInclude } from '@zpress/core'
import { uniq } from 'es-toolkit'
import { match } from 'ts-pattern'
import { z } from 'zod'

const CONFIG_GLOBS = [
Expand All @@ -19,16 +20,42 @@ const CONFIG_GLOBS = [
/**
* Registers the `diff` CLI command to show changed files in watched directories.
*
* By default outputs a space-separated file list to stdout (suitable for
* lefthook, scripts, and piping). Use `--pretty` for human-readable output.
* By default uses `git status` to detect uncommitted changes and outputs a
* space-separated file list to stdout (suitable for lefthook, scripts, and piping).
*
* Use `--ref <ref>` to compare between commits instead of checking working tree
* status. This uses `git diff --name-only <ref> HEAD` under the hood and exits
* with code 1 when changes are detected — matching the Vercel `ignoreCommand`
* convention (exit 1 = proceed with build, exit 0 = skip).
*
* @example
* ```bash
* # Detect uncommitted changes (default, uses git status)
* zpress diff
*
* # Compare against parent commit (CI / Vercel ignoreCommand)
* zpress diff --ref HEAD^
*
* # Compare against main branch (PR context)
* zpress diff --ref main
*
* # Human-readable output
* zpress diff --ref main --pretty
* ```
*/
export const diffCommand = command({
description: 'Show changed files in configured source directories',
options: z.object({
pretty: z.boolean().optional().default(false),
ref: z
.string()
.optional()
.describe(
'Git ref to compare against HEAD (e.g. HEAD^, main). Exits 1 when changes are detected.'
),
}),
handler: async (ctx) => {
const { pretty } = ctx.args
const { pretty, ref } = ctx.args
const paths = createPaths(process.cwd())

const [configErr, config] = await loadConfig(paths.repoRoot)
Expand Down Expand Up @@ -58,7 +85,12 @@ export const diffCommand = command({
return
}

const [gitErr, changed] = gitChangedFiles({ repoRoot: paths.repoRoot, dirs })
const [gitErr, changed] = match(ref)
.when(
(r): r is string => r !== undefined,
(r) => gitDiffFiles({ repoRoot: paths.repoRoot, dirs, ref: r })
)
.otherwise(() => gitChangedFiles({ repoRoot: paths.repoRoot, dirs }))

if (gitErr) {
if (pretty) {
Expand All @@ -83,10 +115,15 @@ export const diffCommand = command({
ctx.logger.step(`Watching ${dirs.length} path(s)`)
ctx.logger.note(changed.join('\n'), `${changed.length} changed file(s)`)
ctx.logger.outro('Done')
return
} else {
process.stdout.write(`${changed.join(' ')}\n`)
}

process.stdout.write(`${changed.join(' ')}\n`)
// When --ref is used (e.g. as a Vercel ignoreCommand), exit 1 signals
// "changes detected, proceed with build". Exit 0 (no changes) means skip.
if (ref) {
process.exit(1)
}
},
})

Expand Down Expand Up @@ -251,6 +288,40 @@ function stripQuotes(value: string): string {
return trimmed
}

/**
* Run `git diff --name-only <ref> HEAD` scoped to the given directories and return changed file paths.
*
* Use this instead of `gitChangedFiles` when comparing between commits (e.g. for
* CI ignore commands like Vercel's `ignoreCommand`), since `git status` only
* detects uncommitted changes and always returns empty on clean checkouts.
*
* @private
* @param params - Parameters for the git diff query
* @param params.repoRoot - Absolute path to the repo root
* @param params.dirs - Directories to scope the git diff to
* @param params.ref - Git ref to compare against HEAD (e.g. `HEAD^`, `main`)
* @returns Result tuple with changed file paths (repo-relative) or an error
*/
function gitDiffFiles(params: {
readonly repoRoot: string
readonly dirs: readonly string[]
readonly ref: string
}): Result<readonly string[]> {
const [err, output] = execSilent({
file: 'git',
args: ['diff', '--name-only', params.ref, 'HEAD', '--', ...params.dirs],
cwd: params.repoRoot,
})
if (err) {
return [err, null]
}
if (!output) {
return [null, []]
}
const files = output.split('\n').filter((line) => line.length > 0)
return [null, files]
}

/**
* Run a command silently with an explicit argument array, returning a Result
* tuple with trimmed stdout on success or an Error on failure.
Expand Down
22 changes: 22 additions & 0 deletions packages/cli/src/lib/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ interface PresentResultsParams {
interface RunBuildCheckParams {
readonly config: ZpressConfig
readonly paths: Paths
readonly verbose?: boolean
}

interface RunConfigCheckParams {
Expand Down Expand Up @@ -94,6 +95,10 @@ export function runConfigCheck(params: RunConfigCheckParams): ConfigCheckResult
* @returns A `BuildCheckResult` with pass/fail status and any deadlinks found
*/
export async function runBuildCheck(params: RunBuildCheckParams): Promise<BuildCheckResult> {
if (params.verbose) {
return runBuildCheckVerbose(params)
}

const { error, captured } = await captureOutput(() =>
buildSiteForCheck({ config: params.config, paths: params.paths })
)
Expand Down Expand Up @@ -157,6 +162,23 @@ export function presentResults(params: PresentResultsParams): boolean {
// Private
// ---------------------------------------------------------------------------

/**
* Run the build check without capturing output, letting Rspress/Rspack
* write directly to stdout/stderr so the full error details are visible.
*
* @private
* @param params - Config and paths for the build
* @returns A `BuildCheckResult` with pass/fail status
*/
async function runBuildCheckVerbose(params: RunBuildCheckParams): Promise<BuildCheckResult> {
try {
await buildSiteForCheck({ config: params.config, paths: params.paths })
return { status: 'passed' }
} catch (error) {
return { status: 'error', message: toError(error).message }
}
}

/**
* Strip ANSI escape codes from a string.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"gray-matter": "^4.0.3",
"jiti": "^2.6.1",
"js-yaml": "^4.1.1",
"liquidjs": "^10.25.0",
"liquidjs": "^10.25.1",
"ts-pattern": "catalog:"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"@iconify-json/material-icon-theme": "^1.2.56",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/pixelarticons": "^1.2.4",
"@iconify-json/simple-icons": "^1.2.74",
"@iconify-json/simple-icons": "^1.2.75",
"@iconify-json/skill-icons": "^1.2.4",
"@iconify-json/vscode-icons": "^1.2.45",
"@iconify/react": "^6.0.2",
Expand All @@ -78,7 +78,7 @@
"react": "^19.2.4",
"react-dom": "^19.2.4",
"rspress-plugin-devkit": "^1.0.0",
"rspress-plugin-file-tree": "^1.0.4",
"rspress-plugin-file-tree": "1.0.3",
"rspress-plugin-katex": "^1.0.0",
"rspress-plugin-supersub": "^1.0.0",
"typescript": "catalog:",
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export default defineConfig({
from: './src/plugins/mermaid/mermaid.css',
to: 'plugins/mermaid/mermaid.css',
},
{
from: './node_modules/rspress-plugin-file-tree/dist/components',
to: 'components',
},
],
},
})
Loading
Loading