Skip to content

Commit 1e966e1

Browse files
zrosenbauerclaudecoderabbitai[bot]CodeRabbit
authored
fix(packages/core): use deep glob pattern for recursive workspace includes (#69)
* fix(packages/core): use deep glob pattern for recursive workspace includes The default include pattern for workspace items was always `docs/*.md` regardless of the `recursive` flag. This meant recursive mode with no explicit `include` would only discover files at the top level of `docs/`, never in subdirectories like `docs/guides/setup.md`. Switch the default to `docs/**/*.md` when `recursive: true` so fast-glob discovers nested files as expected. Co-Authored-By: Claude <noreply@anthropic.com> * chore(repo): add changeset and disable coderabbit walkthrough Co-Authored-By: Claude <noreply@anthropic.com> * fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai> * chore(repo): gut coderabbit walkthrough content Collapse walkthrough and disable all sub-features: changed files summary, sequence diagrams, linked issues, related issues/PRs, and review status messages. Co-Authored-By: Claude <noreply@anthropic.com> * chore(repo): disable all coderabbit walkthrough sub-features Disable every feature that populates the walkthrough comment: high_level_summary, review_status, review_details, changed_files_summary, sequence_diagrams, estimate_code_review_effort, assess_linked_issues, related_issues, related_prs. The walkthrough comment itself has no master toggle in the v2 schema — this is as close to off as it gets. Co-Authored-By: Claude <noreply@anthropic.com> * fix(packages/core): prevent double-prefixing repo-relative include paths When a workspace item provides an explicit repo-relative include like `apps/api/docs/**/*.md`, `normalizeAndResolveInclude` was prepending the basePath derived from `path`, producing a broken double path: `apps/api/apps/api/docs/**/*.md`. The glob silently matched 0 files, so only landing pages appeared in the output. Skip the basePath prefix when the pattern already starts with it. Co-Authored-By: Claude <noreply@anthropic.com> * chore(repo): update changeset to cover both workspace fixes Co-Authored-By: Claude <noreply@anthropic.com> * fix(packages/core): warn on double-prefixed include instead of auto-correcting Replace the silent auto-fix with a warning that tells the user their include pattern is already repo-relative and will be double-prefixed. Include is documented as relative to the workspace path — the user should fix their config. Co-Authored-By: Claude <noreply@anthropic.com> * fix(packages/core): move include prefix check to config validation Remove the log.warn from the sync hot path and add a proper config check that surfaces during `zpress check`. When a workspace include pattern already starts with the basePath derived from path, warn that the resolved glob will be double-prefixed and likely match zero files. - Add ConfigWarning type and configWarning factory to @zpress/config - Add checkWorkspaceIncludes to @zpress/core (exported) - Wire warnings into runConfigCheck and presentResults in @zpress/cli Co-Authored-By: Claude <noreply@anthropic.com> * docs(packages/core): update JSDoc for conditional include default Reflect that the default include pattern is now docs/**/*.md when recursive is true, otherwise docs/*.md. Co-Authored-By: Claude <noreply@anthropic.com> * fix(packages/core): remove glob patterns from JSDoc to fix declaration emit TypeScript's declaration emitter interprets the sequence star-slash inside JSDoc backtick spans as end-of-comment, breaking the build. Remove glob examples from exported function JSDoc blocks. Co-Authored-By: Claude <noreply@anthropic.com> * fix(packages/cli): add warnings field to check test fixtures The ConfigCheckResult interface gained a warnings field but existing test fixtures were not updated, causing presentResults to crash on configResult.warnings.length. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
1 parent 1d81aaa commit 1e966e1

9 files changed

Lines changed: 132 additions & 26 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@zpress/core': patch
3+
'@zpress/config': patch
4+
'@zpress/cli': patch
5+
---
6+
7+
Fix workspace include resolution for `apps` and `packages` items:
8+
9+
- Use deep glob pattern (`docs/**/*.md`) as default include when `recursive: true`. Previously the default was always `docs/*.md` regardless of the flag.
10+
- Add config check warning when an explicit include pattern already starts with the basePath derived from `path`, which causes double-prefixing and silently matches zero files. Surfaces during `zpress check` before the build step.

.coderabbit.yaml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,19 @@ early_access: true
66
reviews:
77
profile: assertive
88
request_changes_workflow: true
9-
high_level_summary: true
9+
high_level_summary: false
1010
high_level_summary_in_walkthrough: false
11-
review_status: true
11+
review_status: false
12+
review_details: false
1213
commit_status: true
1314
fail_commit_status: false
14-
collapse_walkthrough: false
15-
changed_files_summary: true
16-
sequence_diagrams: true
17-
assess_linked_issues: true
18-
related_issues: true
19-
related_prs: true
15+
collapse_walkthrough: true
16+
changed_files_summary: false
17+
sequence_diagrams: false
18+
estimate_code_review_effort: false
19+
assess_linked_issues: false
20+
related_issues: false
21+
related_prs: false
2022
suggested_labels: false
2123
suggested_reviewers: false
2224
poem: false

packages/cli/src/lib/check.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const loadError = {
1818
const mockLogger = {
1919
success: vi.fn(),
2020
error: vi.fn(),
21+
warn: vi.fn(),
2122
message: vi.fn(),
2223
}
2324

@@ -48,7 +49,7 @@ describe('runConfigCheck()', () => {
4849
describe('presentResults()', () => {
4950
it('should return true when both config passed and build passed', () => {
5051
const result = presentResults({
51-
configResult: { passed: true, errors: [] },
52+
configResult: { passed: true, errors: [], warnings: [] },
5253
buildResult: { status: 'passed' },
5354
logger: mockLogger,
5455
})
@@ -57,7 +58,7 @@ describe('presentResults()', () => {
5758

5859
it('should return false when config failed', () => {
5960
const result = presentResults({
60-
configResult: { passed: false, errors: [loadError] },
61+
configResult: { passed: false, errors: [loadError], warnings: [] },
6162
buildResult: { status: 'passed' },
6263
logger: mockLogger,
6364
})
@@ -66,7 +67,7 @@ describe('presentResults()', () => {
6667

6768
it('should return false when build has deadlinks', () => {
6869
const result = presentResults({
69-
configResult: { passed: true, errors: [] },
70+
configResult: { passed: true, errors: [], warnings: [] },
7071
buildResult: {
7172
status: 'failed',
7273
deadlinks: [{ file: 'docs/page.md', links: ['/missing'] }],
@@ -78,7 +79,7 @@ describe('presentResults()', () => {
7879

7980
it('should call logger.success when config is valid', () => {
8081
presentResults({
81-
configResult: { passed: true, errors: [] },
82+
configResult: { passed: true, errors: [], warnings: [] },
8283
buildResult: { status: 'passed' },
8384
logger: mockLogger,
8485
})
@@ -87,7 +88,7 @@ describe('presentResults()', () => {
8788

8889
it('should call logger.error when config failed', () => {
8990
presentResults({
90-
configResult: { passed: false, errors: [loadError] },
91+
configResult: { passed: false, errors: [loadError], warnings: [] },
9192
buildResult: { status: 'skipped' },
9293
logger: mockLogger,
9394
})

packages/cli/src/lib/check.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import path from 'node:path'
1111

1212
import type { CliLogger } from '@kidd-cli/core/logger'
13-
import { configError } from '@zpress/core'
14-
import type { ConfigError, Paths, ZpressConfig } from '@zpress/core'
13+
import { configError, checkWorkspaceIncludes } from '@zpress/core'
14+
import type { ConfigError, ConfigWarning, Paths, ZpressConfig } from '@zpress/core'
1515

1616
import { toError } from './error.ts'
1717
import { buildSiteForCheck } from './rspress.ts'
@@ -20,6 +20,7 @@ import { buildSiteForCheck } from './rspress.ts'
2020
const ANSI_PATTERN = /\u001B\[[0-9;]*m/g
2121

2222
const RED = '\u001B[31m'
23+
const YELLOW = '\u001B[33m'
2324
const DIM = '\u001B[2m'
2425
const RESET = '\u001B[0m'
2526

@@ -31,6 +32,7 @@ interface DeadlinkInfo {
3132
interface ConfigCheckResult {
3233
readonly passed: boolean
3334
readonly errors: readonly ConfigError[]
35+
readonly warnings: readonly ConfigWarning[]
3436
}
3537

3638
type BuildCheckResult =
@@ -75,12 +77,13 @@ interface RunConfigCheckParams {
7577
export function runConfigCheck(params: RunConfigCheckParams): ConfigCheckResult {
7678
const { config, loadError } = params
7779
if (loadError) {
78-
return { passed: false, errors: [loadError] }
80+
return { passed: false, errors: [loadError], warnings: [] }
7981
}
8082
if (!config) {
81-
return { passed: false, errors: [configError('empty_sections', 'Config is missing')] }
83+
return { passed: false, errors: [configError('empty_sections', 'Config is missing')], warnings: [] }
8284
}
83-
return { passed: true, errors: [] }
85+
const warnings = checkWorkspaceIncludes(config)
86+
return { passed: true, errors: [], warnings }
8487
}
8588

8689
/**
@@ -142,6 +145,15 @@ export function presentResults(params: PresentResultsParams): boolean {
142145
})
143146
}
144147

148+
if (configResult.warnings.length > 0) {
149+
logger.warn(`${configResult.warnings.length} config warning(s):`)
150+
// oxlint-disable-next-line no-unused-expressions -- side-effect logging over config warnings
151+
configResult.warnings.map((w) => {
152+
logger.message(` ${YELLOW}${RESET} ${w.message}`)
153+
return null
154+
})
155+
}
156+
145157
if (buildResult.status === 'passed') {
146158
logger.success('No broken links')
147159
} else if (buildResult.status === 'skipped') {

packages/config/schemas/schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "http://json-schema.org/draft-07/schema#",
3-
"$id": "https://raw.githubusercontent.com/joggrdocs/zpress/v0.4.0/packages/config/schemas/schema.json",
3+
"$id": "https://raw.githubusercontent.com/joggrdocs/zpress/v0.5.0/packages/config/schemas/schema.json",
44
"title": "Zpress Configuration",
55
"description": "Configuration file for zpress documentation framework",
66
"$ref": "#/definitions/ZpressConfig",

packages/config/src/errors.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Error types for config loading and validation.
2+
* Error and warning types for config loading and validation.
33
*/
44

55
import type { ZodError } from 'zod/v3'
@@ -33,6 +33,17 @@ export interface ConfigError {
3333

3434
export type ConfigResult<T> = Result<T, ConfigError>
3535

36+
export type ConfigWarningType = 'duplicate_include_prefix'
37+
38+
/**
39+
* Non-fatal config issue that may cause unexpected behavior.
40+
*/
41+
export interface ConfigWarning {
42+
readonly _tag: 'ConfigWarning'
43+
readonly type: ConfigWarningType
44+
readonly message: string
45+
}
46+
3647
/**
3748
* Create a ConfigError with the given type and message.
3849
*
@@ -48,6 +59,21 @@ export function configError(type: ConfigErrorType, message: string): ConfigError
4859
}
4960
}
5061

62+
/**
63+
* Create a ConfigWarning with the given type and message.
64+
*
65+
* @param type - The warning type discriminant
66+
* @param message - Human-readable warning message
67+
* @returns A ConfigWarning object
68+
*/
69+
export function configWarning(type: ConfigWarningType, message: string): ConfigWarning {
70+
return {
71+
_tag: 'ConfigWarning',
72+
type,
73+
message,
74+
}
75+
}
76+
5177
/**
5278
* Convert a Zod validation error into a ConfigError.
5379
*

packages/config/src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,14 @@ export { validateConfig } from './validator.ts'
3939

4040
export { zpressConfigSchema, pathsSchema } from './schema.ts'
4141

42-
export { configError, configErrorFromZod } from './errors.ts'
43-
export type { ConfigError, ConfigErrorType, ConfigResult } from './errors.ts'
42+
export { configError, configErrorFromZod, configWarning } from './errors.ts'
43+
export type {
44+
ConfigError,
45+
ConfigErrorType,
46+
ConfigResult,
47+
ConfigWarning,
48+
ConfigWarningType,
49+
} from './errors.ts'
4450

4551
export {
4652
THEME_NAMES,

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export type {
4040
} from './sync/types.ts'
4141
export type { SyncError, SyncOutcome, ConfigError, ConfigResult } from './sync/errors.ts'
4242
export { syncError, configError } from './sync/errors.ts'
43+
export type { ConfigWarning, ConfigWarningType } from '@zpress/config'
44+
export { checkWorkspaceIncludes } from './sync/workspace.ts'
4345

4446
export {
4547
generateAssets,

packages/core/src/sync/workspace.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { configWarning } from '@zpress/config'
2+
import type { ConfigWarning } from '@zpress/config'
13
import { isNil, isString, isUndefined, kebabCase, omitBy } from 'es-toolkit'
24
import { match, P } from 'ts-pattern'
35

6+
import { normalizeInclude } from '../glob.ts'
47
import { resolveOptionalIcon, serializeIcon } from '../icon.ts'
58
import type { Section, TitleConfig, ZpressConfig, Workspace } from '../types.ts'
69
import { collectAllWorkspaceItems } from './collect-workspaces.ts'
@@ -240,6 +243,22 @@ export function synthesizeWorkspaceSections(config: ZpressConfig): Section[] {
240243
)
241244
}
242245

246+
/**
247+
* Check workspace items for include patterns that will be double-prefixed.
248+
*
249+
* When a workspace `include` pattern already starts with the basePath
250+
* derived from `path`, the resolved glob becomes double-prefixed
251+
* and will silently match zero files. This produces warnings so the
252+
* user can fix their config before broken links appear in the build.
253+
*
254+
* @param config - Validated zpress config
255+
* @returns Array of warnings for any workspace items with suspect includes
256+
*/
257+
export function checkWorkspaceIncludes(config: ZpressConfig): readonly ConfigWarning[] {
258+
const allItems = collectAllWorkspaceItems(config)
259+
return allItems.flatMap((item) => checkItemInclude(item))
260+
}
261+
243262
/**
244263
* Convert display text to a URL-safe slug.
245264
* E.g. "Getting Started" → "getting-started", "updatePet" → "update-pet"
@@ -255,6 +274,32 @@ export function slugify(text: string): string {
255274
// Private
256275
// ---------------------------------------------------------------------------
257276

277+
/**
278+
* Check a single workspace item for include patterns that already start
279+
* with the basePath derived from `path`.
280+
*
281+
* @private
282+
* @param item - Workspace item to check
283+
* @returns Array of warnings (empty if no issues)
284+
*/
285+
function checkItemInclude(item: Workspace): readonly ConfigWarning[] {
286+
if (item.include === null || item.include === undefined) {
287+
return []
288+
}
289+
const basePath = item.path.replace(/^\//, '')
290+
const patterns = normalizeInclude(item.include)
291+
return patterns
292+
.filter((pattern) => pattern.startsWith(basePath))
293+
.map((pattern) =>
294+
configWarning(
295+
'duplicate_include_prefix',
296+
`Workspace "${String(item.title)}" include "${pattern}" already starts with "${basePath}" — ` +
297+
`it will resolve to "${basePath}/${pattern}" which likely matches zero files. ` +
298+
`Did you mean "${pattern.slice(basePath.length + 1)}"? (include is relative to path)`
299+
)
300+
)
301+
}
302+
258303
/**
259304
* Recursively collect all links from a section tree.
260305
* Walks sections and their nested items to find every defined link.
@@ -381,9 +426,8 @@ function buildWorkspaceSection(
381426
*
382427
* Uses `path` as both the section URL and URL prefix for glob-discovered children.
383428
* The `include` field is resolved relative to the workspace item's base path
384-
* (derived from `path`). Defaults to `"docs/*.md"` when omitted.
385-
*
386-
* For example, `path: "/apps/api"` + `include: "docs/*.md"` resolves to `"apps/api/docs/*.md"`.
429+
* (derived from `path`). When `recursive` is true the default include is a deep
430+
* glob matching all nested markdown, otherwise a shallow single-level glob.
387431
*
388432
* @private
389433
* @param item - Workspace item to convert
@@ -408,7 +452,10 @@ function workspaceToSection(item: Workspace): Section {
408452
* @returns Complete section with all discovery fields resolved
409453
*/
410454
function applyOptionalFields(base: Section, item: Workspace): Section {
411-
const fromPattern = item.include ?? 'docs/*.md'
455+
const defaultPattern = match(item.recursive)
456+
.with(true, () => 'docs/**/*.md')
457+
.otherwise(() => 'docs/*.md')
458+
const fromPattern = item.include ?? defaultPattern
412459
const basePath = item.path.replace(/^\//, '')
413460
const resolvedInclude = normalizeAndResolveInclude(fromPattern, basePath)
414461

0 commit comments

Comments
 (0)