Skip to content

Instantly share code, notes, and snippets.

@Xophmeister
Last active October 23, 2025 14:36
Show Gist options
  • Select an option

  • Save Xophmeister/14f10dbd0a3d204c55fdc0084701fded to your computer and use it in GitHub Desktop.

Select an option

Save Xophmeister/14f10dbd0a3d204c55fdc0084701fded to your computer and use it in GitHub Desktop.
Attempt to normalise two Gatsby builds so they can be compared
#!/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