// Thanks to https://github.com/Yuyz0112/icomoon-cli const fs = require('fs-extra'); const path = require('path'); const extract = require('extract-zip'); const puppeteer = require('puppeteer'); const DEFAULT_TIMEOUT = 60000; const PAGE = { IMPORT_CONFIG_BUTTON: '.file.unit', IMPORT_SELECTION_INPUT: '.file.unit input[type="file"]', OVERLAY_CONFIRM: '.overlay button.mrl', NEW_SET_BUTTON: '.menuList1 button', MAIN_MENU_BUTTON: '.bar-top button .icon-menu', MENU_BUTTON: 'h1 button .icon-menu', MENU: '.menuList2.menuList3', ICON_INPUT: '.menuList2.menuList3 .file input[type="file"]', FIRST_ICON_BOX: '#set0 .miBox:not(.mi-selected)', REMOVE_SET_BUTTON: '.menuList2.menuList3 li:last-child button', SELECT_ALL_BUTTON: 'button[ng-click="selectAllNone($index, true)"]', GENERATE_LINK: 'a[href="#/select/font"]', GLYPH_SET: '#glyphSet0', GLYPH_NAME: '.glyphName', DOWNLOAD_BUTTON: '.btn4', PREFERENCES: '#pref', SHOW_METRICS: '[ng-class*="showMetricsFocused"] label', CLOSE_OVERLAY: 'button[ng-click*="visiblePanels.fontPref = false"]', IE8_SUPPORTED: '[ng-class*="noie8Focused"] .icon-checked', IE8_DISABLE: 'label[ng-class*="noie8Focused"]', FONT_NAME_INPUT: '[ng-model="fontPref.metadata.fontFamily"]', CLASS_PREFIX_INPUT: '[ng-model="fontPref.prefix"]', CSS_VARS_LABEL: '[ng-class*="fontPref.cssVars"]', EM_HEIGHT_INPUT: '[model="fontPref.metrics.emSize"] input', BASELINE_HEIGHT_INPUT: '[model="fontPref.metrics.baseline"] input', WHITESPACE_WIDTH_INPUT: '[model="fontPref.metrics.whitespace"] input', }; const DEFAULT_OPTIONS = { outputDir: path.join(__dirname, 'output'), }; const logger = (...args) => { console.log('[icomoon-cli]', ...args); }; const sleep = time => new Promise(resolve => setTimeout(resolve, time)); const getAbsolutePath = inputPath => { let absoluteSelectionPath = inputPath; if (!path.isAbsolute(inputPath)) { if (!process.env.PWD) { process.env.PWD = process.cwd(); } absoluteSelectionPath = path.resolve(process.env.PWD, inputPath); } return absoluteSelectionPath; }; const checkDownload = dest => new Promise((resolve, reject) => { const interval = 1000; let downloadSize = 0; let timeCount = 0; const timer = setInterval(async () => { timeCount += interval; /* const exist = await fs.exists(dest); if (!exist) { return; } */ const stats = fs.statSync(dest); if (stats.size > 0 && stats.size === downloadSize) { clearInterval(timer); resolve(); } else { downloadSize = stats.size; } if (timeCount > DEFAULT_TIMEOUT) { reject('Timeout when download file, please check your network.'); } }, interval); }); const checkDuplicateName = ({ selectionPath, icons, names }, forceOverride) => { const iconNames = icons.map((icon, index) => { if (names[index]) { return names[index]; } return path.basename(icon).replace(path.extname(icon), ''); }); const duplicates = []; const selection = fs.readJSONSync(selectionPath); selection.icons.forEach((icon, index) => { const name = icon.tags[0]; if (iconNames.includes(name)) { duplicates.push({ name, index }); } }); if (!duplicates.length) { return; } if (forceOverride) { selection.icons = selection.icons.filter((icon, index) => !duplicates.some(d => d.index === index)); fs.writeJSONSync(selectionPath, selection, { spaces: 2 }); } else { throw new Error(`Found duplicate icon names: ${duplicates.map(d => d.name).join(',')}`); } }; async function pipeline(options = {}) { try { const { icons, names = [], selectionPath, forceOverride = false, whenFinished, visible = false } = options; const outputDir = options.outputDir ? getAbsolutePath(options.outputDir) : DEFAULT_OPTIONS.outputDir; // prepare stage logger('Preparing...'); if(!icons || !icons.length) { if(whenFinished) { whenFinished({ outputDir }); } return logger('No new icons found.'); } if(!selectionPath) { throw new Error('Please config a valid selection file path.'); } let absoluteSelectionPath = getAbsolutePath(selectionPath); // checkDuplicateName({ // selectionPath: absoluteSelectionPath, // icons, // names, // }, forceOverride); await fs.remove(outputDir); await fs.ensureDir(outputDir); const browser = await puppeteer.launch({headless: !visible}); logger('Started a new chrome instance, going to load icomoon.io.'); const page = await (await browser).newPage(); await page._client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath: outputDir }); await page.goto('https://icomoon.io/app/#/select'); await page.waitForSelector(PAGE.IMPORT_CONFIG_BUTTON); logger('Dashboard is visible, going to upload config file'); // remove init set await page.click(PAGE.MENU_BUTTON); await page.click(PAGE.REMOVE_SET_BUTTON); const importInput = await page.waitForSelector(PAGE.IMPORT_SELECTION_INPUT); await importInput.uploadFile(absoluteSelectionPath); logger('Uploaded config, going to upload new icon files'); try { await Promise.race([ sleep(1000).then(() => { throw 0; }), page.waitForSelector(PAGE.OVERLAY_CONFIRM, { visible: true }) ]); await page.click(PAGE.OVERLAY_CONFIRM); } catch(err) { logger('Overlay is missed?'); } const selection = fs.readJSONSync(selectionPath); /* if (selection.icons.length === 0) { logger('Selection icons is empty, going to create an empty set'); await page.click(PAGE.MAIN_MENU_BUTTON); await page.waitForSelector(PAGE.NEW_SET_BUTTON, { visible: true }); await page.click(PAGE.NEW_SET_BUTTON); } */ await page.click(PAGE.MENU_BUTTON); const iconInput = await page.waitForSelector(PAGE.ICON_INPUT); const iconPaths = icons.map(getAbsolutePath); await iconInput.uploadFile(...iconPaths); await page.waitForSelector(PAGE.FIRST_ICON_BOX); await page.click(PAGE.SELECT_ALL_BUTTON); logger('Uploaded and selected all new icons'); await page.click(PAGE.GENERATE_LINK); await page.waitForSelector(PAGE.GLYPH_SET); await page.click(PAGE.PREFERENCES); try { await Promise.race([ sleep(1000).then(() => { throw 0; }), page.waitForSelector(PAGE.IE8_SUPPORTED) ]); await page.click(PAGE.IE8_DISABLE); } catch(err) { logger('IE8 is already disabled'); } async function fillInput(selector, value) { if(typeof(value) !== 'string') { value = '' + value; } await page.focus(selector); for(let i = 0; i < 100; ++i) { await page.keyboard.press('Backspace'); } await page.keyboard.type(value); } await fillInput(PAGE.FONT_NAME_INPUT, selection.preferences.fontPref.metadata.fontFamily); await fillInput(PAGE.CLASS_PREFIX_INPUT, selection.preferences.fontPref.prefix); await page.click(PAGE.CSS_VARS_LABEL); await page.click(PAGE.SHOW_METRICS); await fillInput(PAGE.EM_HEIGHT_INPUT, selection.preferences.fontPref.metrics.emSize); await fillInput(PAGE.BASELINE_HEIGHT_INPUT, selection.preferences.fontPref.metrics.baseline); await fillInput(PAGE.WHITESPACE_WIDTH_INPUT, selection.preferences.fontPref.metrics.whitespace); // await sleep(100000); await page.click(PAGE.CLOSE_OVERLAY); // (await page.waitForSelector(PAGE.FONT_NAME_INPUT)).; // if(names.length) { // logger('Changed names of icons'); // // sleep to ensure indexedDB is ready // await sleep(1000); // await page.evaluate(names => { // const request = indexedDB.open('IDBWrapper-storage', 1); // request.onsuccess = function() { // const db = request.result; // const tx = db.transaction('storage', 'readwrite'); // const store = tx.objectStore('storage'); // const keys = store.getAllKeys(); // keys.onsuccess = function() { // let timestamp; // keys.result.forEach(function(key) { // if (typeof key === 'number') { // timestamp = key; // } // }); // const main = store.get(timestamp); // main.onsuccess = function() { // const data = main.result; // for (let i = 0; i < names.length; i++) { // data.obj.iconSets[0].selection[i].name = names[i]; // } // store.put(data); // }; // }; // }; // }, names); // } // // sleep to ensure the code was executed // await sleep(1000); // // reload the page let icomoon read latest indexedDB data // await page.reload(); await sleep(2000); await page.waitForSelector(PAGE.DOWNLOAD_BUTTON); await page.click(PAGE.DOWNLOAD_BUTTON); const meta = selection.preferences.fontPref.metadata; const zipName = meta.majorVersion ? `${meta.fontFamily}-v${meta.majorVersion}.${meta.minorVersion || 0}.zip` : `${meta.fontFamily}.zip`; logger(`Started to download ${zipName}`); const zipPath = path.join(outputDir, zipName); await checkDownload(zipPath); logger('Successfully downloaded, going to unzip it.'); await page.close(); await browser.close(); // unzip stage extract(zipPath, {dir: outputDir}, async(err) => { if(err) { throw err; } await fs.remove(zipPath); logger(`Finished. The output directory is ${outputDir}.`); if(whenFinished) { whenFinished({outputDir}); } }); } catch(error) { console.error(error); } } module.exports = pipeline;