diff --git a/lib/app-categories.js b/lib/app-categories.js new file mode 100644 index 00000000000..4e08ea38cc9 --- /dev/null +++ b/lib/app-categories.js @@ -0,0 +1,28 @@ +module.exports = [ + 'Books', + 'Business', + 'Catalogs', + 'Developer Tools', + 'Education', + 'Entertainment', + 'Finance', + 'Food & Drink', + 'Games', + 'Health & Fitness', + 'Graphics & Design', + 'Lifestyle', + 'Kids', + 'Magazines & Newspapers', + 'Music', + 'Navigation', + 'News', + 'Photo & Video', + 'Productivity', + 'Reference', + 'Science & Medicine', + 'Shopping', + 'Social Networking', + 'Sports', + 'Travel', + 'Utilities', +] diff --git a/lib/apps-with-github-repos.js b/lib/apps-with-github-repos.js new file mode 100644 index 00000000000..e47a8b8d59c --- /dev/null +++ b/lib/apps-with-github-repos.js @@ -0,0 +1,10 @@ +const apps = require('./raw-app-list')() +const parseGitUrl = require('github-url-to-object') + +module.exports = apps.filter((app) => { + // inherit repository from website if possible + if (!app.repository && parseGitUrl(app.website)) app.repository = app.website + if (!app.repository) return false + if (!parseGitUrl(app.repository)) return false + return true +}) diff --git a/lib/broken-links.js b/lib/broken-links.js new file mode 100644 index 00000000000..a861af38e88 --- /dev/null +++ b/lib/broken-links.js @@ -0,0 +1,93 @@ +const fetch = require('node-fetch') +const fsPromises = require('fs').promises +const isUrl = require('is-url') +const path = require('path') +const readdirp = require('readdirp') +const yaml = require('yaml') + +const topDir = path.dirname(__dirname) + +// walk an object subtree looking for URL strings +const getObjectUrls = (root) => { + const urls = [] + const queue = [root] + while (queue.length !== 0) { + const vals = Object.values(queue.shift()) + urls.push(...vals.filter(isUrl)) + queue.push(...vals.filter((v) => typeof v === 'object')) + } + return urls +} + +// scrape a url to see if the link is broken. +// return a Promise that resolves as { url, err } +const scrape = (url) => + fetch(url, { method: 'HEAD' }) // just scrape headers; body not needed + .then( + (res) => ({ + url, + err: res.ok ? null : `${res.status} ${res.statusText}`, + status: res.status, + }), + (err) => ({ url, err, status: -2 }) + ) + +// scrape all the urls found in a yml file. +// report broken links to console.log(). +// return a Promise that resolves as an array of broken links: [ { url, err }, ... ] +const processYmlEntry = (entry) => + fsPromises + .readFile(entry.fullPath, { encoding: 'utf8' }) + .then((file) => { + try { + return yaml.parse(file) + } catch (error) { + console.error(`Failed to parse ${entry.path}. Skipping.`) + return { disabled: true } + } + }) + .then((o) => (o.disabled ? [] : getObjectUrls(o))) + .then(async (urls) => { + const results = [] + + for (const url of urls) { + // Scrape one by one to handle rate limiting + const r = await scrape(url) + results.push(r) + } + + return results + }) + .then((results) => results.filter((res) => !!res.err)) + .then((fails) => { + fails.forEach((f) => console.log(`${entry.path} - ${f.url} (${f.err})`)) + return fails + }) + +const findBrokenLinks = (start = 0, end = Infinity) => + readdirp + .promise(topDir, { + fileFilter: '*.yml', + directoryFilter: (entry) => entry.path.startsWith('apps'), + }) + .then(async (entries) => { + const result = [] + let limitedEntries = entries + + if (start !== 0 || end !== Infinity) { + limitedEntries = entries.slice(start, end) + } + + for (const entry of limitedEntries) { + console.log(`Processing ${entry.path}`) + result.push({ + entry, + result: await processYmlEntry(entry), + }) + } + + return result + }) + .then((arr) => arr.filter((inner) => !!inner.result.length)) + +module.exports = findBrokenLinks diff --git a/lib/colors.js b/lib/colors.js new file mode 100755 index 00000000000..16f774d1b5a --- /dev/null +++ b/lib/colors.js @@ -0,0 +1,90 @@ +'use strict' + +const fs = require('fs') +const colorConvert = require('color-convert') +const getImageColors = require('get-image-colors') +const mime = require('mime-types') +const path = require('path') +const pickAGoodColor = require('pick-a-good-color') +const revHash = require('rev-hash') +const stringify = require('json-stable-stringify') + +/** + * Generates good colors for an image. + * + * @param slugsAndIconPaths [ { slug: foo, iconPath: bar } ... ] + * @param oldColors: reference colors from previous call to getColors() + * @param root: repo toplevel directory so that saved iconPaths are relative to it + * @return { slug: { palette, goodColorOnWhite, goodColorOnBlack, faintColorOnWhite, source: { revHash, iconPath } } + */ +async function getColors(slugsAndIconPaths, oldColors, root) { + return Promise.all( + slugsAndIconPaths.map(async (app) => { + const slug = app.slug + try { + const data = fs.readFileSync(app.iconPath) + const hash = revHash(data) + + // if nothing's changed, don't recalculate + let o = oldColors[slug] + if (o && o.source && o.source.revHash === hash) return { [slug]: o } + + console.info(`calculating good colors for ${slug}`) + return await getImageColors(data, mime.lookup(app.iconPath)).then( + (iconColors) => { + const palette = iconColors.map((color) => color.hex()) + const goodColorOnWhite = pickAGoodColor(palette) + const goodColorOnBlack = pickAGoodColor(palette, { + background: 'black', + }) + const faintColorOnWhite = `rgba(${colorConvert.hex + .rgb(goodColorOnWhite) + .join(', ')}, 0.1)` + return { + [slug]: { + source: { + revHash: hash, + path: path.relative(root, app.iconPath), + }, + palette, + goodColorOnWhite, + goodColorOnBlack, + faintColorOnWhite, + }, + } + } + ) + } catch (e) { + console.error(`Error processing ${app.iconPath}`, e) + } + }) + ).then((values) => Object.assign({}, ...values)) +} + +/** + * Wrapper around getColors() that uses the same file for input & output, + * refreshing the file when the data changes + * + * @param slugsAndIconPaths [ { slug: foo, iconPath: bar } ... ] + * @param colorsFile: the file that keeps the list of complimentary colors + * @param root: repo toplevel directory so that saved iconPaths are relative to it + */ +const rebuildColorFile = (slugsAndIconPaths, colorsFile, root) => { + let oldColors + try { + oldColors = require(colorsFile) + } catch (e) { + oldColors = {} + } + + getColors(slugsAndIconPaths, oldColors, root).then((colors) => { + try { + fs.writeFileSync(colorsFile, stringify(colors, { space: 2 })) + } catch (e) { + console.error(`Error writing ${colorsFile}`, e) + } + }) +} + +module.exports = rebuildColorFile +module.exports.getColors = getColors diff --git a/lib/github.js b/lib/github.js new file mode 100644 index 00000000000..f0a35ce223b --- /dev/null +++ b/lib/github.js @@ -0,0 +1,10 @@ +if (!process.env.GH_TOKEN) { + require('dotenv-safe').config() +} + +const { Octokit } = require('@octokit/rest') +const github = new Octokit({ + auth: process.env.GH_TOKEN, +}) + +module.exports = github diff --git a/lib/grandfathered-descriptions.js b/lib/grandfathered-descriptions.js new file mode 100644 index 00000000000..91f601fed3e --- /dev/null +++ b/lib/grandfathered-descriptions.js @@ -0,0 +1,763 @@ +// Apps in this list were submitted before tests were added +// to enforce the submission guidelines for descriptions +module.exports = [ + '1clipboard', + '5eclient', + 'aalarm', + 'abricotine', + 'abstract', + 'activechart', + 'admin-scheduler', + 'advanced-rest-client', + 'agantty', + 'aipo-com', + 'airtame', + 'aiting', + 'akiee', + 'alchemy', + 'alduin', + 'allow2automate', + 'altair', + 'altus', + 'alva', + 'amanote', + 'amipwned', + 'amium', + 'android-messages', + 'anote', + 'ansel', + 'ao', + 'appear-in', + 'appium', + 'appserver', + 'argo', + 'asarui', + 'astroprint', + 'atom', + 'audionodes', + 'auryo', + 'autobeat-player', + 'autoedit', + 'avocode', + 'aws-s3-backup', + 'backlog', + 'basecamp-3', + 'batcave', + 'bdash', + 'beaker-browser', + 'bearsweeper', + 'bearychat', + 'bitbloq', + 'bitcrypt', + 'blankup', + 'blockbench', + 'booker', + 'boostnote', + 'brave-browser', + 'browser-dispatcher', + 'browserosaurus', + 'buckets', + 'budgie', + 'buka', + 'bunqdesktop', + 'buttercup', + 'cacher', + 'calcy', + 'cansnippet', + 'canvas-file-sync', + 'caprine', + 'caption', + 'caret', + 'cargo-messenger', + 'cashnotify', + 'catlight', + 'cells', + 'cemui', + 'cerebro', + 'chatwork', + 'checksum', + 'checksum-validator', + 'chronobreak', + 'chronocube', + 'chronos-timetracker', + 'c-ip', + 'circuit', + 'citrus', + 'cleaver', + 'clipboard-anywhere', + 'clipboard-manager-electron', + 'clipboardmemo', + 'clippo', + 'cliptext', + 'cloudcmd', + 'cloudtag', + 'cocos-creator', + 'code-notes', + 'codepilot-ai', + 'code-rpgify', + 'code-story', + 'colibri', + 'collectie', + 'colol', + 'colon', + 'colorpicker', + 'composercat', + 'concats', + 'container-ps', + 'correo', + 'coursehunt', + 'covepdf', + 'coypu', + 'cozy-desktop', + 'criptio', + 'cromberg', + 'crypter', + 'cryptobar', + 'crypto-bot', + 'cryptocat', + 'cryptoseed', + 'cryptowallet', + 'csv-to-sqlite', + 'cuba-studio', + 'cumulus', + 'cycle', + 'cycligent-git-tool', + 'cypress', + 'darkj', + 'dat', + 'data-pixels-playground', + 'dataproofer', + 'data-store', + 'datazenit', + 'dbglass', + 'debugtron', // Added here because the word "Electron" is necessary in its description + 'deckard-ai', + 'deckboard', + 'deckhub', + 'deco-ide', + 'deepnest', + 'deer', + 'demio', + 'deplify', + 'destroyer', + 'devdocs-app', + 'devhub', + 'devrant-io-unofficial', + 'devrantron', + 'dext', + 'dict', + 'diffuse', + 'digiexam', + 'dipp', + 'discord', + 'dixa', + 'dn-tool-container', + 'dockstation', + 'doki-doki-mod-manager', + 'domterm', + 'donut', + 'dotgrid', + 'downline', + 'd-tools', + 'dupfinder', + 'dusk-player', + 'eagle', + 'easyhand', + 'easytongue', + 'egret-wing', + 'elcalc', + 'electorrent', + 'electro', + 'elements', + 'elite-journal', + 'e-mage', + 'email-securely-app', + 'englishextra-app', + 'epictask', + 'esteem-surfer', + 'etcher', + 'e-tools', + 'everdo', + 'evetrade', + 'excel-parser-processor', + 'explorer', + 'extraterm', + 'fangyuanjian', + 'fastlane', + 'fenetre', + 'ffftp', + 'figma', + 'final-countdown', + 'find-better-questions', + 'firebase-admin', + 'flex-browser', + 'flexpaper', + 'flow', + 'foco', + 'foda', + 'fog', + 'fontbase', + 'forestpin-analytics', + 'fotojet', + 'franz', + 'free-chess-club', + 'freelook', + 'freeman', + 'freeter', + 'fridayai', + 'friends', + 'fromscratch', + 'front', + 'fudget', + 'gala', + 'galeri', + 'gaucho', + 'gausssense-desktop', + 'geeks-diary', + 'genotify', + 'getraenkeliste', + 'gf-trader', + 'ghost', + 'gifbar', + 'gif-maker', + 'gitblade', + 'gitbook', + 'githoard', + 'github-desktop', + 'gitify', + 'gitkraken', + 'gitmoji', + 'gitscout', + 'glass-browser', + 'gluppi', + 'glyphr-studio', + 'google-play-music-desktop-player', + 'gordie', + 'grabcad-print', + 'gram-tools', + 'grap', + 'graphiql', + 'graphql-playground', + 'gravit-designer', + 'groupme', + 'gsubs', + 'hain', + 'hardinfo', + 'harmony', + 'hastyheroes', + 'hawkeye', + 'hbbatchbeast', + 'headlines', + 'headset', + 'healthi', + 'hedgehog-cloud-browser', + 'hexo-client', + 'hive', + 'hoster', + 'hostsdock', + 'hozz', + 'https-checker', + 'http-toolkit', + 'hueify', + 'hyper', + 'hyperspace', + 'i18n-manager', + 'i5sing', + 'iease-music', + 'illyriad', + 'image-resizer-tech', + 'image-shrinker', + 'imagine', + 'inboxer', + 'infinitex', + 'infinity', + 'inkdrop', + 'inpad', + 'insomnia', + 'instatron', + 'intu-mind', + 'ionic-creator', + 'ionic-lab', + 'iperius-console', + 'ironnode', + 'istrolid', + 'itch', + 'j', + 'james', + 'jamovi', + 'jandi', + 'janus-workspace', + 'jasper', + 'jibo', + 'joplin', + 'jqi', + 'jukeboks', + 'jumblepassword', + 'jumpfm', + 'justmd', + 'kakapo', + 'kaku', + 'kalk', + 'kap', + 'kappo', + 'katana', + 'kattana', + 'keeper', + 'keeper-password-manager-digital-vault', + 'keeweb', + 'keyfant-offline-password-manager', + 'kinesis-ci', + 'kitematic', + 'kobiton', + 'kongdash', + 'laravel-kit', + 'laverna', + 'lbry-desktop', + 'lectrote', + 'left', + 'lepton', + 'lifeboat', + 'lightgallery', + 'light-table', + 'ling', + 'lionshare', + 'lisk-hub', + 'local-browser', + 'losslesscut', + 'lps-studio', + 'luna', + 'magiccap', + 'mailspring', + 'makeappicon-desktop', + 'makerscad', + 'manageyum', + 'mancy', + 'manta', + 'mapbox', + 'markdown-c3', + 'markdown-explorer', + 'markdownify', + 'markdown-office', + 'marksearch', + 'marktext', + 'marp', + 'matrix-writer', + 'mattermost', + 'md5app', + 'mdnote', + 'media-mate', + 'medley', + 'meistertask', + 'mercury', + 'messenger-demo-viewer', + 'metastream', + 'metrogit', + 'metronome-wallet', + 'metube', + 'microsoft-teams', + 'microstockr', + 'min', + 'mindmapp', + 'minetime', + 'minimalist', + 'minta', + 'missive', + 'mist', + 'mixmax', + 'mjml-app', + 'mobile-locker', + 'mobirise', + 'mocker', + 'mockingbot', + 'mockman', + 'mockoon', + 'moeditor', + 'mojibar', + 'monerite', + 'mongoclient', + 'mongodb-compass', + 'mongotron', + 'montenote', + 'moonitor', + 'movieprint', + 'moviescores', + 'mstream', + 'multiple-file-manager', + 'multrin', + 'muno', + 'museeks', + 'music-kitten', + 'music-player', + 'musictube-player', + 'musify', + 'mylottery', + 'mypsn', + 'nattt', + 'ndm', + 'negative', + 'neko', + 'netbeast', + 'netron', + 'neutrinometrics', + 'nfov', + 'nicepage', + 'nighthawk', + 'nimble', + 'nodejs-package-manager', + 'node-red', + 'notable', + 'notion', + 'notr', + 'noty', + 'now', + 'nteract', + 'nubido', + 'nuclear', + 'nuclide', + 'nylas-n1', + 'octopi-init', + 'odio', + 'odrive', + 'oh-my-desk', + 'one-left', + 'opale-messenger', + 'openbazaar', + 'open-log-viewer', + 'open-stage-control', + 'opus', + 'overlay', + 'oversetter', + 'p3x-onenote', + 'p3x-redis-ui', + 'paintsupreme3d', + 'pamfax', + 'panda-2', + 'paperarxiv', + 'papercubes', + 'papyrus', + 'particl', + 'particle-dev', + 'patchwork', + 'paws-for-trello', + 'paymo-time-tracker', + 'pencil', + 'pepefe', + 'perlotto', + 'petal', + 'pexels', + 'phiewer', + 'phonegap', + 'phonepresenter', + 'photoscreensaver', + 'phreshistant', + 'phreshplayer', + 'picpipe', + 'pile', + 'pilemd', + 'pinesql', + 'piqture', + 'plain-email', + 'playback', + 'playcode', + 'playlist', + 'playme', + 'playork', + 'playout', + 'poddr', + 'poddycast-app', + 'poi', + 'pokerclock', + 'polar', + 'pomolectron', + 'pomotroid', + 'pomotron', + 'popkey', + 'popsql', + 'postbird', + 'postman', + 'pracontrol', + 'premid', + 'preserver', + 'presets-io', + 'pretzel', + 'prexview', + 'primitive-nextgen', + 'prodoctor-medicamentos', + 'proposales', + 'prosecrec', + 'protegopdf', + 'protopie', + 'pullp', + 'punk', + 'punycodeconverter', + 'pupafm', + 'puppetry', + 'putler', + 'qawl', + 'qbox', + 'qmui-web', + 'quail', + 'quickbooks', + 'quickcalc', + 'quickwords', + 'r6rc', + 'radio5050', + 'rambox', + 'ramme', + 'ratemymovie', + 'raven-reader', + 'ray', + 'reach-podcast-player', + 'rebaslight', + 'recollectr', + 'redp', + 'remember', + 'remind', + 'reqview', + 'reversee', + 'ride', + 'ridereceipts', + 'riot', + 'ripplectron', + 'rocket-chat', + 's3uploader', + 'saadhn', + 'sai', + 'sandman', + 'scancode-workbench', + 'school-timetable', + 'sciencefair', + 'screenaware', + 'screencat', + 'screentray', + 'sealtalk', + 'seapig', + 'sejda-pdf-desktop', + 'sencha-architect', + 'sencha-inspector', + 'sencha-test', + 'sencha-themer', + 'serina', + 'servpane', + 'seton', + 'sftpclient', + 'shaper', + 'shapespark', + 'sharepod', + 'sharp-tune', + 'sheepchat', + 'shiba', + 'shift', + 'shopify', + 'shortcm', + 'shortexts', + 'shots', + 'signal', + 'simpico', + 'simplenote', + 'skrifa', + 'skutti', + 'skype', + 'slack', + 'slack-catchup', + 'sloth', + 'smallpdf', + 'smartholdem', + 'smokerstopper', + 'snake', + 'snipaway', + 'snippetstore', + 'snowyowl', + 'socialcast', + 'socket-io-tester', + 'somiibo', + 'soube', + 'soundkeys', + 'soundnode', + 'source-me', + 'sparkchess', + 'spectrum', + 'sportfx-studio', + 'spotspot', + 'spraybottle', + 'spreaker-studio', + 'sqlectron', + 'stack', + 'stacker', + 'stamp', + 'standard-notes', + 'standup-picker', + 'station', + 'steelseries-engine-3', + 'stickynotes', + 'stoplight', + 'storaji', + 'storm', + 'strawberry', + 'streamlabs-obs', + 'streetviewdownload360', + 'studymd', + 'subgenesis', + 'subordination', + 'sunder', + 'superpowers-html5-2d-3d-game-maker', + 'super-productivity', + 'superscript', + 'surf', + 'svgsus', + 'switchhosts', + 'symphony', + 'synap', + 'syng', + 'system-designer', + 'tagflow', + 'tagspaces', + 'tagstoo', + 'tape', + 'taskade', + 'tasksq', + 'tea-ebook', + 'teamsql', + 'temps', + 'teseve', + 'testrec', + 'themebuilder', + 'theme-juice', + 'themer', + 'the-poker-timer', + 'thomas', + 'thrifty', + 'thunder', + 'thunderdocs', + 'ticodex-sql-schema-compare', + 'tidal', + 'tidy-up', + 'tiliq', + 'tim', + 'timeseriesadmin', + 'timestamp', + 'timetable', + 'time-zone-converter', + 'tiny-timer', + 'todokit', + 'todometer', + 'todo-sticker', + 'todu', + 'tofino', + 'top-browser', + 'tournamenter-manager', + 'transee', + 'translation-editor', + 'translatium', + 'treevea', + 'trico', + 'trilium-notes', + 'tropy', + 'trunk', + 'trym', + 'tunlookup', + 'turbo-download-manager', + 'turn-off-app', + 'tusk', + 'tweakstyle', + 'tweeten', + 'tweetman', + 'twitch', + 'typetalk', + 'ubauth', + 'udl-gui', + 'ueli', + 'un-colored', + 'unfound.txt', + 'unfx-proxy-checker', + 'unicode-plus', + 'unicopedia-plus', + 'unofficial-zalo', + 'updrive', + 'uphone', + 'vade-mecum-shelf', + 'vagrant-manager', + 'vechain-sync', + 'vectr', + 'vega-clipboard', + 'video-hub-app', + 'vii', + 'visual-comic-reader', + 'visual-studio-code', + 'vivifyscrum', + 'vizgraph', + 'vk-messenger', + 'vocare-helpdesk', + 'voice-notifies', + 'voltra', + 'vrap', + 'vue-calc', + 'vzl', + 'wail', + 'waiterio-restaurant-pos', + 'wakefy', + 'wallapatta', + 'wallpaperviewer', + 'wanna', + 'wantedly-chat', + 'waqt', + 'wavebox', + 'wayward', + 'weatherapp', + 'webcatalog', + 'webnet', + 'webtorrent', + 'weflow', + 'weighthub', + 'westeroscraftlauncher', + 'wewe-chat', + 'wexond', + 'whale', + 'wharf', + 'whatever', + 'whatsapp', + 'wheredat', + 'widgetoko', + 'wildlink', + 'winds', + 'wire', + 'wnr', + 'wonder-reader', + 'wordmark', + 'wordofthehour', + 'wordpress-com', + 'workpuls', + 'world-history-ap', + 'wowcrypt', + 'wow-stat', + 'wp-express', + 'writebar', + 'wwii-stats-viewer', + 'xcel', + 'xmind-zen', + 'xuanxuan', + 'y2mp3', + 'yeoman', + 'yhat-rodeo', + 'yosoro', + 'youget', + 'yout', + 'youtube-mp3', + 'youtube-music-desktop-app', + 'youtube-to-mp3', + 'ytdownloader', + 'ytdx', + 'z11and2', + 'zap', + 'zazu-app', + 'zector', + 'zefenify', + 'zenfocus', + 'zeplin', + 'zettlr', + 'zlilith', + 'zneon', + 'zoommy', + 'zulip', + 'zuzu', +] diff --git a/lib/grandfathered-links.js b/lib/grandfathered-links.js new file mode 100644 index 00000000000..9aad5af7e49 --- /dev/null +++ b/lib/grandfathered-links.js @@ -0,0 +1,66 @@ +// Apps in this list were submitted before tests were added +// to enforce the submission guidelines for links +module.exports = [ + '1clipboard', + 'autobeat-player', + 'backlog', + 'chatwork', + 'clipboard-anywhere', + 'cocos-creator', + 'code-rpgify', + 'code-story', + 'colol', + 'cromberg', + 'dataproofer', + 'dbglass', + 'destroyer', + 'dupfinder', + 'egret-wing', + 'extraterm', + 'fangyuanjian', + 'flex-browser', + 'forestpin-analytics', + 'gala', + 'gausssense-desktop', + 'gf-trader', + 'gif-maker', + 'inpad', + 'kakapo', + 'light-table', + 'mailspring', + 'mancy', + 'matrix-writer', + 'monerite', + 'mongotron', + 'moviescores', + 'muno', + 'nattt', + 'nuclear', + 'paintsupreme3d', + 'paws-for-trello', + 'plain-email', + 'playme', + 'playork', + 'popkey', + 'q-player', + 'sealtalk', + 'sharepod', + 'socialcast', + 'soube', + 'soundnode', + 'spotspot', + 'storm', + 'superpowers-html5-2d-3d-game-maker', + 'svgsus', + 'thismypc', + 'ubauth', + 'udl-gui', + 'vega-clipboard', + 'voice-notifies', + 'wayward', + 'webnet', + 'wheredat', + 'yhat-rodeo', + 'zazu-app', + 'zector', +] diff --git a/lib/grandfathered-small-icons.js b/lib/grandfathered-small-icons.js new file mode 100644 index 00000000000..6f02164efb5 --- /dev/null +++ b/lib/grandfathered-small-icons.js @@ -0,0 +1,72 @@ +// Apps in this list were submitted before we bumped the minimum +// icon dimension from 128 to 256. + +module.exports = [ + 'airtame', + 'aiting', + 'ansel', + 'argo', + 'autoedit', + 'avocode', + 'buka', + 'catlight', + 'cryptocat', + 'datazenit', + 'deckboard', + 'deckhub', + 'devrantron', + 'discord', + 'fastlane', + 'flow', + 'gif-maker', + 'gitify', + 'google-play-music-desktop-player', + 'gram-tools', + 'gravit-designer', + 'hain', + 'hozz', + 'https-checker', + 'iease-music', + 'ionic-lab', + 'jukeboks', + 'kakapo', + 'keeweb', + 'kitematic', + 'lectrote', + 'lionshare', + 'makeappicon-desktop', + 'manageyum', + 'mancy', + 'mapbox', + 'marktext', + 'mattermost', + 'meistertask', + 'microstockr', + 'mongotron', + 'multiple-file-manager', + 'now', + 'nuclide', + 'particle-dev', + 'perlotto', + 'popkey', + 'postman', + 'preserver', + 'presets-io', + 'remember', + 'rocket-chat', + 'slack', + 'smallpdf', + 'soundkeys', + 'spectrum', + 'spreaker-studio', + 'streetviewdownload360', + 'superpowers-html5-2d-3d-game-maker', + 'tagspaces', + 'turbo-download-manager', + 'twitch', + 'webtorrent', + 'wewe-chat', + 'wire', + 'wordpress-com', + 'writebar', +] diff --git a/lib/old-electron-apps.js b/lib/old-electron-apps.js new file mode 100644 index 00000000000..dcd3dcda02d --- /dev/null +++ b/lib/old-electron-apps.js @@ -0,0 +1,99 @@ +const fetch = require('node-fetch') +const fsPromises = require('fs').promises +const isUrl = require('is-url') +const path = require('path') +const readdirp = require('readdirp') +const yaml = require('yaml') + +const topDir = path.dirname(__dirname) + +// walk an object subtree looking for URL strings +const getObjectUrls = (root) => { + const urls = [] + const queue = [root] + while (queue.length !== 0) { + const vals = Object.values(queue.shift()) + urls.push(...vals.filter(isUrl)) + queue.push(...vals.filter((v) => typeof v === 'object')) + } + return urls +} + +// scrape a url to see if the link is broken. +// return a Promise that resolves as { url, err } +const scrape = (url) => { + const package = + 'https://raw.githubusercontent.com/' + + url.replace(/^.*com/g, '').replace(/.git$/g, '') + + '/master/package.json' + + return fetch(package, { method: 'GET' }) + .then((res) => (res.status === 200 ? res.json() : {})) + .then((json) => { + let version + if (json.hasOwnProperty('devDependencies')) { + if (json.devDependencies.hasOwnProperty('electron')) + version = json.devDependencies.electron + .replace(/^\^/g, '') + .replace(/^~/g, '') + } + if (json.hasOwnProperty('dependencies')) { + if (json.dependencies.hasOwnProperty('electron')) + version = json.dependencies.electron + .replace(/^\^/g, '') + .replace(/^~/g, '') + } + + return version + }) +} + +// scrape all the urls found in a yml file. +// report broken links to console.log(). +// return a Promise that resolves as an array of broken links: [ { url, err }, ... ] +const processYmlEntry = (entry) => + fsPromises + .readFile(entry.fullPath, { encoding: 'utf8' }) + .then((file) => { + try { + return yaml.parse(file) + } catch (error) { + console.error(`Failed to parse ${entry.path}. Skipping.`) + return { disabled: true } + } + }) + .then((o) => (o.disabled ? undefined : o.repository)) + .then(async (url) => (url ? await scrape(url) : undefined)) + .then((version) => { + version + ? console.log(`${entry.path} - Electron v${version}`) + : console.log(`${entry.path} - Closed Source`) + return version + }) + +const findOldElectronApps = (start = 0, end = Infinity) => + readdirp + .promise(topDir, { + fileFilter: '*.yml', + directoryFilter: (entry) => entry.path.startsWith('apps'), + }) + .then(async (entries) => { + const result = [] + let limitedEntries = entries + + if (start !== 0 || end !== Infinity) { + limitedEntries = entries.slice(start, end) + } + + for (const entry of limitedEntries) { + console.log(`Processing ${entry.path}`) + result.push({ + entry, + result: await processYmlEntry(entry), + }) + } + + return result + }) + +module.exports = findOldElectronApps diff --git a/lib/raw-app-list.js b/lib/raw-app-list.js new file mode 100644 index 00000000000..5c26c7b0efd --- /dev/null +++ b/lib/raw-app-list.js @@ -0,0 +1,29 @@ +const fs = require('fs') +const path = require('path') +const yaml = require('js-yaml') + +module.exports = function getSlugs() { + return fs + .readdirSync(path.join(__dirname, '../apps')) + .filter((filename) => { + return fs + .statSync(path.join(__dirname, `../apps/${filename}`)) + .isDirectory() + }) + .sort() + .reduce((slugs, slug) => { + const yamlFile = path.join(__dirname, `../apps/${slug}/${slug}.yml`) + const meta = yaml.load(fs.readFileSync(yamlFile)) + + if (meta.disabled) { + return slugs + } else { + const app = { + slug: slug, + iconPath: path.join(__dirname, `../apps/${slug}/${slug}-icon.png`), + ...meta, + } + return [...slugs, app] + } + }, []) +}