Permalink
Browse files

Added project config reader

Some unit-tests
  • Loading branch information...
1 parent ed10503 commit 4c7ba7f17a5920ad4aeac10db6d7c75eaa213a17 @sergeche sergeche committed Jan 2, 2016
View
@@ -0,0 +1,67 @@
+/**
+ * A simple time-based cache storage: stores items and removes them when they
+ * expire. Unlike LRU, items in this cache will always expire, no matter how
+ * often you access them.
+ */
+'use strict';
+
+const extend = require('xtend');
+
+const defaultOptions = {
+ maxAge: 10000
+};
+
+class CacheMap extends Map {
+ constructor(options) {
+ super();
+ this.options = extend(defaultOptions, options || {});
+ this._times = new Map();
+ }
+
+ set(key, value) {
+ super.set(key, value);
+ this._times.set(key, Date.now());
+ }
+
+ get(key) {
+ removeExpired(this, this._times, this.options.maxAge);
+ return super.get(key);
+ }
+
+ delete(key) {
+ this._times.delete(key);
+ return super.delete(key);
+ }
+
+ clear() {
+ this._times.clear();
+ return super.clear();
+ }
+
+ keys() {
+ removeExpired(this, this._times, this.options.maxAge);
+ return super.keys();
+ }
+
+ values() {
+ removeExpired(this, this._times, this.options.maxAge);
+ return super.values();
+ }
+
+ entries() {
+ removeExpired(this, this._times, this.options.maxAge);
+ return super.entries();
+ }
+};
+
+module.exports = CacheMap;
+
+function removeExpired(storage, times, maxAge) {
+ var expTime = Date.now() - maxAge;
+ times.forEach((v, k) => {
+ if (v < expTime) {
+ storage.delete(k);
+ times.delete(k);
+ }
+ });
+}
View
@@ -38,4 +38,7 @@ module.exports = function(prefix) {
};
return out;
-};
+};
+
+module.exports.disable = () => enabled = false;
+module.exports.enable = () => enabled = true;
View
@@ -0,0 +1,131 @@
+/**
+ * A project config reader for LiveStyle, mostly for fetching global deps.
+ * Reads data from `livestyle.json` file found in one of the top folder of given
+ * file.
+ */
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const glob = require('glob');
+const extend = require('xtend');
+const CacheMap = require('./cache-map');
+
+const configFiles = ['livestyle.json', '.livestyle.json'];
+const configCache = new CacheMap({maxAge: 5000});
+const lookupCache = new CacheMap({maxAge: 30000});
+
+module.exports = function(file) {
+ let dir = path.dirname(file);
+ if (!lookupCache.get(dir)) {
+ lookupCache.set(dir, findConfigFile(dir));
+ }
+
+ return lookupCache.get(dir)
+ .then(configFile => {
+ if (!configCache.get(configFile)) {
+ configCache.set(configFile, readConfig(configFile));
+ }
+ return configCache.get(configFile);
+ })
+};
+
+// for unit-tesing
+module.exports._caches = () => ({configCache, lookupCache});
+
+/**
+ * Find config for given `dir`, moving upward the file structure
+ * @param {String} dir Initial dir to start searching
+ * @return {Promise}
+ */
+function findConfigFile(dir) {
+ return new Promise((resolve, reject) => {
+ let next = (d) => {
+ if (!d) {
+ return reject(error('ENOCONFIG', `No config found for ${dir} path`));
+ }
+
+ fs.readdir(d, (err, items) => {
+ if (err) {
+ return reject(err);
+ }
+
+ let found = configFiles.filter(f => items.indexOf(f) !== -1);
+ if (found.length) {
+ return resolve(path.resolve(d, found[0]));
+ }
+
+ let nextDir = path.dirname(d);
+ next(nextDir && nextDir !== d ? nextDir : null);
+ });
+ };
+
+ next(dir);
+ });
+}
+
+/**
+ * Reads given config file into a final config data
+ * @param {String} file Path to config file
+ * @return {Promise}
+ */
+function readConfig(file) {
+ return new Promise((resolve, reject) => {
+ fs.readFile(file, 'utf8', (err, content) => {
+ if (err) {
+ return reject(error(err.code, `Unable to read ${file}: ${err.message}`));
+ }
+
+ try {
+ content = JSON.parse(content) || {};
+ } catch(e) {
+ return reject(error('EINVALIDJSON', `Unable to parse ${file}: ${e.message}`));
+ }
+
+ readDeps(file, content).then(resolve, reject);
+ });
+ });
+}
+
+function readDeps(file, config) {
+ var globals = config.globals;
+ if (!config.globals) {
+ return Promise.resolve(extend(config, {globals: []}));
+ }
+
+ return globAllDeps(globals, path.dirname(file))
+ .then(globals => extend(config, {globals}));
+}
+
+function globDeps(pattern, cwd) {
+ return new Promise((resolve, reject) => {
+ glob(pattern, {cwd}, (err, items) => resolve(items || []));
+ });
+}
+
+function globAllDeps(patters, cwd) {
+ if (typeof patters === 'string') {
+ return globDeps(patters, cwd);
+ }
+
+ if (Array.isArray(patters)) {
+ return Promise.all(patters.map(p => globDeps(p, cwd)))
+ .then(values => values
+ .reduce((r, v) => r.concat(v), [])
+ .filter(unique)
+ .map(v => path.resolve(cwd, v))
+ );
+ }
+
+ return Promise.reject(new Error('Unknown pattern type for global dependencies'));
+}
+
+function error(code, message) {
+ let err = new Error(message || code);
+ err.code = code;
+ return err;
+}
+
+function unique(value, i, array) {
+ return array.indexOf(value) === i;
+}
View
@@ -5,26 +5,26 @@
const fs = require('fs');
const path = require('path');
-const LRU = require('lru-cache');
const crc32 = require('crc').crc32;
+const CacheMap = require('./cache-map');
const debug = require('./debug')('LiveStyle File Reader').disable();
-const maxAge = 10000;
-const cache = new LRU({maxAge});
+const cache = new CacheMap({maxAge: 10000});
module.exports = function(uri) {
debug('requested file', uri);
uri = path.resolve(uri);
- var f = cache.get(uri);
- if (!f) {
+ if (!cache.get(uri)) {
debug('no cached copy, fetch new');
- f = fetch(uri);
- cache.set(uri, f);
+ cache.set(uri, fetch(uri));
}
- return f.then(validate);
+ return cache.get(uri);
};
+// for unit-testing
+module.exports._cache = cache;
+
function fetch(uri) {
return read(uri).then(content => ({
uri,
@@ -38,12 +38,4 @@ function read(filePath) {
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, content) => resolve(err ? null : content));
});
-}
-
-function validate(fileObj) {
- if (fileObj.readTime + maxAge > Date.now()) {
- return fileObj;
- }
- debug(`cached copy of ${fileObj.uri} is obsolete, fetch new`);
- return fetch(fileObj.uri);
}
View
@@ -4,7 +4,7 @@
"description": "Atom plugin for Emmet LiveStyle",
"main": "index.js",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "./node_modules/.bin/mocha"
},
"keywords": [
"livestyle",
@@ -18,20 +18,23 @@
"license": "ISC",
"dependencies": {
"crc": "^3.4.0",
+ "glob": "^6.0.3",
"lru-cache": "^4.0.0",
"ws": "^1.0.0",
"xtend": "^4.0.1"
},
"config": {
"websocketUrl": "ws://127.0.0.1:54000/livestyle"
},
- "devDependencies": {},
+ "devDependencies": {
+ "mocha": "^2.3.4"
+ },
"repository": {
"type": "git",
"url": "https://github.com/livestyle/atom.git"
},
"bugs": {
"url": "https://github.com/livestyle/atom/issues"
},
- "homepage": "https://github.com/livestyle/atom"
+ "homepage": "http://livestyle.io"
}
View
@@ -0,0 +1,64 @@
+'use strict';
+
+const assert = require('assert');
+const CacheMap = require('../lib/cache-map');
+
+describe('Cache Map', () => {
+ it('create & auto-remove', done => {
+ let cache = new CacheMap({maxAge: 100});
+
+ cache.set('k1', 'v1');
+ cache.set('k2', 'v2');
+
+ assert.equal(cache.get('k1'), 'v1');
+ assert.equal(cache.get('k2'), 'v2');
+ assert.deepEqual(getKeys(cache), ['k1', 'k2']);
+
+ setTimeout(() => {
+ cache.set('k2', 'v2-2');
+ cache.set('k3', 'v3');
+ assert.deepEqual(getKeys(cache), ['k1', 'k2', 'k3']);
+ }, 50);
+
+ setTimeout(() => {
+ // `k1` should be removed here
+ assert.deepEqual(getKeys(cache), ['k2', 'k3']);
+ assert.equal(cache.get('k1'), undefined);
+ assert.equal(cache.get('k2'), 'v2-2');
+ assert.equal(cache.get('k3'), 'v3');
+ }, 110);
+
+ setTimeout(() => {
+ assert.deepEqual(getKeys(cache), []);
+ assert.equal(cache.get('k1'), undefined);
+ assert.equal(cache.get('k2'), undefined);
+ assert.equal(cache.get('k3'), undefined);
+
+ done();
+ }, 200);
+ });
+
+ it('sync times and values', () => {
+ let cache = new CacheMap({maxAge: 100});
+ cache.set('k1', 'v1');
+ cache.set('k2', 'v2');
+
+ assert.deepEqual(getKeys(cache), ['k1', 'k2']);
+
+ cache.delete('k1');
+ assert.deepEqual(getKeys(cache), ['k2']);
+ assert.deepEqual(getKeys(cache._times), ['k2']);
+
+ cache.clear();
+ assert.deepEqual(getKeys(cache), []);
+ assert.deepEqual(getKeys(cache._times), []);
+ });
+});
+
+function getKeys(map) {
+ let keys = [];
+ for (let k of map.keys()) {
+ keys.push(k);
+ }
+ return keys;
+}
@@ -0,0 +1 @@
+.foo {padding: 10px;}
No changes.
No changes.
@@ -0,0 +1,3 @@
+{
+ "globals": ["d1.scss", "deps/*.scss"]
+}
@@ -0,0 +1,3 @@
+{
+ "globals": ["d1.scss", "deps/*.scss"]
+}
No changes.
No changes.
No changes.
@@ -0,0 +1,3 @@
+{
+ globals: ["d1.scss", "deps/*.scss"]
+}
No changes.
@@ -0,0 +1,3 @@
+{
+ "globals": ["d1.scss", "deps/*.scss"]
+}
Oops, something went wrong.

0 comments on commit 4c7ba7f

Please sign in to comment.