#!/usr/bin/env node const fs = require('fs') const path = require('path') const walk = require('walk-sync') const { execSync } = require('child_process') const { loadPages } = require('../../lib/pages') const patterns = require('../../lib/patterns') const { supported } = require('../../lib/enterprise-server-releases') const semver = require('semver') const imagesPath = [ '/assets/enterprise/3.0', '/assets/enterprise/github-ae', '/assets/enterprise/2.22', '/assets/enterprise/2.21', '/assets/enterprise/2.20' ] // these paths should remain in the repo even if they are not referenced directly const ignoreList = [ '/assets/images/help/site-policy', '/assets/images/site', '/assets/images/octicons', '/assets/fonts' ] // search these dirs for images or data references // content files are handled separately const dirsToGrep = [ 'includes', 'layouts', 'javascripts', 'stylesheets', 'README.md', 'data' ] async function main () { const pages = await getEnglishPages() // step 1. find assets referenced in content by searching page markdown const markdownImageData = await getMarkdownImageData(pages) // step 2. find assets referenced in non-content directories const assetsReferencedInNonContentDirs = await getAssetsReferencedInNonContentDirs() // step 3. remove enterprise assets that are only used on dotcom pages await removeDotcomOnlyImagesFromEnterprise(markdownImageData) // step 4. find all assets that exist in the /assets/enterprise directory // and remove assets that are referenced in any files for (const directory of imagesPath) { const allImagesInDir = await getAllAssetsInDirectory(directory) await removeEnterpriseImages(markdownImageData, allImagesInDir, assetsReferencedInNonContentDirs, directory) } // step 5. find all assets that exist in the /assets/images directory // and remove assets that are referenced in any files const allDotcomImagesInDir = await getAllAssetsInDirectory('/assets/images') await removeUnusedDotcomImages(markdownImageData, allDotcomImagesInDir, assetsReferencedInNonContentDirs) } // Returns an object of all the images referenced in Markdown // content and their associated versions // // key: image asset path // value: object key is the plan name and the object value is the plan's range // // ex: { // '/assets/images/foo/bar.png': { 'enterprise-server': '<=2.22'}, // '/assets/images/bar/foo/png': { 'github-ae': '*'} // } async function getMarkdownImageData (pages) { const imageData = {} // loop through each page and get all /assets/images references from Markdown for (const page of pages) { const fullContent = [page.intro, page.title, page.product, page.markdown].join() const pageImageList = await getImageReferencesOnPage(fullContent) // for each asset reference on a Markdown page, see if it needs to be // added to the imageData object for (const imagePath of pageImageList) { // if the image isn't already there add the image and its versions if (!Object.prototype.hasOwnProperty.call(imageData, imagePath)) { imageData[imagePath] = page.versions continue } // if the image reference already exists, see if any new version keys // or values need to be added or updated for (const pageVersion in page.versions) { const imageVersions = imageData[imagePath] const versionAlreadyExists = Object.prototype.hasOwnProperty.call(imageVersions, pageVersion) const existingVersionRangeIsAll = imageVersions[pageVersion] === '*' if (!versionAlreadyExists) { imageVersions[pageVersion] = page.versions[pageVersion] } else { // github-ae and free-pro-team versions only have '*' range if (pageVersion !== 'enterprise-server' || existingVersionRangeIsAll) continue const rangeOfVersionToAddIsAll = page.versions[pageVersion] === '*' // see if the version to add is a superset of the existing version const existingVersionIsSubsetOfNewVersion = semver.lt( semver.coerce(page.versions[pageVersion].replace('*', 0)), semver.coerce(imageVersions[pageVersion].replace('*', 0)) ) // only update the version range if the existing range is not '*' or // the new range is a superset of the existing range if (rangeOfVersionToAddIsAll || existingVersionIsSubsetOfNewVersion) { imageVersions[pageVersion] = page.versions[pageVersion] } } imageData[imagePath] = imageVersions } } } return imageData } async function getEnglishPages () { const pages = await loadPages() return pages.filter(page => page.languageCode === 'en') } async function getAllAssetsInDirectory (directory) { return walk(path.join(process.cwd(), directory), { directories: false }) .map(relPath => path.join(directory, relPath)) } async function getAssetsReferencedInNonContentDirs () { const regex = patterns.imagePath.source const grepCmd = `egrep -rh '${regex}' ${dirsToGrep.join(' ')}` const grepResults = execSync(grepCmd).toString() return await getImageReferencesOnPage(grepResults) } async function getImageReferencesOnPage (text) { return (text.match(patterns.imagePath) || []) .map(ref => { return ref .replace(/\.\.\//g, '') .trim() }) } // loop through images referenced in Markdown and check whether the image // is only referenced in pages versioned for free-pro-team. If the image // is only used on free-pro-team pages, then it shouldn't exist in the // assets/enterprise directory. function removeDotcomOnlyImagesFromEnterprise (markdownImageData) { for (const image in markdownImageData) { const imageVersions = markdownImageData[image] if (!Object.prototype.hasOwnProperty.call(imageVersions, 'enterprise-server')) { supported.forEach(enterpriseReleaseNumber => { const imagePath = path.join(__dirname, '../..', `/assets/enterprise/${enterpriseReleaseNumber}`, image) if (fs.existsSync(imagePath)) fs.unlinkSync(imagePath) }) } if (!Object.prototype.hasOwnProperty.call(imageVersions, 'github-ae')) { const imagePath = path.join(__dirname, '../..', '/assets/enterprise/github-ae', image) if (fs.existsSync(imagePath)) fs.unlinkSync(imagePath) } } } // loop through each image in a directory under /assets/enterprise // and check the image's version to determine if the image should be // removed from the directory async function removeEnterpriseImages (markdownImageData, directoryImageList, assetsReferencedInNonContentDirs, directory) { const directoryVersion = directory.split('/').pop() for (const directoryImage of directoryImageList) { // get the asset's format that is stored in the markdownImageData object const imageDataKey = directoryImage.replace(directory, '') // if the image is in a non content file (i.e., javascript or data file) // we don't have the page version info so assume it's used in all versions if (assetsReferencedInNonContentDirs.includes(imageDataKey)) continue const imageFullPath = path.join(process.cwd(), directoryImage) const imageVersions = markdownImageData[imageDataKey] // if the asset isn't referenced in Markdown at all, remove it if (markdownImageData[imageDataKey] === undefined) { fs.unlinkSync(imageFullPath) continue } // if the asset is in Markdown but is not used on GitHub AE pages, remove it if (directoryVersion === 'github-ae' && !Object.prototype.hasOwnProperty.call(imageVersions, 'github-ae')) { fs.unlinkSync(imageFullPath) continue // if the asset is in Markdown but is not used on a page versioned for the // directoryVersion (i.e., GHES release number), remove it } if (directoryVersion !== 'github-ae' && !Object.prototype.hasOwnProperty.call(imageVersions, 'enterprise-server')) { fs.unlinkSync(imageFullPath) continue } if (directoryVersion !== 'github-ae' && semver.lt( semver.coerce(directoryVersion), semver.coerce(imageVersions['enterprise-server'].replace('*', 0.0)))) { fs.unlinkSync(imageFullPath) } } } // loop through each file in /assets/images and check if async function removeUnusedDotcomImages (markdownImageData, directoryImageList, assetsReferencedInNonContentDirs) { for (const directoryImage of directoryImageList) { if (ignoreList.find(ignored => directoryImage.startsWith(ignored))) continue // if the image is in a non content file (i.e., javascript or data file) // we don't have the page version info so assume it's used in all versions if (assetsReferencedInNonContentDirs.includes(directoryImage)) continue if (markdownImageData[directoryImage] === undefined) { fs.unlinkSync(path.join(process.cwd(), directoryImage)) } } } main() .catch(console.error) .finally(() => console.log('Done!'))