Last active
October 23, 2025 14:36
-
-
Save Xophmeister/14f10dbd0a3d204c55fdc0084701fded to your computer and use it in GitHub Desktop.
Attempt to normalise two Gatsby builds so they can be compared
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /* | |
| WARNING! This was vibe coded using Claude and messes with your filesystem | |
| Run in a sandbox (chroot, Docker, etc.) | |
| */ | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const glob = require('glob'); | |
| const prettier = require('prettier'); | |
| const BUILD_OLD = 'public-old'; | |
| const BUILD_NEW = 'public-new'; | |
| const BUILD_OLD_NORM = 'public-old-normalised'; | |
| const BUILD_NEW_NORM = 'public-new-normalised'; | |
| const normaliseContent = (content) => { | |
| return content | |
| // Webpack paths | |
| .replace(/webpack:\/\/\/\.\/.cache\/.*?\.js/g, 'WEBPACK_CACHE') | |
| // Long hashes (20+ hex chars) | |
| .replace(/\b[a-f0-9]{20,}\b/g, 'HASH') | |
| // Chunk names | |
| .replace(/"componentChunkName":"[^"]*"/g, '"componentChunkName":"CHUNK"') | |
| // Common Gatsby hash patterns | |
| .replace(/\bcomponent---[a-f0-9-]+/g, 'component---HASH') | |
| .replace(/\bapp-[a-f0-9]+\.js/g, 'app-HASH.js') | |
| .replace(/\bframework-[a-f0-9]+\.js/g, 'framework-HASH.js') | |
| .replace(/\bwebpack-runtime-[a-f0-9]+\.js/g, 'webpack-runtime-HASH.js') | |
| // Page data paths | |
| .replace(/\/page-data\/[^\/]+\/page-data\.json/g, '/page-data/HASH/page-data.json') | |
| // UUIDs and similar | |
| .replace(/\b[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g, 'UUID'); | |
| }; | |
| const normaliseFilename = (filename) => { | |
| // Normalise hash-based filenames for comparison | |
| return filename | |
| .replace(/[a-f0-9]{20,}/g, 'HASH') | |
| .replace(/component---[a-f0-9-]+/g, 'component---HASH') | |
| .replace(/app-[a-f0-9]+/g, 'app-HASH') | |
| .replace(/framework-[a-f0-9]+/g, 'framework-HASH') | |
| .replace(/webpack-runtime-[a-f0-9]+/g, 'webpack-runtime-HASH'); | |
| }; | |
| const unminify = async (content, filepath) => { | |
| const ext = path.extname(filepath).toLowerCase(); | |
| try { | |
| if (ext === '.js') { | |
| return await prettier.format(content, { | |
| parser: 'babel', | |
| printWidth: 100, | |
| tabWidth: 2, | |
| semi: true, | |
| singleQuote: true | |
| }); | |
| } else if (ext === '.json') { | |
| // For JSON, just use JSON.parse and stringify for formatting | |
| try { | |
| const parsed = JSON.parse(content); | |
| return JSON.stringify(parsed, null, 2); | |
| } catch (e) { | |
| return content; | |
| } | |
| } else if (ext === '.html' || ext === '.htm') { | |
| return await prettier.format(content, { | |
| parser: 'html', | |
| printWidth: 100, | |
| tabWidth: 2 | |
| }); | |
| } else if (ext === '.css') { | |
| return await prettier.format(content, { | |
| parser: 'css', | |
| printWidth: 100, | |
| tabWidth: 2 | |
| }); | |
| } | |
| } catch (e) { | |
| // If prettier fails (e.g., syntax error), return original | |
| console.warn(` Warning: Could not format ${filepath}: ${e.message}`); | |
| } | |
| return content; | |
| }; | |
| const getAllFiles = (dir) => { | |
| return glob.sync(`${dir}/**/*`, { nodir: true }) | |
| .map(f => path.relative(dir, f)); | |
| }; | |
| const normaliseAndWriteBuild = async (sourceDir, targetDir) => { | |
| console.log(`Normalising ${sourceDir} -> ${targetDir}`); | |
| // Create target directory | |
| if (fs.existsSync(targetDir)) { | |
| fs.rmSync(targetDir, { recursive: true }); | |
| } | |
| fs.mkdirSync(targetDir, { recursive: true }); | |
| const files = getAllFiles(sourceDir); | |
| let processedCount = 0; | |
| let unminifiedCount = 0; | |
| let skippedCount = 0; | |
| for (let i = 0; i < files.length; i++) { | |
| const relativePath = files[i]; | |
| const sourcePath = path.join(sourceDir, relativePath); | |
| const normalisedPath = normaliseFilename(relativePath); | |
| const targetPath = path.join(targetDir, normalisedPath); | |
| // Progress indicator | |
| if (i % 100 === 0) { | |
| process.stdout.write(`\r Processing: ${i}/${files.length} files...`); | |
| } | |
| // Create directory structure | |
| const targetDirPath = path.dirname(targetPath); | |
| if (!fs.existsSync(targetDirPath)) { | |
| fs.mkdirSync(targetDirPath, { recursive: true }); | |
| } | |
| try { | |
| // Try to read as text | |
| const content = fs.readFileSync(sourcePath, 'utf8'); | |
| // Check if file should be un-minified | |
| const ext = path.extname(relativePath).toLowerCase(); | |
| let processedContent = content; | |
| if (['.js', '.json', '.html', '.htm', '.css'].includes(ext)) { | |
| // Un-minify FIRST | |
| processedContent = await unminify(content, relativePath); | |
| unminifiedCount++; | |
| } | |
| // Then normalise content | |
| processedContent = normaliseContent(processedContent); | |
| fs.writeFileSync(targetPath, processedContent, 'utf8'); | |
| processedCount++; | |
| } catch (e) { | |
| // If it's a binary file or can't be read as UTF-8, just copy it | |
| try { | |
| fs.copyFileSync(sourcePath, targetPath); | |
| skippedCount++; | |
| } catch (copyError) { | |
| console.error(`\nError processing ${relativePath}: ${copyError.message}`); | |
| } | |
| } | |
| } | |
| process.stdout.write(`\r Processing: ${files.length}/${files.length} files... Done!\n`); | |
| console.log(` Processed: ${processedCount} text files`); | |
| console.log(` Un-minified: ${unminifiedCount} files`); | |
| console.log(` Copied: ${skippedCount} binary files`); | |
| console.log(); | |
| }; | |
| const compareBuilds = () => { | |
| const oldFiles = new Set(getAllFiles(BUILD_OLD_NORM)); | |
| const newFiles = new Set(getAllFiles(BUILD_NEW_NORM)); | |
| console.log('=== FILE STRUCTURE CHANGES ===\n'); | |
| // Files only in old build | |
| const onlyInOld = [...oldFiles].filter(f => !newFiles.has(f)); | |
| if (onlyInOld.length > 0) { | |
| console.log('Removed files:'); | |
| onlyInOld.forEach(f => console.log(` - ${f}`)); | |
| console.log(); | |
| } | |
| // Files only in new build | |
| const onlyInNew = [...newFiles].filter(f => !oldFiles.has(f)); | |
| if (onlyInNew.length > 0) { | |
| console.log('Added files:'); | |
| onlyInNew.forEach(f => console.log(` + ${f}`)); | |
| console.log(); | |
| } | |
| console.log('=== CONTENT CHANGES ===\n'); | |
| // Compare content of matching files | |
| const commonFiles = [...oldFiles].filter(f => newFiles.has(f)); | |
| let changedCount = 0; | |
| commonFiles.forEach(relativePath => { | |
| const oldFile = path.join(BUILD_OLD_NORM, relativePath); | |
| const newFile = path.join(BUILD_NEW_NORM, relativePath); | |
| try { | |
| const oldContent = fs.readFileSync(oldFile, 'utf8'); | |
| const newContent = fs.readFileSync(newFile, 'utf8'); | |
| if (oldContent !== newContent) { | |
| changedCount++; | |
| console.log(`Modified: ${relativePath}`); | |
| } | |
| } catch (e) { | |
| // Skip binary files or read errors | |
| if (!e.message.includes('ENOENT')) { | |
| // Binary files - compare size at least | |
| try { | |
| const oldStat = fs.statSync(oldFile); | |
| const newStat = fs.statSync(newFile); | |
| if (oldStat.size !== newStat.size) { | |
| changedCount++; | |
| console.log(`Modified (binary): ${relativePath}`); | |
| } | |
| } catch (statError) { | |
| // Ignore stat errors | |
| } | |
| } | |
| } | |
| }); | |
| console.log(`\n${changedCount} files with content changes`); | |
| console.log(`${onlyInOld.length} files removed`); | |
| console.log(`${onlyInNew.length} files added`); | |
| }; | |
| // Main execution | |
| (async () => { | |
| console.log('=== NORMALISING BUILDS ===\n'); | |
| await normaliseAndWriteBuild(BUILD_OLD, BUILD_OLD_NORM); | |
| await normaliseAndWriteBuild(BUILD_NEW, BUILD_NEW_NORM); | |
| console.log('=== COMPARING NORMALISED BUILDS ===\n'); | |
| compareBuilds(); | |
| console.log('\n=== DONE ==='); | |
| console.log(`You can now use standard diff tools on:`); | |
| console.log(` ${BUILD_OLD_NORM}`); | |
| console.log(` ${BUILD_NEW_NORM}`); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment