Skip to content

Commit

Permalink
Added project config reader
Browse files Browse the repository at this point in the history
Some unit-tests
  • Loading branch information
sergeche committed Jan 2, 2016
1 parent ed10503 commit 4c7ba7f
Show file tree
Hide file tree
Showing 19 changed files with 446 additions and 20 deletions.
67 changes: 67 additions & 0 deletions lib/cache-map.js
Original file line number Original file line Diff line number Diff line change
@@ -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);
}
});
}
5 changes: 4 additions & 1 deletion lib/debug.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@ module.exports = function(prefix) {
}; };


return out; return out;
}; };

module.exports.disable = () => enabled = false;
module.exports.enable = () => enabled = true;
131 changes: 131 additions & 0 deletions lib/project-config.js
Original file line number Original file line Diff line number Diff line change
@@ -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;
}
24 changes: 8 additions & 16 deletions lib/read-file.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@


const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const LRU = require('lru-cache');
const crc32 = require('crc').crc32; const crc32 = require('crc').crc32;
const CacheMap = require('./cache-map');
const debug = require('./debug')('LiveStyle File Reader').disable(); const debug = require('./debug')('LiveStyle File Reader').disable();


const maxAge = 10000; const cache = new CacheMap({maxAge: 10000});
const cache = new LRU({maxAge});


module.exports = function(uri) { module.exports = function(uri) {
debug('requested file', uri); debug('requested file', uri);
uri = path.resolve(uri); uri = path.resolve(uri);
var f = cache.get(uri); if (!cache.get(uri)) {
if (!f) {
debug('no cached copy, fetch new'); debug('no cached copy, fetch new');
f = fetch(uri); cache.set(uri, fetch(uri));
cache.set(uri, f);
} }


return f.then(validate); return cache.get(uri);
}; };


// for unit-testing
module.exports._cache = cache;

function fetch(uri) { function fetch(uri) {
return read(uri).then(content => ({ return read(uri).then(content => ({
uri, uri,
Expand All @@ -38,12 +38,4 @@ function read(filePath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, content) => resolve(err ? null : content)); 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);
} }
9 changes: 6 additions & 3 deletions package.json
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Atom plugin for Emmet LiveStyle", "description": "Atom plugin for Emmet LiveStyle",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "./node_modules/.bin/mocha"
}, },
"keywords": [ "keywords": [
"livestyle", "livestyle",
Expand All @@ -18,20 +18,23 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"crc": "^3.4.0", "crc": "^3.4.0",
"glob": "^6.0.3",
"lru-cache": "^4.0.0", "lru-cache": "^4.0.0",
"ws": "^1.0.0", "ws": "^1.0.0",
"xtend": "^4.0.1" "xtend": "^4.0.1"
}, },
"config": { "config": {
"websocketUrl": "ws://127.0.0.1:54000/livestyle" "websocketUrl": "ws://127.0.0.1:54000/livestyle"
}, },
"devDependencies": {}, "devDependencies": {
"mocha": "^2.3.4"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/livestyle/atom.git" "url": "https://github.com/livestyle/atom.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/livestyle/atom/issues" "url": "https://github.com/livestyle/atom/issues"
}, },
"homepage": "https://github.com/livestyle/atom" "homepage": "http://livestyle.io"
} }
64 changes: 64 additions & 0 deletions test/cache-map.js
Original file line number Original file line Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions test/fixtures/conf1/d1.scss
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1 @@
.foo {padding: 10px;}
Empty file.
Empty file.
3 changes: 3 additions & 0 deletions test/fixtures/conf1/livestyle.json
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"globals": ["d1.scss", "deps/*.scss"]
}
3 changes: 3 additions & 0 deletions test/fixtures/conf2/.livestyle.json
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"globals": ["d1.scss", "deps/*.scss"]
}
Empty file added test/fixtures/conf2/d1.scss
Empty file.
Empty file.
Empty file.
3 changes: 3 additions & 0 deletions test/fixtures/conf3/livestyle.json
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,3 @@
{
globals: ["d1.scss", "deps/*.scss"]
}
Empty file added test/fixtures/conf4/d1.scss
Empty file.
3 changes: 3 additions & 0 deletions test/fixtures/conf4/livestyle.json
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"globals": ["d1.scss", "deps/*.scss"]
}
Loading

0 comments on commit 4c7ba7f

Please sign in to comment.