Skip to content

Commit 9b502da

Browse files
committed
fix(docs): resolve agent index links
1 parent 115c118 commit 9b502da

5 files changed

Lines changed: 88 additions & 22 deletions

File tree

scripts/build-agent-index.ts

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { existsSync } from 'node:fs'
12
import { readFile, writeFile } from 'node:fs/promises'
23
import { dirname, join, resolve } from 'node:path'
34
import { fileURLToPath } from 'node:url'
@@ -6,10 +7,50 @@ import { versions } from '../vocs/versions'
67
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
78
const publicDir = join(root, 'vocs/dist/public')
89
const pagesDir = join(root, 'vocs/docs/pages')
10+
const examplesDir = join(root, 'lib/examples')
911
const baseUrl = 'https://alloy.rs'
1012

13+
function proseLines(markdown: string, transform: (line: string) => string): string {
14+
let inFence = false
15+
16+
return markdown
17+
.split('\n')
18+
.map((line) => {
19+
if (/^\s*(```|~~~)/.test(line)) {
20+
inFence = !inFence
21+
return line
22+
}
23+
return inFence ? line : transform(line)
24+
})
25+
.join('\n')
26+
}
27+
1128
function absoluteLinks(markdown: string): string {
12-
return markdown.replace(/]\(\/(?!\/)/g, `](${baseUrl}/`)
29+
return proseLines(markdown, (line) =>
30+
line
31+
.replace(/(!?\[[^\]]*]\()\/(?!\/)/g, `$1${baseUrl}/`)
32+
.replace(/(\b(?:href|src)\s*=\s*["'])\/(?!\/)/g, `$1${baseUrl}/`),
33+
)
34+
}
35+
36+
function unresolvedLinks(markdown: string): string[] {
37+
const unresolved: string[] = []
38+
proseLines(markdown, (line) => {
39+
const targets = [
40+
...[...line.matchAll(/!?\[[^\]]*]\((<?[^)\s>]+>?)/g)].map((match) => match[1]),
41+
...[...line.matchAll(/\b(?:href|src)\s*=\s*["']([^"']+)["']/g)].map(
42+
(match) => match[1],
43+
),
44+
]
45+
46+
for (const rawTarget of targets) {
47+
const target = rawTarget.replace(/^<|>$/g, '')
48+
if (/^[a-z][a-z+.-]*:/i.test(target) || target.startsWith('//')) continue
49+
unresolved.push(target)
50+
}
51+
return line
52+
})
53+
return unresolved
1354
}
1455

1556
const llmsPath = join(publicDir, 'llms.txt')
@@ -20,13 +61,35 @@ const llmsFull = absoluteLinks(await readFile(llmsFullPath, 'utf8'))
2061
await writeFile(llmsPath, llms)
2162
await writeFile(llmsFullPath, llmsFull)
2263

23-
if (/]\(\/(?!\/)/.test(llms) || /]\(\/(?!\/)/.test(llmsFull)) {
24-
throw new Error('Agent text still contains root-relative Markdown links')
64+
const unresolved = [...unresolvedLinks(llms), ...unresolvedLinks(llmsFull)]
65+
if (unresolved.length) {
66+
throw new Error(`Agent text still contains unresolved links: ${[...new Set(unresolved)].join(', ')}`)
2567
}
2668

2769
const sourceByRoute = new Map<string, string>()
70+
const exampleSourceByRoute = new Map<string, string>()
2871
for (const file of new Bun.Glob('**/*.{md,mdx}').scanSync({ cwd: pagesDir, onlyFiles: true })) {
29-
sourceByRoute.set(`/${file.replace(/\\/g, '/').replace(/\.(md|mdx)$/, '')}`, file)
72+
const pathname = `/${file.replace(/\\/g, '/').replace(/\.(md|mdx)$/, '')}`
73+
sourceByRoute.set(pathname, file)
74+
75+
if (!pathname.startsWith('/examples/') || pathname.endsWith('/README')) continue
76+
77+
const text = await readFile(join(pagesDir, file), 'utf8')
78+
const match = /https:\/\/github\.com\/alloy-rs\/examples\/(?:blob|tree)\/[^/)\s]+\/(examples\/[^)\s]+\.rs)/.exec(text)
79+
if (!match) throw new Error(`Generated example page ${pathname} has no source metadata`)
80+
if (!existsSync(join(examplesDir, match[1]))) {
81+
throw new Error(`Generated example page ${pathname} references missing source ${match[1]}`)
82+
}
83+
84+
exampleSourceByRoute.set(pathname, match[0].replace('/tree/', '/blob/'))
85+
}
86+
87+
function exampleSourceFor(pathname: string): string | null {
88+
if (!pathname.startsWith('/examples/') || pathname.endsWith('/README')) return null
89+
90+
const source = exampleSourceByRoute.get(pathname)
91+
if (!source) throw new Error(`Example page ${pathname} has no verified source URL`)
92+
return source
3093
}
3194

3295
function kindFor(pathname: string): string {
@@ -43,8 +106,6 @@ const pages = [...llms.matchAll(/^- \[([^\]]+)]\((https:\/\/alloy\.rs\/[^)]+)\)(
43106
const pathname = new URL(url).pathname
44107
const source = sourceByRoute.get(pathname)
45108
const parts = pathname.split('/').filter(Boolean)
46-
const category = parts[1]
47-
const name = parts[2]
48109

49110
return {
50111
id: pathname,
@@ -56,10 +117,7 @@ const pages = [...llms.matchAll(/^- \[([^\]]+)]\((https:\/\/alloy\.rs\/[^)]+)\)(
56117
source_url: source
57118
? `https://github.com/alloy-rs/docs/blob/main/vocs/docs/pages/${source}`
58119
: null,
59-
example_source_url:
60-
parts[0] === 'examples' && category && name && name !== 'README'
61-
? `https://github.com/alloy-rs/examples/blob/main/examples/${category}/examples/${name}.rs`
62-
: null,
120+
example_source_url: exampleSourceFor(pathname),
63121
}
64122
})
65123
.filter((page, index, all) => all.findIndex((candidate) => candidate.url === page.url) === index)
@@ -89,10 +147,7 @@ for (const [pathname, source] of sourceByRoute) {
89147
section: parts[0] ?? 'home',
90148
url: `${baseUrl}${pathname}`,
91149
source_url: `https://github.com/alloy-rs/docs/blob/main/vocs/docs/pages/${source}`,
92-
example_source_url:
93-
parts[0] === 'examples' && category && name && name !== 'README'
94-
? `https://github.com/alloy-rs/examples/blob/main/examples/${category}/examples/${name}.rs`
95-
: null,
150+
example_source_url: exampleSourceFor(pathname),
96151
})
97152
indexedPaths.add(pathname)
98153
}

