Prettier all files, update dependencies

This commit is contained in:
Vjacheslav Trushkin 2020-07-22 20:48:17 +03:00
parent 6adfc9d131
commit d28f8f095f
23 changed files with 2737 additions and 2323 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
[{*.json,*.yml}]
indent_style = space
indent_size = 2

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"singleQuote": true,
"useTabs": true,
"semi": true,
"quoteProps": "consistent"
}

264
app.js
View File

@ -7,52 +7,54 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
// Load required modules // Load required modules
const fs = require('fs'), const fs = require('fs'),
util = require('util'), util = require('util'),
express = require('express'); express = require('express');
// Log uncaught exceptions to stderr // Log uncaught exceptions to stderr
process.on('uncaughtException', function (err) { process.on('uncaughtException', function(err) {
console.error('Uncaught exception:', err); console.error('Uncaught exception:', err);
}); });
// Create application // Create application
let app = { let app = {
root: __dirname root: __dirname,
}; };
/** /**
* Load config.json and config-default.json * Load config.json and config-default.json
*/ */
app.config = JSON.parse(fs.readFileSync(__dirname + '/config-default.json', 'utf8')); app.config = JSON.parse(
fs.readFileSync(__dirname + '/config-default.json', 'utf8')
);
try { try {
let customConfig = fs.readFileSync(__dirname + '/config.json', 'utf8'); let customConfig = fs.readFileSync(__dirname + '/config.json', 'utf8');
if (typeof customConfig === 'string') { if (typeof customConfig === 'string') {
try { try {
customConfig = JSON.parse(customConfig); customConfig = JSON.parse(customConfig);
Object.keys(customConfig).forEach(key => { Object.keys(customConfig).forEach(key => {
if (typeof app.config[key] !== typeof customConfig[key]) { if (typeof app.config[key] !== typeof customConfig[key]) {
return; return;
} }
if (typeof app.config[key] === 'object') { if (typeof app.config[key] === 'object') {
// merge object // merge object
Object.assign(app.config[key], customConfig[key]); Object.assign(app.config[key], customConfig[key]);
} else { } else {
// overwrite scalar variables // overwrite scalar variables
app.config[key] = customConfig[key]; app.config[key] = customConfig[key];
} }
}); });
} catch (err) { } catch (err) {
console.error('Error parsing config.json', err); console.error('Error parsing config.json', err);
} }
} }
} catch (err) { } catch (err) {
console.log('Missing config.json. Using default API configuration'); console.log('Missing config.json. Using default API configuration');
} }
// Add logging and mail modules // Add logging and mail modules
@ -69,22 +71,27 @@ app.logger = require('./src/logger').bind(this, app);
*/ */
// Port // Port
if (app.config['env-port'] && process.env.PORT) { if (app.config['env-port'] && process.env.PORT) {
app.config.port = process.env.PORT; app.config.port = process.env.PORT;
} }
// Region file to easy identify server in CDN // Region file to easy identify server in CDN
if (!app.config['env-region'] && process.env.region) { if (!app.config['env-region'] && process.env.region) {
app.config.region = process.env.region; app.config.region = process.env.region;
} }
if (app.config.region.length > 10 || !app.config.region.match(/^[a-z0-9_-]+$/i)) { if (
app.config.region = ''; app.config.region.length > 10 ||
app.error('Invalid value for region config variable.'); !app.config.region.match(/^[a-z0-9_-]+$/i)
) {
app.config.region = '';
app.error('Invalid value for region config variable.');
} }
// Reload secret key // Reload secret key
if (app.config['reload-secret'] === '') { if (app.config['reload-secret'] === '') {
// Add reload-secret to config.json to be able to run /reload?key=your-secret-key that will reload collections without restarting server // Add reload-secret to config.json to be able to run /reload?key=your-secret-key that will reload collections without restarting server
console.log('reload-secret configuration is empty. You will not be able to update all collections without restarting server.'); console.log(
'reload-secret configuration is empty. You will not be able to update all collections without restarting server.'
);
} }
/** /**
@ -92,7 +99,9 @@ if (app.config['reload-secret'] === '') {
*/ */
// Get version // Get version
app.version = JSON.parse(fs.readFileSync(__dirname + '/package.json', 'utf8')).version; app.version = JSON.parse(
fs.readFileSync(__dirname + '/package.json', 'utf8')
).version;
// Files helper // Files helper
app.fs = require('./src/files')(app); app.fs = require('./src/files')(app);
@ -103,8 +112,10 @@ app.loadJSON = require('./src/json').bind(this, app);
// Add directories storage // Add directories storage
app.dirs = require('./src/dirs')(app); app.dirs = require('./src/dirs')(app);
if (!app.dirs.getRepos().length) { if (!app.dirs.getRepos().length) {
console.error('No repositories found. Make sure either Iconify or custom repository is set in configuration.'); console.error(
return; 'No repositories found. Make sure either Iconify or custom repository is set in configuration.'
);
return;
} }
// Collections // Collections
@ -120,88 +131,123 @@ app.iconsRequest = require('./src/request-icons').bind(this, app);
app.miscRequest = require('./src/request').bind(this, app); app.miscRequest = require('./src/request').bind(this, app);
// Start application // Start application
require('./src/startup')(app).then(() => { require('./src/startup')(app)
.then(() => {
// Create HTTP server
app.server = express();
// Create HTTP server // Disable X-Powered-By header
app.server = express(); app.server.disable('x-powered-by');
// Disable X-Powered-By header // CORS
app.server.disable('x-powered-by'); app.server.options('/*', (req, res) => {
if (app.config.cors) {
res.header(
'Access-Control-Allow-Origin',
app.config.cors.origins
);
res.header(
'Access-Control-Allow-Methods',
app.config.cors.methods
);
res.header(
'Access-Control-Allow-Headers',
app.config.cors.headers
);
res.header('Access-Control-Max-Age', app.config.cors.timeout);
}
res.send(200);
});
// CORS // GET 3 part request
app.server.options('/*', (req, res) => { app.server.get(
if (app.config.cors) { /^\/([a-z0-9-]+)\/([a-z0-9-]+)\.(js|json|svg)$/,
res.header('Access-Control-Allow-Origin', app.config.cors.origins); (req, res) => {
res.header('Access-Control-Allow-Methods', app.config.cors.methods); // prefix/icon.svg
res.header('Access-Control-Allow-Headers', app.config.cors.headers); // prefix/icons.json
res.header('Access-Control-Max-Age', app.config.cors.timeout); app.iconsRequest(
} req,
res.send(200); res,
}); req.params[0],
req.params[1],
req.params[2]
);
}
);
// GET 3 part request // GET 2 part JS/JSON request
app.server.get(/^\/([a-z0-9-]+)\/([a-z0-9-]+)\.(js|json|svg)$/, (req, res) => { app.server.get(/^\/([a-z0-9-]+)\.(js|json)$/, (req, res) => {
// prefix/icon.svg // prefix.json
// prefix/icons.json app.iconsRequest(req, res, req.params[0], 'icons', req.params[1]);
app.iconsRequest(req, res, req.params[0], req.params[1], req.params[2]); });
});
// GET 2 part JS/JSON request // GET 2 part SVG request
app.server.get(/^\/([a-z0-9-]+)\.(js|json)$/, (req, res) => { app.server.get(/^\/([a-z0-9:-]+)\.svg$/, (req, res) => {
// prefix.json let parts = req.params[0].split(':');
app.iconsRequest(req, res, req.params[0], 'icons', req.params[1]);
});
// GET 2 part SVG request if (parts.length === 2) {
app.server.get(/^\/([a-z0-9:-]+)\.svg$/, (req, res) => { // prefix:icon.svg
let parts = req.params[0].split(':'); app.iconsRequest(req, res, parts[0], parts[1], 'svg');
return;
}
if (parts.length === 2) { if (parts.length === 1) {
// prefix:icon.svg parts = parts[0].split('-');
app.iconsRequest(req, res, parts[0], parts[1], 'svg'); if (parts.length > 1) {
return; // prefix-icon.svg
} app.iconsRequest(
req,
res,
parts.shift(),
parts.join('-'),
'svg'
);
return;
}
}
if (parts.length === 1) { app.response(req, res, 404);
parts = parts[0].split('-'); });
if (parts.length > 1) {
// prefix-icon.svg
app.iconsRequest(req, res, parts.shift(), parts.join('-'), 'svg');
return;
}
}
app.response(req, res, 404); // Send robots.txt that disallows everything
}); app.server.get('/robots.txt', (req, res) =>
app.miscRequest(req, res, 'robots')
);
app.server.post('/robots.txt', (req, res) =>
app.miscRequest(req, res, 'robots')
);
// Send robots.txt that disallows everything // API version information
app.server.get('/robots.txt', (req, res) => app.miscRequest(req, res, 'robots')); app.server.get('/version', (req, res) =>
app.server.post('/robots.txt', (req, res) => app.miscRequest(req, res, 'robots')); app.miscRequest(req, res, 'version')
);
// API version information // Reload collections without restarting app
app.server.get('/version', (req, res) => app.miscRequest(req, res, 'version')); app.server.get('/reload', (req, res) =>
app.miscRequest(req, res, 'reload')
// Reload collections without restarting app );
app.server.get('/reload', (req, res) => app.miscRequest(req, res, 'reload')); app.server.post('/reload', (req, res) =>
app.server.post('/reload', (req, res) => app.miscRequest(req, res, 'reload')); app.miscRequest(req, res, 'reload')
);
// Get latest collection from Git repository
app.server.get('/sync', (req, res) => app.miscRequest(req, res, 'sync'));
app.server.post('/sync', (req, res) => app.miscRequest(req, res, 'sync'));
// Redirect home page
app.server.get('/', (req, res) => {
res.redirect(301, app.config['index-page']);
});
// Create server
app.server.listen(app.config.port, () => {
app.log('Listening on port ' + app.config.port);
});
}).catch(err => {
console.error(err);
});
// Get latest collection from Git repository
app.server.get('/sync', (req, res) =>
app.miscRequest(req, res, 'sync')
);
app.server.post('/sync', (req, res) =>
app.miscRequest(req, res, 'sync')
);
// Redirect home page
app.server.get('/', (req, res) => {
res.redirect(301, app.config['index-page']);
});
// Create server
app.server.listen(app.config.port, () => {
app.log('Listening on port ' + app.config.port);
});
})
.catch(err => {
console.error(err);
});

View File

@ -1,52 +1,52 @@
{ {
"port": 3000, "port": 3000,
"env-port": true, "env-port": true,
"region": "", "region": "",
"env-region": true, "env-region": true,
"reload-secret": "", "reload-secret": "",
"custom-icons-dir": "{dir}/json", "custom-icons-dir": "{dir}/json",
"serve-default-icons": true, "serve-default-icons": true,
"index-page": "https://iconify.design/", "index-page": "https://iconify.design/",
"json-loader": "parse", "json-loader": "parse",
"cache": { "cache": {
"timeout": 604800, "timeout": 604800,
"min-refresh": 604800, "min-refresh": 604800,
"private": false "private": false
}, },
"cors": { "cors": {
"origins": "*", "origins": "*",
"timeout": 86400, "timeout": 86400,
"methods": "GET, OPTIONS", "methods": "GET, OPTIONS",
"headers": "Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding" "headers": "Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding"
}, },
"sync": { "sync": {
"sync-on-startup": "missing", "sync-on-startup": "missing",
"sync-delay": 60, "sync-delay": 60,
"repeated-sync-delay": 60, "repeated-sync-delay": 60,
"versions": "{dir}/git-repos/versions.json", "versions": "{dir}/git-repos/versions.json",
"storage": "{dir}/git-repos", "storage": "{dir}/git-repos",
"git": "git clone {repo} --depth 1 --no-tags {target}", "git": "git clone {repo} --depth 1 --no-tags {target}",
"secret": "", "secret": "",
"iconify": "https://github.com/iconify/collections-json.git", "iconify": "https://github.com/iconify/collections-json.git",
"custom": "", "custom": "",
"custom-dir": "", "custom-dir": "",
"rm": "rm -rf {dir}" "rm": "rm -rf {dir}"
}, },
"mail": { "mail": {
"active": false, "active": false,
"throttle": 30, "throttle": 30,
"repeat": 180, "repeat": 180,
"from": "noreply@localhost", "from": "noreply@localhost",
"to": "noreply@localhost", "to": "noreply@localhost",
"subject": "Iconify API log", "subject": "Iconify API log",
"transport": { "transport": {
"host": "smtp.ethereal.email", "host": "smtp.ethereal.email",
"port": 587, "port": 587,
"secure": false, "secure": false,
"auth": { "auth": {
"user": "username", "user": "username",
"pass": "password" "pass": "password"
} }
} }
} }
} }

1238
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,30 @@
{ {
"version": "2.0.1", "version": "2.0.1",
"description": "Node.js version of api.iconify.design", "description": "Node.js version of api.iconify.design",
"private": true, "private": true,
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"start": "node app.js", "start": "node app.js",
"test": "mocha tests/*_test.js" "test": "mocha tests/*_test.js"
}, },
"author": "Vjacheslav Trushkin", "author": "Vjacheslav Trushkin",
"license": "MIT", "license": "MIT",
"bugs": "https://github.com/iconify/api.js/issues", "bugs": "https://github.com/iconify/api.js/issues",
"homepage": "https://github.com/iconify/api.js", "homepage": "https://github.com/iconify/api.js",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+ssh://git@github.com/iconify/api.js.git" "url": "https://github.com/iconify/api.js.git"
}, },
"dependencies": { "dependencies": {
"@iconify/json-tools": "^1.0.6", "@iconify/json-tools": "^1.0.6",
"express": "^4.17.1" "express": "^4.17.1"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.2.0", "chai": "^4.2.0",
"mocha": "^5.2.0" "mocha": "^5.2.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@iconify/json": "*", "@iconify/json": "*",
"nodemailer": "^4.6.8" "nodemailer": "^4.6.8"
} }
} }

View File

@ -7,7 +7,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
const fs = require('fs'); const fs = require('fs');
@ -19,172 +19,180 @@ const fs = require('fs');
* @returns {object} * @returns {object}
*/ */
module.exports = app => { module.exports = app => {
let functions = Object.create(null), let functions = Object.create(null),
dirs = Object.create(null), dirs = Object.create(null),
custom = Object.create(null), custom = Object.create(null),
repos = [], repos = [],
storageDir = null, storageDir = null,
versionsFile = null; versionsFile = null;
/** /**
* Get root directory of repository * Get root directory of repository
* *
* @param {string} repo * @param {string} repo
* @returns {string} * @returns {string}
*/ */
functions.rootDir = repo => dirs[repo] === void 0 ? '' : dirs[repo]; functions.rootDir = repo => (dirs[repo] === void 0 ? '' : dirs[repo]);
/** /**
* Get storage directory * Get storage directory
* *
* @return {string} * @return {string}
*/ */
functions.storageDir = () => storageDir; functions.storageDir = () => storageDir;
/** /**
* Get icons directory * Get icons directory
* *
* @param {string} repo * @param {string} repo
* @returns {string} * @returns {string}
*/ */
functions.iconsDir = repo => { functions.iconsDir = repo => {
let dir; let dir;
switch (repo) { switch (repo) {
case 'iconify': case 'iconify':
dir = functions.rootDir(repo); dir = functions.rootDir(repo);
return dir === '' ? '' : dir + '/json'; return dir === '' ? '' : dir + '/json';
default: default:
return functions.rootDir(repo); return functions.rootDir(repo);
} }
}; };
/** /**
* Set root directory for repository * Set root directory for repository
* *
* @param {string} repo * @param {string} repo
* @param {string} dir * @param {string} dir
*/ */
functions.setRootDir = (repo, dir) => { functions.setRootDir = (repo, dir) => {
// Append additional directory from config // Append additional directory from config
let extra; let extra;
try { try {
extra = app.config.sync[repo + '-dir']; extra = app.config.sync[repo + '-dir'];
} catch (err) { } catch (err) {
extra = ''; extra = '';
} }
if (extra !== void 0 && extra !== '') { if (extra !== void 0 && extra !== '') {
if (extra.slice(0, 1) !== '/') { if (extra.slice(0, 1) !== '/') {
extra = '/' + extra; extra = '/' + extra;
} }
if (extra.slice(-1) === '/') { if (extra.slice(-1) === '/') {
extra = extra.slice(0, extra.length - 1); extra = extra.slice(0, extra.length - 1);
} }
dir += extra; dir += extra;
} }
// Set directory // Set directory
dirs[repo] = dir; dirs[repo] = dir;
}; };
/** /**
* Set root directory for repository using repository time * Set root directory for repository using repository time
* *
* @param {string} repo * @param {string} repo
* @param {number} time * @param {number} time
* @param {boolean} [save] True if new versions.json should be saved * @param {boolean} [save] True if new versions.json should be saved
*/ */
functions.setSynchronizedRepoDir = (repo, time, save) => { functions.setSynchronizedRepoDir = (repo, time, save) => {
let dir = storageDir + '/' + repo + '.' + time; let dir = storageDir + '/' + repo + '.' + time;
custom[repo] = time; custom[repo] = time;
functions.setRootDir(repo, dir); functions.setRootDir(repo, dir);
if (save === true) { if (save === true) {
fs.writeFileSync(versionsFile, JSON.stringify(custom, null, 4), 'utf8'); fs.writeFileSync(
} versionsFile,
}; JSON.stringify(custom, null, 4),
'utf8'
);
}
};
/** /**
* Get all repositories * Get all repositories
* *
* @returns {string[]} * @returns {string[]}
*/ */
functions.keys = () => Object.keys(dirs); functions.keys = () => Object.keys(dirs);
/** /**
* Get all repositories * Get all repositories
* *
* @returns {string[]} * @returns {string[]}
*/ */
functions.getRepos = () => repos; functions.getRepos = () => repos;
/** /**
* Check if repository has been synchronized * Check if repository has been synchronized
* *
* @param {string} repo * @param {string} repo
* @return {boolean} * @return {boolean}
*/ */
functions.synchronized = repo => custom[repo] === true; functions.synchronized = repo => custom[repo] === true;
/** /**
* Initialize * Initialize
*/ */
// Get synchronized repositories // Get synchronized repositories
let cached = Object.create(null); let cached = Object.create(null);
app.config.canSync = false; app.config.canSync = false;
try { try {
if (app.config.sync.versions && app.config.sync.storage) { if (app.config.sync.versions && app.config.sync.storage) {
// Set storage directory and versions.json location // Set storage directory and versions.json location
storageDir = app.config.sync.storage.replace('{dir}', app.root); storageDir = app.config.sync.storage.replace('{dir}', app.root);
versionsFile = app.config.sync.versions.replace('{dir}', app.root); versionsFile = app.config.sync.versions.replace('{dir}', app.root);
app.config.canSync = true; app.config.canSync = true;
// Try getting latest repositories // Try getting latest repositories
cached = fs.readFileSync(versionsFile, 'utf8'); cached = fs.readFileSync(versionsFile, 'utf8');
cached = JSON.parse(cached); cached = JSON.parse(cached);
} }
} catch (err) { } catch (err) {
if (typeof cached !== 'object') { if (typeof cached !== 'object') {
cached = Object.create(null); cached = Object.create(null);
} }
} }
if (storageDir !== null) { if (storageDir !== null) {
try { try {
fs.mkdirSync(storageDir); fs.mkdirSync(storageDir);
} catch (err) { } catch (err) {}
} }
}
// Set default directories // Set default directories
if (app.config['serve-default-icons']) { if (app.config['serve-default-icons']) {
let key = 'iconify'; let key = 'iconify';
if (cached && cached[key]) { if (cached && cached[key]) {
repos.push(key); repos.push(key);
functions.setSynchronizedRepoDir(key, cached[key], false); functions.setSynchronizedRepoDir(key, cached[key], false);
} else { } else {
let icons; let icons;
try { try {
icons = require('@iconify/json'); icons = require('@iconify/json');
repos.push(key); repos.push(key);
dirs[key] = icons.rootDir(); dirs[key] = icons.rootDir();
} catch (err) { } catch (err) {
app.error('Cannot load Iconify icons because @iconify/json package is not installed'); app.error(
} 'Cannot load Iconify icons because @iconify/json package is not installed'
} );
} }
}
}
if (app.config['custom-icons-dir']) { if (app.config['custom-icons-dir']) {
let key = 'custom'; let key = 'custom';
repos.push(key); repos.push(key);
if (cached[key]) { if (cached[key]) {
functions.setSynchronizedRepoDir(key, cached[key], false); functions.setSynchronizedRepoDir(key, cached[key], false);
} else { } else {
dirs[key] = app.config['custom-icons-dir'].replace('{dir}', app.root); dirs[key] = app.config['custom-icons-dir'].replace(
} '{dir}',
} app.root
);
}
}
return functions; return functions;
}; };

View File

@ -7,7 +7,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
const fs = require('fs'); const fs = require('fs');
const util = require('util'); const util = require('util');
@ -16,87 +16,115 @@ const promiseEach = require('./promise');
let _app; let _app;
let functions = { let functions = {
/** /**
* Remove file * Remove file
* *
* @param file * @param file
* @param options * @param options
* @return {Promise<any>} * @return {Promise<any>}
*/ */
unlink: (file, options) => new Promise((fulfill, reject) => { unlink: (file, options) =>
fs.unlink(file, err => { new Promise((fulfill, reject) => {
if (err) { fs.unlink(file, err => {
_app.error('Error deleting file ' + file, Object.assign({ if (err) {
key: 'unlink-' + file _app.error(
}, typeof options === 'object' ? options : Object.create(null))); 'Error deleting file ' + file,
} Object.assign(
fulfill(); {
}) key: 'unlink-' + file,
}), },
typeof options === 'object'
? options
: Object.create(null)
)
);
}
fulfill();
});
}),
/** /**
* Recursively remove directory * Recursively remove directory
* *
* @param dir * @param dir
* @param options * @param options
* @return {Promise<any>} * @return {Promise<any>}
*/ */
rmdir: (dir, options) => new Promise((fulfill, reject) => { rmdir: (dir, options) =>
options = typeof options === 'object' ? options : Object.create(null); new Promise((fulfill, reject) => {
options =
typeof options === 'object' ? options : Object.create(null);
function done() { function done() {
fs.rmdir(dir, err => { fs.rmdir(dir, err => {
if (err) { if (err) {
_app.error('Error deleting directory ' + dir, Object.assign({ _app.error(
key: 'rmdir-' + dir 'Error deleting directory ' + dir,
}, options)); Object.assign(
} {
fulfill(); key: 'rmdir-' + dir,
}); },
} options
)
);
}
fulfill();
});
}
fs.readdir(dir, (err, files) => { fs.readdir(dir, (err, files) => {
if (err) { if (err) {
// fulfill instead of rejecting // fulfill instead of rejecting
fulfill(); fulfill();
return; return;
} }
let children = Object.create(null); let children = Object.create(null);
files.forEach(file => { files.forEach(file => {
let filename = dir + '/' + file, let filename = dir + '/' + file,
stats = fs.lstatSync(filename); stats = fs.lstatSync(filename);
if (stats.isDirectory()) { if (stats.isDirectory()) {
children[filename] = true; children[filename] = true;
return; return;
} }
if (stats.isFile() || stats.isSymbolicLink()) { if (stats.isFile() || stats.isSymbolicLink()) {
children[filename] = false; children[filename] = false;
} }
}); });
promiseEach(Object.keys(children), file => { promiseEach(Object.keys(children), file => {
if (children[file]) { if (children[file]) {
return functions.rmdir(file, options); return functions.rmdir(file, options);
} else { } else {
return functions.unlink(file, options); return functions.unlink(file, options);
} }
}).then(() => { })
done(); .then(() => {
}).catch(err => { done();
_app.error('Error recursively removing directory ' + dir + '\n' + util.format(err), Object.assign({ })
key: 'rmdir-' + dir .catch(err => {
}, options)); _app.error(
done(); 'Error recursively removing directory ' +
}); dir +
}); '\n' +
}) util.format(err),
Object.assign(
{
key: 'rmdir-' + dir,
},
options
)
);
done();
});
});
}),
}; };
module.exports = app => { module.exports = app => {
_app = app; _app = app;
return functions; return functions;
}; };

