Skip to content

Commit d6c6991

Browse files
committed
Introduce scripts to shrink images
1 parent 2e8637f commit d6c6991

File tree

2 files changed

+1056
-0
lines changed

2 files changed

+1056
-0
lines changed

find-unused-images.js

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
#!/usr/bin/env node
2+
3+
import fs from 'fs';
4+
import path from 'path';
5+
import { fileURLToPath } from 'url';
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = path.dirname(__filename);
9+
10+
// Configuration
11+
const CONFIG = {
12+
imagesDir: 'static/images',
13+
docsDir: 'docs',
14+
rootFiles: ['.'], // Check root markdown files too
15+
imageExtensions: ['.gif', '.png', '.jpg', '.jpeg', '.webp', '.svg'],
16+
markupExtensions: ['.md', '.mdx', '.js', '.ts', '.tsx', '.jsx'], // Include JS/TS for potential imports
17+
verbose: false
18+
};
19+
20+
class UnusedImageFinder {
21+
constructor(config) {
22+
this.config = config;
23+
this.stats = {
24+
totalImages: 0,
25+
referencedImages: 0,
26+
unusedImages: 0,
27+
totalMarkupFiles: 0
28+
};
29+
this.imageReferences = new Set(); // Track which images are referenced
30+
}
31+
32+
log(message, level = 'info') {
33+
if (!this.config.verbose && level === 'debug') return;
34+
const prefix = level === 'error' ? '❌' : level === 'warn' ? '⚠️' : level === 'debug' ? '🔍' : '';
35+
if (prefix) {
36+
console.error(`${prefix} ${message}`);
37+
} else {
38+
console.error(message); // Use stderr for logging, stdout for results
39+
}
40+
}
41+
42+
// Recursively find all files with given extensions
43+
findFiles(dir, extensions) {
44+
const files = [];
45+
46+
const walk = (currentDir) => {
47+
try {
48+
const items = fs.readdirSync(currentDir);
49+
for (const item of items) {
50+
const fullPath = path.join(currentDir, item);
51+
let stat;
52+
try {
53+
stat = fs.statSync(fullPath);
54+
} catch (error) {
55+
continue; // Skip inaccessible files
56+
}
57+
58+
if (stat.isDirectory()) {
59+
// Skip node_modules and other common ignore patterns
60+
if (!item.startsWith('.') && item !== 'node_modules') {
61+
walk(fullPath);
62+
}
63+
} else if (extensions.some(ext => item.toLowerCase().endsWith(ext.toLowerCase()))) {
64+
files.push(fullPath);
65+
}
66+
}
67+
} catch (error) {
68+
this.log(`Cannot read directory ${currentDir}: ${error.message}`, 'warn');
69+
}
70+
};
71+
72+
walk(dir);
73+
return files;
74+
}
75+
76+
// Get all images in the repository
77+
getAllImages() {
78+
const imageFiles = this.findFiles(this.config.imagesDir, this.config.imageExtensions);
79+
this.stats.totalImages = imageFiles.length;
80+
this.log(`Found ${imageFiles.length} images`, 'debug');
81+
return imageFiles;
82+
}
83+
84+
// Get all markup files that could reference images
85+
getAllMarkupFiles() {
86+
const markupFiles = [];
87+
88+
// Check docs directory
89+
if (fs.existsSync(this.config.docsDir)) {
90+
markupFiles.push(...this.findFiles(this.config.docsDir, this.config.markupExtensions));
91+
}
92+
93+
// Check root files
94+
const rootMarkupFiles = this.findFiles('.', this.config.markupExtensions)
95+
.filter(file => !file.includes('node_modules') && !file.includes(this.config.docsDir));
96+
markupFiles.push(...rootMarkupFiles);
97+
98+
this.stats.totalMarkupFiles = markupFiles.length;
99+
this.log(`Found ${markupFiles.length} markup files`, 'debug');
100+
return markupFiles;
101+
}
102+
103+
// Extract image references from a file's content
104+
extractImageReferences(filePath, content) {
105+
const references = new Set();
106+
107+
// Pattern 1: Markdown image syntax ![alt](path)
108+
const markdownImagePattern = /!\[[^\]]*\]\(([^)]+)\)/g;
109+
let match;
110+
while ((match = markdownImagePattern.exec(content)) !== null) {
111+
references.add(this.normalizeImagePath(match[1], filePath));
112+
}
113+
114+
// Pattern 2: HTML img src
115+
const htmlImagePattern = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
116+
while ((match = htmlImagePattern.exec(content)) !== null) {
117+
references.add(this.normalizeImagePath(match[1], filePath));
118+
}
119+
120+
// Pattern 3: Import statements (for JS/TS files)
121+
const importPattern = /import\s+[^'"]*['"]([^'"]*\.(?:png|jpg|jpeg|gif|webp|svg))['"];?/gi;
122+
while ((match = importPattern.exec(content)) !== null) {
123+
references.add(this.normalizeImagePath(match[1], filePath));
124+
}
125+
126+
// Pattern 4: require() calls for images
127+
const requirePattern = /require\s*\(\s*['"]([^'"]*\.(?:png|jpg|jpeg|gif|webp|svg))['"]?\s*\)/gi;
128+
while ((match = requirePattern.exec(content)) !== null) {
129+
references.add(this.normalizeImagePath(match[1], filePath));
130+
}
131+
132+
// Pattern 5: URL() in CSS-like content
133+
const urlPattern = /url\s*\(\s*['"]?([^'"]*\.(?:png|jpg|jpeg|gif|webp|svg))['"]?\s*\)/gi;
134+
while ((match = urlPattern.exec(content)) !== null) {
135+
references.add(this.normalizeImagePath(match[1], filePath));
136+
}
137+
138+
return Array.from(references);
139+
}
140+
141+
// Normalize image paths to match actual file paths
142+
normalizeImagePath(imagePath, referencingFile) {
143+
// Remove query parameters and fragments
144+
imagePath = imagePath.split('?')[0].split('#')[0];
145+
146+
// Skip external URLs
147+
if (imagePath.startsWith('http://') || imagePath.startsWith('https://') || imagePath.startsWith('//')) {
148+
return null;
149+
}
150+
151+
let normalizedPath;
152+
153+
if (imagePath.startsWith('static/images/')) {
154+
// Absolute reference from project root
155+
normalizedPath = imagePath;
156+
} else if (imagePath.startsWith('images/')) {
157+
// Relative to static/ directory
158+
normalizedPath = `static/${imagePath}`;
159+
} else if (imagePath.startsWith('./images/') || imagePath.startsWith('../')) {
160+
// Relative path - need to resolve based on referencing file location
161+
const referencingDir = path.dirname(referencingFile);
162+
const resolved = path.resolve(referencingDir, imagePath);
163+
const relative = path.relative('.', resolved);
164+
normalizedPath = relative.replace(/\\/g, '/'); // Normalize path separators
165+
} else if (imagePath.startsWith('/')) {
166+
// Absolute path from web root - assume it's in static/
167+
normalizedPath = `static${imagePath}`;
168+
} else {
169+
// Relative path without explicit prefix
170+
if (imagePath.includes('/')) {
171+
// Has directory structure, likely relative to static/images
172+
normalizedPath = `static/images/${imagePath}`;
173+
} else {
174+
// Just a filename, could be in various locations
175+
normalizedPath = imagePath;
176+
}
177+
}
178+
179+
return normalizedPath;
180+
}
181+
182+
// Scan all markup files for image references
183+
scanForImageReferences() {
184+
const markupFiles = this.getAllMarkupFiles();
185+
186+
for (const filePath of markupFiles) {
187+
try {
188+
const content = fs.readFileSync(filePath, 'utf8');
189+
const references = this.extractImageReferences(filePath, content);
190+
191+
for (const ref of references) {
192+
if (ref) {
193+
this.imageReferences.add(ref);
194+
this.log(`${filePath} references: ${ref}`, 'debug');
195+
}
196+
}
197+
} catch (error) {
198+
this.log(`Could not read ${filePath}: ${error.message}`, 'warn');
199+
}
200+
}
201+
202+
this.log(`Found ${this.imageReferences.size} unique image references`, 'debug');
203+
}
204+
205+
// Check if an image file is referenced
206+
isImageReferenced(imagePath) {
207+
// Try exact match first
208+
if (this.imageReferences.has(imagePath)) {
209+
return true;
210+
}
211+
212+
// Try various normalizations
213+
const relativePath = path.relative('.', imagePath).replace(/\\/g, '/');
214+
if (this.imageReferences.has(relativePath)) {
215+
return true;
216+
}
217+
218+
// Check if any reference ends with this file's path
219+
const fileName = path.basename(imagePath);
220+
for (const ref of this.imageReferences) {
221+
if (ref.endsWith(imagePath) || ref.endsWith(relativePath) || ref.endsWith(fileName)) {
222+
return true;
223+
}
224+
}
225+
226+
// Check path variations
227+
const variations = [
228+
imagePath.replace('static/images/', 'images/'),
229+
imagePath.replace('static/', ''),
230+
`/${imagePath}`,
231+
`/${relativePath}`
232+
];
233+
234+
for (const variation of variations) {
235+
if (this.imageReferences.has(variation)) {
236+
return true;
237+
}
238+
}
239+
240+
return false;
241+
}
242+
243+
// Main function to find unused images
244+
findUnusedImages() {
245+
this.log('🔍 Finding unused images in Discord API docs...');
246+
247+
// Get all images and references
248+
const allImages = this.getAllImages();
249+
this.scanForImageReferences();
250+
251+
// Find unused images
252+
const unusedImages = [];
253+
254+
for (const imagePath of allImages) {
255+
if (!this.isImageReferenced(imagePath)) {
256+
unusedImages.push(imagePath);
257+
this.stats.unusedImages++;
258+
} else {
259+
this.stats.referencedImages++;
260+
}
261+
}
262+
263+
// Output results
264+
for (const unusedImage of unusedImages) {
265+
console.log(unusedImage); // Output to stdout for piping
266+
}
267+
268+
// Log summary to stderr
269+
this.log(`\n📊 Summary:`);
270+
this.log(` Total images: ${this.stats.totalImages}`);
271+
this.log(` Referenced images: ${this.stats.referencedImages}`);
272+
this.log(` Unused images: ${this.stats.unusedImages}`);
273+
this.log(` Markup files scanned: ${this.stats.totalMarkupFiles}`);
274+
275+
if (this.stats.unusedImages > 0) {
276+
this.log(`\n💡 To delete unused images: node find-unused-images.js | xargs rm`);
277+
this.log(` Or to see sizes: node find-unused-images.js | xargs ls -lh`);
278+
}
279+
280+
return unusedImages;
281+
}
282+
}
283+
284+
// CLI handling
285+
if (import.meta.url === `file://${process.argv[1]}`) {
286+
const args = process.argv.slice(2);
287+
const verbose = args.includes('--verbose') || args.includes('-v');
288+
289+
if (args.includes('--help') || args.includes('-h')) {
290+
console.log(`
291+
Discord API Docs - Find Unused Images
292+
293+
Usage: node find-unused-images.js [options]
294+
295+
Options:
296+
-v, --verbose Show detailed logging
297+
-h, --help Show this help message
298+
299+
Output:
300+
Prints unused image paths to stdout (one per line)
301+
Logs summary and progress to stderr
302+
303+
Examples:
304+
node find-unused-images.js # Find unused images
305+
node find-unused-images.js | wc -l # Count unused images
306+
node find-unused-images.js | xargs ls -lh # Show sizes of unused images
307+
node find-unused-images.js | xargs rm # Delete unused images (careful!)
308+
`);
309+
process.exit(0);
310+
}
311+
312+
const config = { ...CONFIG, verbose };
313+
const finder = new UnusedImageFinder(config);
314+
315+
try {
316+
finder.findUnusedImages();
317+
} catch (error) {
318+
console.error('❌ Failed to find unused images:', error.message);
319+
if (verbose) {
320+
console.error(error.stack);
321+
}
322+
process.exit(1);
323+
}
324+
}
325+
326+
export { UnusedImageFinder };

0 commit comments

Comments
 (0)