scripts/check-docs.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,15 @@ function validateTarget(sourceRoute: string, sourceFile: string, rawTarget: stri
6565
const withoutQuery = withoutHash.split('?', 1)[0]
6666
let route = sourceRoute
6767

68+
if (target.startsWith('#')) {
69+
errors.push(`${sourceFile}: internal links must be root-relative: ${rawTarget}`)
70+
}
71+
6872
if (withoutQuery) {
73+
if (!withoutQuery.startsWith('/')) {
74+
errors.push(`${sourceFile}: internal links must be root-relative: ${rawTarget}`)
75+
}
76+
6977
if (withoutQuery.startsWith('/')) {
7078
route = withoutQuery.length > 1 ? withoutQuery.replace(/\/$/, '') : '/'
7179
if (route === '/' || existsSync(join(publicRoot, route.slice(1)))) return
@@ -113,6 +121,9 @@ for (const [route, page] of pages) {
113121
for (const match of page.text.matchAll(/!?\[[^\]]*]\((<?[^)\s>]+>?)/g)) {
114122
validateTarget(route, page.file, match[1])
115123
}
124+
for (const match of page.text.matchAll(/\b(?:href|src)\s*=\s*["']([^"']+)["']/g)) {
125+
validateTarget(route, page.file, match[1])
126+
}
116127
}
117128

118129
const sidebar = readFileSync(sidebarPath, 'utf8')

vocs/docs/pages/migrating-from-ethers/reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ The following is a reference guide for finding the migration path for your speci
2323
- Compilers: [`ethers::solc`](https://github.com/gakonst/ethers-rs/tree/master/ethers-solc) `->` [`foundry-compilers`](https://github.com/foundry-rs/compilers)
2424
- Contract: [`ethers::contract`](https://github.com/gakonst/ethers-rs/tree/master/ethers-contract) `->` [`alloy::contract`](https://github.com/alloy-rs/alloy/tree/main/crates/contract)
2525
- Core: [`ethers::core`](https://github.com/gakonst/ethers-rs/tree/master/ethers-core) `->` [`alloy::core`](https://github.com/alloy-rs/core)
26-
- Types: [`ethers::core::types::*`](https://github.com/gakonst/ethers-rs/tree/master/ethers-core/src/types) `->` See [Types](#types) section
26+
- Types: [`ethers::core::types::*`](https://github.com/gakonst/ethers-rs/tree/master/ethers-core/src/types) `->` See [Types](/migrating-from-ethers/reference#types) section
2727
- Etherscan: [`ethers::etherscan`](https://github.com/gakonst/ethers-rs/tree/master/ethers-etherscan) `->` [`foundry-block-explorers`](https://github.com/foundry-rs/block-explorers)
2828
- Middleware: [`ethers::middleware`](https://github.com/gakonst/ethers-rs/tree/master/ethers-middleware) `->` Fillers [`alloy::provider::{fillers, layers}`](https://github.com/alloy-rs/alloy/tree/main/crates/provider/src)
2929
- Gas oracle: [`ethers::middleware::GasOracleMiddleware`](https://github.com/gakonst/ethers-rs/tree/master/ethers-middleware/src/gas_oracle/middleware.rs) `->` Gas filler [`alloy::provider::GasFiller`](https://github.com/alloy-rs/examples/tree/main/examples/fillers/examples/gas_filler.rs)

vocs/docs/pages/migrating-to-core-v1/encoding-decoding-changes/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ description: Overview of ABI encoding and decoding changes in Alloy v1.0
44

55
## Simply ABI encoding and decoding
66

7-
- [ABI encoding function return structs](./encoding-return-structs.md)
8-
- [Removing `validate: bool` from the `abi_decode` methods](./removing-validate-bool.md)
7+
- [ABI encoding function return structs](/migrating-to-core-v1/encoding-decoding-changes/encoding-return-structs)
8+
- [Removing `validate: bool` from the `abi_decode` methods](/migrating-to-core-v1/encoding-decoding-changes/removing-validate-bool)

vocs/docs/pages/migrating-to-core-v1/sol!-changes/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ description: Overview of changes to the sol! macro bindings in Alloy v1.0
44

55
## sol! changes
66

7-
- [Removing the `T` transport generic](./removing-T-generic.md)
8-
- [Improving function return type](./improving-function-return-types.md)
9-
- [Changes to function call bindings](./changes-to-function-call-bindings.md)
10-
- [Changes to event bindings](./changes-to-event-bindings.md)
11-
- [Changes to error bindings](./changes-to-error-bindings.md)
7+
- [Removing the `T` transport generic](/migrating-to-core-v1/sol!-changes/removing-T-generic)
8+
- [Improving function return type](/migrating-to-core-v1/sol!-changes/improving-function-return-types)
9+
- [Changes to function call bindings](/migrating-to-core-v1/sol!-changes/changes-to-function-call-bindings)
10+
- [Changes to event bindings](/migrating-to-core-v1/sol!-changes/changes-to-event-bindings)
11+
- [Changes to error bindings](/migrating-to-core-v1/sol!-changes/changes-to-error-bindings)

0 commit comments

Comments
 (0)