const fs = require('fs-extra'); const fetch = require('node-fetch'); const cheerio = require('cheerio'); const dayjs = require('dayjs'); const showdown = require('showdown'); const Parcel = require('parcel-bundler'); const sm = require('sitemap'); process.env.NODE_ENV = 'production'; const LOG = { error: (...args) => console.error('❌ ERROR', { ...args }), debug: (...args) => { if (process.env.DEBUG) console.log('💡 DEBUG: ', { ...args }); }, }; const handleFailure = err => { LOG.error(err); process.exit(1); }; process.on('unhandledRejection', handleFailure); // --- FILES const README = 'README.md'; const WEBSITE_FOLDER = 'website'; const DATA_FOLDER = 'data'; const LATEST_FILENAME = `${DATA_FOLDER}/latest`; const MAPPING = `${DATA_FOLDER}/mapping.json`; const CATEGORY = `${DATA_FOLDER}/category.json`; const indexTemplate = `${WEBSITE_FOLDER}/index.tmpl.html`; const indexDestination = `${WEBSITE_FOLDER}/index.html`; const tableTemplate = `${WEBSITE_FOLDER}/table.tmpl.html`; const tableDestination = `${WEBSITE_FOLDER}/table.html`; // --- CONFIG const valueNames = [ 'name', 'description', 'homepage', 'star', 'updated', 'language', 'license', 'author', ]; const sitemapOpts = { hostname: 'https://awesome-docker.netlify.com/', cacheTime: 6000000, // 600 sec (10 min) cache purge period urls: [ { url: '/', changefreq: 'daily', priority: 0.8, lastmodrealtime: true, lastmodfile: 'dist/index.html', }, { url: '/table.html', changefreq: 'daily', priority: 0.8, lastmodrealtime: true, lastmodfile: 'dist/table.html', }, ], }; // --- FORMAT const loadEmoji = () => fetch('https://api.github.com/emojis') .then(r => r.json()) .catch(handleFailure); let emojiMapURL = {}; const emojify = text => { if (!text) return text; const colonWrapped = /(:[\w\-+]+:)/g; const result = text.replace(colonWrapped, match => { const name = match.replace(/:/g, ''); const url = emojiMapURL[name]; return url ? `${name}` : match; }); return result || text; }; const getLastUpdate = updated => { const updt = Number(dayjs(updated).diff(dayjs(), 'days')); if (updt < 0) { if (Math.abs(updt) === 1) return `1 day ago`; return `${Math.abs(updt)} days ago`; } else if (updt === 0) return 'today'; return updated; }; const mapHomePage = h => { if (h === 'manageiq.org') return 'https://manageiq.org'; else if (h === 'dev-sec.io') return 'https://dev-sec.io'; return h; }; const mapLicense = l => { if (l === 'GNU Lesser General Public License v3.0') return 'GNU LGPL v3.0'; else if (l === 'GNU General Public License v2.0') return 'GNU GPL v2.0'; else if (l === 'GNU General Public License v3.0') return 'GNU GPL v3.0'; else if (l === 'BSD 3-Clause "New" or "Revised" License') return 'BSD 3-Clause'; else if (l === 'BSD 2-Clause "Simplified" License') return 'BSD 2-Clause'; return l; }; const formatEntry = ( { name, html_url: repoURL, description, homepage, stargazers_count: stargazers, pushed_at: updated, language, license, owner, categoryName, }, i, ) => [ `
  • `, `${name}`, `

    ${emojify(description) || '-'}

    `, `

    Last code update: ${getLastUpdate( updated, )}

    `, (homepage && `website`) || '

    ', `

    ⭐️${stargazers}

    `, (language && `

    ${language}

    `) || '

    ', (license && license.url !== null && `${mapLicense( license.name, )}`) || '

    ', `

    ${categoryName}

    `, owner && `${ owner.login }`, '
  • ', ].join(''); const buttonHTLM = valueNames .filter(x => !['description', 'homepage'].includes(x)) .map(v => ``) .join(''); const processMetadata = metaData => [ `
    `, ``, `

    Sort by

    ${buttonHTLM}
    `, `
    `, '', ].join(''); const normalizedMetadata = ([mapping, category, data]) => data.reduce((acc, repo) => { const m = mapping[repo.html_url]; if (!m) { console.log('MISSING:', { repo: repo.html_url }); return acc; } const c = m && category[m.category]; if (!c) { console.log('CATEGORY MISSING', { mapping: m }); return acc; } return { ...acc, ...{ [repo.html_url.toLowerCase()]: { ...repo, ownerType: repo.owner && repo.owner.type, categoryName: c.name, categoryDescription: c.description, status: m.status, }, }, }; }, {}); async function processTable() { try { LOG.debug('Loading files...', { LATEST_FILENAME, tableTemplate }); const latestFilename = await fs.readFile(LATEST_FILENAME, 'utf8'); LOG.debug({ latestFilename }); const data = await Promise.all([ fs.readJSON(MAPPING), fs.readJSON(CATEGORY), fs.readJSON(latestFilename), ]); const metaData = normalizedMetadata(data); LOG.debug({ metaData }); const template = await fs.readFile(tableTemplate, 'utf8'); LOG.debug('Processing template'); const $ = cheerio.load(template); $('#md').append(processMetadata(metaData)); LOG.debug('Writing table.html'); await fs.outputFile(tableDestination, $.html(), 'utf8'); LOG.debug('✅ DONE 👍'); } catch (err) { handleFailure(err); } } async function processIndex() { const converter = new showdown.Converter({ omitExtraWLInCodeBlocks: true, simplifiedAutoLink: true, excludeTrailingPunctuationFromURLs: true, literalMidWordUnderscores: true, strikethrough: true, tables: true, tablesHeaderId: true, ghCodeBlocks: true, tasklists: true, disableForced4SpacesIndentedSublists: true, simpleLineBreaks: true, requireSpaceBeforeHeadingText: true, ghCompatibleHeaderId: true, ghMentions: true, backslashEscapesHTMLTags: true, emoji: true, splitAdjacentBlockquotes: true, }); // converter.setFlavor('github'); try { LOG.debug('Loading files...', { indexTemplate, README }); const template = await fs.readFile(indexTemplate, 'utf8'); const markdown = await fs.readFile(README, 'utf8'); LOG.debug('Merging files...'); const $ = cheerio.load(template); $('#md').append(converter.makeHtml(markdown)); LOG.debug('Writing index.html'); await fs.outputFile(indexDestination, $.html(), 'utf8'); LOG.debug('DONE 👍'); } catch (err) { handleFailure(err); } } const bundle = () => { LOG.debug('---'); LOG.debug('📦 Bundling with Parcel.js'); LOG.debug('---'); new Parcel(indexDestination, { name: 'build', publicURL: '/', }) .bundle() .then(() => // Creates a sitemap object given the input configuration with URLs fs.outputFile( 'dist/sitemap.xml', sm.createSitemap(sitemapOpts).toString(), ), ); }; async function main() { emojiMapURL = await loadEmoji(); await processTable(); await processIndex(); await bundle(); } main();