View File

@ -7,7 +7,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
const fs = require('fs'); const fs = require('fs');
const util = require('util'); const util = require('util');
@ -23,121 +23,126 @@ let imported = Object.create(null);
* @param {*} [hash] Hash of previously loaded file. If hashes match, load will be aborted * @param {*} [hash] Hash of previously loaded file. If hashes match, load will be aborted
* @returns {Promise} * @returns {Promise}
*/ */
module.exports = (app, file, hash) => new Promise((fulfill, reject) => { module.exports = (app, file, hash) =>
let newHash = null, new Promise((fulfill, reject) => {
result; let newHash = null,
result;
/** /**
* Parse json using JSONStream library * Parse json using JSONStream library
* *
* @param JSONStream * @param JSONStream
* @param es * @param es
*/ */
function parseStream(JSONStream, es) { function parseStream(JSONStream, es) {
let stream = fs.createReadStream(file, 'utf8'), let stream = fs.createReadStream(file, 'utf8'),
data; data;
stream.on('error', err => { stream.on('error', err => {
reject('Error importing ' + file + '\n' + util.format(err)); reject('Error importing ' + file + '\n' + util.format(err));
}); });
stream.on('end', () => { stream.on('end', () => {
result.data = data; result.data = data;
fulfill(result); fulfill(result);
}); });
stream.pipe(JSONStream.parse(true)).pipe(es.mapSync(res => { stream.pipe(JSONStream.parse(true)).pipe(
data = res; es.mapSync(res => {
})); data = res;
} })
);
}
/** /**
* Common parser that uses synchronous functions to convert string to object * Common parser that uses synchronous functions to convert string to object
* *
* @param method * @param method
*/ */
function syncParser(method) { function syncParser(method) {
fs.readFile(file, 'utf8', (err, data) => { fs.readFile(file, 'utf8', (err, data) => {
if (err) { if (err) {
reject('Error importing ' + file + '\n' + util.format(err)); reject('Error importing ' + file + '\n' + util.format(err));
return; return;
} }
try { try {
switch (method) { switch (method) {
case 'eval': case 'eval':
data = Function('return ' + data)(); data = Function('return ' + data)();
break; break;
default: default:
data = JSON.parse(data); data = JSON.parse(data);
break; break;
} }
} catch (err) { } catch (err) {
reject('Error importing ' + file + '\n' + util.format(err)); reject('Error importing ' + file + '\n' + util.format(err));
return; return;
} }
result.data = data; result.data = data;
fulfill(result); fulfill(result);
}); });
} }
// Get file information // Get file information
fs.lstat(file, (err, stats) => { fs.lstat(file, (err, stats) => {
if (!err) { if (!err) {
// Use file size instead of hash for faster loading // Use file size instead of hash for faster loading
// assume json files are same when size is not changed // assume json files are same when size is not changed
newHash = stats.size; newHash = stats.size;
} }
if (newHash && newHash === hash) { if (newHash && newHash === hash) {
fulfill({ fulfill({
changed: false, changed: false,
hash: newHash hash: newHash,
}); });
return; return;
} }
result = { result = {
changed: true, changed: true,
hash: newHash hash: newHash,
}; };
// Figure out which parser to use // Figure out which parser to use
// 'eval' is fastest, but its not safe // 'eval' is fastest, but its not safe
// 'json' is slower, but might crash when memory limit is low // 'json' is slower, but might crash when memory limit is low
// 'stream' is // 'stream' is
let parser = 'parse'; let parser = 'parse';
try { try {
parser = typeof app === 'string' ? app : app.config['json-loader']; parser =
} catch(err) { typeof app === 'string' ? app : app.config['json-loader'];
} } catch (err) {}
switch (parser) { switch (parser) {
case 'stream': case 'stream':
// use stream // use stream
if (imported.JSONStream === void 0) { if (imported.JSONStream === void 0) {
try { try {
imported.JSONStream = require('JSONStream'); imported.JSONStream = require('JSONStream');
imported.eventStream = require('event-stream'); imported.eventStream = require('event-stream');
} catch (err) { } catch (err) {
console.error('Cannot use stream JSON parser because JSONStream or event-stream module is not available. Switching to default parser.'); console.error(
imported.JSONStream = null; 'Cannot use stream JSON parser because JSONStream or event-stream module is not available. Switching to default parser.'
} );
} imported.JSONStream = null;
}
}
if (imported.JSONStream === null) { if (imported.JSONStream === null) {
syncParser('json'); syncParser('json');
} else { } else {
parseStream(imported.JSONStream, imported.eventStream); parseStream(imported.JSONStream, imported.eventStream);
} }
break; break;
case 'eval': case 'eval':
// use Function() // use Function()
syncParser('eval'); syncParser('eval');
break; break;
default: default:
// use JSON.parse() // use JSON.parse()
syncParser('json'); syncParser('json');
} }
}); });
}); });

View File

@ -7,22 +7,22 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
const util = require('util'); const util = require('util');
const defaultOptions = { const defaultOptions = {
// True if message should be copied to stdout or stderr // True if message should be copied to stdout or stderr
log: true, log: true,
// Logger object for event logging (combines multiple messages for one big log) // Logger object for event logging (combines multiple messages for one big log)
logger: null, logger: null,
// Unique key. If set, message with that key will be sent by mail only once. Used to avoid sending too many emails // Unique key. If set, message with that key will be sent by mail only once. Used to avoid sending too many emails
key: null, key: null,
// Console object // Console object
console: console console: console,
}; };
// List of notices that are sent only once per session // List of notices that are sent only once per session
@ -37,9 +37,9 @@ let throttled = null;
* @param app * @param app
*/ */
const sendQueue = app => { const sendQueue = app => {
let text = throttled.join('\n\n- - - - - - - - - - -\n\n'); let text = throttled.join('\n\n- - - - - - - - - - -\n\n');
throttled = null; throttled = null;
app.mail(text); app.mail(text);
}; };
/** /**
@ -54,71 +54,86 @@ const sendQueue = app => {
* @param {object|boolean} [options] * @param {object|boolean} [options]
*/ */
module.exports = (app, error, message, options) => { module.exports = (app, error, message, options) => {
options = Object.assign(Object.create(null), defaultOptions, options === void 0 ? Object.create(null) : (typeof options === 'boolean' ? { options = Object.assign(
log: options Object.create(null),
}: options)); defaultOptions,
options === void 0
? Object.create(null)
: typeof options === 'boolean'
? {
log: options,
}
: options
);
// Convert to test // Convert to test
if (typeof message !== 'string') { if (typeof message !== 'string') {
message = util.format(message); message = util.format(message);
} }
// Get time stamp // Get time stamp
let time = new Date(); let time = new Date();
time = (time.getUTCHours() > 9 ? '[' : '[0') + time.getUTCHours() + (time.getUTCMinutes() > 9 ? ':' : ':0') + time.getUTCMinutes() + (time.getUTCSeconds() > 9 ? ':' : ':0') + time.getUTCSeconds() + '] '; time =
(time.getUTCHours() > 9 ? '[' : '[0') +
time.getUTCHours() +
(time.getUTCMinutes() > 9 ? ':' : ':0') +
time.getUTCMinutes() +
(time.getUTCSeconds() > 9 ? ':' : ':0') +
time.getUTCSeconds() +
'] ';
// Copy message to console // Copy message to console
if (options.log || !app.mail) { if (options.log || !app.mail) {
if (error) { if (error) {
options.console.error(time + '\x1b[31m' + message + '\x1b[0m'); options.console.error(time + '\x1b[31m' + message + '\x1b[0m');
} else { } else {
options.console.log(time + message); options.console.log(time + message);
} }
} }
if (!app.mail) { if (!app.mail) {
return; return;
} }
message = time + message; message = time + message;
// Copy to mail logger // Copy to mail logger
if (options.logger) { if (options.logger) {
options.logger[error ? 'error' : 'log'](message); options.logger[error ? 'error' : 'log'](message);
return; return;
} }
// Send email if its a error and has not been sent before // Send email if its a error and has not been sent before
if (!error) { if (!error) {
return; return;
} }
if (options.key) { if (options.key) {
let time = Date.now() / 1000, let time = Date.now() / 1000,
repeat; repeat;
try { try {
repeat = app.config.mail.repeat; repeat = app.config.mail.repeat;
} catch (err) { } catch (err) {
repeat = 0; repeat = 0;
} }
if (logged[options.key]) { if (logged[options.key]) {
if (!repeat || logged[options.key] > time) { if (!repeat || logged[options.key] > time) {
return; return;
} }
} }
logged[options.key] = repeat ? time + repeat : true; logged[options.key] = repeat ? time + repeat : true;
} }
// Add message to throttled data // Add message to throttled data
if (throttled === null) { if (throttled === null) {
throttled = []; throttled = [];
let delay; let delay;
try { try {
delay = app.config.mail.throttle; delay = app.config.mail.throttle;
} catch (err) { } catch (err) {
delay = 60; delay = 60;
} }
setTimeout(sendQueue.bind(null, app), delay * 1000) setTimeout(sendQueue.bind(null, app), delay * 1000);
} }
throttled.push(message); throttled.push(message);
}; };

View File

