From 25af2b5e0b4b6502f7b6b71bc746433f80a7712e Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Wed, 10 Aug 2016 01:30:07 +0100 Subject: [PATCH 01/30] Use package root document --- src/install.js | 85 ++--------------- src/local.js | 47 ++++++++++ src/ping.js | 4 +- src/registry.js | 188 ++++++++++++++++++++++--------------- src/util.js | 19 +--- test/spec/registry.spec.js | 26 ----- 6 files changed, 168 insertions(+), 201 deletions(-) create mode 100644 src/local.js diff --git a/src/install.js b/src/install.js index e6f41a5..9c462c6 100644 --- a/src/install.js +++ b/src/install.js @@ -13,7 +13,6 @@ import {_catch} from 'rxjs/operator/catch' import {mergeMap} from 'rxjs/operator/mergeMap' import {retry} from 'rxjs/operator/retry' import {skip} from 'rxjs/operator/skip' -import {satisfies} from 'semver' import needle from 'needle' import assert from 'assert' import npa from 'npm-package-arg' @@ -22,6 +21,7 @@ import memoize from 'lodash.memoize' import * as cache from './cache' import * as config from './config' import * as registry from './registry' +import * as local from './local' import * as git from './git' import * as util from './util' import * as progress from './progress' @@ -55,60 +55,6 @@ export const DEPENDENCY_FIELDS = [ 'optionalDependencies' ] -/** - * error class used for representing an error that occurs due to a lifecycle - * script that exits with a non-zero status code. - */ -export class LocalConflictError extends Error { - /** - * create instance. - * @param {String} name - name of the dependency. - * @param {String} version - local version. - * @param {String} expected - expected version. - */ - constructor (name, version, expected) { - super(`Local version ${name}@${version} does not match required version @${expected}`) - this.name = 'LocalConflictError' - } -} - -/** - * resolve a dependency's `package.json` file from the local file system. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {String} name - name of the dependency. - * @param {String} version - version of the dependency. - * @param {Boolean} isExplicit - whether the install command asks for an explicit install. - * @return {Observable} - observable sequence of `package.json` objects. - */ -export function resolveLocal (nodeModules, parentTarget, name, version, isExplicit) { - const linkname = path.join(nodeModules, parentTarget, 'node_modules', name) - const mockFetch = () => EmptyObservable.create() - log(`resolving ${linkname} from node_modules`) - - // support `file:` with symlinks - if (version.substr(0, 5) === 'file:') { - log(`resolved ${name}@${version} as local symlink`) - const isScoped = name.charAt(0) === '@' - const src = path.join(parentTarget, isScoped ? '..' : '', version.substr(5)) - const dst = path.join('node_modules', parentTarget, 'node_modules', name) - return util.forceSymlink(src, dst)::_finally(progress.complete) - } - - return util.readlink(linkname)::mergeMap((rel) => { - const target = path.basename(path.dirname(rel)) - const filename = path.join(linkname, 'package.json') - log(`reading package.json from ${filename}`) - - return util.readFileJSON(filename)::map((pkgJson) => { - if (isExplicit && !satisfies(pkgJson.version, version)) { - throw new LocalConflictError(name, pkgJson.version, version) - } - return {parentTarget, pkgJson, target, name, fetch: mockFetch} - }) - }) -} - /** * resolve a dependency's `package.json` file from a remote registry. * @param {String} nodeModules - `node_modules` base directory. @@ -127,7 +73,10 @@ export function resolveRemote (nodeModules, parentTarget, name, version) { case 'range': case 'version': case 'tag': - return resolveFromNpm(nodeModules, parentTarget, parsedSpec) + return registry.resolve(nodeModules, parentTarget, name, version, { + ...config.httpOptions, + registry: config.registry + }) case 'remote': return resolveFromTarball(nodeModules, parentTarget, parsedSpec) case 'hosted': @@ -139,24 +88,6 @@ export function resolveRemote (nodeModules, parentTarget, name, version) { } } -/** - * resolve a dependency's `package.json` file from the npm registry. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {Object} parsedSpec - parsed package name and specifier. - * @return {Observable} - observable sequence of `package.json` objects. - */ -export function resolveFromNpm (nodeModules, parentTarget, parsedSpec) { - const {raw, name, type, spec} = parsedSpec - log(`resolving ${raw} from npm`) - const options = {...config.httpOptions, retries: config.retries} - return registry.match(name, spec, options)::map((pkgJson) => { - const target = pkgJson.dist.shasum - log(`resolved ${raw} to tarball shasum ${target} from npm`) - return {parentTarget, pkgJson, target, name, type, fetch} - }) -} - /** * resolve a dependency's `package.json` file from an url tarball. * @param {String} nodeModules - `node_modules` base directory. @@ -211,7 +142,7 @@ export function resolveFromHosted (nodeModules, parentTarget, parsedSpec) { throw new Error(`Unknown hosted type: ${hosted.type} for ${name}`) } - return registry.fetch(hosted.directUrl, options) + return registry.getJson(hosted.directUrl, options) ::map(({body}) => JSON.parse(body)) ::map(pkgJson => { pkgJson.dist = {tarball, shasum} // eslint-disable-line no-param-reassign @@ -269,7 +200,7 @@ export function resolve (nodeModules, parentTarget, isExplicit) { progress.report(`resolving ${name}@${version}`) log(`resolving ${name}@${version}`) - return resolveLocal(nodeModules, parentTarget, name, version, isExplicit) + return local.resolve(nodeModules, parentTarget, name, version, isExplicit) ::_catch((error) => { if (error.name !== 'LocalConflictError' && error.code !== 'ENOENT') { throw error @@ -406,7 +337,7 @@ function fixPermissions (target, bin) { ::mergeMap((filepath) => util.chmod(filepath, execMode)) } -function fetch (nodeModules) { +export function fetch (nodeModules) { const {target, type, pkgJson: {name, bin, dist: {tarball, shasum}}} = this const where = path.join(nodeModules, target, 'package') diff --git a/src/local.js b/src/local.js new file mode 100644 index 0000000..d504ae3 --- /dev/null +++ b/src/local.js @@ -0,0 +1,47 @@ +import path from 'path' +import {EmptyObservable} from 'rxjs/observable/EmptyObservable' +import {_finally} from 'rxjs/operator/finally' +import {map} from 'rxjs/operator/map' +import {mergeMap} from 'rxjs/operator/mergeMap' +import {satisfies} from 'semver' +import {inherits} from 'util' + +import * as util from './util' +import * as progress from './progress' + +// thrown when the currently installed version does not satisfy the semantic +// version constraint. +inherits(LocalConflictError, Error) +function LocalConflictError (name, version, expected) { + Error.captureStackTrace(this, this.constructor) + this.name = 'LocalConflictError' + this.message = `Local version ${name}@${version} does not match required\\ +version @${expected}` + this.extra = {name, version, expected} +} + +export const fetch = () => EmptyObservable.create() + +export const resolve = (nodeModules, parentTarget, name, version, isExplicit) => { + const linkname = path.join(nodeModules, parentTarget, 'node_modules', name) + + // support `file:` with symlinks + if (version.substr(0, 5) === 'file:') { + const isScoped = name.charAt(0) === '@' + const src = path.join(parentTarget, isScoped ? '..' : '', version.substr(5)) + const dst = path.join('node_modules', parentTarget, 'node_modules', name) + return util.forceSymlink(src, dst)::_finally(progress.complete) + } + + return util.readlink(linkname)::mergeMap((rel) => { + const target = path.basename(path.dirname(rel)) + const filename = path.join(linkname, 'package.json') + + return util.readFileJSON(filename)::map((pkgJson) => { + if (isExplicit && !satisfies(pkgJson.version, version)) { + throw new LocalConflictError(name, pkgJson.version, version) + } + return {parentTarget, pkgJson, target, name, fetch} + }) + }) +} diff --git a/src/ping.js b/src/ping.js index c3b5f9e..92363b8 100644 --- a/src/ping.js +++ b/src/ping.js @@ -1,6 +1,6 @@ import url from 'url' import {registry} from './config' -import {httpGetJSON} from './util' +import {httpGet} from './util' /** * ping the pre-configured npm registry by hitting `/-/ping?write=true`. @@ -8,5 +8,5 @@ import {httpGetJSON} from './util' */ export function ping () { const uri = url.resolve(registry, '-/ping?write=true') - return httpGetJSON(uri) + return httpGet(uri) } diff --git a/src/registry.js b/src/registry.js index a7d6a4c..f3039ed 100644 --- a/src/registry.js +++ b/src/registry.js @@ -1,99 +1,131 @@ import url from 'url' import {map} from 'rxjs/operator/map' import {_do} from 'rxjs/operator/do' -import {retry} from 'rxjs/operator/retry' import {publishReplay} from 'rxjs/operator/publishReplay' import {httpGet} from './util' -import assert from 'assert' - -/** - * default registry URL to be used. can be overridden via options on relevant - * functions. - * @type {String} - */ -export const DEFAULT_REGISTRY = 'https://registry.npmjs.org/' - -/** - * default number of retries to attempt before failing to resolve to a package - * @type {Number} - */ -export const DEFAULT_RETRIES = 5 - -/** - * register of pending and completed HTTP requests mapped to their respective - * observable sequences. - * @type {Object} - */ +import {maxSatisfying} from 'semver' +import {inherits} from 'util' +import {fetch} from './install' + +// default registry to be used. +export const REGISTRY = 'https://registry.npmjs.org/' + +// register of pending and completed HTTP requests mapped to their respective +// observable sequences. export const requests = Object.create(null) -/** - * clear the internal cache used for pending and completed HTTP requests. - */ -export function reset () { +// clear the internal cache used for pending and completed HTTP requests. +export const reset = () => { const uris = Object.keys(requests) - for (const uri of uris) { - delete requests[uri] - } + uris.forEach(uri => delete requests[uri]) } -/** - * ensure that the registry responded with an accepted HTTP status code - * (`200`). - * @param {String} uri - URI used for retrieving the supplied response. - * @param {Number} resp - HTTP response object. - * @throws {assert.AssertionError} if the status code is not 200. - */ -export function checkStatus (uri, resp) { - const {statusCode, body: {error}} = resp - assert.equal(statusCode, 200, `error status code ${uri}: ${error}`) -} +// scoped npm modules, such as @alexanderGugel/some-package, are "@"-prefixed. +const isScoped = name => + name.charAt(0) === '@' -/** - * escape the given package name, which can then be used as part of the package - * root URL. - * @param {String} name - package name. - * @return {String} - escaped package name. - */ -export function escapeName (name) { - const isScoped = name.charAt(0) === '@' - const escapedName = isScoped +// escape the given package name, which can then be used as part of the package +// root URL. +const escapeName = name => ( + isScoped(name) ? `@${encodeURIComponent(name.substr(1))}` : encodeURIComponent(name) - return escapedName -} +) -/** - * HTTP GET the resource at the supplied URI. if a request to the same URI has - * already been made, return the cached (pending) request. - * @param {String} uri - endpoint to fetch data from. - * @param {Object} [options = {}] - optional HTTP and `retries` options. - * @return {Observable} - observable sequence of pending / completed request. - */ -export function fetch (uri, options = {}) { - const {retries = DEFAULT_RETRIES, ...needleOptions} = options +// HTTP GET the resource at the supplied URI. if a request to the same URI has +// already been made, return the cached (pending) request. +export const getJson = (uri, options = {}) => { const existingRequest = requests[uri] + if (existingRequest) return existingRequest - if (existingRequest) { - return existingRequest - } - const newRequest = httpGet(uri, needleOptions) - ::_do((resp) => checkStatus(uri, resp)) - ::retry(retries)::publishReplay().refCount() + // if there is no pending / fulfilled request to this URI, dispatch a + // new request. + const newRequest = httpGet(uri, options) + ::publishReplay().refCount() requests[uri] = newRequest return newRequest } -/** - * resolve a package defined via an ambiguous semantic version string to a - * specific `package.json` file. - * @param {String} name - package name. - * @param {String} version - semantic version string or tag name. - * @param {Object} options - HTTP request options. - * @return {Observable} - observable sequence of the `package.json` file. - */ -export function match (name, version, options = {}) { - const escapedName = escapeName(name) - const {registry = DEFAULT_REGISTRY, ...fetchOptions} = options - const uri = url.resolve(registry, `${escapedName}/${version}`) - return fetch(uri, fetchOptions)::map(({body}) => body) +// The package root url is the base URL where a client can get top-level +// information about a package and all of the versions known to the registry. +// A valid “package root url” response MUST be returned when the client requests +// {registry root url}/{package name}. +// Ideally we would use the package version url, but npm's caching policy seems +// a bit buggy in that regard. +// Source: http://wiki.commonjs.org/wiki/Packages/Registry#package_root_url +export const getPackageRootUrl = (registry, name) => + url.resolve(registry, escapeName(name)) + +// find a matching version or tag given the registry response. +// versions: An object hash of version identifiers to valid “package version +// url” responses: either URL strings or package descriptor objects. +// Source: http://wiki.commonjs.org/wiki/Packages/Registry#Package_Root_Object +export const findVersion = (name, versionOrTag) => body => { + const versionsKeys = Object.keys(body.versions) + const versionKey = body['dist-tags'][versionOrTag] + || maxSatisfying(versionsKeys, versionOrTag) + const version = versionKey && body.versions[versionKey] + if (!version) { + const available = versionsKeys.concat(Object.keys(body['dist-tags'])) + throw new NoMatchingVersionError(name, version, available) + } + return version +} + +// thrown when the package exists, but no matching version is available. +inherits(NoMatchingVersionError, Error) +export function NoMatchingVersionError (name, version, available) { + Error.captureStackTrace(this, this.constructor) + this.name = 'NoMatchingVersionError' + this.message = `no matching version for ${name}@${version} +available: ${available.join(',') || '[none]'}` + this.extra = {name, version, available} } + +// thrown when the package itself (= name) does not exist. +inherits(NoMatchingNameError, Error) +export function NoMatchingNameError (name, version) { + Error.captureStackTrace(this, this.constructor) + this.name = 'NoMatchingNameError' + this.message = `no matching name for ${name}@${version}` + this.extra = {name, version} +} + +// thrown when registry responded with an unexpected status code, such as a 500 +// indicating an internal registry error. +inherits(StatusCodeError, Error) +export function StatusCodeError (name, version, statusCode, error) { + Error.captureStackTrace(this, this.constructor) + this.name = 'StatusCodeError' + const message = `unexpected status code ${statusCode} for ${name}@${version}` + this.message = error != null ? `${message}: ${error}` : message + this.extra = {name, version, statusCode, error} +} + +// ensure that the registry responded with an accepted HTTP status code (`200`). +export const checkStatus = (name, version) => ({statusCode, body: {error}}) => { + switch (statusCode) { + case 200: break + case 404: throw new NoMatchingNameError(name, version) + default: throw new StatusCodeError(name, version, statusCode, error) + } +} + +const extractBody = ({body}) => body + +// resolve a package defined via an ambiguous semantic version string to a +// specific `package.json` file. +export const match = (name, version, {registry = REGISTRY, ...options} = {}) => + getJson(getPackageRootUrl(registry, name), options) + ::_do(checkStatus(name, version)) + ::map(extractBody) + ::map(findVersion(name, version)) + +export const resolve = (nodeModules, parentTarget, name, version, options) => + match(name, version, options)::map(pkgJson => ({ + parentTarget, + pkgJson, + target: pkgJson.dist.shasum, + name, + fetch + })) diff --git a/src/util.js b/src/util.js index 41bb78f..94259a2 100644 --- a/src/util.js +++ b/src/util.js @@ -36,7 +36,7 @@ export function createObservableFactory (fn, thisArg) { * @return {Observable} - observable sequence of a single response object. */ export function httpGet (...args) { - return Observable.create((observer) => { + return Observable.create(observer => { needle.get(...args, (error, response) => { if (error) observer.error(error) else { @@ -47,23 +47,6 @@ export function httpGet (...args) { }) } -/** - * GETs JSON documents from an HTTP endpoint. - * @param {String} url - endpoint to which the GET request should be made - * @return {Object} An observable sequence of the fetched JSON document. - */ -export function httpGetJSON (url) { - return Observable.create((observer) => { - needle.get(url, config.httpOptions, (error, response) => { - if (error) observer.error(error) - else { - observer.next(response.body) - observer.complete() - } - }) - }) -} - /** @type {Function} Observable wrapper function around `fs.readFile`. */ export const readFile = createObservableFactory(fs.readFile, fs) diff --git a/test/spec/registry.spec.js b/test/spec/registry.spec.js index c9878da..4a13e8b 100644 --- a/test/spec/registry.spec.js +++ b/test/spec/registry.spec.js @@ -10,20 +10,6 @@ describe('registry', () => { afterEach(() => sandbox.restore()) afterEach(() => registry.reset()) - describe('DEFAULT_REGISTRY', () => { - it('should be HTTPS URL', () => { - assert.equal(typeof registry.DEFAULT_REGISTRY, 'string') - assert.equal(url.parse(registry.DEFAULT_REGISTRY).protocol, 'https:') - }) - }) - - describe('DEFAULT_RETRIES', () => { - it('should be a number', () => { - assert.equal(typeof registry.DEFAULT_RETRIES, 'number') - assert(registry.DEFAULT_RETRIES >= 0) - }) - }) - describe('escapeName', () => { context('when name is scoped', () => { it('should preserve "@"', () => { @@ -74,16 +60,4 @@ describe('registry', () => { }) }) }) - - describe('fetch', () => { - context('when request has already been made', () => { - it('should return pending request', () => { - const uri = 'https://example.com/example' - const pendingRequest = EmptyObservable.create() - registry.requests[uri] = pendingRequest - const request = registry.fetch(uri) - assert.equal(request, pendingRequest) - }) - }) - }) }) From b600e81718995f2741b6ae76565b84c9ec77d45e Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Sat, 13 Aug 2016 17:42:33 +0100 Subject: [PATCH 02/30] Flatten local resolve --- src/local.js | 59 +++++++++++++++++++++++++++++++--------------------- src/util.js | 8 +++---- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/local.js b/src/local.js index d504ae3..73e296c 100644 --- a/src/local.js +++ b/src/local.js @@ -1,13 +1,12 @@ import path from 'path' import {EmptyObservable} from 'rxjs/observable/EmptyObservable' -import {_finally} from 'rxjs/operator/finally' +import {_do} from 'rxjs/operator/do' import {map} from 'rxjs/operator/map' import {mergeMap} from 'rxjs/operator/mergeMap' import {satisfies} from 'semver' import {inherits} from 'util' import * as util from './util' -import * as progress from './progress' // thrown when the currently installed version does not satisfy the semantic // version constraint. @@ -15,33 +14,45 @@ inherits(LocalConflictError, Error) function LocalConflictError (name, version, expected) { Error.captureStackTrace(this, this.constructor) this.name = 'LocalConflictError' - this.message = `Local version ${name}@${version} does not match required\\ + this.message = `Local version ${name}@${version} does not match required\ version @${expected}` this.extra = {name, version, expected} } -export const fetch = () => EmptyObservable.create() +export const fetch = () => + EmptyObservable.create() -export const resolve = (nodeModules, parentTarget, name, version, isExplicit) => { - const linkname = path.join(nodeModules, parentTarget, 'node_modules', name) - - // support `file:` with symlinks - if (version.substr(0, 5) === 'file:') { - const isScoped = name.charAt(0) === '@' - const src = path.join(parentTarget, isScoped ? '..' : '', version.substr(5)) - const dst = path.join('node_modules', parentTarget, 'node_modules', name) - return util.forceSymlink(src, dst)::_finally(progress.complete) +// TODO should not happen in resolve +// support `file:` with symlinks +// if (version.substr(0, 5) === 'file:') { +// const isScoped = name.charAt(0) === '@' +// const src = path.join(parentTarget, isScoped ? '..' : '', version.substr(5)) +// const dst = path.join('node_modules', parentTarget, 'node_modules', name) +// return util.forceSymlink(src, dst)::_finally(progress.complete) +// } + +const getLinkname = (nodeModules, parentTarget, name) => + path.join(nodeModules, parentTarget, 'node_modules', name) + +const getTarget = dst => + path.basename(path.dirname(dst)) + +const checkConflict = (name, version) => ({pkgJson}) => { + if (!satisfies(pkgJson.version, version)) { + throw new LocalConflictError(name, pkgJson.version, version) } +} + +export const resolve = (nodeModules, parentTarget, name, version, isExplicit) => { + const linkname = getLinkname(nodeModules, parentTarget, name) + const filename = path.join(linkname, 'package.json') - return util.readlink(linkname)::mergeMap((rel) => { - const target = path.basename(path.dirname(rel)) - const filename = path.join(linkname, 'package.json') - - return util.readFileJSON(filename)::map((pkgJson) => { - if (isExplicit && !satisfies(pkgJson.version, version)) { - throw new LocalConflictError(name, pkgJson.version, version) - } - return {parentTarget, pkgJson, target, name, fetch} - }) - }) + return util.readlink(linkname) + ::map(getTarget) + ::mergeMap(target => + util.readFile(filename, 'utf8') + ::map(JSON.parse) + ::map(pkgJson => ({parentTarget, pkgJson, target, name, fetch})) + ) + ::_do(isExplicit ? checkConflict(name, version) : Function.prototype) } diff --git a/src/util.js b/src/util.js index 94259a2..06165dc 100644 --- a/src/util.js +++ b/src/util.js @@ -5,7 +5,6 @@ import _forceSymlink from 'force-symlink' import needle from 'needle' import {map} from 'rxjs/operator/map' import {mergeMap} from 'rxjs/operator/mergeMap' -import * as config from './config' /** * given an arbitrary asynchronous function that accepts a callback function, @@ -16,9 +15,9 @@ import * as config from './config' * @param {thisArg} [thisArg] - optional context. * @return {Function} - cold observable sequence factory. */ -export function createObservableFactory (fn, thisArg) { - return (...args) => - Observable.create((observer) => { +export const createObservableFactory = (fn, thisArg) => + (...args) => + Observable.create(observer => { fn.apply(thisArg, [...args, (error, ...results) => { if (error) { observer.error(error) @@ -28,7 +27,6 @@ export function createObservableFactory (fn, thisArg) { } }]) }) -} /** * send a GET request to the given HTTP endpoint by passing the supplied From 8356982855c372cf9859351ae1db4f0d6539f0f6 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Mon, 22 Aug 2016 01:49:44 +0100 Subject: [PATCH 03/30] WIP --- src/build.js | 9 +++---- src/install.js | 64 ++++++++++++++++++++++------------------------ src/install_cmd.js | 23 +++++++++++------ src/local.js | 49 ++++++++++++++++++++--------------- 4 files changed, 78 insertions(+), 67 deletions(-) diff --git a/src/build.js b/src/build.js index 8ee77b7..c80cc33 100644 --- a/src/build.js +++ b/src/build.js @@ -49,7 +49,7 @@ export class FailedBuildError extends Error { * @param {String} dep.script - script to be executed (usually using `sh`). * @return {Observable} - observable sequence of the returned exit code. */ -export function build (nodeModules, dep) { +export const build = nodeModules => dep => { const {target, script} = dep log(`executing "${script}" from ${target}`) @@ -103,12 +103,11 @@ export function parseLifecycleScripts ({target, pkgJson: {scripts = {}}}) { * @return {Observable} - empty observable sequence that will be completed once * all lifecycle scripts have been executed. */ -export function buildAll (nodeModules) { - return this +export const buildAll = nodeModules => o => + o ::map(parseLifecycleScripts) ::mergeMap((scripts) => ArrayObservable.create(scripts)) - ::concatMap((script) => build(nodeModules, script)) + ::concatMap(build(nodeModules)) ::every((code) => code === 0) ::filter((ok) => !ok) ::_do(() => { throw new FailedBuildError() }) -} diff --git a/src/install.js b/src/install.js index 9c462c6..415dc39 100644 --- a/src/install.js +++ b/src/install.js @@ -5,7 +5,9 @@ import {ArrayObservable} from 'rxjs/observable/ArrayObservable' import {EmptyObservable} from 'rxjs/observable/EmptyObservable' import {Observable} from 'rxjs/Observable' import {_finally} from 'rxjs/operator/finally' +import {_do} from 'rxjs/operator/do' import {concatStatic} from 'rxjs/operator/concat' +import {first} from 'rxjs/operator/first' import {distinctKey} from 'rxjs/operator/distinctKey' import {expand} from 'rxjs/operator/expand' import {map} from 'rxjs/operator/map' @@ -55,6 +57,18 @@ export const DEPENDENCY_FIELDS = [ 'optionalDependencies' ] +export const strategies = [ + local, + registry +] + +export const findStrategy = (name, version) => + concatStatic( + local.resolve(name, version), + registry.resolve(name, version) + ) + ::first() + /** * resolve a dependency's `package.json` file from a remote registry. * @param {String} nodeModules - `node_modules` base directory. @@ -63,6 +77,7 @@ export const DEPENDENCY_FIELDS = [ * @param {String} version - version of the dependency. * @return {Observable} - observable sequence of `package.json` objects. */ + export function resolveRemote (nodeModules, parentTarget, name, version) { const source = `${name}@${version}` log(`resolving ${source} from remote registry`) @@ -219,27 +234,18 @@ export function resolve (nodeModules, parentTarget, isExplicit) { * @param {Boolean} isExplicit - whether the install command asks for an explicit install. * @return {Observable} - an observable sequence of resolved dependencies. */ -export function resolveAll (nodeModules, targets = Object.create(null), isExplicit) { - return this::expand(({target, pkgJson, isProd = false}) => { - // cancel when we get into a circular dependency - if (target in targets) { - log(`aborting due to circular dependency ${target}`) +export function resolveAll (nodeModules) { + const targets = Object.create(null) + + return this::expand(result => { + if (targets[result.target]) { return EmptyObservable.create() } - - targets[target] = true // eslint-disable-line no-param-reassign - - // install devDependencies of entry dependency (project-level) - const fields = (target === '..' && !isProd) - ? ENTRY_DEPENDENCY_FIELDS - : DEPENDENCY_FIELDS - - log(`extracting ${fields} from ${target}`) - - const dependencies = parseDependencies(pkgJson, fields) - - return ArrayObservable.create(dependencies) - ::resolve(nodeModules, target, isExplicit) + targets[result.target] = true + const isEntry = result.target === '..' && !result.isProd + const fields = isEntry ? ENTRY_DEPENDENCY_FIELDS : DEPENDENCY_FIELDS + return ArrayObservable.create(parseDependencies(result.pkgJson, fields)) + ::resolve(nodeModules, result.target, result.isExplicit) }) } @@ -269,25 +275,16 @@ function getDirectLink (dep) { return [src, dst] } -/** - * symlink the intermediate results of the underlying observable sequence - * @return {Observable} - empty observable sequence that will be completed - * once all dependencies have been symlinked. - */ export function linkAll () { return this ::mergeMap((dep) => [getDirectLink(dep), ...getBinLinks(dep)]) ::map(([src, dst]) => resolveSymlink(src, dst)) - ::mergeMap(([src, dst]) => { - log(`symlinking ${src} -> ${dst}`) - return util.forceSymlink(src, dst) - }) + ::mergeMap(([src, dst]) => util.forceSymlink(src, dst)) } -function checkShasum (shasum, expected, tarball) { - assert.equal(shasum, expected, +export const checkShasum = (shasum, expected, tarball) => + void assert.equal(shasum, expected, `shasum mismatch for ${tarball}: ${shasum} <-> ${expected}`) -} function download (tarball, expected, type) { log(`downloading ${tarball}, expecting ${expected}`) @@ -362,6 +359,7 @@ export function fetch (nodeModules) { } export function fetchAll (nodeModules) { - const fetchWithRetry = (dep) => dep.fetch(nodeModules)::retry(config.retries) - return this::distinctKey('target')::mergeMap(fetchWithRetry) + return this + ::distinctKey('target') + ::map(dep => dep.fetch(nodeModules)) } diff --git a/src/install_cmd.js b/src/install_cmd.js index 93aef9f..de62e23 100644 --- a/src/install_cmd.js +++ b/src/install_cmd.js @@ -27,22 +27,29 @@ export default function installCmd (cwd, argv) { const nodeModules = path.join(cwd, 'node_modules') const resolvedAll = updatedPkgJSONs - ::map((pkgJson) => ({pkgJson, target: '..', isProd})) - ::resolveAll(nodeModules, undefined, isExplicit)::skip(1) + ::map((pkgJson) => ({pkgJson, target: '..', isProd, isExplicit})) + ::resolveAll(nodeModules)::skip(1) ::publishReplay().refCount() const initialized = initCache()::ignoreElements() - const linkedAll = resolvedAll::linkAll() - const fetchedAll = resolvedAll::fetchAll(nodeModules) - const installedAll = mergeStatic(linkedAll, fetchedAll) + const installedAll = mergeStatic( + resolvedAll::linkAll(), + resolvedAll::fetchAll(nodeModules) + ) const builtAll = argv.build - ? resolvedAll::buildAll(nodeModules) + ? resolvedAll.lift(buildAll(nodeModules)) : EmptyObservable.create() - const saved = (argv.save || argv['save-dev'] || argv['save-optional']) + const shouldSave = argv.save || argv['save-dev'] || argv['save-optional'] + const saved = shouldSave ? updatedPkgJSONs::save(cwd) : EmptyObservable.create() - return concatStatic(initialized, installedAll, saved, builtAll) + return concatStatic( + initialized, + installedAll, + saved, + builtAll + ) } diff --git a/src/local.js b/src/local.js index 73e296c..695bd4f 100644 --- a/src/local.js +++ b/src/local.js @@ -1,10 +1,12 @@ -import path from 'path' import {EmptyObservable} from 'rxjs/observable/EmptyObservable' import {_do} from 'rxjs/operator/do' import {map} from 'rxjs/operator/map' -import {mergeMap} from 'rxjs/operator/mergeMap' -import {satisfies} from 'semver' +import {mergeStatic} from 'rxjs/operator/merge' +import {reduce} from 'rxjs/operator/reduce' + +import path from 'path' import {inherits} from 'util' +import {satisfies} from 'semver' import * as util from './util' @@ -22,20 +24,12 @@ version @${expected}` export const fetch = () => EmptyObservable.create() -// TODO should not happen in resolve -// support `file:` with symlinks -// if (version.substr(0, 5) === 'file:') { -// const isScoped = name.charAt(0) === '@' -// const src = path.join(parentTarget, isScoped ? '..' : '', version.substr(5)) -// const dst = path.join('node_modules', parentTarget, 'node_modules', name) -// return util.forceSymlink(src, dst)::_finally(progress.complete) -// } - const getLinkname = (nodeModules, parentTarget, name) => path.join(nodeModules, parentTarget, 'node_modules', name) -const getTarget = dst => - path.basename(path.dirname(dst)) +const getTarget = dst => ({ + target: path.basename(path.dirname(dst)) +}) const checkConflict = (name, version) => ({pkgJson}) => { if (!satisfies(pkgJson.version, version)) { @@ -43,16 +37,29 @@ const checkConflict = (name, version) => ({pkgJson}) => { } } +const readTarget = linkname => + util.readlink(linkname)::map(getTarget) + +const readPkgJson = filename => + util.readFile(filename, 'utf8') + ::map(JSON.parse) + ::map(pkgJson => ({pkgJson})) + +const acc = (_, x) => + ({..._, ...x}) + export const resolve = (nodeModules, parentTarget, name, version, isExplicit) => { const linkname = getLinkname(nodeModules, parentTarget, name) const filename = path.join(linkname, 'package.json') - return util.readlink(linkname) - ::map(getTarget) - ::mergeMap(target => - util.readFile(filename, 'utf8') - ::map(JSON.parse) - ::map(pkgJson => ({parentTarget, pkgJson, target, name, fetch})) + return mergeStatic( + readTarget(linkname), + readPkgJson(filename) + ) + ::reduce(acc, {parentTarget, name, fetch}) + ::_do( + isExplicit + ? checkConflict(name, version) + : Function.prototype ) - ::_do(isExplicit ? checkConflict(name, version) : Function.prototype) } From 90a7979ed4bb36b6f160f6e96f904baa02686d04 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Mon, 5 Sep 2016 19:03:03 +0100 Subject: [PATCH 04/30] WIP --- src/_local.js | 22 ++++++++++++ src/cache.js | 24 ++++++------- src/install.js | 44 +++++++---------------- src/install_cmd.js | 73 ++++++++++++++++++-------------------- src/local.js | 78 ++++++++++++++++------------------------- src:_install_cmd.js | 21 +++++++++++ test/spec/local.spec.js | 6 ++++ 7 files changed, 135 insertions(+), 133 deletions(-) create mode 100644 src/_local.js create mode 100644 src:_install_cmd.js create mode 100644 test/spec/local.spec.js diff --git a/src/_local.js b/src/_local.js new file mode 100644 index 0000000..817ad92 --- /dev/null +++ b/src/_local.js @@ -0,0 +1,22 @@ +// import {inherits} from 'util' +// thrown when the currently installed version does not satisfy the semantic +// version constraint. +// inherits(LocalConflictError, Error) +// function LocalConflictError (name, version, expected) { +// Error.captureStackTrace(this, this.constructor) +// this.name = 'LocalConflictError' +// this.message = `Local version ${name}@${version} does not match required\ +// version @${expected}` +// this.extra = {name, version, expected} +// } + +// import {_do} from 'rxjs/operator/do' +// import {satisfies} from 'semver' +// const checkConflict = (name, version) => ({pkgJson}) => { +// if (!satisfies(pkgJson.version, version)) { +// throw new LocalConflictError(name, pkgJson.version, version) +// } +// } +// ::_do(isExplicit ? checkConflict(name, version) : Function.prototype) + +// ::_do(({pkgJson}) => console.log(`${pkgJson.name}@${pkgJson.version}`)) diff --git a/src/cache.js b/src/cache.js index 97a59b2..ac1f850 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,6 +1,7 @@ import {Observable} from 'rxjs/Observable' import {mergeMap} from 'rxjs/operator/mergeMap' import {retryWhen} from 'rxjs/operator/retryWhen' +import {ignoreElements} from 'rxjs/operator/ignoreElements' import gunzip from 'gunzip-maybe' import tar from 'tar-fs' import fs from 'fs' @@ -14,37 +15,32 @@ import * as config from './config' * @return {Observable} - observable sequence that will be completed once the * base directory of the cache has been created. */ -export function init () { - return util.mkdirp(path.join(config.cacheDir, '.tmp')) -} +export const init = () => + util.mkdirp(path.join(config.cacheDir, '.tmp')) + ::ignoreElements() /** * get a random temporary filename. * @return {String} - temporary filename. */ -export function getTmp () { - const filename = path.join(config.cacheDir, '.tmp', uuid.v4()) - return filename -} +export const getTmp = () => + path.join(config.cacheDir, '.tmp', uuid.v4()) /** * open a write stream into a temporarily cached file for caching a new * package. * @return {WriteStream} - Write Stream */ -export function write () { - return fs.createWriteStream(getTmp()) -} +export const write = () => + fs.createWriteStream(getTmp()) /** * open a read stream to a cached dependency. * @param {String} id - id (unique identifier) of the cached tarball. * @return {ReadStream} - Read Stream */ -export function read (id) { - const filename = path.join(config.cacheDir, id) - return fs.createReadStream(filename) -} +export const read = id => + fs.createReadStream(path.join(config.cacheDir, id)) /** * extract a dependency from the cache. diff --git a/src/install.js b/src/install.js index 415dc39..207c07c 100644 --- a/src/install.js +++ b/src/install.js @@ -64,8 +64,8 @@ export const strategies = [ export const findStrategy = (name, version) => concatStatic( - local.resolve(name, version), - registry.resolve(name, version) + local.resolve(name, version) + // registry.resolve(name, version) ) ::first() @@ -199,50 +199,30 @@ export function resolveFromGit (nodeModules, parentTarget, parsedSpec) { }) } -/** - * resolve an individual sub-dependency based on the parent's target and the - * current working directory. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - target path used for determining the sub- - * dependency's path. - * @param {Boolean} isExplicit - whether the install command asks for an explicit install. - * @return {Obserable} - observable sequence of `package.json` root documents - * wrapped into dependency objects representing the resolved sub-dependency. - */ -export function resolve (nodeModules, parentTarget, isExplicit) { +function resolve (nodeModules, parentTarget, isExplicit) { return this::mergeMap(([name, version]) => { progress.add() progress.report(`resolving ${name}@${version}`) - log(`resolving ${name}@${version}`) - - return local.resolve(nodeModules, parentTarget, name, version, isExplicit) - ::_catch((error) => { - if (error.name !== 'LocalConflictError' && error.code !== 'ENOENT') { - throw error - } - log(`failed to resolve ${name}@${version} from local ${parentTarget} via ${nodeModules}`) - return resolveRemote(nodeModules, parentTarget, name, version, isExplicit) - }) + + return concatStatic( + local.resolve(nodeModules, parentTarget, name), + resolveRemote(nodeModules, parentTarget, name, version, isExplicit) + ) + ::first() ::_finally(progress.complete) }) } -/** - * resolve all dependencies starting at the current working directory. - * @param {String} nodeModules - `node_modules` base directory. - * @param {Object} [targets=Object.create(null)] - resolved / active targets. - * @param {Boolean} isExplicit - whether the install command asks for an explicit install. - * @return {Observable} - an observable sequence of resolved dependencies. - */ export function resolveAll (nodeModules) { const targets = Object.create(null) + const entryTarget = '..' return this::expand(result => { if (targets[result.target]) { return EmptyObservable.create() } targets[result.target] = true - const isEntry = result.target === '..' && !result.isProd + const isEntry = result.target === entryTarget && !result.isProd const fields = isEntry ? ENTRY_DEPENDENCY_FIELDS : DEPENDENCY_FIELDS return ArrayObservable.create(parseDependencies(result.pkgJson, fields)) ::resolve(nodeModules, result.target, result.isExplicit) @@ -361,5 +341,5 @@ export function fetch (nodeModules) { export function fetchAll (nodeModules) { return this ::distinctKey('target') - ::map(dep => dep.fetch(nodeModules)) + ::mergeMap(dep => dep.fetch(nodeModules)) } diff --git a/src/install_cmd.js b/src/install_cmd.js index de62e23..c102462 100644 --- a/src/install_cmd.js +++ b/src/install_cmd.js @@ -1,55 +1,50 @@ import path from 'path' -import {EmptyObservable} from 'rxjs/observable/EmptyObservable' import {concatStatic} from 'rxjs/operator/concat' import {publishReplay} from 'rxjs/operator/publishReplay' import {skip} from 'rxjs/operator/skip' import {map} from 'rxjs/operator/map' import {mergeStatic} from 'rxjs/operator/merge' -import {ignoreElements} from 'rxjs/operator/ignoreElements' import {resolveAll, fetchAll, linkAll} from './install' import {init as initCache} from './cache' -import {fromArgv, fromFs, save} from './pkg_json' -import {buildAll} from './build' - -/** - * run the installation command. - * @param {String} cwd - current working directory (absolute path). - * @param {Object} argv - parsed command line arguments. - * @return {Observable} - an observable sequence that will be completed once - * the installation is complete. - */ -export default function installCmd (cwd, argv) { - const isExplicit = argv._.length - 1 - const updatedPkgJSONs = isExplicit ? fromArgv(cwd, argv) : fromFs(cwd) - const isProd = argv.production - - const nodeModules = path.join(cwd, 'node_modules') - - const resolvedAll = updatedPkgJSONs - ::map((pkgJson) => ({pkgJson, target: '..', isProd, isExplicit})) - ::resolveAll(nodeModules)::skip(1) - ::publishReplay().refCount() +import {fromArgv, fromFs} from './pkg_json' - const initialized = initCache()::ignoreElements() - const installedAll = mergeStatic( - resolvedAll::linkAll(), - resolvedAll::fetchAll(nodeModules) +function installAll (dir) { + return mergeStatic( + this::linkAll(), + this::fetchAll(dir) ) +} - const builtAll = argv.build - ? resolvedAll.lift(buildAll(nodeModules)) - : EmptyObservable.create() - - const shouldSave = argv.save || argv['save-dev'] || argv['save-optional'] - const saved = shouldSave - ? updatedPkgJSONs::save(cwd) - : EmptyObservable.create() +const parseArgv = ({_, production}) => ({ + isExplicit: !!(_.length - 1), + isProd: production +}) + +const target = '..' + +export default (cwd, argv) => { + const {isExplicit, isProd} = parseArgv(argv) + const dir = path.join(cwd, 'node_modules') + + // generate the "source" package.json file from which dependencies are being + // parsed and installed. + const srcPkgJson = isExplicit ? fromArgv(cwd, argv) : fromFs(cwd) + + const installedAll = srcPkgJson + ::map(pkgJson => ({ + pkgJson, + target, + isProd, + isExplicit + })) + ::resolveAll(dir) + ::skip(1) + ::publishReplay().refCount() + ::installAll(dir) return concatStatic( - initialized, - installedAll, - saved, - builtAll + initCache(), + installedAll ) } diff --git a/src/local.js b/src/local.js index 695bd4f..a8e37dd 100644 --- a/src/local.js +++ b/src/local.js @@ -1,65 +1,47 @@ import {EmptyObservable} from 'rxjs/observable/EmptyObservable' -import {_do} from 'rxjs/operator/do' import {map} from 'rxjs/operator/map' -import {mergeStatic} from 'rxjs/operator/merge' -import {reduce} from 'rxjs/operator/reduce' - +import {_catch} from 'rxjs/operator/catch' +import {forkJoin} from 'rxjs/observable/forkJoin' +import {readlink, readFile} from './util' import path from 'path' -import {inherits} from 'util' -import {satisfies} from 'semver' - -import * as util from './util' - -// thrown when the currently installed version does not satisfy the semantic -// version constraint. -inherits(LocalConflictError, Error) -function LocalConflictError (name, version, expected) { - Error.captureStackTrace(this, this.constructor) - this.name = 'LocalConflictError' - this.message = `Local version ${name}@${version} does not match required\ -version @${expected}` - this.extra = {name, version, expected} -} - -export const fetch = () => - EmptyObservable.create() -const getLinkname = (nodeModules, parentTarget, name) => - path.join(nodeModules, parentTarget, 'node_modules', name) +const getLinkname = (dir, parentTarget, name) => + path.join(dir, parentTarget, 'node_modules', name) -const getTarget = dst => ({ - target: path.basename(path.dirname(dst)) -}) - -const checkConflict = (name, version) => ({pkgJson}) => { - if (!satisfies(pkgJson.version, version)) { - throw new LocalConflictError(name, pkgJson.version, version) - } -} +const getDir = dst => + path.basename(path.dirname(dst)) const readTarget = linkname => - util.readlink(linkname)::map(getTarget) + readlink(linkname)::map(getDir) const readPkgJson = filename => - util.readFile(filename, 'utf8') - ::map(JSON.parse) - ::map(pkgJson => ({pkgJson})) + readFile(filename)::map(JSON.parse) -const acc = (_, x) => - ({..._, ...x}) +const empty = () => + EmptyObservable.create() -export const resolve = (nodeModules, parentTarget, name, version, isExplicit) => { - const linkname = getLinkname(nodeModules, parentTarget, name) +// EmptyObservable accepts a scheduler, but fetch is invoked with the +// node_modules path, which means we can't alias fetch to the +// EmptyObservable.create. +const fetch = empty + +const readTargetPkgJson = (dir, parentTarget, name) => { + const linkname = getLinkname(dir, parentTarget, name) const filename = path.join(linkname, 'package.json') - return mergeStatic( + return forkJoin( readTarget(linkname), readPkgJson(filename) ) - ::reduce(acc, {parentTarget, name, fetch}) - ::_do( - isExplicit - ? checkConflict(name, version) - : Function.prototype - ) } + +export const resolve = (dir, parentTarget, name) => + readTargetPkgJson(dir, parentTarget, name) + ::map(([target, pkgJson]) => ({ + target, + pkgJson, + parentTarget, + name, + fetch + })) + ::_catch(empty) diff --git a/src:_install_cmd.js b/src:_install_cmd.js new file mode 100644 index 0000000..865884d --- /dev/null +++ b/src:_install_cmd.js @@ -0,0 +1,21 @@ + +// import {EmptyObservable} from 'rxjs/observable/EmptyObservable' + +// import {fromArgv, fromFs, save} from './pkg_json' +// import {buildAll} from './build' + +// return concatStatic( +// initialized, +// installedAll +// // saved, +// // builtAll +// ) + +// const builtAll = argv.build +// ? resolvedAll.lift(buildAll(nodeModules)) +// : EmptyObservable.create() + +// const shouldSave = argv.save || argv['save-dev'] || argv['save-optional'] +// const saved = shouldSave +// ? updatedPkgJSONs::save(cwd) +// : EmptyObservable.create() diff --git a/test/spec/local.spec.js b/test/spec/local.spec.js new file mode 100644 index 0000000..081585c --- /dev/null +++ b/test/spec/local.spec.js @@ -0,0 +1,6 @@ +import * as local from '../../src/local' + +describe('local', () => { + describe('resolve', () => { + }) +}) From 6be94b2e174b2bc45a4e6c79548fff648bba13d0 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Mon, 5 Sep 2016 20:11:50 +0100 Subject: [PATCH 05/30] WIP --- src/_local.js | 22 ------------ src/cmd.js | 2 +- src/fetch_all.js | 8 +++++ src/install.js | 89 +--------------------------------------------- src/install_cmd.js | 17 ++++----- src/link_all.js | 36 +++++++++++++++++++ src/resolve_all.js | 68 +++++++++++++++++++++++++++++++++++ 7 files changed, 123 insertions(+), 119 deletions(-) delete mode 100644 src/_local.js create mode 100644 src/fetch_all.js create mode 100644 src/link_all.js create mode 100644 src/resolve_all.js diff --git a/src/_local.js b/src/_local.js deleted file mode 100644 index 817ad92..0000000 --- a/src/_local.js +++ /dev/null @@ -1,22 +0,0 @@ -// import {inherits} from 'util' -// thrown when the currently installed version does not satisfy the semantic -// version constraint. -// inherits(LocalConflictError, Error) -// function LocalConflictError (name, version, expected) { -// Error.captureStackTrace(this, this.constructor) -// this.name = 'LocalConflictError' -// this.message = `Local version ${name}@${version} does not match required\ -// version @${expected}` -// this.extra = {name, version, expected} -// } - -// import {_do} from 'rxjs/operator/do' -// import {satisfies} from 'semver' -// const checkConflict = (name, version) => ({pkgJson}) => { -// if (!satisfies(pkgJson.version, version)) { -// throw new LocalConflictError(name, pkgJson.version, version) -// } -// } -// ::_do(isExplicit ? checkConflict(name, version) : Function.prototype) - -// ::_do(({pkgJson}) => console.log(`${pkgJson.name}@${pkgJson.version}`)) diff --git a/src/cmd.js b/src/cmd.js index 7d3f398..0b9bbdf 100755 --- a/src/cmd.js +++ b/src/cmd.js @@ -68,7 +68,7 @@ let cacheCmd switch (subCommand) { case 'i': case 'install': - installCmd = require('./install_cmd').default + installCmd = require('./install_cmd').default(config) installCmd(cwd, argv).subscribe() break case 'sh': diff --git a/src/fetch_all.js b/src/fetch_all.js new file mode 100644 index 0000000..abb15a9 --- /dev/null +++ b/src/fetch_all.js @@ -0,0 +1,8 @@ +import {distinctKey} from 'rxjs/operator/distinctKey' +import {mergeMap} from 'rxjs/operator/mergeMap' + +export default function fetchAll (nodeModules) { + return this + ::distinctKey('target') + ::mergeMap(dep => dep.fetch(nodeModules)) +} diff --git a/src/install.js b/src/install.js index 207c07c..0f5f72d 100644 --- a/src/install.js +++ b/src/install.js @@ -2,18 +2,12 @@ import crypto from 'crypto' import path from 'path' import url from 'url' import {ArrayObservable} from 'rxjs/observable/ArrayObservable' -import {EmptyObservable} from 'rxjs/observable/EmptyObservable' import {Observable} from 'rxjs/Observable' -import {_finally} from 'rxjs/operator/finally' -import {_do} from 'rxjs/operator/do' import {concatStatic} from 'rxjs/operator/concat' import {first} from 'rxjs/operator/first' -import {distinctKey} from 'rxjs/operator/distinctKey' -import {expand} from 'rxjs/operator/expand' import {map} from 'rxjs/operator/map' import {_catch} from 'rxjs/operator/catch' import {mergeMap} from 'rxjs/operator/mergeMap' -import {retry} from 'rxjs/operator/retry' import {skip} from 'rxjs/operator/skip' import needle from 'needle' import assert from 'assert' @@ -26,8 +20,7 @@ import * as registry from './registry' import * as local from './local' import * as git from './git' import * as util from './util' -import * as progress from './progress' -import {normalizeBin, parseDependencies} from './pkg_json' +import {normalizeBin} from './pkg_json' import debuglog from './debuglog' @@ -46,17 +39,6 @@ export const ENTRY_DEPENDENCY_FIELDS = [ 'optionalDependencies' ] -/** - * properties of `package.json` of sub-dependencies that will be checked for - * dependences. - * @type {Array.} - * @readonly - */ -export const DEPENDENCY_FIELDS = [ - 'dependencies', - 'optionalDependencies' -] - export const strategies = [ local, registry @@ -199,69 +181,6 @@ export function resolveFromGit (nodeModules, parentTarget, parsedSpec) { }) } -function resolve (nodeModules, parentTarget, isExplicit) { - return this::mergeMap(([name, version]) => { - progress.add() - progress.report(`resolving ${name}@${version}`) - - return concatStatic( - local.resolve(nodeModules, parentTarget, name), - resolveRemote(nodeModules, parentTarget, name, version, isExplicit) - ) - ::first() - ::_finally(progress.complete) - }) -} - -export function resolveAll (nodeModules) { - const targets = Object.create(null) - const entryTarget = '..' - - return this::expand(result => { - if (targets[result.target]) { - return EmptyObservable.create() - } - targets[result.target] = true - const isEntry = result.target === entryTarget && !result.isProd - const fields = isEntry ? ENTRY_DEPENDENCY_FIELDS : DEPENDENCY_FIELDS - return ArrayObservable.create(parseDependencies(result.pkgJson, fields)) - ::resolve(nodeModules, result.target, result.isExplicit) - }) -} - -function resolveSymlink (src, dst) { - const relSrc = path.relative(path.dirname(dst), src) - return [relSrc, dst] -} - -function getBinLinks (dep) { - const {pkgJson, parentTarget, target} = dep - const binLinks = [] - const bin = normalizeBin(pkgJson) - const names = Object.keys(bin) - for (let i = 0; i < names.length; i++) { - const name = names[i] - const src = path.join('node_modules', target, 'package', bin[name]) - const dst = path.join('node_modules', parentTarget, 'node_modules', '.bin', name) - binLinks.push([src, dst]) - } - return binLinks -} - -function getDirectLink (dep) { - const {parentTarget, target, name} = dep - const src = path.join('node_modules', target, 'package') - const dst = path.join('node_modules', parentTarget, 'node_modules', name) - return [src, dst] -} - -export function linkAll () { - return this - ::mergeMap((dep) => [getDirectLink(dep), ...getBinLinks(dep)]) - ::map(([src, dst]) => resolveSymlink(src, dst)) - ::mergeMap(([src, dst]) => util.forceSymlink(src, dst)) -} - export const checkShasum = (shasum, expected, tarball) => void assert.equal(shasum, expected, `shasum mismatch for ${tarball}: ${shasum} <-> ${expected}`) @@ -337,9 +256,3 @@ export function fetch (nodeModules) { return concatStatic(extracted, fixedPermissions) }) } - -export function fetchAll (nodeModules) { - return this - ::distinctKey('target') - ::mergeMap(dep => dep.fetch(nodeModules)) -} diff --git a/src/install_cmd.js b/src/install_cmd.js index c102462..27b6a92 100644 --- a/src/install_cmd.js +++ b/src/install_cmd.js @@ -1,13 +1,15 @@ import path from 'path' import {concatStatic} from 'rxjs/operator/concat' -import {publishReplay} from 'rxjs/operator/publishReplay' -import {skip} from 'rxjs/operator/skip' import {map} from 'rxjs/operator/map' import {mergeStatic} from 'rxjs/operator/merge' +import {publishReplay} from 'rxjs/operator/publishReplay' +import {skip} from 'rxjs/operator/skip' -import {resolveAll, fetchAll, linkAll} from './install' -import {init as initCache} from './cache' +import fetchAll from './fetch_all' +import linkAll from './link_all' +import resolveAll from './resolve_all' import {fromArgv, fromFs} from './pkg_json' +import {init as initCache} from './cache' function installAll (dir) { return mergeStatic( @@ -21,11 +23,10 @@ const parseArgv = ({_, production}) => ({ isProd: production }) -const target = '..' - -export default (cwd, argv) => { +export default config => (cwd, argv) => { const {isExplicit, isProd} = parseArgv(argv) const dir = path.join(cwd, 'node_modules') + const target = '..' // generate the "source" package.json file from which dependencies are being // parsed and installed. @@ -38,7 +39,7 @@ export default (cwd, argv) => { isProd, isExplicit })) - ::resolveAll(dir) + ::resolveAll(dir, config) ::skip(1) ::publishReplay().refCount() ::installAll(dir) diff --git a/src/link_all.js b/src/link_all.js new file mode 100644 index 0000000..a0af6bf --- /dev/null +++ b/src/link_all.js @@ -0,0 +1,36 @@ +import path from 'path' +import {map} from 'rxjs/operator/map' +import {mergeMap} from 'rxjs/operator/mergeMap' +import {forceSymlink} from './util' +import {normalizeBin} from './pkg_json' + +const resolveSymlink = (src, dst) => + [path.relative(path.dirname(dst), src), dst] + +const getBinLinks = dep => { + const {pkgJson, parentTarget, target} = dep + const binLinks = [] + const bin = normalizeBin(pkgJson) + const names = Object.keys(bin) + for (let i = 0; i < names.length; i++) { + const name = names[i] + const src = path.join('node_modules', target, 'package', bin[name]) + const dst = path.join('node_modules', parentTarget, 'node_modules', '.bin', name) + binLinks.push([src, dst]) + } + return binLinks +} + +const getDirectLink = dep => { + const {parentTarget, target, name} = dep + const src = path.join('node_modules', target, 'package') + const dst = path.join('node_modules', parentTarget, 'node_modules', name) + return [src, dst] +} + +export default function linkAll () { + return this + ::mergeMap((dep) => [getDirectLink(dep), ...getBinLinks(dep)]) + ::map(([src, dst]) => resolveSymlink(src, dst)) + ::mergeMap(([src, dst]) => forceSymlink(src, dst)) +} diff --git a/src/resolve_all.js b/src/resolve_all.js new file mode 100644 index 0000000..31c6cd4 --- /dev/null +++ b/src/resolve_all.js @@ -0,0 +1,68 @@ +import {ArrayObservable} from 'rxjs/observable/ArrayObservable' +import {EmptyObservable} from 'rxjs/observable/EmptyObservable' +import {_finally} from 'rxjs/operator/finally' +import {concatStatic} from 'rxjs/operator/concat' +import {expand} from 'rxjs/operator/expand' +import {first} from 'rxjs/operator/first' +import {mergeMap} from 'rxjs/operator/mergeMap' + +import {add, complete, report} from './progress' +import {parseDependencies} from './pkg_json' +import {resolve as resolveFromLocal} from './local' +import {resolve as resolveFromRegistry} from './registry' + +/** + * properties of project-level `package.json` files that will be checked for + * dependencies. + * @type {Array.} + * @readonly + */ +export const ENTRY_DEPENDENCY_FIELDS = [ + 'dependencies', + 'devDependencies', + 'optionalDependencies' +] + +/** + * properties of `package.json` of sub-dependencies that will be checked for + * dependencies. + * @type {Array.} + * @readonly + */ +export const DEPENDENCY_FIELDS = [ + 'dependencies', + 'optionalDependencies' +] + +function resolve (nodeModules, parentTarget, config) { + return this::mergeMap(([name, version]) => { + add() + report(`resolving ${name}@${version}`) + + return concatStatic( + resolveFromLocal(nodeModules, parentTarget, name), + resolveFromRegistry(nodeModules, parentTarget, name, version, { + ...config.httpOptions, + registry: config.registry + }) + ) + ::first() + ::_finally(complete) + }) +} + +export default function resolveAll (nodeModules, config) { + const targets = Object.create(null) + const entryTarget = '..' + + return this::expand(result => { + if (targets[result.target]) { + return EmptyObservable.create() + } + targets[result.target] = true + const isEntry = result.target === entryTarget && !result.isProd + const fields = isEntry ? ENTRY_DEPENDENCY_FIELDS : DEPENDENCY_FIELDS + return ArrayObservable.create(parseDependencies(result.pkgJson, fields)) + ::resolve(nodeModules, result.target, config) + }) +} From 0d2e3a1f433c77534bc586d29a8fc5cc64075f64 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Tue, 6 Sep 2016 02:53:33 +0100 Subject: [PATCH 06/30] WIP --- src/build.js | 19 +++------ src/cache.js | 22 +++++----- src/cache_cmd.js | 13 +++--- src/cmd.js | 17 ++++---- src/config_cmd.js | 7 ++-- src/help_cmd.js | 4 +- src/init_cmd.js | 22 +++++----- src/install.js | 24 ----------- src/install_cmd.js | 1 + src/link.js | 15 ++++--- src/link_cmd.js | 13 +++--- src/ping.js | 7 ++-- src/ping_cmd.js | 7 ++-- src/pkg_json.js | 56 ++++++++++++------------- src/registry.js | 4 +- src/run_cmd.js | 102 +++++++++++++++++++++++++-------------------- src/shell_cmd.js | 18 ++++---- src/unlink_cmd.js | 2 +- src/util.js | 16 ++++--- src/version_cmd.js | 9 ++-- 20 files changed, 175 insertions(+), 203 deletions(-) diff --git a/src/build.js b/src/build.js index c80cc33..4af968c 100644 --- a/src/build.js +++ b/src/build.js @@ -14,27 +14,18 @@ import * as config from './config' import debuglog from './debuglog' const log = debuglog('build') -/** - * names of lifecycle scripts that should be run as part of the installation - * process of a specific package (= properties of `scripts` object in - * `package.json`). - * @type {Array.} - * @readonly - */ +// names of lifecycle scripts that should be run as part of the installation +// process of a specific package (= properties of `scripts` object in +// `package.json`). export const LIFECYCLE_SCRIPTS = [ 'preinstall', 'install', 'postinstall' ] -/** - * error class used for representing an error that occurs due to a lifecycle - * script that exits with a non-zero status code. - */ +// error class used for representing an error that occurs due to a lifecycle +// script that exits with a non-zero status code. export class FailedBuildError extends Error { - /** - * create instance. - */ constructor () { super('failed to build one or more dependencies that exited with != 0') this.name = FailedBuildError diff --git a/src/cache.js b/src/cache.js index ac1f850..37e85d4 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,14 +1,14 @@ -import {Observable} from 'rxjs/Observable' -import {mergeMap} from 'rxjs/operator/mergeMap' -import {retryWhen} from 'rxjs/operator/retryWhen' -import {ignoreElements} from 'rxjs/operator/ignoreElements' -import gunzip from 'gunzip-maybe' -import tar from 'tar-fs' +import * as config from './config' +import * as util from './util' import fs from 'fs' +import gunzip from 'gunzip-maybe' import path from 'path' +import tar from 'tar-fs' import uuid from 'node-uuid' -import * as util from './util' -import * as config from './config' +import {Observable} from 'rxjs/Observable' +import {ignoreElements} from 'rxjs/operator/ignoreElements' +import {mergeMap} from 'rxjs/operator/mergeMap' +import {retryWhen} from 'rxjs/operator/retryWhen' /** * initialize the cache. @@ -59,15 +59,15 @@ export function extract (dest, id) { observer.next(tmpDest) observer.complete() } - const errorHandler = (err) => observer.error(err) + const errorHandler = err => observer.error(err) this.read(id).on('error', errorHandler) .pipe(gunzip()).on('error', errorHandler) .pipe(untar).on('error', errorHandler) .on('finish', completeHandler) }) - ::mergeMap((tmpDest) => util.rename(tmpDest, dest) - ::retryWhen((errors) => errors::mergeMap((error) => { + ::mergeMap(tmpDest => util.rename(tmpDest, dest) + ::retryWhen(errors => errors::mergeMap(error => { if (error.code !== 'ENOENT') { throw error } diff --git a/src/cache_cmd.js b/src/cache_cmd.js index 63e8723..9e739dc 100644 --- a/src/cache_cmd.js +++ b/src/cache_cmd.js @@ -1,27 +1,24 @@ -import rimraf from 'rimraf' import path from 'path' -import * as config from './config' +import rimraf from 'rimraf' /** * print help if invoked without any further sub-command, empty the cache * directory (delete it) if invoked via `ied cache clean`. - * @param {String} cwd - current working directory. - * @param {Object} argv - parsed command line arguments. */ -export default function cacheCmd (cwd, argv) { +export default ({cacheDir}) => (cwd, argv) => { switch (argv._[1]) { // `ied cache clean` case 'clean': const shasum = argv._[2] if (shasum) { - rimraf.sync(path.join(config.cacheDir, shasum)) + rimraf.sync(path.join(cacheDir, shasum)) } else { - rimraf.sync(config.cacheDir) + rimraf.sync(cacheDir) } break // `ied cache` default: const helpCmd = require('./help_cmd').default - helpCmd(cwd, argv) + helpCmd(cwd, argv).subscribe() } } diff --git a/src/cmd.js b/src/cmd.js index 0b9bbdf..80a58c7 100755 --- a/src/cmd.js +++ b/src/cmd.js @@ -2,6 +2,7 @@ import minimist from 'minimist' import * as config from './config' + if (['development', 'test'].indexOf(process.env.NODE_ENV) !== -1) { require('source-map-support').install() } @@ -73,7 +74,7 @@ let cacheCmd break case 'sh': case 'shell': - shellCmd = require('./shell_cmd').default + shellCmd = require('./shell_cmd').default(config) shellCmd(cwd).subscribe() break case 'r': @@ -91,20 +92,20 @@ let cacheCmd runCmd(cwd, {...argv, _: ['run'].concat(argv._)}).subscribe() break case 'ping': - pingCmd = require('./ping_cmd').default - pingCmd() + pingCmd = require('./ping_cmd').default(config) + pingCmd().subscribe() break case 'conf': case 'config': - configCmd = require('./config_cmd').default + configCmd = require('./config_cmd').default(config) configCmd() break case 'init': - initCmd = require('./init_cmd').default - initCmd(cwd, argv) + initCmd = require('./init_cmd').default(config) + initCmd(cwd, argv).subscribe() break case 'link': - linkCmd = require('./link_cmd').default + linkCmd = require('./link_cmd').default(config) linkCmd(cwd, argv).subscribe() break case 'unlink': @@ -112,7 +113,7 @@ let cacheCmd unlinkCmd(cwd, argv).subscribe() break case 'cache': - cacheCmd = require('./cache_cmd').default + cacheCmd = require('./cache_cmd').default(config) cacheCmd(cwd, argv) break case 'version': diff --git a/src/config_cmd.js b/src/config_cmd.js index 907f1fc..5b3bca0 100644 --- a/src/config_cmd.js +++ b/src/config_cmd.js @@ -1,13 +1,14 @@ -import * as config from './config' import Table from 'easy-table' /** * print the used configuration object as an ASCII table. */ -export default function configCmd () { +export default config => () => { const table = new Table() const keys = Object.keys(config) - for (const key of keys) { + + for (let i = 0; i < keys.length; i++) { + const key = keys[i] const value = config[key] table.cell('key', key) table.cell('value', value) diff --git a/src/help_cmd.js b/src/help_cmd.js index 502b076..3fd0162 100644 --- a/src/help_cmd.js +++ b/src/help_cmd.js @@ -1,13 +1,13 @@ import path from 'path' -import {readFile} from './util' import {_do} from 'rxjs/operator/do' +import {readFile} from './util' /** * print the `USAGE` document. can be invoked using `ied help` or implicitly as * a fall back. * @return {Observable} - observable sequence of `USAGE`. */ -export default function helpCmd () { +export default () => { const filename = path.join(__dirname, '../USAGE.txt') return readFile(filename, 'utf8')::_do(console.log) } diff --git a/src/init_cmd.js b/src/init_cmd.js index bd57b08..de95cc3 100644 --- a/src/init_cmd.js +++ b/src/init_cmd.js @@ -1,22 +1,20 @@ import init from 'init-package-json' import path from 'path' -import * as config from './config' +import {Observable} from 'rxjs/Observable' /** * initialize a new `package.json` file. - * @param {String} cwd - current working directory. * @see https://www.npmjs.com/package/init-package-json */ -export default function initCmd (cwd) { - const initFile = path.resolve(config.home, '.ied-init') +export default ({home}) => cwd => + Observable.create(observer => { + const initFile = path.resolve(home, '.ied-init') - init(cwd, initFile, (err) => { - if (err) { - if (err.message === 'canceled') { - console.log('init canceled!') - return + init(cwd, initFile, (err) => { + if (err && err.message !== 'canceled') { + observer.error(err) + } else { + observer.complete() } - throw err - } + }) }) -} diff --git a/src/install.js b/src/install.js index 0f5f72d..ded33de 100644 --- a/src/install.js +++ b/src/install.js @@ -27,30 +27,6 @@ import debuglog from './debuglog' const log = debuglog('install') const cachedNpa = memoize(npa) -/** - * properties of project-level `package.json` files that will be checked for - * dependencies. - * @type {Array.} - * @readonly - */ -export const ENTRY_DEPENDENCY_FIELDS = [ - 'dependencies', - 'devDependencies', - 'optionalDependencies' -] - -export const strategies = [ - local, - registry -] - -export const findStrategy = (name, version) => - concatStatic( - local.resolve(name, version) - // registry.resolve(name, version) - ) - ::first() - /** * resolve a dependency's `package.json` file from a remote registry. * @param {String} nodeModules - `node_modules` base directory. diff --git a/src/install_cmd.js b/src/install_cmd.js index 27b6a92..1fbecc5 100644 --- a/src/install_cmd.js +++ b/src/install_cmd.js @@ -1,4 +1,5 @@ import path from 'path' + import {concatStatic} from 'rxjs/operator/concat' import {map} from 'rxjs/operator/map' import {mergeStatic} from 'rxjs/operator/merge' diff --git a/src/link.js b/src/link.js index 96663b7..9062930 100644 --- a/src/link.js +++ b/src/link.js @@ -11,7 +11,7 @@ import {mergeMap} from 'rxjs/operator/mergeMap' * @return {Array.} - an array of tuples representing symbolic links to be * created. */ -export function getSymlinks (cwd, pkgJson) { +export const getSymlinks = (cwd, pkgJson) => { const libSymlink = [cwd, path.join(config.globalNodeModules, pkgJson.name)] let bin = pkgJson.bin if (typeof bin === 'string') { @@ -19,7 +19,7 @@ export function getSymlinks (cwd, pkgJson) { bin[pkgJson.name] = pkgJson.bin } bin = bin || {} - const binSymlinks = Object.keys(bin).map((name) => ([ + const binSymlinks = Object.keys(bin).map(name => ([ path.join(config.globalNodeModules, pkgJson.name, bin[name]), path.join(config.globalBin, name) ])) @@ -31,11 +31,10 @@ export function getSymlinks (cwd, pkgJson) { * @param {String} cwd - current working directory. * @return {Observable} - observable sequence. */ -export function linkToGlobal (cwd) { - return readFileJSON(path.join(cwd, 'package.json')) +export const linkToGlobal = cwd => + readFileJSON(path.join(cwd, 'package.json')) ::mergeMap((pkgJson) => getSymlinks(cwd, pkgJson)) ::mergeMap(([src, dst]) => forceSymlink(src, dst)) -} /** * links a globally linked package into the package present in the current @@ -46,7 +45,7 @@ export function linkToGlobal (cwd) { * @param {String} name - name of the dependency to be linked. * @return {Observable} - observable sequence. */ -export function linkFromGlobal (cwd, name) { +export const linkFromGlobal = (cwd, name) => { const dst = path.join(cwd, 'node_modules', name) const src = path.join(config.globalNodeModules, name) return forceSymlink(src, dst) @@ -58,7 +57,7 @@ export function linkFromGlobal (cwd, name) { * @param {String} cwd - current working directory. * @return {Observable} - observable sequence. */ -export function unlinkToGlobal (cwd) { +export const unlinkToGlobal = cwd => { const pkg = require(path.join(cwd, 'package.json')) const symlinks = getSymlinks(cwd, pkg) return ArrayObservable.create(symlinks) @@ -74,7 +73,7 @@ export function unlinkToGlobal (cwd) { * project's `node_modules`. * @return {Observable} - observable sequence. */ -export function unlinkFromGlobal (cwd, name) { +export const unlinkFromGlobal = (cwd, name) => { const dst = path.join(cwd, 'node_modules', name) return unlink(dst) } diff --git a/src/link_cmd.js b/src/link_cmd.js index 7ae6271..0e5751f 100644 --- a/src/link_cmd.js +++ b/src/link_cmd.js @@ -1,10 +1,9 @@ -import * as config from './config' -import {mkdirp} from './util' -import {linkFromGlobal, linkToGlobal} from './link' -import {concatStatic} from 'rxjs/operator/concat' +import path from 'path' import {ArrayObservable} from 'rxjs/observable/ArrayObservable' +import {concatStatic} from 'rxjs/operator/concat' +import {linkFromGlobal, linkToGlobal} from './link' import {mergeMap} from 'rxjs/operator/mergeMap' -import path from 'path' +import {mkdirp} from './util' /** * can be used in two ways: @@ -24,14 +23,14 @@ import path from 'path' * @param {Object} argv - parsed command line arguments. * @return {Observable} - observable sequence. */ -export default function linkCmd (cwd, argv) { +export default config => (cwd, argv) => { const names = argv._.slice(1) if (names.length) { const localNodeModules = path.join(cwd, 'node_modules') const init = mkdirp(localNodeModules) return concatStatic(init, ArrayObservable.create(names) - ::mergeMap((name) => linkFromGlobal(cwd, name))) + ::mergeMap(name => linkFromGlobal(cwd, name))) } const init = concatStatic( diff --git a/src/ping.js b/src/ping.js index 92363b8..8c904cf 100644 --- a/src/ping.js +++ b/src/ping.js @@ -1,12 +1,13 @@ import url from 'url' -import {registry} from './config' import {httpGet} from './util' +import {map} from 'rxjs/operator/map' /** * ping the pre-configured npm registry by hitting `/-/ping?write=true`. + * @param {String} registry - root registry url to ping. * @return {Observable} - observable sequence of the returned JSON object. */ -export function ping () { +export const ping = registry => { const uri = url.resolve(registry, '-/ping?write=true') - return httpGet(uri) + return httpGet(uri)::map(({body}) => body) } diff --git a/src/ping_cmd.js b/src/ping_cmd.js index 96598c5..6a6b36c 100644 --- a/src/ping_cmd.js +++ b/src/ping_cmd.js @@ -1,9 +1,8 @@ +import {_do} from 'rxjs/operator/do' import {ping} from './ping' /** * ping the registry and print the received response. - * @return {Subscription} - subscription to the {@link ping} command. */ -export default function pingCmd () { - return ping().subscribe(console.log) -} +export default ({registry}) => () => + ping(registry)::_do(console.log) diff --git a/src/pkg_json.js b/src/pkg_json.js index bda52e1..ff1700f 100644 --- a/src/pkg_json.js +++ b/src/pkg_json.js @@ -1,9 +1,9 @@ +import fromPairs from 'lodash.frompairs' import path from 'path' import {ScalarObservable} from 'rxjs/observable/ScalarObservable' -import fromPairs from 'lodash.frompairs' +import {_catch} from 'rxjs/operator/catch' import {map} from 'rxjs/operator/map' import {mergeMap} from 'rxjs/operator/mergeMap' -import {_catch} from 'rxjs/operator/catch' import * as util from './util' @@ -15,7 +15,7 @@ import * as util from './util' * together. * @return {Object} - merged dependencies. */ -export function mergeDependencies (pkgJson, fields) { +export const mergeDependencies = (pkgJson, fields) => { const allDependencies = {} for (let i = 0; i < fields.length; i++) { const field = fields[i] @@ -37,12 +37,10 @@ export function mergeDependencies (pkgJson, fields) { * `package.json` file. * @return {Array.} - array of bundled dependency names. */ -export function parseBundleDependencies (pkgJson) { - const bundleDependencies = [] +export const parseBundleDependencies = pkgJson => + [] .concat(pkgJson.bundleDependencies || []) .concat(pkgJson.bundledDependencies || []) - return bundleDependencies -} /** * extract specified dependencies from a specific `package.json`. @@ -51,7 +49,7 @@ export function parseBundleDependencies (pkgJson) { * @param {Array.} fields - array of dependency fields to be followed. * @return {Array} - array of dependency pairs. */ -export function parseDependencies (pkgJson, fields) { +export const parseDependencies = (pkgJson, fields) => { // bundleDependencies and bundledDependencies are optional. we need to // exclude those form the final [name, version] pairs that we're // generating. @@ -75,7 +73,7 @@ export function parseDependencies (pkgJson, fields) { * `package.json` file. * @return {Object} - normalized `bin` property. */ -export function normalizeBin (pkgJson) { +export const normalizeBin = pkgJson => { switch (typeof pkgJson.bin) { case 'string': return ({[pkgJson.name]: pkgJson.bin}) case 'object': return pkgJson.bin @@ -88,12 +86,12 @@ export function normalizeBin (pkgJson) { * @param {String} baseDir - base directory of the project. * @return {Observabel} - an observable sequence of an `EntryDep`. */ -export function fromFs (baseDir) { +export const fromFs = baseDir => { const filename = path.join(baseDir, 'package.json') return util.readFileJSON(filename) } -export function updatePkgJson (pkgJson, diff) { +export const updatePkgJson = (pkgJson, diff) => { const updatedPkgJson = {...pkgJson} const fields = Object.keys(diff) for (const field of fields) { @@ -109,25 +107,15 @@ export function save (baseDir) { const filename = path.join(baseDir, 'package.json') return this - ::mergeMap((diff) => fromFs(baseDir) + ::mergeMap(diff => fromFs(baseDir) ::_catch(() => ScalarObservable.create({})) - ::map((pkgJson) => updatePkgJson(pkgJson, diff)) + ::map(pkgJson => updatePkgJson(pkgJson, diff)) ) - ::map((pkgJson) => JSON.stringify(pkgJson, null, '\t')) - ::mergeMap((pkgJson) => util.writeFile(filename, pkgJson, 'utf8')) + ::map(pkgJson => JSON.stringify(pkgJson, null, '\t')) + ::mergeMap(pkgJson => util.writeFile(filename, pkgJson, 'utf8')) } -/** - * create an instance by parsing the explicit dependencies supplied via - * command line arguments. - * @param {String} baseDir - base directory of the project. - * @param {Array} argv - command line arguments. - * @return {Observabel} - an observable sequence of an `EntryDep`. - */ -export function fromArgv (baseDir, argv) { - const pkgJson = parseArgv(argv) - return ScalarObservable.create(pkgJson) -} +const argvRegExp = /^(@?.+?)(?:@(.+)?)?$/ /** * parse the command line arguments and create the dependency field of a @@ -136,11 +124,11 @@ export function fromArgv (baseDir, argv) { * @return {NullPkgJSON} - package.json created from explicit dependencies * supplied via command line arguments. */ -export function parseArgv (argv) { +export const parseArgv = argv => { const names = argv._.slice(1) const nameVersionPairs = fromPairs(names.map((target) => { - const nameVersion = /^(@?.+?)(?:@(.+)?)?$/.exec(target) + const nameVersion = argvRegExp.exec(target) return [nameVersion[1], nameVersion[2] || '*'] })) @@ -150,3 +138,15 @@ export function parseArgv (argv) { return {[field]: nameVersionPairs} } + +/** + * create an instance by parsing the explicit dependencies supplied via + * command line arguments. + * @param {String} baseDir - base directory of the project. + * @param {Array} argv - command line arguments. + * @return {Observabel} - an observable sequence of an `EntryDep`. + */ +export const fromArgv = (baseDir, argv) => { + const pkgJson = parseArgv(argv) + return ScalarObservable.create(pkgJson) +} diff --git a/src/registry.js b/src/registry.js index f3039ed..6f4b563 100644 --- a/src/registry.js +++ b/src/registry.js @@ -17,7 +17,9 @@ export const requests = Object.create(null) // clear the internal cache used for pending and completed HTTP requests. export const reset = () => { const uris = Object.keys(requests) - uris.forEach(uri => delete requests[uri]) + for (let i = 0; i < uris.length; i++) { + delete requests[uris[i]] + } } // scoped npm modules, such as @alexanderGugel/some-package, are "@"-prefixed. diff --git a/src/run_cmd.js b/src/run_cmd.js index acde388..e8c47b8 100644 --- a/src/run_cmd.js +++ b/src/run_cmd.js @@ -1,48 +1,39 @@ -import path from 'path' import assert from 'assert' -import {readFile, entries} from './util' -import {concatMap} from 'rxjs/operator/concatMap' +import path from 'path' import {Observable} from 'rxjs' -import {map} from 'rxjs/operator/map' +import {_do} from 'rxjs/operator/do' +import {concatMap} from 'rxjs/operator/concatMap' +import {entries} from './util' import {filter} from 'rxjs/operator/filter' +import {fromFs} from './pkg_json' +import {map} from 'rxjs/operator/map' import {reduce} from 'rxjs/operator/reduce' -import {_do} from 'rxjs/operator/do' -import {spawn} from 'child_process' import {sh, shFlag} from './config' +import {spawn} from 'child_process' -/** - * run a `package.json` script and the related pre- and postscript. - * @param {String} cwd - current working directory. - * @param {Object} argv - command line arguments. - * @return {Observable} - observable sequence. - */ -export default function runCmd (cwd, argv) { - const scripts = argv._.slice(1) - - const pkgJson = readFile(path.join(cwd, 'package.json'), 'utf8') - ::map(JSON.parse) +const logCode = ([code, name, script]) => { + const prefix = `${name}: \`${script}\`` - // if no script(s) have been specified, log out the available scripts. - if (!scripts.length) { - return pkgJson::_do(logAvailable) + switch (code) { + case 0: + console.log(`${prefix} succeeded`) + break + default: + console.error(`${prefix} failed (exit status ${code})`) } +} - const PATH = [path.join(cwd, 'node_modules/.bin'), process.env.PATH] - .join(path.delimiter) - const env = {...process.env, PATH} - const runOptions = {env, stdio: 'inherit'} - - return pkgJson::map(({scripts = {}}) => scripts)::entries() // eslint-disable-line no-shadow - ::filter(([name]) => ~scripts.indexOf(name)) - ::concatMap(([name, script]) => run(script, runOptions) - ::map((code) => ([name, script, code]))) - ::_do(logCode) - ::reduce((codes, [name, script, code]) => codes + code, 0) - ::_do((code) => assert.equal(code, 0, 'exit status != 0')) +const logAvailable = ({scripts = {}}) => { + const available = Object.keys(scripts) + if (available.length) { + console.log(`available scripts: \n\t${available.join('\n\t')}`) + } else { + console.error('no scripts in package.json') + } } -export function run (script, options = {}) { - return Observable.create((observer) => { +export const run = (script, options = {}) => + Observable.create((observer) => { const args = [shFlag, script] const childProcess = spawn(sh, args, options) childProcess.on('close', (code) => { @@ -53,19 +44,38 @@ export function run (script, options = {}) { observer.error(err) }) }) -} -function logCode ([name, script, code]) { - const prefix = `${name}: \`${script}\`` - if (code === 0) console.log(`${prefix} succeeded`) - else console.error(`${prefix} failed (exit status ${code})`) -} +/** + * run a `package.json` script and the related pre- and postscript. + * @param {String} cwd - current working directory. + * @param {Object} argv - command line arguments. + * @return {Observable} - observable sequence. + */ +export default (cwd, argv) => { + const scriptNames = argv._.slice(1) + const pkgJson = fromFs(cwd) -function logAvailable ({scripts = {}}) { - const available = Object.keys(scripts) - if (available.length) { - console.log(`available scripts: ${available.join(', ')}`) - } else { - console.log('no scripts in package.json') + // if no script(s) have been specified, log out the available scripts. + if (!scriptNames.length) { + return pkgJson::_do(logAvailable) + } + + const PATH = [path.join(cwd, 'node_modules/.bin'), process.env.PATH] + .join(path.delimiter) + const env = { + ...process.env, + PATH } + const runOptions = {env, stdio: 'inherit'} + + return pkgJson::map(({scripts = {}}) => scripts) + ::entries() + ::filter(([name]) => ~scriptNames.indexOf(name)) + ::concatMap(([name, script]) => + run(script, runOptions) + ::map(code => ([code, name, script])) + ) + ::_do(logCode) + ::reduce((codes, [code]) => codes + code, 0) + ::_do(code => assert.equal(code, 0, 'exit status != 0')) } diff --git a/src/shell_cmd.js b/src/shell_cmd.js index 5aaf8e1..2cb7643 100644 --- a/src/shell_cmd.js +++ b/src/shell_cmd.js @@ -1,21 +1,21 @@ import path from 'path' -import * as config from './config' -import {spawn} from 'child_process' -import {readdir} from './util' -import {map} from 'rxjs/operator/map' import {_do} from 'rxjs/operator/do' +import {map} from 'rxjs/operator/map' +import {readdir} from './util' +import {spawn} from 'child_process' /** * enter a new session that has access to the CLIs exposed by the installed * packages by using an amended `PATH`. - * @param {String} cwd - current working directory. */ -export default function shellCmd (cwd) { +export default config => cwd => { const binPath = path.join(cwd, 'node_modules/.bin') - const env = Object.create(process.env) - env.PATH = [binPath, process.env.PATH].join(path.delimiter) + const env = { + ...process.env, + PATH: [binPath, process.env.PATH].join(path.delimiter) + } return readdir(binPath) - ::_do(cmds => console.log('\nadded', cmds.join(', '), '\n')) + ::_do(cmds => console.log('added: \n\t', cmds.join('\n\t'))) ::map(() => spawn(config.sh, [], {stdio: 'inherit', env})) } diff --git a/src/unlink_cmd.js b/src/unlink_cmd.js index 6b3a67b..f171797 100644 --- a/src/unlink_cmd.js +++ b/src/unlink_cmd.js @@ -10,7 +10,7 @@ import {unlinkFromGlobal, unlinkToGlobal} from './link' * @param {Object} argv - parsed command line arguments. * @return {Observable} - observable sequence. */ -export default function unlinkCmd (cwd, argv) { +export default (cwd, argv) => { const names = argv._.slice(1) return names.length diff --git a/src/util.js b/src/util.js index 06165dc..6051728 100644 --- a/src/util.js +++ b/src/util.js @@ -33,8 +33,8 @@ export const createObservableFactory = (fn, thisArg) => * arguments to [`needle`](https://www.npmjs.com/package/needle). * @return {Observable} - observable sequence of a single response object. */ -export function httpGet (...args) { - return Observable.create(observer => { +export const httpGet = (...args) => + Observable.create(observer => { needle.get(...args, (error, response) => { if (error) observer.error(error) else { @@ -43,7 +43,6 @@ export function httpGet (...args) { } }) }) -} /** @type {Function} Observable wrapper function around `fs.readFile`. */ export const readFile = createObservableFactory(fs.readFile, fs) @@ -85,7 +84,8 @@ export function entries () { return this::mergeMap((object) => { const results = [] const keys = Object.keys(object) - for (const key of keys) { + for (let i = 0; i < keys.length; i++) { + const key = keys[i] results.push([key, object[key]]) } return results @@ -98,16 +98,14 @@ export function entries () { * @return {Observable} - observable sequence of a single object representing * the read JSON file. */ -export function readFileJSON (file) { - return readFile(file, 'utf8')::map(JSON.parse) -} +export const readFileJSON = file => + readFile(file, 'utf8')::map(JSON.parse) /** * set the terminal title using the required ANSI escape codes. * @param {String} title - title to be set. */ -export function setTitle (title) { +export const setTitle = title => process.stdout.write( `${String.fromCharCode(27)}]0;${title}${String.fromCharCode(7)}` ) -} diff --git a/src/version_cmd.js b/src/version_cmd.js index de0e022..a827631 100644 --- a/src/version_cmd.js +++ b/src/version_cmd.js @@ -1,15 +1,14 @@ -import {readFileJSON} from './util' import path from 'path' -import {map} from 'rxjs/operator/map' import {_do} from 'rxjs/operator/do' +import {map} from 'rxjs/operator/map' +import {readFileJSON} from './util' /** * display the version number. * @return {Subscription} - a subscription that logs the versio number to the * console. */ -export default function versionCmd () { - return readFileJSON(path.join(__dirname, '../package.json')) +export default () => + readFileJSON(path.join(__dirname, '../package.json')) ::map(({version}) => version) ::_do((version) => console.log(`ied version ${version}`)) -} From 2449f99b92a13f09a502f257d891bf3e4c4e3bbe Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Tue, 6 Sep 2016 02:54:48 +0100 Subject: [PATCH 07/30] WIP --- src/cache.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cache.js b/src/cache.js index 37e85d4..2759fd5 100644 --- a/src/cache.js +++ b/src/cache.js @@ -67,11 +67,11 @@ export function extract (dest, id) { .on('finish', completeHandler) }) ::mergeMap(tmpDest => util.rename(tmpDest, dest) - ::retryWhen(errors => errors::mergeMap(error => { - if (error.code !== 'ENOENT') { - throw error + ::retryWhen(errors => errors::mergeMap(err => { + switch (err.code) { + case 'ENOENT': return util.mkdirp(path.dirname(dest)) + default: throw err } - return util.mkdirp(path.dirname(dest)) })) ) } From d34ae96596a051696b635e2e41bebc8af47a0256 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Tue, 6 Sep 2016 03:31:40 +0100 Subject: [PATCH 08/30] WIP --- src/debuglog.js | 4 +- src/link.js | 5 +- src/local.js | 4 +- src/progress.js | 34 +++--- src/registry.js | 4 +- src/{ => todo}/git.js | 0 src/todo/install.js | 234 ++++++++++++++++++++++++++++++++++++++++++ src/util.js | 6 +- src/version_cmd.js | 2 +- 9 files changed, 264 insertions(+), 29 deletions(-) rename src/{ => todo}/git.js (100%) create mode 100644 src/todo/install.js diff --git a/src/debuglog.js b/src/debuglog.js index f27180c..0d24225 100644 --- a/src/debuglog.js +++ b/src/debuglog.js @@ -38,7 +38,7 @@ let debugEnv * @return {Function} - the logging function. * @see https://nodejs.org/api/util.html#util_util_debuglog_section */ -function debuglog (set) { +export default set => { if (debugEnv === undefined) { debugEnv = process.env.NODE_DEBUG || '' } @@ -56,5 +56,3 @@ function debuglog (set) { } return debugs[upperSet] } - -export default debuglog diff --git a/src/link.js b/src/link.js index 9062930..8c6d452 100644 --- a/src/link.js +++ b/src/link.js @@ -23,7 +23,7 @@ export const getSymlinks = (cwd, pkgJson) => { path.join(config.globalNodeModules, pkgJson.name, bin[name]), path.join(config.globalBin, name) ])) - return [libSymlink].concat(binSymlinks) + return [libSymlink, ...binSymlinks] } /* @@ -60,8 +60,9 @@ export const linkFromGlobal = (cwd, name) => { export const unlinkToGlobal = cwd => { const pkg = require(path.join(cwd, 'package.json')) const symlinks = getSymlinks(cwd, pkg) + return ArrayObservable.create(symlinks) - ::mergeMap(([src, dst]) => unlink(dst)) + ::mergeMap(link => unlink(link[1])) } /** diff --git a/src/local.js b/src/local.js index a8e37dd..e7e9053 100644 --- a/src/local.js +++ b/src/local.js @@ -1,9 +1,9 @@ +import path from 'path' import {EmptyObservable} from 'rxjs/observable/EmptyObservable' -import {map} from 'rxjs/operator/map' import {_catch} from 'rxjs/operator/catch' import {forkJoin} from 'rxjs/observable/forkJoin' +import {map} from 'rxjs/operator/map' import {readlink, readFile} from './util' -import path from 'path' const getLinkname = (dir, parentTarget, name) => path.join(dir, parentTarget, 'node_modules', name) diff --git a/src/progress.js b/src/progress.js index f9db564..0408c8a 100644 --- a/src/progress.js +++ b/src/progress.js @@ -1,8 +1,10 @@ import ora from 'ora' -const spinner = ora({ +const options = { spinner: 'clock' -}) +} + +const spinner = ora(options) let completed = 0 let added = 0 @@ -11,11 +13,23 @@ let status = '' // start the spinner on startup spinner.start() +/** + * log the progress by updating the status message, percentage and spinner. + * @param {String} [_status] - optional (updated) status message. defaults to + * the previous status message. + * @see https://www.npmjs.org/package/ora + */ +export const report = (_status = status) => { + status = _status + const progress = Math.round((completed / added) * 100 * 100) / 100 + spinner.text = `${progress}% ${status}` +} + /** * add one or more scheduled tasks. * @param {Number} [n=1] - number of scheduled tasks. */ -export function add (n = 1) { +export const add = (n = 1) => { added += n report() } @@ -25,20 +39,8 @@ export function add (n = 1) { * outstanding tasks. * @param {Number} [n=1] - number of tasks that have been completed. */ -export function complete (n = 1) { +export const complete = (n = 1) => { completed += n if (added === completed) spinner.stop() else report() } - -/** - * log the progress by updating the status message, percentage and spinner. - * @param {String} [_status] - optional (updated) status message. defaults to - * the previous status message. - * @see https://www.npmjs.org/package/ora - */ -export function report (_status = status) { - status = _status - const progress = Math.round((completed / added) * 100 * 100) / 100 - spinner.text = `${progress}% ${status}` -} diff --git a/src/registry.js b/src/registry.js index 6f4b563..d97ba07 100644 --- a/src/registry.js +++ b/src/registry.js @@ -50,8 +50,8 @@ export const getJson = (uri, options = {}) => { // The package root url is the base URL where a client can get top-level // information about a package and all of the versions known to the registry. -// A valid “package root url” response MUST be returned when the client requests -// {registry root url}/{package name}. +// A valid “package root url” response MUST be returned when the client +// requests {registry root url}/{package name}. // Ideally we would use the package version url, but npm's caching policy seems // a bit buggy in that regard. // Source: http://wiki.commonjs.org/wiki/Packages/Registry#package_root_url diff --git a/src/git.js b/src/todo/git.js similarity index 100% rename from src/git.js rename to src/todo/git.js diff --git a/src/todo/install.js b/src/todo/install.js new file mode 100644 index 0000000..ded33de --- /dev/null +++ b/src/todo/install.js @@ -0,0 +1,234 @@ +import crypto from 'crypto' +import path from 'path' +import url from 'url' +import {ArrayObservable} from 'rxjs/observable/ArrayObservable' +import {Observable} from 'rxjs/Observable' +import {concatStatic} from 'rxjs/operator/concat' +import {first} from 'rxjs/operator/first' +import {map} from 'rxjs/operator/map' +import {_catch} from 'rxjs/operator/catch' +import {mergeMap} from 'rxjs/operator/mergeMap' +import {skip} from 'rxjs/operator/skip' +import needle from 'needle' +import assert from 'assert' +import npa from 'npm-package-arg' +import memoize from 'lodash.memoize' + +import * as cache from './cache' +import * as config from './config' +import * as registry from './registry' +import * as local from './local' +import * as git from './git' +import * as util from './util' +import {normalizeBin} from './pkg_json' + +import debuglog from './debuglog' + +const log = debuglog('install') +const cachedNpa = memoize(npa) + +/** + * resolve a dependency's `package.json` file from a remote registry. + * @param {String} nodeModules - `node_modules` base directory. + * @param {String} parentTarget - relative parent's node_modules path. + * @param {String} name - name of the dependency. + * @param {String} version - version of the dependency. + * @return {Observable} - observable sequence of `package.json` objects. + */ + +export function resolveRemote (nodeModules, parentTarget, name, version) { + const source = `${name}@${version}` + log(`resolving ${source} from remote registry`) + + const parsedSpec = cachedNpa(source) + + switch (parsedSpec.type) { + case 'range': + case 'version': + case 'tag': + return registry.resolve(nodeModules, parentTarget, name, version, { + ...config.httpOptions, + registry: config.registry + }) + case 'remote': + return resolveFromTarball(nodeModules, parentTarget, parsedSpec) + case 'hosted': + return resolveFromHosted(nodeModules, parentTarget, parsedSpec) + case 'git': + return resolveFromGit(nodeModules, parentTarget, parsedSpec) + default: + throw new Error(`Unknown package spec: ${parsedSpec.type} for ${name}`) + } +} + +/** + * resolve a dependency's `package.json` file from an url tarball. + * @param {String} nodeModules - `node_modules` base directory. + * @param {String} parentTarget - relative parent's node_modules path. + * @param {Object} parsedSpec - parsed package name and specifier. + * @return {Observable} - observable sequence of `package.json` objects. + */ +export function resolveFromTarball (nodeModules, parentTarget, parsedSpec) { + const {raw, name, type, spec} = parsedSpec + log(`resolving ${raw} from tarball`) + return Observable.create((observer) => { + // create shasum from url for storage + const hash = crypto.createHash('sha1') + hash.update(raw) + const shasum = hash.digest('hex') + const pkgJson = {name, dist: {tarball: spec, shasum}} + log(`resolved ${raw} to uri shasum ${shasum} from tarball`) + observer.next({parentTarget, pkgJson, target: shasum, name, type, fetch}) + observer.complete() + }) +} + +/** + * resolve a dependency's `package.json` file from an hosted GitHub-like registry. + * @param {String} nodeModules - `node_modules` base directory. + * @param {String} parentTarget - relative parent's node_modules path. + * @param {Object} parsedSpec - parsed package name and specifier. + * @return {Observable} - observable sequence of `package.json` objects. + */ +export function resolveFromHosted (nodeModules, parentTarget, parsedSpec) { + const {raw, name, type, hosted} = parsedSpec + log(`resolving ${raw} from ${hosted.type}`) + + const [provider, shortcut] = hosted.shortcut.split(':') + const [repo, ref = 'master'] = shortcut.split('#') + + const options = {...config.httpOptions, retries: config.retries} + // create shasum from directUrl for storage + const hash = crypto.createHash('sha1') + hash.update(hosted.directUrl) + const shasum = hash.digest('hex') + + let tarball + switch (hosted.type) { + case 'github': + tarball = url.resolve('https://codeload.github.com', `${repo}/tar.gz/${ref}`) + break + case 'bitbucket': + tarball = url.resolve('https://bitbucket.org', `${repo}/get/${ref}.tar.gz`) + break + default: + throw new Error(`Unknown hosted type: ${hosted.type} for ${name}`) + } + + return registry.getJson(hosted.directUrl, options) + ::map(({body}) => JSON.parse(body)) + ::map(pkgJson => { + pkgJson.dist = {tarball, shasum} // eslint-disable-line no-param-reassign + log(`resolved ${name}@${ref} to directUrl shasum ${shasum} from ${provider}`) + return {parentTarget, pkgJson, target: shasum, name: pkgJson.name, type, fetch} + }) +} + +/** + * resolve a dependency's `package.json` file from a git endpoint. + * @param {String} nodeModules - `node_modules` base directory. + * @param {String} parentTarget - relative parent's node_modules path. + * @param {Object} parsedSpec - parsed package name and specifier. + * @return {Observable} - observable sequence of `package.json` objects. + */ +export function resolveFromGit (nodeModules, parentTarget, parsedSpec) { + const {raw, type, spec} = parsedSpec + log(`resolving ${raw} from git`) + + const [protocol, host] = spec.split('://') + const [repo, ref = 'master'] = host.split('#') + + // create shasum from spec for storage + const hash = crypto.createHash('sha1') + hash.update(spec) + const shasum = hash.digest('hex') + let repoPath + + return git.clone(repo, ref) + ::mergeMap(tmpDest => { + repoPath = tmpDest + return util.readFileJSON(path.resolve(tmpDest, 'package.json')) + }) + ::map(pkgJson => { + const name = pkgJson.name + pkgJson.dist = {shasum, path: repoPath} // eslint-disable-line no-param-reassign + log(`resolved ${name}@${ref} to spec shasum ${shasum} from git`) + return {parentTarget, pkgJson, target: shasum, name, type, fetch: git.extract} + }) +} + +export const checkShasum = (shasum, expected, tarball) => + void assert.equal(shasum, expected, + `shasum mismatch for ${tarball}: ${shasum} <-> ${expected}`) + +function download (tarball, expected, type) { + log(`downloading ${tarball}, expecting ${expected}`) + return Observable.create((observer) => { + const shasum = crypto.createHash('sha1') + const response = needle.get(tarball, config.httpOptions) + const cached = response.pipe(cache.write()) + + const errorHandler = (error) => observer.error(error) + const dataHandler = (chunk) => shasum.update(chunk) + const finishHandler = () => { + const actualShasum = shasum.digest('hex') + log(`downloaded ${actualShasum} into ${cached.path}`) + // only actually check shasum integrity for npm tarballs + const expectedShasum = ['range', 'version', 'tag'].indexOf(type) !== -1 ? + actualShasum : expected + observer.next({tmpPath: cached.path, shasum: expectedShasum}) + observer.complete() + } + + response.on('data', dataHandler) + response.on('error', errorHandler) + + cached.on('error', errorHandler) + cached.on('finish', finishHandler) + }) + ::mergeMap(({tmpPath, shasum}) => { + if (expected) { + checkShasum(shasum, expected, tarball) + } + + const newPath = path.join(config.cacheDir, shasum) + return util.rename(tmpPath, newPath) + }) +} + +function fixPermissions (target, bin) { + const execMode = 0o777 & (~process.umask()) + const paths = [] + const names = Object.keys(bin) + for (let i = 0; i < names.length; i++) { + const name = names[i] + paths.push(path.resolve(target, bin[name])) + } + log(`fixing persmissions of ${names} in ${target}`) + return ArrayObservable.create(paths) + ::mergeMap((filepath) => util.chmod(filepath, execMode)) +} + +export function fetch (nodeModules) { + const {target, type, pkgJson: {name, bin, dist: {tarball, shasum}}} = this + const where = path.join(nodeModules, target, 'package') + + log(`fetching ${tarball} into ${where}`) + + return util.stat(where)::skip(1)::_catch((error) => { + if (error.code !== 'ENOENT') { + throw error + } + const extracted = cache.extract(where, shasum)::_catch((error) => { // eslint-disable-line + if (error.code !== 'ENOENT') { + throw error + } + return concatStatic( + download(tarball, shasum, type), + cache.extract(where, shasum) + ) + }) + const fixedPermissions = fixPermissions(where, normalizeBin({name, bin})) + return concatStatic(extracted, fixedPermissions) + }) +} diff --git a/src/util.js b/src/util.js index 6051728..74c2ec6 100644 --- a/src/util.js +++ b/src/util.js @@ -1,8 +1,8 @@ -import {Observable} from 'rxjs/Observable' -import fs from 'fs' -import _mkdirp from 'mkdirp' import _forceSymlink from 'force-symlink' +import _mkdirp from 'mkdirp' +import fs from 'fs' import needle from 'needle' +import {Observable} from 'rxjs/Observable' import {map} from 'rxjs/operator/map' import {mergeMap} from 'rxjs/operator/mergeMap' diff --git a/src/version_cmd.js b/src/version_cmd.js index a827631..f2a2f9f 100644 --- a/src/version_cmd.js +++ b/src/version_cmd.js @@ -11,4 +11,4 @@ import {readFileJSON} from './util' export default () => readFileJSON(path.join(__dirname, '../package.json')) ::map(({version}) => version) - ::_do((version) => console.log(`ied version ${version}`)) + ::_do(version => console.log(`ied version ${version}`)) From 35b7948c1449960d2ff1e7b9ef47e0fd228ff78d Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Tue, 6 Sep 2016 03:36:10 +0100 Subject: [PATCH 09/30] WIP --- src/fetch.js | 87 ++++++++++++++++++ src/install.js | 234 ------------------------------------------------ src/registry.js | 8 +- 3 files changed, 91 insertions(+), 238 deletions(-) create mode 100644 src/fetch.js delete mode 100644 src/install.js diff --git a/src/fetch.js b/src/fetch.js new file mode 100644 index 0000000..6e4f5b2 --- /dev/null +++ b/src/fetch.js @@ -0,0 +1,87 @@ +import crypto from 'crypto' +import path from 'path' +import {ArrayObservable} from 'rxjs/observable/ArrayObservable' +import {Observable} from 'rxjs/Observable' +import {concatStatic} from 'rxjs/operator/concat' +import {_catch} from 'rxjs/operator/catch' +import {mergeMap} from 'rxjs/operator/mergeMap' +import {skip} from 'rxjs/operator/skip' +import needle from 'needle' +import assert from 'assert' + +import * as cache from './cache' +import * as config from './config' +import * as util from './util' +import {normalizeBin} from './pkg_json' + +export const checkShasum = (shasum, expected, tarball) => + void assert.equal(shasum, expected, + `shasum mismatch for ${tarball}: ${shasum} <-> ${expected}`) + +const download = (tarball, expected, type) => + Observable.create((observer) => { + const shasum = crypto.createHash('sha1') + const response = needle.get(tarball, config.httpOptions) + const cached = response.pipe(cache.write()) + + const errorHandler = (error) => observer.error(error) + const dataHandler = (chunk) => shasum.update(chunk) + const finishHandler = () => { + const actualShasum = shasum.digest('hex') + // only actually check shasum integrity for npm tarballs + const expectedShasum = ['range', 'version', 'tag'].indexOf(type) !== -1 ? + actualShasum : expected + observer.next({tmpPath: cached.path, shasum: expectedShasum}) + observer.complete() + } + + response.on('data', dataHandler) + response.on('error', errorHandler) + + cached.on('error', errorHandler) + cached.on('finish', finishHandler) + }) + ::mergeMap(({tmpPath, shasum}) => { + if (expected) { + checkShasum(shasum, expected, tarball) + } + + const newPath = path.join(config.cacheDir, shasum) + return util.rename(tmpPath, newPath) + }) + +const fixPermissions = (target, bin) => { + const execMode = 0o777 & (~process.umask()) + const paths = [] + const names = Object.keys(bin) + for (let i = 0; i < names.length; i++) { + const name = names[i] + paths.push(path.resolve(target, bin[name])) + } + return ArrayObservable.create(paths) + ::mergeMap(filepath => util.chmod(filepath, execMode)) +} + +export default function fetch (nodeModules) { + const {target, type, pkgJson: {name, bin, dist: {tarball, shasum}}} = this + const where = path.join(nodeModules, target, 'package') + + return util.stat(where)::skip(1)::_catch(err => { + if (err.code !== 'ENOENT') { + throw err + } + const extracted = cache.extract(where, shasum)::_catch(err2 => { + if (err2.code !== 'ENOENT') { + throw err2 + } + return concatStatic( + download(tarball, shasum, type), + cache.extract(where, shasum) + ) + }) + return concatStatic( + extracted, + fixPermissions(where, normalizeBin({name, bin})) + ) + }) +} diff --git a/src/install.js b/src/install.js deleted file mode 100644 index ded33de..0000000 --- a/src/install.js +++ /dev/null @@ -1,234 +0,0 @@ -import crypto from 'crypto' -import path from 'path' -import url from 'url' -import {ArrayObservable} from 'rxjs/observable/ArrayObservable' -import {Observable} from 'rxjs/Observable' -import {concatStatic} from 'rxjs/operator/concat' -import {first} from 'rxjs/operator/first' -import {map} from 'rxjs/operator/map' -import {_catch} from 'rxjs/operator/catch' -import {mergeMap} from 'rxjs/operator/mergeMap' -import {skip} from 'rxjs/operator/skip' -import needle from 'needle' -import assert from 'assert' -import npa from 'npm-package-arg' -import memoize from 'lodash.memoize' - -import * as cache from './cache' -import * as config from './config' -import * as registry from './registry' -import * as local from './local' -import * as git from './git' -import * as util from './util' -import {normalizeBin} from './pkg_json' - -import debuglog from './debuglog' - -const log = debuglog('install') -const cachedNpa = memoize(npa) - -/** - * resolve a dependency's `package.json` file from a remote registry. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {String} name - name of the dependency. - * @param {String} version - version of the dependency. - * @return {Observable} - observable sequence of `package.json` objects. - */ - -export function resolveRemote (nodeModules, parentTarget, name, version) { - const source = `${name}@${version}` - log(`resolving ${source} from remote registry`) - - const parsedSpec = cachedNpa(source) - - switch (parsedSpec.type) { - case 'range': - case 'version': - case 'tag': - return registry.resolve(nodeModules, parentTarget, name, version, { - ...config.httpOptions, - registry: config.registry - }) - case 'remote': - return resolveFromTarball(nodeModules, parentTarget, parsedSpec) - case 'hosted': - return resolveFromHosted(nodeModules, parentTarget, parsedSpec) - case 'git': - return resolveFromGit(nodeModules, parentTarget, parsedSpec) - default: - throw new Error(`Unknown package spec: ${parsedSpec.type} for ${name}`) - } -} - -/** - * resolve a dependency's `package.json` file from an url tarball. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {Object} parsedSpec - parsed package name and specifier. - * @return {Observable} - observable sequence of `package.json` objects. - */ -export function resolveFromTarball (nodeModules, parentTarget, parsedSpec) { - const {raw, name, type, spec} = parsedSpec - log(`resolving ${raw} from tarball`) - return Observable.create((observer) => { - // create shasum from url for storage - const hash = crypto.createHash('sha1') - hash.update(raw) - const shasum = hash.digest('hex') - const pkgJson = {name, dist: {tarball: spec, shasum}} - log(`resolved ${raw} to uri shasum ${shasum} from tarball`) - observer.next({parentTarget, pkgJson, target: shasum, name, type, fetch}) - observer.complete() - }) -} - -/** - * resolve a dependency's `package.json` file from an hosted GitHub-like registry. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {Object} parsedSpec - parsed package name and specifier. - * @return {Observable} - observable sequence of `package.json` objects. - */ -export function resolveFromHosted (nodeModules, parentTarget, parsedSpec) { - const {raw, name, type, hosted} = parsedSpec - log(`resolving ${raw} from ${hosted.type}`) - - const [provider, shortcut] = hosted.shortcut.split(':') - const [repo, ref = 'master'] = shortcut.split('#') - - const options = {...config.httpOptions, retries: config.retries} - // create shasum from directUrl for storage - const hash = crypto.createHash('sha1') - hash.update(hosted.directUrl) - const shasum = hash.digest('hex') - - let tarball - switch (hosted.type) { - case 'github': - tarball = url.resolve('https://codeload.github.com', `${repo}/tar.gz/${ref}`) - break - case 'bitbucket': - tarball = url.resolve('https://bitbucket.org', `${repo}/get/${ref}.tar.gz`) - break - default: - throw new Error(`Unknown hosted type: ${hosted.type} for ${name}`) - } - - return registry.getJson(hosted.directUrl, options) - ::map(({body}) => JSON.parse(body)) - ::map(pkgJson => { - pkgJson.dist = {tarball, shasum} // eslint-disable-line no-param-reassign - log(`resolved ${name}@${ref} to directUrl shasum ${shasum} from ${provider}`) - return {parentTarget, pkgJson, target: shasum, name: pkgJson.name, type, fetch} - }) -} - -/** - * resolve a dependency's `package.json` file from a git endpoint. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {Object} parsedSpec - parsed package name and specifier. - * @return {Observable} - observable sequence of `package.json` objects. - */ -export function resolveFromGit (nodeModules, parentTarget, parsedSpec) { - const {raw, type, spec} = parsedSpec - log(`resolving ${raw} from git`) - - const [protocol, host] = spec.split('://') - const [repo, ref = 'master'] = host.split('#') - - // create shasum from spec for storage - const hash = crypto.createHash('sha1') - hash.update(spec) - const shasum = hash.digest('hex') - let repoPath - - return git.clone(repo, ref) - ::mergeMap(tmpDest => { - repoPath = tmpDest - return util.readFileJSON(path.resolve(tmpDest, 'package.json')) - }) - ::map(pkgJson => { - const name = pkgJson.name - pkgJson.dist = {shasum, path: repoPath} // eslint-disable-line no-param-reassign - log(`resolved ${name}@${ref} to spec shasum ${shasum} from git`) - return {parentTarget, pkgJson, target: shasum, name, type, fetch: git.extract} - }) -} - -export const checkShasum = (shasum, expected, tarball) => - void assert.equal(shasum, expected, - `shasum mismatch for ${tarball}: ${shasum} <-> ${expected}`) - -function download (tarball, expected, type) { - log(`downloading ${tarball}, expecting ${expected}`) - return Observable.create((observer) => { - const shasum = crypto.createHash('sha1') - const response = needle.get(tarball, config.httpOptions) - const cached = response.pipe(cache.write()) - - const errorHandler = (error) => observer.error(error) - const dataHandler = (chunk) => shasum.update(chunk) - const finishHandler = () => { - const actualShasum = shasum.digest('hex') - log(`downloaded ${actualShasum} into ${cached.path}`) - // only actually check shasum integrity for npm tarballs - const expectedShasum = ['range', 'version', 'tag'].indexOf(type) !== -1 ? - actualShasum : expected - observer.next({tmpPath: cached.path, shasum: expectedShasum}) - observer.complete() - } - - response.on('data', dataHandler) - response.on('error', errorHandler) - - cached.on('error', errorHandler) - cached.on('finish', finishHandler) - }) - ::mergeMap(({tmpPath, shasum}) => { - if (expected) { - checkShasum(shasum, expected, tarball) - } - - const newPath = path.join(config.cacheDir, shasum) - return util.rename(tmpPath, newPath) - }) -} - -function fixPermissions (target, bin) { - const execMode = 0o777 & (~process.umask()) - const paths = [] - const names = Object.keys(bin) - for (let i = 0; i < names.length; i++) { - const name = names[i] - paths.push(path.resolve(target, bin[name])) - } - log(`fixing persmissions of ${names} in ${target}`) - return ArrayObservable.create(paths) - ::mergeMap((filepath) => util.chmod(filepath, execMode)) -} - -export function fetch (nodeModules) { - const {target, type, pkgJson: {name, bin, dist: {tarball, shasum}}} = this - const where = path.join(nodeModules, target, 'package') - - log(`fetching ${tarball} into ${where}`) - - return util.stat(where)::skip(1)::_catch((error) => { - if (error.code !== 'ENOENT') { - throw error - } - const extracted = cache.extract(where, shasum)::_catch((error) => { // eslint-disable-line - if (error.code !== 'ENOENT') { - throw error - } - return concatStatic( - download(tarball, shasum, type), - cache.extract(where, shasum) - ) - }) - const fixedPermissions = fixPermissions(where, normalizeBin({name, bin})) - return concatStatic(extracted, fixedPermissions) - }) -} diff --git a/src/registry.js b/src/registry.js index d97ba07..8ad15da 100644 --- a/src/registry.js +++ b/src/registry.js @@ -1,11 +1,11 @@ +import fetch from './fetch' import url from 'url' -import {map} from 'rxjs/operator/map' import {_do} from 'rxjs/operator/do' -import {publishReplay} from 'rxjs/operator/publishReplay' import {httpGet} from './util' -import {maxSatisfying} from 'semver' import {inherits} from 'util' -import {fetch} from './install' +import {map} from 'rxjs/operator/map' +import {maxSatisfying} from 'semver' +import {publishReplay} from 'rxjs/operator/publishReplay' // default registry to be used. export const REGISTRY = 'https://registry.npmjs.org/' From 5a5a2fd4e7c49e8095a66c8e4bc59374eaabce13 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Tue, 6 Sep 2016 03:48:27 +0100 Subject: [PATCH 10/30] WIP --- src/fetch.js | 15 +-------------- src/util.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/fetch.js b/src/fetch.js index 6e4f5b2..a65c572 100644 --- a/src/fetch.js +++ b/src/fetch.js @@ -1,6 +1,5 @@ import crypto from 'crypto' import path from 'path' -import {ArrayObservable} from 'rxjs/observable/ArrayObservable' import {Observable} from 'rxjs/Observable' import {concatStatic} from 'rxjs/operator/concat' import {_catch} from 'rxjs/operator/catch' @@ -50,18 +49,6 @@ const download = (tarball, expected, type) => return util.rename(tmpPath, newPath) }) -const fixPermissions = (target, bin) => { - const execMode = 0o777 & (~process.umask()) - const paths = [] - const names = Object.keys(bin) - for (let i = 0; i < names.length; i++) { - const name = names[i] - paths.push(path.resolve(target, bin[name])) - } - return ArrayObservable.create(paths) - ::mergeMap(filepath => util.chmod(filepath, execMode)) -} - export default function fetch (nodeModules) { const {target, type, pkgJson: {name, bin, dist: {tarball, shasum}}} = this const where = path.join(nodeModules, target, 'package') @@ -81,7 +68,7 @@ export default function fetch (nodeModules) { }) return concatStatic( extracted, - fixPermissions(where, normalizeBin({name, bin})) + util.fixPermissions(where, normalizeBin({name, bin})) ) }) } diff --git a/src/util.js b/src/util.js index 74c2ec6..c47a204 100644 --- a/src/util.js +++ b/src/util.js @@ -2,6 +2,8 @@ import _forceSymlink from 'force-symlink' import _mkdirp from 'mkdirp' import fs from 'fs' import needle from 'needle' +import path from 'path' +import {ArrayObservable} from 'rxjs/observable/ArrayObservable' import {Observable} from 'rxjs/Observable' import {map} from 'rxjs/operator/map' import {mergeMap} from 'rxjs/operator/mergeMap' @@ -109,3 +111,21 @@ export const setTitle = title => process.stdout.write( `${String.fromCharCode(27)}]0;${title}${String.fromCharCode(7)}` ) + +/** + * fix the permissions of a downloaded dependencies. + * @param {String]} target - target directory to resolve from. + * @param {Object} bin - `package.json` bin object. + * @return {Observable} - empty observable sequence. + */ +export const fixPermissions = (target, bin) => { + const execMode = 0o777 & (~process.umask()) + const paths = [] + const names = Object.keys(bin) + for (let i = 0; i < names.length; i++) { + const name = names[i] + paths.push(path.resolve(target, bin[name])) + } + return ArrayObservable.create(paths) + ::mergeMap(filepath => chmod(filepath, execMode)) +} From f380a2c6f92aae842aae1b94991a1dba38d2107a Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Wed, 7 Sep 2016 00:56:22 +0100 Subject: [PATCH 11/30] WIP --- src/build.js | 32 +++++++++++------------ src/shell_cmd.js | 6 ++++- src/util.js | 10 ++++++-- test/spec/config_cmd.spec.js | 14 +++++++--- test/spec/registry.spec.js | 8 +++--- test/spec/shell_cmd.spec.js | 19 ++++++++------ test/spec/util.spec.js | 50 ++++++++++++++++++++++++++---------- 7 files changed, 91 insertions(+), 48 deletions(-) diff --git a/src/build.js b/src/build.js index 4af968c..6b84382 100644 --- a/src/build.js +++ b/src/build.js @@ -2,18 +2,16 @@ import path from 'path' import {ArrayObservable} from 'rxjs/observable/ArrayObservable' import {Observable} from 'rxjs/Observable' import {_do} from 'rxjs/operator/do' -import {map} from 'rxjs/operator/map' import {concatMap} from 'rxjs/operator/concatMap' -import {mergeMap} from 'rxjs/operator/mergeMap' -import {filter} from 'rxjs/operator/filter' import {every} from 'rxjs/operator/every' +import {filter} from 'rxjs/operator/filter' +import {inherits} from 'util' +import {map} from 'rxjs/operator/map' +import {mergeMap} from 'rxjs/operator/mergeMap' import {spawn} from 'child_process' import * as config from './config' -import debuglog from './debuglog' -const log = debuglog('build') - // names of lifecycle scripts that should be run as part of the installation // process of a specific package (= properties of `scripts` object in // `package.json`). @@ -23,13 +21,14 @@ export const LIFECYCLE_SCRIPTS = [ 'postinstall' ] + // error class used for representing an error that occurs due to a lifecycle // script that exits with a non-zero status code. -export class FailedBuildError extends Error { - constructor () { - super('failed to build one or more dependencies that exited with != 0') - this.name = FailedBuildError - } +inherits(FailedBuildError, Error) +export function FailedBuildError () { + Error.captureStackTrace(this, this.constructor) + this.name = 'FailedBuildError' + this.message = 'failed to build dependencies' } /** @@ -42,7 +41,6 @@ export class FailedBuildError extends Error { */ export const build = nodeModules => dep => { const {target, script} = dep - log(`executing "${script}" from ${target}`) return Observable.create((observer) => { // some packages do expect a defined `npm_execpath` env @@ -97,8 +95,10 @@ export function parseLifecycleScripts ({target, pkgJson: {scripts = {}}}) { export const buildAll = nodeModules => o => o ::map(parseLifecycleScripts) - ::mergeMap((scripts) => ArrayObservable.create(scripts)) + ::mergeMap(scripts => ArrayObservable.create(scripts)) ::concatMap(build(nodeModules)) - ::every((code) => code === 0) - ::filter((ok) => !ok) - ::_do(() => { throw new FailedBuildError() }) + ::every(code => code === 0) + ::filter(ok => !ok) + ::_do(() => { + throw new FailedBuildError() + }) diff --git a/src/shell_cmd.js b/src/shell_cmd.js index 2cb7643..f443309 100644 --- a/src/shell_cmd.js +++ b/src/shell_cmd.js @@ -14,8 +14,12 @@ export default config => cwd => { ...process.env, PATH: [binPath, process.env.PATH].join(path.delimiter) } + const options = { + stdio: 'inherit', + env + } return readdir(binPath) ::_do(cmds => console.log('added: \n\t', cmds.join('\n\t'))) - ::map(() => spawn(config.sh, [], {stdio: 'inherit', env})) + ::map(() => spawn(config.sh, [], options)) } diff --git a/src/util.js b/src/util.js index c47a204..2b14495 100644 --- a/src/util.js +++ b/src/util.js @@ -83,7 +83,7 @@ export const mkdirp = createObservableFactory(_mkdirp) * @return {Observable} - observable sequence of pairs. */ export function entries () { - return this::mergeMap((object) => { + return this::mergeMap(object => { const results = [] const keys = Object.keys(object) for (let i = 0; i < keys.length; i++) { @@ -112,6 +112,12 @@ export const setTitle = title => `${String.fromCharCode(27)}]0;${title}${String.fromCharCode(7)}` ) +/** + * file permissions for executable bin files. + * @type {Number} + */ +const execMode = 0o777 & (~process.umask()) + /** * fix the permissions of a downloaded dependencies. * @param {String]} target - target directory to resolve from. @@ -119,9 +125,9 @@ export const setTitle = title => * @return {Observable} - empty observable sequence. */ export const fixPermissions = (target, bin) => { - const execMode = 0o777 & (~process.umask()) const paths = [] const names = Object.keys(bin) + for (let i = 0; i < names.length; i++) { const name = names[i] paths.push(path.resolve(target, bin[name])) diff --git a/test/spec/config_cmd.spec.js b/test/spec/config_cmd.spec.js index 0c40c64..87b323e 100644 --- a/test/spec/config_cmd.spec.js +++ b/test/spec/config_cmd.spec.js @@ -1,15 +1,23 @@ -import sinon from 'sinon' -import * as config from '../../src/config' import configCmd from '../../src/config_cmd' +import sinon from 'sinon' describe('configCmd', () => { const sandbox = sinon.sandbox.create() + let config afterEach(() => sandbox.restore()) + beforeEach(() => { + config = { + 'key-0': 'value-0', + 'key-1': 'value-1', + 'key-2': 'value-2' + } + }) + it('should print all config variables', () => { sandbox.stub(console, 'log') - configCmd() + configCmd(config)() Object.keys(config).forEach(key => { sinon.assert.calledWith(console.log, sinon.match(key)) sinon.assert.calledWith(console.log, sinon.match(String(config[key]))) diff --git a/test/spec/registry.spec.js b/test/spec/registry.spec.js index 4a13e8b..00be783 100644 --- a/test/spec/registry.spec.js +++ b/test/spec/registry.spec.js @@ -1,10 +1,8 @@ -import sinon from 'sinon' -import url from 'url' -import assert from 'assert' -import {EmptyObservable} from 'rxjs/observable/EmptyObservable' import * as registry from '../../src/registry' +import assert from 'assert' +import sinon from 'sinon' -describe('registry', () => { +describe.only('registry', () => { const sandbox = sinon.sandbox.create() afterEach(() => sandbox.restore()) diff --git a/test/spec/shell_cmd.spec.js b/test/spec/shell_cmd.spec.js index 1462346..ec1be66 100644 --- a/test/spec/shell_cmd.spec.js +++ b/test/spec/shell_cmd.spec.js @@ -1,24 +1,27 @@ import assert from 'assert' -import sinon from 'sinon' -import shellCmd from '../../src/shell_cmd' import childProcess from 'child_process' -import * as config from '../../src/config' -import * as util from '../../src/util' +import sinon from 'sinon' import {ScalarObservable} from 'rxjs/observable/ScalarObservable' +import * as util from '../../src/util' +import shellCmd from '../../src/shell_cmd' + describe('shellCmd', () => { const sandbox = sinon.sandbox.create() + let config + afterEach(() => sandbox.restore()) beforeEach(() => { + config = {sh: 'sh'} sandbox.stub(childProcess, 'spawn') sandbox.stub(util, 'readdir') - sandbox.spy(console, 'log') + sandbox.stub(console, 'log') }) it('should spawn child process', () => { util.readdir.returns(ScalarObservable.create([])) - shellCmd('/cwd').subscribe() + shellCmd(config)('/cwd').subscribe() sinon.assert.calledOnce(childProcess.spawn) sinon.assert.calledWith(childProcess.spawn, config.sh, [], { @@ -32,7 +35,7 @@ describe('shellCmd', () => { it('should add node_modules/.bin to PATH', () => { util.readdir.returns(ScalarObservable.create([])) - shellCmd('/cwd').subscribe() + shellCmd(config)('/cwd').subscribe() const {env: {PATH}} = childProcess.spawn.getCall(0).args[2] assert.equal(PATH.indexOf('/cwd/node_modules/.bin:'), 0) @@ -41,7 +44,7 @@ describe('shellCmd', () => { it('should log available commands', () => { const cmds = ['browserify', 'tape', 'npm'] util.readdir.returns(ScalarObservable.create(cmds)) - shellCmd('/cwd').subscribe() + shellCmd(config)('/cwd').subscribe() const out = console.log.getCall(0).args.join(' ') for (const cmd of cmds) { assert.notEqual(out.indexOf(cmd), -1, `should log ${cmd}`) diff --git a/test/spec/util.spec.js b/test/spec/util.spec.js index 9eafc1a..755eace 100644 --- a/test/spec/util.spec.js +++ b/test/spec/util.spec.js @@ -1,9 +1,12 @@ -import sinon from 'sinon' import assert from 'assert' +import sinon from 'sinon' +import {ScalarObservable} from 'rxjs/observable/ScalarObservable' + import * as util from '../../src/util' describe('util', () => { const sandbox = sinon.sandbox.create() + afterEach(() => sandbox.restore()) describe('createObservableFactory', () => { @@ -42,65 +45,86 @@ describe('util', () => { }) }) - describe('readFile', () => { - it('should be an exporter function', () => { + it('should be an exported function', () => { assert.equal(typeof util.readFile, 'function') }) }) describe('writeFile', () => { - it('should be an exporter function', () => { + it('should be an exported function', () => { assert.equal(typeof util.writeFile, 'function') }) }) describe('stat', () => { - it('should be an exporter function', () => { + it('should be an exported function', () => { assert.equal(typeof util.stat, 'function') }) }) describe('rename', () => { - it('should be an exporter function', () => { + it('should be an exported function', () => { assert.equal(typeof util.rename, 'function') }) }) describe('readlink', () => { - it('should be an exporter function', () => { + it('should be an exported function', () => { assert.equal(typeof util.readlink, 'function') }) }) describe('chmod', () => { - it('should be an exporter function', () => { + it('should be an exported function', () => { assert.equal(typeof util.chmod, 'function') }) }) describe('forceSymlink', () => { - it('should be an exporter function', () => { + it('should be an exported function', () => { assert.equal(typeof util.forceSymlink, 'function') }) }) describe('mkdirp', () => { - it('should be an exporter function', () => { + it('should be an exported function', () => { assert.equal(typeof util.mkdirp, 'function') }) }) + describe('entries', () => { + it('should split an object into pairs', () => { + const object = ScalarObservable.create({ + a: 1, + b: 2, + c: 3 + })::util.entries() + + const next = sandbox.spy() + const error = sandbox.spy() + const complete = sandbox.spy() + + object.subscribe(next, error, complete) + sinon.assert.calledOnce(complete) + sinon.assert.notCalled(error) + sinon.assert.callCount(next, 3) + sinon.assert.calledWith(next, ['a', 1]) + sinon.assert.calledWith(next, ['b', 2]) + sinon.assert.calledWith(next, ['c', 3]) + }) + }) + describe('setTitle', () => { it('should set terminal title', () => { const title = 'some title' sandbox.stub(process.stdout, 'write') util.setTitle(title) - const exepctedTitle = `${String.fromCharCode(27)}]0;${title}${String.fromCharCode(7)}` + const exepectedTitle = `${String.fromCharCode(27)}]0;${title}${String.fromCharCode(7)}` sinon.assert.calledOnce(process.stdout.write) - sinon.assert.calledWithExactly(process.stdout.write, exepctedTitle) + sinon.assert.calledWithExactly(process.stdout.write, exepectedTitle) - // otherwise it won't be printed + // otherwise nothing will be logged. sandbox.restore() }) }) From f9516c0acbdb14c27e713126d0433644e7fc096e Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Wed, 7 Sep 2016 02:01:52 +0100 Subject: [PATCH 12/30] WIP --- src/cache.js | 34 +++++++++++++++------------- src/cmd.js | 45 ++++++++++++++++++++++---------------- src/fetch.js | 44 ++++++++++++++++--------------------- src/link_all.js | 10 ++++----- test/mocha.opts | 1 + test/spec/registry.spec.js | 2 +- 6 files changed, 70 insertions(+), 66 deletions(-) diff --git a/src/cache.js b/src/cache.js index 2759fd5..0710c6e 100644 --- a/src/cache.js +++ b/src/cache.js @@ -42,6 +42,10 @@ export const write = () => export const read = id => fs.createReadStream(path.join(config.cacheDir, id)) +const extractOptions = { + strip: 1 +} + /** * extract a dependency from the cache. * @param {String} dest - pathname into which the cached dependency should be @@ -50,28 +54,28 @@ export const read = id => * @return {Observable} - observable sequence that will be completed once * the cached dependency has been fetched. */ -export function extract (dest, id) { - return Observable.create((observer) => { +export const extract = (dest, id) => + Observable.create(observer => { const tmpDest = getTmp() - const untar = tar.extract(tmpDest, {strip: 1}) + const untar = tar.extract(tmpDest, extractOptions) - const completeHandler = () => { + const finishHandler = () => { observer.next(tmpDest) observer.complete() } const errorHandler = err => observer.error(err) - this.read(id).on('error', errorHandler) + read(id).on('error', errorHandler) .pipe(gunzip()).on('error', errorHandler) .pipe(untar).on('error', errorHandler) - .on('finish', completeHandler) + .on('finish', finishHandler) }) - ::mergeMap(tmpDest => util.rename(tmpDest, dest) - ::retryWhen(errors => errors::mergeMap(err => { - switch (err.code) { - case 'ENOENT': return util.mkdirp(path.dirname(dest)) - default: throw err - } - })) - ) -} + ::mergeMap(tmpDest => + util.rename(tmpDest, dest) + ::retryWhen(errors => errors::mergeMap(err => { + switch (err.code) { + case 'ENOENT': return util.mkdirp(path.dirname(dest)) + default: throw err + } + })) + ) diff --git a/src/cmd.js b/src/cmd.js index 80a58c7..d35d73a 100755 --- a/src/cmd.js +++ b/src/cmd.js @@ -3,10 +3,6 @@ import minimist from 'minimist' import * as config from './config' -if (['development', 'test'].indexOf(process.env.NODE_ENV) !== -1) { - require('source-map-support').install() -} - const alias = { h: 'help', v: 'version', @@ -33,7 +29,12 @@ const boolean = [ ] const cwd = process.cwd() -const argv = minimist(process.argv.slice(2), {alias, string, boolean}) + +const argv = minimist(process.argv.slice(2), { + alias, + string, + boolean +}) if (argv.registry) { config.registry = argv.registry @@ -51,19 +52,13 @@ let helpCmd let versionCmd let cacheCmd -(() => { - if (argv.help) { - helpCmd = require('./help_cmd').default - helpCmd().subscribe() - return - } - - if (argv.version) { - versionCmd = require('./version_cmd').default - versionCmd().subscribe() - return - } - +if (argv.help) { + helpCmd = require('./help_cmd').default + helpCmd().subscribe() +} else if (argv.version) { + versionCmd = require('./version_cmd').default + versionCmd().subscribe() +} else { const [subCommand] = argv._ switch (subCommand) { @@ -72,17 +67,20 @@ let cacheCmd installCmd = require('./install_cmd').default(config) installCmd(cwd, argv).subscribe() break + case 'sh': case 'shell': shellCmd = require('./shell_cmd').default(config) shellCmd(cwd).subscribe() break + case 'r': case 'run': case 'run-script': runCmd = require('./run_cmd').default runCmd(cwd, argv).subscribe() break + case 't': case 'test': case 'start': @@ -91,37 +89,46 @@ let cacheCmd runCmd = require('./run_cmd').default runCmd(cwd, {...argv, _: ['run'].concat(argv._)}).subscribe() break + case 'ping': pingCmd = require('./ping_cmd').default(config) pingCmd().subscribe() break + case 'conf': case 'config': configCmd = require('./config_cmd').default(config) configCmd() break + case 'init': initCmd = require('./init_cmd').default(config) initCmd(cwd, argv).subscribe() break + case 'link': linkCmd = require('./link_cmd').default(config) linkCmd(cwd, argv).subscribe() break + case 'unlink': unlinkCmd = require('./unlink_cmd').default unlinkCmd(cwd, argv).subscribe() break + case 'cache': cacheCmd = require('./cache_cmd').default(config) cacheCmd(cwd, argv) break + case 'version': versionCmd = require('./version_cmd').default versionCmd().subscribe() break + default: helpCmd = require('./help_cmd').default helpCmd().subscribe() } -})() +} + diff --git a/src/fetch.js b/src/fetch.js index a65c572..6380285 100644 --- a/src/fetch.js +++ b/src/fetch.js @@ -1,12 +1,12 @@ +import assert from 'assert' import crypto from 'crypto' +import needle from 'needle' import path from 'path' import {Observable} from 'rxjs/Observable' -import {concatStatic} from 'rxjs/operator/concat' import {_catch} from 'rxjs/operator/catch' +import {concatStatic} from 'rxjs/operator/concat' +import {ignoreElements} from 'rxjs/operator/ignoreElements' import {mergeMap} from 'rxjs/operator/mergeMap' -import {skip} from 'rxjs/operator/skip' -import needle from 'needle' -import assert from 'assert' import * as cache from './cache' import * as config from './config' @@ -18,13 +18,13 @@ export const checkShasum = (shasum, expected, tarball) => `shasum mismatch for ${tarball}: ${shasum} <-> ${expected}`) const download = (tarball, expected, type) => - Observable.create((observer) => { + Observable.create(observer => { const shasum = crypto.createHash('sha1') const response = needle.get(tarball, config.httpOptions) const cached = response.pipe(cache.write()) - const errorHandler = (error) => observer.error(error) - const dataHandler = (chunk) => shasum.update(chunk) + const errorHandler = error => observer.error(error) + const dataHandler = chunk => shasum.update(chunk) const finishHandler = () => { const actualShasum = shasum.digest('hex') // only actually check shasum integrity for npm tarballs @@ -41,9 +41,7 @@ const download = (tarball, expected, type) => cached.on('finish', finishHandler) }) ::mergeMap(({tmpPath, shasum}) => { - if (expected) { - checkShasum(shasum, expected, tarball) - } + if (expected) checkShasum(shasum, expected, tarball) const newPath = path.join(config.cacheDir, shasum) return util.rename(tmpPath, newPath) @@ -51,24 +49,20 @@ const download = (tarball, expected, type) => export default function fetch (nodeModules) { const {target, type, pkgJson: {name, bin, dist: {tarball, shasum}}} = this - const where = path.join(nodeModules, target, 'package') + const packageDir = path.join(nodeModules, target, 'package') - return util.stat(where)::skip(1)::_catch(err => { - if (err.code !== 'ENOENT') { - throw err - } - const extracted = cache.extract(where, shasum)::_catch(err2 => { - if (err2.code !== 'ENOENT') { - throw err2 - } + return util.stat(packageDir) + ::_catch(err => { + if (err.code !== 'ENOENT') throw err + return cache.extract(packageDir, shasum) + }) + ::_catch(err => { + if (err.code !== 'ENOENT') throw err return concatStatic( download(tarball, shasum, type), - cache.extract(where, shasum) + cache.extract(packageDir, shasum), + util.fixPermissions(packageDir, normalizeBin({name, bin})) ) }) - return concatStatic( - extracted, - util.fixPermissions(where, normalizeBin({name, bin})) - ) - }) + ::ignoreElements() } diff --git a/src/link_all.js b/src/link_all.js index a0af6bf..eb2cd01 100644 --- a/src/link_all.js +++ b/src/link_all.js @@ -1,14 +1,13 @@ import path from 'path' +import {forceSymlink} from './util' import {map} from 'rxjs/operator/map' import {mergeMap} from 'rxjs/operator/mergeMap' -import {forceSymlink} from './util' import {normalizeBin} from './pkg_json' const resolveSymlink = (src, dst) => [path.relative(path.dirname(dst), src), dst] -const getBinLinks = dep => { - const {pkgJson, parentTarget, target} = dep +const getBinLinks = ({pkgJson, parentTarget, target}) => { const binLinks = [] const bin = normalizeBin(pkgJson) const names = Object.keys(bin) @@ -21,8 +20,7 @@ const getBinLinks = dep => { return binLinks } -const getDirectLink = dep => { - const {parentTarget, target, name} = dep +const getDirectLink = ({parentTarget, target, name}) => { const src = path.join('node_modules', target, 'package') const dst = path.join('node_modules', parentTarget, 'node_modules', name) return [src, dst] @@ -30,7 +28,7 @@ const getDirectLink = dep => { export default function linkAll () { return this - ::mergeMap((dep) => [getDirectLink(dep), ...getBinLinks(dep)]) + ::mergeMap(dep => [getDirectLink(dep), ...getBinLinks(dep)]) ::map(([src, dst]) => resolveSymlink(src, dst)) ::mergeMap(([src, dst]) => forceSymlink(src, dst)) } diff --git a/test/mocha.opts b/test/mocha.opts index 017a516..9522037 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,4 @@ --ui bdd --compilers js:babel-register --recursive +--require source-map-support/register diff --git a/test/spec/registry.spec.js b/test/spec/registry.spec.js index 00be783..3ffa816 100644 --- a/test/spec/registry.spec.js +++ b/test/spec/registry.spec.js @@ -2,7 +2,7 @@ import * as registry from '../../src/registry' import assert from 'assert' import sinon from 'sinon' -describe.only('registry', () => { +describe('registry', () => { const sandbox = sinon.sandbox.create() afterEach(() => sandbox.restore()) From 91867361de6cd6caa8c8653c646fc1d1774dfe50 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Wed, 7 Sep 2016 02:51:24 +0100 Subject: [PATCH 13/30] WIP --- src/install_cmd.js | 17 ++++++++++++++--- src/{ => todo}/build.js | 0 2 files changed, 14 insertions(+), 3 deletions(-) rename src/{ => todo}/build.js (100%) diff --git a/src/install_cmd.js b/src/install_cmd.js index 1fbecc5..4defd59 100644 --- a/src/install_cmd.js +++ b/src/install_cmd.js @@ -1,5 +1,6 @@ import path from 'path' +import {EmptyObservable} from 'rxjs/observable/EmptyObservable' import {concatStatic} from 'rxjs/operator/concat' import {map} from 'rxjs/operator/map' import {mergeStatic} from 'rxjs/operator/merge' @@ -9,7 +10,7 @@ import {skip} from 'rxjs/operator/skip' import fetchAll from './fetch_all' import linkAll from './link_all' import resolveAll from './resolve_all' -import {fromArgv, fromFs} from './pkg_json' +import {fromArgv, fromFs, save} from './pkg_json' import {init as initCache} from './cache' function installAll (dir) { @@ -24,6 +25,9 @@ const parseArgv = ({_, production}) => ({ isProd: production }) +const shouldSave = argv => + !!(argv.save || argv['save-dev'] || argv['save-optional']) + export default config => (cwd, argv) => { const {isExplicit, isProd} = parseArgv(argv) const dir = path.join(cwd, 'node_modules') @@ -31,7 +35,13 @@ export default config => (cwd, argv) => { // generate the "source" package.json file from which dependencies are being // parsed and installed. - const srcPkgJson = isExplicit ? fromArgv(cwd, argv) : fromFs(cwd) + const srcPkgJson = isExplicit + ? fromArgv(cwd, argv) + : fromFs(cwd) + + const savedPkgJson = shouldSave(argv) + ? srcPkgJson::save(cwd) + : EmptyObservable.create() const installedAll = srcPkgJson ::map(pkgJson => ({ @@ -47,6 +57,7 @@ export default config => (cwd, argv) => { return concatStatic( initCache(), - installedAll + installedAll, + savedPkgJson ) } diff --git a/src/build.js b/src/todo/build.js similarity index 100% rename from src/build.js rename to src/todo/build.js From 5de5a96b493c12ec51b5d5e87a21bbedf59781a8 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Thu, 8 Sep 2016 01:17:40 +0100 Subject: [PATCH 14/30] Add back buildAll --- src/{todo/build.js => build_all.js} | 21 +++++++++------------ src/cmd.js | 7 +++++-- src/install_cmd.js | 21 +++++++++++++++------ src/registry.js | 2 +- src/run_cmd.js | 14 ++++---------- src:_install_cmd.js | 21 --------------------- test/spec/cache.spec.js | 8 ++++---- test/spec/registry.spec.js | 4 ++-- 8 files changed, 40 insertions(+), 58 deletions(-) rename src/{todo/build.js => build_all.js} (86%) delete mode 100644 src:_install_cmd.js diff --git a/src/todo/build.js b/src/build_all.js similarity index 86% rename from src/todo/build.js rename to src/build_all.js index 6b84382..857cf9f 100644 --- a/src/todo/build.js +++ b/src/build_all.js @@ -21,7 +21,6 @@ export const LIFECYCLE_SCRIPTS = [ 'postinstall' ] - // error class used for representing an error that occurs due to a lifecycle // script that exits with a non-zero status code. inherits(FailedBuildError, Error) @@ -39,12 +38,10 @@ export function FailedBuildError () { * @param {String} dep.script - script to be executed (usually using `sh`). * @return {Observable} - observable sequence of the returned exit code. */ -export const build = nodeModules => dep => { - const {target, script} = dep - - return Observable.create((observer) => { - // some packages do expect a defined `npm_execpath` env - // eg. https://github.com/chrisa/node-dtrace-provider/blob/v0.6.0/scripts/install.js#L19 +export const build = nodeModules => ({target, script}) => + Observable.create(observer => { + // some packages do expect a defined `npm_execpath` env, e.g. + // https://github.com/chrisa/node-dtrace-provider/blob/v0.6.0/scripts/install.js#L19 const env = {npm_execpath: '', ...process.env} env.PATH = [ @@ -57,7 +54,6 @@ export const build = nodeModules => dep => { cwd: path.join(nodeModules, target, 'package'), env, stdio: 'inherit' - // shell: true // does break `dtrace-provider@0.6.0` build }) childProcess.on('error', (error) => { observer.error(error) @@ -67,14 +63,13 @@ export const build = nodeModules => dep => { observer.complete() }) }) -} /** * extract lifecycle scripts from supplied dependency. * @param {Dep} dep - dependency to be parsed. * @return {Array.} - array of script targets to be executed. */ -export function parseLifecycleScripts ({target, pkgJson: {scripts = {}}}) { +export const parseLifecycleScripts = ({target, pkgJson: {scripts = {}}}) => { const results = [] for (let i = 0; i < LIFECYCLE_SCRIPTS.length; i++) { const name = LIFECYCLE_SCRIPTS[i] @@ -92,13 +87,15 @@ export function parseLifecycleScripts ({target, pkgJson: {scripts = {}}}) { * @return {Observable} - empty observable sequence that will be completed once * all lifecycle scripts have been executed. */ -export const buildAll = nodeModules => o => - o +export default function buildAll (nodeModules) { + return this ::map(parseLifecycleScripts) ::mergeMap(scripts => ArrayObservable.create(scripts)) + // build dependencies sequentially. ::concatMap(build(nodeModules)) ::every(code => code === 0) ::filter(ok => !ok) ::_do(() => { throw new FailedBuildError() }) +} diff --git a/src/cmd.js b/src/cmd.js index d35d73a..f470ae2 100755 --- a/src/cmd.js +++ b/src/cmd.js @@ -78,16 +78,19 @@ if (argv.help) { case 'run': case 'run-script': runCmd = require('./run_cmd').default - runCmd(cwd, argv).subscribe() + runCmd(config)(cwd, argv) + .subscribe(process.exit) break case 't': case 'test': case 'start': + case 'lint': case 'build': case 'stop': runCmd = require('./run_cmd').default - runCmd(cwd, {...argv, _: ['run'].concat(argv._)}).subscribe() + runCmd(config)(cwd, {...argv, _: ['run', ...argv._]}) + .subscribe(process.exit) break case 'ping': diff --git a/src/install_cmd.js b/src/install_cmd.js index 4defd59..9758d1f 100644 --- a/src/install_cmd.js +++ b/src/install_cmd.js @@ -7,16 +7,22 @@ import {mergeStatic} from 'rxjs/operator/merge' import {publishReplay} from 'rxjs/operator/publishReplay' import {skip} from 'rxjs/operator/skip' +import buildAll from './build_all' import fetchAll from './fetch_all' import linkAll from './link_all' import resolveAll from './resolve_all' import {fromArgv, fromFs, save} from './pkg_json' import {init as initCache} from './cache' -function installAll (dir) { - return mergeStatic( - this::linkAll(), - this::fetchAll(dir) +function installAll (dir, shouldBuild) { + return concatStatic( + mergeStatic( + this::linkAll(), + this::fetchAll(dir) + ), + shouldBuild + ? this::buildAll(dir) + : EmptyObservable.create() ) } @@ -26,7 +32,10 @@ const parseArgv = ({_, production}) => ({ }) const shouldSave = argv => - !!(argv.save || argv['save-dev'] || argv['save-optional']) + argv.save || argv['save-dev'] || argv['save-optional'] + +const shouldBuild = argv => + argv.build export default config => (cwd, argv) => { const {isExplicit, isProd} = parseArgv(argv) @@ -53,7 +62,7 @@ export default config => (cwd, argv) => { ::resolveAll(dir, config) ::skip(1) ::publishReplay().refCount() - ::installAll(dir) + ::installAll(dir, shouldBuild(argv)) return concatStatic( initCache(), diff --git a/src/registry.js b/src/registry.js index 8ad15da..f2c3140 100644 --- a/src/registry.js +++ b/src/registry.js @@ -28,7 +28,7 @@ const isScoped = name => // escape the given package name, which can then be used as part of the package // root URL. -const escapeName = name => ( +export const escapeName = name => ( isScoped(name) ? `@${encodeURIComponent(name.substr(1))}` : encodeURIComponent(name) diff --git a/src/run_cmd.js b/src/run_cmd.js index e8c47b8..05bbef4 100644 --- a/src/run_cmd.js +++ b/src/run_cmd.js @@ -1,4 +1,3 @@ -import assert from 'assert' import path from 'path' import {Observable} from 'rxjs' import {_do} from 'rxjs/operator/do' @@ -8,7 +7,6 @@ import {filter} from 'rxjs/operator/filter' import {fromFs} from './pkg_json' import {map} from 'rxjs/operator/map' import {reduce} from 'rxjs/operator/reduce' -import {sh, shFlag} from './config' import {spawn} from 'child_process' const logCode = ([code, name, script]) => { @@ -32,8 +30,8 @@ const logAvailable = ({scripts = {}}) => { } } -export const run = (script, options = {}) => - Observable.create((observer) => { +export const run = (sh, shFlag, script, options = {}) => + Observable.create(observer => { const args = [shFlag, script] const childProcess = spawn(sh, args, options) childProcess.on('close', (code) => { @@ -47,11 +45,8 @@ export const run = (script, options = {}) => /** * run a `package.json` script and the related pre- and postscript. - * @param {String} cwd - current working directory. - * @param {Object} argv - command line arguments. - * @return {Observable} - observable sequence. */ -export default (cwd, argv) => { +export default ({sh, shFlag}) => (cwd, argv) => { const scriptNames = argv._.slice(1) const pkgJson = fromFs(cwd) @@ -72,10 +67,9 @@ export default (cwd, argv) => { ::entries() ::filter(([name]) => ~scriptNames.indexOf(name)) ::concatMap(([name, script]) => - run(script, runOptions) + run(sh, shFlag, script, runOptions) ::map(code => ([code, name, script])) ) ::_do(logCode) ::reduce((codes, [code]) => codes + code, 0) - ::_do(code => assert.equal(code, 0, 'exit status != 0')) } diff --git a/src:_install_cmd.js b/src:_install_cmd.js deleted file mode 100644 index 865884d..0000000 --- a/src:_install_cmd.js +++ /dev/null @@ -1,21 +0,0 @@ - -// import {EmptyObservable} from 'rxjs/observable/EmptyObservable' - -// import {fromArgv, fromFs, save} from './pkg_json' -// import {buildAll} from './build' - -// return concatStatic( -// initialized, -// installedAll -// // saved, -// // builtAll -// ) - -// const builtAll = argv.build -// ? resolvedAll.lift(buildAll(nodeModules)) -// : EmptyObservable.create() - -// const shouldSave = argv.save || argv['save-dev'] || argv['save-optional'] -// const saved = shouldSave -// ? updatedPkgJSONs::save(cwd) -// : EmptyObservable.create() diff --git a/test/spec/cache.spec.js b/test/spec/cache.spec.js index bd19e5c..b4d3a15 100644 --- a/test/spec/cache.spec.js +++ b/test/spec/cache.spec.js @@ -72,14 +72,14 @@ describe('cache', () => { context('when cache.read read stream emits an error', () => { it('should throw an error', () => { const readStream = new stream.Readable() - sandbox.stub(cache, 'read').returns(readStream) + sandbox.stub(fs, 'createReadStream').returns(readStream) const expectedError = new Error() const next = sandbox.stub() const error = sandbox.stub() const complete = sandbox.stub() - cache.extract().subscribe(next, error, complete) + cache.extract('/dest', 'id').subscribe(next, error, complete) readStream.emit('error', expectedError) sinon.assert.notCalled(next) @@ -92,7 +92,7 @@ describe('cache', () => { context('when tar.extract read stream emits an error', () => { it('should thorw an error', () => { const readStream = new stream.Readable() - sandbox.stub(cache, 'read').returns(new stream.Readable()) + sandbox.stub(fs, 'createReadStream').returns(readStream) sandbox.stub(tar, 'extract').returns(readStream) const expectedError = new Error() @@ -101,7 +101,7 @@ describe('cache', () => { const error = sandbox.stub() const complete = sandbox.stub() - cache.extract().subscribe(next, error, complete) + cache.extract('/dest', 'id').subscribe(next, error, complete) readStream.emit('error', expectedError) sinon.assert.notCalled(next) diff --git a/test/spec/registry.spec.js b/test/spec/registry.spec.js index 3ffa816..f0abda7 100644 --- a/test/spec/registry.spec.js +++ b/test/spec/registry.spec.js @@ -28,14 +28,14 @@ describe('registry', () => { it('should throw an error', () => { const response = {statusCode: 400, body: {error: 'Some error'}} assert.throws(() => { - registry.checkStatus('http://example.com', response) + registry.checkStatus('name', 'version')(response) }) }) }) context('when statusCode is 200', () => { it('should not throw an error', () => { const response = {statusCode: 200, body: {}} - registry.checkStatus('http://example.com', response) + registry.checkStatus('name', 'version')(response) }) }) }) From 2a6d95ac50ac48555d8228199a255efef278c4fe Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Fri, 9 Sep 2016 02:03:09 +0100 Subject: [PATCH 15/30] WIP --- .travis.yml | 2 +- package.json | 8 +- test/{e2e => ____}/install.e2e.js | 0 test/____/test.sh | 111 +++++++++++++++++++ test/e2e | 78 ++++++++++++++ test/packages.txt | 144 +++++++++++++++++++++++++ test/{spec => unit}/cache.spec.js | 0 test/{spec => unit}/config.spec.js | 0 test/{spec => unit}/config_cmd.spec.js | 0 test/{spec => unit}/init_cmd.spec.js | 0 test/{spec => unit}/link.spec.js | 0 test/{spec => unit}/local.spec.js | 0 test/{spec => unit}/pkg_json.spec.js | 0 test/{spec => unit}/registry.spec.js | 0 test/{spec => unit}/shell_cmd.spec.js | 0 test/{spec => unit}/util.spec.js | 0 16 files changed, 338 insertions(+), 5 deletions(-) rename test/{e2e => ____}/install.e2e.js (100%) create mode 100755 test/____/test.sh create mode 100755 test/e2e create mode 100644 test/packages.txt rename test/{spec => unit}/cache.spec.js (100%) rename test/{spec => unit}/config.spec.js (100%) rename test/{spec => unit}/config_cmd.spec.js (100%) rename test/{spec => unit}/init_cmd.spec.js (100%) rename test/{spec => unit}/link.spec.js (100%) rename test/{spec => unit}/local.spec.js (100%) rename test/{spec => unit}/pkg_json.spec.js (100%) rename test/{spec => unit}/registry.spec.js (100%) rename test/{spec => unit}/shell_cmd.spec.js (100%) rename test/{spec => unit}/util.spec.js (100%) diff --git a/.travis.yml b/.travis.yml index 31f6343..d705a95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ node_js: - '0.11' - iojs script: -- time npm run test:spec +- time npm run test:unit - time npm run test:e2e - npm run lint - npm run test:coverage diff --git a/package.json b/package.json index 065c447..7ef0b35 100644 --- a/package.json +++ b/package.json @@ -47,11 +47,11 @@ "sinon": "^1.17.4" }, "scripts": { - "start": "npm run test:spec -- --watch", - "test": "npm run test:spec && npm run test:e2e", + "start": "npm run test:unit -- --watch", + "test": "npm run test:unit && npm run test:e2e", "test:e2e": "rimraf .tmp/test/*; cross-env NODE_ENV=test mocha test/e2e", - "test:spec": "cross-env NODE_ENV=test mocha test/spec", - "test:coverage": "nyc --reporter=lcov npm run test:spec -- --reporter dot && nyc report", + "test:unit": "cross-env NODE_ENV=test mocha test/unit", + "test:coverage": "nyc --reporter=lcov npm run test:unit -- --reporter dot && nyc report", "compile": "rimraf lib/*; cross-env NODE_ENV=production babel src/ -d lib/", "compile:watch": "npm run compile -- -w", "dev": "rimraf lib/*; cross-env NODE_ENV=development babel src/ -d lib/ -s", diff --git a/test/e2e/install.e2e.js b/test/____/install.e2e.js similarity index 100% rename from test/e2e/install.e2e.js rename to test/____/install.e2e.js diff --git a/test/____/test.sh b/test/____/test.sh new file mode 100755 index 0000000..ea78978 --- /dev/null +++ b/test/____/test.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +packages=( + "browserify" + # "express" + # "karma" + # "bower" + # "cordova" + # "coffee-script" + # "gulp" + # "forever" + # "grunt" + # "less" + # "tape" +) + +####################################### +# Clear global ied_cache directory +# Globals: +# HOME +# Arguments: +# None +# Returns: +# None +####################################### +clearCache () +{ + echo "clearing cache" + rm -rf $HOME/.ied_cache + echo "cleared cache" +} + +####################################### +# Create a dir and cd into it +# Globals: +# None +# Arguments: +# Dir +# Returns: +# None +mkcdir () +{ + mkdir -p "$1" && cd "$_" +} + +####################################### +# Clear node_modules +# Globals: +# None +# Arguments: +# None +# Returns: +# None +####################################### +clearNodeModules () { + echo "clearing node_modules" + rm -rf node_modules + echo "cleared node_modules" +} + +installPackage () +{ + echo "installing ${package}" + mkcdir "sandbox/${package}" + ied install ${package} + echo "installed ${package}" +} + +verifyInstallPackage () +{ + echo "verifying ${package} install" + node -e "require('assert')(require(\"${package}\"))" + echo "verified ${package} install" +} + +####################################### +# Test a package installation +# Globals: +# None +# Arguments: +# package +# Returns: +# None +####################################### +testPackage () +{ + echo "testing ${package}" + clearCache + installPackage ${package} + verifyInstallPackage ${package} + echo "tested ${package}" +} + +####################################### +# Run the test suite +# Globals: +# None +# Arguments: +# None +# Returns: +# None +####################################### +main () +{ + for package in "${packages[@]}" + do + testPackage $package + done +} + +main "$@" diff --git a/test/e2e b/test/e2e new file mode 100755 index 0000000..df3237a --- /dev/null +++ b/test/e2e @@ -0,0 +1,78 @@ +#!/bin/bash + +err () +{ + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2 +} + +comment () +{ + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" +} + +# Clear global .ied_cache directory +clear_cache () +{ + rm -rf $HOME/.ied_cache; + return $? +} + +# Create a dir and cd into it +mkcdir () +{ + mkdir -p "$1" && cd "$_" + return $? +} + +verify_require () +{ + node -e "require('assert')(require(\"${package}\"))"; + return $? +} + +# Test a package installation +test_package () +{ + local sandbox_dir="sandbox/install_${package}" + + rm -rf $sandbox_dir + mkcdir $sandbox_dir && + ied install ${package} && + verify_require ${package} && + cd ../.. + + return $? +} + +# Run the test suite +main () +{ + local error_count=0 + local success_count=0 + + while read package; do + comment "testing ${package}" + clear_cache + test_package $package; + + # install from cache + test_package $package; + + if [ $? -eq 0 ]; then + comment "SUCCESS" + ((success_count++)) + else + err "ERROR" + ((error_count++)) + fi + echo + done Date: Fri, 9 Sep 2016 02:17:42 +0100 Subject: [PATCH 16/30] Fix e2e npm script --- package.json | 2 +- test/e2e | 44 +++++++++++++++----------------------------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 7ef0b35..56131d7 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "scripts": { "start": "npm run test:unit -- --watch", "test": "npm run test:unit && npm run test:e2e", - "test:e2e": "rimraf .tmp/test/*; cross-env NODE_ENV=test mocha test/e2e", + "test:e2e": "./test/e2e", "test:unit": "cross-env NODE_ENV=test mocha test/unit", "test:coverage": "nyc --reporter=lcov npm run test:unit -- --reporter dot && nyc report", "compile": "rimraf lib/*; cross-env NODE_ENV=production babel src/ -d lib/", diff --git a/test/e2e b/test/e2e index df3237a..9684e33 100755 --- a/test/e2e +++ b/test/e2e @@ -1,47 +1,31 @@ #!/bin/bash -err () -{ - echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2 -} - comment () { echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" } -# Clear global .ied_cache directory -clear_cache () +clear_global_cache () { - rm -rf $HOME/.ied_cache; + rm -rf $HOME/.ied_cache return $? } -# Create a dir and cd into it -mkcdir () -{ - mkdir -p "$1" && cd "$_" - return $? -} - -verify_require () +require_package () { node -e "require('assert')(require(\"${package}\"))"; return $? } -# Test a package installation -test_package () +install_package () { local sandbox_dir="sandbox/install_${package}" rm -rf $sandbox_dir - mkcdir $sandbox_dir && - ied install ${package} && - verify_require ${package} && - cd ../.. + mkdir -p $sandbox_dir + cd $sandbox_dir - return $? + ied install ${package} } # Run the test suite @@ -50,22 +34,24 @@ main () local error_count=0 local success_count=0 + local main_dir=$(pwd) + while read package; do comment "testing ${package}" - clear_cache - test_package $package; - - # install from cache - test_package $package; + clear_global_cache + install_package $package + require_package ${package} if [ $? -eq 0 ]; then comment "SUCCESS" ((success_count++)) else - err "ERROR" + comment "ERROR" ((error_count++)) fi echo + + cd $main_dir done Date: Sat, 10 Sep 2016 16:35:32 +0100 Subject: [PATCH 17/30] Use TAP-like format --- test/e2e | 213 +++++++++++++++++++++++++++++++++++++++------- test/packages.txt | 144 ------------------------------- 2 files changed, 182 insertions(+), 175 deletions(-) delete mode 100644 test/packages.txt diff --git a/test/e2e b/test/e2e index 9684e33..9f3ac56 100755 --- a/test/e2e +++ b/test/e2e @@ -1,64 +1,215 @@ #!/bin/bash -comment () -{ - echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" -} +################################################################################ +# This script runs the `ied install ` tests. # +# As part of the test, it verifies that `ied` can successfully install the top # +# ~ 100 npm packages (most dependent upon). # +# This test is meant to be run on a regular basis, e.g. overnight, since it # +# can potentially take up to a couple of hours. # +# The test output is being formatted according to a strict subset of the TAP # +# 13 specification: https://testanything.org/tap-version-13-specification.html # +################################################################################ -clear_global_cache () -{ - rm -rf $HOME/.ied_cache - return $? -} +# List of packages to install. +packages=( + "lodash" + "request" + "async" + "underscore" + "express" + "commander" + "bluebird" + "chalk" + "debug" + "moment" + "mkdirp" + "colors" + "q" + "through2" + "react" + "yeoman-generator" + "glob" + "minimist" + "gulp-util" + "coffee-script" + "body-parser" + "jquery" + "fs-extra" + "cheerio" + "node-uuid" + "optimist" + "yargs" + "babel-runtime" + "react-dom" + "gulp" + "winston" + "yosay" + "socket.io" + "semver" + "object-assign" + "redis" + "rimraf" + "jade" + "superagent" + "ember-cli-babel" + "handlebars" + "mongoose" + "mongodb" + "extend" + "mocha" + "aws-sdk" + "co" + "shelljs" + "inquirer" + "xml2js" + "babel-preset-es2015" + "js-yaml" + "ejs" + "uglify-js" + "babel-core" + "mime" + "underscore.string" + "uuid" + "chai" + "marked" + "xtend" + "browserify" + "grunt" + "morgan" + "joi" + "through" + "cookie-parser" + "es6-promise" + "mysql" + "promise" + "ws" + "webpack" + "nan" + "connect" + "prompt" + "path" + "minimatch" + "bunyan" + "less" + "babel-polyfill" + "gulp-rename" + "angular" + "immutable" + "ramda" + "postcss" + "request-promise" + "meow" + "qs" + "inherits" + "redux" + "chokidar" + "readable-stream" + "event-stream" + "passport" + "jsdom" + "socket.io-client" + "validator" + "express-session" + "eslint" + "babel-preset-react" + "babel-loader" + "nodemailer" + "ember-cli-htmlbars" + "concat-stream" + "when" + "open" + "del" + "compression" + "babel" + "gulp-uglify" + "esprima" + "pg" + "boom" + "npm" + "d3" + "update-notifier" + "jsonwebtoken" + "fs" + "serve-favicon" + "hoek" + "mustache" + "node-sass" + "bootstrap" + "react-redux" + "backbone" + "clone" + "react-router" + "rxjs" + "stylus" + "merge" + "nconf" + "cli-color" + "loader-utils" + "gulp-concat" + "css-loader" + "cli-table" + "resolve" + "cors" + "ncp" + "iconv-lite" + "style-loader" + "log4js" + "progress" + "config" +) +# Check if a package can be resolved from the current working directory. require_package () { node -e "require('assert')(require(\"${package}\"))"; return $? } +# Install a package in a local sandbox directory. install_package () { local sandbox_dir="sandbox/install_${package}" - rm -rf $sandbox_dir - mkdir -p $sandbox_dir - cd $sandbox_dir - + rm -rf $sandbox_dir && + mkdir -p $sandbox_dir && + cd $sandbox_dir && ied install ${package} + + return $? } -# Run the test suite +# Run the test suite. main () { - local error_count=0 - local success_count=0 + local not_ok_count=0 + local ok_count=0 local main_dir=$(pwd) - while read package; do - comment "testing ${package}" - clear_global_cache - install_package $package + echo "TAP version 13" + echo "1..${#packages[@]}" + + for i in "${!packages[@]}" + do + local package=${packages[$i]} + local test_number=$((i + 1)) + + rm -rf $HOME/.ied_cache && + install_package $package && require_package ${package} if [ $? -eq 0 ]; then - comment "SUCCESS" - ((success_count++)) + echo "ok ${test_number} - ied install ${package}" + ((ok_count++)) else - comment "ERROR" - ((error_count++)) + echo "not ok ${test_number} - ied install ${package}" + ((not_ok_count++)) fi - echo cd $main_dir - done Date: Wed, 14 Sep 2016 02:50:10 +0100 Subject: [PATCH 18/30] WIP --- TODO.md | 7 ++ {src/todo => old}/git.js | 0 {src/todo => old}/install.js | 0 src/registry.js | 127 ++++++++++++++++++----- src/tarball.js | 10 ++ src/util.js | 2 +- test/____/install.e2e.js | 191 ----------------------------------- test/____/test.sh | 111 -------------------- 8 files changed, 120 insertions(+), 328 deletions(-) create mode 100644 TODO.md rename {src/todo => old}/git.js (100%) rename {src/todo => old}/install.js (100%) create mode 100644 src/tarball.js delete mode 100644 test/____/install.e2e.js delete mode 100755 test/____/test.sh diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..9308740 --- /dev/null +++ b/TODO.md @@ -0,0 +1,7 @@ +- [ ] Rename parentTarget -> pId +- [ ] Rename target -> id +- [ ] `tarball` resolver +- [ ] `git` resolver +- [ ] `local` resolver +- [ ] `ied sign` command +- [ ] `ied license` diff --git a/src/todo/git.js b/old/git.js similarity index 100% rename from src/todo/git.js rename to old/git.js diff --git a/src/todo/install.js b/old/install.js similarity index 100% rename from src/todo/install.js rename to old/install.js diff --git a/src/registry.js b/src/registry.js index f2c3140..bf66e9d 100644 --- a/src/registry.js +++ b/src/registry.js @@ -4,17 +4,36 @@ import {_do} from 'rxjs/operator/do' import {httpGet} from './util' import {inherits} from 'util' import {map} from 'rxjs/operator/map' +import {retry} from 'rxjs/operator/retry' import {maxSatisfying} from 'semver' import {publishReplay} from 'rxjs/operator/publishReplay' -// default registry to be used. +/** + * Number of times a failed request should be retried. + * + * @type {number} + */ +export const RETRY_COUNT = 5 + +/** + * Default CommonJS registry used for resolving dependencies. + * + * @type {string} + */ export const REGISTRY = 'https://registry.npmjs.org/' -// register of pending and completed HTTP requests mapped to their respective -// observable sequences. +/** + * Register of pending and completed HTTP requests mapped to their respective + * observable sequences. Used for ensuring that no duplicate requests to the + * same URLs are being made. + * + * @type {Object} + */ export const requests = Object.create(null) -// clear the internal cache used for pending and completed HTTP requests. +/** + * Clears the internal cache used for pending and completed HTTP requests. + */ export const reset = () => { const uris = Object.keys(requests) for (let i = 0; i < uris.length; i++) { @@ -22,20 +41,39 @@ export const reset = () => { } } -// scoped npm modules, such as @alexanderGugel/some-package, are "@"-prefixed. +/** + * Check if the passed in depednency name is "scoped". Scoped npm modules, such + * as @alexanderGugel/some-package, are "@"-prefixed ans usually "private (thus + * require a bearer token in the corresponding HTTP request). + * + * @param {string} name Package name to be checked. + * @return {boolean} if the package name is scoped. + */ const isScoped = name => name.charAt(0) === '@' -// escape the given package name, which can then be used as part of the package -// root URL. +/** + * Escape the given package name, which can then be used as part of the package + * root URL. + * + * @param {string} name Package name to be escaped. + * @return {string} Escaped package name. + */ export const escapeName = name => ( isScoped(name) ? `@${encodeURIComponent(name.substr(1))}` : encodeURIComponent(name) ) -// HTTP GET the resource at the supplied URI. if a request to the same URI has -// already been made, return the cached (pending) request. +/** + * HTTP GET the resource at the supplied URI. if a request to the same URI has + * already been made, return the cached (pending) request. + * + * @param {string} uri URI to be requested. + * @param {Object} [options] HTTP Options. + * @return {Observable} Cold cached observable representing the outstanding + * HTTP request. + */ export const getJson = (uri, options = {}) => { const existingRequest = requests[uri] if (existingRequest) return existingRequest @@ -48,20 +86,34 @@ export const getJson = (uri, options = {}) => { return newRequest } -// The package root url is the base URL where a client can get top-level -// information about a package and all of the versions known to the registry. -// A valid “package root url” response MUST be returned when the client -// requests {registry root url}/{package name}. -// Ideally we would use the package version url, but npm's caching policy seems -// a bit buggy in that regard. -// Source: http://wiki.commonjs.org/wiki/Packages/Registry#package_root_url +/** + * The package root url is the base URL where a client can get top-level + * information about a package and all of the versions known to the registry. + * A valid “package root url” response MUST be returned when the client requests + * {registry root url}/{package name}. + * Source: http://wiki.commonjs.org/wiki/Packages/Registry#package_root_url + * Ideally we would use the package version url, but npm's caching policy seems + * a bit buggy in that regard. + * + * @param {string} registry CommonJS registry root URL. + * @param {string} name Package name to be resolved. + * @return {string} The package root URL. + */ export const getPackageRootUrl = (registry, name) => url.resolve(registry, escapeName(name)) -// find a matching version or tag given the registry response. -// versions: An object hash of version identifiers to valid “package version -// url” responses: either URL strings or package descriptor objects. -// Source: http://wiki.commonjs.org/wiki/Packages/Registry#Package_Root_Object + +/** + * Find a matching version or tag given the registry response. + * versions: An object hash of version identifiers to valid “package version + * url” responses: either URL strings or package descriptor objects. + * Source: http://wiki.commonjs.org/wiki/Packages/Registry#Package_Root_Object + * + * @param {string} name Package name to be matched. + * @param {string} versionOrTag SemVer version number or npm tag. + * @return {function} A function to be used for processing subsequent registry + * responses and parsing out the matched [package version + */ export const findVersion = (name, versionOrTag) => body => { const versionsKeys = Object.keys(body.versions) const versionKey = body['dist-tags'][versionOrTag] @@ -104,8 +156,21 @@ export function StatusCodeError (name, version, statusCode, error) { this.extra = {name, version, statusCode, error} } -// ensure that the registry responded with an accepted HTTP status code (`200`). -export const checkStatus = (name, version) => ({statusCode, body: {error}}) => { +/** + * Ensures that the registry responded with an accepted HTTP status code + * (`200` in this case). This creates a function that — when applicable — throws + * a corresponding error. The function arguments are used exclusively for + * constructing the custom error messages. + * + * @param {string} name Package name to be used in potential error object. + * @param {string} version Package version to be used in potential error + * object. + * @return {[type]} [description] + */ +export const checkStatus = (name, version) => ({ + statusCode, + body: {error} +}) => { switch (statusCode) { case 200: break case 404: throw new NoMatchingNameError(name, version) @@ -115,10 +180,22 @@ export const checkStatus = (name, version) => ({statusCode, body: {error}}) => { const extractBody = ({body}) => body -// resolve a package defined via an ambiguous semantic version string to a -// specific `package.json` file. -export const match = (name, version, {registry = REGISTRY, ...options} = {}) => +/** + * Resolves a package defined via an ambiguous semantic version string to a + * specific `package.json` file. + * @param {string} name Package name to be matched. + * @param {string} version Package version to be matched. + * @param {object} [options] HTTP and custom options. + * @return {Observable} An observable sequence representing the asynchronously + * resolved `package.json` document representing the dependency. + */ +export const match = (name, version, { + registry = REGISTRY, + retryCount = RETRY_COUNT, + ...options +} = {}) => getJson(getPackageRootUrl(registry, name), options) + ::retry(retryCount) ::_do(checkStatus(name, version)) ::map(extractBody) ::map(findVersion(name, version)) diff --git a/src/tarball.js b/src/tarball.js new file mode 100644 index 0000000..32f0c72 --- /dev/null +++ b/src/tarball.js @@ -0,0 +1,10 @@ + + +export const resolve = (nodeModules, parentTarget, name, version, options) => + match(name, version, options)::map(pkgJson => ({ + parentTarget, + pkgJson, + target: pkgJson.dist.shasum, + name, + fetch + })) diff --git a/src/util.js b/src/util.js index 2b14495..bb0aa02 100644 --- a/src/util.js +++ b/src/util.js @@ -120,7 +120,7 @@ const execMode = 0o777 & (~process.umask()) /** * fix the permissions of a downloaded dependencies. - * @param {String]} target - target directory to resolve from. + * @param {String} target - target directory to resolve from. * @param {Object} bin - `package.json` bin object. * @return {Observable} - empty observable sequence. */ diff --git a/test/____/install.e2e.js b/test/____/install.e2e.js deleted file mode 100644 index 0267b91..0000000 --- a/test/____/install.e2e.js +++ /dev/null @@ -1,191 +0,0 @@ -import assert from 'assert' -import {spawn} from 'child_process' -import path from 'path' -import mkdirp from 'mkdirp' -import resolve from 'resolve' -import rimraf from 'rimraf' - -const targets = [ - 'browserify', - 'express', - 'karma', - 'bower', - 'cordova', - 'coffee-script', - 'gulp', - 'forever', - 'grunt', - 'less', - 'tape' -] - -const buildTargets = [ - 'dtrace-provider' -] - -const localTargets = [ - 'mocha@file:../../../node_modules/mocha' -] - -const hostedTargets = [ - 'gulp@github:gulpjs/gulp#4.0', - 'double-ended-queue@git+https://github.com/petkaantonov/deque.git', - 'bitbucket-api@git+https://bitbucket.org/hgarcia/node-bitbucket-api.git' -] - -const base = path.join(__dirname, '../../.tmp/test') -const ied = path.join(__dirname, '../../lib/cmd') - -describe('e2e install', () => { - targets.forEach((target) => { - describe(`ied install ${target}`, function () { - const cwd = path.join(base, target) - this.timeout(60 * 1000) - - before((done) => { - rimraf(cwd, done) - }) - - before((done) => { - mkdirp(cwd, done) - }) - - before((done) => { - spawn('node', [ied, 'install', target], { - cwd, - stdio: 'inherit' - }) - .on('error', done) - .on('close', (code) => { - assert.equal(code, 0) - done() - }) - }) - - it(`should make ${target} require\'able`, (done) => { - resolve(target, {basedir: cwd}, (err, res) => { - assert.ifError(err) - assert.notEqual(res.indexOf(cwd), -1) - require(res) - done() - }) - }) - }) - }) -}) - -describe('e2e install & build', () => { - buildTargets.forEach((target) => { - describe(`ied install ${target} --build`, function () { - const cwd = path.join(base, target) - this.timeout(60 * 1000) - - before((done) => { - rimraf(cwd, done) - }) - - before((done) => { - mkdirp(cwd, done) - }) - - before((done) => { - spawn('node', [ied, 'install', '--build', target], { - cwd, - stdio: 'inherit' - }) - .on('error', done) - .on('close', (code) => { - assert.equal(code, 0) - done() - }) - }) - - it(`should make ${target} require\'able`, (done) => { - resolve(target, {basedir: cwd}, (err, res) => { - assert.ifError(err) - assert.notEqual(res.indexOf(cwd), -1) - require(res) - done() - }) - }) - }) - }) -}) - -describe('e2e local install', () => { - localTargets.forEach((target) => { - describe(`ied install ${target}`, function () { - const [name] = target.split('@') - const cwd = path.join(base, name) - this.timeout(60 * 1000) - - before((done) => { - rimraf(cwd, done) - }) - - before((done) => { - mkdirp(cwd, done) - }) - - before((done) => { - spawn('node', [ied, 'install', target], { - cwd, - stdio: 'inherit' - }) - .on('error', done) - .on('close', (code) => { - assert.equal(code, 0) - done() - }) - }) - - it(`should make ${name} require\'able`, (done) => { - resolve(name, {basedir: cwd}, (err, res) => { - assert.ifError(err) - assert.notEqual(res.indexOf(cwd), -1) - require(res) - done() - }) - }) - }) - }) -}) - -describe('e2e hosted install', () => { - hostedTargets.forEach((target) => { - describe(`ied install ${target}`, function () { - const [name] = target.split('@') - const cwd = path.join(base, name) - this.timeout(60 * 1000) - - before((done) => { - rimraf(cwd, done) - }) - - before((done) => { - mkdirp(cwd, done) - }) - - before((done) => { - spawn('node', [ied, 'install', target], { - cwd, - stdio: 'inherit' - }) - .on('error', done) - .on('close', (code) => { - assert.equal(code, 0) - done() - }) - }) - - it(`should make ${name} require\'able`, (done) => { - resolve(name, {basedir: cwd}, (err, res) => { - assert.ifError(err) - assert.notEqual(res.indexOf(cwd), -1) - require(res) - done() - }) - }) - }) - }) -}) diff --git a/test/____/test.sh b/test/____/test.sh deleted file mode 100755 index ea78978..0000000 --- a/test/____/test.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/bin/bash - -packages=( - "browserify" - # "express" - # "karma" - # "bower" - # "cordova" - # "coffee-script" - # "gulp" - # "forever" - # "grunt" - # "less" - # "tape" -) - -####################################### -# Clear global ied_cache directory -# Globals: -# HOME -# Arguments: -# None -# Returns: -# None -####################################### -clearCache () -{ - echo "clearing cache" - rm -rf $HOME/.ied_cache - echo "cleared cache" -} - -####################################### -# Create a dir and cd into it -# Globals: -# None -# Arguments: -# Dir -# Returns: -# None -mkcdir () -{ - mkdir -p "$1" && cd "$_" -} - -####################################### -# Clear node_modules -# Globals: -# None -# Arguments: -# None -# Returns: -# None -####################################### -clearNodeModules () { - echo "clearing node_modules" - rm -rf node_modules - echo "cleared node_modules" -} - -installPackage () -{ - echo "installing ${package}" - mkcdir "sandbox/${package}" - ied install ${package} - echo "installed ${package}" -} - -verifyInstallPackage () -{ - echo "verifying ${package} install" - node -e "require('assert')(require(\"${package}\"))" - echo "verified ${package} install" -} - -####################################### -# Test a package installation -# Globals: -# None -# Arguments: -# package -# Returns: -# None -####################################### -testPackage () -{ - echo "testing ${package}" - clearCache - installPackage ${package} - verifyInstallPackage ${package} - echo "tested ${package}" -} - -####################################### -# Run the test suite -# Globals: -# None -# Arguments: -# None -# Returns: -# None -####################################### -main () -{ - for package in "${packages[@]}" - do - testPackage $package - done -} - -main "$@" From 5150fb6b7a34dd0b17adb28e24998b48ce87ebe5 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Fri, 16 Sep 2016 02:17:22 +0100 Subject: [PATCH 19/30] Make docs consistent --- src/config_cmd.js | 2 +- src/help_cmd.js | 4 ++-- src/init_cmd.js | 2 +- src/link.js | 36 ++++++++++++++--------------- src/link_cmd.js | 12 +++++----- src/ping.js | 6 ++--- src/ping_cmd.js | 5 ++++- src/pkg_json.js | 56 +++++++++++++++++++++++----------------------- src/progress.js | 6 ++--- src/registry.js | 18 ++++----------- src/resolve_all.js | 14 ++++++------ src/run_cmd.js | 10 +++++++-- src/shell_cmd.js | 5 ++++- src/tarball.js | 2 +- src/unlink_cmd.js | 10 ++++----- src/util.js | 43 ++++++++++++++++++----------------- src/version_cmd.js | 6 ++--- 17 files changed, 120 insertions(+), 117 deletions(-) diff --git a/src/config_cmd.js b/src/config_cmd.js index 5b3bca0..cd7afac 100644 --- a/src/config_cmd.js +++ b/src/config_cmd.js @@ -1,7 +1,7 @@ import Table from 'easy-table' /** - * print the used configuration object as an ASCII table. + * Prints the used configuration object as an ASCII table. */ export default config => () => { const table = new Table() diff --git a/src/help_cmd.js b/src/help_cmd.js index 3fd0162..c3190e4 100644 --- a/src/help_cmd.js +++ b/src/help_cmd.js @@ -3,9 +3,9 @@ import {_do} from 'rxjs/operator/do' import {readFile} from './util' /** - * print the `USAGE` document. can be invoked using `ied help` or implicitly as + * Prints the `USAGE` document. can be invoked using `ied help` or implicitly as * a fall back. - * @return {Observable} - observable sequence of `USAGE`. + * @return {Observable} Observable sequence of `USAGE` document. */ export default () => { const filename = path.join(__dirname, '../USAGE.txt') diff --git a/src/init_cmd.js b/src/init_cmd.js index de95cc3..090569d 100644 --- a/src/init_cmd.js +++ b/src/init_cmd.js @@ -3,7 +3,7 @@ import path from 'path' import {Observable} from 'rxjs/Observable' /** - * initialize a new `package.json` file. + * Initializes a new `package.json` file. * @see https://www.npmjs.com/package/init-package-json */ export default ({home}) => cwd => diff --git a/src/link.js b/src/link.js index 8c6d452..db35edb 100644 --- a/src/link.js +++ b/src/link.js @@ -5,10 +5,10 @@ import {readFileJSON, forceSymlink, unlink} from './util' import {mergeMap} from 'rxjs/operator/mergeMap' /** - * generate the symlinks to be created in order to link to passed in package. - * @param {String} cwd - current working directory. + * Generates the symlinks to be created in order to link to passed in package. + * @param {string} cwd - Current working directory. * @param {Object} pkgJson - `package.json` file to be linked. - * @return {Array.} - an array of tuples representing symbolic links to be + * @return {Array.} An array of tuples representing symbolic links to be * created. */ export const getSymlinks = (cwd, pkgJson) => { @@ -27,9 +27,9 @@ export const getSymlinks = (cwd, pkgJson) => { } /* - * globally expose the package we're currently in (used for `ied link`). - * @param {String} cwd - current working directory. - * @return {Observable} - observable sequence. + * Globally exposes the package we're currently in (used for `ied link`). + * @param {string} cwd - Current working directory. + * @return {Observable} Observable sequence. */ export const linkToGlobal = cwd => readFileJSON(path.join(cwd, 'package.json')) @@ -37,13 +37,13 @@ export const linkToGlobal = cwd => ::mergeMap(([src, dst]) => forceSymlink(src, dst)) /** - * links a globally linked package into the package present in the current + * Links a globally linked package into the package present in the current * working directory (used for `ied link some-package`). - * the package can be `require`d afterwards. + * The package can be `require`d afterwards. * `node_modules/.bin` stays untouched. - * @param {String} cwd - current working directory. - * @param {String} name - name of the dependency to be linked. - * @return {Observable} - observable sequence. + * @param {String} cwd - Current working directory. + * @param {String} name - Name of the dependency to be linked. + * @return {Observable} Observable sequence. */ export const linkFromGlobal = (cwd, name) => { const dst = path.join(cwd, 'node_modules', name) @@ -52,10 +52,10 @@ export const linkFromGlobal = (cwd, name) => { } /** - * revert the effects of `ied link` by removing the previously created + * Reverts the effects of `ied link` by removing the previously created * symbolic links (used for `ied unlink`). - * @param {String} cwd - current working directory. - * @return {Observable} - observable sequence. + * @param {String} cwd - Current working directory. + * @return {Observable} Observable sequence. */ export const unlinkToGlobal = cwd => { const pkg = require(path.join(cwd, 'package.json')) @@ -66,13 +66,13 @@ export const unlinkToGlobal = cwd => { } /** - * revert the effects of `ied link some-package` by removing the previously + * Reverts the effects of `ied link some-package` by removing the previously * created symbolic links from the project's `node_modules` directory (used for * `ied unlink some-package`). - * @param {String} cwd - current working directory. - * @param {String} name - name of the dependency to be unlinked from the + * @param {String} cwd - Current working directory. + * @param {String} name - Name of the dependency to be unlinked from the * project's `node_modules`. - * @return {Observable} - observable sequence. + * @return {Observable} Observable sequence. */ export const unlinkFromGlobal = (cwd, name) => { const dst = path.join(cwd, 'node_modules', name) diff --git a/src/link_cmd.js b/src/link_cmd.js index 0e5751f..9857d3c 100644 --- a/src/link_cmd.js +++ b/src/link_cmd.js @@ -6,22 +6,22 @@ import {mergeMap} from 'rxjs/operator/mergeMap' import {mkdirp} from './util' /** - * can be used in two ways: + * Can be used in two ways: * 1. in order to globally _expose_ the current package (`ied link`). * 2. in order to use a previously globally _exposed_ package (`ied link tap`). * - * useful for local development when you want to use a dependency in a + * Useful for local development when you want to use a dependency in a * different project without publishing to the npm registry / installing from * local FS. * - * create a symlink either in the global `node_modules` directory (`ied link`) + * Creates a symlink either in the global `node_modules` directory (`ied link`) * or in the project's `node_modules` directory (e.g. `ied link tap` would * create a symlink in `current-project/node_modules/tap` pointing to a * globally installed tap version). * - * @param {String} cwd - current working directory. - * @param {Object} argv - parsed command line arguments. - * @return {Observable} - observable sequence. + * @param {string} cwd - Current working directory. + * @param {Object} argv - Parsed command line arguments. + * @return {Observable} Observable sequence. */ export default config => (cwd, argv) => { const names = argv._.slice(1) diff --git a/src/ping.js b/src/ping.js index 8c904cf..ad1b9e1 100644 --- a/src/ping.js +++ b/src/ping.js @@ -3,9 +3,9 @@ import {httpGet} from './util' import {map} from 'rxjs/operator/map' /** - * ping the pre-configured npm registry by hitting `/-/ping?write=true`. - * @param {String} registry - root registry url to ping. - * @return {Observable} - observable sequence of the returned JSON object. + * Pings the pre-configured npm registry by hitting `/-/ping?write=true`. + * @param {String} registry - Root registry url to ping. + * @return {Observable} Observable sequence of the returned JSON object. */ export const ping = registry => { const uri = url.resolve(registry, '-/ping?write=true') diff --git a/src/ping_cmd.js b/src/ping_cmd.js index 6a6b36c..7c6d9d4 100644 --- a/src/ping_cmd.js +++ b/src/ping_cmd.js @@ -2,7 +2,10 @@ import {_do} from 'rxjs/operator/do' import {ping} from './ping' /** - * ping the registry and print the received response. + * Pings the registry and print the received response. + * @param {Object} config - Config object. + * @param {Object} config.registry - CommonJS registry to be pinged. + * @return {Function} Actual command function. */ export default ({registry}) => () => ping(registry)::_do(console.log) diff --git a/src/pkg_json.js b/src/pkg_json.js index ff1700f..690aaa1 100644 --- a/src/pkg_json.js +++ b/src/pkg_json.js @@ -8,12 +8,12 @@ import {mergeMap} from 'rxjs/operator/mergeMap' import * as util from './util' /** - * merge dependency fields. + * Merges dependency fields. * @param {Object} pkgJson - `package.json` object from which the dependencies - * should be obtained. - * @param {Array.} fields - property names of dependencies to be merged - * together. - * @return {Object} - merged dependencies. + * should be obtained. + * @param {Array.} fields - Property names of dependencies to be merged + * together. + * @return {Object} Merged dependencies. */ export const mergeDependencies = (pkgJson, fields) => { const allDependencies = {} @@ -30,12 +30,12 @@ export const mergeDependencies = (pkgJson, fields) => { } /** - * extract an array of bundled dependency names from the passed in - * `package.json`. uses the `bundleDependencies` and `bundledDependencies` + * Extracts an array of bundled dependency names from the passed in + * `package.json`. Uses the `bundleDependencies` and `bundledDependencies` * properties. - * @param {Object} pkgJson - plain JavaScript object representing a - * `package.json` file. - * @return {Array.} - array of bundled dependency names. + * @param {Object} pkgJson - Plain JavaScript object representing a + * `package.json` file. + * @return {Array.} Array of bundled dependency names. */ export const parseBundleDependencies = pkgJson => [] @@ -43,11 +43,11 @@ export const parseBundleDependencies = pkgJson => .concat(pkgJson.bundledDependencies || []) /** - * extract specified dependencies from a specific `package.json`. - * @param {Object} pkgJson - plain JavaScript object representing a + * Extracts specified dependencies from a specific `package.json`. + * @param {Object} pkgJson - Plain JavaScript object representing a * `package.json` file. - * @param {Array.} fields - array of dependency fields to be followed. - * @return {Array} - array of dependency pairs. + * @param {Array.} fields - Array of dependency fields to be followed. + * @return {Array.} Array of dependency pairs. */ export const parseDependencies = (pkgJson, fields) => { // bundleDependencies and bundledDependencies are optional. we need to @@ -67,11 +67,11 @@ export const parseDependencies = (pkgJson, fields) => { } /** - * normalize the `bin` property in `package.json`, which could either be a + * Normalizes the `bin` property in `package.json`, which could either be a * string, object or undefined. - * @param {Object} pkgJson - plain JavaScript object representing a + * @param {Object} pkgJson - Plain JavaScript object representing a * `package.json` file. - * @return {Object} - normalized `bin` property. + * @return {Object} Normalized `bin` property. */ export const normalizeBin = pkgJson => { switch (typeof pkgJson.bin) { @@ -82,9 +82,9 @@ export const normalizeBin = pkgJson => { } /** - * create an instance by reading a `package.json` from disk. - * @param {String} baseDir - base directory of the project. - * @return {Observabel} - an observable sequence of an `EntryDep`. + * Create an instance by reading a `package.json` from disk. + * @param {String} baseDir - Base directory of the project. + * @return {Observable} Observable sequence of an `EntryDep`. */ export const fromFs = baseDir => { const filename = path.join(baseDir, 'package.json') @@ -118,11 +118,11 @@ export function save (baseDir) { const argvRegExp = /^(@?.+?)(?:@(.+)?)?$/ /** - * parse the command line arguments and create the dependency field of a + * Parses the command line arguments and create the dependency field of a * `package.json` file from it. - * @param {Array} argv - command line arguments. - * @return {NullPkgJSON} - package.json created from explicit dependencies - * supplied via command line arguments. + * @param {Array} argv - Command line arguments. + * @return {NullPkgJSON} - `package.json` created from explicit dependencies + * supplied via command line arguments. */ export const parseArgv = argv => { const names = argv._.slice(1) @@ -140,11 +140,11 @@ export const parseArgv = argv => { } /** - * create an instance by parsing the explicit dependencies supplied via + * Creates an instance by parsing the explicit dependencies supplied via * command line arguments. - * @param {String} baseDir - base directory of the project. - * @param {Array} argv - command line arguments. - * @return {Observabel} - an observable sequence of an `EntryDep`. + * @param {String} baseDir - Base directory of the project. + * @param {Array} argv - Command line arguments. + * @return {Observable} Observable sequence of an `EntryDep`. */ export const fromArgv = (baseDir, argv) => { const pkgJson = parseArgv(argv) diff --git a/src/progress.js b/src/progress.js index 0408c8a..e88faca 100644 --- a/src/progress.js +++ b/src/progress.js @@ -14,7 +14,7 @@ let status = '' spinner.start() /** - * log the progress by updating the status message, percentage and spinner. + * Logs the progress by updating the status message, percentage and spinner. * @param {String} [_status] - optional (updated) status message. defaults to * the previous status message. * @see https://www.npmjs.org/package/ora @@ -26,7 +26,7 @@ export const report = (_status = status) => { } /** - * add one or more scheduled tasks. + * Adds one or more scheduled tasks. * @param {Number} [n=1] - number of scheduled tasks. */ export const add = (n = 1) => { @@ -35,7 +35,7 @@ export const add = (n = 1) => { } /** - * complete a previously scheduled task. stop the spinner when there are no + * Completes a previously scheduled task. Stops the spinner when there are no * outstanding tasks. * @param {Number} [n=1] - number of tasks that have been completed. */ diff --git a/src/registry.js b/src/registry.js index bf66e9d..58667bb 100644 --- a/src/registry.js +++ b/src/registry.js @@ -10,14 +10,12 @@ import {publishReplay} from 'rxjs/operator/publishReplay' /** * Number of times a failed request should be retried. - * * @type {number} */ export const RETRY_COUNT = 5 /** * Default CommonJS registry used for resolving dependencies. - * * @type {string} */ export const REGISTRY = 'https://registry.npmjs.org/' @@ -26,7 +24,6 @@ export const REGISTRY = 'https://registry.npmjs.org/' * Register of pending and completed HTTP requests mapped to their respective * observable sequences. Used for ensuring that no duplicate requests to the * same URLs are being made. - * * @type {Object} */ export const requests = Object.create(null) @@ -42,10 +39,9 @@ export const reset = () => { } /** - * Check if the passed in depednency name is "scoped". Scoped npm modules, such + * Checks if the passed in depednency name is "scoped". Scoped npm modules, such * as @alexanderGugel/some-package, are "@"-prefixed ans usually "private (thus * require a bearer token in the corresponding HTTP request). - * * @param {string} name Package name to be checked. * @return {boolean} if the package name is scoped. */ @@ -53,9 +49,8 @@ const isScoped = name => name.charAt(0) === '@' /** - * Escape the given package name, which can then be used as part of the package + * Escapes the given package name, which can then be used as part of the package * root URL. - * * @param {string} name Package name to be escaped. * @return {string} Escaped package name. */ @@ -66,9 +61,8 @@ export const escapeName = name => ( ) /** - * HTTP GET the resource at the supplied URI. if a request to the same URI has + * Fetches the resource at the supplied URI. if a request to the same URI has * already been made, return the cached (pending) request. - * * @param {string} uri URI to be requested. * @param {Object} [options] HTTP Options. * @return {Observable} Cold cached observable representing the outstanding @@ -94,7 +88,6 @@ export const getJson = (uri, options = {}) => { * Source: http://wiki.commonjs.org/wiki/Packages/Registry#package_root_url * Ideally we would use the package version url, but npm's caching policy seems * a bit buggy in that regard. - * * @param {string} registry CommonJS registry root URL. * @param {string} name Package name to be resolved. * @return {string} The package root URL. @@ -104,11 +97,10 @@ export const getPackageRootUrl = (registry, name) => /** - * Find a matching version or tag given the registry response. + * Finds a matching version or tag given the registry response. * versions: An object hash of version identifiers to valid “package version * url” responses: either URL strings or package descriptor objects. * Source: http://wiki.commonjs.org/wiki/Packages/Registry#Package_Root_Object - * * @param {string} name Package name to be matched. * @param {string} versionOrTag SemVer version number or npm tag. * @return {function} A function to be used for processing subsequent registry @@ -161,11 +153,9 @@ export function StatusCodeError (name, version, statusCode, error) { * (`200` in this case). This creates a function that — when applicable — throws * a corresponding error. The function arguments are used exclusively for * constructing the custom error messages. - * * @param {string} name Package name to be used in potential error object. * @param {string} version Package version to be used in potential error * object. - * @return {[type]} [description] */ export const checkStatus = (name, version) => ({ statusCode, diff --git a/src/resolve_all.js b/src/resolve_all.js index 31c6cd4..9f74b47 100644 --- a/src/resolve_all.js +++ b/src/resolve_all.js @@ -12,7 +12,7 @@ import {resolve as resolveFromLocal} from './local' import {resolve as resolveFromRegistry} from './registry' /** - * properties of project-level `package.json` files that will be checked for + * Properties of project-level `package.json` files that will be checked for * dependencies. * @type {Array.} * @readonly @@ -24,7 +24,7 @@ export const ENTRY_DEPENDENCY_FIELDS = [ ] /** - * properties of `package.json` of sub-dependencies that will be checked for + * Properties of `package.json` of sub-dependencies that will be checked for * dependencies. * @type {Array.} * @readonly @@ -34,7 +34,7 @@ export const DEPENDENCY_FIELDS = [ 'optionalDependencies' ] -function resolve (nodeModules, parentTarget, config) { +function resolve (nodeModules, parentTarget, options) { return this::mergeMap(([name, version]) => { add() report(`resolving ${name}@${version}`) @@ -42,8 +42,8 @@ function resolve (nodeModules, parentTarget, config) { return concatStatic( resolveFromLocal(nodeModules, parentTarget, name), resolveFromRegistry(nodeModules, parentTarget, name, version, { - ...config.httpOptions, - registry: config.registry + ...options.httpOptions, + registry: options.registry }) ) ::first() @@ -51,7 +51,7 @@ function resolve (nodeModules, parentTarget, config) { }) } -export default function resolveAll (nodeModules, config) { +export default function resolveAll (nodeModules, options) { const targets = Object.create(null) const entryTarget = '..' @@ -63,6 +63,6 @@ export default function resolveAll (nodeModules, config) { const isEntry = result.target === entryTarget && !result.isProd const fields = isEntry ? ENTRY_DEPENDENCY_FIELDS : DEPENDENCY_FIELDS return ArrayObservable.create(parseDependencies(result.pkgJson, fields)) - ::resolve(nodeModules, result.target, config) + ::resolve(nodeModules, result.target, options) }) } diff --git a/src/run_cmd.js b/src/run_cmd.js index 05bbef4..bbaf996 100644 --- a/src/run_cmd.js +++ b/src/run_cmd.js @@ -44,9 +44,15 @@ export const run = (sh, shFlag, script, options = {}) => }) /** - * run a `package.json` script and the related pre- and postscript. + * Runs a `package.json` script and the related pre- and postscript. + * @param {Object} config - Config object. + * @param {Object} config.sh - Command used for spawning new sub-shell session. + * @param {Object} config.shFlag - Command line flags to be passed to the new + * sub-shell command. + * @return {Function} Actual command function. */ -export default ({sh, shFlag}) => (cwd, argv) => { +export default config => (cwd, argv) => { + const {sh, shFlag} = config const scriptNames = argv._.slice(1) const pkgJson = fromFs(cwd) diff --git a/src/shell_cmd.js b/src/shell_cmd.js index f443309..7716dbe 100644 --- a/src/shell_cmd.js +++ b/src/shell_cmd.js @@ -5,8 +5,11 @@ import {readdir} from './util' import {spawn} from 'child_process' /** - * enter a new session that has access to the CLIs exposed by the installed + * Enters a new session that has access to the CLIs exposed by the installed * packages by using an amended `PATH`. + * @param {Object} config - Config object. + * @param {Object} config.sh - Command used for spawning new sub-shell session. + * @return {Function} Actual command function. */ export default config => cwd => { const binPath = path.join(cwd, 'node_modules/.bin') diff --git a/src/tarball.js b/src/tarball.js index 32f0c72..568f6bd 100644 --- a/src/tarball.js +++ b/src/tarball.js @@ -1,4 +1,4 @@ - +// TODO export const resolve = (nodeModules, parentTarget, name, version, options) => match(name, version, options)::map(pkgJson => ({ diff --git a/src/unlink_cmd.js b/src/unlink_cmd.js index f171797..f053b2c 100644 --- a/src/unlink_cmd.js +++ b/src/unlink_cmd.js @@ -3,18 +3,18 @@ import {mergeMap} from 'rxjs/operator/mergeMap' import {unlinkFromGlobal, unlinkToGlobal} from './link' /** - * unlink one or more previously linked dependencies. can be invoked via + * Unlinks one or more previously linked dependencies. can be invoked via * `ied unlink`. E.g. `ied unlink browserify tap webpack` would unlink all * _three_ dependencies. - * @param {String} cwd - current working directory. - * @param {Object} argv - parsed command line arguments. - * @return {Observable} - observable sequence. + * @param {String} cwd - Current working directory. + * @param {Object} argv - Parsed command line arguments. + * @return {Observable} Empty observable sequence. */ export default (cwd, argv) => { const names = argv._.slice(1) return names.length ? ArrayObservable.create(names) - ::mergeMap((name) => unlinkFromGlobal(cwd, name)) + ::mergeMap(name => unlinkFromGlobal(cwd, name)) : unlinkToGlobal(cwd) } diff --git a/src/util.js b/src/util.js index bb0aa02..4ac3e13 100644 --- a/src/util.js +++ b/src/util.js @@ -9,13 +9,13 @@ import {map} from 'rxjs/operator/map' import {mergeMap} from 'rxjs/operator/mergeMap' /** - * given an arbitrary asynchronous function that accepts a callback function, - * wrap the outer asynchronous function into an observable sequence factory. - * invoking the returned generated function is going to return a new **cold** + * Given an arbitrary asynchronous function that accepts a callback function, + * wraps the outer asynchronous function into an observable sequence factory. + * Invoking the returned generated function is going to return a new **cold** * observable sequence. - * @param {Function} fn - function to be wrapped. - * @param {thisArg} [thisArg] - optional context. - * @return {Function} - cold observable sequence factory. + * @param {Function} fn - The function to be wrapped. + * @param {thisArg} [thisArg] - Optional context. + * @return {Function} A cold observable sequence factory. */ export const createObservableFactory = (fn, thisArg) => (...args) => @@ -31,7 +31,7 @@ export const createObservableFactory = (fn, thisArg) => }) /** - * send a GET request to the given HTTP endpoint by passing the supplied + * Sends a GET request to the given HTTP endpoint by passing the supplied * arguments to [`needle`](https://www.npmjs.com/package/needle). * @return {Observable} - observable sequence of a single response object. */ @@ -79,8 +79,8 @@ export const forceSymlink = createObservableFactory(_forceSymlink) export const mkdirp = createObservableFactory(_mkdirp) /** - * equivalent to `Map#entries` for observables, but operates on objects. - * @return {Observable} - observable sequence of pairs. + * Equivalent to `Map#entries` for observables, but operates on objects. + * @return {Observable} Observable sequence of pairs. */ export function entries () { return this::mergeMap(object => { @@ -95,34 +95,35 @@ export function entries () { } /** - * read a UTF8 encoded JSON file from disk. + * Reads an UTF8 encoded JSON file from disk. * @param {String} file - filename to be used. - * @return {Observable} - observable sequence of a single object representing + * @return {Observable} Observable sequence of a single object representing * the read JSON file. */ export const readFileJSON = file => readFile(file, 'utf8')::map(JSON.parse) /** - * set the terminal title using the required ANSI escape codes. - * @param {String} title - title to be set. + * Sets the terminal title using the required ANSI escape codes. + * @param {String} title - Title to be set. */ -export const setTitle = title => - process.stdout.write( - `${String.fromCharCode(27)}]0;${title}${String.fromCharCode(7)}` - ) +export const setTitle = title => { + const out = `${String.fromCharCode(27)}]0;${title}${String.fromCharCode(7)}` + process.stdout.write(out) +} /** - * file permissions for executable bin files. + * File permissions for executable bin files. * @type {Number} + * @private */ const execMode = 0o777 & (~process.umask()) /** - * fix the permissions of a downloaded dependencies. - * @param {String} target - target directory to resolve from. + * Fixes the permissions of a downloaded dependencies. + * @param {String} target - Target directory to resolve from. * @param {Object} bin - `package.json` bin object. - * @return {Observable} - empty observable sequence. + * @return {Observable} Empty observable sequence. */ export const fixPermissions = (target, bin) => { const paths = [] diff --git a/src/version_cmd.js b/src/version_cmd.js index f2a2f9f..c1721ce 100644 --- a/src/version_cmd.js +++ b/src/version_cmd.js @@ -4,9 +4,9 @@ import {map} from 'rxjs/operator/map' import {readFileJSON} from './util' /** - * display the version number. - * @return {Subscription} - a subscription that logs the versio number to the - * console. + * Logs the version number. + * @return {Observable} An observable that logs the version number to the + * console. */ export default () => readFileJSON(path.join(__dirname, '../package.json')) From 8f66259bc1eb5369e7ea8521427d02c1ce68c185 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Sat, 17 Sep 2016 19:14:44 +0100 Subject: [PATCH 20/30] Improve docs --- old/git.js | 6 +++--- old/install.js | 20 ++++++++--------- src/build_all.js | 37 ++++++++++++++++--------------- src/cache.js | 33 ++++++++++++++-------------- src/cache_cmd.js | 4 ++-- src/config.js | 54 +++++++++++++++++++++++----------------------- src/debuglog.js | 2 +- src/install_cmd.js | 17 ++++++++++++++- src/link.js | 10 ++++----- src/link_cmd.js | 4 ++-- src/ping.js | 2 +- src/pkg_json.js | 4 ++-- src/progress.js | 2 +- src/unlink_cmd.js | 2 +- src/util.js | 6 +++--- 15 files changed, 111 insertions(+), 92 deletions(-) diff --git a/old/git.js b/old/git.js index 339070d..39e760e 100644 --- a/old/git.js +++ b/old/git.js @@ -24,8 +24,8 @@ function spawnGit (args) { /** * clone a git repository. - * @param {String} repo - git repository to clone. - * @param {String} ref - git reference to checkout. + * @param {string} repo - git repository to clone. + * @param {string} ref - git reference to checkout. * @return {Observable} - observable sequence that will be completed once * the git repository has been cloned. */ @@ -50,7 +50,7 @@ export function clone (repo, ref) { /** * extract a cloned git repository to destination. - * @param {String} dest - pathname into which the cloned repository should be + * @param {string} dest - pathname into which the cloned repository should be * extracted. * @return {Observable} - observable sequence that will be completed once * the cloned repository has been extracted. diff --git a/old/install.js b/old/install.js index ded33de..33d6a31 100644 --- a/old/install.js +++ b/old/install.js @@ -29,10 +29,10 @@ const cachedNpa = memoize(npa) /** * resolve a dependency's `package.json` file from a remote registry. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {String} name - name of the dependency. - * @param {String} version - version of the dependency. + * @param {string} nodeModules - `node_modules` base directory. + * @param {string} parentTarget - relative parent's node_modules path. + * @param {string} name - name of the dependency. + * @param {string} version - version of the dependency. * @return {Observable} - observable sequence of `package.json` objects. */ @@ -63,8 +63,8 @@ export function resolveRemote (nodeModules, parentTarget, name, version) { /** * resolve a dependency's `package.json` file from an url tarball. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. + * @param {string} nodeModules - `node_modules` base directory. + * @param {string} parentTarget - relative parent's node_modules path. * @param {Object} parsedSpec - parsed package name and specifier. * @return {Observable} - observable sequence of `package.json` objects. */ @@ -85,8 +85,8 @@ export function resolveFromTarball (nodeModules, parentTarget, parsedSpec) { /** * resolve a dependency's `package.json` file from an hosted GitHub-like registry. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. + * @param {string} nodeModules - `node_modules` base directory. + * @param {string} parentTarget - relative parent's node_modules path. * @param {Object} parsedSpec - parsed package name and specifier. * @return {Observable} - observable sequence of `package.json` objects. */ @@ -126,8 +126,8 @@ export function resolveFromHosted (nodeModules, parentTarget, parsedSpec) { /** * resolve a dependency's `package.json` file from a git endpoint. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. + * @param {string} nodeModules - `node_modules` base directory. + * @param {string} parentTarget - relative parent's node_modules path. * @param {Object} parsedSpec - parsed package name and specifier. * @return {Observable} - observable sequence of `package.json` objects. */ diff --git a/src/build_all.js b/src/build_all.js index 857cf9f..9d85ca3 100644 --- a/src/build_all.js +++ b/src/build_all.js @@ -12,9 +12,12 @@ import {spawn} from 'child_process' import * as config from './config' -// names of lifecycle scripts that should be run as part of the installation -// process of a specific package (= properties of `scripts` object in -// `package.json`). +/** + * Names of lifecycle scripts that should be run as part of the installation + * process of a specific package (= properties of `scripts` object in + * `package.json`). + * @type {Array.} + */ export const LIFECYCLE_SCRIPTS = [ 'preinstall', 'install', @@ -31,12 +34,12 @@ export function FailedBuildError () { } /** - * build a dependency by executing the given lifecycle script. - * @param {String} nodeModules - absolute path of the `node_modules` directory. - * @param {Object} dep - dependency to be built. - * @param {String} dep.target - relative location of the target directory. - * @param {String} dep.script - script to be executed (usually using `sh`). - * @return {Observable} - observable sequence of the returned exit code. + * Builds a dependency by executing the given lifecycle script. + * @param {string} nodeModules - Absolute path of the `node_modules` directory. + * @param {Object} dep - Dependency to be built. + * @param {string} dep.target - Relative location of the target directory. + * @param {string} dep.script - Script to be executed (usually using `sh`). + * @return {Observable} Observable sequence of the returned exit code. */ export const build = nodeModules => ({target, script}) => Observable.create(observer => { @@ -65,9 +68,9 @@ export const build = nodeModules => ({target, script}) => }) /** - * extract lifecycle scripts from supplied dependency. - * @param {Dep} dep - dependency to be parsed. - * @return {Array.} - array of script targets to be executed. + * Extracts lifecycle scripts from supplied dependency. + * @param {Dep} dep - Dependency to be parsed. + * @return {Array.} Array of script targets to be executed. */ export const parseLifecycleScripts = ({target, pkgJson: {scripts = {}}}) => { const results = [] @@ -80,12 +83,12 @@ export const parseLifecycleScripts = ({target, pkgJson: {scripts = {}}}) => { } /** - * run all lifecycle scripts upon completion of the installation process. - * ensures that all scripts exit with 0 (success), otherwise an error will be + * Runs all lifecycle scripts upon completion of the installation process. + * Ensures that all scripts exit with 0 (success), otherwise an error will be * thrown. - * @param {String} nodeModules - `node_modules` base directory. - * @return {Observable} - empty observable sequence that will be completed once - * all lifecycle scripts have been executed. + * @param {string} nodeModules - `node_modules` base directory. + * @return {Observable} - Empty observable sequence that will be completed once + * all lifecycle scripts have been executed. */ export default function buildAll (nodeModules) { return this diff --git a/src/cache.js b/src/cache.js index 0710c6e..ce48072 100644 --- a/src/cache.js +++ b/src/cache.js @@ -11,33 +11,34 @@ import {mergeMap} from 'rxjs/operator/mergeMap' import {retryWhen} from 'rxjs/operator/retryWhen' /** - * initialize the cache. - * @return {Observable} - observable sequence that will be completed once the - * base directory of the cache has been created. + * Initializes the cache. + * @return {Observable} Observable sequence that will be completed once the + * base directory of the cache has been created. */ export const init = () => util.mkdirp(path.join(config.cacheDir, '.tmp')) ::ignoreElements() /** - * get a random temporary filename. - * @return {String} - temporary filename. + * Generates a random temporary filename. The returned path is absolute and can + * be used for creating a `WriteStream` etc. + * @return {string} A temporary filename. */ export const getTmp = () => path.join(config.cacheDir, '.tmp', uuid.v4()) /** - * open a write stream into a temporarily cached file for caching a new + * Opens a write stream into a temporarily cached file for caching a new * package. - * @return {WriteStream} - Write Stream + * @return {WriteStream} Write Stream. */ export const write = () => fs.createWriteStream(getTmp()) /** - * open a read stream to a cached dependency. - * @param {String} id - id (unique identifier) of the cached tarball. - * @return {ReadStream} - Read Stream + * Opens a read stream to a cached dependency. + * @param {string} id - Unique identifier of the cached tarball. + * @return {ReadStream} Read Stream. */ export const read = id => fs.createReadStream(path.join(config.cacheDir, id)) @@ -47,12 +48,12 @@ const extractOptions = { } /** - * extract a dependency from the cache. - * @param {String} dest - pathname into which the cached dependency should be - * extracted. - * @param {String} id - id (unique identifier) of the cached tarball. - * @return {Observable} - observable sequence that will be completed once - * the cached dependency has been fetched. + * Extracts a dependency from the cache. + * @param {string} dest - path "into" which the cached dependency should be + * extracted. + * @param {string} id - Unique identifier of the cached tarball. + * @return {Observable} Observable sequence that will be completed once + * the cached dependency has been fetched. */ export const extract = (dest, id) => Observable.create(observer => { diff --git a/src/cache_cmd.js b/src/cache_cmd.js index 9e739dc..6e1fbcd 100644 --- a/src/cache_cmd.js +++ b/src/cache_cmd.js @@ -2,8 +2,8 @@ import path from 'path' import rimraf from 'rimraf' /** - * print help if invoked without any further sub-command, empty the cache - * directory (delete it) if invoked via `ied cache clean`. + * Prints help if invoked without any further sub-command, empties the cache + * directory (deletes it) if invoked via `ied cache clean`. */ export default ({cacheDir}) => (cwd, argv) => { switch (argv._[1]) { diff --git a/src/config.js b/src/config.js index 2f18005..6b356a4 100644 --- a/src/config.js +++ b/src/config.js @@ -3,81 +3,81 @@ import path from 'path' const {env, platform, execPath} = process /** - * boolean value indicating whether or not we're running on `win32` (Windows). - * @type {Boolean} + * Boolean value indicating whether or not we're running on `win32` (Windows). + * @type {boolean} */ export const isWindows = platform === 'win32' /** - * absolute location of the user's home directory. - * @type {String} + * Absolute location of the user's home directory. + * @type {string} */ export const home = env[isWindows ? 'USERPROFILE' : 'HOME'] /** - * registry endpoint configured via `IED_REGISTRY`, defaults to the npm + * Registry endpoint configured via `IED_REGISTRY`, defaults to the npm * registry [`https://registry.npmjs.org/`]('https://registry.npmjs.org/'). - * @type {String} + * @type {string} */ export const registry = env.IED_REGISTRY || 'https://registry.npmjs.org/' /** - * cache directory used for storing downloaded package tarballs. configurable + * Cache directory used for storing downloaded package tarballs. Configurable * via `IED_CACHE_DIR`, default to `.ied_cache` in the user's home directory. - * @type {String} + * @type {string} */ export const cacheDir = env.IED_CACHE_DIR || path.join(home, '.ied_cache') /** - * directory used for globally installed `node_modules`. - * configurable via `IED_GLOBAL_NODE_MODULES`, default to `.node_modules` in + * Directory used for globally installed `node_modules`. + * Configurable via `IED_GLOBAL_NODE_MODULES`, default to `.node_modules` in * the user's home directory. - * @type {String} + * @type {string} */ export const globalNodeModules = env.IED_GLOBAL_NODE_MODULES || path.join(home, '.node_modules') /** - * similar to {@link globalNodeModules}. directory used for symlinks of + * Similar to {@link globalNodeModules}. directory used for symlinks of * globally linked executables. - * configurable via `IED_GLOBAL_BIN`, default to parent of `process.execPath` + * Configurable via `IED_GLOBAL_BIN`, default to parent of `process.execPath` * (location of `node` binary). - * @type {String} + * @type {string} */ export const globalBin = env.IED_GLOBAL_BIN || path.resolve(execPath, '..') /** - * proxy server endpoint. can be set via `IED_PROXY` or `http_proxy`. optional - * and default to `null`. - * @type {String|null} + * Proxy server endpoint. Can be set via `IED_PROXY` or `http_proxy`. Optional + * and might be set to `null`. + * @type {string|null} */ export const proxy = env.IED_PROXY || env.http_proxy || null /** - * how often `ied` should retry HTTP requests before indicating failure. - * defaults to `5` requests. can be set via `IED_REQUEST_RETRIES`. + * How often `ied` should retry HTTP requests before indicating failure. + * Defaults to `5` requests, but can be configured via `IED_REQUEST_RETRIES`. * @type {Number} */ export const retries = parseInt(env.IED_REQUEST_RETRIES, 10) || 5 /** - * shell command used for executing lifecycle scripts (such as `postinstall`). - * platform dependent: default to `cmd` on Windows, otherwise use `sh`. - * can be overridden using `IED_SH`. - * @type {String} + * Shell command used for executing lifecycle scripts (such as `postinstall`). + * Platform dependent: Defaults to `cmd` on Windows, otherwise uses `sh`. + * cCan be overridden using `IED_SH`. + * @type {string} */ export const sh = env.IED_SH || env.SHELL || (platform === 'win32' ? env.comspec || 'cmd' : 'sh') /** - * additional flags supplied to the `sh` executable. platform dependent: + * Additional flags supplied to the `sh` executable. platform dependent: * default to `/d /s /c` on Windows, otherwise use `-c`. - * can be overridden using `IED_SH_FLAG`. - * @type {String} + * Can be overridden using `IED_SH_FLAG`. + * @type {string} */ export const shFlag = env.IED_SH_FLAG || (isWindows ? '/d /s /c' : '-c') /** - * bearer token used for downloading access restricted packages (scoped + * Bearer token used for downloading access restricted packages (scoped * modules). this token will be set as `Authorization` header field on all * subsequent HTTP requests to the registry, thus exposing a potential * **security** risk. diff --git a/src/debuglog.js b/src/debuglog.js index 0d24225..00b60f6 100644 --- a/src/debuglog.js +++ b/src/debuglog.js @@ -34,7 +34,7 @@ let debugEnv * be similar to `console.error()`. If not, then the returned function is a * no-op. * - * @param {String} set - the section of the program to be debugged. + * @param {string} set - the section of the program to be debugged. * @return {Function} - the logging function. * @see https://nodejs.org/api/util.html#util_util_debuglog_section */ diff --git a/src/install_cmd.js b/src/install_cmd.js index 9758d1f..358f28b 100644 --- a/src/install_cmd.js +++ b/src/install_cmd.js @@ -31,9 +31,24 @@ const parseArgv = ({_, production}) => ({ isProd: production }) +/** + * Check if the updated `package.json` file should be persisted. Useful since + * `--save`, `--save-dev` and `--save-optional` imply that the updated file + * should be saved. + * @param {Object} argv - Parsed command line arguments. + * @return {boolean} If the updated `package.json` file should be persisted. + */ const shouldSave = argv => argv.save || argv['save-dev'] || argv['save-optional'] +/** + * Check if the dependencies should be built. lifecycle scripts are not being + * executed by default, but require the usage of `--build` for security and + * performance reasons. + * @param {Object} argv - Parsed command line arguments. + * @return {boolean} If the respective `npm` lifecycle scripts should be + * executed. + */ const shouldBuild = argv => argv.build @@ -42,7 +57,7 @@ export default config => (cwd, argv) => { const dir = path.join(cwd, 'node_modules') const target = '..' - // generate the "source" package.json file from which dependencies are being + // Generates the "source" package.json file from which dependencies are being // parsed and installed. const srcPkgJson = isExplicit ? fromArgv(cwd, argv) diff --git a/src/link.js b/src/link.js index db35edb..2e7ba3c 100644 --- a/src/link.js +++ b/src/link.js @@ -41,8 +41,8 @@ export const linkToGlobal = cwd => * working directory (used for `ied link some-package`). * The package can be `require`d afterwards. * `node_modules/.bin` stays untouched. - * @param {String} cwd - Current working directory. - * @param {String} name - Name of the dependency to be linked. + * @param {string} cwd - Current working directory. + * @param {string} name - Name of the dependency to be linked. * @return {Observable} Observable sequence. */ export const linkFromGlobal = (cwd, name) => { @@ -54,7 +54,7 @@ export const linkFromGlobal = (cwd, name) => { /** * Reverts the effects of `ied link` by removing the previously created * symbolic links (used for `ied unlink`). - * @param {String} cwd - Current working directory. + * @param {string} cwd - Current working directory. * @return {Observable} Observable sequence. */ export const unlinkToGlobal = cwd => { @@ -69,8 +69,8 @@ export const unlinkToGlobal = cwd => { * Reverts the effects of `ied link some-package` by removing the previously * created symbolic links from the project's `node_modules` directory (used for * `ied unlink some-package`). - * @param {String} cwd - Current working directory. - * @param {String} name - Name of the dependency to be unlinked from the + * @param {string} cwd - Current working directory. + * @param {string} name - Name of the dependency to be unlinked from the * project's `node_modules`. * @return {Observable} Observable sequence. */ diff --git a/src/link_cmd.js b/src/link_cmd.js index 9857d3c..cacc4ec 100644 --- a/src/link_cmd.js +++ b/src/link_cmd.js @@ -7,8 +7,8 @@ import {mkdirp} from './util' /** * Can be used in two ways: - * 1. in order to globally _expose_ the current package (`ied link`). - * 2. in order to use a previously globally _exposed_ package (`ied link tap`). + * 1. In order to globally _expose_ the current package (`ied link`). + * 2. In order to use a previously globally _exposed_ package (`ied link tap`). * * Useful for local development when you want to use a dependency in a * different project without publishing to the npm registry / installing from diff --git a/src/ping.js b/src/ping.js index ad1b9e1..f068261 100644 --- a/src/ping.js +++ b/src/ping.js @@ -4,7 +4,7 @@ import {map} from 'rxjs/operator/map' /** * Pings the pre-configured npm registry by hitting `/-/ping?write=true`. - * @param {String} registry - Root registry url to ping. + * @param {string} registry - Root registry url to ping. * @return {Observable} Observable sequence of the returned JSON object. */ export const ping = registry => { diff --git a/src/pkg_json.js b/src/pkg_json.js index 690aaa1..a469beb 100644 --- a/src/pkg_json.js +++ b/src/pkg_json.js @@ -83,7 +83,7 @@ export const normalizeBin = pkgJson => { /** * Create an instance by reading a `package.json` from disk. - * @param {String} baseDir - Base directory of the project. + * @param {string} baseDir - Base directory of the project. * @return {Observable} Observable sequence of an `EntryDep`. */ export const fromFs = baseDir => { @@ -142,7 +142,7 @@ export const parseArgv = argv => { /** * Creates an instance by parsing the explicit dependencies supplied via * command line arguments. - * @param {String} baseDir - Base directory of the project. + * @param {string} baseDir - Base directory of the project. * @param {Array} argv - Command line arguments. * @return {Observable} Observable sequence of an `EntryDep`. */ diff --git a/src/progress.js b/src/progress.js index e88faca..2ceef16 100644 --- a/src/progress.js +++ b/src/progress.js @@ -15,7 +15,7 @@ spinner.start() /** * Logs the progress by updating the status message, percentage and spinner. - * @param {String} [_status] - optional (updated) status message. defaults to + * @param {string} [_status] - optional (updated) status message. defaults to * the previous status message. * @see https://www.npmjs.org/package/ora */ diff --git a/src/unlink_cmd.js b/src/unlink_cmd.js index f053b2c..6a2758e 100644 --- a/src/unlink_cmd.js +++ b/src/unlink_cmd.js @@ -6,7 +6,7 @@ import {unlinkFromGlobal, unlinkToGlobal} from './link' * Unlinks one or more previously linked dependencies. can be invoked via * `ied unlink`. E.g. `ied unlink browserify tap webpack` would unlink all * _three_ dependencies. - * @param {String} cwd - Current working directory. + * @param {string} cwd - Current working directory. * @param {Object} argv - Parsed command line arguments. * @return {Observable} Empty observable sequence. */ diff --git a/src/util.js b/src/util.js index 4ac3e13..696168d 100644 --- a/src/util.js +++ b/src/util.js @@ -96,7 +96,7 @@ export function entries () { /** * Reads an UTF8 encoded JSON file from disk. - * @param {String} file - filename to be used. + * @param {string} file - filename to be used. * @return {Observable} Observable sequence of a single object representing * the read JSON file. */ @@ -105,7 +105,7 @@ export const readFileJSON = file => /** * Sets the terminal title using the required ANSI escape codes. - * @param {String} title - Title to be set. + * @param {string} title - Title to be set. */ export const setTitle = title => { const out = `${String.fromCharCode(27)}]0;${title}${String.fromCharCode(7)}` @@ -121,7 +121,7 @@ const execMode = 0o777 & (~process.umask()) /** * Fixes the permissions of a downloaded dependencies. - * @param {String} target - Target directory to resolve from. + * @param {string} target - Target directory to resolve from. * @param {Object} bin - `package.json` bin object. * @return {Observable} Empty observable sequence. */ From 6bcb09bf6b1e4a09abcb7f00e1f197431cb1bf95 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Sun, 18 Sep 2016 03:21:05 +0100 Subject: [PATCH 21/30] Use config as first argument vs higher order function --- src/cache_cmd.js | 2 +- src/cmd.js | 35 +++++++++++++++++------------------ src/config_cmd.js | 3 ++- src/init_cmd.js | 5 ++++- src/install_cmd.js | 2 +- src/link_cmd.js | 3 ++- src/ping_cmd.js | 4 ++-- src/run_cmd.js | 7 +++++-- src/shell_cmd.js | 6 ++++-- src/unlink_cmd.js | 3 ++- 10 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/cache_cmd.js b/src/cache_cmd.js index 6e1fbcd..1b082fd 100644 --- a/src/cache_cmd.js +++ b/src/cache_cmd.js @@ -5,7 +5,7 @@ import rimraf from 'rimraf' * Prints help if invoked without any further sub-command, empties the cache * directory (deletes it) if invoked via `ied cache clean`. */ -export default ({cacheDir}) => (cwd, argv) => { +export default ({cacheDir}, cwd, argv) => { switch (argv._[1]) { // `ied cache clean` case 'clean': diff --git a/src/cmd.js b/src/cmd.js index f470ae2..37c2f9e 100755 --- a/src/cmd.js +++ b/src/cmd.js @@ -64,22 +64,21 @@ if (argv.help) { switch (subCommand) { case 'i': case 'install': - installCmd = require('./install_cmd').default(config) - installCmd(cwd, argv).subscribe() + installCmd = require('./install_cmd').default + installCmd(config, cwd, argv).subscribe() break case 'sh': case 'shell': - shellCmd = require('./shell_cmd').default(config) - shellCmd(cwd).subscribe() + shellCmd = require('./shell_cmd').default + shellCmd(config, cwd).subscribe() break case 'r': case 'run': case 'run-script': runCmd = require('./run_cmd').default - runCmd(config)(cwd, argv) - .subscribe(process.exit) + runCmd(config, cwd, argv).subscribe(process.exit) break case 't': @@ -89,39 +88,39 @@ if (argv.help) { case 'build': case 'stop': runCmd = require('./run_cmd').default - runCmd(config)(cwd, {...argv, _: ['run', ...argv._]}) + runCmd(config, cwd, {...argv, _: ['run', ...argv._]}) .subscribe(process.exit) break case 'ping': - pingCmd = require('./ping_cmd').default(config) - pingCmd().subscribe() + pingCmd = require('./ping_cmd').default + pingCmd(config).subscribe() break case 'conf': case 'config': - configCmd = require('./config_cmd').default(config) - configCmd() + configCmd = require('./config_cmd').default + configCmd(config) break case 'init': - initCmd = require('./init_cmd').default(config) - initCmd(cwd, argv).subscribe() + initCmd = require('./init_cmd').default + initCmd(config, cwd, argv).subscribe() break case 'link': - linkCmd = require('./link_cmd').default(config) - linkCmd(cwd, argv).subscribe() + linkCmd = require('./link_cmd').default + linkCmd(config, cwd, argv).subscribe() break case 'unlink': unlinkCmd = require('./unlink_cmd').default - unlinkCmd(cwd, argv).subscribe() + unlinkCmd(config, cwd, argv).subscribe() break case 'cache': - cacheCmd = require('./cache_cmd').default(config) - cacheCmd(cwd, argv) + cacheCmd = require('./cache_cmd').default + cacheCmd(config, cwd, argv) break case 'version': diff --git a/src/config_cmd.js b/src/config_cmd.js index cd7afac..aa6ee3c 100644 --- a/src/config_cmd.js +++ b/src/config_cmd.js @@ -2,8 +2,9 @@ import Table from 'easy-table' /** * Prints the used configuration object as an ASCII table. + * @param {Object} config - Config object. */ -export default config => () => { +export default config => { const table = new Table() const keys = Object.keys(config) diff --git a/src/init_cmd.js b/src/init_cmd.js index 090569d..e1ba5d3 100644 --- a/src/init_cmd.js +++ b/src/init_cmd.js @@ -4,9 +4,12 @@ import {Observable} from 'rxjs/Observable' /** * Initializes a new `package.json` file. + * @param {Object} config - Config object. + * @param {string} cwd - Current working directory. + * @return {Observable} Empty observable sequence. * @see https://www.npmjs.com/package/init-package-json */ -export default ({home}) => cwd => +export default ({home}, cwd) => Observable.create(observer => { const initFile = path.resolve(home, '.ied-init') diff --git a/src/install_cmd.js b/src/install_cmd.js index 358f28b..fd2ecd7 100644 --- a/src/install_cmd.js +++ b/src/install_cmd.js @@ -52,7 +52,7 @@ const shouldSave = argv => const shouldBuild = argv => argv.build -export default config => (cwd, argv) => { +export default (config, cwd, argv) => { const {isExplicit, isProd} = parseArgv(argv) const dir = path.join(cwd, 'node_modules') const target = '..' diff --git a/src/link_cmd.js b/src/link_cmd.js index cacc4ec..c067641 100644 --- a/src/link_cmd.js +++ b/src/link_cmd.js @@ -19,11 +19,12 @@ import {mkdirp} from './util' * create a symlink in `current-project/node_modules/tap` pointing to a * globally installed tap version). * + * @param {Object} config - Config object. * @param {string} cwd - Current working directory. * @param {Object} argv - Parsed command line arguments. * @return {Observable} Observable sequence. */ -export default config => (cwd, argv) => { +export default (config, cwd, argv) => { const names = argv._.slice(1) if (names.length) { diff --git a/src/ping_cmd.js b/src/ping_cmd.js index 7c6d9d4..d81d06b 100644 --- a/src/ping_cmd.js +++ b/src/ping_cmd.js @@ -5,7 +5,7 @@ import {ping} from './ping' * Pings the registry and print the received response. * @param {Object} config - Config object. * @param {Object} config.registry - CommonJS registry to be pinged. - * @return {Function} Actual command function. + * @return {Observable} Observable sequence of the registry's response. */ -export default ({registry}) => () => +export default ({registry}) => ping(registry)::_do(console.log) diff --git a/src/run_cmd.js b/src/run_cmd.js index bbaf996..5fadd46 100644 --- a/src/run_cmd.js +++ b/src/run_cmd.js @@ -49,9 +49,12 @@ export const run = (sh, shFlag, script, options = {}) => * @param {Object} config.sh - Command used for spawning new sub-shell session. * @param {Object} config.shFlag - Command line flags to be passed to the new * sub-shell command. - * @return {Function} Actual command function. + * @param {string} cwd - Current working directory. + * @param {Object} argv - Parsed command line arguments. + * @return {Observable} Observable sequence representing used for indicating + * the success / failure of the executed npm command. */ -export default config => (cwd, argv) => { +export default (config, cwd, argv) => { const {sh, shFlag} = config const scriptNames = argv._.slice(1) const pkgJson = fromFs(cwd) diff --git a/src/shell_cmd.js b/src/shell_cmd.js index 7716dbe..8d7b9b4 100644 --- a/src/shell_cmd.js +++ b/src/shell_cmd.js @@ -9,9 +9,11 @@ import {spawn} from 'child_process' * packages by using an amended `PATH`. * @param {Object} config - Config object. * @param {Object} config.sh - Command used for spawning new sub-shell session. - * @return {Function} Actual command function. + * @param {string} cwd - Current working directory. + * @return {Observable} Observable sequence wrapping the result of the output of + * the spawned child process. */ -export default config => cwd => { +export default (config, cwd) => { const binPath = path.join(cwd, 'node_modules/.bin') const env = { ...process.env, diff --git a/src/unlink_cmd.js b/src/unlink_cmd.js index 6a2758e..c6ff0a4 100644 --- a/src/unlink_cmd.js +++ b/src/unlink_cmd.js @@ -6,11 +6,12 @@ import {unlinkFromGlobal, unlinkToGlobal} from './link' * Unlinks one or more previously linked dependencies. can be invoked via * `ied unlink`. E.g. `ied unlink browserify tap webpack` would unlink all * _three_ dependencies. + * @param {Object} config - Config object. * @param {string} cwd - Current working directory. * @param {Object} argv - Parsed command line arguments. * @return {Observable} Empty observable sequence. */ -export default (cwd, argv) => { +export default (config, cwd, argv) => { const names = argv._.slice(1) return names.length From 4017cd3a76a243ab6884a00da69ba9e866233b74 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Thu, 22 Sep 2016 02:27:00 +0100 Subject: [PATCH 22/30] install_cmd: Improve docs --- src/install_cmd.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/install_cmd.js b/src/install_cmd.js index fd2ecd7..83190e7 100644 --- a/src/install_cmd.js +++ b/src/install_cmd.js @@ -14,6 +14,13 @@ import resolveAll from './resolve_all' import {fromArgv, fromFs, save} from './pkg_json' import {init as initCache} from './cache' +/** + * Installs all the dependencies and optionally tuns the individual (build) + * lifecycle scripts. + * @param {string} dir - Directory in which `ied` is running. + * @param {boolean} shouldBuild - If the dependencies should be build. + * @return {Observable} Empty observable sequence. + */ function installAll (dir, shouldBuild) { return concatStatic( mergeStatic( @@ -26,6 +33,14 @@ function installAll (dir, shouldBuild) { ) } +/** + * Parse the command line arguments. + * @param {Array.} options._ - List of dependencies to be installed, + * e.g. `express browserify`, + * @param {boolean} options.production - If ied is running in `--production` + * mode. + * @return {Object} Parsed `argv`. + */ const parseArgv = ({_, production}) => ({ isExplicit: !!(_.length - 1), isProd: production From 3b1daf0a4bee57fc19cf845e8a74c81496366615 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Thu, 22 Sep 2016 02:31:54 +0100 Subject: [PATCH 23/30] Fix fetch shasum logic --- src/fetch.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/fetch.js b/src/fetch.js index 6380285..558495e 100644 --- a/src/fetch.js +++ b/src/fetch.js @@ -13,11 +13,12 @@ import * as config from './config' import * as util from './util' import {normalizeBin} from './pkg_json' -export const checkShasum = (shasum, expected, tarball) => - void assert.equal(shasum, expected, +export const checkShasum = (shasum, expected, tarball) => { + assert.equal(shasum, expected, `shasum mismatch for ${tarball}: ${shasum} <-> ${expected}`) +} -const download = (tarball, expected, type) => +const download = (tarball, expected) => Observable.create(observer => { const shasum = crypto.createHash('sha1') const response = needle.get(tarball, config.httpOptions) @@ -26,11 +27,10 @@ const download = (tarball, expected, type) => const errorHandler = error => observer.error(error) const dataHandler = chunk => shasum.update(chunk) const finishHandler = () => { - const actualShasum = shasum.digest('hex') - // only actually check shasum integrity for npm tarballs - const expectedShasum = ['range', 'version', 'tag'].indexOf(type) !== -1 ? - actualShasum : expected - observer.next({tmpPath: cached.path, shasum: expectedShasum}) + observer.next({ + tmpPath: cached.path, + shasum: shasum.digest('hex') + }) observer.complete() } @@ -41,14 +41,16 @@ const download = (tarball, expected, type) => cached.on('finish', finishHandler) }) ::mergeMap(({tmpPath, shasum}) => { - if (expected) checkShasum(shasum, expected, tarball) + if (expected) { + checkShasum(shasum, expected, tarball) + } const newPath = path.join(config.cacheDir, shasum) return util.rename(tmpPath, newPath) }) export default function fetch (nodeModules) { - const {target, type, pkgJson: {name, bin, dist: {tarball, shasum}}} = this + const {target, pkgJson: {name, bin, dist: {tarball, shasum}}} = this const packageDir = path.join(nodeModules, target, 'package') return util.stat(packageDir) @@ -59,7 +61,7 @@ export default function fetch (nodeModules) { ::_catch(err => { if (err.code !== 'ENOENT') throw err return concatStatic( - download(tarball, shasum, type), + download(tarball, shasum), cache.extract(packageDir, shasum), util.fixPermissions(packageDir, normalizeBin({name, bin})) ) From 910c24fb4be63e31cd9a2f66686aa5f20082d089 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Sun, 25 Sep 2016 13:43:46 +0100 Subject: [PATCH 24/30] Refactor --- src/build_all.js | 5 ++- src/link_all.js | 14 +++++-- src/local.js | 2 +- src/registry.js | 2 +- src/resolve_all.js | 94 +++++++++++++++++++++++++++++++++------------- src/sign_cmd.js | 5 +++ src/util.js | 42 +++++++++++---------- 7 files changed, 111 insertions(+), 53 deletions(-) create mode 100644 src/sign_cmd.js diff --git a/src/build_all.js b/src/build_all.js index 9d85ca3..3d17a46 100644 --- a/src/build_all.js +++ b/src/build_all.js @@ -45,7 +45,10 @@ export const build = nodeModules => ({target, script}) => Observable.create(observer => { // some packages do expect a defined `npm_execpath` env, e.g. // https://github.com/chrisa/node-dtrace-provider/blob/v0.6.0/scripts/install.js#L19 - const env = {npm_execpath: '', ...process.env} + const env = { + npm_execpath: '', + ...process.env + } env.PATH = [ path.join(nodeModules, target, 'node_modules', '.bin'), diff --git a/src/link_all.js b/src/link_all.js index eb2cd01..003dcb8 100644 --- a/src/link_all.js +++ b/src/link_all.js @@ -4,7 +4,7 @@ import {map} from 'rxjs/operator/map' import {mergeMap} from 'rxjs/operator/mergeMap' import {normalizeBin} from './pkg_json' -const resolveSymlink = (src, dst) => +const resolveSymlink = ([src, dst]) => [path.relative(path.dirname(dst), src), dst] const getBinLinks = ({pkgJson, parentTarget, target}) => { @@ -26,9 +26,15 @@ const getDirectLink = ({parentTarget, target, name}) => { return [src, dst] } +const getAllLinks = dep => + [getDirectLink(dep), ...getBinLinks(dep)] + +const createSymlink = ([src, dst]) => + forceSymlink(src, dst) + export default function linkAll () { return this - ::mergeMap(dep => [getDirectLink(dep), ...getBinLinks(dep)]) - ::map(([src, dst]) => resolveSymlink(src, dst)) - ::mergeMap(([src, dst]) => forceSymlink(src, dst)) + ::mergeMap(getAllLinks) + ::map(resolveSymlink) + ::mergeMap(createSymlink) } diff --git a/src/local.js b/src/local.js index e7e9053..6609d68 100644 --- a/src/local.js +++ b/src/local.js @@ -35,7 +35,7 @@ const readTargetPkgJson = (dir, parentTarget, name) => { ) } -export const resolve = (dir, parentTarget, name) => +export default (dir, parentTarget, name) => readTargetPkgJson(dir, parentTarget, name) ::map(([target, pkgJson]) => ({ target, diff --git a/src/registry.js b/src/registry.js index 58667bb..e2e61e6 100644 --- a/src/registry.js +++ b/src/registry.js @@ -190,7 +190,7 @@ export const match = (name, version, { ::map(extractBody) ::map(findVersion(name, version)) -export const resolve = (nodeModules, parentTarget, name, version, options) => +export default (nodeModules, parentTarget, name, version, options) => match(name, version, options)::map(pkgJson => ({ parentTarget, pkgJson, diff --git a/src/resolve_all.js b/src/resolve_all.js index 9f74b47..e930c20 100644 --- a/src/resolve_all.js +++ b/src/resolve_all.js @@ -1,6 +1,7 @@ import {ArrayObservable} from 'rxjs/observable/ArrayObservable' import {EmptyObservable} from 'rxjs/observable/EmptyObservable' import {_finally} from 'rxjs/operator/finally' +import {_do} from 'rxjs/operator/do' import {concatStatic} from 'rxjs/operator/concat' import {expand} from 'rxjs/operator/expand' import {first} from 'rxjs/operator/first' @@ -8,8 +9,8 @@ import {mergeMap} from 'rxjs/operator/mergeMap' import {add, complete, report} from './progress' import {parseDependencies} from './pkg_json' -import {resolve as resolveFromLocal} from './local' -import {resolve as resolveFromRegistry} from './registry' +import resolveFromLocal from './local' +import resolveFromRegistry from './registry' /** * Properties of project-level `package.json` files that will be checked for @@ -34,35 +35,76 @@ export const DEPENDENCY_FIELDS = [ 'optionalDependencies' ] -function resolve (nodeModules, parentTarget, options) { - return this::mergeMap(([name, version]) => { - add() - report(`resolving ${name}@${version}`) +const logStartResolve = ([name, version]) => { + add() + report(`resolving ${name}@${version}`) +} - return concatStatic( - resolveFromLocal(nodeModules, parentTarget, name), - resolveFromRegistry(nodeModules, parentTarget, name, version, { - ...options.httpOptions, - registry: options.registry - }) - ) +function resolve (nodeModules, parentTarget, options) { + return this + ::_do(logStartResolve) + ::mergeMap(([name, version]) => + concatStatic( + resolveFromLocal(nodeModules, parentTarget, name), + resolveFromRegistry(nodeModules, parentTarget, name, version, { + ...options.httpOptions, + registry: options.registry + }) + ) ::first() ::_finally(complete) - }) + ) +} + +/** + * Helper class used for implementing a fast, mutable set using a plain + * JavaScript object. Using Set is unsupported on older Node versions and + * significantly slower — especially when exclusively using stringified values. + */ +class MutableSet { + /** + * Creates an instance. + * @constructor + */ + constructor () { + this.keys = Object.create(null) + } + + /** + * Adds a value to the set. This is slightly different from `Set#add` in the + * sense that it doesn't allow chained operations and only handles string + * values (hence `keys`). + * @param {string} key - Value to be added to the set + * @return {boolean} True if the value was add newly added to the set, false + * if the value was already contained in the set. + */ + add (key) { + if (this.keys[key]) return false + return (this.keys[key] = true) + } +} + +const getFields = ({target, isProd}) => ( + (target === '..' && !isProd) + ? ENTRY_DEPENDENCY_FIELDS + : DEPENDENCY_FIELDS +) + +const resolveAllInner = (nodeModules, options) => result => { + const fields = getFields(result) + const dependencies = parseDependencies(result.pkgJson, fields) + + return ArrayObservable.create(dependencies) + ::resolve(nodeModules, result.target, options) } export default function resolveAll (nodeModules, options) { - const targets = Object.create(null) - const entryTarget = '..' + const targets = new MutableSet() + const boundResolveAllInner = resolveAllInner(nodeModules, options) - return this::expand(result => { - if (targets[result.target]) { - return EmptyObservable.create() - } - targets[result.target] = true - const isEntry = result.target === entryTarget && !result.isProd - const fields = isEntry ? ENTRY_DEPENDENCY_FIELDS : DEPENDENCY_FIELDS - return ArrayObservable.create(parseDependencies(result.pkgJson, fields)) - ::resolve(nodeModules, result.target, options) - }) + return this::expand(result => ( + targets.add(result.target) + ? boundResolveAllInner(result) + : EmptyObservable.create() + )) } diff --git a/src/sign_cmd.js b/src/sign_cmd.js new file mode 100644 index 0000000..7b5f70c --- /dev/null +++ b/src/sign_cmd.js @@ -0,0 +1,5 @@ +/** + * Creates a merkle tree used for verifying the integrity of the resolved + * dependency graph. + */ + diff --git a/src/util.js b/src/util.js index 696168d..343f39e 100644 --- a/src/util.js +++ b/src/util.js @@ -17,18 +17,18 @@ import {mergeMap} from 'rxjs/operator/mergeMap' * @param {thisArg} [thisArg] - Optional context. * @return {Function} A cold observable sequence factory. */ -export const createObservableFactory = (fn, thisArg) => - (...args) => - Observable.create(observer => { - fn.apply(thisArg, [...args, (error, ...results) => { - if (error) { - observer.error(error) - } else { - results.forEach(result => observer.next(result)) - observer.complete() +export const createObservableFactory = (fn, thisArg) => (...args) => + Observable.create(observer => { + fn.apply(thisArg, [...args, (error, ...results) => { + if (error) observer.error(error) + else { + for (let i = 0; i < results.length; i++) { + observer.next(results[i]) } - }]) - }) + observer.complete() + } + }]) + }) /** * Sends a GET request to the given HTTP endpoint by passing the supplied @@ -78,20 +78,22 @@ export const forceSymlink = createObservableFactory(_forceSymlink) [`mkdirp`](https://www.npmjs.com/package/mkdirp). */ export const mkdirp = createObservableFactory(_mkdirp) +const entriesIterator = object => { + const results = [] + const keys = Object.keys(object) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + results.push([key, object[key]]) + } + return results +} + /** * Equivalent to `Map#entries` for observables, but operates on objects. * @return {Observable} Observable sequence of pairs. */ export function entries () { - return this::mergeMap(object => { - const results = [] - const keys = Object.keys(object) - for (let i = 0; i < keys.length; i++) { - const key = keys[i] - results.push([key, object[key]]) - } - return results - }) + return this::mergeMap(entriesIterator) } /** From c2c7213049187285fc53afb8004ba8ab42c9ec2c Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Tue, 27 Sep 2016 03:40:46 +0100 Subject: [PATCH 25/30] Refactor parentTarget / ids / fetch --- src/build_all.js | 14 ++++++------ src/fetch.js | 54 ++++++++++++++++++++++++++++++---------------- src/fetch_all.js | 2 +- src/git.js | 0 src/install_cmd.js | 4 ++-- src/link_all.js | 12 +++++------ src/local.js | 18 ++++++++-------- src/pkg_json.js | 4 ++-- src/registry.js | 6 +++--- src/resolve_all.js | 44 ++++++++++++++++++++++++++----------- src/tarball.js | 37 ++++++++++++++++++++++++------- src/util.js | 6 +++--- 12 files changed, 129 insertions(+), 72 deletions(-) create mode 100644 src/git.js diff --git a/src/build_all.js b/src/build_all.js index 3d17a46..912c1fa 100644 --- a/src/build_all.js +++ b/src/build_all.js @@ -37,11 +37,11 @@ export function FailedBuildError () { * Builds a dependency by executing the given lifecycle script. * @param {string} nodeModules - Absolute path of the `node_modules` directory. * @param {Object} dep - Dependency to be built. - * @param {string} dep.target - Relative location of the target directory. + * @param {string} dep.id - Relative location of the id directory. * @param {string} dep.script - Script to be executed (usually using `sh`). * @return {Observable} Observable sequence of the returned exit code. */ -export const build = nodeModules => ({target, script}) => +export const build = nodeModules => ({id, script}) => Observable.create(observer => { // some packages do expect a defined `npm_execpath` env, e.g. // https://github.com/chrisa/node-dtrace-provider/blob/v0.6.0/scripts/install.js#L19 @@ -51,13 +51,13 @@ export const build = nodeModules => ({target, script}) => } env.PATH = [ - path.join(nodeModules, target, 'node_modules', '.bin'), + path.join(nodeModules, id, 'node_modules', '.bin'), path.resolve(__dirname, '..', 'node_modules', '.bin'), process.env.PATH ].join(path.delimiter) const childProcess = spawn(config.sh, [config.shFlag, script], { - cwd: path.join(nodeModules, target, 'package'), + cwd: path.join(nodeModules, id, 'package'), env, stdio: 'inherit' }) @@ -73,14 +73,14 @@ export const build = nodeModules => ({target, script}) => /** * Extracts lifecycle scripts from supplied dependency. * @param {Dep} dep - Dependency to be parsed. - * @return {Array.} Array of script targets to be executed. + * @return {Array.} Array of script ids to be executed. */ -export const parseLifecycleScripts = ({target, pkgJson: {scripts = {}}}) => { +export const parseLifecycleScripts = ({id, pkgJson: {scripts = {}}}) => { const results = [] for (let i = 0; i < LIFECYCLE_SCRIPTS.length; i++) { const name = LIFECYCLE_SCRIPTS[i] const script = scripts[name] - if (script) results.push({target, script}) + if (script) results.push({id, script}) } return results } diff --git a/src/fetch.js b/src/fetch.js index 558495e..6a6bcc7 100644 --- a/src/fetch.js +++ b/src/fetch.js @@ -4,6 +4,7 @@ import needle from 'needle' import path from 'path' import {Observable} from 'rxjs/Observable' import {_catch} from 'rxjs/operator/catch' +import {_do} from 'rxjs/operator/do' import {concatStatic} from 'rxjs/operator/concat' import {ignoreElements} from 'rxjs/operator/ignoreElements' import {mergeMap} from 'rxjs/operator/mergeMap' @@ -13,13 +14,18 @@ import * as config from './config' import * as util from './util' import {normalizeBin} from './pkg_json' +import debuglog from './debuglog' + +const debug = debuglog('fetch') + export const checkShasum = (shasum, expected, tarball) => { assert.equal(shasum, expected, `shasum mismatch for ${tarball}: ${shasum} <-> ${expected}`) } -const download = (tarball, expected) => +export const download = (tarball) => Observable.create(observer => { + debug('downloading %s', tarball) const shasum = crypto.createHash('sha1') const response = needle.get(tarball, config.httpOptions) const cached = response.pipe(cache.write()) @@ -27,41 +33,51 @@ const download = (tarball, expected) => const errorHandler = error => observer.error(error) const dataHandler = chunk => shasum.update(chunk) const finishHandler = () => { - observer.next({ - tmpPath: cached.path, - shasum: shasum.digest('hex') - }) + observer.next([shasum.digest('hex'), cached.path]) observer.complete() } - response.on('data', dataHandler) - response.on('error', errorHandler) + response + .on('data', dataHandler) + .on('error', errorHandler) - cached.on('error', errorHandler) - cached.on('finish', finishHandler) + cached + .on('error', errorHandler) + .on('finish', finishHandler) }) - ::mergeMap(({tmpPath, shasum}) => { - if (expected) { - checkShasum(shasum, expected, tarball) - } +function verifyDownload (tarball, expected) { + return this::_do(([shasum]) => { + checkShasum(shasum, expected, tarball) + }) +} + +function indexDownload () { + return this::mergeMap(([shasum, tmpPath]) => { const newPath = path.join(config.cacheDir, shasum) return util.rename(tmpPath, newPath) }) +} -export default function fetch (nodeModules) { - const {target, pkgJson: {name, bin, dist: {tarball, shasum}}} = this - const packageDir = path.join(nodeModules, target, 'package') +export default function (nodeModules) { + const {id, pkgJson: {name, bin, dist: {tarball, shasum}}} = this + const packageDir = path.join(nodeModules, id, 'package') return util.stat(packageDir) ::_catch(err => { - if (err.code !== 'ENOENT') throw err + if (err.code !== 'ENOENT') { + throw err + } return cache.extract(packageDir, shasum) }) ::_catch(err => { - if (err.code !== 'ENOENT') throw err + if (err.code !== 'ENOENT') { + throw err + } return concatStatic( - download(tarball, shasum), + download(tarball) + ::verifyDownload(tarball, shasum) + ::indexDownload(), cache.extract(packageDir, shasum), util.fixPermissions(packageDir, normalizeBin({name, bin})) ) diff --git a/src/fetch_all.js b/src/fetch_all.js index abb15a9..08cb8d7 100644 --- a/src/fetch_all.js +++ b/src/fetch_all.js @@ -3,6 +3,6 @@ import {mergeMap} from 'rxjs/operator/mergeMap' export default function fetchAll (nodeModules) { return this - ::distinctKey('target') + ::distinctKey('id') ::mergeMap(dep => dep.fetch(nodeModules)) } diff --git a/src/git.js b/src/git.js new file mode 100644 index 0000000..e69de29 diff --git a/src/install_cmd.js b/src/install_cmd.js index 83190e7..9d63a10 100644 --- a/src/install_cmd.js +++ b/src/install_cmd.js @@ -70,7 +70,7 @@ const shouldBuild = argv => export default (config, cwd, argv) => { const {isExplicit, isProd} = parseArgv(argv) const dir = path.join(cwd, 'node_modules') - const target = '..' + const id = '..' // Generates the "source" package.json file from which dependencies are being // parsed and installed. @@ -85,7 +85,7 @@ export default (config, cwd, argv) => { const installedAll = srcPkgJson ::map(pkgJson => ({ pkgJson, - target, + id, isProd, isExplicit })) diff --git a/src/link_all.js b/src/link_all.js index 003dcb8..cfb89df 100644 --- a/src/link_all.js +++ b/src/link_all.js @@ -7,22 +7,22 @@ import {normalizeBin} from './pkg_json' const resolveSymlink = ([src, dst]) => [path.relative(path.dirname(dst), src), dst] -const getBinLinks = ({pkgJson, parentTarget, target}) => { +const getBinLinks = ({pkgJson, pId, id}) => { const binLinks = [] const bin = normalizeBin(pkgJson) const names = Object.keys(bin) for (let i = 0; i < names.length; i++) { const name = names[i] - const src = path.join('node_modules', target, 'package', bin[name]) - const dst = path.join('node_modules', parentTarget, 'node_modules', '.bin', name) + const src = path.join('node_modules', id, 'package', bin[name]) + const dst = path.join('node_modules', pId, 'node_modules', '.bin', name) binLinks.push([src, dst]) } return binLinks } -const getDirectLink = ({parentTarget, target, name}) => { - const src = path.join('node_modules', target, 'package') - const dst = path.join('node_modules', parentTarget, 'node_modules', name) +const getDirectLink = ({pId, id, name}) => { + const src = path.join('node_modules', id, 'package') + const dst = path.join('node_modules', pId, 'node_modules', name) return [src, dst] } diff --git a/src/local.js b/src/local.js index 6609d68..2998e1c 100644 --- a/src/local.js +++ b/src/local.js @@ -5,8 +5,8 @@ import {forkJoin} from 'rxjs/observable/forkJoin' import {map} from 'rxjs/operator/map' import {readlink, readFile} from './util' -const getLinkname = (dir, parentTarget, name) => - path.join(dir, parentTarget, 'node_modules', name) +const getLinkname = (dir, pId, name) => + path.join(dir, pId, 'node_modules', name) const getDir = dst => path.basename(path.dirname(dst)) @@ -25,8 +25,8 @@ const empty = () => // EmptyObservable.create. const fetch = empty -const readTargetPkgJson = (dir, parentTarget, name) => { - const linkname = getLinkname(dir, parentTarget, name) +const readTargetPkgJson = (dir, pId, name) => { + const linkname = getLinkname(dir, pId, name) const filename = path.join(linkname, 'package.json') return forkJoin( @@ -35,12 +35,12 @@ const readTargetPkgJson = (dir, parentTarget, name) => { ) } -export default (dir, parentTarget, name) => - readTargetPkgJson(dir, parentTarget, name) - ::map(([target, pkgJson]) => ({ - target, +export default (dir, pId, name) => + readTargetPkgJson(dir, pId, name) + ::map(([id, pkgJson]) => ({ + id, pkgJson, - parentTarget, + pId, name, fetch })) diff --git a/src/pkg_json.js b/src/pkg_json.js index a469beb..7fdfd08 100644 --- a/src/pkg_json.js +++ b/src/pkg_json.js @@ -127,8 +127,8 @@ const argvRegExp = /^(@?.+?)(?:@(.+)?)?$/ export const parseArgv = argv => { const names = argv._.slice(1) - const nameVersionPairs = fromPairs(names.map((target) => { - const nameVersion = argvRegExp.exec(target) + const nameVersionPairs = fromPairs(names.map((id) => { + const nameVersion = argvRegExp.exec(id) return [nameVersion[1], nameVersion[2] || '*'] })) diff --git a/src/registry.js b/src/registry.js index e2e61e6..a79fdee 100644 --- a/src/registry.js +++ b/src/registry.js @@ -190,11 +190,11 @@ export const match = (name, version, { ::map(extractBody) ::map(findVersion(name, version)) -export default (nodeModules, parentTarget, name, version, options) => +export default (nodeModules, pId, name, version, options) => match(name, version, options)::map(pkgJson => ({ - parentTarget, + pId, pkgJson, - target: pkgJson.dist.shasum, + id: pkgJson.dist.shasum, name, fetch })) diff --git a/src/resolve_all.js b/src/resolve_all.js index e930c20..c265bab 100644 --- a/src/resolve_all.js +++ b/src/resolve_all.js @@ -11,6 +11,7 @@ import {add, complete, report} from './progress' import {parseDependencies} from './pkg_json' import resolveFromLocal from './local' import resolveFromRegistry from './registry' +import resolveFromTarball from './tarball' /** * Properties of project-level `package.json` files that will be checked for @@ -40,16 +41,31 @@ const logStartResolve = ([name, version]) => { report(`resolving ${name}@${version}`) } -function resolve (nodeModules, parentTarget, options) { +function resolve (nodeModules, pId, options) { return this ::_do(logStartResolve) ::mergeMap(([name, version]) => concatStatic( - resolveFromLocal(nodeModules, parentTarget, name), - resolveFromRegistry(nodeModules, parentTarget, name, version, { - ...options.httpOptions, - registry: options.registry - }) + // Chain of responsibility + // resolveFromTarball( + // nodeModules, + // pId, + // name, + // version, + // options + // ), + resolveFromLocal( + nodeModules, + pId, + name + ), + resolveFromRegistry( + nodeModules, + pId, + name, + version, + options + ) ) ::first() ::_finally(complete) @@ -84,8 +100,8 @@ class MutableSet { } } -const getFields = ({target, isProd}) => ( - (target === '..' && !isProd) +const getFields = ({id, isProd}) => ( + (id === '..' && !isProd) ? ENTRY_DEPENDENCY_FIELDS : DEPENDENCY_FIELDS ) @@ -95,15 +111,19 @@ const resolveAllInner = (nodeModules, options) => result => { const dependencies = parseDependencies(result.pkgJson, fields) return ArrayObservable.create(dependencies) - ::resolve(nodeModules, result.target, options) + ::resolve(nodeModules, result.id, options) } -export default function resolveAll (nodeModules, options) { - const targets = new MutableSet() +export default function resolveAll (nodeModules, config) { + const ids = new MutableSet() + const options = { + ...config.httpOptions, + registry: config.registry + } const boundResolveAllInner = resolveAllInner(nodeModules, options) return this::expand(result => ( - targets.add(result.target) + ids.add(result.id) ? boundResolveAllInner(result) : EmptyObservable.create() )) diff --git a/src/tarball.js b/src/tarball.js index 568f6bd..33aebe9 100644 --- a/src/tarball.js +++ b/src/tarball.js @@ -1,10 +1,31 @@ // TODO -export const resolve = (nodeModules, parentTarget, name, version, options) => - match(name, version, options)::map(pkgJson => ({ - parentTarget, - pkgJson, - target: pkgJson.dist.shasum, - name, - fetch - })) +import url from 'url' +import {EmptyObservable} from 'rxjs/observable/EmptyObservable' +import {download} from './fetch' +import {mergeMap} from 'rxjs/operator/mergeMap' + +const supportedProtocols = { + 'http:': true, + 'https:': true +} + +export default (nodeModules, pId, name, version, options) => { + const {protocol} = url.parse(version) + if (!supportedProtocols[protocol]) { + return EmptyObservable.create() + } + + return download(version) + ::mergeMap(where => { + console.log('>>>', where) + }) +} + +// match(name, version, options)::map(pkgJson => ({ +// pId, +// pkgJson, +// id: pkgJson.dist.shasum, +// name, +// fetch +// })) diff --git a/src/util.js b/src/util.js index 343f39e..b8bdff0 100644 --- a/src/util.js +++ b/src/util.js @@ -123,17 +123,17 @@ const execMode = 0o777 & (~process.umask()) /** * Fixes the permissions of a downloaded dependencies. - * @param {string} target - Target directory to resolve from. + * @param {string} id - Target directory to resolve from. * @param {Object} bin - `package.json` bin object. * @return {Observable} Empty observable sequence. */ -export const fixPermissions = (target, bin) => { +export const fixPermissions = (id, bin) => { const paths = [] const names = Object.keys(bin) for (let i = 0; i < names.length; i++) { const name = names[i] - paths.push(path.resolve(target, bin[name])) + paths.push(path.resolve(id, bin[name])) } return ArrayObservable.create(paths) ::mergeMap(filepath => chmod(filepath, execMode)) From c260010b3f72c26b6133c40bf0dd64d35c75c7e7 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Tue, 27 Sep 2016 03:42:45 +0100 Subject: [PATCH 26/30] No longer use * import --- src/fetch.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/fetch.js b/src/fetch.js index 6a6bcc7..c75904d 100644 --- a/src/fetch.js +++ b/src/fetch.js @@ -10,8 +10,15 @@ import {ignoreElements} from 'rxjs/operator/ignoreElements' import {mergeMap} from 'rxjs/operator/mergeMap' import * as cache from './cache' -import * as config from './config' -import * as util from './util' +import { + httpOptions, + cacheDir +} from './config' +import { + rename, + stat, + fixPermissions +} from './util' import {normalizeBin} from './pkg_json' import debuglog from './debuglog' @@ -27,7 +34,7 @@ export const download = (tarball) => Observable.create(observer => { debug('downloading %s', tarball) const shasum = crypto.createHash('sha1') - const response = needle.get(tarball, config.httpOptions) + const response = needle.get(tarball, httpOptions) const cached = response.pipe(cache.write()) const errorHandler = error => observer.error(error) @@ -54,8 +61,8 @@ function verifyDownload (tarball, expected) { function indexDownload () { return this::mergeMap(([shasum, tmpPath]) => { - const newPath = path.join(config.cacheDir, shasum) - return util.rename(tmpPath, newPath) + const newPath = path.join(cacheDir, shasum) + return rename(tmpPath, newPath) }) } @@ -63,7 +70,7 @@ export default function (nodeModules) { const {id, pkgJson: {name, bin, dist: {tarball, shasum}}} = this const packageDir = path.join(nodeModules, id, 'package') - return util.stat(packageDir) + return stat(packageDir) ::_catch(err => { if (err.code !== 'ENOENT') { throw err @@ -79,7 +86,7 @@ export default function (nodeModules) { ::verifyDownload(tarball, shasum) ::indexDownload(), cache.extract(packageDir, shasum), - util.fixPermissions(packageDir, normalizeBin({name, bin})) + fixPermissions(packageDir, normalizeBin({name, bin})) ) }) ::ignoreElements() From 700725e77ba1870746ab6544806fdaf34f423b58 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Wed, 28 Sep 2016 03:08:30 +0100 Subject: [PATCH 27/30] Fix version default --- src/pkg_json.js | 14 +++++++++----- src/registry.js | 4 ++-- src/resolve_all.js | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pkg_json.js b/src/pkg_json.js index 7fdfd08..74ea6c1 100644 --- a/src/pkg_json.js +++ b/src/pkg_json.js @@ -129,14 +129,18 @@ export const parseArgv = argv => { const nameVersionPairs = fromPairs(names.map((id) => { const nameVersion = argvRegExp.exec(id) - return [nameVersion[1], nameVersion[2] || '*'] + return [nameVersion[1], nameVersion[2]] })) - const field = argv['save-dev'] ? 'devDependencies' - : argv['save-optional'] ? 'optionalDependencies' - : 'dependencies' + if (argv['save-dev']) { + return {devDependencies: nameVersionPairs} + } + + if (argv['save-optional']) { + return {optionalDependencies: nameVersionPairs} + } - return {[field]: nameVersionPairs} + return {dependencies: nameVersionPairs} } /** diff --git a/src/registry.js b/src/registry.js index a79fdee..ac3664b 100644 --- a/src/registry.js +++ b/src/registry.js @@ -179,7 +179,7 @@ const extractBody = ({body}) => body * @return {Observable} An observable sequence representing the asynchronously * resolved `package.json` document representing the dependency. */ -export const match = (name, version, { +const match = (name, version, { registry = REGISTRY, retryCount = RETRY_COUNT, ...options @@ -190,7 +190,7 @@ export const match = (name, version, { ::map(extractBody) ::map(findVersion(name, version)) -export default (nodeModules, pId, name, version, options) => +export default (nodeModules, pId, name, version = '*', options) => match(name, version, options)::map(pkgJson => ({ pId, pkgJson, diff --git a/src/resolve_all.js b/src/resolve_all.js index c265bab..da88122 100644 --- a/src/resolve_all.js +++ b/src/resolve_all.js @@ -38,7 +38,7 @@ export const DEPENDENCY_FIELDS = [ const logStartResolve = ([name, version]) => { add() - report(`resolving ${name}@${version}`) + report(`resolving ${name}@${version || '[no version]'}`) } function resolve (nodeModules, pId, options) { From fe8e9e2ee0b7604421407ca5184dab9bbe3e4681 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Sun, 2 Oct 2016 01:05:32 +0100 Subject: [PATCH 28/30] Inline registry#fetch --- src/fetch_all.js | 2 +- src/registry.js | 27 ++++++++------------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/fetch_all.js b/src/fetch_all.js index 08cb8d7..3023916 100644 --- a/src/fetch_all.js +++ b/src/fetch_all.js @@ -1,7 +1,7 @@ import {distinctKey} from 'rxjs/operator/distinctKey' import {mergeMap} from 'rxjs/operator/mergeMap' -export default function fetchAll (nodeModules) { +export default function (nodeModules) { return this ::distinctKey('id') ::mergeMap(dep => dep.fetch(nodeModules)) diff --git a/src/registry.js b/src/registry.js index ac3664b..0f8c341 100644 --- a/src/registry.js +++ b/src/registry.js @@ -170,16 +170,7 @@ export const checkStatus = (name, version) => ({ const extractBody = ({body}) => body -/** - * Resolves a package defined via an ambiguous semantic version string to a - * specific `package.json` file. - * @param {string} name Package name to be matched. - * @param {string} version Package version to be matched. - * @param {object} [options] HTTP and custom options. - * @return {Observable} An observable sequence representing the asynchronously - * resolved `package.json` document representing the dependency. - */ -const match = (name, version, { +export default (nodeModules, pId, name, version = '*', { registry = REGISTRY, retryCount = RETRY_COUNT, ...options @@ -189,12 +180,10 @@ const match = (name, version, { ::_do(checkStatus(name, version)) ::map(extractBody) ::map(findVersion(name, version)) - -export default (nodeModules, pId, name, version = '*', options) => - match(name, version, options)::map(pkgJson => ({ - pId, - pkgJson, - id: pkgJson.dist.shasum, - name, - fetch - })) + ::map(pkgJson => ({ + pId, + pkgJson, + id: pkgJson.dist.shasum, + name, + fetch + })) From e947bd888ddbefe9bb557602c116e2256e8e7eaa Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Tue, 4 Oct 2016 00:56:05 +0100 Subject: [PATCH 29/30] Fix tests --- test/unit/config_cmd.spec.js | 9 +++------ test/unit/shell_cmd.spec.js | 6 +++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/test/unit/config_cmd.spec.js b/test/unit/config_cmd.spec.js index 87b323e..48fef0d 100644 --- a/test/unit/config_cmd.spec.js +++ b/test/unit/config_cmd.spec.js @@ -3,21 +3,18 @@ import sinon from 'sinon' describe('configCmd', () => { const sandbox = sinon.sandbox.create() - let config afterEach(() => sandbox.restore()) - beforeEach(() => { - config = { + it('should print all config variables', () => { + const config = { 'key-0': 'value-0', 'key-1': 'value-1', 'key-2': 'value-2' } - }) - it('should print all config variables', () => { sandbox.stub(console, 'log') - configCmd(config)() + configCmd(config) Object.keys(config).forEach(key => { sinon.assert.calledWith(console.log, sinon.match(key)) sinon.assert.calledWith(console.log, sinon.match(String(config[key]))) diff --git a/test/unit/shell_cmd.spec.js b/test/unit/shell_cmd.spec.js index ec1be66..1180884 100644 --- a/test/unit/shell_cmd.spec.js +++ b/test/unit/shell_cmd.spec.js @@ -21,7 +21,7 @@ describe('shellCmd', () => { it('should spawn child process', () => { util.readdir.returns(ScalarObservable.create([])) - shellCmd(config)('/cwd').subscribe() + shellCmd(config, '/cwd').subscribe() sinon.assert.calledOnce(childProcess.spawn) sinon.assert.calledWith(childProcess.spawn, config.sh, [], { @@ -35,7 +35,7 @@ describe('shellCmd', () => { it('should add node_modules/.bin to PATH', () => { util.readdir.returns(ScalarObservable.create([])) - shellCmd(config)('/cwd').subscribe() + shellCmd(config, '/cwd').subscribe() const {env: {PATH}} = childProcess.spawn.getCall(0).args[2] assert.equal(PATH.indexOf('/cwd/node_modules/.bin:'), 0) @@ -44,7 +44,7 @@ describe('shellCmd', () => { it('should log available commands', () => { const cmds = ['browserify', 'tape', 'npm'] util.readdir.returns(ScalarObservable.create(cmds)) - shellCmd(config)('/cwd').subscribe() + shellCmd(config, '/cwd').subscribe() const out = console.log.getCall(0).args.join(' ') for (const cmd of cmds) { assert.notEqual(out.indexOf(cmd), -1, `should log ${cmd}`) From c8024fc8f18a3233a49e0a5573e9214a5c486a3c Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Mon, 7 Nov 2016 19:57:14 +0000 Subject: [PATCH 30/30] WIP --- example/package.json | 5 +++++ src/git.js | 1 + src/resolve_all.js | 48 ++++++++++++++++++-------------------------- src/tarball.js | 8 +++++--- 4 files changed, 31 insertions(+), 31 deletions(-) create mode 100644 example/package.json diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..7eb8c96 --- /dev/null +++ b/example/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "express": "https://github.com/expressjs/express/archive/master.zip" + } +} \ No newline at end of file diff --git a/src/git.js b/src/git.js index e69de29..0ffdd02 100644 --- a/src/git.js +++ b/src/git.js @@ -0,0 +1 @@ +// TODO \ No newline at end of file diff --git a/src/resolve_all.js b/src/resolve_all.js index da88122..84879ac 100644 --- a/src/resolve_all.js +++ b/src/resolve_all.js @@ -36,39 +36,30 @@ export const DEPENDENCY_FIELDS = [ 'optionalDependencies' ] -const logStartResolve = ([name, version]) => { +const logStartResolve = ([name, version = '[no version]']) => { add() - report(`resolving ${name}@${version || '[no version]'}`) + report(`resolving ${name}@${version}`) } +const compose = fns => (...args) => + // TODO Consider using a for-loop here, since V8 doesn't seem to be able to + // optimize this. + fns.map(fn => fn(...args)) + +// Chain of responsibility +const resolveAny = compose([ + resolveFromLocal, + resolveFromTarball, + resolveFromRegistry +]) + function resolve (nodeModules, pId, options) { return this ::_do(logStartResolve) ::mergeMap(([name, version]) => - concatStatic( - // Chain of responsibility - // resolveFromTarball( - // nodeModules, - // pId, - // name, - // version, - // options - // ), - resolveFromLocal( - nodeModules, - pId, - name - ), - resolveFromRegistry( - nodeModules, - pId, - name, - version, - options - ) - ) - ::first() - ::_finally(complete) + concatStatic(...resolveAny(nodeModules, pId, name, version, options)) + ::first() + ::_finally(complete) ) } @@ -107,11 +98,12 @@ const getFields = ({id, isProd}) => ( ) const resolveAllInner = (nodeModules, options) => result => { + const {pkgJson, id} = result const fields = getFields(result) - const dependencies = parseDependencies(result.pkgJson, fields) + const dependencies = parseDependencies(pkgJson, fields) return ArrayObservable.create(dependencies) - ::resolve(nodeModules, result.id, options) + ::resolve(nodeModules, id, options) } export default function resolveAll (nodeModules, config) { diff --git a/src/tarball.js b/src/tarball.js index 33aebe9..8baa060 100644 --- a/src/tarball.js +++ b/src/tarball.js @@ -1,5 +1,3 @@ -// TODO - import url from 'url' import {EmptyObservable} from 'rxjs/observable/EmptyObservable' import {download} from './fetch' @@ -11,7 +9,11 @@ const supportedProtocols = { } export default (nodeModules, pId, name, version, options) => { - const {protocol} = url.parse(version) + // When invoked via CLI, the top-level dependency won't have a version + // associated with it. In that case, we should use the name as url. + const source = version || name + + const {protocol} = url.parse(source) if (!supportedProtocols[protocol]) { return EmptyObservable.create() }