forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
find-orphaned-assets.mjs
executable file
·128 lines (112 loc) · 3.3 KB
/
find-orphaned-assets.mjs
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
#!/usr/bin/env node
// [start-readme]
//
// Print a list of all the asset files that can't be found mentioned
// in any of the source files (content & code).
//
// [end-readme]
import fs from 'fs'
import path from 'path'
import program from 'commander'
import walk from 'walk-sync'
const EXCEPTIONS = new Set(['assets/images/site/favicon.ico'])
function isExceptionPath(imagePath) {
// We also check for .DS_Store because any macOS user that has opened
// a folder with images will have this on disk. It won't get added
// to git anyway thanks to our .DS_Store.
// But if we don't make it a valid exception, it can become inconvenient
// to run this script locally.
return EXCEPTIONS.has(imagePath) || path.basename('.DS_Store')
}
program
.description('Print all images that are in ./assets/ but not found in any source files')
.option('-e, --exit', 'Exit script by count of orphans (useful for CI)')
.option('-v, --verbose', 'Verbose outputs')
.option('--json', 'Output in JSON format')
.option('--exclude-translations', "Don't search in translations/")
.parse(process.argv)
main(program.opts(), program.args)
async function main(opts) {
const { json, verbose, exit, excludeTranslations } = opts
const walkOptions = {
directories: false,
includeBasePath: true,
}
const sourceFiles = []
const roots = [
'content',
'data',
'tests',
'components',
'script',
'stylesheets',
'contributing',
'pages',
]
if (!excludeTranslations) {
roots.push('translations')
}
for (const root of roots) {
sourceFiles.push(
...walk(
root,
Object.assign(
{
globs: ['!**/*.+(png|csv|graphql|json|svg)'],
},
walkOptions
)
)
)
}
// Add exceptions
sourceFiles.push('CONTRIBUTING.md')
sourceFiles.push('README.md')
verbose && console.log(`${sourceFiles.length.toLocaleString()} source files found in total.`)
const allImages = new Set(
walk(
'assets',
Object.assign(
{
globs: ['!**/*.+(md)'],
},
walkOptions
)
).filter((filePath) => !filePath.endsWith('.md'))
)
verbose && console.log(`${allImages.size.toLocaleString()} images found in total.`)
for (const sourceFile of sourceFiles) {
const content = fs.readFileSync(sourceFile, 'utf-8')
for (const imagePath of allImages) {
const needle = imagePath.split(path.sep).slice(-2).join('/')
if (content.includes(needle) || isExceptionPath(imagePath)) {
allImages.delete(imagePath)
}
}
}
if (verbose && allImages.size) {
console.log('The following files are not mentioned anywhere in any source file')
}
if (json) {
console.log(JSON.stringify([...allImages], undefined, 2))
} else {
for (const imagePath of [...allImages].sort((a, b) => a.localeCompare(b))) {
console.log(imagePath)
}
}
if (verbose) {
console.log(`${allImages.size.toLocaleString()} orphans left.`)
const totalDiskSize = getTotalDiskSize(allImages)
console.log(`Total disk size of all of these: ${(totalDiskSize / 1024 / 1024).toFixed(1)}MB`)
}
if (exit) {
process.exit(allImages.size)
}
}
function getTotalDiskSize(filePaths) {
let sum = 0
for (const filePath of filePaths) {
sum += fs.statSync(filePath).size
}
return sum
}