@ -7,47 +7,51 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
class Logger { class Logger {
constructor(app, subject, delay) { constructor(app, subject, delay) {
this.app = app; this.app = app;
this.subject = subject; this.subject = subject;
this.messages = []; this.messages = [];
this.delay = typeof delay === 'number' ? Math.min(Math.max(delay, 15), 300) : 60; this.delay =
this.throttled = false; typeof delay === 'number' ? Math.min(Math.max(delay, 15), 300) : 60;
} this.throttled = false;
}
send() { send() {
if (this.messages.length) { if (this.messages.length) {
this.app.mail((this.subject ? this.subject + '\n\n' : '') + this.messages.join('\n')); this.app.mail(
this.messages = []; (this.subject ? this.subject + '\n\n' : '') +
} this.messages.join('\n')
} );
this.messages = [];
}
}
queue() { queue() {
if (!this.throttled) { if (!this.throttled) {
this.throttled = true; this.throttled = true;
setTimeout(() => { setTimeout(() => {
this.send(); this.send();
this.throttled = false; this.throttled = false;
}, this.delay * 1000); }, this.delay * 1000);
} }
} }
log(message) { log(message) {
this.messages.push(message); this.messages.push(message);
if (!this.throttled) { if (!this.throttled) {
this.queue(); this.queue();
} }
} }
error(message) { error(message) {
this.messages.push(message); this.messages.push(message);
if (!this.throttled) { if (!this.throttled) {
this.queue(); this.queue();
} }
} }
} }
/** /**

View File

@ -7,44 +7,44 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
let nodemailer; let nodemailer;
module.exports = (app, message) => { module.exports = (app, message) => {
if (nodemailer === null) { if (nodemailer === null) {
return; return;
} }
let config; let config;
try { try {
config = app.config.mail; config = app.config.mail;
if (!config.active) { if (!config.active) {
return; return;
} }
if (nodemailer === void 0) { if (nodemailer === void 0) {
nodemailer = require('nodemailer'); nodemailer = require('nodemailer');
} }
} catch (err) { } catch (err) {
nodemailer = null; nodemailer = null;
return; return;
} }
let transporter = nodemailer.createTransport(config.transport); let transporter = nodemailer.createTransport(config.transport);
// Set data // Set data
let mailOptions = { let mailOptions = {
from: config.from, from: config.from,
to: config.to, to: config.to,
subject: config.subject, subject: config.subject,
text: message text: message,
}; };
// Send email // Send email
transporter.sendMail(mailOptions, (err, info) => { transporter.sendMail(mailOptions, (err, info) => {
if (err) { if (err) {
console.error('Error sending mail:', err); console.error('Error sending mail:', err);
} }
}); });
}; };

View File

@ -7,7 +7,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
/** /**
* Alternative to Promise.all() that runs each promise after another, not simultaneously * Alternative to Promise.all() that runs each promise after another, not simultaneously
@ -16,31 +16,34 @@
* @param callback * @param callback
* @returns {Promise<any>} * @returns {Promise<any>}
*/ */
module.exports = (list, callback) => new Promise((fulfill, reject) => { module.exports = (list, callback) =>
let results = [], new Promise((fulfill, reject) => {
index = -1, let results = [],
total = list.length; index = -1,
total = list.length;
function next() { function next() {
index ++; index++;
if (index === total) { if (index === total) {
fulfill(results); fulfill(results);
return; return;
} }
let promise = callback(list[index]); let promise = callback(list[index]);
if (promise === null) { if (promise === null) {
// skip // skip
next(); next();
return; return;
} }
promise.then(result => { promise
results.push(result); .then(result => {
next(); results.push(result);
}).catch(err => { next();
reject(err); })
}) .catch(err => {
} reject(err);
});
}
next(); next();
}); });

View File

@ -7,7 +7,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
const fs = require('fs'); const fs = require('fs');
const util = require('util'); const util = require('util');
@ -15,238 +15,340 @@ const promiseEach = require('./promise');
const Collection = require('@iconify/json-tools').Collection; const Collection = require('@iconify/json-tools').Collection;
const defaultOptions = { const defaultOptions = {
// Logger instance // Logger instance
logger: null logger: null,
}; };
let repoItems = Object.create(null), let repoItems = Object.create(null),
collectionRepos = Object.create(null), collectionRepos = Object.create(null),
hashes = Object.create(null), hashes = Object.create(null),
nextReload = 0; nextReload = 0;
class Loader { class Loader {
constructor(app, repos, options) { constructor(app, repos, options) {
this.app = app; this.app = app;
this.repos = repos; this.repos = repos;
this.options = options; this.options = options;
this.updated = []; this.updated = [];
this.start = Date.now(); this.start = Date.now();
this.reloadInfo = false; this.reloadInfo = false;
} }
/** /**
* Remove root directory from filename * Remove root directory from filename
* *
* @param {string} filename * @param {string} filename
* @return {string} * @return {string}
* @private * @private
*/ */
_prettyFile(filename) { _prettyFile(filename) {
return filename.slice(0, this.app.root.length) === this.app.root ? filename.slice(this.app.root.length + 1) : filename; return filename.slice(0, this.app.root.length) === this.app.root
} ? filename.slice(this.app.root.length + 1)
: filename;
}
/** /**
* Find collections * Find collections
* *
* @return {Promise<Array>} * @return {Promise<Array>}
*/ */
findCollections() { findCollections() {
return new Promise((fulfill, reject) => { return new Promise((fulfill, reject) => {
promiseEach(this.repos, repo => new Promise((fulfill, reject) => { promiseEach(
// Get directory this.repos,
let dir = this.app.dirs.iconsDir(repo); repo =>
if (dir === '') { new Promise((fulfill, reject) => {
reject('Missing directory for repository "' + repo + '"'); // Get directory
return; let dir = this.app.dirs.iconsDir(repo);
} if (dir === '') {
reject(
'Missing directory for repository "' +
repo +
'"'
);
return;
}
// Find all files // Find all files
fs.readdir(dir, (err, files) => { fs.readdir(dir, (err, files) => {
let items = []; let items = [];
if (err) { if (err) {
reject('Error reading directory: ' + this._prettyFile(dir) + '\n' + util.format(err)); reject(
return; 'Error reading directory: ' +
} this._prettyFile(dir) +
files.forEach(file => { '\n' +
if (file.slice(-5) !== '.json') { util.format(err)
return; );
} return;
items.push({ }
repo: repo, files.forEach(file => {
file: file, if (file.slice(-5) !== '.json') {
filename: dir + '/' + file, return;
prefix: file.slice(0, file.length - 5) }
}); items.push({
}); repo: repo,
fulfill(items); file: file,
}); filename: dir + '/' + file,
})).then(results => { prefix: file.slice(0, file.length - 5),
let items = []; });
});
fulfill(items);
});
})
)
.then(results => {
let items = [];
results.forEach(result => { results.forEach(result => {
result.forEach(item => { result.forEach(item => {
if (collectionRepos[item.prefix] === void 0) { if (collectionRepos[item.prefix] === void 0) {
// New collection. Add it to list // New collection. Add it to list
if (repoItems[item.repo] === void 0) { if (repoItems[item.repo] === void 0) {
repoItems[item.repo] = [item.prefix]; repoItems[item.repo] = [item.prefix];
} else { } else {
repoItems[item.repo].push(item.prefix); repoItems[item.repo].push(item.prefix);
} }
items.push(item); items.push(item);
return; return;
} }
if (collectionRepos[item.prefix] !== item.repo) { if (collectionRepos[item.prefix] !== item.repo) {
// Conflict: same prefix in multiple repositories // Conflict: same prefix in multiple repositories
this.app.error('Collection "' + item.prefix + '" is found in multiple repositories. Ignoring json file from ' + item.repo + ', using file from ' + collectionRepos[item.prefix], Object.assign({ this.app.error(
key: 'json-duplicate/' + item.repo + '/' + item.prefix 'Collection "' +
}, this.options)); item.prefix +
return; '" is found in multiple repositories. Ignoring json file from ' +
} item.repo +
', using file from ' +
collectionRepos[item.prefix],
Object.assign(
{
key:
'json-duplicate/' +
item.repo +
'/' +
item.prefix,
},
this.options
)
);
return;
}
// Everything is fine // Everything is fine
items.push(item); items.push(item);
}); });
});
fulfill(items);
})
.catch(err => {
reject(err);
});
});
}
}); /**
fulfill(items); * Load collections
}).catch(err => { *
reject(err); * @param {Array} items
}); * @return {Promise<any>}
}); */
} loadCollections(items) {
return new Promise((fulfill, reject) => {
let total;
/** // Load all files
* Load collections promiseEach(
* items,
* @param {Array} items item =>
* @return {Promise<any>} new Promise((fulfill, reject) => {
*/ let collection;
loadCollections(items) {
return new Promise((fulfill, reject) => {
let total;
// Load all files // Load JSON file
promiseEach(items, item => new Promise((fulfill, reject) => { this.app
let collection; .loadJSON(
item.filename,
hashes[item.prefix] === void 0
? null
: hashes[item.prefix]
)
.then(result => {
if (!result.changed) {
// Nothing to do
fulfill(true);
return;
}
// Load JSON file return this.loadCollection(item, result);
this.app.loadJSON(item.filename, hashes[item.prefix] === void 0 ? null : hashes[item.prefix]).then(result => { })
if (!result.changed) { .then(result => {
// Nothing to do collection = result;
fulfill(true);
return;
}
return this.loadCollection(item, result); // Run post-load function if there is one
}).then(result => { if (this.app.postLoadCollection) {
collection = result; return this.app.postLoadCollection(
collection,
this.options
);
}
})
.then(() => {
fulfill(collection);
})
.catch(err => {
reject(
'Error loading json file: ' +
this._prettyFile(item.filename) +
'\n' +
util.format(err)
);
});
})
)
.then(collections => {
let loaded = 0,
skipped = 0;
// Run post-load function if there is one total = 0;
if (this.app.postLoadCollection) { collections.forEach(collection => {
return this.app.postLoadCollection(collection, this.options); if (collection === true) {
} skipped++;
}).then(() => { return;
fulfill(collection); }
}).catch(err => { loaded++;
reject('Error loading json file: ' + this._prettyFile(item.filename) + '\n' + util.format(err));
});
})).then(collections => { let count = Object.keys(collection.items.icons).length,
let loaded = 0, prefix = collection.prefix();
skipped = 0;
total = 0; this.app.log(
collections.forEach(collection => { 'Loaded collection ' +
if (collection === true) { prefix +
skipped ++; ' from ' +
return; collection.filename +
} ' (' +
loaded ++; count +
' icons)',
this.options
);
total += count;
this.app.collections[prefix] = collection;
});
this.app.log(
'Loaded ' +
total +
' icons from ' +
loaded +
(loaded > 1 ? ' collections ' : ' collection ') +
(skipped
? '(no changes in ' +
skipped +
(skipped > 1
? ' collections) '
: ' collection) ')
: '') +
'in ' +
(Date.now() - this.start) / 1000 +
' seconds.',
this.options
);
let count = Object.keys(collection.items.icons).length, if (this.reloadInfo) {
prefix = collection.prefix(); return this.getCollectionsJSON();
}
})
.then(() => {
fulfill(total);
})
.catch(err => {
reject(err);
});
});
}
this.app.log('Loaded collection ' + prefix + ' from ' + collection.filename + ' (' + count + ' icons)', this.options); /**
total += count; * Get Iconify collections data
this.app.collections[prefix] = collection; *
}); * @return {Promise<any>}
this.app.log('Loaded ' + total + ' icons from ' + loaded + (loaded > 1 ? ' collections ' : ' collection ') + (skipped ? '(no changes in ' + skipped + (skipped > 1 ? ' collections) ' : ' collection) ') : '') + 'in ' + (Date.now() - this.start) / 1000 + ' seconds.', this.options); */
getCollectionsJSON() {
return new Promise((fulfill, reject) => {
let filename =
this.app.dirs.rootDir('iconify') + '/collections.json';
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
reject(
'Error locating collections.json for Iconify default icons.\n' +
util.format(err)
);
return;
}
if (this.reloadInfo) { try {
return this.getCollectionsJSON(); data = JSON.parse(data);
} } catch (err) {
reject(
'Error reading contents of' +
filename +
'\n' +
util.format(err)
);
return;
}
}).then(() => { this.app.collectionsJSON = data;
fulfill(total); fulfill();
}).catch(err => { });
reject(err); });
}); }
});
}
/** /**
* Get Iconify collections data * Load one collection
* *
* @return {Promise<any>} * @param {object} item findCollections() result
*/ * @param {object} data loadJSON() result
getCollectionsJSON() { * @return {Promise<Collection>}
return new Promise((fulfill, reject) => { */
let filename = this.app.dirs.rootDir('iconify') + '/collections.json'; loadCollection(item, data) {
fs.readFile(filename, 'utf8', (err, data) => { return new Promise((fulfill, reject) => {
if (err) { let collection = new Collection();
reject('Error locating collections.json for Iconify default icons.\n' + util.format(err)); if (!collection.loadJSON(data.data, item.prefix)) {
return; delete data.data;
} reject(
'Error loading collection "' +
item.prefix +
'" from repository "' +
item.repo +
'": error parsing JSON'
);
return;
}
delete data.data;
try { let prefix = collection.prefix();
data = JSON.parse(data); if (prefix !== item.prefix) {
} catch (err) { delete collection.items;
reject('Error reading contents of' + filename + '\n' + util.format(err)); reject(
return; 'Error loading collection "' +
} item.prefix +
'" from repository "' +
item.repo +
'": invalid prefix in JSON file: ' +
prefix
);
return;
}
this.app.collectionsJSON = data; collection.filename = this._prettyFile(item.filename);
fulfill(); collection.repo = item.repo;
}); hashes[item.prefix] = data.hash;
}); this.updated.push(item.prefix);
} if (item.repo === 'iconify') {
this.reloadInfo = true;
}
/** fulfill(collection);
* Load one collection });
* }
* @param {object} item findCollections() result
* @param {object} data loadJSON() result
* @return {Promise<Collection>}
*/
loadCollection(item, data) {
return new Promise((fulfill, reject) => {
let collection = new Collection();
if (!collection.loadJSON(data.data, item.prefix)) {
delete data.data;
reject('Error loading collection "' + item.prefix + '" from repository "' + item.repo + '": error parsing JSON');
return;
}
delete data.data;
let prefix = collection.prefix();
if (prefix !== item.prefix) {
delete collection.items;
reject('Error loading collection "' + item.prefix + '" from repository "' + item.repo + '": invalid prefix in JSON file: ' + prefix);
return;
}
collection.filename = this._prettyFile(item.filename);
collection.repo = item.repo;
hashes[item.prefix] = data.hash;
this.updated.push(item.prefix);
if (item.repo === 'iconify') {
this.reloadInfo = true;
}
fulfill(collection);
});
}
} }
/** /**
@ -256,89 +358,99 @@ class Loader {
* @param {Array|string|boolean} [repos] Repositories to reload * @param {Array|string|boolean} [repos] Repositories to reload
* @param {object} [options] * @param {object} [options]
*/ */
module.exports = (app, repos, options) => new Promise((fulfill, reject) => { module.exports = (app, repos, options) =>
// Options new Promise((fulfill, reject) => {
options = Object.assign(Object.create(null), defaultOptions, typeof options === 'object' ? options : Object.create(null)); // Options
options = Object.assign(
Object.create(null),
defaultOptions,
typeof options === 'object' ? options : Object.create(null)
);
// Get list of repositories to reload // Get list of repositories to reload
let availableRepos = app.dirs.getRepos(); let availableRepos = app.dirs.getRepos();
// noinspection FallThroughInSwitchStatementJS // noinspection FallThroughInSwitchStatementJS
switch (typeof repos) { switch (typeof repos) {
case 'string': case 'string':
if (availableRepos.indexOf(repos) === -1) { if (availableRepos.indexOf(repos) === -1) {
reject('Cannot update repository: ' + repos); reject('Cannot update repository: ' + repos);
return; return;
} }
repos = [repos]; repos = [repos];
break; break;
case 'object': case 'object':
if (repos instanceof Array) { if (repos instanceof Array) {
let newList = []; let newList = [];
repos.forEach(repo => { repos.forEach(repo => {
if (availableRepos.indexOf(repo) !== -1) { if (availableRepos.indexOf(repo) !== -1) {
newList.push(repo); newList.push(repo);
} }
}); });
repos = newList; repos = newList;
break; break;
} }
case 'boolean': case 'boolean':
if (repos === false) { if (repos === false) {
// false -> reload was called by /reload url // false -> reload was called by /reload url
// limit such reloads to 1 per 30 seconds // limit such reloads to 1 per 30 seconds
if (Date.now() < nextReload) { if (Date.now() < nextReload) {
fulfill(false); fulfill(false);
return; return;
} }
} }
default: default:
repos = availableRepos.slice(0); repos = availableRepos.slice(0);
} }
if (!repos.length) { if (!repos.length) {
reject('No available repositories to update.'); reject('No available repositories to update.');
return; return;
} }
if (app.reloading === true) { if (app.reloading === true) {
reject('Reload is already in progress.'); reject('Reload is already in progress.');
return; return;
} }
// Create logger if its missing // Create logger if its missing
if (!options.logger) { if (!options.logger) {
options.logger = app.logger('Loading repositories', 30); options.logger = app.logger('Loading repositories', 30);
} }
// Create loader instance and do stuff // Create loader instance and do stuff
let loader = new Loader(app, repos, options), let loader = new Loader(app, repos, options),
count; count;
app.reloading = true; app.reloading = true;
loader.findCollections().then(items => { loader
return loader.loadCollections(items); .findCollections()
}).then(total => { .then(items => {
count = total; return loader.loadCollections(items);
})
.then(total => {
count = total;
// Run post-load function if there is one // Run post-load function if there is one
if (app.postReload) { if (app.postReload) {
return app.postReload(loader.updated, options); return app.postReload(loader.updated, options);
} }
}).then(() => { })
// Do not allow /reload for 30 seconds .then(() => {
nextReload = Date.now() + 30000; // Do not allow /reload for 30 seconds
nextReload = Date.now() + 30000;
// Done // Done
fulfill({ fulfill({
icons: count, icons: count,
updated: loader.updated updated: loader.updated,
}); });
app.reloading = false; app.reloading = false;
}).catch(err => { })
reject(err); .catch(err => {
app.reloading = false; reject(err);
}); app.reloading = false;
}); });
});

