From 2841bfe819e27b8ec825f6eb9c64fe76222f0349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Cruz?= Date: Sat, 6 Apr 2013 00:21:28 +0100 Subject: [PATCH] Initial commit --- .gitignore | 4 + .jshintrc | 62 ++++ README.md | 219 +++++++++++++ lib/core/Package.js | 273 +++++++++++++++++ lib/core/UnitOfWork.js | 216 +++++++++++++ lib/core/config.js | 58 ++++ lib/core/createPackage.js | 20 ++ lib/core/packages/GitFsPackage.js | 43 +++ lib/core/packages/GitRemotePackage.js | 75 +++++ lib/core/packages/UrlPackage.js | 47 +++ lib/core/packages/index.js | 5 + lib/util/createError.js | 14 + package.json | 21 ++ test/test.js | 29 ++ test/unit_of_work.js | 423 ++++++++++++++++++++++++++ 15 files changed, 1509 insertions(+) create mode 100644 .gitignore create mode 100644 .jshintrc create mode 100644 README.md create mode 100644 lib/core/Package.js create mode 100644 lib/core/UnitOfWork.js create mode 100644 lib/core/config.js create mode 100644 lib/core/createPackage.js create mode 100644 lib/core/packages/GitFsPackage.js create mode 100644 lib/core/packages/GitRemotePackage.js create mode 100644 lib/core/packages/UrlPackage.js create mode 100644 lib/core/packages/index.js create mode 100644 lib/util/createError.js create mode 100644 package.json create mode 100644 test/test.js create mode 100644 test/unit_of_work.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..04c46376 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +Thumbs.db +node_modules +components \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000..acdf0b58 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,62 @@ +{ + "predef": [ + "console", + "describe", + "it", + "after", + "afterEach", + "before", + "beforeEach" + ], + + "indent": 4, + "node": true, + "devel": true, + + "bitwise": false, + "curly": false, + "eqeqeq": true, + "forin": false, + "immed": true, + "latedef": false, + "newcap": true, + "noarg": true, + "noempty": false, + "nonew": true, + "plusplus": false, + "regexp": false, + "undef": true, + "unused": true, + "quotmark": "single", + "strict": false, + "trailing": true, + + "asi": false, + "boss": false, + "debug": false, + "eqnull": true, + "es5": false, + "esnext": false, + "evil": false, + "expr": false, + "funcscope": false, + "globalstrict": false, + "iterator": false, + "lastsemic": false, + "laxbreak": true, + "laxcomma": false, + "loopfunc": true, + "multistr": false, + "onecase": true, + "regexdash": false, + "scripturl": false, + "smarttabs": false, + "shadow": false, + "sub": false, + "supernew": true, + "validthis": false, + + "nomen": false, + "onevar": false, + "white": true +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..be7eecd0 --- /dev/null +++ b/README.md @@ -0,0 +1,219 @@ +# Bower rewrite + +This repository is just an experiment around the new bower rewrite. +It will remain private and only trustworthy people will have access to it. +If the general consensus is to advance with it, the code will move to a new branch on the official repository. + +## Why? + +Bower codebase is becoming unmanageable, especially at its core. +Main issues are: + +- ___Monolithic Package.js that handles all package types (GitHub, url, local, etc).___ +- ___Package.js has too many nesting level of callbacks, causing confusion and making it hard to read___ +- Some commands, such as install and update, have incorrect behaviour (#200, #256) + - This is directly related with the current implementation of bower core: Package.js and Manager.js +- Programmatic usage needs improvement + - Some commands simply do not fire the `end` event + - Others fire the `error` event many times + - Some commands should fire more meaningful events (e.g.: install should fire each installed package) + +## Solution + +The rewrite will give a chance to make bower more manageable, solving the issues mentioned above while also improving the overall codebase. Readable code is crucial to increase the number of contributors and to the success of bower. + +Solutions to the main issues: + +- Polymorphism can be used to make different kind of packages share a common API. Different kind of packages will be implemented separatly but will share common functionality. This will be further explained bellow. +- Promises will resolve the nesting problem found in the codebase. +- TODO +- TODO + +## Implementation details + +### Package factory +Simple function that takes an endpoint with options and creates an intance of a package that obey the base package interface. + +```js +function createPackage(endpoint, options) -> Promise +``` + +This function could perform transformations/normalizations to the endpoint. +For instance, if `endpoint` is a shorthand it would expand it. +The function is actually async to allow query the bower registry to know the real endpoint. + +### Package.js -> EventEmitter + +The Package.js class extends EventEmitter. +Think of it as an abstract class that implements the package interface as well as serving as a base for other package types. + +#### Events + +- name_change (fired when the name of the package has changed) +- action (fired to inform the current action being performed by the package) + +------------ + +#### Constructor + +Package(endpoint, options) + +Options: + +- name - the package name (if none is passed, one will be guessed from the endpoint) +- range - a valid semver range (defaults to *) +- unitOfWork - the unit of work to use (if none is passed, one will be created) + +------------ + +Public functions + +#### Package#getName() -> String +Returns the package name. + +#### Package#getEndpoint() -> String +Returns the package endpoint. + +#### Package#getRange() -> String +Returns the semver range it should resolve to. + +#### Package#getTempDir() -> String +Returns the temporary directory that the package can use to resolve itself. + +#### Package#resolve() -> Promise +Resolves the package. +The resolve process obeys a very explicit flow: + +- Enqueues the package to be resolved in the unit of work and waits +- When accepted calls #_createTempDir and waits +- When done, calls #_resolveSelf and waits +- When done, calls #_readRc and waits +- When done, calls #_readJson and waits +- When done, calls #_parseJson and waits +- When done, marks the package as resolved and informs the unit of work +- Afterwards, calls #_resolveDependencies and waits + +#### Package#getResolveError() -> Error +Get the error occurred during the resolve process. +Returns null if no error occurred. + +#### Package#getJson() -> Object +Get the package component.json. +Throws an error if the package is not yet resolved. + +#### Package#getDependencies() -> Array +Get an array of packages that are direct dependencies of the package. +Throws an error if the package is not yet resolved. + +#### Package#install(directory) -> Promise +Installs the package into the specified directory. +The base implementation simply renames the temporary directory to the install directory. +If the install directory already exists, it will be deleted unless it is some kind of repository. +If so, the promise should be rejected with a meaningful error. +Throws an error if the package is not yet resolved. + +----------- + +Protected functions + +#### Package#_createTempDir() -> Promise +Creates a temporary dir. + +#### Package#_readRc() -> Promise +Reads the local .bowerrc configuration. + +#### Package#_readJson(rc) -> Promise +Reads the package component.json, possibly by using a dedicated `read-json` package that will be available in the bower organization. It will ensure everything is valid. + +### Package#_parseJson(json) -> Promise +Parses the json: + +- Checks if the packages name is different from the json one. If so and if the name was "guessed", the name of the package will be updated and a `name_change` event will be emited. +- Deletes files that are specified in the `ignore` property of the json from the temporary directory. +- For each dependency found in the json, a package should be created using the `createPackage` function. + +#### Package#_resolveDependencies() -> Promise +Cycle through all the package dependences, calling #resolve() on them. The promise is fulfilled only when all dependencies are resolved. + +-------- + +Abstract functions that must be implemented by concrete packages. + +#### Package#_resolveSelf() -> Promise +Resolves self. This method should be implemented by the concrete packages. For instance, the UrlPackage would download the contents of a URL into the temporary directory. + +### Type of packages + +The following packages will extend from the Package.js and will obey its interface. + +- `LocalPackage` extends `Package` (packages pointing to files of folders in the own system) +- `UrlPackage` extends `Package` (packages pointing to downloadable resources) +- `GitFsPackage` extends `Package` (git packages available in the own system) +- `GitRemotePackage` extends `Package` or `GitFsPackage` (remote git packages) +- `PublishedPackage` extends `Package` (? makes sense if bower supports a publish model, just like npm). +- `InstalledPackage` extends `Package` (locally installed packages located in the components folder) + +These type of packages will be known and created (instantiated) by the `createPackage`. + +This architecture will make it very easy for the community to create others package types, for instance, a MercurialLocalPackage and a MercurialRemotePackage. + + +### Unit of work -> EventEmitter + +The unit of work is a central entity in which state will be stored during the unroll process of the dependency tree. + +- Guarantees that a maximum of X packages are being resolved at every instant. +- Guarantees that packages with the same endpoint will not be resolved at the same time. +- Guarantees that packages with the exact same endpoint and range will not be resolved twice. +- Stores all the resolved/unresolved packages during the unroll of the dependency tree. +- When a package fails to resolve, it will make all the other enqueued ones to fail-fast. + +------------ + +#### Events + +- enqueue - fired when a package is enqueued +- dequeue - fired when a package is dequeued +- before_resolve - fired when a package is about to be resolved (fired after dequeue) +- resolve - fired when a package resolved successfully +- unresolve - fired when a package failed to resolve + +With this events, it will be possible to track the current status of each package during the expansion of the dependency tree. + +------------ + +#### Constructor + +UnitOfWork(options) + +Options: + +- maxConcurrent - maximum number of concurrent packages being resolved (defaults to 5) +- failFast - true to fail-fast if an error occurred while resolving a package (defaults to true) + +#### UnitOfWork#enqueue(package) -> Promise +Enqueues a package to be resolved. +The promise is fulfilled when the package is accepted to be resolved or is rejected if the unit of work is doomed to fail. +When fullfilled, a `done` function is passed that should be called when the resolve process of the package is finished: +Throws an error if the package is already queued or being resolved. + +- If the package failed resolving, it should be called with an instance of `Error`. In that case, the package will be marked as unresolved and all the remaining enqueued packages will have the `enqueue` promise rejected, making the whole process to fail-fast. +- If the packages succeed resolving, it should be called with no arguments. In that case, the package will be marked as resolved + +#### UnitOfWork#dequeue(package) -> Itself +Removes a previously enqueued package. + +#### UnitOfWork#getResolved(name) -> Itself +Returns an array of resolved packages whose names are `name`. +When called without a name, returns an object with all the resolved packages. + +#### UnitOfWork#getUnresolved(name) -> Itself +Returns an array of unresolved packages whose names are `name`. +When called without a name, returns an object with all the unresolved packages. + + +### Project / Manager -> EventEmitter + +TODO + + diff --git a/lib/core/Package.js b/lib/core/Package.js new file mode 100644 index 00000000..95f957c2 --- /dev/null +++ b/lib/core/Package.js @@ -0,0 +1,273 @@ +var util = require('util'); +var fs = require('fs'); +var path = require('path'); +var events = require('events'); +var mout = require('mout'); +var Q = require('q'); +var tmp = require('tmp'); +var UnitOfWork = require('./UnitOfWork'); +var config = require('./config'); +var createPackage; +var createError = require('../util/createError'); + +var Package = function (endpoint, options) { + options = options || {}; + + this._endpoint = endpoint; + this._name = options.name; + this._explicitName = !!this.name; + this._range = options.range || '*'; + this._unitOfWork = options.unitOfWork || new UnitOfWork(); +}; + +util.inherits(Package, events.EventEmitter); + +// ----------------- + +Package.prototype.getName = function () { + return this._name; +}; + +Package.prototype.getEndpoint = function () { + return this._endpoint; +}; + +Package.prototype.getRange = function () { + return this._range; +}; + +Package.prototype.getTempDir = function () { + return this._tempDir; +}; + +Package.prototype.resolve = function () { + // Throw if already resolved + if (this._resolved) { + throw createError('Package is already resolved', 'EALREADYRES'); + } + + // 1st - Enqueue the package in the unit of work + return this._unitOfWork.enqueue(this) + .then(function (done) { + // 2nd - Create temporary dir + return this._createTempDir() + // 3nd - Resolve self + .then(this._resolveSelf.bind(this)) + // 4th - Read local rc + .then(this._readRc.bind(this)) + // 5th - Read json + .then(this._readJson.bind(this)) + // 6th - Parse json + .then(this._parseJson.bind(this)) + // 7th - Mark as resolved & call done + // to inform the unit of work + .then(function (dependencies) { + this._resolved = true; + done(); + return dependencies; + }.bind(this), function (err) { + this._resolveError = err; + done(); + throw err; + }.bind(this)) + // 8th - Resolve dependencies + .then(this._resolveDependencies.bind(this)); + }.bind(this), function (err) { + // If error is of a duplicate package, + // copy everything from the resolved package (duplicate) to itself + if (err.code === 'EDUPL') { + mout.object.mixIn(this, err.pkg); + } else { + this._resolveError = err; + throw err; + } + }); +}; + +Package.prototype.getResolveError = function () { + return this._resolveError; +}; + +Package.prototype.getJson = function () { + this._assertResolved(); + return this._json; +}; + +Package.prototype.getDependencies = function () { + this._assertResolved(); + return this._dependencies; +}; + +Package.prototype.install = function () { + this._assertResolved(); + + // TODO +}; + +// ----------------- + +Package.prototype._resolveSelf = function () {}; + +// ----------------- + + +Package.prototype._createTempDir = function () { + console.log('_createTempDir'); + + // Resolved if cached + if (this._tempDir) { + return Q.fcall(this._tempDir); + } + + return Q.nfcall(tmp.dir, { + prefix: 'bower-' + this.name + '-', + mode: parseInt('0777', 8) & (~process.umask()) + }) + .then(function (dir) { + this._tempDir = dir; + return dir; + }.bind(this)); +}; + +Package.prototype._readRc = function () { + console.log('_readRc'); + + // Resolved if cached + if (this._rc) { + return Q.fcall(this._rc); + } + + var rcFile = path.join(this.getTempDir(), '.bowerrc'); + + // 1nd - Read rc as string + return Q.nfcall(fs.readFile, rcFile) + // 2nd - If successfull, parse it as json + // - If the file does not exist, make it the global config + .then(function (contents) { + try { + this._rc = JSON.parse(contents); + return this._rc; + } catch (e) { + throw createError('Unable to parse local ".bowerrc" file', 'EINVJSON', { + details: 'Unable to parse JSON file "' + rcFile + '": ' + e.message + }); + } + }.bind(this), function (err) { + // If the file does not exist, return the global config + if (err.code === 'ENOENT') { + return config; + } + + throw err; + }); +}; + +Package.prototype._readJson = function (rc) { + console.log('_readJson'); + + // Resolve if cached + if (this._json) { + return Q.fcall(this._json); + } + + var jsonFile = path.join(this.getTempDir(), rc.json); + + // 1nd - Read json as string + return Q.nfcall(fs.readFile, jsonFile) + // 2nd - If successfull, parse and validate json + // - If the file does not exist, make it an empty object + .then(function (contents) { + return Q.fcall(function () { + // TODO: change the read & validation to a separate package in the bower organization + try { + this._json = JSON.parse(contents); + return this._json; + } catch (e) { + throw createError('Unable to parse local "' + this._rc.json + '" file', 'EINVJSON', { + details: 'Unable to parse JSON file "' + jsonFile + '": ' + e.message + }); + } + }); + }.bind(this), function (err) { + // At this point, there was an error reading the file + // Throw if the error code is not ENOENT + if (err.code !== 'ENOENT') { + throw err; + } + + // If the json was already the component.json, + // simply assume an empty one + if (rc.json === 'component.json') { + this._json = {}; + return this._json; + } + // Otherwise, if the json is equal to the project's config + // try the standard 'component.json' + if (rc.json === config.json) { + return this._readJson(mout.object.mixIn(rc, { json: 'component.json' })); + } + // Otherwise, the json was a custom defined one at the package level + // try the project's config one + return this._readJson(mout.object.mixIn(rc, { json: config.json })); + }.bind(this)); +}; + +Package.prototype._parseJson = function (json) { + console.log('_parseJson'); + + // Resolve if cached + if (this._dependencies) { + return Q.fcall(this._dependencies); + } + + // 1st - Check if name defined in the json is different + // Only handle it if the package name was not explicitly set + if (!this._explicitName && json.name !== this.name) { + this.name = json.name; + this.emit('name_change', this.name); + } + + // 2nd - Handle ignore property + return Q.fcall(function () { + // Delete all the files specified in the ignore from the temporary directory + // TODO: + }.bind(this)) + // 3rd - Handle the dependencies property + .then(function () { + var key, + promises = []; + + // Read the dependencies, creating a package for each one + createPackage = createPackage || require('./createPackage'); + if (json.dependencies) { + for (key in json.dependencies) { + promises.push(createPackage(json.dependencies[key], { name: key, unitOfWork: this._unitOfWork })); + } + } + + // Since the create package actually returns a promise, we must resolve them all + return Q.all(promises).then(function (packages) { + this._dependencies = packages; + return packages; + }.bind(this)); + }); +}; + + +Package.prototype._resolveDependencies = function (dependencies) { + console.log('_resolveDependencies'); + + var promises = dependencies.map(function (dep) { + return dep.resolve(); + }); + + return Q.all(promises); +}; + +Package.prototype._assertResolved = function () { + if (!this._resolved) { + throw createError('Package is not yet resolved', 'ENOTRES'); + } +}; + +module.exports = Package; \ No newline at end of file diff --git a/lib/core/UnitOfWork.js b/lib/core/UnitOfWork.js new file mode 100644 index 00000000..306fdc8c --- /dev/null +++ b/lib/core/UnitOfWork.js @@ -0,0 +1,216 @@ +var Q = require('q'); +var util = require('util'); +var mout = require('mout'); +var events = require('events'); +var createError = require('../util/createError'); + +var UnitOfWork = function (options) { + // Ensure options defaults + this._options = mout.object.mixIn({ + failFast: true, + maxConcurrent: 5 + }, options); + + // Parse some of the options + this._options.failFast = !!this._options.failFast; + this._options.maxConcurrent = this._options.maxConcurrent > 0 ? this._options.maxConcurrent : 0; + + // Initialize some needed properties + this._queue = []; + this._beingResolved = []; + this._beingResolvedEndpoints = {}; + this._resolved = {}; + this._unresolved = {}; + this._completed = {}; +}; + +util.inherits(UnitOfWork, events.EventEmitter); + +// ----------------- + +UnitOfWork.prototype.enqueue = function (pkg) { + var deferred = Q.defer(), + index; + + // Throw it if already queued + index = this._indexOf(this._queue, pkg); + if (index !== -1) { + throw new Error('Package is already queued'); + } + + // Throw if already resolving + index = this._indexOf(this._beingResolved, pkg); + if (index !== -1) { + throw new Error('Package is already being resolved'); + } + + // Add to the queue + this._queue.push({ + pkg: pkg, + deferred: deferred + }); + this.emit('enqueue', pkg); + + // Process the queue shortly later so that handlers can be attached to the returned promise + Q.fcall(this._processQueue.bind(this)); + + return deferred.promise; +}; + +UnitOfWork.prototype.dequeue = function (pkg) { + var index; + + // Throw if the package is already is being resolved + index = this._indexOf(this._beingResolved, pkg); + if (index !== -1) { + throw new Error('Package is already being resolved'); + } + + // Attempt to remove from the queue + index = this._indexOf(this._queue, pkg); + if (index !== -1) { + this._queue.splice(index, 1); + this.emit('dequeue', pkg); + } + + return this; +}; + +UnitOfWork.prototype.getResolved = function (name) { + return name ? this._resolved[name] || [] : this._resolved; +}; + +UnitOfWork.prototype.getUnresolved = function (name) { + return name ? this._unresolved[name] || [] : this._unresolved; +}; + +// ----------------- + +UnitOfWork.prototype._processQueue = function () { + // If marked to fail all, reject everything + if (this._failAll) { + return this._rejectAll(); + } + + // Check if the number of allowed packages being resolved reached the maximum + if (this._options.maxConcurrent && this._beingResolved.length >= this._options.maxConcurrent) { + return; + } + + // Find candidates for the free spots + var freeSpots = this._options.maxConcurrent ? this._options.maxConcurrent - this._beingResolved.length : -1, + endpoint, + duplicate, + entry, + x; + + for (x = 0; x < this._queue.length && freeSpots; ++x) { + entry = this._queue[x]; + endpoint = entry.pkg.getEndpoint(); + + // Skip if there is a package being resolved with the same endpoint + if (this._beingResolvedEndpoints[endpoint]) { + continue; + } + + // Remove from the queue + this._queue.splice(x--, 1); + this.emit('dequeue', entry.pkg); + + // Check if the exact same package has been resolved (same endpoint and range) + // If so, we reject the promise with an appropriate error + duplicate = this._findDuplicate(entry.pkg); + if (duplicate) { + entry.deferred.reject(createError('Package with same endpoint and range was already resolved', 'EDUPL', { pkg: duplicate })); + continue; + } + + // Package is ok to resolve + // Put it in the being resolved list + this._beingResolved.push(entry); + this._beingResolvedEndpoints[endpoint] = true; + + // Decrement the free spots available + freeSpots--; + + // Resolve the promise to let the package know that it can proceed + this.emit('before_resolve', entry.pkg); + entry.deferred.resolve(this._onPackageDone.bind(this, entry.pkg)); + } +}; + +UnitOfWork.prototype._rejectAll = function () { + var error, + queue; + + // Reset the queue and being resolved list + queue = this._queue; + this._queue = []; + this._beingResolved = []; + this._beingResolvedEndpoints = {}; + + // Reject every deferred + error = createError('Package rejected to be resolved', 'EFFAST'); + queue.forEach(function (entry) { + entry.deferred.reject(error); + }); +}; + +UnitOfWork.prototype._onPackageDone = function (pkg, err) { + var pkgName = pkg.getName(), + pkgEndpoint = pkg.getEndpoint(), + arr, + index; + + // Ignore if already completed + if (this._completed[pkgEndpoint] && this._completed[pkgEndpoint].indexOf(pkg) !== -1) { + return; + } + + // Add it as completed + arr = this._completed[pkgEndpoint] = this._completed[pkgEndpoint] || []; + arr.push(pkg); + + // Remove the package from the being resolved list + index = this._indexOf(this._beingResolved, pkg); + this._beingResolved.splice(index, 1); + delete this._beingResolvedEndpoints[pkg.getEndpoint()]; + + // If called with no error then add it as resolved + if (!err) { + arr = this._resolved[pkgName] = this._resolved[pkgName] || []; + arr.push(pkg); + this.emit('resolve', pkg); + // Otherwise, it failed to resolve so we mark it as unresolved + } else { + arr = this._unresolved[pkgName] = this._unresolved[pkgName] || []; + arr.push(pkg); + this.emit('unresolve', pkg); + + // If fail fast is enabled, make every other package in the queue to fail + this._failAll = this._options.failFast; + } + + // Call process queue in order to allow packages to take over the free spots in the queue + this._processQueue(); +}; + +UnitOfWork.prototype._indexOf = function (arr, pkg) { + return mout.array.findIndex(arr, function (item) { + return item.pkg === pkg; + }); +}; + +UnitOfWork.prototype._findDuplicate = function (pkg) { + var arr = this._completed[pkg.getEndpoint()]; + + if (!arr) { + return null; + } + + return mout.array.find(arr, function (item) { + return item.getRange() === pkg.getRange(); + }); +}; + +module.exports = UnitOfWork; \ No newline at end of file diff --git a/lib/core/config.js b/lib/core/config.js new file mode 100644 index 00000000..b2e76ec3 --- /dev/null +++ b/lib/core/config.js @@ -0,0 +1,58 @@ +var path = require('path'); +var fs = require('fs'); +var mout = require('mout'); +var tmp = require('tmp'); + +// Guess some needed properties based on the user OS +var temp = process.env.TMPDIR + || process.env.TMP + || process.env.TEMP + || process.platform === 'win32' ? 'c:\\windows\\temp' : '/tmp'; + +var home = (process.platform === 'win32' + ? process.env.USERPROFILE + : process.env.HOME) || temp; + +var roaming = process.platform === 'win32' + ? path.resolve(process.env.APPDATA || home || temp) + : path.resolve(home || temp); + +var folder = process.platform === 'win32' + ? 'bower' + : '.bower'; + +var proxy = process.env.HTTPS_PROXY + || process.env.https_proxy + || process.env.HTTP_PROXY + || process.env.http_proxy; + +// Setup bower config +var config; +try { + config = require('rc')('bower', { + cwd: process.cwd(), // TODO: read working dir from the process argv, possibly using nopt + roaming: path.join(roaming, folder), + json: 'component.json', + directory: 'components', + proxy: proxy + }); +} catch (e) { + throw new Error('Unable to parse global .bowerrc file: ' + e.message); +} + +// If there is a local .bowerrc file, merge it +var localConfig = path.join(config.cwd, '.bowerrc'); +try { + localConfig = fs.readFileSync(localConfig); + try { + mout.object.mixIn(config, JSON.parse(localConfig)); + } catch (e) { + throw new Error('Unable to parse local .bowerrc file: ' + e.message); + } +} catch (e) {} + +// Configure tmp package to use graceful degradation +// If an uncaught exception occurs, the temporary directories will be deleted nevertheless +tmp.setGracefulCleanup(); + +module.exports = config; diff --git a/lib/core/createPackage.js b/lib/core/createPackage.js new file mode 100644 index 00000000..72b53d42 --- /dev/null +++ b/lib/core/createPackage.js @@ -0,0 +1,20 @@ +var Q = require('Q'); +var packages = require('./packages'); + +function createPackage(endpoint, options) { + var split = endpoint.split('#'), + range; + + // Extract the range from the endpoint + endpoint = split[0]; + range = split[1]; + + // Ensure options + options = options || {}; + options.range = options.range || range; + + // TODO: analyze endpoint and create appropriate package + return Q.fcall(new packages.UrlPackage(endpoint, options)); +} + +module.exports = createPackage; \ No newline at end of file diff --git a/lib/core/packages/GitFsPackage.js b/lib/core/packages/GitFsPackage.js new file mode 100644 index 00000000..78fdc394 --- /dev/null +++ b/lib/core/packages/GitFsPackage.js @@ -0,0 +1,43 @@ +var util = require('util'); +var Q = require('q'); +var Package = require('../Package'); + +var GitFsPackage = function (endpoint, options) { + Package.call(this, endpoint, options); +}; + +util.inherits(GitFsPackage, Package); + +// ----------------- + +GitFsPackage.prototype._resolveSelf = function () { + var promise; + + console.log('_resolveSelf of git local package'); + promise = this._copy() + .then(this._fetch.bind(this)) + .then(this._versions.bind(this)) + .then(this._checkout.bind(this)); + + return promise; +}; + +GitFsPackage.prototype._copy = function () { + // create temporary folder + // copy over +}; + +GitFsPackage.prototype._fetch = function () { + // fetch origin + // reset --hard +}; + +GitFsPackage.prototype._versions = function () { + // retrieve versions +}; + +GitFsPackage.prototype._checkout = function () { + // resolve range to a specific version and check it out +}; + +module.exports = GitFsPackage; diff --git a/lib/core/packages/GitRemotePackage.js b/lib/core/packages/GitRemotePackage.js new file mode 100644 index 00000000..575ac544 --- /dev/null +++ b/lib/core/packages/GitRemotePackage.js @@ -0,0 +1,75 @@ +var util = require('util'); +var Q = require('q'); +var Package = require('../Package'); + +var GitRemotePackage = function (endpoint, options) { + Package.call(this, endpoint, options); +}; + +util.inherits(GitRemotePackage, Package); + +// ----------------- + +GitRemotePackage.prototype._resolveSelf = function () { + var promise; + + console.log('_resolveSelf of git remote package'); + promise = this._clone() + .then(this._fetch.bind(this)) + .then(this._versions.bind(this)) + .then(this._checkout.bind(this)); + + return promise; +}; + +GitRemotePackage.prototype._clone = function () { + // check cache + // clone only if not cached + var deferred = Q.defer(); + + console.log('_clone'); + setTimeout(function () { + deferred.resolve(); + }, 1000); + + return deferred.promise; +}; + +GitRemotePackage.prototype._fetch = function () { + // fetch origin with --prune + // reset --hard origin/HEAD + var deferred = Q.defer(); + + console.log('_fetch'); + setTimeout(function () { + deferred.resolve(); + }, 1000); + + return deferred.promise; +}; + +GitRemotePackage.prototype._versions = function () { + // retrieve versions + var deferred = Q.defer(); + + console.log('_versions'); + setTimeout(function () { + deferred.resolve(); + }, 1000); + + return deferred.promise; +}; + +GitRemotePackage.prototype._checkout = function () { + // resolve range to a specific version and check it out + var deferred = Q.defer(); + + console.log('_checkout'); + setTimeout(function () { + deferred.resolve(); + }, 1000); + + return deferred.promise; +}; + +module.exports = GitRemotePackage; diff --git a/lib/core/packages/UrlPackage.js b/lib/core/packages/UrlPackage.js new file mode 100644 index 00000000..21b9e69b --- /dev/null +++ b/lib/core/packages/UrlPackage.js @@ -0,0 +1,47 @@ +var util = require('util'); +var Q = require('q'); +var Package = require('../Package'); + +var UrlPackage = function (endpoint, options) { + Package.call(this, endpoint, options); +}; + +util.inherits(UrlPackage, Package); + +// ----------------- + +UrlPackage.prototype._resolveSelf = function () { + var promise; + + console.log('_resolveSelf of url package'); + promise = this._download() + .then(this._extract.bind(this)); + + return promise; +}; + +UrlPackage.prototype._download = function () { + var deferred = Q.defer(); + + console.log('_download'); + setTimeout(function () { + deferred.resolve(); + }, 1000); + + return deferred.promise; +}; + +UrlPackage.prototype._extract = function () { + var deferred = Q.defer(); + + // If the file extension is not a zip and a tar, resolve the promise on next tick + + console.log('_extract'); + setTimeout(function () { + deferred.resolve(); + }, 1000); + + return deferred.promise; +}; + +module.exports = UrlPackage; \ No newline at end of file diff --git a/lib/core/packages/index.js b/lib/core/packages/index.js new file mode 100644 index 00000000..97e06876 --- /dev/null +++ b/lib/core/packages/index.js @@ -0,0 +1,5 @@ +module.exports = { + UrlPackage: require('./UrlPackage'), + GitFsPackage: require('./GitFsPackage'), + GitRemotePackage: require('./GitRemotePackage') +}; \ No newline at end of file diff --git a/lib/util/createError.js b/lib/util/createError.js new file mode 100644 index 00000000..73f10954 --- /dev/null +++ b/lib/util/createError.js @@ -0,0 +1,14 @@ +var mout = require('mout'); + +function createError(msg, code, properties) { + var err = new Error(msg); + err.code = code; + + if (properties) { + mout.object.mixIn(err, properties); + } + + return err; +} + +module.exports = createError; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..4a91df26 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "bower-rewrite", + "version": "0.0.0", + "description": "This repository is just an experiment around the new bower rewrite. It will remain private and only trustworthy people will have access to it. It's meant to be a common place to share thoughts.", + "dependencies": { + "mout": "~0.4.0", + "q": "~0.9.2", + "tmp": "0.0.16", + "rc": "~0.1.0" + }, + "devDependencies": { + "mocha": "~1.8.2", + "expect.js": "~0.2.0" + }, + "scripts": { + "test": "mocha -R spec" + }, + "author": "Twitter", + "license": "MIT", + "private": true +} diff --git a/test/test.js b/test/test.js new file mode 100644 index 00000000..f67a241d --- /dev/null +++ b/test/test.js @@ -0,0 +1,29 @@ +var UrlPackage = require('../lib/core/packages/UrlPackage'); +var GitRemotePackage = require('../lib/core/packages/GitRemotePackage'); + +function testUrlPackage() { + var bootstrapPackage = new UrlPackage('http://twitter.github.com/bootstrap/assets/bootstrap.zip', { name: 'bootstrap' }); + + return bootstrapPackage.resolve() + .then(function () { + console.log('ok!'); + }, function (err) { + console.log('failed to resolve', err); + }); +} + +function testGitRemotePackage() { + var dejavuPackage = new GitRemotePackage('git://github.com/IndigoUnited/dejavu.git', { name: 'bootstrap' }); + + return dejavuPackage.resolve() + .then(function () { + console.log('ok!'); + }, function (err) { + console.log('failed to resolve', err); + }); +} + +if (process.argv[1] && !/mocha/.test(process.argv[1])) { + testUrlPackage() + .then(testGitRemotePackage); +} \ No newline at end of file diff --git a/test/unit_of_work.js b/test/unit_of_work.js new file mode 100644 index 00000000..6350d090 --- /dev/null +++ b/test/unit_of_work.js @@ -0,0 +1,423 @@ +var expect = require('expect.js'); +var Package = require('../lib/core/Package.js'); +var UnitOfWork = require('../lib/core/UnitOfWork'); + +describe('UnitOfWork', function () { + describe('.enqueue', function () { + it('return a promise', function () { + var pkg = new Package('foo'), + unitOfWork = new UnitOfWork(), + promise; + + promise = unitOfWork.enqueue(pkg); + + expect(promise.then).to.be.a('function'); + }); + + it('should resolve the promise with a callback that should be called once the package is done resolving', function (done) { + var pkg = new Package('foo'), + unitOfWork = new UnitOfWork(); + + unitOfWork.enqueue(pkg) + .then(function (cb) { + expect(cb).to.be.a('function'); + cb(); + done(); + }, done); + }); + + it('should fire the "enqueue" event', function () { + var pkg = new Package('foo'), + unitOfWork = new UnitOfWork(), + fired = false; + + unitOfWork.on('enqueue', function (pkg) { + expect(pkg).to.be.an(Package); + fired = true; + }); + unitOfWork.enqueue(pkg); + + expect(fired).to.be(true); + }); + + it('should throw if the package is already queued', function () { + var pkg = new Package('foo'), + unitOfWork = new UnitOfWork(); + + unitOfWork.enqueue(pkg); + expect(function () { + unitOfWork.enqueue(pkg); + }).to.throwException(/already queued/); + }); + + it('should throw if the package is already being resolved', function (done) { + var pkg = new Package('foo'), + unitOfWork = new UnitOfWork(); + + unitOfWork.enqueue(pkg); + + setTimeout(function () { + expect(function () { + unitOfWork.enqueue(pkg); + }).to.throwException(/already being resolved/); + + done(); + }, 500); + }); + }); + + describe('.dequeue', function () { + it('should dequeue a package', function (done) { + var pkg = new Package('foo'), + unitOfWork = new UnitOfWork(), + promise, + error, + timeout; + + promise = unitOfWork.enqueue(pkg); + unitOfWork.dequeue(pkg); + + error = function () { + clearTimeout(timeout); + done(new Error('Package was not dequeued')); + }; + promise.then(error, error); + + timeout = setTimeout(done, 500); + }); + + it('should fire the "dequeue" event if the package was really dequeued', function () { + var pkg = new Package('foo'), + unitOfWork = new UnitOfWork(), + fired = false; + + unitOfWork.on('dequeue', function () { + fired = true; + }); + + unitOfWork.enqueue(pkg); + unitOfWork.dequeue(pkg); + + expect(fired).to.be(true); + }); + + it('should not fire the "dequeue" event if the package is not queued', function () { + var pkg = new Package('foo'), + unitOfWork = new UnitOfWork(), + fired = false; + + unitOfWork.on('dequeue', function () { + fired = true; + }); + + unitOfWork.dequeue(pkg); + + expect(fired).to.be(false); + }); + }); + + describe('.getResolved()', function () { + it('should always return a valid array/object', function () { + var unitOfWork = new UnitOfWork(); + + expect(unitOfWork.getResolved('foo')).to.eql([]); + expect(unitOfWork.getResolved()).to.eql({}); + }); + + it('should return resolved packages of a specific name', function (done) { + var unitOfWork = new UnitOfWork(), + pkg1 = new Package('foo', { name: 'foo' }), + pkg2 = new Package('bar', { name: 'bar' }), + pkg3 = new Package('foo', { name: 'foo', range: '~0.0.1' }), + pkg4 = new Package('bar', { name: 'bar', range: '~0.0.1' }), + arr; + + function ok(cb, time) { + return function (cb) { + setTimeout(cb, time); + }; + } + + unitOfWork.enqueue(pkg1).then(ok(50)); + unitOfWork.enqueue(pkg2).then(ok(50)); + unitOfWork.enqueue(pkg3).then(ok(100)); + unitOfWork.enqueue(pkg4).then(ok(100)); + + setTimeout(function () { + arr = unitOfWork.getResolved('foo'); + expect(arr.length).to.be(2); + expect(arr[0]).to.equal(pkg1); + expect(arr[1]).to.equal(pkg3); + arr = unitOfWork.getResolved('bar'); + expect(arr.length).to.be(2); + expect(arr[0]).to.equal(pkg2); + expect(arr[1]).to.equal(pkg4); + + done(); + }, 500); + }); + + it('should return all resolved packages', function (done) { + var unitOfWork = new UnitOfWork({ failFast: false }), + pkg1 = new Package('foo', { name: 'foo' }), + pkg2 = new Package('bar', { name: 'bar' }), + pkg3 = new Package('foo', { name: 'foo', range: '~0.0.1' }), + pkg4 = new Package('bar', { name: 'bar', range: '~0.0.1' }), + obj; + + function ok(cb, time) { + return function (cb) { + setTimeout(cb, time); + }; + } + + unitOfWork.enqueue(pkg1).then(ok(50)); + unitOfWork.enqueue(pkg2).then(ok(50)); + unitOfWork.enqueue(pkg3).then(ok(100)); + unitOfWork.enqueue(pkg4).then(ok(100)); + + setTimeout(function () { + obj = unitOfWork.getResolved(); + expect(Object.keys(obj)).to.eql(['foo', 'bar']); + expect(obj.foo).to.equal(unitOfWork.getResolved('foo')); + expect(obj.bar).to.equal(unitOfWork.getResolved('bar')); + + done(); + }, 500); + }); + }); + + describe('.getUnresolved()', function () { + it('should always return a valid array/object', function () { + var unitOfWork = new UnitOfWork(); + + expect(unitOfWork.getUnresolved('foo')).to.eql([]); + expect(unitOfWork.getUnresolved()).to.eql({}); + }); + + it('should return unresolved packages of a specific name', function (done) { + var unitOfWork = new UnitOfWork({ failFast: false }), + pkg1 = new Package('foo', { name: 'foo' }), + pkg2 = new Package('bar', { name: 'bar' }), + pkg3 = new Package('foo', { name: 'foo', range: '~0.0.1' }), + pkg4 = new Package('bar', { name: 'bar', range: '~0.0.1' }), + arr; + + function error(cb, time) { + return function (cb) { + setTimeout(cb.bind(cb, new Error('some error')), time); + }; + } + + unitOfWork.enqueue(pkg1).then(error(50)); + unitOfWork.enqueue(pkg2).then(error(50)); + unitOfWork.enqueue(pkg3).then(error(100)); + unitOfWork.enqueue(pkg4).then(error(100)); + + setTimeout(function () { + arr = unitOfWork.getUnresolved('foo'); + expect(arr.length).to.be(2); + expect(arr[0]).to.equal(pkg1); + expect(arr[1]).to.equal(pkg3); + arr = unitOfWork.getUnresolved('bar'); + expect(arr.length).to.be(2); + expect(arr[0]).to.equal(pkg2); + expect(arr[1]).to.equal(pkg4); + + done(); + }, 500); + }); + + it('should return all unresolved packages', function (done) { + var unitOfWork = new UnitOfWork({ failFast: false }), + pkg1 = new Package('foo', { name: 'foo' }), + pkg2 = new Package('bar', { name: 'bar' }), + pkg3 = new Package('foo', { name: 'foo', range: '~0.0.1' }), + pkg4 = new Package('bar', { name: 'bar', range: '~0.0.1' }), + obj; + + function error(cb, time) { + return function (cb) { + setTimeout(cb.bind(cb, new Error('some error')), time); + }; + } + + unitOfWork.enqueue(pkg1).then(error(50)); + unitOfWork.enqueue(pkg2).then(error(50)); + unitOfWork.enqueue(pkg3).then(error(100)); + unitOfWork.enqueue(pkg4).then(error(100)); + + setTimeout(function () { + obj = unitOfWork.getUnresolved(); + expect(Object.keys(obj)).to.eql(['foo', 'bar']); + expect(obj.foo).to.equal(unitOfWork.getUnresolved('foo')); + expect(obj.bar).to.equal(unitOfWork.getUnresolved('bar')); + + done(); + }, 500); + }); + }); + + describe('general stuff', function () { + it('should let only "maxConcurrent" packages to resolve at the same time', function (done) { + var unitOfWork = new UnitOfWork({ maxConcurrent: 2 }), + nrBeingResolved = 0, + pkg1 = new Package('foo'), + pkg2 = new Package('bar'), + pkg3 = new Package('baz'), + timeout; + + unitOfWork.enqueue(pkg1).then(function () { nrBeingResolved++; }); + unitOfWork.enqueue(pkg2).then(function () { nrBeingResolved++; }); + unitOfWork.enqueue(pkg3).then(function () { + clearTimeout(timeout); + done(new Error('Maximum concurrent packages not being accounted correctly')); + }); + + timeout = setTimeout(function () { + expect(nrBeingResolved).to.equal(2); + done(); + }, 500); + }); + + it('should let every package to resolve if the "maxConcurrent" option is less or equal than 0', function (done) { + var unitOfWork = new UnitOfWork({ maxConcurrent: 0 }), + unitOfWork2 = new UnitOfWork({ maxConcurrent: -1 }), + nrBeingResolved = 0, + pkg1 = new Package('foo1'), + pkg2 = new Package('bar1'), + pkg3 = new Package('baz1'), + pkg4 = new Package('foo2'), + pkg5 = new Package('bar2'), + pkg6 = new Package('baz2'), + timeout; + + function increment() { + nrBeingResolved++; + } + + unitOfWork.enqueue(pkg1).then(increment); + unitOfWork.enqueue(pkg2).then(increment); + unitOfWork.enqueue(pkg3).then(increment); + unitOfWork.enqueue(pkg4).then(increment); + unitOfWork.enqueue(pkg5).then(increment); + unitOfWork.enqueue(pkg6).then(increment); + unitOfWork2.enqueue(pkg1).then(increment); + unitOfWork2.enqueue(pkg2).then(increment); + unitOfWork2.enqueue(pkg3).then(increment); + unitOfWork2.enqueue(pkg4).then(increment); + unitOfWork2.enqueue(pkg5).then(increment); + unitOfWork2.enqueue(pkg6).then(increment); + + timeout = setTimeout(function () { + expect(nrBeingResolved).to.equal(12); + done(); + }, 500); + }); + + it('should prevent packages with same endpoint from being resolved at the same time', function (done) { + var unitOfWork = new UnitOfWork({ maxConcurrent: 2 }), + resolving = [], + pkg1 = new Package('foo'), + pkg2 = new Package('foo'), + pkg3 = new Package('baz'); + + unitOfWork.enqueue(pkg1).then(function () { resolving.push('foo'); }); + unitOfWork.enqueue(pkg2).then(function () { resolving.push('foo'); }); + unitOfWork.enqueue(pkg3).then(function () { resolving.push('baz'); }); + + setTimeout(function () { + expect(resolving).to.eql(['foo', 'baz']); + done(); + }, 500); + }); + + it('should reject promises when a package with same endpoint and range was already resolved', function (done) { + var unitOfWork = new UnitOfWork(), + pkg1 = new Package('foo', { range: '~0.1.1' }), + pkg2 = new Package('foo', { range: '~0.1.1' }); + + unitOfWork.enqueue(pkg1).then(function (cb) { + setTimeout(cb, 200); + }); + + unitOfWork.enqueue(pkg2).then(function () { + done(new Error('Should have detected a duplicate')); + }, function (err) { + expect(err.code).to.equal('EDUPL'); + expect(err.pkg).to.equal(pkg1); + done(); + }); + }); + + it('should fire "dequeue", "before_resolve", "resolve" and "unresolve" during the resolve process', function (done) { + var unitOfWork = new UnitOfWork(), + events = [], + pkg1 = new Package('foo'), + pkg2 = new Package('bar'); + + unitOfWork.on('dequeue', function (pkg) { + events.push('dequeue', pkg.getEndpoint()); + }); + unitOfWork.on('before_resolve', function (pkg) { + events.push('before_resolve', pkg.getEndpoint()); + }); + unitOfWork.on('resolve', function (pkg) { + events.push('resolve', pkg.getEndpoint()); + }); + unitOfWork.on('unresolve', function (pkg) { + events.push('unresolve', pkg.getEndpoint()); + }); + + unitOfWork.enqueue(pkg1).then(function (cb) { + setTimeout(cb, 100); + }); + unitOfWork.enqueue(pkg2).then(function (cb) { + setTimeout(cb.bind(cb, new Error('some error')), 200); + }); + + setTimeout(function () { + expect(events).to.eql([ + 'dequeue', pkg1.getEndpoint(), + 'before_resolve', pkg1.getEndpoint(), + 'dequeue', pkg2.getEndpoint(), + 'before_resolve', pkg2.getEndpoint(), + 'resolve', pkg1.getEndpoint(), + 'unresolve', pkg2.getEndpoint() + ]); + done(); + }, 500); + }); + + it('should fail fast by default', function (done) { + var unitOfWork = new UnitOfWork({ maxConcurrent: 1 }), + pkg1 = new Package('foo', { name: 'foo' }), + pkg2 = new Package('bar', { name: 'bar' }); + + unitOfWork.enqueue(pkg1).then(function (cb) { + setTimeout(cb.bind(cb, new Error('some error')), 200); + }); + unitOfWork.enqueue(pkg2).then(function () { + done(new Error('Should have failed fast')); + }, function (err) { + expect(err.code).to.equal('EFFAST'); + done(); + }); + }); + + it('should not fail fast if the "failFast" option is false', function (done) { + var unitOfWork = new UnitOfWork({ maxConcurrent: 1, failFast: false }), + pkg1 = new Package('foo', { name: 'foo' }), + pkg2 = new Package('bar', { name: 'bar' }); + + unitOfWork.enqueue(pkg1).then(function (cb) { + setTimeout(cb.bind(cb, new Error('some error')), 200); + }); + unitOfWork.enqueue(pkg2).then(function () { + done(); + }, function (err) { + done(err.code === 'EFFAST' ? new Error('Should not have failed fast') : null); + }); + }); + }); +}); \ No newline at end of file