forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathupdate-internal-links.js
executable file
·192 lines (175 loc) · 6.45 KB
/
update-internal-links.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
#!/usr/bin/env node
// [start-readme]
//
// Run this script to update content's internal links.
// It can correct the title part or the URL part or both.
//
// Best way to understand how to use it is to run it with `--help`.
//
// [end-readme]
import fs from 'fs'
import path from 'path'
import { program } from 'commander'
import chalk from 'chalk'
import { updateInternalLinks } from '../lib/update-internal-links.js'
import frontmatter from '../lib/read-frontmatter.js'
import walkFiles from './helpers/walk-files.js'
program
.description('Update internal links in content files')
.option('-v, --verbose', 'Verbose outputs')
.option('--debug', "Don't hide any errors")
.option('--dry-run', "Don't actually write changes to disk")
.option('--dont-set-autotitle', "Do NOT transform the link text to 'AUTOTITLE' (if applicable)")
.option('--dont-fix-href', 'Do NOT fix the link href value (if necessary)')
.option('--check', 'Exit and fail if it found something to fix')
.option('--aggregate-stats', 'Display aggregate numbers about all possible changes')
.option('--strict', "Throw an error (instead of a warning) if a link can't be processed")
.option('--exclude [paths...]', 'Specific files to exclude')
.arguments('[files-or-directories...]', '')
.parse(process.argv)
main(program.args, program.opts())
async function main(files, opts) {
const { debug } = opts
const excludeFilePaths = new Set(opts.exclude || [])
try {
if (opts.check && !opts.dryRun) {
throw new Error("Can't use --check without --dry-run")
}
const actualFiles = []
if (!files.length) {
files.push('content', 'data')
}
for (const file of files) {
if (
!(
file.startsWith('content') ||
file.startsWith('data') ||
file.startsWith('tests/fixtures')
)
) {
throw new Error(`${file} must be a content or data filepath`)
}
if (!fs.existsSync(file)) {
throw new Error(`${file} does not exist`)
}
if (fs.lstatSync(file).isDirectory()) {
actualFiles.push(
...walkFiles(file, ['.md', '.yml']).filter((p) => {
return !excludeFilePaths.has(p)
})
)
} else if (!excludeFilePaths.has(file)) {
actualFiles.push(file)
}
}
if (!actualFiles.length) {
throw new Error(`No files found in ${files}`)
}
// The updateInternalLinks doesn't use "negatives" for certain options
const options = {
setAutotitle: !opts.dontSetAutotitle,
fixHref: !opts.dontFixHref,
verbose: !!opts.verbose,
strict: !!opts.strict,
}
// Remember, updateInternalLinks() doesn't actually change the files
// on disk. That's the responsibility of the caller, i.e. this CLI script.
// The reason why is that updateInternalLinks() can then see if ALL
// improvements are going to work. For example, if you tried run
// it across 10 links and the 7th one had a corrupt broken link that
// can't be corrected, it needs to fail there and then instead of
// leaving 6 of the 10 files changed.
const results = await updateInternalLinks(actualFiles, options)
let exitCheck = 0
for (const { file, content, newContent, replacements, data } of results) {
if (content !== newContent) {
if (opts.verbose || opts.check) {
if (opts.check) {
exitCheck++
}
if (opts.verbose) {
console.log(
opts.dryRun ? 'Would change...' : 'Will change...',
chalk.bold(file),
chalk.dim(`${replacements.length} change${replacements.length !== 1 ? 's' : ''}`)
)
for (const { asMarkdown, newAsMarkdown, line, column } of replacements) {
console.log(' ', chalk.red(asMarkdown))
console.log(' ', chalk.green(newAsMarkdown))
console.log(' ', chalk.dim(`line ${line} column ${column}`))
console.log('')
}
}
}
if (!opts.dryRun) {
// Remember the `content` and `newContent` is the "meat" of the
// Markdown page. To save it you need the frontmatter data too.
fs.writeFileSync(
file,
frontmatter.stringify(newContent, data, { lineWidth: 10000 }),
'utf-8'
)
}
}
}
if (opts.aggregateStats) {
const countFiles = results.length
const countChangedFiles = new Set(results.filter((result) => result.replacements.length > 0))
.size
const countReplacements = results.reduce((prev, next) => prev + next.replacements.length, 0)
console.log('Number of files checked:'.padEnd(30), chalk.bold(countFiles.toLocaleString()))
console.log(
'Number of files changed:'.padEnd(30),
chalk.bold(countChangedFiles.toLocaleString())
)
console.log(
'Sum number of replacements:'.padEnd(30),
chalk.bold(countReplacements.toLocaleString())
)
countByTree(results)
}
if (exitCheck) {
console.log(chalk.yellow(`More than one file would become different. Unsuccessful check.`))
process.exit(exitCheck)
} else if (opts.check) {
console.log(chalk.green('No changes needed or necessary. 🌈'))
}
} catch (err) {
if (debug) {
throw err
}
console.error(chalk.red(err.toString()))
process.exit(1)
}
}
function countByTree(results) {
const files = {}
const changes = {}
for (const { file, replacements } of results) {
const split = path.dirname(file).split(path.sep)
while (split.length > 1) {
const parent = split.slice(1).join(path.sep)
files[parent] = (replacements.length > 0 ? 1 : 0) + (files[parent] || 0)
changes[parent] = replacements.length + (changes[parent] || 0)
split.pop()
}
}
const longest = Math.max(...Object.keys(changes).map((x) => x.split(path.sep).at(-1).length))
const padding = longest + 10
const col0 = 'TREE'
const col1 = 'FILES '
console.log('\n')
console.log(`${col0.padEnd(padding)}${col1} CHANGES`)
for (const each of Object.keys(changes).sort()) {
if (!changes[each]) continue
const split = each.split(path.sep)
const last = split.at(-1)
const indentation = split.length - 1
const indentationPad = indentation ? `${' '.repeat(indentation)} ↳ ` : ''
console.log(
`${indentationPad}${last.padEnd(padding - indentationPad.length)} ${String(
files[each]
).padEnd(col1.length)} ${changes[each]}`
)
}
}