View File

@ -7,7 +7,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
const SVG = require('@iconify/json-tools').SVG; const SVG = require('@iconify/json-tools').SVG;
@ -19,8 +19,8 @@ const SVG = require('@iconify/json-tools').SVG;
* @returns {string} * @returns {string}
*/ */
function generateSVG(icon, params) { function generateSVG(icon, params) {
let svg = new SVG(icon); let svg = new SVG(icon);
return svg.getSVG(params); return svg.getSVG(params);
} }
/** /**
@ -42,54 +42,57 @@ const _callbackMatch = /^[a-z0-9_.]+$/i;
* @param {string} ext Extension * @param {string} ext Extension
*/ */
module.exports = (app, req, res, prefix, query, ext) => { module.exports = (app, req, res, prefix, query, ext) => {
if (app.collections[prefix] === void 0) { if (app.collections[prefix] === void 0) {
app.response(req, res, 404); app.response(req, res, 404);
return; return;
} }
let collection = app.collections[prefix], let collection = app.collections[prefix],
params = req.query; params = req.query;
let parse = () => { let parse = () => {
switch (ext) { switch (ext) {
case 'svg': case 'svg':
// Generate SVG // Generate SVG
// query = icon name // query = icon name
let icon = collection.getIconData(query); let icon = collection.getIconData(query);
if (icon === null) { if (icon === null) {
return 404; return 404;
} }
return { return {
filename: query + '.svg', filename: query + '.svg',
type: 'image/svg+xml; charset=utf-8', type: 'image/svg+xml; charset=utf-8',
body: generateSVG(icon, params) body: generateSVG(icon, params),
}; };
case 'js': case 'js':
case 'json': case 'json':
if (query !== 'icons' || typeof params.icons !== 'string') { if (query !== 'icons' || typeof params.icons !== 'string') {
return 404; return 404;
} }
let result = collection.getIcons(params.icons.split(',')); let result = collection.getIcons(params.icons.split(','));
if (result === null || !Object.keys(result.icons).length) { if (result === null || !Object.keys(result.icons).length) {
return 404; return 404;
} }
if (result.aliases !== void 0 && !Object.keys(result.aliases).length) { if (
delete result.aliases; result.aliases !== void 0 &&
} !Object.keys(result.aliases).length
) {
delete result.aliases;
}
return { return {
js: ext === 'js', js: ext === 'js',
defaultCallback: 'SimpleSVG._loaderCallback', defaultCallback: 'SimpleSVG._loaderCallback',
data: result data: result,
}; };
default: default:
return 404; return 404;
} }
}; };
app.response(req, res, parse()); app.response(req, res, parse());
}; };

View File

@ -7,7 +7,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
/** /**
* Parse request * Parse request
@ -18,63 +18,80 @@
* @param {string} query Query * @param {string} query Query
*/ */
module.exports = (app, req, res, query) => { module.exports = (app, req, res, query) => {
let body; let body;
switch (query) { switch (query) {
case 'version': case 'version':
body = 'Iconify API version ' + app.version + ' (Node'; body = 'Iconify API version ' + app.version + ' (Node';
if (app.config.region.length) { if (app.config.region.length) {
body += ', ' + app.config.region; body += ', ' + app.config.region;
} }
body += ')'; body += ')';
app.response(req, res, { app.response(req, res, {
type: 'text/plain', type: 'text/plain',
body: body body: body,
}); });
return; return;
case 'robots': case 'robots':
app.response(req, res, { app.response(req, res, {
type: 'text/plain', type: 'text/plain',
body: 'User-agent: *\nDisallow: /' body: 'User-agent: *\nDisallow: /',
}); });
return; return;
case 'reload': case 'reload':
// Send 200 response regardless of success to prevent visitors from guessing key // Send 200 response regardless of success to prevent visitors from guessing key
app.response(req, res, 200); app.response(req, res, 200);
// Do stuff // Do stuff
if (app.config['reload-secret'].length && req.query && typeof req.query.key === 'string' && req.query.key === app.config['reload-secret'] && !app.reloading) { if (
process.nextTick(() => { app.config['reload-secret'].length &&
app.reload(false).then(() => { req.query &&
}).catch(err => { typeof req.query.key === 'string' &&
app.error('Error reloading collections:\n' + util.format(err)); req.query.key === app.config['reload-secret'] &&
}); !app.reloading
}); ) {
} process.nextTick(() => {
return; app.reload(false)
.then(() => {})
.catch(err => {
app.error(
'Error reloading collections:\n' +
util.format(err)
);
});
});
}
return;
case 'sync': case 'sync':
// Send 200 response regardless of success to prevent visitors from guessing key // Send 200 response regardless of success to prevent visitors from guessing key
app.response(req, res, 200); app.response(req, res, 200);
let repo = req.query.repo; let repo = req.query.repo;
if (typeof repo !== 'string' || !app.config.canSync || !app.config.sync[repo] || !app.config.sync.git || !app.config.sync.secret) { if (
return; typeof repo !== 'string' ||
} !app.config.canSync ||
!app.config.sync[repo] ||
!app.config.sync.git ||
!app.config.sync.secret
) {
return;
}
let key = req.query.key; let key = req.query.key;
if (key !== app.config.sync.secret) { if (key !== app.config.sync.secret) {
return; return;
} }
process.nextTick(() => { process.nextTick(() => {
app.sync(repo).then(() => { app.sync(repo)
}).catch(err => { .then(() => {})
app.error(err); .catch(err => {
}); app.error(err);
}); });
return; });
} return;
}
}; };

