| #!/usr/bin/env node |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import fs from 'fs/promises'; |
| import path from 'path'; |
| import { fileURLToPath } from 'url'; |
|
|
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| const APP_ROOT = path.resolve(__dirname, '..'); |
| const CONTENT_DIR = path.join(APP_ROOT, 'src', 'content'); |
| const EMBEDS_DIR = path.join(CONTENT_DIR, 'embeds'); |
| const IMAGES_DIR = path.join(CONTENT_DIR, 'assets', 'image'); |
| const DATA_DIR = path.join(CONTENT_DIR, 'assets', 'data'); |
| const OUTPUT_DIR = path.join(APP_ROOT, 'src', 'generated'); |
| const OUTPUT_FILE = path.join(OUTPUT_DIR, 'used-assets.json'); |
|
|
| const args = process.argv.slice(2); |
| const isVerbose = args.includes('--verbose') || args.includes('-v'); |
| const isJson = args.includes('--json'); |
|
|
| |
| |
| |
| async function dirExists(dir) { |
| try { |
| const stat = await fs.stat(dir); |
| return stat.isDirectory(); |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| async function findFiles(dir, extensions, files = [], baseDir = null) { |
| if (!await dirExists(dir)) return files; |
| |
| baseDir = baseDir || dir; |
| const entries = await fs.readdir(dir, { withFileTypes: true }); |
| |
| for (const entry of entries) { |
| const fullPath = path.join(dir, entry.name); |
| |
| if (entry.isDirectory()) { |
| |
| if (!entry.name.startsWith('.') && entry.name !== 'node_modules') { |
| await findFiles(fullPath, extensions, files, baseDir); |
| } |
| } else { |
| const ext = path.extname(entry.name).toLowerCase(); |
| if (extensions.includes(ext)) { |
| |
| files.push(path.relative(baseDir, fullPath)); |
| } |
| } |
| } |
| |
| return files; |
| } |
|
|
| |
| |
| |
| async function findMdxFiles(dir, files = []) { |
| const entries = await fs.readdir(dir, { withFileTypes: true }); |
| |
| for (const entry of entries) { |
| const fullPath = path.join(dir, entry.name); |
| |
| if (entry.isDirectory()) { |
| |
| if (!entry.name.startsWith('.') && entry.name !== 'node_modules') { |
| await findMdxFiles(fullPath, files); |
| } |
| } else if (entry.name.endsWith('.mdx')) { |
| files.push(fullPath); |
| } |
| } |
| |
| return files; |
| } |
|
|
| |
| |
| |
| function extractEmbedSources(content) { |
| const sources = new Set(); |
| |
| |
| |
| const regex = /<HtmlEmbed[^>]*\bsrc=["']([^"']+)["']/g; |
| let match; |
| |
| while ((match = regex.exec(content)) !== null) { |
| let src = match[1]; |
| |
| src = src.replace(/^\//, '').replace(/^embeds\//, ''); |
| sources.add(src); |
| } |
| |
| return [...sources]; |
| } |
|
|
| |
| |
| |
| function extractImageImports(content) { |
| const images = new Set(); |
| |
| |
| |
| const regex = /import\s+\w+\s+from\s+['"]\.\/assets\/image\/([^'"]+)['"]/g; |
| let match; |
| |
| while ((match = regex.exec(content)) !== null) { |
| images.add(match[1]); |
| } |
| |
| |
| const regexRelative = /import\s+\w+\s+from\s+['"]\.\.\/assets\/image\/([^'"]+)['"]/g; |
| while ((match = regexRelative.exec(content)) !== null) { |
| images.add(match[1]); |
| } |
| |
| |
| const regexDeeper = /import\s+\w+\s+from\s+['"](?:\.\.\/)+assets\/image\/([^'"]+)['"]/g; |
| while ((match = regexDeeper.exec(content)) !== null) { |
| images.add(match[1]); |
| } |
| |
| return [...images]; |
| } |
|
|
| |
| |
| |
| function extractDataFiles(content) { |
| const dataFiles = new Set(); |
| |
| |
| const regexSingle = /<HtmlEmbed[^>]*\bdata=["']([^"']+)["']/g; |
| let match; |
| |
| while ((match = regexSingle.exec(content)) !== null) { |
| dataFiles.add(match[1]); |
| } |
| |
| |
| const regexArray = /<HtmlEmbed[^>]*\bdata=\{\[([^\]]+)\]\}/g; |
| while ((match = regexArray.exec(content)) !== null) { |
| const arrayContent = match[1]; |
| |
| const stringRegex = /["']([^"']+)["']/g; |
| let strMatch; |
| while ((strMatch = stringRegex.exec(arrayContent)) !== null) { |
| dataFiles.add(strMatch[1]); |
| } |
| } |
| |
| return [...dataFiles]; |
| } |
|
|
| |
| |
| |
| async function extractAssets() { |
| console.log('๐ Extracting used assets from MDX files...\n'); |
| |
| |
| const mdxFiles = await findMdxFiles(CONTENT_DIR); |
| |
| if (isVerbose) { |
| console.log(`Found ${mdxFiles.length} MDX files:\n`); |
| mdxFiles.forEach(f => console.log(` - ${path.relative(CONTENT_DIR, f)}`)); |
| console.log(''); |
| } |
| |
| |
| const existingEmbeds = new Set(await findFiles(EMBEDS_DIR, ['.html'])); |
| const existingImages = new Set(await findFiles(IMAGES_DIR, ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'])); |
| const existingData = new Set(await findFiles(DATA_DIR, ['.csv', '.json', '.tsv'])); |
| |
| const result = { |
| generatedAt: new Date().toISOString(), |
| embeds: new Set(), |
| images: new Set(), |
| dataFiles: new Set(), |
| byFile: {} |
| }; |
| |
| |
| for (const file of mdxFiles) { |
| const content = await fs.readFile(file, 'utf-8'); |
| const relativePath = path.relative(CONTENT_DIR, file); |
| |
| const embeds = extractEmbedSources(content); |
| const images = extractImageImports(content); |
| const dataFiles = extractDataFiles(content); |
| |
| |
| embeds.forEach(e => result.embeds.add(e)); |
| images.forEach(i => result.images.add(i)); |
| dataFiles.forEach(d => result.dataFiles.add(d)); |
| |
| |
| if (embeds.length > 0 || images.length > 0 || dataFiles.length > 0) { |
| result.byFile[relativePath] = { |
| embeds: embeds.length > 0 ? embeds : undefined, |
| images: images.length > 0 ? images : undefined, |
| dataFiles: dataFiles.length > 0 ? dataFiles : undefined |
| }; |
| } |
| } |
| |
| |
| const usedEmbeds = [...result.embeds]; |
| const usedImages = [...result.images]; |
| const usedData = [...result.dataFiles]; |
| |
| const missingEmbeds = usedEmbeds.filter(e => !existingEmbeds.has(e)); |
| const unusedEmbeds = [...existingEmbeds].filter(e => !usedEmbeds.includes(e)); |
| |
| const missingImages = usedImages.filter(i => !existingImages.has(i)); |
| const unusedImages = [...existingImages].filter(i => !usedImages.includes(i)); |
| |
| |
| const normalizeDataPath = (p) => p.replace(/^data\//, ''); |
| const usedDataNormalized = usedData.map(normalizeDataPath); |
| const missingData = usedDataNormalized.filter(d => !existingData.has(d)); |
| const unusedData = [...existingData].filter(d => !usedDataNormalized.includes(d)); |
| |
| |
| const output = { |
| generatedAt: result.generatedAt, |
| summary: { |
| totalMdxFiles: mdxFiles.length, |
| embeds: { |
| used: result.embeds.size, |
| existing: existingEmbeds.size, |
| missing: missingEmbeds.length, |
| unused: unusedEmbeds.length |
| }, |
| images: { |
| used: result.images.size, |
| existing: existingImages.size, |
| missing: missingImages.length, |
| unused: unusedImages.length |
| }, |
| dataFiles: { |
| used: result.dataFiles.size, |
| existing: existingData.size, |
| missing: missingData.length, |
| unused: unusedData.length |
| } |
| }, |
| embeds: { |
| used: [...result.embeds].sort(), |
| missing: missingEmbeds.sort(), |
| unused: unusedEmbeds.sort() |
| }, |
| images: { |
| used: [...result.images].sort(), |
| missing: missingImages.sort(), |
| unused: unusedImages.sort() |
| }, |
| dataFiles: { |
| used: [...result.dataFiles].sort(), |
| missing: missingData.sort(), |
| unused: unusedData.sort() |
| }, |
| byFile: result.byFile |
| }; |
| |
| |
| await fs.mkdir(OUTPUT_DIR, { recursive: true }); |
| |
| |
| await fs.writeFile(OUTPUT_FILE, JSON.stringify(output, null, 2)); |
| |
| |
| if (isJson) { |
| console.log(JSON.stringify(output, null, 2)); |
| } else { |
| console.log('๐ ASSET EXTRACTION SUMMARY'); |
| console.log('===========================\n'); |
| |
| console.log(`๐ MDX files scanned: ${mdxFiles.length}\n`); |
| |
| |
| console.log(`๐จ HTML EMBEDS`); |
| console.log(` Used: ${result.embeds.size} | Existing: ${existingEmbeds.size}`); |
| if (missingEmbeds.length > 0) { |
| console.log(` โ Missing: ${missingEmbeds.length}`); |
| } |
| if (unusedEmbeds.length > 0) { |
| console.log(` โ ๏ธ Unused: ${unusedEmbeds.length}`); |
| } |
| |
| |
| console.log(`\n๐ผ๏ธ IMAGES`); |
| console.log(` Used: ${result.images.size} | Existing: ${existingImages.size}`); |
| if (missingImages.length > 0) { |
| console.log(` โ Missing: ${missingImages.length}`); |
| } |
| if (unusedImages.length > 0) { |
| console.log(` โ ๏ธ Unused: ${unusedImages.length}`); |
| } |
| |
| |
| console.log(`\n๐ DATA FILES`); |
| console.log(` Used: ${result.dataFiles.size} | Existing: ${existingData.size}`); |
| if (missingData.length > 0) { |
| console.log(` โ Missing: ${missingData.length}`); |
| } |
| if (unusedData.length > 0) { |
| console.log(` โ ๏ธ Unused: ${unusedData.length}`); |
| } |
| |
| |
| if (isVerbose) { |
| if (missingEmbeds.length > 0) { |
| console.log('\nโ MISSING EMBEDS (referenced but don\'t exist):'); |
| missingEmbeds.sort().forEach(e => console.log(` - ${e}`)); |
| } |
| |
| if (unusedEmbeds.length > 0) { |
| console.log('\nโ ๏ธ UNUSED EMBEDS (exist but not referenced):'); |
| unusedEmbeds.sort().forEach(e => console.log(` - ${e}`)); |
| } |
| |
| if (missingImages.length > 0) { |
| console.log('\nโ MISSING IMAGES (referenced but don\'t exist):'); |
| missingImages.sort().forEach(i => console.log(` - ${i}`)); |
| } |
| |
| if (unusedImages.length > 0) { |
| console.log('\nโ ๏ธ UNUSED IMAGES (exist but not referenced):'); |
| unusedImages.sort().forEach(i => console.log(` - ${i}`)); |
| } |
| |
| if (missingData.length > 0) { |
| console.log('\nโ MISSING DATA FILES (referenced but don\'t exist):'); |
| missingData.sort().forEach(d => console.log(` - ${d}`)); |
| } |
| |
| if (unusedData.length > 0) { |
| console.log('\nโ ๏ธ UNUSED DATA FILES (exist but not referenced):'); |
| unusedData.sort().forEach(d => console.log(` - ${d}`)); |
| } |
| } |
| |
| |
| const hasIssues = missingEmbeds.length > 0 || missingImages.length > 0 || missingData.length > 0; |
| const hasWarnings = unusedEmbeds.length > 0 || unusedImages.length > 0 || unusedData.length > 0; |
| |
| console.log('\n' + 'โ'.repeat(40)); |
| if (hasIssues) { |
| console.log('โ ISSUES FOUND - Some assets are missing!'); |
| if (!isVerbose) { |
| console.log(' Run with --verbose to see details.'); |
| } |
| } else if (hasWarnings) { |
| console.log('โ ๏ธ WARNINGS - Some assets are unused.'); |
| if (!isVerbose) { |
| console.log(' Run with --verbose to see details.'); |
| } |
| } else { |
| console.log('โ
All assets are healthy!'); |
| } |
| |
| console.log(`\n๐ Output written to: ${path.relative(APP_ROOT, OUTPUT_FILE)}`); |
| } |
| |
| return output; |
| } |
|
|
| |
| extractAssets().catch(err => { |
| console.error('โ Error:', err.message); |
| process.exit(1); |
| }); |
|
|