Skip to content

Commit 2273ae0

Browse files
louis-preclaude
andcommitted
fix: Deduplicate redirect errors to one per redirect
Report the most actionable error (section mismatch over file-not-found) and skip redundant checks. Also simplify by removing duplicated logic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent caaf366 commit 2273ae0

1 file changed

Lines changed: 54 additions & 74 deletions

File tree

codegen/validate-redirects.ts

Lines changed: 54 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import YAML from 'yaml'
66
import { siteSections } from './lib/config.js'
77

88
interface GitbookConfig {
9+
root?: string
910
redirects?: Record<string, string>
1011
}
1112

@@ -16,102 +17,81 @@ interface BrokenRedirect {
1617
reason: string
1718
}
1819

20+
function findForeignSection(
21+
path: string,
22+
ownRoot: string,
23+
): string | undefined {
24+
for (const other of siteSections) {
25+
if (other.root === ownRoot) continue
26+
const prefix = other.urlPrefix.replace(/^\//, '') + '/'
27+
if (prefix !== '/' && path.startsWith(prefix)) return other.name
28+
}
29+
return undefined
30+
}
31+
1932
const broken: BrokenRedirect[] = []
2033

34+
interface ConfigToValidate {
35+
configPath: string
36+
root: string
37+
redirects: Record<string, string>
38+
}
39+
40+
const configs: ConfigToValidate[] = []
41+
2142
for (const section of siteSections) {
2243
const configPath = join(section.root, '.gitbook.yaml')
2344
if (!existsSync(configPath)) continue
24-
2545
const config = YAML.parse(readFileSync(configPath, 'utf-8')) as GitbookConfig
26-
const redirects = config.redirects ?? {}
27-
for (const [source, target] of Object.entries(redirects)) {
28-
// Source must not start with another section's URL prefix
29-
for (const other of siteSections) {
30-
if (other.root === section.root) continue
31-
const otherPrefix = other.urlPrefix.replace(/^\//, '') + '/'
32-
if (otherPrefix !== '/' && source.startsWith(otherPrefix)) {
33-
broken.push({
34-
configPath,
35-
source,
36-
target,
37-
reason: `Source belongs to the "${other.name}" section, not "${section.name}". Move this redirect to ${join(other.root, '.gitbook.yaml')}`,
38-
})
39-
}
40-
}
41-
42-
// Target must not start with another section's URL prefix
43-
for (const other of siteSections) {
44-
if (other.root === section.root) continue
45-
const otherPrefix = other.urlPrefix.replace(/^\//, '') + '/'
46-
if (otherPrefix !== '/' && target.startsWith(otherPrefix)) {
47-
broken.push({
48-
configPath,
49-
source,
50-
target,
51-
reason: `Target points to the "${other.name}" section. Move this redirect to ${join(other.root, '.gitbook.yaml')}`,
52-
})
53-
}
54-
}
55-
56-
// Target must resolve to an existing file within this section
57-
const fullPath = join(section.root, target)
58-
if (!existsSync(fullPath) || !statSync(fullPath).isFile()) {
59-
broken.push({
60-
configPath,
61-
source,
62-
target,
63-
reason: `File not found: ${fullPath}`,
64-
})
65-
}
66-
}
46+
configs.push({
47+
configPath,
48+
root: section.root,
49+
redirects: config.redirects ?? {},
50+
})
6751
}
6852

69-
// Also check the root .gitbook.yaml if it's not already a section config
7053
const rootConfigPath = '.gitbook.yaml'
71-
const rootIsSection = siteSections.some(
72-
(s) => join(s.root, '.gitbook.yaml') === rootConfigPath,
73-
)
74-
54+
const rootIsSection = configs.some((c) => c.configPath === rootConfigPath)
7555
if (!rootIsSection && existsSync(rootConfigPath)) {
7656
const config = YAML.parse(
7757
readFileSync(rootConfigPath, 'utf-8'),
78-
) as GitbookConfig & { root?: string }
79-
const redirects = config.redirects ?? {}
58+
) as GitbookConfig
8059
const root = (config.root ?? './').replace(/^\.\//, '').replace(/\/$/, '')
60+
configs.push({
61+
configPath: rootConfigPath,
62+
root,
63+
redirects: config.redirects ?? {},
64+
})
65+
}
8166

82-
const section = siteSections.find((s) => s.root === root)
83-
67+
for (const { configPath, root, redirects } of configs) {
8468
for (const [source, target] of Object.entries(redirects)) {
85-
for (const other of siteSections) {
86-
if (section != null && other.root === section.root) continue
87-
const otherPrefix = other.urlPrefix.replace(/^\//, '') + '/'
88-
if (otherPrefix !== '/' && source.startsWith(otherPrefix)) {
89-
broken.push({
90-
configPath: rootConfigPath,
91-
source,
92-
target,
93-
reason: `Source belongs to the "${other.name}" section. Move this redirect to ${join(other.root, '.gitbook.yaml')}`,
94-
})
95-
}
69+
const foreignSource = findForeignSection(source, root)
70+
if (foreignSource != null) {
71+
broken.push({
72+
configPath,
73+
source,
74+
target,
75+
reason: `Source belongs to the "${foreignSource}" section. Move this redirect to that section's .gitbook.yaml`,
76+
})
77+
continue
9678
}
9779

98-
for (const other of siteSections) {
99-
if (section != null && other.root === section.root) continue
100-
const otherPrefix = other.urlPrefix.replace(/^\//, '') + '/'
101-
if (otherPrefix !== '/' && target.startsWith(otherPrefix)) {
102-
broken.push({
103-
configPath: rootConfigPath,
104-
source,
105-
target,
106-
reason: `Target points to the "${other.name}" section. Move this redirect to ${join(other.root, '.gitbook.yaml')}`,
107-
})
108-
}
80+
const foreignTarget = findForeignSection(target, root)
81+
if (foreignTarget != null) {
82+
broken.push({
83+
configPath,
84+
source,
85+
target,
86+
reason: `Target points to the "${foreignTarget}" section. Move this redirect to that section's .gitbook.yaml`,
87+
})
88+
continue
10989
}
11090

11191
const fullPath = join(root, target)
11292
if (!existsSync(fullPath) || !statSync(fullPath).isFile()) {
11393
broken.push({
114-
configPath: rootConfigPath,
94+
configPath,
11595
source,
11696
target,
11797
reason: `File not found: ${fullPath}`,

0 commit comments

Comments
 (0)