View File

@ -7,7 +7,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
/** /**
* Regexp for checking callback attribute * Regexp for checking callback attribute
@ -26,62 +26,84 @@ const callbackMatch = /^[a-z0-9_.]+$/i;
* @param result * @param result
*/ */
module.exports = (app, req, res, result) => { module.exports = (app, req, res, result) => {
if (typeof result === 'number') { if (typeof result === 'number') {
// Send error // Send error
res.sendStatus(result); res.sendStatus(result);
return; return;
} }
// Convert JSON(P) response // Convert JSON(P) response
if (result.body === void 0 && result.data !== void 0) { if (result.body === void 0 && result.data !== void 0) {
if (typeof result.data === 'object') { if (typeof result.data === 'object') {
result.body = (req.query.pretty === '1' || req.query.pretty === 'true') ? JSON.stringify(result.data, null, 4) : JSON.stringify(result.data); result.body =
} req.query.pretty === '1' || req.query.pretty === 'true'
? JSON.stringify(result.data, null, 4)
: JSON.stringify(result.data);
}
if (result.js === void 0) { if (result.js === void 0) {
result.js = req.query.callback !== void 0; result.js = req.query.callback !== void 0;
} }
if (result.js === true) { if (result.js === true) {
let callback; let callback;
if (result.callback === void 0 && req.query.callback !== void 0) { if (result.callback === void 0 && req.query.callback !== void 0) {
callback = req.query.callback; callback = req.query.callback;
if (!callback.match(callbackMatch)) { if (!callback.match(callbackMatch)) {
// Invalid callback // Invalid callback
res.sendStatus(400); res.sendStatus(400);
return; return;
} }
} else { } else {
callback = result.callback === void 0 ? result.defaultCallback : result.callback; callback =
if (callback === void 0) { result.callback === void 0
res.sendStatus(400); ? result.defaultCallback
return; : result.callback;
} if (callback === void 0) {
} res.sendStatus(400);
result.body = callback + '(' + result.body + ');'; return;
result.type = 'application/javascript; charset=utf-8'; }
} else { }
result.type = 'application/json; charset=utf-8'; result.body = callback + '(' + result.body + ');';
} result.type = 'application/javascript; charset=utf-8';
} } else {
result.type = 'application/json; charset=utf-8';
}
}
// Send cache header // Send cache header
if ( if (
app.config.cache && app.config.cache.timeout && app.config.cache &&
(req.get('Pragma') === void 0 || req.get('Pragma').indexOf('no-cache') === -1) && app.config.cache.timeout &&
(req.get('Cache-Control') === void 0 || req.get('Cache-Control').indexOf('no-cache') === -1) (req.get('Pragma') === void 0 ||
) { req.get('Pragma').indexOf('no-cache') === -1) &&
res.set('Cache-Control', (app.config.cache.private ? 'private' : 'public') + ', max-age=' + app.config.cache.timeout + ', min-refresh=' + app.config.cache['min-refresh']); (req.get('Cache-Control') === void 0 ||
if (!app.config.cache.private) { req.get('Cache-Control').indexOf('no-cache') === -1)
res.set('Pragma', 'cache'); ) {
} res.set(
} 'Cache-Control',
(app.config.cache.private ? 'private' : 'public') +
', max-age=' +
app.config.cache.timeout +
', min-refresh=' +
app.config.cache['min-refresh']
);
if (!app.config.cache.private) {
res.set('Pragma', 'cache');
}
}
// Check for download // Check for download
if (result.filename !== void 0 && (req.query.download === '1' || req.query.download === 'true')) { if (
res.set('Content-Disposition', 'attachment; filename="' + result.filename + '"'); result.filename !== void 0 &&
} (req.query.download === '1' || req.query.download === 'true')
) {
res.set(
'Content-Disposition',
'attachment; filename="' + result.filename + '"'
);
}
// Send data // Send data
res.type(result.type).send(result.body); res.type(result.type).send(result.body);
}; };

View File

@ -7,76 +7,103 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
const util = require('util'); const util = require('util');
const promiseEach = require('./promise'); const promiseEach = require('./promise');
module.exports = app => new Promise((fulfill, reject) => { module.exports = app =>
let actions = [], new Promise((fulfill, reject) => {
logger = app.logger('Starting API...', 60), let actions = [],
start = Date.now(); logger = app.logger('Starting API...', 60),
start = Date.now();
// Check for repositories to synchronize // Check for repositories to synchronize
if (app.config.canSync) { if (app.config.canSync) {
switch (app.config['sync-on-startup']) { switch (app.config['sync-on-startup']) {
case 'always': case 'always':
case 'missing': case 'missing':
app.dirs.getRepos().forEach(repo => { app.dirs.getRepos().forEach(repo => {
if (app.sync[repo] && ( if (
app.config['sync-on-startup'] === 'always' || !app.dirs.synchronized(repo) app.sync[repo] &&
)) { (app.config['sync-on-startup'] === 'always' ||
actions.push({ !app.dirs.synchronized(repo))
action: 'sync', ) {
repo: repo actions.push({
}); action: 'sync',
} repo: repo,
}); });
break; }
} });
} break;
}
}
// Load icons // Load icons
actions.push({ actions.push({
action: 'load' action: 'load',
}); });
// Parse each promise // Parse each promise
promiseEach(actions, action => new Promise((fulfill, reject) => { promiseEach(
switch (action.action) { actions,
case 'load': action =>
// Load icons new Promise((fulfill, reject) => {
app.reload(null, { switch (action.action) {
logger: logger case 'load':
}).then(() => { // Load icons
if (!Object.keys(app.collections).length) { app.reload(null, {
reject('No collections were found.'); logger: logger,
} else { })
fulfill(); .then(() => {
} if (!Object.keys(app.collections).length) {
}).catch(err => { reject('No collections were found.');
reject('Error loading collections: ' + util.format(err)); } else {
}); fulfill();
return; }
})
.catch(err => {
reject(
'Error loading collections: ' +
util.format(err)
);
});
return;
case 'sync': case 'sync':
// Load icons // Load icons
app.sync(action.repo, { app.sync(action.repo, {
noDelay: true, noDelay: true,
reload: false, reload: false,
logger: logger logger: logger,
}).then(res => { })
fulfill(); .then(res => {
}).catch(err => { fulfill();
reject('Error synchronizing repository "' + repo + '": ' + util.format(err)); })
}); .catch(err => {
return; reject(
} 'Error synchronizing repository "' +
})).then(() => { repo +
logger.log('\nStart up process completed in ' + (Date.now() - start) / 1000 + ' seconds.'); '": ' +
fulfill(); util.format(err)
}).catch(err => { );
logger.error('\nStart up process failed!\n\n' + util.format(err)); });
reject(err); return;
}); }
}); })
)
.then(() => {
logger.log(
'\nStart up process completed in ' +
(Date.now() - start) / 1000 +
' seconds.'
);
fulfill();
})
.catch(err => {
logger.error(
'\nStart up process failed!\n\n' + util.format(err)
);
reject(err);
});
});

View File

@ -7,157 +7,210 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
"use strict"; 'use strict';
const fs = require('fs'); const fs = require('fs');
const util = require('util'); const util = require('util');
const child_process = require('child_process'); const child_process = require('child_process');
const defaultOptions = { const defaultOptions = {
logger: null, logger: null,
noDelay: false, noDelay: false,
reload: true reload: true,
}; };
let active = Object.create(null), let active = Object.create(null),
queued = Object.create(null); queued = Object.create(null);
class Sync { class Sync {
constructor(app, repo, options) { constructor(app, repo, options) {
this.app = app; this.app = app;
this.repo = repo; this.repo = repo;
this.options = options; this.options = options;
} }
sync() { sync() {
return new Promise((fulfill, reject) => { return new Promise((fulfill, reject) => {
this.app.log('Synchronizing repository "' + this.repo + '"...', this.options); this.app.log(
'Synchronizing repository "' + this.repo + '"...',
this.options
);
let time = Date.now(), let time = Date.now(),
root = this.app.dirs.storageDir(), root = this.app.dirs.storageDir(),
targetDir = root + '/' + this.repo + '.' + time, targetDir = root + '/' + this.repo + '.' + time,
repoURL = this.app.config.sync[this.repo], repoURL = this.app.config.sync[this.repo],
cmd = this.app.config.sync.git.replace('{target}', '"' + targetDir + '"').replace('{repo}', '"' + repoURL + '"'); cmd = this.app.config.sync.git
.replace('{target}', '"' + targetDir + '"')
.replace('{repo}', '"' + repoURL + '"');
child_process.exec(cmd, { child_process.exec(
cwd: root, cmd,
env: process.env, {
uid: process.getuid() cwd: root,
}, (error, stdout, stderr) => { env: process.env,
if (error) { uid: process.getuid(),
reject('Error executing git:' + util.format(error)); },
return; (error, stdout, stderr) => {
} if (error) {
reject('Error executing git:' + util.format(error));
return;
}
// Done. Set new directory and reload collections // Done. Set new directory and reload collections
this.app.dirs.setSynchronizedRepoDir(this.repo, time, true); this.app.dirs.setSynchronizedRepoDir(this.repo, time, true);
fulfill(true); fulfill(true);
}); }
}); );
} });
}
/** /**
* Synchronize repository * Synchronize repository
* *
* @param app * @param app
* @param repo * @param repo
* @param options * @param options
* @param fulfill * @param fulfill
* @param reject * @param reject
*/ */
static sync(app, repo, options, fulfill, reject) { static sync(app, repo, options, fulfill, reject) {
active[repo] = true; active[repo] = true;
queued[repo] = false; queued[repo] = false;
let sync = new Sync(app, repo, options); let sync = new Sync(app, repo, options);
sync.sync(fulfill, reject).then(() => { sync.sync(fulfill, reject)
active[repo] = false; .then(() => {
if (queued[repo]) { active[repo] = false;
// Retry if (queued[repo]) {
let retryDelay; // Retry
try { let retryDelay;
retryDelay = app.config.sync['repeated-sync-delay']; try {
} catch (err) { retryDelay = app.config.sync['repeated-sync-delay'];
retryDelay = 60; } catch (err) {
} retryDelay = 60;
app.log('Repository "' + repo + '" has finished synchronizing, but there is another sync request queued. Will do another sync in ' + retryDelay + ' seconds.', options); }
app.log(
'Repository "' +
repo +
'" has finished synchronizing, but there is another sync request queued. Will do another sync in ' +
retryDelay +
' seconds.',
options
);
setTimeout(() => { setTimeout(() => {
Sync.sync(app, repo, options, fulfill, reject); Sync.sync(app, repo, options, fulfill, reject);
}, retryDelay * 1000); }, retryDelay * 1000);
return; return;
} }
// Done // Done
app.log('Completed synchronization of repository "' + repo + '".', options); app.log(
if (options.reload && !queued[repo]) { 'Completed synchronization of repository "' + repo + '".',
app.reload(repo, options).then(() => { options
fulfill(true); );
}).catch(err => { if (options.reload && !queued[repo]) {
reject(err); app.reload(repo, options)
}); .then(() => {
} else { fulfill(true);
fulfill(true); })
} .catch(err => {
}).catch(err => { reject(err);
reject(err); });
}) } else {
} fulfill(true);
}
})
.catch(err => {
reject(err);
});
}
} }
module.exports = (app, repo, options) => new Promise((fulfill, reject) => { module.exports = (app, repo, options) =>
// Options new Promise((fulfill, reject) => {
options = Object.assign(Object.create(null), defaultOptions, typeof options !== 'object' ? options : Object.create(null)); // Options
options = Object.assign(
Object.create(null),
defaultOptions,
typeof options !== 'object' ? options : Object.create(null)
);
// Check if synchronization is disabled // Check if synchronization is disabled
if (!app.config.canSync || !app.config.sync[repo] || !app.config.sync.git) { if (
reject('Synchronization is disabled.'); !app.config.canSync ||
return; !app.config.sync[repo] ||
} !app.config.sync.git
) {
reject('Synchronization is disabled.');
return;
}
// Check if repository sync is already in queue // Check if repository sync is already in queue
if (queued[repo]) { if (queued[repo]) {
app.log('Repository "' + repo + '" is already in synchronization queue.', options); app.log(
fulfill(false); 'Repository "' +
return; repo +
} '" is already in synchronization queue.',
options
);
fulfill(false);
return;
}
let delay, retryDelay; let delay, retryDelay;
try { try {
delay = app.config.sync['sync-delay']; delay = app.config.sync['sync-delay'];
retryDelay = app.config.sync['repeated-sync-delay']; retryDelay = app.config.sync['repeated-sync-delay'];
} catch (err) { } catch (err) {
delay = 60; delay = 60;
retryDelay = 60; retryDelay = 60;
} }
if (options.noDelay) { if (options.noDelay) {
delay = 0; delay = 0;
} }
// Add to queue // Add to queue
queued[repo] = true; queued[repo] = true;
// Check if repository is already being synchronized // Check if repository is already being synchronized
if (active[repo]) { if (active[repo]) {
app.log('Repository "' + repo + '" is already being synchronized. Will do another sync ' + retryDelay + ' seconds after previous sync completes.', options); app.log(
fulfill(false); 'Repository "' +
return; repo +
} '" is already being synchronized. Will do another sync ' +
retryDelay +
' seconds after previous sync completes.',
options
);
fulfill(false);
return;
}
// Create logger if its missing // Create logger if its missing
if (!options.logger) { if (!options.logger) {
options.logger = app.logger('Synchronizing repository: ' + repo, delay + 15); options.logger = app.logger(
} 'Synchronizing repository: ' + repo,
delay + 15
// Start time );
if (!delay) { }
Sync.sync(app, repo, options, fulfill, reject);
} else {
app.log('Repository "' + repo + '" will start synchronizing in ' + delay + ' seconds.', options);
setTimeout(() => {
Sync.sync(app, repo, options, fulfill, reject);
}, delay * 1000);
}
});
// Start time
if (!delay) {
Sync.sync(app, repo, options, fulfill, reject);
} else {
app.log(
'Repository "' +
repo +
'" will start synchronizing in ' +
delay +
' seconds.',
options
);
setTimeout(() => {
Sync.sync(app, repo, options, fulfill, reject);
}, delay * 1000);
}
});

