diff --git a/.gitignore b/.gitignore index 7a19eaf8..dfed7ce1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /node_modules/ /dist/ /.idea/ +/yarn-error.log diff --git a/.travis.yml b/.travis.yml index a596eab3..4aee601c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,17 @@ +sudo: false language: node_js node_js: - - 0.10 + - 6 + +cache: yarn + env: matrix: - VERSION=1.2 - VERSION=1.3 - VERSION=1.4 + - VERSION=1.5 + - VERSION=1.6 global: - BROWSER_PROVIDER_READY_FILE=/tmp/sauce-connect-ready - LOGS_DIR=/tmp/angular-hint-build/logs @@ -15,10 +21,8 @@ env: install: - mkdir -p $LOGS_DIR - ./scripts/sauce_connect_setup.sh - - npm install -g npm - - npm install - - npm install -g karma-cli - - ./npm-install.sh + - yarn install --ignore-engines + - ./scripts/npm-install.sh - ./scripts/wait_for_browser_provider.sh script: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d4b69b0..f17161c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,14 +12,7 @@ Please read and follow our [Code of Conduct](https://github.com/angular/code-of- ## Open an Issue If you find a bug in AngularHint, you can help us by submitting an issue to our -[GitHub Repository](https://github.com/angular/angular-hint). If you can trace the issue to a specific AngularHint module, please go -to the module's repository to submit the issue. The current repositories for AngularHint modules are: - - [Angular Hint Controllers](https://github.com/angular/angular-hint-controllers) - - [Angular Hint Directives](https://github.com/angular/angular-hint-directives) - - [Angular Hint DOM](https://github.com/angular/angular-hint-dom) - - [Angular Hint Events](https://github.com/angular/angular-hint-events) - - [Angular Hint Interpolation](https://github.com/angular/angular-hint-interpolation) - - [Angular Hint Modules](https://github.com/angular/angular-hint-modules) +[GitHub Repository](https://github.com/angular/angular-hint). To help us effectively address your issue, please follow these steps: 1. Use GitHub to search for duplicate issues. If a similar issue already exists, add more @@ -56,33 +49,21 @@ send us a Pull Request. Following these steps will help us to include your code: ``` 3. Create your patch, **including appropriate test cases** 4. Follow our [Coding Style Guidelines](#style) - 5. Run the AngularHint protractor testing suite using the `gulp` command, or run an AngularHint - module's unit tests using `karma start`. Ensure that all tests pass. + 5. Run the AngularHint protractor testing suite using `yarn test-e2e`, or run an AngularHint + module's unit tests using `yarn test-unit`. Ensure that all tests pass. 6. Commit your changes using a descriptive commit message that follows our [commit guidelines](#commit) ```shell git commit -a ``` Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. - 7. Build your changes locally to ensure all the tests pass - For AngularHint: - - ```shell - gulp - ``` - - For an AngularHint module: - - ```shell - karma start - ``` - 8. Push your branch to GitHub: + 7. Push your branch to GitHub: ```shell git push origin my-fix-branch ``` - 9. In GitHub, send a pull request to AngularHint or the appropriate AngularHint module - 10. If we suggest changes then: + 8. In GitHub, send a pull request to AngularHint. + 9. If we suggest changes then: - Make the required updates - Re-run the tests and ensure they are still passing - Rebase your branch and force push to your GitHub repository (this will update your PR): @@ -91,7 +72,7 @@ send us a Pull Request. Following these steps will help us to include your code: git rebase master -i git push -f ``` - 11. After your pull request is merged you can delete your branch an pull the changes from the + 10. After your pull request is merged you can delete your branch an pull the changes from the main repository. - Delete the remote branch: @@ -121,8 +102,7 @@ send us a Pull Request. Following these steps will help us to include your code: ## Coding Style Guidelines - To help keep the code in AngularHint and the AngularHint modules consistent, please follow - these conventions. + To help keep the code in AngularHint consistent, please follow these conventions. ###1. AngularHint Guidelines @@ -138,8 +118,8 @@ send us a Pull Request. Following these steps will help us to include your code: - All features should be covered by appropriate tests. In AngularHint the e2e tests are run through protractor and contained in the `e2e` directory. In individual AngularHint - modules the unit tests are run through karma. Check the existing tests in the module you are working - with to see the conventions for that module. + modules the unit tests are run through karma. Check the existing tests in the module you are + working with to see the conventions for that module. #### Coding diff --git a/README.md b/README.md index 9a0a4643..4064a84b 100644 --- a/README.md +++ b/README.md @@ -51,11 +51,11 @@ Example: ## Building ```shell -$ npm install +$ yarn install ``` ```shell -$ npm run build +$ yarn build ``` ##Example In the `example` directory, you can find a sample application that appears diff --git a/config/karma.sauce.conf.js b/config/karma.sauce.conf.js index b3f0dbb5..3c352d9f 100644 --- a/config/karma.sauce.conf.js +++ b/config/karma.sauce.conf.js @@ -6,18 +6,18 @@ var CUSTOM_LAUNCHERS = { 'SL_Chrome': { base: 'SauceLabs', browserName: 'chrome', - version: '35' + version: 'latest' }, 'SL_Firefox': { base: 'SauceLabs', browserName: 'firefox', - version: '26' + version: 'latest' }, 'SL_Safari': { base: 'SauceLabs', browserName: 'safari', - platform: 'OS X 10.9', - version: '7' + platform: 'OS X 10.12', + version: '10' } }; diff --git a/config/karma.travis.conf.js b/config/karma.travis.conf.js index b1ca12e6..8055cb97 100644 --- a/config/karma.travis.conf.js +++ b/config/karma.travis.conf.js @@ -12,10 +12,6 @@ module.exports = function(options) { options.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; options.sauceLabs.startConnect = false; - // TODO(vojta): remove once SauceLabs supports websockets. - // This speeds up the capturing a bit, as browsers don't even try to use websocket. - options.transports = ['xhr-polling']; - options.singleRun = true; }; diff --git a/dist/hint.js b/dist/hint.js index b1219031..4e996aee 100755 --- a/dist/hint.js +++ b/dist/hint.js @@ -176,25 +176,47 @@ function defaultHash () { function configure(conf) { if (conf) { - this._conf = conf; conf.delimiter && (this.delimiter = conf.delimiter); - conf.maxListeners && (this._events.maxListeners = conf.maxListeners); + this._maxListeners = conf.maxListeners !== undefined ? conf.maxListeners : defaultMaxListeners; + conf.wildcard && (this.wildcard = conf.wildcard); conf.newListener && (this.newListener = conf.newListener); + conf.verboseMemoryLeak && (this.verboseMemoryLeak = conf.verboseMemoryLeak); if (this.wildcard) { this.listenerTree = {}; } + } else { + this._maxListeners = defaultMaxListeners; + } + } + + function logPossibleMemoryLeak(count, eventName) { + var errorMsg = '(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.'; + + if(this.verboseMemoryLeak){ + errorMsg += ' Event name: %s.'; + console.error(errorMsg, count, eventName); + } else { + console.error(errorMsg, count); + } + + if (console.trace){ + console.trace(); } } function EventEmitter(conf) { this._events = {}; this.newListener = false; + this.verboseMemoryLeak = false; configure.call(this, conf); } + EventEmitter.EventEmitter2 = EventEmitter; // backwards compatibility for exporting EventEmitter property // // Attention, function return type now is array, always ! @@ -323,7 +345,7 @@ function defaultHash () { var tree = this.listenerTree; var name = type.shift(); - while (name) { + while (name !== undefined) { if (!tree[name]) { tree[name] = {}; @@ -336,30 +358,20 @@ function defaultHash () { if (!tree._listeners) { tree._listeners = listener; } - else if(typeof tree._listeners === 'function') { - tree._listeners = [tree._listeners, listener]; - } - else if (isArray(tree._listeners)) { + else { + if (typeof tree._listeners === 'function') { + tree._listeners = [tree._listeners]; + } tree._listeners.push(listener); - if (!tree._listeners.warned) { - - var m = defaultMaxListeners; - - if (typeof this._events.maxListeners !== 'undefined') { - m = this._events.maxListeners; - } - - if (m > 0 && tree._listeners.length > m) { - - tree._listeners.warned = true; - console.error('(node) warning: possible EventEmitter memory ' + - 'leak detected. %d listeners added. ' + - 'Use emitter.setMaxListeners() to increase limit.', - tree._listeners.length); - console.trace(); - } + if ( + !tree._listeners.warned && + this._maxListeners > 0 && + tree._listeners.length > this._maxListeners + ) { + tree._listeners.warned = true; + logPossibleMemoryLeak.call(this, tree._listeners.length, name); } } return true; @@ -379,20 +391,38 @@ function defaultHash () { EventEmitter.prototype.delimiter = '.'; EventEmitter.prototype.setMaxListeners = function(n) { - this._events || init.call(this); - this._events.maxListeners = n; - if (!this._conf) this._conf = {}; - this._conf.maxListeners = n; + if (n !== undefined) { + this._maxListeners = n; + if (!this._conf) this._conf = {}; + this._conf.maxListeners = n; + } }; EventEmitter.prototype.event = ''; + EventEmitter.prototype.once = function(event, fn) { - this.many(event, 1, fn); + return this._once(event, fn, false); + }; + + EventEmitter.prototype.prependOnceListener = function(event, fn) { + return this._once(event, fn, true); + }; + + EventEmitter.prototype._once = function(event, fn, prepend) { + this._many(event, 1, fn, prepend); return this; }; EventEmitter.prototype.many = function(event, ttl, fn) { + return this._many(event, ttl, fn, false); + } + + EventEmitter.prototype.prependMany = function(event, ttl, fn) { + return this._many(event, ttl, fn, true); + } + + EventEmitter.prototype._many = function(event, ttl, fn, prepend) { var self = this; if (typeof fn !== 'function') { @@ -403,12 +433,12 @@ function defaultHash () { if (--ttl === 0) { self.off(event, listener); } - fn.apply(this, arguments); + return fn.apply(this, arguments); } listener._origin = fn; - this.on(event, listener); + this._on(event, listener, prepend); return self; }; @@ -420,91 +450,242 @@ function defaultHash () { var type = arguments[0]; if (type === 'newListener' && !this.newListener) { - if (!this._events.newListener) { return false; } + if (!this._events.newListener) { + return false; + } } - // Loop through the *_all* functions and invoke them. - if (this._all) { - var l = arguments.length; - var args = new Array(l - 1); - for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; - for (i = 0, l = this._all.length; i < l; i++) { + var al = arguments.length; + var args,l,i,j; + var handler; + + if (this._all && this._all.length) { + handler = this._all.slice(); + if (al > 3) { + args = new Array(al); + for (j = 0; j < al; j++) args[j] = arguments[j]; + } + + for (i = 0, l = handler.length; i < l; i++) { this.event = type; - this._all[i].apply(this, args); + switch (al) { + case 1: + handler[i].call(this, type); + break; + case 2: + handler[i].call(this, type, arguments[1]); + break; + case 3: + handler[i].call(this, type, arguments[1], arguments[2]); + break; + default: + handler[i].apply(this, args); + } } } - // If there is no 'error' event listener then throw. - if (type === 'error') { - - if (!this._all && - !this._events.error && - !(this.wildcard && this.listenerTree.error)) { + if (this.wildcard) { + handler = []; + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + searchListenerTree.call(this, handler, ns, this.listenerTree, 0); + } else { + handler = this._events[type]; + if (typeof handler === 'function') { + this.event = type; + switch (al) { + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + default: + args = new Array(al - 1); + for (j = 1; j < al; j++) args[j - 1] = arguments[j]; + handler.apply(this, args); + } + return true; + } else if (handler) { + // need to make copy of handlers because list can change in the middle + // of emit call + handler = handler.slice(); + } + } - if (arguments[1] instanceof Error) { - throw arguments[1]; // Unhandled 'error' event - } else { - throw new Error("Uncaught, unspecified 'error' event."); + if (handler && handler.length) { + if (al > 3) { + args = new Array(al - 1); + for (j = 1; j < al; j++) args[j - 1] = arguments[j]; + } + for (i = 0, l = handler.length; i < l; i++) { + this.event = type; + switch (al) { + case 1: + handler[i].call(this); + break; + case 2: + handler[i].call(this, arguments[1]); + break; + case 3: + handler[i].call(this, arguments[1], arguments[2]); + break; + default: + handler[i].apply(this, args); } - return false; } + return true; + } else if (!this._all && type === 'error') { + if (arguments[1] instanceof Error) { + throw arguments[1]; // Unhandled 'error' event + } else { + throw new Error("Uncaught, unspecified 'error' event."); + } + return false; + } + + return !!this._all; + }; + + EventEmitter.prototype.emitAsync = function() { + + this._events || init.call(this); + + var type = arguments[0]; + + if (type === 'newListener' && !this.newListener) { + if (!this._events.newListener) { return Promise.resolve([false]); } } + var promises= []; + + var al = arguments.length; + var args,l,i,j; var handler; - if(this.wildcard) { + if (this._all) { + if (al > 3) { + args = new Array(al); + for (j = 1; j < al; j++) args[j] = arguments[j]; + } + for (i = 0, l = this._all.length; i < l; i++) { + this.event = type; + switch (al) { + case 1: + promises.push(this._all[i].call(this, type)); + break; + case 2: + promises.push(this._all[i].call(this, type, arguments[1])); + break; + case 3: + promises.push(this._all[i].call(this, type, arguments[1], arguments[2])); + break; + default: + promises.push(this._all[i].apply(this, args)); + } + } + } + + if (this.wildcard) { handler = []; var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); searchListenerTree.call(this, handler, ns, this.listenerTree, 0); - } - else { + } else { handler = this._events[type]; } if (typeof handler === 'function') { this.event = type; - if (arguments.length === 1) { - handler.call(this); + switch (al) { + case 1: + promises.push(handler.call(this)); + break; + case 2: + promises.push(handler.call(this, arguments[1])); + break; + case 3: + promises.push(handler.call(this, arguments[1], arguments[2])); + break; + default: + args = new Array(al - 1); + for (j = 1; j < al; j++) args[j - 1] = arguments[j]; + promises.push(handler.apply(this, args)); } - else if (arguments.length > 1) - switch (arguments.length) { - case 2: - handler.call(this, arguments[1]); - break; - case 3: - handler.call(this, arguments[1], arguments[2]); - break; - // slower - default: - var l = arguments.length; - var args = new Array(l - 1); - for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; - handler.apply(this, args); - } - return true; - } - else if (handler) { - var l = arguments.length; - var args = new Array(l - 1); - for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; - - var listeners = handler.slice(); - for (var i = 0, l = listeners.length; i < l; i++) { + } else if (handler && handler.length) { + handler = handler.slice(); + if (al > 3) { + args = new Array(al - 1); + for (j = 1; j < al; j++) args[j - 1] = arguments[j]; + } + for (i = 0, l = handler.length; i < l; i++) { this.event = type; - listeners[i].apply(this, args); + switch (al) { + case 1: + promises.push(handler[i].call(this)); + break; + case 2: + promises.push(handler[i].call(this, arguments[1])); + break; + case 3: + promises.push(handler[i].call(this, arguments[1], arguments[2])); + break; + default: + promises.push(handler[i].apply(this, args)); + } + } + } else if (!this._all && type === 'error') { + if (arguments[1] instanceof Error) { + return Promise.reject(arguments[1]); // Unhandled 'error' event + } else { + return Promise.reject("Uncaught, unspecified 'error' event."); } - return (listeners.length > 0) || !!this._all; - } - else { - return !!this._all; } + return Promise.all(promises); }; EventEmitter.prototype.on = function(type, listener) { + return this._on(type, listener, false); + }; + + EventEmitter.prototype.prependListener = function(type, listener) { + return this._on(type, listener, true); + }; + + EventEmitter.prototype.onAny = function(fn) { + return this._onAny(fn, false); + }; + + EventEmitter.prototype.prependAny = function(fn) { + return this._onAny(fn, true); + }; + + EventEmitter.prototype.addListener = EventEmitter.prototype.on; + + EventEmitter.prototype._onAny = function(fn, prepend){ + if (typeof fn !== 'function') { + throw new Error('onAny only accepts instances of Function'); + } + + if (!this._all) { + this._all = []; + } + // Add the function to the event listener collection. + if(prepend){ + this._all.unshift(fn); + }else{ + this._all.push(fn); + } + + return this; + } + + EventEmitter.prototype._on = function(type, listener, prepend) { if (typeof type === 'function') { - this.onAny(type); + this._onAny(type, listener); return this; } @@ -517,7 +698,7 @@ function defaultHash () { // adding it to the listeners, first emit "newListeners". this.emit('newListener', type, listener); - if(this.wildcard) { + if (this.wildcard) { growListenerTree.call(this, type, listener); return this; } @@ -526,53 +707,32 @@ function defaultHash () { // Optimize the case of one listener. Don't need the extra array object. this._events[type] = listener; } - else if(typeof this._events[type] === 'function') { - // Adding the second element, need to change to array. - this._events[type] = [this._events[type], listener]; - } - else if (isArray(this._events[type])) { - // If we've already got an array, just append. - this._events[type].push(listener); + else { + if (typeof this._events[type] === 'function') { + // Change to array. + this._events[type] = [this._events[type]]; + } + // If we've already got an array, just add + if(prepend){ + this._events[type].unshift(listener); + }else{ + this._events[type].push(listener); + } + // Check for listener leak - if (!this._events[type].warned) { - - var m = defaultMaxListeners; - - if (typeof this._events.maxListeners !== 'undefined') { - m = this._events.maxListeners; - } - - if (m > 0 && this._events[type].length > m) { - - this._events[type].warned = true; - console.error('(node) warning: possible EventEmitter memory ' + - 'leak detected. %d listeners added. ' + - 'Use emitter.setMaxListeners() to increase limit.', - this._events[type].length); - console.trace(); - } + if ( + !this._events[type].warned && + this._maxListeners > 0 && + this._events[type].length > this._maxListeners + ) { + this._events[type].warned = true; + logPossibleMemoryLeak.call(this, this._events[type].length, type); } } - return this; - }; - - EventEmitter.prototype.onAny = function(fn) { - - if (typeof fn !== 'function') { - throw new Error('onAny only accepts instances of Function'); - } - if(!this._all) { - this._all = []; - } - - // Add the function to the event listener collection. - this._all.push(fn); return this; - }; - - EventEmitter.prototype.addListener = EventEmitter.prototype.on; + } EventEmitter.prototype.off = function(type, listener) { if (typeof listener !== 'function') { @@ -627,6 +787,9 @@ function defaultHash () { delete this._events[type]; } } + + this.emit("removeListener", type, listener); + return this; } else if (handlers === listener || @@ -638,8 +801,30 @@ function defaultHash () { else { delete this._events[type]; } + + this.emit("removeListener", type, listener); + } + } + + function recursivelyGarbageCollect(root) { + if (root === undefined) { + return; + } + var keys = Object.keys(root); + for (var i in keys) { + var key = keys[i]; + var obj = root[key]; + if ((obj instanceof Function) || (typeof obj !== "object") || (obj === null)) + continue; + if (Object.keys(obj).length > 0) { + recursivelyGarbageCollect(root[key]); + } + if (Object.keys(obj).length === 0) { + delete root[key]; + } } } + recursivelyGarbageCollect(this.listenerTree); return this; }; @@ -651,10 +836,14 @@ function defaultHash () { for(i = 0, l = fns.length; i < l; i++) { if(fn === fns[i]) { fns.splice(i, 1); + this.emit("removeListenerAny", fn); return this; } } } else { + fns = this._all; + for(i = 0, l = fns.length; i < l; i++) + this.emit("removeListenerAny", fns[i]); this._all = []; } return this; @@ -668,7 +857,7 @@ function defaultHash () { return this; } - if(this.wildcard) { + if (this.wildcard) { var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); var leafs = searchListenerTree.call(this, null, ns, this.listenerTree, 0); @@ -677,15 +866,14 @@ function defaultHash () { leaf._listeners = null; } } - else { - if (!this._events[type]) return this; + else if (this._events) { this._events[type] = null; } return this; }; EventEmitter.prototype.listeners = function(type) { - if(this.wildcard) { + if (this.wildcard) { var handlers = []; var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); searchListenerTree.call(this, handlers, ns, this.listenerTree, 0); @@ -701,6 +889,14 @@ function defaultHash () { return this._events[type]; }; + EventEmitter.prototype.eventNames = function(){ + return Object.keys(this._events); + } + + EventEmitter.prototype.listenerCount = function(type) { + return this.listeners(type).length; + }; + EventEmitter.prototype.listenersAny = function() { if(this._all) { @@ -719,7 +915,7 @@ function defaultHash () { }); } else if (typeof exports === 'object') { // CommonJS - exports.EventEmitter2 = EventEmitter; + module.exports = EventEmitter; } else { // Browser global. @@ -1662,6 +1858,7 @@ var TYPES = [ function scopeDescriptor (elt, scope) { var val, theseTypes = [], + noDataDefault = 'scope.$id=' + scope.$id, type; if (elt) { @@ -1672,11 +1869,33 @@ function scopeDescriptor (elt, scope) { } } } - if (theseTypes.length === 0) { - return 'scope.$id=' + scope.$id; - } else { - return theseTypes.join(' '); + if (theseTypes.length) { + // We have info from the HTML + noDataDefault = theseTypes.join(' '); + + if (theseTypes[0].indexOf(' as ') > -1) { + // It's controllerAs + var caPrefix = theseTypes[0].match(/ as ([^"]+)"/); + + if (caPrefix && caPrefix[1]) { + // We have enough info to make a decision + return scope[caPrefix[1]].__ngHintName || noDataDefault; + } + } + } + + if (scope.__ngHintName) { + // Without controllerAs, we need to check to ensure the name wasn't + // inherited from the parent scope + if (scope.$parent) { + var sameNameAsParent = scope.__ngHintName === scope.$parent.__ngHintName; + } + + // If we have a name, use it, otherwise use the next best thing + return sameNameAsParent ? + noDataDefault : scope.__ngHintName; } + return noDataDefault; } function humanReadableWatchExpression (fn) { diff --git a/examples/app.css b/e2e/_unused/app.css similarity index 100% rename from examples/app.css rename to e2e/_unused/app.css diff --git a/examples/broken-example.html b/e2e/_unused/broken-example.html similarity index 99% rename from examples/broken-example.html rename to e2e/_unused/broken-example.html index 20555193..6d00d023 100644 --- a/examples/broken-example.html +++ b/e2e/_unused/broken-example.html @@ -57,7 +57,7 @@