Skip to content

Commit

Permalink
Merge pull request #23 from cachilders/dev
Browse files Browse the repository at this point in the history
Add yarn.lock support
  • Loading branch information
cachilders committed May 14, 2017
2 parents 690db71 + 5abccb0 commit 2807dea
Show file tree
Hide file tree
Showing 6 changed files with 3,516 additions and 52 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## 0.7.0 - 2017-5-13
### Added
- Yarn lock parsing
- More tests

### Changed
- Correct logical error retrieving repo URLs
- Revise dependency processing to allow for very large collections

## 0.6.3 - 2017-1-19
### Changed
- Minor bugfix
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "backpat",
"version": "0.6.3",
"version": "0.7.0",
"description": "A simple tool for high-fiving your tech stack",
"main": "dist/index.js",
"scripts": {
Expand Down
101 changes: 73 additions & 28 deletions src/helpers.js
Expand Up @@ -11,25 +11,45 @@ export const nodeDetails = {
url : 'https://nodejs.org',
version : process.versions.node,
description : 'A JavaScript runtime ✨🐢🚀✨',
downloads : 10000000 // A fake number since Node isn't downloaded on npm
downloads : 100000000 // A fake number since Node isn't downloaded on npm
}
};

export const readPackageJson = (path: string = rootDir) => {
export const readPackageJson = (path: string = rootDir, dependency) => {
if (typeof path !== 'string') {
throw new TypeError(`Function readPackageJson expected type: string but received ${ typeof path } instead`);
}
if (dependency) path = path + dependency;
return new Promise((resolve, reject) => {
readFile(path + '/package.json', (err, data) => {
readFile(`${path}/package.json`, (err, data) => {
if (err) {
resolve({name: dependency});
} else {
resolve(JSON.parse(data.toString()));
}
});
});
};

export const readYarnLock = (path: string = rootDir) => {
if (typeof path !== 'string') {
throw new TypeError(`Function readYarnLock expected type: string but received ${ typeof path } instead`);
}
return new Promise((resolve) => {
readFile(`${path}/yarn.lock`, (err, data) => {
if (err) {
reject(err);
resolve({yarnDependencies: {}});
} else {
resolve(JSON.parse(data.toString()));
const lockArray = data.toString().match(/\w.*\@.*(?=:)/g);
const yarnDeps = {
yarnDependencies: lockArray.reduce((deps, dep) => {
deps[dep.replace(/\@.*/, '')] = dep.replace(/.*[\@\^\~\=\>\<]/, '');
return deps;
}, {}),
};
resolve(yarnDeps);
}
});
})
.catch((reason) => {
throw new Error(reason);
});
};

Expand All @@ -41,6 +61,8 @@ export function instantiateDependencies(packageJson: {}) {
formatVersions(packageJson.dependencies) : null,
packageJson.devDependencies ?
formatVersions(packageJson.devDependencies): null,
packageJson.yarnDependencies ?
formatVersions(packageJson.yarnDependencies): null,
);
resolve(dependencies);
});
Expand All @@ -63,41 +85,64 @@ export function fetchDependency(dependency: string) {
if (typeof dependency !== 'string') {
throw new TypeError(`Function fetchDependency expected type: string but received ${ typeof dependency } instead`);
}
return readPackageJson(rootDir + 'node_modules/' + dependency)
return readPackageJson(rootDir + 'node_modules/', dependency)
.then(resolveDependency);
}

export function resolveDependency(dependency: { name: string, homepage: string, description: string, repository: { url: string } }) {
return new Promise((resolve) => {
resolve({
name: dependency.name,
url: dependency.homepage || dependency.repository ?
'https://' + dependency.repository.url.replace(/\w*.*\:\/\/|git@|\.git/g, '') : '',
url: dependency.homepage || (dependency.repository && dependency.repository.url ?
'https://' + dependency.repository.url.replace(/\w*.*\:\/\/|git@|\.git/g, '') : ''),
description: dependency.description
});
});
}

export function NpmConfig(dependencies: {}) {
return {
hostname: 'api.npmjs.org',
path: '/downloads/point/last-month/' + Object.keys(dependencies).join(','),
method: 'GET',
headers: {
'User-Agent': 'cachilders/backpat'
}
};
export function chopDependencies(depChunk: [], depChunks: [] = []) {
if (depChunk.length === 0) return depChunks;
if (depChunk.length < 100) {
depChunks.push(depChunk.join(','));
return depChunks;
}
depChunks.push(depChunk.slice(0, 100).join(','));
chopDependencies(depChunk.slice(100), depChunks);
return depChunks;
}

export function httpsGetPromise(opts: {}) {
return new Promise((resolve, reject) => {
https.get(opts, (res) => {
const body = [];
res.on('data', (chunk) => body.push(chunk));
res.on('end', () => resolve(JSON.parse(Buffer.concat(body).toString())));
res.on('error', reject);
export function NpmConfig(dependencies: {}) {
const deps = Object.keys(dependencies);
const depChunks = chopDependencies(deps);

return depChunks.reduce((optsArray, depChunk) => {
optsArray.push({
hostname: 'api.npmjs.org',
path: '/downloads/point/last-month/' + depChunk,
method: 'GET',
headers: {
'User-Agent': 'cachilders/backpat'
}
});
});
return optsArray;
}, []);
}

export function httpsGetPromise(optsArray: []) {
const promiseArray = optsArray.reduce((promises, opts) => {
promises.push(new Promise((resolve, reject) => {
https.get(opts, (res) => {
const body = [];
res.on('data', (chunk) => body.push(chunk));
res.on('end', () => resolve(JSON.parse(Buffer.concat(body).toString())));
res.on('error', reject);
});
}));
return promises;
}, []);
return Promise.all(promiseArray)
.then((results) => Object.assign({}, ...results))
.catch(console.error);
}

export const addNode = (dependencies: {}) => Object.assign({}, dependencies, nodeDetails);
93 changes: 71 additions & 22 deletions src/helpers.test.js
Expand Up @@ -3,13 +3,15 @@ import chaiSpies from 'chai-spies';
import {
NpmConfig,
readPackageJson,
readYarnLock,
rootDir,
instantiateDependencies,
fetchEachDependency,
fetchDependency,
resolveDependency,
addNode,
nodeDetails
nodeDetails,
chopDependencies
} from './helpers';
import { getNpmData } from './utilities';

Expand All @@ -31,7 +33,7 @@ describe('Helpers', function() {

describe('readPackageJson', () => {

it('should be a function that accepts one optional argument', () => {
it('should be a function that accepts two optional arguments', () => {
expect(readPackageJson).to.be.a('function');
expect(readPackageJson.length).to.equal(0);
});
Expand All @@ -44,11 +46,12 @@ describe('Helpers', function() {
expect(() => readPackageJson().to.be.an.instanceof(Promise));
});

it('should throw error when path does not exist', (done) => {
readPackageJson('').catch((reason) => {
expect(reason).to.be.an.instanceof(Error);
done();
it('should return a simple, name-only object when path does not exist', (done) => {
readPackageJson('/', 'abroxia').then((pkg) => {
expect(pkg).to.be.an('object');
expect(pkg).to.contain.only.keys('name');
});
done();
});

it('should return project\'s parsed package.json', (done) => {
Expand All @@ -64,8 +67,41 @@ describe('Helpers', function() {
'dependencies',
'devDependencies'
);
done();
});
done();
});

});

describe('readYarnLock', () => {

it('should be a function that accepts two optional arguments', () => {
expect(readYarnLock).to.be.a('function');
expect(readYarnLock.length).to.equal(0);
});

it('should throw TypeError when passed argument that is not a string', () => {
expect(() => readYarnLock(42)).to.throw(TypeError);
});

it('should return a promise object', () => {
expect(() => readYarnLock().to.be.an.instanceof(Promise));
});

it('should return a hollow object when path does not exist', (done) => {
readYarnLock('/', 'abroxia').then((yarnDeps) => {
expect(yarnDeps).to.be.an('object');
expect(yarnDeps).to.contain.only.keys('yarnDependencies');
});
done();
});

it('should return project\'s parsed yarn.lock', (done) => {
readYarnLock().then((lock) => {
expect(lock).to.be.an('object');
expect(lock).to.contain.only.keys('yarnDependencies');
});
done();
});

});
Expand Down Expand Up @@ -130,18 +166,31 @@ describe('Helpers', function() {
};

describe('NpmConfig', () => {
it('should be a factory that returns a config object', () => {
it('should be a factory that returns an array of config objects', () => {
expect(NpmConfig).to.be.a('function');
const obj = NpmConfig(deps);
expect(obj).to.be.an('object');
expect(obj).to.deep.equal({
const result = NpmConfig(deps);
expect(result).to.be.an('array');
expect(result).to.deep.equal([{
hostname: 'api.npmjs.org',
path: '/downloads/point/last-month/lodash,ramda',
method: 'GET',
headers: {
'User-Agent': 'cachilders/backpat'
}
});
}]);
});
});

describe('chopDependencies', () => {
it('should be a function', () => {
expect(chopDependencies).to.be.a('function');
});
it('should be a return an array of strings', () => {
const result = chopDependencies(new Array(400).fill('a'));
expect(result).to.be.an('array');
expect(result.length).to.equal(4);
expect(result[0]).to.be.a('string');
expect(result[0].match(/,/g).length).to.equal(99);
});
});

Expand All @@ -158,8 +207,8 @@ describe('Helpers', function() {
expect(result).to.be.an('object');
expect(result.lodash).to.have.any.keys('downloads');
expect(result.ramda).to.have.any.keys('downloads');
done();
});
done();
});
});

Expand Down Expand Up @@ -205,15 +254,15 @@ describe('Helpers', function() {
it('should eventually return an object', (done) => {
fetchEachDependency(deps).then((dependencies) => {
expect(dependencies).to.be.an('object');
done();
});
done();
});

it('should eventually map dependencies to objects', (done) => {
fetchEachDependency(deps).then((dependencies) => {
expect(Object.keys(dependencies)).to.have.length(2);
done();
});
done();
});

});
Expand All @@ -238,8 +287,8 @@ describe('Helpers', function() {
it('should eventually resolve to a dependency\'s package', (done) => {
fetchDependency('ramda').then((result) => {
expect(result).to.be.an('object');
done();
});
done();
});

});
Expand Down Expand Up @@ -283,43 +332,43 @@ describe('Helpers', function() {
it('should eventually return the correct output', (done) => {
resolveDependency(dependency1).then((result) => {
expect(result).to.have.all.keys('name', 'url', 'description');
done();
});
done();
});

it('should adequately process standard URL patterns', (done) => {
resolveDependency(dependency1).then((result) => {
expect(result.url).to.equal('https://lodash.com/');
done();
});
done();
});

it('should adequately process ssh URL patterns', (done) => {
resolveDependency(dependency2).then((result) => {
expect(result.url).to.equal('https://github.com/webpack/style-loader');
done();
});
done();
});

it('should adequately process minimal URL patterns', (done) => {
resolveDependency(dependency3).then((result) => {
expect(result.url).to.equal('https://shrg.biz');
done();
});
done();
});

it('should adequately process git URL patterns', (done) => {
resolveDependency(dependency4).then((result) => {
expect(result.url).to.equal('https://github.com/jtangelder/sass-loader');
done();
});
done();
});

it('should return blank URL value in absence of URL string', (done) => {
resolveDependency(dependency5).then((result) => {
expect(result.url).to.equal('');
done();
});
done();
});

});
Expand Down
4 changes: 3 additions & 1 deletion src/index.js
Expand Up @@ -9,12 +9,14 @@ import {
readPackageJson,
fetchEachDependency,
instantiateDependencies,
readYarnLock,
addNode
} from './helpers';

export function backpat() {
return new Promise((resolve) => {
readPackageJson()
Promise.all([readPackageJson(), readYarnLock()])
.then((result) => Object.assign({}, ...result))
.then(instantiateDependencies)
.then((dependencies) => {
const merge = curriedMerge(dependencies);
Expand Down

0 comments on commit 2807dea

Please sign in to comment.