View File

@ -1,53 +1,57 @@
"use strict"; 'use strict';
(() => { (() => {
const loadJSON = require('../src/json'); const loadJSON = require('../src/json');
const fs = require('fs'), const fs = require('fs'),
chai = require('chai'), chai = require('chai'),
expect = chai.expect, expect = chai.expect,
should = chai.should(); should = chai.should();
describe('Loading JSON file', () => { describe('Loading JSON file', () => {
const filename = __dirname + '/fixtures/test1.json', const filename = __dirname + '/fixtures/test1.json',
expectedResult = JSON.parse(fs.readFileSync(filename, 'utf8')); expectedResult = JSON.parse(fs.readFileSync(filename, 'utf8'));
// Check if stream method is available // Check if stream method is available
let testStream; let testStream;
try { try {
require('JSONStream'); require('JSONStream');
require('event-stream'); require('event-stream');
testStream = true; testStream = true;
} catch (err) { } catch (err) {
testStream = false; testStream = false;
} }
// Test with each method // Test with each method
['json', 'eval', 'stream'].forEach(method => { ['json', 'eval', 'stream'].forEach(method => {
it(method, function(done) { it(method, function(done) {
if (method === 'stream' && !testStream) { if (method === 'stream' && !testStream) {
this.skip(); this.skip();
return; return;
} }
// Load file // Load file
loadJSON(method, filename).then(result => { loadJSON(method, filename)
expect(result.changed).to.be.equal(true); .then(result => {
expect(result.data).to.be.eql(expectedResult); expect(result.changed).to.be.equal(true);
expect(result.data).to.be.eql(expectedResult);
// Load file with same hash // Load file with same hash
loadJSON(method, filename, result.hash).then(result2 => { loadJSON(method, filename, result.hash)
expect(result2.changed).to.be.equal(false); .then(result2 => {
expect(result2.hash).to.be.equal(result.hash); expect(result2.changed).to.be.equal(false);
expect(result2.hash).to.be.equal(result.hash);
done(); done();
}).catch(err => { })
done(err); .catch(err => {
}); done(err);
}).catch(err => { });
done(err); })
}); .catch(err => {
}); done(err);
}); });
}); });
});
});
})(); })();

View File

@ -1,144 +1,154 @@
"use strict"; 'use strict';
(() => { (() => {
const log = require('../src/log'); const log = require('../src/log');
const chai = require('chai'), const chai = require('chai'),
expect = chai.expect, expect = chai.expect,
should = chai.should(); should = chai.should();
describe('Logging messages', () => { describe('Logging messages', () => {
it ('logging error to console and mail', done => { it('logging error to console and mail', done => {
let logged = { let logged = {
log: false, log: false,
mail: false mail: false,
}; };
let fakeApp = { let fakeApp = {
mail: message => { mail: message => {
expect(message.indexOf(expectedMessage) !== false).to.be.equal(true); expect(
logged.mail = true; message.indexOf(expectedMessage) !== false
}, ).to.be.equal(true);
logger: () => { logged.mail = true;
done('logger() should not have been called'); },
}, logger: () => {
config: { done('logger() should not have been called');
mail: { },
throttle: 0.2 config: {
} mail: {
} throttle: 0.2,
}; },
},
};
let expectedMessage = 'This is a test'; let expectedMessage = 'This is a test';
log(fakeApp, true, expectedMessage, { log(fakeApp, true, expectedMessage, {
console: { console: {
error: message => { error: message => {
expect(message.indexOf(expectedMessage) !== false).to.be.equal(true); expect(
logged.log = true; message.indexOf(expectedMessage) !== false
}, ).to.be.equal(true);
log: message => { logged.log = true;
done('console.log should not have been called'); },
} log: message => {
} done('console.log should not have been called');
}); },
},
});
setTimeout(() => { setTimeout(() => {
expect(logged).to.be.eql({ expect(logged).to.be.eql({
log: true, log: true,
mail: true mail: true,
}); });
done(); done();
}, 500); }, 500);
}); });
it ('logging message to console', done => { it('logging message to console', done => {
let logged = { let logged = {
log: false log: false,
}; };
let fakeApp = { let fakeApp = {
mail: message => { mail: message => {
done('mail() should not have been called'); done('mail() should not have been called');
}, },
logger: () => { logger: () => {
done('logger() should not have been called'); done('logger() should not have been called');
}, },
config: { config: {
mail: { mail: {
throttle: 0.2 throttle: 0.2,
} },
} },
}; };
let expectedMessage = 'This is a test'; let expectedMessage = 'This is a test';
log(fakeApp, false, expectedMessage, { log(fakeApp, false, expectedMessage, {
console: { console: {
log: message => { log: message => {
expect(message.indexOf(expectedMessage) !== false).to.be.equal(true); expect(
logged.log = true; message.indexOf(expectedMessage) !== false
}, ).to.be.equal(true);
error: message => { logged.log = true;
done('console.log should not have been called'); },
} error: message => {
} done('console.log should not have been called');
}); },
},
});
setTimeout(() => { setTimeout(() => {
expect(logged).to.be.eql({ expect(logged).to.be.eql({
log: true log: true,
}); });
done(); done();
}, 500); }, 500);
}); });
it ('logging same error only once', done => { it('logging same error only once', done => {
let logged = { let logged = {
log: false, log: false,
mail: false mail: false,
}; };
let fakeApp = { let fakeApp = {
mail: message => { mail: message => {
if (logged.mail) { if (logged.mail) {
done('mail() was called twice'); done('mail() was called twice');
} }
expect(message.indexOf(expectedMessage) !== false).to.be.equal(true); expect(
logged.mail = true; message.indexOf(expectedMessage) !== false
}, ).to.be.equal(true);
logger: () => { logged.mail = true;
done('logger() should not have been called'); },
}, logger: () => {
config: { done('logger() should not have been called');
mail: { },
throttle: 0.2 config: {
} mail: {
} throttle: 0.2,
}; },
},
};
let expectedMessage = 'This is a test', let expectedMessage = 'This is a test',
fakeConsole = { fakeConsole = {
error: message => { error: message => {
expect(message.indexOf(expectedMessage) !== false).to.be.equal(true); expect(
logged.log = true; message.indexOf(expectedMessage) !== false
}, ).to.be.equal(true);
log: message => { logged.log = true;
done('console.log should not have been called'); },
} log: message => {
}; done('console.log should not have been called');
},
};
log(fakeApp, true, expectedMessage, { log(fakeApp, true, expectedMessage, {
console: fakeConsole, console: fakeConsole,
key: 'test' key: 'test',
}); });
log(fakeApp, true, expectedMessage, { log(fakeApp, true, expectedMessage, {
console: fakeConsole, console: fakeConsole,
key: 'test' key: 'test',
}); });
setTimeout(() => { setTimeout(() => {
expect(logged).to.be.eql({ expect(logged).to.be.eql({
log: true, log: true,
mail: true mail: true,
}); });
done(); done();
}, 500); }, 500);
}); });
}); });
})(); })();

View File

@ -1,107 +1,122 @@
"use strict"; 'use strict';
(() => { (() => {
const chai = require('chai'), const chai = require('chai'),
expect = chai.expect, expect = chai.expect,
should = chai.should(); should = chai.should();
describe('Splitting query string', () => { describe('Splitting query string', () => {
it('3 part requests', () => { it('3 part requests', () => {
const exp = /^\/([a-z0-9-]+)\/([a-z0-9-]+)\.(js|json|svg)$/; const exp = /^\/([a-z0-9-]+)\/([a-z0-9-]+)\.(js|json|svg)$/;
function test(str) { function test(str) {
let result = str.match(exp); let result = str.match(exp);
if (!result) { if (!result) {
return null; return null;
} }
// Remove first parameter and named parameters that don't exist in Expression.js params // Remove first parameter and named parameters that don't exist in Expression.js params
result.shift(); result.shift();
delete result.index; delete result.index;
delete result.input; delete result.input;
return result; return result;
} }
// SVG // SVG
expect(test('/foo/bar.svg')).to.be.eql(['foo', 'bar', 'svg']); expect(test('/foo/bar.svg')).to.be.eql(['foo', 'bar', 'svg']);
expect(test('/fa-pro/test-icon.svg')).to.be.eql(['fa-pro', 'test-icon', 'svg']); expect(test('/fa-pro/test-icon.svg')).to.be.eql([
'fa-pro',
'test-icon',
'svg',
]);
// icons // icons
expect(test('/foo/icons.js')).to.be.eql(['foo', 'icons', 'js']); expect(test('/foo/icons.js')).to.be.eql(['foo', 'icons', 'js']);
expect(test('/long-prefixed-v1/icons.json')).to.be.eql(['long-prefixed-v1', 'icons', 'json']); expect(test('/long-prefixed-v1/icons.json')).to.be.eql([
'long-prefixed-v1',
'icons',
'json',
]);
// Too long // Too long
expect(test('/fa-pro/test/icon.svg')).to.be.equal(null); expect(test('/fa-pro/test/icon.svg')).to.be.equal(null);
// Upper case // Upper case
expect(test('/SomePrefix/Test.SVG')).to.be.equal(null); expect(test('/SomePrefix/Test.SVG')).to.be.equal(null);
// Invalid characters // Invalid characters
expect(test('/foo_bar/test.svg')).to.be.equal(null); expect(test('/foo_bar/test.svg')).to.be.equal(null);
}); });
it('2 part js/json requests', () => { it('2 part js/json requests', () => {
const exp = /^\/([a-z0-9-]+)\.(js|json)$/; const exp = /^\/([a-z0-9-]+)\.(js|json)$/;
function test(str) { function test(str) {
let result = str.match(exp); let result = str.match(exp);
if (!result) { if (!result) {
return null; return null;
} }
// Remove first parameter and named parameters that don't exist in Expression.js params // Remove first parameter and named parameters that don't exist in Expression.js params
result.shift(); result.shift();
delete result.index; delete result.index;
delete result.input; delete result.input;
return result; return result;
} }
// icons // icons
expect(test('/foo.js')).to.be.eql(['foo', 'js']); expect(test('/foo.js')).to.be.eql(['foo', 'js']);
expect(test('/long-prefixed-v1.json')).to.be.eql(['long-prefixed-v1', 'json']); expect(test('/long-prefixed-v1.json')).to.be.eql([
'long-prefixed-v1',
'json',
]);
// Too long // Too long
expect(test('/fa-pro/icons.js')).to.be.equal(null); expect(test('/fa-pro/icons.js')).to.be.equal(null);
// Upper case // Upper case
expect(test('/SomePrefix.JSON')).to.be.equal(null); expect(test('/SomePrefix.JSON')).to.be.equal(null);
// Invalid characters // Invalid characters
expect(test('/foo_bar.json')).to.be.equal(null); expect(test('/foo_bar.json')).to.be.equal(null);
}); });
it('2 part svg requests', () => { it('2 part svg requests', () => {
const exp = /^\/([a-z0-9:\-]+)\.svg$/; const exp = /^\/([a-z0-9:\-]+)\.svg$/;
function test(str) { function test(str) {
let result = str.match(exp); let result = str.match(exp);
if (!result) { if (!result) {
return null; return null;
} }
// Remove first parameter and named parameters that don't exist in Expression.js params // Remove first parameter and named parameters that don't exist in Expression.js params
result.shift(); result.shift();
delete result.index; delete result.index;
delete result.input; delete result.input;
return result; return result;
} }
// icons // icons
expect(test('/foo.svg')).to.be.eql(['foo']); expect(test('/foo.svg')).to.be.eql(['foo']);
expect(test('/long-prefixed-v1.svg')).to.be.eql(['long-prefixed-v1']); expect(test('/long-prefixed-v1.svg')).to.be.eql([
expect(test('/long-prefixed:icon-v1.svg')).to.be.eql(['long-prefixed:icon-v1']); 'long-prefixed-v1',
]);
expect(test('/long-prefixed:icon-v1.svg')).to.be.eql([
'long-prefixed:icon-v1',
]);
// Too long // Too long
expect(test('/fa-pro/icons.svg')).to.be.equal(null); expect(test('/fa-pro/icons.svg')).to.be.equal(null);
// Upper case // Upper case
expect(test('/SomePrefix.SVG')).to.be.equal(null); expect(test('/SomePrefix.SVG')).to.be.equal(null);
// Invalid characters // Invalid characters
expect(test('/foo_bar.svg')).to.be.equal(null); expect(test('/foo_bar.svg')).to.be.equal(null);
}); });
}); });
})(); })();

View File

@ -1,167 +1,189 @@
"use strict"; 'use strict';
(() => { (() => {
const chai = require('chai'), const chai = require('chai'),
expect = chai.expect, expect = chai.expect,
should = chai.should(); should = chai.should();
const Collection = require('@iconify/json-tools').Collection, const Collection = require('@iconify/json-tools').Collection,
request = require('../src/request-icons'); request = require('../src/request-icons');
let collection1 = new Collection('test'), let collection1 = new Collection('test'),
collection2 = new Collection('test2'); collection2 = new Collection('test2');
const parseQuery = (prefix, query, ext, params) => { const parseQuery = (prefix, query, ext, params) => {
let result = null; let result = null;
request({ request(
// fake app {
response: (req, res, data) => { // fake app
result = data; response: (req, res, data) => {
}, result = data;
collections: { },
test1: collection1, collections: {
test2: collection2 test1: collection1,
} test2: collection2,
}, { },
// fake request },
query: params {
}, {}, prefix, query, ext); // fake request
return result; query: params,
}; },
{},
prefix,
query,
ext
);
return result;
};
describe('Requests for icons and collections', () => { describe('Requests for icons and collections', () => {
before(() => { before(() => {
collection1.loadJSON({ collection1.loadJSON({
prefix: 'test', prefix: 'test',
icons: { icons: {
icon1: { icon1: {
body: '<icon1 fill="currentColor" />', body: '<icon1 fill="currentColor" />',
width: 30 width: 30,
}, },
icon2: { icon2: {
body: '<icon2 />' body: '<icon2 />',
} },
}, },
aliases: { aliases: {
alias1: { alias1: {
parent: 'icon2', parent: 'icon2',
hFlip: true hFlip: true,
} },
}, },
width: 24, width: 24,
height: 24 height: 24,
}); });
collection2.loadJSON({ collection2.loadJSON({
icons: { icons: {
'test2-icon1': { 'test2-icon1': {
body: '<icon1 fill="currentColor" />', body: '<icon1 fill="currentColor" />',
width: 30 width: 30,
}, },
'test2-icon2': { 'test2-icon2': {
body: '<icon2 />' body: '<icon2 />',
}, },
'test2-icon3': { 'test2-icon3': {
body: '<defs><foo id="bar" /></defs><bar use="url(#bar)" fill="currentColor" stroke="currentColor" />' body:
} '<defs><foo id="bar" /></defs><bar use="url(#bar)" fill="currentColor" stroke="currentColor" />',
}, },
aliases: { },
'test2-alias1': { aliases: {
parent: 'test2-icon2', 'test2-alias1': {
hFlip: true parent: 'test2-icon2',
} hFlip: true,
}, },
width: 24, },
height: 24 width: 24,
}); height: 24,
});
expect(collection1.items).to.not.be.equal(null); expect(collection1.items).to.not.be.equal(null);
expect(collection2.items).to.not.be.equal(null); expect(collection2.items).to.not.be.equal(null);
}); });
it('icons list', () => { it('icons list', () => {
// Simple query with prefix // Simple query with prefix
expect(parseQuery('test1', 'icons', 'js', { expect(
icons: 'alias1' parseQuery('test1', 'icons', 'js', {
})).to.be.eql({ icons: 'alias1',
js: true, })
defaultCallback: 'SimpleSVG._loaderCallback', ).to.be.eql({
data: { js: true,
prefix: 'test', defaultCallback: 'SimpleSVG._loaderCallback',
icons: { data: {
icon2: { body: '<icon2 />' } prefix: 'test',
}, icons: {
aliases: { icon2: { body: '<icon2 />' },
alias1: { parent: 'icon2', hFlip: true } },
}, aliases: {
width: 24, alias1: { parent: 'icon2', hFlip: true },
height: 24 },
} width: 24,
}); height: 24,
},
});
// Query collection without prefix, json // Query collection without prefix, json
expect(parseQuery('test2', 'icons', 'json', { expect(
icons: 'alias1' parseQuery('test2', 'icons', 'json', {
})).to.be.eql({ icons: 'alias1',
js: false, })
defaultCallback: 'SimpleSVG._loaderCallback', ).to.be.eql({
data: { js: false,
prefix: 'test2', defaultCallback: 'SimpleSVG._loaderCallback',
icons: { data: {
icon2: { body: '<icon2 />' } prefix: 'test2',
}, icons: {
aliases: { icon2: { body: '<icon2 />' },
alias1: { parent: 'icon2', hFlip: true } },
}, aliases: {
width: 24, alias1: { parent: 'icon2', hFlip: true },
height: 24 },
} width: 24,
}); height: 24,
},
});
// Custom callback // Custom callback
expect(parseQuery('test1', 'icons', 'js', { expect(
icons: 'icon1,icon2', parseQuery('test1', 'icons', 'js', {
callback: 'console.log' icons: 'icon1,icon2',
})).to.be.eql({ callback: 'console.log',
js: true, })
defaultCallback: 'SimpleSVG._loaderCallback', ).to.be.eql({
data: { js: true,
prefix: 'test', defaultCallback: 'SimpleSVG._loaderCallback',
icons: { data: {
icon1: { body: '<icon1 fill="currentColor" />', width: 30 }, prefix: 'test',
icon2: { body: '<icon2 />' } icons: {
}, icon1: {
width: 24, body: '<icon1 fill="currentColor" />',
height: 24 width: 30,
} },
}); icon2: { body: '<icon2 />' },
}); },
width: 24,
height: 24,
},
});
});
it('svg', () => { it('svg', () => {
// Simple icon // Simple icon
expect(parseQuery('test1', 'icon1', 'svg', { expect(parseQuery('test1', 'icon1', 'svg', {})).to.be.eql({
})).to.be.eql({ filename: 'icon1.svg',
filename: 'icon1.svg', type: 'image/svg+xml; charset=utf-8',
type: 'image/svg+xml; charset=utf-8', body:
body: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1.25em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 30 24" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);"><icon1 fill="currentColor" /></svg>' '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1.25em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 30 24" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);"><icon1 fill="currentColor" /></svg>',
}); });
// Icon with custom attributes // Icon with custom attributes
expect(parseQuery('test2', 'alias1', 'svg', { expect(
color: 'red' parseQuery('test2', 'alias1', 'svg', {
})).to.be.eql({ color: 'red',
filename: 'alias1.svg', })
type: 'image/svg+xml; charset=utf-8', ).to.be.eql({
body: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);"><g transform="translate(24 0) scale(-1 1)"><icon2 /></g></svg>' filename: 'alias1.svg',
}); type: 'image/svg+xml; charset=utf-8',
body:
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);"><g transform="translate(24 0) scale(-1 1)"><icon2 /></g></svg>',
});
// Icon with id replacement // Icon with id replacement
let result = parseQuery('test2', 'icon3', 'svg', { let result = parseQuery('test2', 'icon3', 'svg', {
color: 'red', color: 'red',
rotate: '90deg' rotate: '90deg',
}).body.replace(/IconifyId-[0-9a-f]+-[0-9a-f]+-[0-9]+/g, 'some-id'); }).body.replace(/IconifyId-[0-9a-f]+-[0-9a-f]+-[0-9]+/g, 'some-id');
expect(result).to.be.equal('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);"><g transform="rotate(90 12 12)"><defs><foo id="some-id" /></defs><bar use="url(#some-id)" fill="red" stroke="red" /></g></svg>'); expect(result).to.be.equal(
}); '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);"><g transform="rotate(90 12 12)"><defs><foo id="some-id" /></defs><bar use="url(#some-id)" fill="red" stroke="red" /></g></svg>'
}); );
});
});
})(); })();