From 69e0968309da250ae2b5ecb2a3de745a51439be0 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 18 Oct 2017 13:29:50 +0200 Subject: [PATCH 001/469] chore(doc-gen): add directive names that aren't params to usage section When a directive can be used as an attribute or CSS class, but doesn't take a value, its name is not included in the parameters, which previously meant that the directive name was missing from the Attribute / CSS Class usage section of the docs. This commit adds the name to the Usage section when it is missing from the parameters. Closes #14045 Closes #16265 --- .../e2e/api-docs/directive-pages.scenario.js | 58 +++++++++++++++++++ .../ngdoc/api/directive.template.html | 17 ++++++ 2 files changed, 75 insertions(+) create mode 100644 docs/app/e2e/api-docs/directive-pages.scenario.js diff --git a/docs/app/e2e/api-docs/directive-pages.scenario.js b/docs/app/e2e/api-docs/directive-pages.scenario.js new file mode 100644 index 000000000000..cec2d484545d --- /dev/null +++ b/docs/app/e2e/api-docs/directive-pages.scenario.js @@ -0,0 +1,58 @@ +'use strict'; + +describe('directives', function() { + + describe('parameter section', function() { + + it('should show the directive name only if it is a param (attribute) with a value', function() { + browser.get('build/docs/index.html#!/api/ng/directive/ngInclude'); + expect(getParamNames().getText()).toContain('ngInclude | src'); + + browser.get('build/docs/index.html#!/api/ngRoute/directive/ngView'); + expect(getParamNames().getText()).not.toContain('ngView'); + }); + }); + + describe('usage section', function() { + + it('should show the directive name if it is a param (attribute) with a value', function() { + browser.get('build/docs/index.html#!/api/ng/directive/ngInclude'); + + expect(getUsageAs('element', 'ng-include').isPresent()).toBe(true); + expect(getUsageAs('attribute', 'ng-include').isPresent()).toBe(true); + expect(getUsageAs('CSS class', 'ng-include').isPresent()).toBe(true); + }); + + it('should show the directive name if it is a void param (attribute)', function() { + browser.get('build/docs/index.html#!/api/ngRoute/directive/ngView'); + + expect(getUsageAs('element', 'ng-view').isPresent()).toBe(true); + expect(getUsageAs('attribute', 'ng-view').isPresent()).toBe(true); + expect(getUsageAs('CSS class', 'ng-view').isPresent()).toBe(true); + }); + }); +}); + +function getParamNames() { + var argsSection = element(by.className('input-arguments')); + + var paramNames = argsSection.all(by.css('tr td:nth-child(1)')); + + return paramNames; +} + +// Based on the type of directive usage, the directive name will show up in the code block +// with a specific class +var typeClassMap = { + element: 'tag', + attribute: 'atn', + 'CSS class': 'atv' +}; + +function getUsageAs(type, directiveName) { + var usage = element(by.className('usage')); + + var as = usage.element(by.cssContainingText('li', 'as ' + type)); + + return as.element(by.cssContainingText('span.' + typeClassMap[type], directiveName)); +} diff --git a/docs/config/templates/ngdoc/api/directive.template.html b/docs/config/templates/ngdoc/api/directive.template.html index fd9c9972f1c1..0a3480eee988 100644 --- a/docs/config/templates/ngdoc/api/directive.template.html +++ b/docs/config/templates/ngdoc/api/directive.template.html @@ -29,10 +29,23 @@

Usage

{% endif -%} + + {% set hasNameAsParam = false %} + + {# when a directive's name is not a parameter (i.e. doesn't take a value), + add the directive name to the list of attributes and/or css classes #} + + {%- for param in doc.params %} + {% set hasNameAsParam = true if param.name === doc.name else hasNameAsParam %} + {%- endfor %} + {%- if doc.restrict.attribute -%}
  • as attribute: {% code %} <{$ doc.element $} + {%- if not hasNameAsParam %} + {$ lib.directiveParam(doc.name, {}, '', '') $} + {%- endif -%} {%- for param in doc.params %} {$ lib.directiveParam(param.name, param.type, '="', '"') $} {%- endfor %}> @@ -43,10 +56,14 @@

    Usage

    {% endif -%} {%- if doc.restrict.cssClass -%} +
  • as CSS class: {% code %} {% set sep = joiner(' ') %} <{$ doc.element $} class=" + {%- if not hasNameAsParam -%} + {$ sep() $}{$ lib.directiveParam(doc.name, {}, '', '') $} + {%- endif -%} {%- for param in doc.params -%} {$ sep() $}{$ lib.directiveParam(param.name, param.type, ': ', ';') $} {%- endfor %}"> ... From 84294ec1fcf8a2b01438951c5bbb41c98d6da3b7 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 18 Oct 2017 13:52:47 +0200 Subject: [PATCH 002/469] chore(travis): unit-test latest & latest-1 Chrome, FF, Edge Previously, we used fixed versions that became outdated quickly for FF and Chrome. Safari 10/11 is not included because during the latest test there were failures, see https://github.com/angular/angular.js/pull/15717 Jasmine is fixed to 2.5.2 because 2.6.0+ is not compatible with the suite: https://github.com/angular/angular.js/pull/15927#issuecomment-309206419 Closes #15927 --- karma-shared.conf.js | 23 +++++++++++++++++++---- scripts/travis/build.sh | 4 ++-- yarn.lock | 14 +++++++++----- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/karma-shared.conf.js b/karma-shared.conf.js index 6609c05fd76c..88c297b8fbf7 100644 --- a/karma-shared.conf.js +++ b/karma-shared.conf.js @@ -45,12 +45,22 @@ module.exports = function(config, specificOptions) { 'SL_Chrome': { base: 'SauceLabs', browserName: 'chrome', - version: '59' + version: 'latest' + }, + 'SL_Chrome-1': { + base: 'SauceLabs', + browserName: 'chrome', + version: 'latest-1' }, 'SL_Firefox': { base: 'SauceLabs', browserName: 'firefox', - version: '54' + version: 'latest' + }, + 'SL_Firefox-1': { + base: 'SauceLabs', + browserName: 'firefox', + version: 'latest-1' }, 'SL_Safari_8': { base: 'SauceLabs', @@ -86,7 +96,13 @@ module.exports = function(config, specificOptions) { base: 'SauceLabs', browserName: 'microsoftedge', platform: 'Windows 10', - version: '14' + version: 'latest' + }, + 'SL_EDGE-1': { + base: 'SauceLabs', + browserName: 'microsoftedge', + platform: 'Windows 10', + version: 'latest-1' }, 'SL_iOS': { base: 'SauceLabs', @@ -137,7 +153,6 @@ module.exports = function(config, specificOptions) { 'BS_EDGE': { base: 'BrowserStack', browser: 'edge', - browser_version: '14', os: 'Windows', os_version: '10' }, diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index b0ca7489e919..87871c0a0afd 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -21,7 +21,7 @@ case "$JOB" in if [ "$BROWSER_PROVIDER" == "browserstack" ]; then BROWSERS="BS_Chrome,BS_Safari,BS_Firefox,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9" else - BROWSERS="SL_Chrome,SL_Firefox,SL_Safari_8,SL_Safari_9,SL_IE_9,SL_IE_10,SL_IE_11,SL_EDGE,SL_iOS" + BROWSERS="SL_Chrome,SL_Chrome-1,SL_Firefox,SL_Firefox-1,SL_Safari_8,SL_Safari_9,SL_IE_9,SL_IE_10,SL_IE_11,SL_EDGE,SL_EDGE-1,SL_iOS" fi grunt test:promises-aplus @@ -56,4 +56,4 @@ case "$JOB" in *) echo "Unknown job type. Please set JOB=ci-checks, JOB=unit, JOB=deploy or JOB=e2e-*." ;; -esac +esac \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 81359ce0b693..551d4e915d41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5609,14 +5609,18 @@ semver-diff@^0.1.0: dependencies: semver "^2.2.1" -semver@*, "semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.1.0, semver@^5.2.0, semver@^5.3.0, semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - "semver@2 || 3 || 4", semver@^4.1.0, semver@~4.3.3: version "4.3.6" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" +"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.1.0, semver@^5.2.0, semver@^5.3.0, semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +semver@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-2.2.1.tgz#7941182b3ffcc580bff1c17942acdf7951c0d213" + semver@^2.2.1, semver@~2.3.0: version "2.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52" @@ -6335,7 +6339,7 @@ traceur@vojtajina/traceur-compiler#add-es6-pure-transformer-dist: dependencies: commander ">=1.1" q-io "~1.10.6" - semver "*" + semver "2.2.1" "traverse@>=0.3.0 <0.4": version "0.3.9" From 0f0a16ec409f5b4f1de37982c04b14667568f802 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 19 Oct 2017 11:51:30 +0200 Subject: [PATCH 003/469] chore(travis): remove unnessecary addons The "Trusty" build environment on Travis includes these addons, see https://docs.travis-ci.com/user/languages/javascript-with-nodejs#Node.js-v4-(or-io.js-v3)-compiler-requirements Closes #16286 --- .travis.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4f4841e9bc03..64d03a450274 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,22 +20,12 @@ env: - JOB=e2e TEST_TARGET=jqlite BROWSER_PROVIDER=saucelabs - JOB=e2e TEST_TARGET=jquery BROWSER_PROVIDER=saucelabs global: - # node 4 likes the G++ v4.8 compiler - # see https://docs.travis-ci.com/user/languages/javascript-with-nodejs#Node.js-v4-(or-io.js-v3)-compiler-requirements - - CXX=g++-4.8 - SAUCE_USERNAME=angular-ci - SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987 - LOGS_DIR=/tmp/angular-build/logs - BROWSER_PROVIDER_READY_FILE=/tmp/browsersprovider-tunnel-ready - secure: oTBjhnOKhs0qDSKTf7fE4f6DYiNDPycvB7qfSF5QRIbJK/LK/J4UtFwetXuXj79HhUZG9qnoT+5e7lPaiaMlpsIKn9ann7ffqFWN1E8TMtpJF+AGigx3djYElwfgf5nEnFUFhwjFzvbfpZNnxVGgX5YbIZpe/WUbHkP4ffU0Wks= -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.8 - before_install: - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.27.5 - export PATH="$HOME/.yarn/bin:$PATH" From 10b48094d72e0381e498a674c711807ce5e4cbde Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 16 Oct 2017 18:55:12 +0200 Subject: [PATCH 004/469] chore(doc-gen, docs-app): generate "stable snapshot" for distTag=latest The "stable snapshot" is the current state of the branch that has distTag=latest, i.e. a preview of the next patch version of the stable branch. --- docs/app/src/versions.js | 10 ++++++-- docs/config/processors/versions-data.js | 31 ++++++++++++++++++++++++- lib/versions/version-info.js | 1 + 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/docs/app/src/versions.js b/docs/app/src/versions.js index 9f0d3dc0c5c4..aa5c3618b0fd 100644 --- a/docs/app/src/versions.js +++ b/docs/app/src/versions.js @@ -12,10 +12,16 @@ angular.module('versions', ['currentVersionData', 'allVersionsData']) /** @this VersionPickerController */ function VersionPickerController($location, $window, CURRENT_NG_VERSION, ALL_NG_VERSIONS) { - var versionStr = CURRENT_NG_VERSION.isSnapshot ? 'snapshot' : CURRENT_NG_VERSION.version; + var versionStr = CURRENT_NG_VERSION.version; + + if (CURRENT_NG_VERSION.isSnapshot) { + versionStr = CURRENT_NG_VERSION.distTag === 'latest' ? 'snapshot-stable' : 'snapshot'; + } this.versions = ALL_NG_VERSIONS; - this.selectedVersion = find(ALL_NG_VERSIONS, function(value) { return value.version.version === versionStr; }); + this.selectedVersion = find(ALL_NG_VERSIONS, function(value) { + return value.version.version === versionStr; + }); this.jumpToDocsVersion = function(value) { var currentPagePath = $location.path().replace(/\/$/, ''); diff --git a/docs/config/processors/versions-data.js b/docs/config/processors/versions-data.js index 9856ef75682b..22b4570cc326 100644 --- a/docs/config/processors/versions-data.js +++ b/docs/config/processors/versions-data.js @@ -55,6 +55,9 @@ module.exports = function generateVersionDocProcessor(gitData) { if (missesCurrentVersion) versions.push(currentVersion.version); + // Get the stable release with the highest version + var highestStableRelease = versions.reverse().find(semverIsStable); + versions = versions .filter(function(versionStr) { return blacklist.indexOf(versionStr) === -1; @@ -82,7 +85,21 @@ module.exports = function generateVersionDocProcessor(gitData) { var latest = sortObject(latestMap, reverse(semver.compare)) .map(function(version) { return makeOption(version, 'Latest'); }); - return [makeOption({version: 'snapshot'}, 'Latest', 'master')] + // Generate master and stable snapshots + var snapshots = [ + makeOption( + {version: 'snapshot'}, + 'Latest', + 'master-snapshot' + ), + makeOption( + {version: 'snapshot-stable'}, + 'Latest', + createSnapshotStableLabel(highestStableRelease) + ) + ]; + + return snapshots .concat(latest) .concat(versions); } @@ -112,6 +129,18 @@ module.exports = function generateVersionDocProcessor(gitData) { function sortObject(obj, cmp) { return Object.keys(obj).map(function(key) { return obj[key]; }).sort(cmp); } + + // https://github.com/kaelzhang/node-semver-stable/blob/34dd29842409295d49889d45871bec55a992b7f6/index.js#L25 + function semverIsStable(version) { + var semverObj = semver.parse(version); + return semverObj === null ? false : !semverObj.prerelease.length; + } + + function createSnapshotStableLabel(version) { + var label = 'v' + version.replace(/.$/, 'x') + '-snapshot'; + + return label; + } } }; }; diff --git a/lib/versions/version-info.js b/lib/versions/version-info.js index d245afdceb9c..1f5eca9368d1 100644 --- a/lib/versions/version-info.js +++ b/lib/versions/version-info.js @@ -210,6 +210,7 @@ var getSnapshotVersion = function() { version.format(); version.full = version.version + '+' + version.build; version.branch = 'master'; + version.distTag = currentPackage.distTag; return version; }; From dfcb5ac1e43e25f2df19dd89c0514022e6ff62b3 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 19 Oct 2017 14:24:19 +0200 Subject: [PATCH 005/469] chore(travis): deploy to docs and code when distTag=latest We now deploy to code.angularjs.org and docs.angularjs.org when we are on the branch which has distTag=latest set in the package.json, i.e. the stable branch. Previously, we deployed to docs only when distTag=latest and the commit was tagged, and to code only on the master branch. --- .travis.yml | 9 ++++--- Gruntfile.js | 6 ++++- .../functions/index.js | 17 ++++++++++--- .../readme.firebase.code.md | 3 +++ scripts/travis/build.sh | 25 ++++++++++++++++--- 5 files changed, 48 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 64d03a450274..4f75e03b30a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,6 +52,9 @@ notifications: jobs: include: - stage: deploy + # Don't deploy from PRs. + # The deployment logic for pushed branches is defined in scripts\travis\build.sh + if: type != pull_request env: - JOB=deploy before_script: skip @@ -75,8 +78,7 @@ jobs: on: repo: angular/angular.js all_branches: true - # deploy a new docs version when the commit is tagged on the "latest" npm version - condition: $TRAVIS_TAG != '' && $( jq ".distTag" "package.json" | tr -d "\"[:space:]" ) = latest + condition: $DEPLOY_DOCS - provider: gcs skip_cleanup: true access_key_id: GOOGLDB7W2J3LFHICF3R @@ -88,6 +90,5 @@ jobs: on: repo: angular/angular.js all_branches: true - # upload the build when the commit is tagged or the branch is "master" - condition: $TRAVIS_TAG != '' || ($TRAVIS_PULL_REQUEST = false && $TRAVIS_BRANCH = master) + condition: $DEPLOY_CODE diff --git a/Gruntfile.js b/Gruntfile.js index 0808eb468fe7..ff5eb26de2a0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -63,7 +63,11 @@ module.exports = function(grunt) { NG_VERSION.cdn = versionInfo.cdnVersion; var dist = 'angular-' + NG_VERSION.full; - var deployVersion = NG_VERSION.isSnapshot ? 'snapshot' : NG_VERSION.full; + var deployVersion = NG_VERSION.full; + + if (NG_VERSION.isSnapshot) { + deployVersion = NG_VERSION.distTag === 'latest' ? 'snapshot-stable' : 'snapshot'; + } if (versionInfo.cdnVersion == null) { throw new Error('Unable to read CDN version, are you offline or has the CDN not been properly pushed?\n' + diff --git a/scripts/code.angularjs.org-firebase/functions/index.js b/scripts/code.angularjs.org-firebase/functions/index.js index af68f88f9063..8b54322395cc 100644 --- a/scripts/code.angularjs.org-firebase/functions/index.js +++ b/scripts/code.angularjs.org-firebase/functions/index.js @@ -169,6 +169,13 @@ function sendStoredFile(request, response) { } } +const snapshotRegex = /^snapshot(-stable)?\//; + +/** + * The build folder contains a zip file that is unique per build. + * When a new zip file is uploaded into snapshot or snapshot-stable, + * delete the previous zip file. + */ function deleteOldSnapshotZip(event) { const object = event.data; @@ -179,15 +186,17 @@ function deleteOldSnapshotZip(event) { const bucket = gcs.bucket(bucketId); - if (contentType !== 'application/zip' || - !filePath.startsWith('snapshot/') || + const snapshotFolderMatch = filePath.match(snapshotRegex); + + if (!snapshotFolderMatch || + contentType !== 'application/zip' || resourceState === 'not_exists' // Deletion event ) { return; } bucket.getFiles({ - prefix: 'snapshot/', + prefix: snapshotFolderMatch[0], delimiter: '/', autoPaginate: false }).then(function(data) { @@ -197,6 +206,8 @@ function deleteOldSnapshotZip(event) { return file.metadata.name !== filePath && file.metadata.contentType === 'application/zip'; }); + console.info(`found ${oldZipFiles.length} old zip files to delete`); + oldZipFiles.forEach(function(file) { file.delete(); }); diff --git a/scripts/code.angularjs.org-firebase/readme.firebase.code.md b/scripts/code.angularjs.org-firebase/readme.firebase.code.md index 58a5d25835d4..bf39a78c0761 100644 --- a/scripts/code.angularjs.org-firebase/readme.firebase.code.md +++ b/scripts/code.angularjs.org-firebase/readme.firebase.code.md @@ -6,6 +6,9 @@ This folder contains the Google Firebase scripts for the code.angularjs.org setu firebase.json contains the rewrite rules that route every subdirectory request to the cloud function in functions/index.js that serves the docs from the Firebase Google Cloud Storage bucket. +functions/index.js also contains a rule that deletes outdated build zip files +from the snapshot and snapshot-stable folders when new zip files are uploaded. + The deployment to the Google Cloud Storage bucket happens automatically via Travis. See the travis.yml file in the repository root. diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index 87871c0a0afd..860bf282a860 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -18,7 +18,7 @@ case "$JOB" in fi ;; "unit") - if [ "$BROWSER_PROVIDER" == "browserstack" ]; then + if [[ "$BROWSER_PROVIDER" == "browserstack" ]]; then BROWSERS="BS_Chrome,BS_Safari,BS_Firefox,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9" else BROWSERS="SL_Chrome,SL_Chrome-1,SL_Firefox,SL_Firefox-1,SL_Safari_8,SL_Safari_9,SL_IE_9,SL_IE_10,SL_IE_11,SL_EDGE,SL_EDGE-1,SL_iOS" @@ -46,11 +46,28 @@ case "$JOB" in grunt test:travis-protractor --specs="$TARGET_SPECS" ;; "deploy") - # we never deploy on Pull requests, so it's safe to skip the build here - if [[ "$TRAVIS_PULL_REQUEST" == "false" ]]; then + export DEPLOY_DOCS + export DEPLOY_CODE + + DIST_TAG=$( jq ".distTag" "package.json" | tr -d "\"[:space:]" ) + + # upload docs if the branch distTag from package.json is "latest" (i.e. stable branch) + if [[ "$DIST_TAG" == latest ]]; then + DEPLOY_DOCS=true + fi + + # upload the build (code + docs) if ... + # the commit is tagged + # - or the branch is "master" + # - or the branch distTag from package.json is "latest" (i.e. stable branch) + if [[ "$TRAVIS_TAG" != '' || "$TRAVIS_BRANCH" == master || "$DIST_TAG" == latest ]]; then + DEPLOY_CODE=true + fi + + if [[ "$DEPLOY_DOCS" || "$DEPLOY_CODE" ]]; then grunt prepareFirebaseDeploy else - echo "Skipping build because Travis has been triggered by Pull Request" + echo "Skipping deployment build because conditions have not been met." fi ;; *) From 9ba07e974a45eea4ff0b29d1df2d64863f484c7b Mon Sep 17 00:00:00 2001 From: Sagir Khan Date: Sun, 22 Oct 2017 13:05:44 +0530 Subject: [PATCH 006/469] docs(tutorial/step_14): replace broken web platform docs link Replace broken [webplatform-animations][1] link with [mdn-animations][2]. The original link returns a 404. The closest match that works is https://webplatform.github.io/docs/css/properties/animation. However, the notice at the top of the page reads: > The WebPlatform project, supported by various stewards between 2012 > and 2015, has been discontinued. The CSS animations guide on MDN web docs is not only current, but also more comprehensive. [1]: https://docs.webplatform.org/wiki/css/properties/animations [2]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations Closes #16294 --- docs/content/tutorial/step_14.ngdoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/tutorial/step_14.ngdoc b/docs/content/tutorial/step_14.ngdoc index 409d3f984cc3..b1b5ff043f58 100644 --- a/docs/content/tutorial/step_14.ngdoc +++ b/docs/content/tutorial/step_14.ngdoc @@ -328,7 +328,7 @@ The applied CSS classes are much the same as with `ngRepeat`. Each time a new pa ensures that all views are contained within a single HTML element, which allows for easy animation control. -For more on CSS animations, see the [Web Platform documentation][webplatform-animations]. +For more on CSS animations, see the [MDN web docs][mdn-animations]. ## Animating `ngClass` with JavaScript @@ -561,4 +561,4 @@ There you have it! We have created a web application in a relatively short amoun [caniuse-css-transitions]: http://caniuse.com/#feat=css-transitions [jquery]: https://jquery.com/ [jquery-animate]: https://api.jquery.com/animate/ -[webplatform-animations]: https://docs.webplatform.org/wiki/css/properties/animations +[mdn-animations]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations From 32fbb2e78f53d765fbb170f7cf99e42e072d363b Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 23 Oct 2017 12:53:36 +0200 Subject: [PATCH 007/469] chore(travis): split unit test into 'core' and 'jquery' "unit-core" consists of code+jqlite, module test, and promise A+ tests. "unit-jquery" is code+jquery "docs-app" includes unit and e2e tests Splitting the unit tests into more than one job makes it faster to rerun jobs that fail because Safari or Edge cannot complete the suite, which seemingly happens on random. Closes #16292 --- .travis.yml | 5 ++-- scripts/travis/before_build.sh | 4 ++-- scripts/travis/build.sh | 43 ++++++++++++++++++++++++---------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4f75e03b30a9..8a39e7e244c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,8 +15,9 @@ branches: env: matrix: - JOB=ci-checks - - JOB=unit BROWSER_PROVIDER=saucelabs - - JOB=docs-e2e BROWSER_PROVIDER=saucelabs + - JOB=unit-core BROWSER_PROVIDER=saucelabs + - JOB=unit-jquery BROWSER_PROVIDER=saucelabs + - JOB=docs-app BROWSER_PROVIDER=saucelabs - JOB=e2e TEST_TARGET=jqlite BROWSER_PROVIDER=saucelabs - JOB=e2e TEST_TARGET=jquery BROWSER_PROVIDER=saucelabs global: diff --git a/scripts/travis/before_build.sh b/scripts/travis/before_build.sh index df9e78fbb8e6..e1d83d7977ad 100755 --- a/scripts/travis/before_build.sh +++ b/scripts/travis/before_build.sh @@ -12,12 +12,12 @@ if [ "$JOB" != "ci-checks" ]; then fi # ci-checks and unit tests do not run against the packaged code -if [ "$JOB" != "ci-checks" ] && [ "$JOB" != "unit" ]; then +if [[ "$JOB" != "ci-checks" ]] && [[ "$JOB" != unit-* ]]; then grunt package fi # unit runs the docs tests too which need a built version of the code -if [ "$JOB" = "unit" ]; then +if [[ "$JOB" = unit-* ]]; then grunt bower grunt validate-angular-files grunt build diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index 860bf282a860..b4e0f4e29d46 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -2,8 +2,18 @@ set -e -export BROWSER_STACK_ACCESS_KEY=`echo $BROWSER_STACK_ACCESS_KEY | rev` -export SAUCE_ACCESS_KEY=`echo $SAUCE_ACCESS_KEY | rev` +export BROWSER_STACK_ACCESS_KEY +export SAUCE_ACCESS_KEY + +BROWSER_STACK_ACCESS_KEY=$(echo "$BROWSER_STACK_ACCESS_KEY" | rev) +SAUCE_ACCESS_KEY=$(echo "$SAUCE_ACCESS_KEY" | rev) + +BROWSERS="SL_Chrome,SL_Chrome-1,\ +SL_Firefox,SL_Firefox-1,\ +SL_Safari_8,SL_Safari_9,\ +SL_iOS,\ +SL_IE_9,SL_IE_10,SL_IE_11,\ +SL_EDGE,SL_EDGE-1" case "$JOB" in "ci-checks") @@ -17,18 +27,18 @@ case "$JOB" in yarn run commitplease -- "${TRAVIS_COMMIT_RANGE/.../..}" fi ;; - "unit") - if [[ "$BROWSER_PROVIDER" == "browserstack" ]]; then - BROWSERS="BS_Chrome,BS_Safari,BS_Firefox,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9" - else - BROWSERS="SL_Chrome,SL_Chrome-1,SL_Firefox,SL_Firefox-1,SL_Safari_8,SL_Safari_9,SL_IE_9,SL_IE_10,SL_IE_11,SL_EDGE,SL_EDGE-1,SL_iOS" - fi - + "unit-core") grunt test:promises-aplus - grunt test:unit --browsers="$BROWSERS" --reporters=spec - grunt tests:docs --browsers="$BROWSERS" --reporters=spec + grunt test:jqlite --browsers="$BROWSERS" --reporters=spec + grunt test:modules --browsers="$BROWSERS" --reporters=spec ;; - "docs-e2e") + "unit-jquery") + grunt test:jquery --browsers="$BROWSERS" --reporters=spec + grunt test:jquery-2.2 --browsers="$BROWSERS" --reporters=spec + grunt test:jquery-2.1 --browsers="$BROWSERS" --reporters=spec + ;; + "docs-app") + grunt tests:docs --browsers="$BROWSERS" --reporters=spec grunt test:travis-protractor --specs="docs/app/e2e/**/*.scenario.js" ;; "e2e") @@ -71,6 +81,13 @@ case "$JOB" in fi ;; *) - echo "Unknown job type. Please set JOB=ci-checks, JOB=unit, JOB=deploy or JOB=e2e-*." + echo "Unknown job type. Please set JOB to one of\ + 'ci-checks',\ + 'unit-core',\ + 'unit-jquery',\ + 'docs-app',\ + 'e2e',\ + or\ + 'deploy'." ;; esac \ No newline at end of file From 202f1809ad14827a6ac6a125157c605d65e0b551 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 25 Oct 2017 10:44:55 +0200 Subject: [PATCH 008/469] chore(travis): fix deploy conditions Closes #16296 --- .travis.yml | 31 ++++++++++++++++++++----------- scripts/travis/build.sh | 12 ++++++++---- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8a39e7e244c2..6a4d3b139250 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,22 +54,31 @@ jobs: include: - stage: deploy # Don't deploy from PRs. - # The deployment logic for pushed branches is defined in scripts\travis\build.sh + # This is a Travis-specific boolean language: https://docs.travis-ci.com/user/conditional-builds-stages-jobs#Specifying-conditions + # The deployment logic for pushed branches is further defined in scripts\travis\build.sh if: type != pull_request env: - JOB=deploy before_script: skip script: - - "./scripts/travis/build.sh" + # Export the variables into the current process + - . ./scripts/travis/build.sh + - "echo DEPLOY_DOCS: $DEPLOY_DOCS, DEPLOY_CODE: $DEPLOY_CODE" + after_script: skip # Work around the 10min Travis timeout so the code.angularjs firebase+gcs code deploy can complete + # Only run the keep_alive once (before_deploy is run for each provider) before_deploy: | - function keep_alive() { - while true; do - echo -en "\a" - sleep 5 - done - } - keep_alive & + if ! [ "$BEFORE_DEPLOY_RUN" ]; then + export BEFORE_DEPLOY_RUN=1; + + function keep_alive() { + while true; do + echo -en "\a" + sleep 10 + done + } + keep_alive & + fi deploy: - provider: firebase # the upload folder for firebase is configured in /firebase.json @@ -79,7 +88,7 @@ jobs: on: repo: angular/angular.js all_branches: true - condition: $DEPLOY_DOCS + condition: "$DEPLOY_DOCS == true" - provider: gcs skip_cleanup: true access_key_id: GOOGLDB7W2J3LFHICF3R @@ -91,5 +100,5 @@ jobs: on: repo: angular/angular.js all_branches: true - condition: $DEPLOY_CODE + condition: "$DEPLOY_CODE == true" diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index b4e0f4e29d46..f59711467e99 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -46,13 +46,13 @@ case "$JOB" in export USE_JQUERY=1 fi - export TARGET_SPECS="build/docs/ptore2e/**/default_test.js" - if [[ "$TEST_TARGET" == jquery* ]]; then TARGET_SPECS="build/docs/ptore2e/**/jquery_test.js" + else + TARGET_SPECS="build/docs/ptore2e/**/default_test.js" fi - export TARGET_SPECS="test/e2e/tests/**/*.js,$TARGET_SPECS" + TARGET_SPECS="test/e2e/tests/**/*.js,$TARGET_SPECS" grunt test:travis-protractor --specs="$TARGET_SPECS" ;; "deploy") @@ -64,6 +64,8 @@ case "$JOB" in # upload docs if the branch distTag from package.json is "latest" (i.e. stable branch) if [[ "$DIST_TAG" == latest ]]; then DEPLOY_DOCS=true + else + DEPLOY_DOCS=false fi # upload the build (code + docs) if ... @@ -72,9 +74,11 @@ case "$JOB" in # - or the branch distTag from package.json is "latest" (i.e. stable branch) if [[ "$TRAVIS_TAG" != '' || "$TRAVIS_BRANCH" == master || "$DIST_TAG" == latest ]]; then DEPLOY_CODE=true + else + DEPLOY_CODE=false fi - if [[ "$DEPLOY_DOCS" || "$DEPLOY_CODE" ]]; then + if [[ "$DEPLOY_DOCS" == true || "$DEPLOY_CODE" == true ]]; then grunt prepareFirebaseDeploy else echo "Skipping deployment build because conditions have not been met." From 9871ada04bc52d9a11f4232764aae89d948ed1ee Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 26 Oct 2017 13:37:29 +0200 Subject: [PATCH 009/469] chore(travis): tighten up deploy conditions --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6a4d3b139250..d86faa8ea81e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,10 +53,10 @@ notifications: jobs: include: - stage: deploy - # Don't deploy from PRs. + # Don't deploy from PRs and only from our default branches. # This is a Travis-specific boolean language: https://docs.travis-ci.com/user/conditional-builds-stages-jobs#Specifying-conditions # The deployment logic for pushed branches is further defined in scripts\travis\build.sh - if: type != pull_request + if: type != pull_request and branch =~ ^(v1\.\d+\.x|master)$ env: - JOB=deploy before_script: skip From c15c8a1380d7cc991a27e0f17737b84013ad9f63 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Wed, 2 Aug 2017 21:44:35 -0700 Subject: [PATCH 010/469] test($rootScope): test removal of event listeners during event broadcast --- test/ng/rootScopeSpec.js | 84 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 92e713a5b99e..d84e38b4080a 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -2316,6 +2316,90 @@ describe('Scope', function() { })); + it('should call next listener after removing the current listener via its own handler', inject(function($rootScope) { + var listener1 = jasmine.createSpy('listener1').and.callFake(function() { remove1(); }); + var remove1 = $rootScope.$on('abc', listener1); + + var listener2 = jasmine.createSpy('listener2'); + var remove2 = $rootScope.$on('abc', listener2); + + var listener3 = jasmine.createSpy('listener3'); + var remove3 = $rootScope.$on('abc', listener3); + + $rootScope.$broadcast('abc'); + expect(listener1).toHaveBeenCalled(); + expect(listener2).toHaveBeenCalled(); + expect(listener3).toHaveBeenCalled(); + + listener1.calls.reset(); + listener2.calls.reset(); + listener3.calls.reset(); + + $rootScope.$broadcast('abc'); + expect(listener1).not.toHaveBeenCalled(); + expect(listener2).toHaveBeenCalled(); + expect(listener3).toHaveBeenCalled(); + })); + + + it('should call all subsequent listeners when a previous listener is removed via a handler', inject(function($rootScope) { + var listener1 = jasmine.createSpy(); + var remove1 = $rootScope.$on('abc', listener1); + + var listener2 = jasmine.createSpy().and.callFake(remove1); + var remove2 = $rootScope.$on('abc', listener2); + + var listener3 = jasmine.createSpy(); + var remove3 = $rootScope.$on('abc', listener3); + + $rootScope.$broadcast('abc'); + expect(listener1).toHaveBeenCalled(); + expect(listener2).toHaveBeenCalled(); + expect(listener3).toHaveBeenCalled(); + + listener1.calls.reset(); + listener2.calls.reset(); + listener3.calls.reset(); + + $rootScope.$broadcast('abc'); + expect(listener1).not.toHaveBeenCalled(); + expect(listener2).toHaveBeenCalled(); + expect(listener3).toHaveBeenCalled(); + })); + + + it('should not call listener when removed by previous', inject(function($rootScope) { + var listener1 = jasmine.createSpy('listener1'); + var remove1 = $rootScope.$on('abc', listener1); + + var listener2 = jasmine.createSpy('listener2').and.callFake(function() { remove3(); }); + var remove2 = $rootScope.$on('abc', listener2); + + var listener3 = jasmine.createSpy('listener3'); + var remove3 = $rootScope.$on('abc', listener3); + + var listener4 = jasmine.createSpy('listener4'); + var remove4 = $rootScope.$on('abc', listener4); + + $rootScope.$broadcast('abc'); + expect(listener1).toHaveBeenCalled(); + expect(listener2).toHaveBeenCalled(); + expect(listener3).not.toHaveBeenCalled(); + expect(listener4).toHaveBeenCalled(); + + listener1.calls.reset(); + listener2.calls.reset(); + listener3.calls.reset(); + listener4.calls.reset(); + + $rootScope.$broadcast('abc'); + expect(listener1).toHaveBeenCalled(); + expect(listener2).toHaveBeenCalled(); + expect(listener3).not.toHaveBeenCalled(); + expect(listener4).toHaveBeenCalled(); + })); + + it('should decrement ancestor $$listenerCount entries', inject(function($rootScope) { var child1 = $rootScope.$new(), child2 = child1.$new(), From 0864f73458864dadc92753bb1fae5d7f32e90a9a Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 30 Oct 2017 11:23:44 +0100 Subject: [PATCH 011/469] docs($compileProvider): improve strictComponentBindingsEnabled info Related to #16303 Closes #16306 --- docs/content/error/$compile/missingattr.ngdoc | 34 +++++++++++++++++-- src/ng/compile.js | 13 ++++--- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/content/error/$compile/missingattr.ngdoc b/docs/content/error/$compile/missingattr.ngdoc index 1fb2a346b4a2..62e877e84f6f 100644 --- a/docs/content/error/$compile/missingattr.ngdoc +++ b/docs/content/error/$compile/missingattr.ngdoc @@ -3,6 +3,34 @@ @fullName Missing required attribute @description -This error may occur only when `$compileProvider.strictComponentBindingsEnabled` is set to `true`. -Then all attributes mentioned in `bindings` without `?` must be set. If one or more aren't set, -the first one will throw an error. +This error may occur only when {@link $compileProvider#strictComponentBindingsEnabled `$compileProvider.strictComponentBindingsEnabled`} is set to `true`. + +If that is the case, then all {@link $compileProvider#component component} controller bindings and +{@link $compileProvider#directive directive} scope / controller bindings that are non-optional, +must be provided when the directive is instantiated. + +To make a binding optional, add '?' to the definition. + +## Example: + +```js + +app.component('myTest', { + bindings: { + first: '=?', // optional + second: '=' + }, + controller: function() { + ... + }, + template: '...' +}); + +``` + +This component will throw `missingattr` for the `second` binding when used as follows: + +```html + +``` + diff --git a/src/ng/compile.js b/src/ng/compile.js index ebfbc875ad5c..561bb13fef16 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1415,16 +1415,19 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @ngdoc method * @name $compileProvider#strictComponentBindingsEnabled * - * @param {boolean=} enabled update the strictComponentBindingsEnabled state if provided, otherwise just return the - * current strictComponentBindingsEnabled state + * @param {boolean=} enabled update the strictComponentBindingsEnabled state if provided, + * otherwise return the current strictComponentBindingsEnabled state. * @returns {*} current value if used as getter or itself (chaining) if used as setter * * @kind function * * @description - * Call this method to enable/disable strict component bindings check. If enabled, the compiler will enforce that - * for all bindings of a component that are not set as optional with `?`, an attribute needs to be provided - * on the component's HTML tag. + * Call this method to enable / disable the strict component bindings check. If enabled, the + * compiler will enforce that all scope / controller bindings of a + * {@link $compileProvider#directive directive} / {@link $compileProvider#component component} + * that are not set as optional with `?`, must be provided when the directive is instantiated. + * If not provided, the compiler will throw the + * {@link error/$compile/missingattr $compile:missingattr error}. * * The default value is false. */ From 817ac56719505680ac4c9997972e8f39eb40a6d0 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Sat, 14 Oct 2017 17:36:50 -0700 Subject: [PATCH 012/469] fix($rootScope): fix potential memory leak when removing scope listeners Previously the array entry for listeners was set to null but the array size was not trimmed until the event was broadcasted again (see e6966e05f508d1d2633b9ff327fea912b12555ac). By keeping track of the listener iteration index globally it can be adjusted if a listener removal effects the index. Fixes #16135 Closes #16293 BREAKING CHANGE: Recursively invoking `$emit` or `$broadcast` with the same event name is no longer supported. This will now throw a `inevt` minErr. --- docs/content/error/$rootScope/inevt.ngdoc | 22 ++++ src/ng/rootScope.js | 79 +++++++-------- test/ng/rootScopeSpec.js | 118 +++++++++++++++++++++- 3 files changed, 172 insertions(+), 47 deletions(-) create mode 100644 docs/content/error/$rootScope/inevt.ngdoc diff --git a/docs/content/error/$rootScope/inevt.ngdoc b/docs/content/error/$rootScope/inevt.ngdoc new file mode 100644 index 000000000000..a06eeba18627 --- /dev/null +++ b/docs/content/error/$rootScope/inevt.ngdoc @@ -0,0 +1,22 @@ +@ngdoc error +@name $rootScope:inevt +@fullName Recursive $emit/$broadcast event +@description + +This error occurs when the an event is `$emit`ed or `$broadcast`ed recursively on a scope. + +For example, when an event listener fires the same event being listened to. + +``` +$scope.$on('foo', function() { + $scope.$emit('foo'); +}); +``` + +Or when a parent element causes indirect recursion. + +``` +$scope.$on('foo', function() { + $rootScope.$broadcast('foo'); +}); +``` \ No newline at end of file diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index e293b7f4e483..616cc69676d0 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -1181,10 +1181,14 @@ function $RootScopeProvider() { var self = this; return function() { - var indexOfListener = namedListeners.indexOf(listener); - if (indexOfListener !== -1) { - namedListeners[indexOfListener] = null; + var index = arrayRemove(namedListeners, listener); + if (index >= 0) { decrementListenerCount(self, 1, name); + // We are removing a listener while iterating over the list of listeners. + // Update the current $$index if necessary to ensure no listener is skipped. + if (index <= namedListeners.$$index) { + namedListeners.$$index--; + } } }; }, @@ -1213,9 +1217,7 @@ function $RootScopeProvider() { * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). */ $emit: function(name, args) { - var empty = [], - namedListeners, - scope = this, + var scope = this, stopPropagation = false, event = { name: name, @@ -1226,28 +1228,11 @@ function $RootScopeProvider() { }, defaultPrevented: false }, - listenerArgs = concat([event], arguments, 1), - i, length; + listenerArgs = concat([event], arguments, 1); do { - namedListeners = scope.$$listeners[name] || empty; - event.currentScope = scope; - for (i = 0, length = namedListeners.length; i < length; i++) { - - // if listeners were deregistered, defragment the array - if (!namedListeners[i]) { - namedListeners.splice(i, 1); - i--; - length--; - continue; - } - try { - //allow all listeners attached to the current scope to run - namedListeners[i].apply(null, listenerArgs); - } catch (e) { - $exceptionHandler(e); - } - } + invokeListeners(scope, event, listenerArgs, name); + //if any listener on the current scope stops propagation, prevent bubbling if (stopPropagation) { event.currentScope = null; @@ -1299,28 +1284,11 @@ function $RootScopeProvider() { if (!target.$$listenerCount[name]) return event; - var listenerArgs = concat([event], arguments, 1), - listeners, i, length; + var listenerArgs = concat([event], arguments, 1); //down while you can, then up and next sibling or up and next sibling until back at root while ((current = next)) { - event.currentScope = current; - listeners = current.$$listeners[name] || []; - for (i = 0, length = listeners.length; i < length; i++) { - // if listeners were deregistered, defragment the array - if (!listeners[i]) { - listeners.splice(i, 1); - i--; - length--; - continue; - } - - try { - listeners[i].apply(null, listenerArgs); - } catch (e) { - $exceptionHandler(e); - } - } + invokeListeners(current, event, listenerArgs, name); // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! @@ -1350,6 +1318,27 @@ function $RootScopeProvider() { return $rootScope; + function invokeListeners(scope, event, listenerArgs, name) { + var listeners = scope.$$listeners[name]; + if (listeners) { + if (listeners.$$index !== undefined) { + throw $rootScopeMinErr('inevt', '{0} already $emit/$broadcast-ing on scope ({1})', name, scope.$id); + } + event.currentScope = scope; + try { + for (listeners.$$index = 0; listeners.$$index < listeners.length; listeners.$$index++) { + try { + //allow all listeners attached to the current scope to run + listeners[listeners.$$index].apply(null, listenerArgs); + } catch (e) { + $exceptionHandler(e); + } + } + } finally { + listeners.$$index = undefined; + } + } + } function beginPhase(phase) { if ($rootScope.$$phase) { diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index d84e38b4080a..fc32bb139a5a 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -2316,6 +2316,19 @@ describe('Scope', function() { })); + // See issue https://github.com/angular/angular.js/issues/16135 + it('should deallocate the listener array entry', inject(function($rootScope) { + var remove1 = $rootScope.$on('abc', noop); + $rootScope.$on('abc', noop); + + expect($rootScope.$$listeners['abc'].length).toBe(2); + + remove1(); + + expect($rootScope.$$listeners['abc'].length).toBe(1); + })); + + it('should call next listener after removing the current listener via its own handler', inject(function($rootScope) { var listener1 = jasmine.createSpy('listener1').and.callFake(function() { remove1(); }); var remove1 = $rootScope.$on('abc', listener1); @@ -2448,6 +2461,107 @@ describe('Scope', function() { expect($rootScope.$$listenerCount).toEqual({abc: 1}); expect(child.$$listenerCount).toEqual({abc: 1}); })); + + + it('should throw on recursive $broadcast', inject(function($rootScope) { + $rootScope.$on('e', function() { $rootScope.$broadcast('e'); }); + + expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + })); + + + it('should throw on nested recursive $broadcast', inject(function($rootScope) { + $rootScope.$on('e2', function() { $rootScope.$broadcast('e'); }); + $rootScope.$on('e', function() { $rootScope.$broadcast('e2'); }); + + expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + })); + + + it('should throw on recursive $emit', inject(function($rootScope) { + $rootScope.$on('e', function() { $rootScope.$emit('e'); }); + + expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + })); + + + it('should throw on nested recursive $emit', inject(function($rootScope) { + $rootScope.$on('e2', function() { $rootScope.$emit('e'); }); + $rootScope.$on('e', function() { $rootScope.$emit('e2'); }); + + expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + })); + + + it('should throw on recursive $broadcast on child listener', inject(function($rootScope) { + var child = $rootScope.$new(); + child.$on('e', function() { $rootScope.$broadcast('e'); }); + + expect(function() { child.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (2)'); + expect(function() { child.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (2)'); + })); + + + it('should throw on nested recursive $broadcast on child listener', inject(function($rootScope) { + var child = $rootScope.$new(); + child.$on('e2', function() { $rootScope.$broadcast('e'); }); + child.$on('e', function() { $rootScope.$broadcast('e2'); }); + + expect(function() { child.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (2)'); + expect(function() { child.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (2)'); + })); + + + it('should throw on recursive $emit parent listener', inject(function($rootScope) { + var child = $rootScope.$new(); + $rootScope.$on('e', function() { child.$emit('e'); }); + + expect(function() { child.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + })); + + + it('should throw on nested recursive $emit parent listener', inject(function($rootScope) { + var child = $rootScope.$new(); + $rootScope.$on('e2', function() { child.$emit('e'); }); + $rootScope.$on('e', function() { child.$emit('e2'); }); + + expect(function() { child.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); + })); + + + it('should clear recursive state of $broadcast if $exceptionHandler rethrows', function() { + module(function($exceptionHandlerProvider) { + $exceptionHandlerProvider.mode('rethrow'); + }); + inject(function($rootScope) { + var throwingListener = jasmine.createSpy('thrower').and.callFake(function() { + throw new Error('Listener Error!'); + }); + var secondListener = jasmine.createSpy('second'); + + $rootScope.$on('e', throwingListener); + $rootScope.$on('e', secondListener); + + expect(function() { $rootScope.$broadcast('e'); }).toThrowError('Listener Error!'); + expect(throwingListener).toHaveBeenCalled(); + expect(secondListener).not.toHaveBeenCalled(); + + throwingListener.calls.reset(); + secondListener.calls.reset(); + + expect(function() { $rootScope.$broadcast('e'); }).toThrowError('Listener Error!'); + expect(throwingListener).toHaveBeenCalled(); + expect(secondListener).not.toHaveBeenCalled(); + }); + }); }); }); @@ -2537,7 +2651,7 @@ describe('Scope', function() { expect(spy1).toHaveBeenCalledOnce(); expect(spy2).toHaveBeenCalledOnce(); expect(spy3).toHaveBeenCalledOnce(); - expect(child.$$listeners['evt'].length).toBe(3); // cleanup will happen on next $emit + expect(child.$$listeners['evt'].length).toBe(2); spy1.calls.reset(); spy2.calls.reset(); @@ -2571,7 +2685,7 @@ describe('Scope', function() { expect(spy1).toHaveBeenCalledOnce(); expect(spy2).toHaveBeenCalledOnce(); expect(spy3).toHaveBeenCalledOnce(); - expect(child.$$listeners['evt'].length).toBe(3); //cleanup will happen on next $broadcast + expect(child.$$listeners['evt'].length).toBe(2); spy1.calls.reset(); spy2.calls.reset(); From 2c9c3a07845d9a0aae12fa3259983d37b68f918f Mon Sep 17 00:00:00 2001 From: Lisa Pfisterer Date: Thu, 2 Nov 2017 16:23:20 +0000 Subject: [PATCH 013/469] docs(guide/Unit Testing): change $scope = {} to $scope = $rootScope.$new() {} will just create an empty object. This will break if the module uses for example $watch or others. While it's not necessary for this example, it's good general practice. Closes #16315 --- docs/content/guide/unit-testing.ngdoc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/content/guide/unit-testing.ngdoc b/docs/content/guide/unit-testing.ngdoc index 63f1b8667da4..cc689a23473b 100644 --- a/docs/content/guide/unit-testing.ngdoc +++ b/docs/content/guide/unit-testing.ngdoc @@ -149,16 +149,17 @@ for instantiating controllers. describe('PasswordController', function() { beforeEach(module('app')); - var $controller; + var $controller, $rootScope; - beforeEach(inject(function(_$controller_){ + beforeEach(inject(function(_$controller_, _$rootScope_){ // The injector unwraps the underscores (_) from around the parameter names when matching $controller = _$controller_; + $rootScope = _$rootScope_; })); describe('$scope.grade', function() { it('sets the strength to "strong" if the password length is >8 chars', function() { - var $scope = {}; + var $scope = $rootScope.$new(); var controller = $controller('PasswordController', { $scope: $scope }); $scope.password = 'longerthaneightchars'; $scope.grade(); From 667db466f959f8bbca1451d0f1c1a3db25d46a6c Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Tue, 31 Oct 2017 21:41:28 +0000 Subject: [PATCH 014/469] fix(sanitizeUri): sanitize URIs that contain IDEOGRAPHIC SPACE chars Browsers mutate attributes values such as ` javascript:alert(1)` when they are written to the DOM via `innerHTML` in various vendor specific ways. In Chrome (<62), this mutation removed the preceding "whitespace" resulting in a value that could end up being executed as JavaScript. Here is an example of what could happen: https://plnkr.co/edit/Y6EsbsuDgd18YTn1oARu?p=preview If you run that in Chrome 61 you will get a dialog box pop up. There is background here: http://www.nds.rub.de/media/emma/veroeffentlichungen/2013/12/10/mXSS-CCS13.pdf The sanitizer has a bit of code that triggers this mutation on an inert piece of DOM, before we try to sanitize it: https://github.com/angular/angular.js/blob/817ac567/src/ngSanitize/sanitize.js#L406-L417 Chrome 62 does not appear to mutate this particular string any more, instead it just leaves the "whitespace" in place. This probably means that Chrome 62 is no longer vulnerable to this specific attack vector; but there may be other mutating strings that we haven't found, which are vulnerable. Since we are leaving the mXSS check in place, the sanitizer should still be immune to any strings that try to utilise this attack vector. This commit uses `trim()` to remove the IDEOGRAPHIC SPACE "whitespace" before sanitizing, which allows us to expose this mXSS test to all browsers rather than just Chrome. Closes #16288 --- src/ng/sanitizeUri.js | 2 +- test/ngSanitize/sanitizeSpec.js | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/ng/sanitizeUri.js b/src/ng/sanitizeUri.js index a5302994415d..f7dc60bf3c41 100644 --- a/src/ng/sanitizeUri.js +++ b/src/ng/sanitizeUri.js @@ -62,7 +62,7 @@ function $$SanitizeUriProvider() { return function sanitizeUri(uri, isImage) { var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; var normalizedVal; - normalizedVal = urlResolve(uri).href; + normalizedVal = urlResolve(uri && uri.trim()).href; if (normalizedVal !== '' && !normalizedVal.match(regex)) { return 'unsafe:' + normalizedVal; } diff --git a/test/ngSanitize/sanitizeSpec.js b/test/ngSanitize/sanitizeSpec.js index c3206948e990..2bab68093181 100644 --- a/test/ngSanitize/sanitizeSpec.js +++ b/test/ngSanitize/sanitizeSpec.js @@ -237,11 +237,9 @@ describe('HTML', function() { .toEqual(''); }); - if (isChrome) { - it('should prevent mXSS attacks', function() { - expectHTML('CLICKME').toBe('CLICKME'); - }); - } + it('should prevent mXSS attacks', function() { + expectHTML('CLICKME').toBe('CLICKME'); + }); it('should strip html comments', function() { expectHTML('

    text1text2

    ') From 0cbc50512126fa22546dbe9b79a14939d9dc4459 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 2 Nov 2017 12:58:49 +0000 Subject: [PATCH 015/469] fix($location): do not decode forward slashes in the path in HTML5 mode Closes #16312 --- src/ng/location.js | 30 +++++++++++++++++++++++------- test/ng/locationSpec.js | 11 +++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/ng/location.js b/src/ng/location.js index bf1858079c3f..184428883090 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -16,7 +16,23 @@ function encodePath(path) { i = segments.length; while (i--) { - segments[i] = encodeUriSegment(segments[i]); + // decode forward slashes to prevent them from being double encoded + segments[i] = encodeUriSegment(segments[i].replace(/%2F/g, '/')); + } + + return segments.join('/'); +} + +function decodePath(path, html5Mode) { + var segments = path.split('/'), + i = segments.length; + + while (i--) { + segments[i] = decodeURIComponent(segments[i]); + if (html5Mode) { + // encode forward slashes to prevent them from being mistaken for path separators + segments[i] = segments[i].replace(/\//g, '%2F'); + } } return segments.join('/'); @@ -31,7 +47,7 @@ function parseAbsoluteUrl(absoluteUrl, locationObj) { } var DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/; -function parseAppUrl(url, locationObj) { +function parseAppUrl(url, locationObj, html5Mode) { if (DOUBLE_SLASH_REGEX.test(url)) { throw $locationMinErr('badpath', 'Invalid url "{0}".', url); @@ -42,8 +58,8 @@ function parseAppUrl(url, locationObj) { url = '/' + url; } var match = urlResolve(url); - locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? - match.pathname.substring(1) : match.pathname); + var path = prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname; + locationObj.$$path = decodePath(path, html5Mode); locationObj.$$search = parseKeyValue(match.search); locationObj.$$hash = decodeURIComponent(match.hash); @@ -118,7 +134,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { appBaseNoFile); } - parseAppUrl(pathUrl, this); + parseAppUrl(pathUrl, this, true); if (!this.$$path) { this.$$path = '/'; @@ -221,7 +237,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { } } - parseAppUrl(withoutHashUrl, this); + parseAppUrl(withoutHashUrl, this, false); this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); @@ -406,7 +422,7 @@ var locationPrototype = { } var match = PATH_MATCH.exec(url); - if (match[1] || url === '') this.path(decodeURIComponent(match[1])); + if (match[1] || url === '') this.path(decodeURI(match[1])); if (match[2] || match[1] || url === '') this.search(match[3] || ''); this.hash(match[5] || ''); diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index 723e12740a30..dd48edbe4e52 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -477,6 +477,17 @@ describe('$location', function() { expect(locationUrl.hash()).toBe('x <>#'); }); + + it('should not decode encoded forward slashes in the path', function() { + var locationUrl = new LocationHtml5Url('http://host.com/base/', 'http://host.com/base/'); + locationUrl.$$parse('http://host.com/base/a/ng2;path=%2Fsome%2Fpath'); + expect(locationUrl.path()).toBe('/a/ng2;path=%2Fsome%2Fpath'); + expect(locationUrl.search()).toEqual({}); + expect(locationUrl.hash()).toBe(''); + expect(locationUrl.url()).toBe('/a/ng2;path=%2Fsome%2Fpath'); + expect(locationUrl.absUrl()).toBe('http://host.com/base/a/ng2;path=%2Fsome%2Fpath'); + }); + it('should decode pluses as spaces in urls', function() { var locationUrl = new LocationHtml5Url('http://host.com/', 'http://host.com/'); locationUrl.$$parse('http://host.com/?a+b=c+d'); From 181ac0b7a1823bd9965f5e88b3c5b437dc00fafd Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 3 Nov 2017 12:41:54 +0100 Subject: [PATCH 016/469] docs(*): update CONTRIBUTING.md and create DEVELOPERS.md CONTRIBUTING.md - focus on basic info about issues and pull requests for new contributors - move development heavy info to DEVELOPERS.md + add links - remove outdated info DEVELOPERS.md - contains info about project setup, coding rules, and commit message guidelines from CONTRIBUTING.md - add and update info about writing docs from Wiki - add info about development setup from docs contribute.md - add info about running tests on Saucelabs / Browserstack Closes #7303 Closes #9444 Closes #16297 --- .github/ISSUE_TEMPLATE.md | 5 +- .github/PULL_REQUEST_TEMPLATE.md | 7 +- CONTRIBUTING.md | 296 ++++------- DEVELOPERS.md | 498 ++++++++++++++++++ README.md | 32 +- .../templates/app/indexPage.template.html | 1 + docs/content/misc/contribute.ngdoc | 179 +------ 7 files changed, 638 insertions(+), 380 deletions(-) create mode 100644 DEVELOPERS.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 71d1136be120..2f0d2c1177f8 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -11,7 +11,7 @@ IF YOU DON'T FILL OUT THE FOLLOWING INFORMATION WE MIGHT CLOSE YOUR ISSUE WITHOU - [ ] bug report - [ ] feature request -- [ ] other (Please do not submit support requests here (see above)) +- [ ] other **Current behavior:** @@ -27,7 +27,8 @@ https://plnkr.co or similar (you can use this template as a starting point: http --> **AngularJS version:** 1.x.y - + **Browser:** [all | Chrome XX | Firefox XX | Edge XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9a79ea9890ee..d4c3f81373a3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,4 @@ + **What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)** @@ -15,9 +16,9 @@ **Please check if the PR fulfills these requirements** -- [ ] The commit message follows our guidelines: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format -- [ ] Tests for the changes have been added (for bug fixes / features) -- [ ] Docs have been added / updated (for bug fixes / features) +- [ ] The commit message follows our [guidelines](../DEVELOPERS.md#commits) +- [ ] Fix/Feature: [Docs](../DEVELOPERS.md#documentation) have been added/updated +- [ ] Fix/Feature: Tests have been added; existing tests pass **Other information**: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22849d948da2..e1e61391ddf1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,81 +3,97 @@ We'd love for you to contribute to our source code and to make AngularJS even better than it is today! Here are the guidelines we'd like you to follow: - - [Code of Conduct](#coc) - - [Question or Problem?](#question) - - [Issues and Bugs](#issue) - - [Feature Requests](#feature) - - [Submission Guidelines](#submit) - - [Coding Rules](#rules) - - [Commit Message Guidelines](#commit) - - [Signing the CLA](#cla) - - [Further Info](#info) +* [Code of Conduct](#coc) +* [Questions and Problems](#question) +* [Issues and Bugs](#issue) +* [Feature Requests](#feature) +* [Improving Documentation](#docs) +* [Issue Submission Guidelines](#submit) +* [Pull Request Submission Guidelines](#submit-pr) +* [Signing the CLA](#cla) ## Code of Conduct Help us keep AngularJS open and inclusive. Please read and follow our [Code of Conduct][coc]. -## Got a Question or Problem? +## Questions, Bugs, Features -If you have questions about how to use AngularJS, please direct these to the [Google Group][groups] -discussion list or [StackOverflow][stackoverflow]. We are also available on [IRC][irc] and -[Gitter][gitter]. +### Got a Question or Problem? -## Found an Issue? +Do not open issues for general support questions as we want to keep GitHub issues for bug reports +and feature requests. You've got much better chances of getting your question answered on dedicated +support platforms, the best being [Stack Overflow][stackoverflow]. -If you find a bug in the source code or a mistake in the documentation, you can help us by -submitting an issue to our [GitHub Repository][github]. Even better you can submit a Pull Request -with a fix. +Stack Overflow is a much better place to ask questions since: -**Localization Issues:** AngularJS uses the [Google Closure I18N library] to generate -its own I18N files (the ngLocale module). This means that any changes to these files would be lost -the next time that we import the library. +- there are thousands of people willing to help on Stack Overflow +- questions and answers stay available for public viewing so your question / answer might help + someone else +- Stack Overflow's voting system assures that the best answers are prominently visible. + +To save your and our time, we will systematically close all issues that are requests for general +support and redirect people to the section you are reading right now. + +Other channels for support are: +- the [Google Group][groups] discussion list +- the [AngularJS IRC][irc] +- the [AngularJS Gitter][gitter] + +### Found an Issue or Bug? + +If you find a bug in the source code, you can help us by submitting an issue to our +[GitHub Repository][github]. Even better, you can submit a Pull Request with a fix. + +**Please see the [Submission Guidelines](#submit) below.** + +**Special Note for Localization Issues:** AngularJS uses the [Google Closure I18N library] to +generate its own I18N files (the ngLocale module). This means that any changes to these files +would be lost the next time that we import the library. Since the Closure library i18n data is itself auto-generated from the data of the [Common Locale Data Repository (CLDR)] project, errors in the data should be reported there. See also the [Closure guide to i18n changes]. -**Please see the [Submission Guidelines](#submit) below.** +### Missing a Feature? -## Want a Feature? +You can request a new feature by submitting an issue to our [GitHub Repository][github-issues]. -You can request a new feature by submitting an issue to our [GitHub Repository][github]. If you -would like to implement a new feature then consider what kind of change it is: +If you would like to implement a new feature then consider what kind of change it is: -* **Major Changes** that you wish to contribute to the project should be discussed first on our - [dev mailing list][angular-dev] or [IRC][irc] so that we can better coordinate our efforts, - prevent duplication of work, and help you to craft the change so that it is successfully accepted - into the project. -* **Small Changes** can be crafted and submitted to the [GitHub Repository][github] as a Pull - Request. +* **Major Changes** that you wish to contribute to the project should be discussed first in an + [GitHub issue][github-issues] that clearly outlines the changes and benefits of the feature. +* **Small Changes** can directly be crafted and submitted to the [GitHub Repository][github] + as a Pull Request. See the section about [Pull Request Submission Guidelines](#submit-pr), and + for detailed information the [core development documentation][developers]. +### Want a Doc Fix? -## Want a Doc Fix? +Should you have a suggestion for the documentation, you can open an issue and outline the problem +or improvement you have - however, creating the doc fix yourself is much better! If you want to help improve the docs, it's a good idea to let others know what you're working on to minimize duplication of effort. Create a new issue (or comment on a related existing one) to let others know what you're working on. +If you're making a small change (typo, phrasing) don't worry about filing an issue first. Use the +friendly blue "Improve this doc" button at the top right of the doc page to fork the repository +in-place and make a quick change on the fly. The commit message is preformatted to the right type +and scope, so you only have to add the description. + For large fixes, please build and test the documentation before submitting the PR to be sure you haven't accidentally introduced any layout or formatting issues. You should also make sure that your -commit message starts with "docs" and follows the **[Commit Message Guidelines](#commit)** outlined -below. +commit message follows the **[Commit Message Guidelines][developers.commits]**. -If you're just making a small change, don't worry about filing an issue first. Use the friendly blue -"Improve this doc" button at the top right of the doc page to fork the repository in-place and make -a quick change on the fly. When naming the commit, it is advised to follow the commit message -guidelines below, by starting the commit message with **docs** and referencing the filename. Since -this is not obvious and some changes are made on the fly, this is not strictly necessary and we will -understand if this isn't done the first few times. - -## Submission Guidelines - -### Submitting an Issue +## Issue Submission Guidelines Before you submit your issue search the archive, maybe your question was already answered. If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize the effort we can spend fixing issues and adding new features, by not reporting duplicate issues. -Providing the following information will increase the chances of your issue being dealt with -quickly: + +The "[new issue][github-new-issue]" form contains a number of prompts that you should fill out to +make it easier to understand and categorize the issue. + +In general, providing the following information will increase the chances of your issue being dealt +with quickly: * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps * **Motivation for or Use Case** - explain why this is a bug for you @@ -89,38 +105,40 @@ quickly: * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be causing the problem (line of code or commit) -Here is a great example of a well defined issue: https://github.com/angular/angular.js/issues/5069 +Here is a great example of a well defined issue: https://github.com/angular/angular.js/issues/5069. **If you get help, help others. Good karma rulez!** -### Submitting a Pull Request +## Pull Request Submission Guidelines Before you submit your pull request consider the following guidelines: * Search [GitHub](https://github.com/angular/angular.js/pulls) for an open or closed Pull Request that relates to your submission. You don't want to duplicate effort. -* Please sign our [Contributor License Agreement (CLA)](#cla) before sending pull - requests. We cannot accept code without this. +* Create the [development environment][developers.setup] * Make your changes in a new git branch: ```shell git checkout -b my-fix-branch master ``` -* Create your patch, **including appropriate test cases**. -* Follow our [Coding Rules](#rules). -* Run the full AngularJS test suite, as described in the [developer documentation][dev-doc], - and ensure that all tests pass. +* Create your patch commit, **including appropriate test cases**. +* Follow our [Coding Rules][developers.rules]. +* If the changes affect public APIs, change or add relevant [documentation][developers.documentation]. +* Run the AngularJS [unit][developers.tests-unit] and [E2E test][developers.tests-e2e] suites, and ensure that all tests + pass. It is generally sufficient to run the tests only on Chrome, as our Travis integration will + run the tests on all supported browsers. +* Run `grunt eslint` to check that you have followed the automatically enforced coding rules * Commit your changes using a descriptive commit message that follows our - [commit message conventions](#commit) and passes our commit message presubmit hook - (`validate-commit-msg.js`). Adherence to the [commit message conventions](#commit) is required, - because release notes are automatically generated from these messages. + [commit message conventions][developers.commits]. Adherence to the + [commit message conventions][developers.commits] is required, because release notes are + automatically generated from these messages. ```shell git commit -a ``` Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. -* Build your changes locally to ensure all the tests pass: +* Before creating the Pull Request, package and run all tests a last time: ```shell grunt test @@ -132,24 +150,30 @@ Before you submit your pull request consider the following guidelines: git push origin my-fix-branch ``` -In GitHub, send a pull request to `angular.js:master`. -If we suggest changes, then: +* In GitHub, send a pull request to `angular.js:master`. This will trigger the check of the +[Contributor License Agreement](#cla) and the Travis integration. + +* If you find that the Travis integration has failed, look into the logs on Travis to find out +if your changes caused test failures, the commit message was malformed etc. If you find that the +tests failed or times out for unrelated reasons, you can ping a team member so that the build can be +restarted. + +* If we suggest changes, then: -* Make the required updates. -* Re-run the AngularJS test suite to ensure tests are still passing. -* Commit your changes to your branch (e.g. `my-fix-branch`). -* Push the changes to your GitHub repository (this will update your Pull Request). + * Make the required updates. + * Re-run the AngularJS test suite to ensure tests are still passing. + * Commit your changes to your branch (e.g. `my-fix-branch`). + * Push the changes to your GitHub repository (this will update your Pull Request). -If the PR gets too outdated we may ask you to rebase and force push to update the PR: + You can also amend the initial commits and force push them to the branch. -```shell -git rebase master -i -git push origin my-fix-branch -f -``` + ```shell + git rebase master -i + git push origin my-fix-branch -f + ``` -_WARNING: Squashing or reverting commits and force-pushing thereafter may remove GitHub comments -on code that were previously made by you or others in your commits. Avoid any form of rebasing -unless necessary._ + This is generally easier to follow, but seperate commits are useful if the Pull Request contains + iterations that might be interesting to see side-by-side. That's it! Thank you for your contribution! @@ -182,135 +206,41 @@ from the main (upstream) repository: git pull --ff upstream master ``` -## Coding Rules - -To ensure consistency throughout the source code, keep these rules in mind as you are working: - -* All features or bug fixes **must be tested** by one or more [specs][unit-testing]. -* All public API methods **must be documented** with ngdoc, an extended version of jsdoc (we added - support for markdown and templating via @ngdoc tag). To see how we document our APIs, please check - out the existing source code and see [this wiki page][ngDocs]. -* With the exceptions listed below, we follow the rules contained in - [Google's JavaScript Style Guide][js-style-guide]: - * **Do not use namespaces**: Instead, wrap the entire AngularJS code base in an anonymous closure and - export our API explicitly rather than implicitly. - * Wrap all code at **100 characters**. - * Instead of complex inheritance hierarchies, we **prefer simple objects**. We use prototypal - inheritance only when absolutely necessary. - * We **love functions and closures** and, whenever possible, prefer them over objects. - * To write concise code that can be better minified, we **use aliases internally** that map to the - external API. See our existing code to see what we mean. - * We **don't go crazy with type annotations** for private internal APIs unless it's an internal API - that is used throughout AngularJS. The best guidance is to do what makes the most sense. - -## Git Commit Guidelines - -We have very precise rules over how our git commit messages can be formatted. This leads to **more -readable messages** that are easy to follow when looking through the **project history**. But also, -we use the git commit messages to **generate the AngularJS change log**. - -The commit message formatting can be added using a typical git workflow or through the use of a CLI -wizard ([Commitizen](https://github.com/commitizen/cz-cli)). To use the wizard, run `yarn run commit` -in your terminal after staging your changes in git. - -### Commit Message Format -Each commit message consists of a **header**, a **body** and a **footer**. The header has a special -format that includes a **type**, a **scope** and a **subject**: +## Signing the Contributor License Agreement (CLA) -``` -(): - - - -
    -``` - -The **header** is mandatory and the **scope** of the header is optional. - -Any line of the commit message cannot be longer 100 characters! This allows the message to be easier -to read on GitHub as well as in various git tools. - -### Revert -If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. -In the body it should say: `This reverts commit .`, where the hash is the SHA of the commit being reverted. - -### Type -Must be one of the following: - -* **feat**: A new feature -* **fix**: A bug fix -* **docs**: Documentation only changes -* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing - semi-colons, etc) -* **refactor**: A code change that neither fixes a bug nor adds a feature -* **perf**: A code change that improves performance -* **test**: Adding missing or correcting existing tests -* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation - generation - -### Scope -The scope could be anything specifying place of the commit change. For example `$location`, -`$browser`, `$compile`, `$rootScope`, `ngHref`, `ngClick`, `ngView`, etc... - -You can use `*` when the change affects more than a single scope. - -### Subject -The subject contains succinct description of the change: - -* use the imperative, present tense: "change" not "changed" nor "changes" -* don't capitalize first letter -* no dot (.) at the end - -### Body -Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". -The body should include the motivation for the change and contrast this with previous behavior. - -### Footer -The footer should contain any information about **Breaking Changes** and is also the place to -[reference GitHub issues that this commit closes][closing-issues]. - -**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. -The rest of the commit message is then used for this. - -A detailed explanation can be found in this [document][commit-message-format]. - -## Signing the CLA - -Please sign our Contributor License Agreement (CLA) before sending pull requests. For any code -changes to be accepted, the CLA must be signed. It's a quick process, we promise! +Upon submmitting a Pull Request, a friendly bot will ask you to sign our CLA if you haven't done +so before. Unfortunately, this is necessary for documentation changes, too. +It's a quick process, we promise! * For individuals we have a [simple click-through form][individual-cla]. * For corporations we'll need you to [print, sign and one of scan+email, fax or mail the form][corporate-cla]. -## Further Information -You can find out more detailed information about contributing in the -[AngularJS documentation][contributing]. - -[Google Closure I18N library]: https://github.com/google/closure-library/tree/master/closure/goog/i18n -[angular-dev]: https://groups.google.com/forum/?fromgroups#!forum/angular-dev -[closing-issues]: https://help.github.com/articles/closing-issues-via-commit-messages/ +[Closure guide to i18n changes]: https://github.com/google/closure-library/wiki/Internationalization-%28i18n%29-changes-in-Closure-Library [coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md -[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit# -[contribute]: http://docs.angularjs.org/misc/contribute -[contributing]: http://docs.angularjs.org/misc/contribute +[Common Locale Data Repository (CLDR)]: http://cldr.unicode.org [corporate-cla]: http://code.google.com/legal/corporate-cla-v1.0.html -[dev-doc]: https://docs.angularjs.org/guide +[developers]: DEVELOPERS.md +[developers.commits]: DEVELOPERS.md#commits +[developers.documentation]: DEVELOPERS.md#documentation +[developers.rules]: DEVELOPERS.md#rules +[developers.setup]: DEVELOPERS.md#setup +[developers.tests-e2e]: DEVELOPERS.md#e2e-tests +[developers.tests-unit]: DEVELOPERS.md#unit-tests +[github-issues]: https://github.com/angular/angular.js/issues +[github-new-issue]: https://github.com/angular/angular.js/issues/new [github]: https://github.com/angular/angular.js [gitter]: https://gitter.im/angular/angular.js +[Google Closure I18N library]: https://github.com/google/closure-library/tree/master/closure/goog/i18n [groups]: https://groups.google.com/forum/?fromgroups#!forum/angular [individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html [irc]: http://webchat.freenode.net/?channels=angularjs&uio=d4 -[js-style-guide]: https://google.github.io/styleguide/javascriptguide.xml [jsfiddle]: http://jsfiddle.net/ -[list]: https://groups.google.com/forum/?fromgroups#!forum/angular -[ngDocs]: https://github.com/angular/angular.js/wiki/Writing-AngularJS-Documentation +[karma-browserstack]: https://github.com/karma-runner/karma-browserstack-launcher +[karma-saucelabs]: https://github.com/karma-runner/karma-sauce-launcher [plunker]: http://plnkr.co/edit [stackoverflow]: http://stackoverflow.com/questions/tagged/angularjs -[unit-testing]: https://docs.angularjs.org/guide/unit-testing -[Common Locale Data Repository (CLDR)]: http://cldr.unicode.org -[Closure guide to i18n changes]: https://github.com/google/closure-library/wiki/Internationalization-%28i18n%29-changes-in-Closure-Library [![Analytics](https://ga-beacon.appspot.com/UA-8594346-11/angular.js/CONTRIBUTING.md?pixel)](https://github.com/igrigorik/ga-beacon) diff --git a/DEVELOPERS.md b/DEVELOPERS.md new file mode 100644 index 000000000000..cf3f4d7d04e9 --- /dev/null +++ b/DEVELOPERS.md @@ -0,0 +1,498 @@ +# Developing AngularJS + +* [Development Setup](#setup) +* [Coding Rules](#rules) +* [Commit Message Guidelines](#commits) +* [Writing Documentation](#documentation) + +## Development Setup + +This document describes how to set up your development environment to build and test AngularJS, and +explains the basic mechanics of using `git`, `node`, `yarn`, `grunt`, and `bower`. + +### Installing Dependencies + +Before you can build AngularJS, you must install and configure the following dependencies on your +machine: + +* [Git](http://git-scm.com/): The [Github Guide to + Installing Git][git-setup] is a good source of information. + +* [Node.js v6.x (LTS)](http://nodejs.org): We use Node to generate the documentation, run a + development web server, run tests, and generate distributable files. Depending on your system, + you can install Node either from source or as a pre-packaged bundle. + + We recommend using [nvm](https://github.com/creationix/nvm) (or + [nvm-windows](https://github.com/coreybutler/nvm-windows)) + to manage and install Node.js, which makes it easy to change the version of Node.js per project. + +* [Yarn](https://yarnpkg.com): We use Yarn to install our Node.js module dependencies + (rather than using npm). See the detailed [installation instructions][yarn-install]. + +* [Java](http://www.java.com): We minify JavaScript using + [Closure Tools](https://developers.google.com/closure/), which require Java (version 7 or higher) + to be installed and included in your + [PATH](http://docs.oracle.com/javase/tutorial/essential/environment/paths.html) variable. + +* [Grunt](http://gruntjs.com): We use Grunt as our build system. Install the grunt command-line tool + globally with: + + ```shell + yarn global add grunt-cli + ``` + +### Forking AngularJS on Github + +To contribute code to AngularJS, you must have a GitHub account so you can push code to your own +fork of AngularJS and open Pull Requests in the [GitHub Repository][github]. + +To create a Github account, follow the instructions [here](https://github.com/signup/free). +Afterwards, go ahead and [fork](http://help.github.com/forking) the +[main AngularJS repository][github]. + + +### Building AngularJS + +To build AngularJS, you clone the source code repository and use Grunt to generate the non-minified +and minified AngularJS files: + +```shell +# Clone your Github repository: +git clone https://github.com//angular.js.git + +# Go to the AngularJS directory: +cd angular.js + +# Add the main AngularJS repository as an upstream remote to your repository: +git remote add upstream "https://github.com/angular/angular.js.git" + +# Install node.js dependencies: +yarn install + +# Build AngularJS (which will install `bower` dependencies automatically): +grunt package +``` + +**Note:** If you're using Windows, you must use an elevated command prompt (right click, run as +Administrator). This is because `grunt package` creates some symbolic links. + +**Note:** If you're using Linux, and `yarn install` fails with the message +'Please try running this command again as root/Administrator.', you may need to globally install +`grunt` and `bower`: + +```shell +sudo yarn global add grunt-cli +sudo yarn global add bower +``` + +The build output is in the `build` directory. It consists of the following files and +directories: + +* `angular-.zip` — The complete zip file, containing all of the release build +artifacts. + +* `angular.js` / `angular.min.js` — The regular and minified core AngularJS script file. + +* `angular-*.js` / `angular-*.min.js` — All other AngularJS module script files. + +* `docs/` — A directory that contains a standalone version of the docs + (same as served in `docs.angularjs.org`). + +### Running a Local Development Web Server + +To debug code, run end-to-end tests, and serve the docs, it is often useful to have a local +HTTP server. For this purpose, we have made available a local web server based on Node.js. + +1. To start the web server, run: + ```shell + grunt webserver + ``` + +2. To access the local server, enter the following URL into your web browser: + ```text + http://localhost:8000/ + ``` + By default, it serves the contents of the AngularJS project directory. + +3. To access the locally served docs, visit this URL: + ```text + http://localhost:8000/build/docs/ + ``` + +### Running the Unit Test Suite + +We write unit and integration tests with Jasmine and execute them with Karma. To run all of the +tests once on Chrome run: + +```shell +grunt test:unit +``` + +To run the tests on other browsers (Chrome, ChromeCanary, Firefox and Safari are pre-configured) use: + +```shell +grunt test:unit --browsers=Chrome,Firefox +``` + +**Note:** there should be _no spaces between browsers_. `Chrome, Firefox` is INVALID. + +If you have a Saucelabs or Browserstack account, you can also run the unit tests on these services +via our pre-defined customLaunchers. + +For example, to run the whole unit test suite: + +```shell +# Browserstack +grunt test:unit --browsers=BS_Chrome,BS_Firefox,BS_Safari,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9,BS_iOS_10 +# Saucelabs +grunt test:unit --browsers=BS_Chrome,BS_Firefox,BS_Safari,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9,BS_iOS_10 +``` + +Running these commands requires you to set up [Karma Browserstack][karma-browserstack] or +[Karma-Saucelabs][karma-saucelabs], respectively. + +During development, however, it's more productive to continuously run unit tests every time the +source or test files change. To execute tests in this mode run: + +1. To start the Karma server, capture Chrome browser and run unit tests, run: + + ```shell + grunt autotest + ``` + +2. To capture more browsers, open this URL in the desired browser (URL might be different if you + have multiple instance of Karma running, read Karma's console output for the correct URL): + + ```text + http://localhost:9876/ + ``` + +3. To re-run tests just change any source or test file. + + +To learn more about all of the preconfigured Grunt tasks run: + +```shell +grunt --help +``` + + +### Running the End-to-end Test Suite + +AngularJS's end to end tests are run with Protractor. Simply run: + +```shell +grunt test:e2e +``` + +This will start the webserver and run the tests on Chrome. + +## Coding Rules + +To ensure consistency throughout the source code, keep these rules in mind as you are working: + +* All features or bug fixes **must be tested** by one or more [specs][unit-testing]. +* All public API methods **must be documented** with ngdoc, an extended version of jsdoc (we added + support for markdown and templating via @ngdoc tag). To see how we document our APIs, please check + out the existing source code and see the section about [writing documentation](#documentation) +* With the exceptions listed below, we follow the rules contained in + [Google's JavaScript Style Guide][js-style-guide]: + * **Do not use namespaces**: Instead, wrap the entire AngularJS code base in an anonymous + closure and export our API explicitly rather than implicitly. + * Wrap all code at **100 characters**. + * Instead of complex inheritance hierarchies, we **prefer simple objects**. We use prototypal + inheritance only when absolutely necessary. + * We **love functions and closures** and, whenever possible, prefer them over objects. + * To write concise code that can be better minified, we **use aliases internally** that map to + the external API. See our existing code to see what we mean. + * We **don't go crazy with type annotations** for private internal APIs unless it's an internal + API that is used throughout AngularJS. The best guidance is to do what makes the most sense. + +### Specific topics + +#### Provider configuration + +When adding configuration (options) to [providers][docs.provider], we follow a special pattern. + +- for each option, add a `method` that ... + - works as a getter and returns the current value when called without argument + - works as a setter and returns itself for chaining when called with argument + - for boolean options, uses the naming scheme `
  • Seed App project template
  • GitHub
  • +
  • Changelog
  • Download
  • diff --git a/docs/content/misc/contribute.ngdoc b/docs/content/misc/contribute.ngdoc index 9adda80460d7..9e6bb6721e14 100644 --- a/docs/content/misc/contribute.ngdoc +++ b/docs/content/misc/contribute.ngdoc @@ -2,177 +2,12 @@ @name Develop @description -# Building and Testing AngularJS +# Contributing & Development -This document describes how to set up your development environment to build and test AngularJS, and -explains the basic mechanics of using `git`, `node`, `yarn`, `grunt`, and `bower`. - -See the [contributing guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md) -for how to contribute your own code to AngularJS. - -## Installing Dependencies - -Before you can build AngularJS, you must install and configure the following dependencies on your -machine: - -* [Git](http://git-scm.com/): The [Github Guide to - Installing Git](https://help.github.com/articles/set-up-git) is a good source of information. - -* [Node.js v6.x (LTS)](http://nodejs.org): We use Node to generate the documentation, run a - development web server, run tests, and generate distributable files. Depending on your system, - you can install Node either from source or as a pre-packaged bundle. - - We recommend using [nvm](https://github.com/creationix/nvm) (or [nvm-windows](https://github.com/coreybutler/nvm-windows)) - to manage and install Node.js, which makes it easy to change the version of Node.js per project. - -* [Yarn](https://yarnpkg.com): We use Yarn to install our Node.js module dependencies (rather than using npm). - There are detailed installation instructions available at https://yarnpkg.com/en/docs/install. - -* [Java](http://www.java.com): We minify JavaScript using our - [Closure Tools](https://developers.google.com/closure/) jar. Make sure you have Java (version 7 or higher) - installed and included in your [PATH](http://docs.oracle.com/javase/tutorial/essential/environment/paths.html) - variable. - -* [Grunt](http://gruntjs.com): We use Grunt as our build system. Install the grunt command-line tool globally with: - - ```shell - yarn global add grunt-cli - ``` - -## Forking AngularJS on Github - -To create a Github account, follow the instructions [here](https://github.com/signup/free). -Afterwards, go ahead and [fork](http://help.github.com/forking) the [main AngularJS repository](https://github.com/angular/angular.js). - - -## Building AngularJS - -To build AngularJS, you clone the source code repository and use Grunt to generate the non-minified and -minified AngularJS files: - -```shell -# Clone your Github repository: -git clone https://github.com//angular.js.git - -# Go to the AngularJS directory: -cd angular.js - -# Add the main AngularJS repository as an upstream remote to your repository: -git remote add upstream "https://github.com/angular/angular.js.git" - -# Install node.js dependencies: -yarn install - -# Build AngularJS (which will install `bower` dependencies automatically): -grunt package -``` - - -
    -**Note:** If you're using Windows, you must use an elevated command prompt (right click, run as -Administrator). This is because `grunt package` creates some symbolic links. -
    - -
    -**Note:** If you're using Linux, and `yarn install` fails with the message -'Please try running this command again as root/Administrator.', you may need to globally install `grunt` and `bower`: -
      -
    • sudo yarn global add grunt-cli
    • -
    • sudo yarn global add bower
    • -
    - -
    - -The build output can be located under the `build` directory. It consists of the following files and -directories: - -* `angular-.zip` — The complete zip file, containing all of the release build -artifacts. - -* `angular.js` — The non-minified AngularJS script. - -* `angular.min.js` — The minified AngularJS script. - -* `angular-scenario.js` — The AngularJS End2End test runner. - -* `docs/` — A directory that contains all of the files needed to run `docs.angularjs.org`. - -* `docs/index.html` — The main page for the documentation. - -* `docs/docs-scenario.html` — The End2End test runner for the documentation application. - - -## Running a Local Development Web Server - -To debug code and run end-to-end tests, it is often useful to have a local HTTP server. For this purpose, we have -made available a local web server based on Node.js. - -1. To start the web server, run: - ```shell - grunt webserver - ``` - -2. To access the local server, enter the following URL into your web browser: - ```text - http://localhost:8000/ - ``` - By default, it serves the contents of the AngularJS project directory. - -3. To access the locally served docs, visit this URL: - ```text - http://localhost:8000/build/docs/ - ``` - -## Running the Unit Test Suite - -We write unit and integration tests with Jasmine and execute them with Karma. To run all of the -tests once on Chrome run: - -```shell -grunt test:unit -``` - -To run the tests on other browsers (Chrome, ChromeCanary, Firefox and Safari are pre-configured) use: - -```shell -grunt test:unit --browsers=Chrome,Firefox -``` - -Note there should be _no spaces between browsers_. `Chrome, Firefox` is INVALID. - -During development, however, it's more productive to continuously run unit tests every time the source or test files -change. To execute tests in this mode run: - -1. To start the Karma server, capture Chrome browser and run unit tests, run: - - ```shell - grunt autotest - ``` - -2. To capture more browsers, open this URL in the desired browser (URL might be different if you have multiple instance - of Karma running, read Karma's console output for the correct URL): - - ```text - http://localhost:9876/ - ``` - -3. To re-run tests just change any source or test file. - - -To learn more about all of the preconfigured Grunt tasks run: - -```shell -grunt --help -``` - - -## Running the End-to-end Test Suite - -AngularJS's end to end tests are run with Protractor. Simply run: - -```shell -grunt test:e2e -``` - -This will start the webserver and run the tests on Chrome. +For everything related to contributing, we have a document in our Git Repository that covers the +basics about support channels, creating issues, and pull requests: +[Contributing](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md). +For developers, we have a more detailed document that covers project setup, coding rules, and +a guide to writing documentation: +[Developing](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md). \ No newline at end of file From f876ab71913e17e9126baad19ab795f28b61bfe6 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Sat, 11 Nov 2017 14:10:53 +0100 Subject: [PATCH 017/469] docs(*): remove usage of global grunt-cli Previously, the `DEVELOPERS.md` and `CONTRIBUTING.md` files refered to global `grunt-cli` by default. This commit ensures the local `grunt-cli` is used by default and mentiones the possibility to still use the global `grunt-cli`. --- CONTRIBUTING.md | 4 ++-- DEVELOPERS.md | 42 +++++++++++++++--------------------------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e1e61391ddf1..57c7dd2d6027 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -127,7 +127,7 @@ Before you submit your pull request consider the following guidelines: * Run the AngularJS [unit][developers.tests-unit] and [E2E test][developers.tests-e2e] suites, and ensure that all tests pass. It is generally sufficient to run the tests only on Chrome, as our Travis integration will run the tests on all supported browsers. -* Run `grunt eslint` to check that you have followed the automatically enforced coding rules +* Run `yarn grunt eslint` to check that you have followed the automatically enforced coding rules * Commit your changes using a descriptive commit message that follows our [commit message conventions][developers.commits]. Adherence to the [commit message conventions][developers.commits] is required, because release notes are @@ -141,7 +141,7 @@ Before you submit your pull request consider the following guidelines: * Before creating the Pull Request, package and run all tests a last time: ```shell - grunt test + yarn grunt test ``` * Push your branch to GitHub: diff --git a/DEVELOPERS.md b/DEVELOPERS.md index cf3f4d7d04e9..d70897f9ddb9 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -34,12 +34,9 @@ machine: to be installed and included in your [PATH](http://docs.oracle.com/javase/tutorial/essential/environment/paths.html) variable. -* [Grunt](http://gruntjs.com): We use Grunt as our build system. Install the grunt command-line tool - globally with: - - ```shell - yarn global add grunt-cli - ``` +* [Grunt](http://gruntjs.com): We use Grunt as our build system. We're using it as a local dependency, + but you can also add the grunt command-line tool globally (with `yarn global add grunt-cli`), which allows + you to leave out the `yarn` prefix for all our grunt commands. ### Forking AngularJS on Github @@ -70,20 +67,11 @@ git remote add upstream "https://github.com/angular/angular.js.git" yarn install # Build AngularJS (which will install `bower` dependencies automatically): -grunt package +yarn grunt package ``` **Note:** If you're using Windows, you must use an elevated command prompt (right click, run as -Administrator). This is because `grunt package` creates some symbolic links. - -**Note:** If you're using Linux, and `yarn install` fails with the message -'Please try running this command again as root/Administrator.', you may need to globally install -`grunt` and `bower`: - -```shell -sudo yarn global add grunt-cli -sudo yarn global add bower -``` +Administrator). This is because `yarn grunt package` creates some symbolic links. The build output is in the `build` directory. It consists of the following files and directories: @@ -105,7 +93,7 @@ HTTP server. For this purpose, we have made available a local web server based o 1. To start the web server, run: ```shell - grunt webserver + yarn grunt webserver ``` 2. To access the local server, enter the following URL into your web browser: @@ -125,13 +113,13 @@ We write unit and integration tests with Jasmine and execute them with Karma. To tests once on Chrome run: ```shell -grunt test:unit +yarn grunt test:unit ``` To run the tests on other browsers (Chrome, ChromeCanary, Firefox and Safari are pre-configured) use: ```shell -grunt test:unit --browsers=Chrome,Firefox +yarn grunt test:unit --browsers=Chrome,Firefox ``` **Note:** there should be _no spaces between browsers_. `Chrome, Firefox` is INVALID. @@ -143,9 +131,9 @@ For example, to run the whole unit test suite: ```shell # Browserstack -grunt test:unit --browsers=BS_Chrome,BS_Firefox,BS_Safari,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9,BS_iOS_10 +yarn grunt test:unit --browsers=BS_Chrome,BS_Firefox,BS_Safari,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9,BS_iOS_10 # Saucelabs -grunt test:unit --browsers=BS_Chrome,BS_Firefox,BS_Safari,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9,BS_iOS_10 +yarn grunt test:unit --browsers=BS_Chrome,BS_Firefox,BS_Safari,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9,BS_iOS_10 ``` Running these commands requires you to set up [Karma Browserstack][karma-browserstack] or @@ -157,7 +145,7 @@ source or test files change. To execute tests in this mode run: 1. To start the Karma server, capture Chrome browser and run unit tests, run: ```shell - grunt autotest + yarn grunt autotest ``` 2. To capture more browsers, open this URL in the desired browser (URL might be different if you @@ -173,7 +161,7 @@ source or test files change. To execute tests in this mode run: To learn more about all of the preconfigured Grunt tasks run: ```shell -grunt --help +yarn grunt --help ``` @@ -182,7 +170,7 @@ grunt --help AngularJS's end to end tests are run with Protractor. Simply run: ```shell -grunt test:e2e +yarn grunt test:e2e ``` This will start the webserver and run the tests on Chrome. @@ -329,7 +317,7 @@ documentation generation tool [Dgeni][dgeni]. The docs can be built from scratch using grunt: ```shell -grunt docs +yarn grunt docs ``` This defers the doc-building task to `gulp`. @@ -338,7 +326,7 @@ Note that the docs app is using the local build files to run. This means you mig the build: ```shell -grunt build +yarn grunt build ``` (This is also necessary if you are making changes to minErrors). From 873e26347fd5bb3d978d16d272db30cc5e49241a Mon Sep 17 00:00:00 2001 From: kirk Date: Fri, 17 Nov 2017 05:47:18 -0500 Subject: [PATCH 018/469] docs(linky): mark "target" param as optional This argument is optional in practice, and it is not provided in many of the examples in the documentation. Its optional presence is handled here: https://github.com/angular/angular.js/blob/f876ab71913e17e9126baad19ab795f28b61bfe6/src/ngSanitize/filter/linky.js#L185 Closes #16330 --- src/ngSanitize/filter/linky.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ngSanitize/filter/linky.js b/src/ngSanitize/filter/linky.js index 34881c847729..564799d59e4b 100644 --- a/src/ngSanitize/filter/linky.js +++ b/src/ngSanitize/filter/linky.js @@ -12,7 +12,7 @@ * Requires the {@link ngSanitize `ngSanitize`} module to be installed. * * @param {string} text Input text. - * @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in. + * @param {string} [target] Window (`_blank|_self|_parent|_top`) or named frame to open links in. * @param {object|function(url)} [attributes] Add custom attributes to the link element. * * Can be one of: From 12cf994fccd7df6c2c2ba07d50b921ee80d62be7 Mon Sep 17 00:00:00 2001 From: Denys B Date: Fri, 17 Nov 2017 12:55:18 +0200 Subject: [PATCH 019/469] fix($compile): sanitize special chars in directive name This fixes regression bug when directive name with preceeding special char in HTML markup does not match the registered name. (introduced in https://github.com/angular/angular.js/commit/73050cdda04675bfa6705dc841ddbbb6919eb048) Closes #16314 Closes #16278 --- src/ng/compile.js | 4 +++- test/ng/compileSpec.js | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 561bb13fef16..4ec3ea5d6d94 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -3647,7 +3647,9 @@ var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g; function directiveNormalize(name) { return name .replace(PREFIX_REGEXP, '') - .replace(SPECIAL_CHARS_REGEXP, fnCamelCaseReplace); + .replace(SPECIAL_CHARS_REGEXP, function(_, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }); } /** diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 2890a76ac0c0..ca2d43380e8f 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -294,6 +294,27 @@ describe('$compile', function() { inject(function($compile) {}); }); + it('should ignore special chars before processing attribute directive name', function() { + // a regression https://github.com/angular/angular.js/issues/16278 + module(function() { + directive('t', function(log) { + return { + restrict: 'A', + link: { + pre: log.fn('pre'), + post: log.fn('post') + } + }; + }); + }); + inject(function($compile, $rootScope, log) { + $compile('
    ')($rootScope); + $compile('
    ')($rootScope); + $compile('
    ')($rootScope); + expect(log).toEqual('pre; post; pre; post; pre; post'); + }); + }); + it('should throw an exception if the directive factory is not defined', function() { module(function() { expect(function() { From aa3f951330ec7b10b43ea884d9b5754e296770ec Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 17 Nov 2017 12:28:03 +0100 Subject: [PATCH 020/469] fix(input[number]): validate min/max against viewValue This brings the validation in line with HTML5 validation, i.e. what the user has entered is validated, and not a possibly transformed value. Fixes #12761 Closes #16325 BREAKING CHANGE `input[type=number]` with `ngModel` now validates the input for the `max`/`min` restriction against the `ngModelController.$viewValue` instead of against the `ngModelController.$modelValue`. This affects apps that use `$parsers` or `$formatters` to transform the input / model value. If you rely on the $modelValue validation, you can overwrite the `min`/`max` validator from a custom directive, as seen in the following example directive definition object: ``` { restrict: 'A', require: 'ngModel', link: function(scope, element, attrs, ctrl) { var maxValidator = ctrl.$validators.max; ctrk.$validators.max = function(modelValue, viewValue) { return maxValidator(modelValue, modelValue); }; } } ``` --- src/ng/directive/input.js | 8 +-- test/ng/directive/inputSpec.js | 98 ++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index f87e755b8c8d..ebae9b3bed18 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1604,8 +1604,8 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { var maxVal; if (isDefined(attr.min) || attr.ngMin) { - ctrl.$validators.min = function(value) { - return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; + ctrl.$validators.min = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal; }; attr.$observe('min', function(val) { @@ -1616,8 +1616,8 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { } if (isDefined(attr.max) || attr.ngMax) { - ctrl.$validators.max = function(value) { - return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; + ctrl.$validators.max = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal; }; attr.$observe('max', function(val) { diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 260e11d1d4b7..d1a552194c4e 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -2284,6 +2284,15 @@ describe('input', function() { describe('number', function() { + // Helpers for min / max tests + var subtract = function(value) { + return value - 5; + }; + + var add = function(value) { + return value + 5; + }; + it('should reset the model if view is invalid', function() { var inputElm = helper.compileInput(''); @@ -2465,6 +2474,29 @@ describe('input', function() { expect($rootScope.form.alias.$error.min).toBeFalsy(); }); + + it('should validate against the viewValue', function() { + var inputElm = helper.compileInput( + ''); + + var ngModelCtrl = inputElm.controller('ngModel'); + ngModelCtrl.$parsers.push(subtract); + + helper.changeInputValueTo('10'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(5); + expect($rootScope.form.alias.$error.min).toBeFalsy(); + + ngModelCtrl.$parsers.pop(); + ngModelCtrl.$parsers.push(add); + + helper.changeInputValueTo('5'); + expect(inputElm).toBeInvalid(); + expect($rootScope.form.alias.$error.min).toBeTruthy(); + expect($rootScope.value).toBe(10); + }); + + it('should validate even if min value changes on-the-fly', function() { $rootScope.min = undefined; var inputElm = helper.compileInput(''); @@ -2511,6 +2543,28 @@ describe('input', function() { expect($rootScope.form.alias.$error.min).toBeFalsy(); }); + + it('should validate against the viewValue', function() { + var inputElm = helper.compileInput( + ''); + var ngModelCtrl = inputElm.controller('ngModel'); + ngModelCtrl.$parsers.push(subtract); + + helper.changeInputValueTo('10'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(5); + expect($rootScope.form.alias.$error.min).toBeFalsy(); + + ngModelCtrl.$parsers.pop(); + ngModelCtrl.$parsers.push(add); + + helper.changeInputValueTo('5'); + expect(inputElm).toBeInvalid(); + expect($rootScope.form.alias.$error.min).toBeTruthy(); + expect($rootScope.value).toBe(10); + }); + + it('should validate even if the ngMin value changes on-the-fly', function() { $rootScope.min = undefined; var inputElm = helper.compileInput(''); @@ -2558,6 +2612,28 @@ describe('input', function() { expect($rootScope.form.alias.$error.max).toBeFalsy(); }); + + it('should validate against the viewValue', function() { + var inputElm = helper.compileInput(''); + var ngModelCtrl = inputElm.controller('ngModel'); + ngModelCtrl.$parsers.push(add); + + helper.changeInputValueTo('10'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(15); + expect($rootScope.form.alias.$error.max).toBeFalsy(); + + ngModelCtrl.$parsers.pop(); + ngModelCtrl.$parsers.push(subtract); + + helper.changeInputValueTo('15'); + expect(inputElm).toBeInvalid(); + expect($rootScope.form.alias.$error.max).toBeTruthy(); + expect($rootScope.value).toBe(10); + }); + + it('should validate even if max value changes on-the-fly', function() { $rootScope.max = undefined; var inputElm = helper.compileInput(''); @@ -2604,6 +2680,28 @@ describe('input', function() { expect($rootScope.form.alias.$error.max).toBeFalsy(); }); + + it('should validate against the viewValue', function() { + var inputElm = helper.compileInput(''); + var ngModelCtrl = inputElm.controller('ngModel'); + ngModelCtrl.$parsers.push(add); + + helper.changeInputValueTo('10'); + expect(inputElm).toBeValid(); + expect($rootScope.value).toBe(15); + expect($rootScope.form.alias.$error.max).toBeFalsy(); + + ngModelCtrl.$parsers.pop(); + ngModelCtrl.$parsers.push(subtract); + + helper.changeInputValueTo('15'); + expect(inputElm).toBeInvalid(); + expect($rootScope.form.alias.$error.max).toBeTruthy(); + expect($rootScope.value).toBe(10); + }); + + it('should validate even if the ngMax value changes on-the-fly', function() { $rootScope.max = undefined; var inputElm = helper.compileInput(''); From 55ba44913e02650b56410aa9ab5eeea5d3492b68 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 21 Nov 2017 13:50:25 +0100 Subject: [PATCH 021/469] feat(ngModelOptions): add debounce catch-all + allow debouncing 'default' only Closes #15411 Closes #16335 BREAKING CHANGE: the 'default' key in 'debounce' now only debounces the default event, i.e. the event that is added as an update trigger by the different input directives automatically. Previously, it also applied to other update triggers defined in 'updateOn' that did not have a corresponding key in the 'debounce'. This behavior is now supported via a special wildcard / catch-all key: '*'. See the following example: Pre-1.7: 'mouseup' is also debounced by 500 milliseconds because 'default' is applied: ``` ng-model-options="{ updateOn: 'default blur mouseup', debounce: { 'default': 500, 'blur': 0 } } ``` 1.7: The pre-1.7 behavior can be re-created by setting '*' as a catch-all debounce value: ``` ng-model-options="{ updateOn: 'default blur mouseup', debounce: { '*': 500, 'blur': 0 } } ``` In contrast, when only 'default' is used, 'blur' and 'mouseup' are not debounced: ``` ng-model-options="{ updateOn: 'default blur mouseup', debounce: { 'default': 500 } } ``` --- src/ng/directive/ngModel.js | 6 ++++- src/ng/directive/ngModelOptions.js | 8 ++++++ test/ng/directive/ngModelOptionsSpec.js | 36 +++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js index 5e9aa4a38628..8afa3da7f64a 100644 --- a/src/ng/directive/ngModel.js +++ b/src/ng/directive/ngModel.js @@ -838,8 +838,12 @@ NgModelController.prototype = { if (isNumber(debounceDelay[trigger])) { debounceDelay = debounceDelay[trigger]; - } else if (isNumber(debounceDelay['default'])) { + } else if (isNumber(debounceDelay['default']) && + this.$options.getOption('updateOn').indexOf(trigger) === -1 + ) { debounceDelay = debounceDelay['default']; + } else if (isNumber(debounceDelay['*'])) { + debounceDelay = debounceDelay['*']; } this.$$timeout.cancel(this.$$pendingDebounce); diff --git a/src/ng/directive/ngModelOptions.js b/src/ng/directive/ngModelOptions.js index 47c51e8460a5..2defcee0d128 100644 --- a/src/ng/directive/ngModelOptions.js +++ b/src/ng/directive/ngModelOptions.js @@ -321,6 +321,14 @@ defaultModelOptions = new ModelOptions({ * debounce: { 'default': 500, 'blur': 0 } * }" * ``` + * You can use the `*` key to specify a debounce value that applies to all events that are not + * specifically listed. In the following example, `mouseup` would have a debounce delay of 1000: + * ``` + * ng-model-options="{ + * updateOn: 'default blur mouseup', + * debounce: { 'default': 500, 'blur': 0, '*': 1000 } + * }" + * ``` * - `allowInvalid`: boolean value which indicates that the model can be set with values that did * not validate correctly instead of the default behavior of setting the model to undefined. * - `getterSetter`: boolean value which determines whether or not to treat functions bound to diff --git a/test/ng/directive/ngModelOptionsSpec.js b/test/ng/directive/ngModelOptionsSpec.js index f4142ae217ee..3814ba2bc5ca 100644 --- a/test/ng/directive/ngModelOptionsSpec.js +++ b/test/ng/directive/ngModelOptionsSpec.js @@ -498,9 +498,41 @@ describe('ngModelOptions', function() { helper.changeInputValueTo('c'); browserTrigger(helper.inputElm, 'mouseup'); - // counter-intuitively `default` in `debounce` is a catch-all + // `default` in `debounce` only affects the event triggers that are not defined in updateOn + expect($rootScope.name).toEqual('c'); + }); + + + it('should use the value of * to debounce all unspecified events', + function() { + var inputElm = helper.compileInput( + ''); + + helper.changeInputValueTo('a'); + expect($rootScope.name).toBeUndefined(); + $timeout.flush(6000); + expect($rootScope.name).toBeUndefined(); + $timeout.flush(4000); + expect($rootScope.name).toEqual('a'); + + helper.changeInputValueTo('b'); + browserTrigger(inputElm, 'blur'); + $timeout.flush(4000); + expect($rootScope.name).toEqual('a'); + $timeout.flush(2000); expect($rootScope.name).toEqual('b'); - $timeout.flush(10000); + + helper.changeInputValueTo('c'); + browserTrigger(helper.inputElm, 'mouseup'); + expect($rootScope.name).toEqual('b'); + $timeout.flush(10000); // flush default + expect($rootScope.name).toEqual('b'); + $timeout.flush(5000); expect($rootScope.name).toEqual('c'); }); From f2f5ac7ce42fe9a83306aac8114f0a604ec230ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82=C4=99biowski-Owczarek?= Date: Wed, 22 Nov 2017 13:24:56 +0100 Subject: [PATCH 022/469] chore(*): normalize Vojta's email in .mailmap correctly Closes #16340 --- .mailmap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mailmap b/.mailmap index 220aaa5823d0..f1a2dc0b18e0 100644 --- a/.mailmap +++ b/.mailmap @@ -25,5 +25,5 @@ Shahar Talmi Shyam Seshadri Shyam Seshadri Vojta Jina -Vojta Jina -Vojta Jina +Vojta Jina +Vojta Jina From 5838017f267172fdb97324415a783bec2cfd05ec Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Fri, 24 Nov 2017 00:25:33 -0800 Subject: [PATCH 023/469] refactor($rootScope): simplify $emit stopPropagation handling See https://github.com/angular/angular.js/pull/16293#discussion_r147960028 Closes #16339 --- src/ng/rootScope.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 616cc69676d0..7d29b97a3376 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -1235,8 +1235,7 @@ function $RootScopeProvider() { //if any listener on the current scope stops propagation, prevent bubbling if (stopPropagation) { - event.currentScope = null; - return event; + break; } //traverse upwards scope = scope.$parent; From 07e475137ec39604bca4a8be07a1a605a4fd6e7d Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 23 Nov 2017 15:57:26 +0100 Subject: [PATCH 024/469] docs(CHANGELOG.md): add changes for 1.6.7 --- CHANGELOG.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3811e52cdfcd..85809d7573ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,58 @@ + +# 1.6.7 imperial-backstroke (2017-11-24) + + +## Bug Fixes +- **$compile:** sanitize special chars in directive name + ([c4003f](https://github.com/angular/angular.js/commit/c4003fd03489f876b646f06838f4edb576bacf6f), + [#16314](https://github.com/angular/angular.js/issues/16314), + [#16278](https://github.com/angular/angular.js/issues/16278)) +- **$location:** do not decode forward slashes in the path in HTML5 mode + ([e06ebf](https://github.com/angular/angular.js/commit/e06ebfdbb558544602fe9da4d7d98045a965f468), + [#16312](https://github.com/angular/angular.js/issues/16312)) +- **sanitizeUri:** sanitize URIs that contain IDEOGRAPHIC SPACE chars + ([ddeb1d](https://github.com/angular/angular.js/commit/ddeb1df15a23de93eb95dbe202e83e93673e1c4e), + [#16288](https://github.com/angular/angular.js/issues/16288)) +- **$rootScope:** fix potential memory leak when removing scope listeners + ([358a69](https://github.com/angular/angular.js/commit/358a69fa8b89b251ee44e523458d6c7f40b92b2d), + [#16135](https://github.com/angular/angular.js/issues/16135), + [#16161](https://github.com/angular/angular.js/issues/16161)) +- **http:** do not allow encoded callback params in jsonp requests + ([569e90](https://github.com/angular/angular.js/commit/569e906a5818271416ad0b749be2f58dc34938bd)) +- **ngMock:** pass unexpected request failures in `$httpBackend` to the error handler + ([1555a4](https://github.com/angular/angular.js/commit/1555a4911ad5360c145c0ddc8ec6c4bf9a381c13), + [#16150](https://github.com/angular/angular.js/issues/16150), + [#15855](https://github.com/angular/angular.js/issues/15855)) +- **ngAnimate:** don't close transitions when child transitions close + ([1391e9](https://github.com/angular/angular.js/commit/1391e99c7f73795180b792af21ad4402f96e225d), + [#16210](https://github.com/angular/angular.js/issues/16210)) +- **ngMock.browserTrigger:** add 'bubbles' to Transition/Animation Event + ([7a5f06](https://github.com/angular/angular.js/commit/7a5f06d55d123a39bb7b030667fb1ab672939598)) + + +## New Features +- **$sanitize, $compileProvider, linky:** add support for the "sftp" protocol in links + ([a675ea](https://github.com/angular/angular.js/commit/a675ea034366fbb0fcf0d73fed65216aa99bce11), + [#16102](https://github.com/angular/angular.js/issues/16102)) +- **ngModel.NgModelController:** expose $processModelValue to run model -> view pipeline + ([145194](https://github.com/angular/angular.js/commit/14519488ce9218aa891d34e89fc3271fd4ed0f04), + [#3407](https://github.com/angular/angular.js/issues/3407), + [#10764](https://github.com/angular/angular.js/issues/10764), + [#16237](https://github.com/angular/angular.js/issues/16237)) +- **$injector:** ability to load new modules after bootstrapping + ([6e78fe](https://github.com/angular/angular.js/commit/6e78fee73258bb0ae36414f9db2e8734273e481b)) + + +## Performance Improvements +- **jqLite:** + - avoid setting class attribute when not changed + ([9c95f6](https://github.com/angular/angular.js/commit/9c95f6d5e00ee7e054aabb3e363f5bfb3b7b4103)) + - avoid repeated add/removeAttribute in jqLiteRemoveClass + ([cab9eb](https://github.com/angular/angular.js/commit/cab9ebfd5a02e897f802bf6321b8471e4843c5d3), + [#16078](https://github.com/angular/angular.js/issues/16078), + [#16131](https://github.com/angular/angular.js/issues/16131)) + + # 1.6.6 interdimensional-cable (2017-08-18) From 0fa5a37838a895ca01e0c63b3bb4141bc97a0238 Mon Sep 17 00:00:00 2001 From: jugglinmike Date: Mon, 27 Nov 2017 07:11:50 -0500 Subject: [PATCH 025/469] docs(ngNonBindable): document effect on the element's directives The phrase "contents of the current DOM element" may be interpreted either as inclusive of the DOM element's attributes or as exclusive of the attributes. This situation concerns markup such as:
    In practice, AngularJS does not compile or bind attribute values for elements which specify the `ng-non-bindable` directive. Extend the documentation to definitely describe this behavior. Closes #16338 --- src/ng/directive/ngNonBindable.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ng/directive/ngNonBindable.js b/src/ng/directive/ngNonBindable.js index cab3db9bd648..fed7683af711 100644 --- a/src/ng/directive/ngNonBindable.js +++ b/src/ng/directive/ngNonBindable.js @@ -9,9 +9,10 @@ * * @description * The `ngNonBindable` directive tells AngularJS not to compile or bind the contents of the current - * DOM element. This is useful if the element contains what appears to be AngularJS directives and - * bindings but which should be ignored by AngularJS. This could be the case if you have a site that - * displays snippets of code, for instance. + * DOM element, including directives on the element itself that have a lower priority than + * `ngNonBindable`. This is useful if the element contains what appears to be AngularJS directives + * and bindings but which should be ignored by AngularJS. This could be the case if you have a site + * that displays snippets of code, for instance. * * @example * In this example there are two locations where a simple interpolation binding (`{{}}`) is present, From 8b69d91fffff840fc9eeb16a06707e7bf17f75e4 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 29 Nov 2017 15:45:59 +0100 Subject: [PATCH 026/469] chore(travis): fix deployment condition to include tagged commits Tagged commits are not considered to belong to any branch. Closes #16346 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d86faa8ea81e..6d59cf07affd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,10 +53,10 @@ notifications: jobs: include: - stage: deploy - # Don't deploy from PRs and only from our default branches. + # Don't deploy from PRs. Only deploy from our default branches, or if commit is tagged. # This is a Travis-specific boolean language: https://docs.travis-ci.com/user/conditional-builds-stages-jobs#Specifying-conditions # The deployment logic for pushed branches is further defined in scripts\travis\build.sh - if: type != pull_request and branch =~ ^(v1\.\d+\.x|master)$ + if: type != pull_request and (branch =~ ^(v1\.\d+\.x|master)$ or tag IS present) env: - JOB=deploy before_script: skip From 199d888b8427e222700ee52519e0d1d4a048241b Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Tue, 28 Nov 2017 14:21:25 +0000 Subject: [PATCH 027/469] fix($location): decode non-component special chars in Hashbang URLS Fixes https://github.com/angular/angular.js/pull/16316#issuecomment-347527097 --- src/ng/location.js | 2 +- test/ng/locationSpec.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/ng/location.js b/src/ng/location.js index 184428883090..625b9297720d 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -422,7 +422,7 @@ var locationPrototype = { } var match = PATH_MATCH.exec(url); - if (match[1] || url === '') this.path(decodeURI(match[1])); + if (match[1] || url === '') this.path((this.$$html5 ? decodeURI : decodeURIComponent)(match[1])); if (match[2] || match[1] || url === '') this.search(match[3] || ''); this.hash(match[5] || ''); diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index dd48edbe4e52..af17a1e3db76 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -673,6 +673,20 @@ describe('$location', function() { locationUrl.search({'q': '4/5 6'}); expect(locationUrl.absUrl()).toEqual('http://host.com/?q=4%2F5%206'); }); + + it('url() should decode non-component special characters in hashbang mode', function() { + var locationUrl = new LocationHashbangUrl('http://host.com/', 'http://host.com/'); + locationUrl.$$parse('http://host.com/'); + locationUrl.url('http://github.com/foo%3Abar'); + expect(locationUrl.path()).toEqual('/foo:bar'); + }); + + it('url() should not decode non-component special characters in html5 mode', function() { + var locationUrl = new LocationHtml5Url('http://host.com/', 'http://host.com/'); + locationUrl.$$parse('http://host.com/'); + locationUrl.url('http://github.com/foo%3Abar'); + expect(locationUrl.path()).toEqual('/foo%3Abar'); + }); }); }); From 2e03aedc8520bb863e465b0f35030a02ebe2cade Mon Sep 17 00:00:00 2001 From: Francesco Pipita Date: Sat, 7 Oct 2017 18:27:20 +0200 Subject: [PATCH 028/469] feat($parse): add a hidden interface to retrieve an expression's AST This PR adds a new private method to the `$parse` service, `$$getAst`, which takes an Angular expression as its only argument and returns the computed AST. This feature is not meant to be part of the public API and might be subject to changes, so use it with caution. Closes #16253 Closes #16260 --- src/ng/parse.js | 37 +++++++++++++++++++++++++++---------- test/ng/parseSpec.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 9f8621440cf3..91a1b363e6c4 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1644,11 +1644,26 @@ Parser.prototype = { constructor: Parser, parse: function(text) { - var ast = this.ast.ast(text); - var fn = this.astCompiler.compile(ast); - fn.literal = isLiteral(ast); - fn.constant = isConstant(ast); + var ast = this.getAst(text); + var fn = this.astCompiler.compile(ast.ast); + fn.literal = isLiteral(ast.ast); + fn.constant = isConstant(ast.ast); + fn.oneTime = ast.oneTime; return fn; + }, + + getAst: function(exp) { + var oneTime = false; + exp = exp.trim(); + + if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { + oneTime = true; + exp = exp.substring(2); + } + return { + ast: this.ast.ast(exp), + oneTime: oneTime + }; } }; @@ -1771,10 +1786,11 @@ function $ParseProvider() { isIdentifierStart: isFunction(identStart) && identStart, isIdentifierContinue: isFunction(identContinue) && identContinue }; + $parse.$$getAst = $$getAst; return $parse; function $parse(exp, interceptorFn) { - var parsedExpression, oneTime, cacheKey; + var parsedExpression, cacheKey; switch (typeof exp) { case 'string': @@ -1784,14 +1800,9 @@ function $ParseProvider() { parsedExpression = cache[cacheKey]; if (!parsedExpression) { - if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { - oneTime = true; - exp = exp.substring(2); - } var lexer = new Lexer($parseOptions); var parser = new Parser(lexer, $filter, $parseOptions); parsedExpression = parser.parse(exp); - parsedExpression.oneTime = !!oneTime; cache[cacheKey] = addWatchDelegate(parsedExpression); } @@ -1805,6 +1816,12 @@ function $ParseProvider() { } } + function $$getAst(exp) { + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); + return parser.getAst(exp).ast; + } + function expressionInputDirtyCheck(newValue, oldValueOfValue, compareObjectIdentity) { if (newValue == null || oldValueOfValue == null) { // null/undefined diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 7c5baad1630b..5142bc22a7ed 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -4245,4 +4245,48 @@ describe('parser', function() { }); }); }); + + describe('hidden/unsupported features', function() { + describe('$$getAst()', function() { + it('should be a method exposed on the `$parse` service', inject(function($parse) { + expect(isFunction($parse.$$getAst)).toBeTruthy(); + })); + + it('should accept a string expression argument and return the corresponding AST', inject(function($parse) { + var ast = $parse.$$getAst('foo.bar'); + expect(ast).toEqual({ + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'foo' }, + property: { type: 'Identifier', name: 'bar' }, + computed: false + } + } + ] + }); + })); + + it('should parse one time binding expressions', inject(function($parse) { + var ast = $parse.$$getAst('::foo.bar'); + expect(ast).toEqual({ + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'foo' }, + property: { type: 'Identifier', name: 'bar' }, + computed: false + } + } + ] + }); + })); + }); + }); }); From 74b04c9403af4fc7df5b6420f22c9f45a3e84140 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 4 Dec 2017 11:17:32 +0100 Subject: [PATCH 029/469] fix(ngModel, input): improve handling of built-in named parsers This commit changes how input elements use the private $$parserName property on the ngModelController to name parse errors. Until now, the input types (number, date etc.) would set $$parserName when the inputs were initialized, which meant that any other parsers on the ngModelController would also be named after that type. The effect of that was that the `$error` property and the `ng-invalid-...` class would always be that of the built-in parser, even if the custom parser had nothing to do with it. The new behavior is that the $$parserName is only set if the built-in parser is invalid i.e. returns `undefined`. Also, $$parserName has been removed from input[email] and input[url], as these types do not have a built-in parser anymore. Closes #14292 Closes #10076 Closes #16347 BREAKING CHANGE: *Custom* parsers that fail to parse on input types "email", "url", "number", "date", "month", "time", "datetime-local", "week", do no longer set `ngModelController.$error[inputType]`, and the `ng-invalid-[inputType]` class. Also, custom parsers on input type "range" do no longer set `ngModelController.$error.number` and the `ng-invalid-number` class. Instead, any custom parsers on these inputs set `ngModelController.$error.parse` and `ng-invalid-parse`. This change was made to make distinguishing errors from built-in parsers and custom parsers easier. --- src/ng/directive/input.js | 22 ++++--- src/ng/directive/ngModel.js | 9 ++- test/ng/directive/inputSpec.js | 98 ++++++++++++++++++++++++++++++++ test/ng/directive/ngModelSpec.js | 2 +- 4 files changed, 120 insertions(+), 11 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index ebae9b3bed18..228f5fb2366a 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1429,12 +1429,11 @@ function createDateParser(regexp, mapping) { function createDateInputType(type, regexp, parseDate, format) { return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { - badInputChecker(scope, element, attr, ctrl); + badInputChecker(scope, element, attr, ctrl, type); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); var timezone = ctrl && ctrl.$options.getOption('timezone'); var previousDate; - ctrl.$$parserName = type; ctrl.$parsers.push(function(value) { if (ctrl.$isEmpty(value)) return null; if (regexp.test(value)) { @@ -1447,6 +1446,7 @@ function createDateInputType(type, regexp, parseDate, format) { } return parsedDate; } + ctrl.$$parserName = type; return undefined; }); @@ -1499,22 +1499,28 @@ function createDateInputType(type, regexp, parseDate, format) { }; } -function badInputChecker(scope, element, attr, ctrl) { +function badInputChecker(scope, element, attr, ctrl, parserName) { var node = element[0]; var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity); if (nativeValidation) { ctrl.$parsers.push(function(value) { var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; - return validity.badInput || validity.typeMismatch ? undefined : value; + if (validity.badInput || validity.typeMismatch) { + ctrl.$$parserName = parserName; + return undefined; + } + + return value; }); } } function numberFormatterParser(ctrl) { - ctrl.$$parserName = 'number'; ctrl.$parsers.push(function(value) { if (ctrl.$isEmpty(value)) return null; if (NUMBER_REGEXP.test(value)) return parseFloat(value); + + ctrl.$$parserName = 'number'; return undefined; }); @@ -1596,7 +1602,7 @@ function isValidForStep(viewValue, stepBase, step) { } function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - badInputChecker(scope, element, attr, ctrl); + badInputChecker(scope, element, attr, ctrl, 'number'); numberFormatterParser(ctrl); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); @@ -1643,7 +1649,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { } function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) { - badInputChecker(scope, element, attr, ctrl); + badInputChecker(scope, element, attr, ctrl, 'range'); numberFormatterParser(ctrl); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); @@ -1782,7 +1788,6 @@ function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { baseInputType(scope, element, attr, ctrl, $sniffer, $browser); stringBasedInputType(ctrl); - ctrl.$$parserName = 'url'; ctrl.$validators.url = function(modelValue, viewValue) { var value = modelValue || viewValue; return ctrl.$isEmpty(value) || URL_REGEXP.test(value); @@ -1795,7 +1800,6 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { baseInputType(scope, element, attr, ctrl, $sniffer, $browser); stringBasedInputType(ctrl); - ctrl.$$parserName = 'email'; ctrl.$validators.email = function(modelValue, viewValue) { var value = modelValue || viewValue; return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js index 8afa3da7f64a..1dec8d31ed33 100644 --- a/src/ng/directive/ngModel.js +++ b/src/ng/directive/ngModel.js @@ -277,6 +277,7 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $ this.$$ngModelSet = this.$$parsedNgModelAssign; this.$$pendingDebounce = null; this.$$parserValid = undefined; + this.$$parserName = 'parse'; this.$$currentValidationRunId = 0; @@ -607,7 +608,8 @@ NgModelController.prototype = { processAsyncValidators(); function processParseErrors() { - var errorKey = that.$$parserName || 'parse'; + var errorKey = that.$$parserName; + if (isUndefined(that.$$parserValid)) { setValidity(errorKey, null); } else { @@ -619,6 +621,7 @@ NgModelController.prototype = { setValidity(name, null); }); } + // Set the parse error last, to prevent unsetting it, should a $validators key == parserName setValidity(errorKey, that.$$parserValid); return that.$$parserValid; @@ -721,6 +724,10 @@ NgModelController.prototype = { this.$$parserValid = isUndefined(modelValue) ? undefined : true; + // Reset any previous parse error + this.$setValidity(this.$$parserName, null); + this.$$parserName = 'parse'; + if (this.$$parserValid) { for (var i = 0; i < this.$parsers.length; i++) { modelValue = this.$parsers[i](modelValue); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index d1a552194c4e..afc869074a78 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -610,6 +610,37 @@ describe('input', function() { helper.changeInputValueTo('stuff'); expect(inputElm.val()).toBe('stuff'); expect($rootScope.value).toBeUndefined(); + expect(inputElm).toHaveClass('ng-invalid-month'); + expect(inputElm).toBeInvalid(); + }); + + + it('should not set error=month when a later parser returns undefined', function() { + var inputElm = helper.compileInput(''); + var ctrl = inputElm.controller('ngModel'); + + ctrl.$parsers.push(function() { + return undefined; + }); + + inputElm[0].setAttribute('type', 'text'); + + helper.changeInputValueTo('2017-01'); + + expect($rootScope.value).toBeUndefined(); + expect(ctrl.$error.month).toBeFalsy(); + expect(ctrl.$error.parse).toBeTruthy(); + expect(inputElm).not.toHaveClass('ng-invalid-month'); + expect(inputElm).toHaveClass('ng-invalid-parse'); + expect(inputElm).toBeInvalid(); + + helper.changeInputValueTo('asdf'); + + expect($rootScope.value).toBeUndefined(); + expect(ctrl.$error.month).toBeTruthy(); + expect(ctrl.$error.parse).toBeFalsy(); + expect(inputElm).toHaveClass('ng-invalid-month'); + expect(inputElm).not.toHaveClass('ng-invalid-parse'); expect(inputElm).toBeInvalid(); }); @@ -2457,6 +2488,73 @@ describe('input', function() { expect($rootScope.value).toBe(123214124123412412e-26); }); + it('should not set $error number if any other parser fails', function() { + var inputElm = helper.compileInput(''); + var ctrl = inputElm.controller('ngModel'); + + var previousParserFail = false; + var laterParserFail = false; + + ctrl.$parsers.unshift(function(value) { + return previousParserFail ? undefined : value; + }); + + ctrl.$parsers.push(function(value) { + return laterParserFail ? undefined : value; + }); + + // to allow non-number values, we have to change type so that + // the browser which have number validation will not interfere with + // this test. + inputElm[0].setAttribute('type', 'text'); + + helper.changeInputValueTo('123X'); + expect(inputElm.val()).toBe('123X'); + + expect($rootScope.age).toBeUndefined(); + expect(inputElm).toBeInvalid(); + expect(ctrl.$error.number).toBe(true); + expect(ctrl.$error.parse).toBeFalsy(); + expect(inputElm).toHaveClass('ng-invalid-number'); + expect(inputElm).not.toHaveClass('ng-invalid-parse'); + + previousParserFail = true; + helper.changeInputValueTo('123'); + expect(inputElm.val()).toBe('123'); + + expect($rootScope.age).toBeUndefined(); + expect(inputElm).toBeInvalid(); + expect(ctrl.$error.number).toBeFalsy(); + expect(ctrl.$error.parse).toBe(true); + expect(inputElm).not.toHaveClass('ng-invalid-number'); + expect(inputElm).toHaveClass('ng-invalid-parse'); + + previousParserFail = false; + laterParserFail = true; + + helper.changeInputValueTo('1234'); + expect(inputElm.val()).toBe('1234'); + + expect($rootScope.age).toBeUndefined(); + expect(inputElm).toBeInvalid(); + expect(ctrl.$error.number).toBeFalsy(); + expect(ctrl.$error.parse).toBe(true); + expect(inputElm).not.toHaveClass('ng-invalid-number'); + expect(inputElm).toHaveClass('ng-invalid-parse'); + + laterParserFail = false; + + helper.changeInputValueTo('12345'); + expect(inputElm.val()).toBe('12345'); + + expect($rootScope.age).toBe(12345); + expect(inputElm).toBeValid(); + expect(ctrl.$error.number).toBeFalsy(); + expect(ctrl.$error.parse).toBeFalsy(); + expect(inputElm).not.toHaveClass('ng-invalid-number'); + expect(inputElm).not.toHaveClass('ng-invalid-parse'); + }); + describe('min', function() { diff --git a/test/ng/directive/ngModelSpec.js b/test/ng/directive/ngModelSpec.js index 7e839e962865..bc14959ce890 100644 --- a/test/ng/directive/ngModelSpec.js +++ b/test/ng/directive/ngModelSpec.js @@ -1378,13 +1378,13 @@ describe('ngModel', function() { } }; - ctrl.$$parserName = 'parserOrValidator'; ctrl.$parsers.push(function(value) { switch (value) { case 'allInvalid': case 'stillAllInvalid': case 'parseInvalid-validatorsValid': case 'stillParseInvalid-validatorsValid': + ctrl.$$parserName = 'parserOrValidator'; return undefined; default: return value; From da724777019acbcbc831290b2bb62465a0635766 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 5 Dec 2017 20:07:45 +0100 Subject: [PATCH 030/469] fix(ngModelController): allow $overrideModelOptions to set updateOn Also adds more docs about "default" events and how to override ngModelController options. Closes #16351 Closes #16352 --- src/ng/directive/ngModel.js | 35 ++++++- src/ng/directive/ngModelOptions.js | 125 +++++++++++++++++++++++- test/ng/directive/ngModelOptionsSpec.js | 37 +++++++ 3 files changed, 191 insertions(+), 6 deletions(-) diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js index 1dec8d31ed33..7802d05177e6 100644 --- a/src/ng/directive/ngModel.js +++ b/src/ng/directive/ngModel.js @@ -270,6 +270,9 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $ this.$name = $interpolate($attr.name || '', false)($scope); this.$$parentForm = nullFormCtrl; this.$options = defaultModelOptions; + this.$$updateEvents = ''; + // Attach the correct context to the event handler function for updateOn + this.$$updateEventHandler = this.$$updateEventHandler.bind(this); this.$$parsedNgModel = $parse($attr.ngModel); this.$$parsedNgModelAssign = this.$$parsedNgModel.assign; @@ -884,11 +887,22 @@ NgModelController.prototype = { * See {@link ngModelOptions} for information about what options can be specified * and how model option inheritance works. * + *
    + * **Note:** this function only affects the options set on the `ngModelController`, + * and not the options on the {@link ngModelOptions} directive from which they might have been + * obtained initially. + *
    + * + *
    + * **Note:** it is not possible to override the `getterSetter` option. + *
    + * * @param {Object} options a hash of settings to override the previous options * */ $overrideModelOptions: function(options) { this.$options = this.$options.createChild(options); + this.$$setUpdateOnEvents(); }, /** @@ -1036,6 +1050,21 @@ NgModelController.prototype = { this.$modelValue = this.$$rawModelValue = modelValue; this.$$parserValid = undefined; this.$processModelValue(); + }, + + $$setUpdateOnEvents: function() { + if (this.$$updateEvents) { + this.$$element.off(this.$$updateEvents, this.$$updateEventHandler); + } + + this.$$updateEvents = this.$options.getOption('updateOn'); + if (this.$$updateEvents) { + this.$$element.on(this.$$updateEvents, this.$$updateEventHandler); + } + }, + + $$updateEventHandler: function(ev) { + this.$$debounceViewValueCommit(ev && ev.type); } }; @@ -1327,11 +1356,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) { }, post: function ngModelPostLink(scope, element, attr, ctrls) { var modelCtrl = ctrls[0]; - if (modelCtrl.$options.getOption('updateOn')) { - element.on(modelCtrl.$options.getOption('updateOn'), function(ev) { - modelCtrl.$$debounceViewValueCommit(ev && ev.type); - }); - } + modelCtrl.$$setUpdateOnEvents(); function setTouched() { modelCtrl.$setTouched(); diff --git a/src/ng/directive/ngModelOptions.js b/src/ng/directive/ngModelOptions.js index 2defcee0d128..03c7e5945b5c 100644 --- a/src/ng/directive/ngModelOptions.js +++ b/src/ng/directive/ngModelOptions.js @@ -177,6 +177,8 @@ defaultModelOptions = new ModelOptions({ * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` * to have access to the updated model. * + * ### Overriding immediate updates + * * The following example shows how to override immediate updates. Changes on the inputs within the * form will update the model only when the control loses focus (blur event). If `escape` key is * pressed while the input field is focused, the value is reset to the value in the current model. @@ -236,6 +238,8 @@ defaultModelOptions = new ModelOptions({ * * * + * ### Debouncing updates + * * The next example shows how to debounce model changes. Model will be updated only 1 sec after last change. * If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. * @@ -260,6 +264,106 @@ defaultModelOptions = new ModelOptions({ * * * + * ### Default events, extra triggers, and catch-all debounce values + * + * This example shows the relationship between "default" update events and + * additional `updateOn` triggers. + * + * `default` events are those that are bound to the control, and when fired, update the `$viewValue` + * via {@link ngModel.NgModelController#$setViewValue $setViewValue}. Every event that is not listed + * in `updateOn` is considered a "default" event, since different control types have different + * default events. + * + * The control in this example updates by "default", "click", and "blur", with different `debounce` + * values. You can see that "click" doesn't have an individual `debounce` value - + * therefore it uses the `*` debounce value. + * + * There is also a button that calls {@link ngModel.NgModelController#$setViewValue $setViewValue} + * directly with a "custom" event. Since "custom" is not defined in the `updateOn` list, + * it is considered a "default" event and will update the + * control if "default" is defined in `updateOn`, and will receive the "default" debounce value. + * Note that this is just to illustrate how custom controls would possibly call `$setViewValue`. + * + * You can change the `updateOn` and `debounce` configuration to test different scenarios. This + * is done with {@link ngModel.NgModelController#$overrideModelOptions $overrideModelOptions}. + * + + + + + + angular.module('optionsExample', []) + .component('modelUpdateDemo', { + templateUrl: 'template.html', + controller: function() { + this.name = 'Chinua'; + + this.options = { + updateOn: 'default blur click', + debounce: { + default: 2000, + blur: 0, + '*': 1000 + } + }; + + this.updateEvents = function() { + var eventList = this.options.updateOn.split(' '); + eventList.push('*'); + var events = {}; + + for (var i = 0; i < eventList.length; i++) { + events[eventList[i]] = this.options.debounce[eventList[i]]; + } + + this.events = events; + }; + + this.updateOptions = function() { + var options = angular.extend(this.options, { + updateOn: Object.keys(this.events).join(' ').replace('*', ''), + debounce: this.events + }); + + this.form.input.$overrideModelOptions(options); + }; + + // Initialize the event form + this.updateEvents(); + } + }); + + +
    + Input: +
    + Model: {{$ctrl.name}} +
    + + +
    +
    + updateOn
    + + + + + + + + + + + +
    OptionDebounce value
    {{key}}
    + +
    + +
    +
    +
    + * + * * ## Model updates and validation * * The default behaviour in `ngModel` is that the model value is set to `undefined` when the @@ -307,11 +411,30 @@ defaultModelOptions = new ModelOptions({ * You can specify the timezone that date/time input directives expect by providing its name in the * `timezone` property. * + * + * ## Programmatically changing options + * + * The `ngModelOptions` expression is only evaluated once when the directive is linked; it is not + * watched for changes. However, it is possible to override the options on a single + * {@link ngModel.NgModelController} instance with + * {@link ngModel.NgModelController#$overrideModelOptions}. See also the example for + * {@link ngModelOptions#default-events-extra-triggers-and-catch-all-debounce-values + * Default events, extra triggers, and catch-all debounce values}. + * + * * @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and * and its descendents. Valid keys are: * - `updateOn`: string specifying which event should the input be bound to. You can set several * events using an space delimited list. There is a special event called `default` that - * matches the default events belonging to the control. + * matches the default events belonging to the control. These are the events that are bound to + * the control, and when fired, update the `$viewValue` via `$setViewValue`. + * + * `ngModelOptions` considers every event that is not listed in `updateOn` a "default" event, + * since different control types use different default events. + * + * See also the section {@link ngModelOptions#triggering-and-debouncing-model-updates + * Triggering and debouncing model updates}. + * * - `debounce`: integer value which contains the debounce model update value in milliseconds. A * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a * custom value for each event. For example: diff --git a/test/ng/directive/ngModelOptionsSpec.js b/test/ng/directive/ngModelOptionsSpec.js index 3814ba2bc5ca..09a9ad5f4a7c 100644 --- a/test/ng/directive/ngModelOptionsSpec.js +++ b/test/ng/directive/ngModelOptionsSpec.js @@ -391,6 +391,43 @@ describe('ngModelOptions', function() { browserTrigger(inputElm[2], 'click'); expect($rootScope.color).toBe('blue'); }); + + it('should re-set the trigger events when overridden with $overrideModelOptions', function() { + var inputElm = helper.compileInput( + ''); + + var ctrl = inputElm.controller('ngModel'); + + helper.changeInputValueTo('a'); + expect($rootScope.name).toBeUndefined(); + browserTrigger(inputElm, 'blur'); + expect($rootScope.name).toEqual('a'); + + helper.changeInputValueTo('b'); + expect($rootScope.name).toBe('a'); + browserTrigger(inputElm, 'click'); + expect($rootScope.name).toEqual('b'); + + $rootScope.$apply('name = undefined'); + expect(inputElm.val()).toBe(''); + ctrl.$overrideModelOptions({updateOn: 'blur mousedown'}); + + helper.changeInputValueTo('a'); + expect($rootScope.name).toBeUndefined(); + browserTrigger(inputElm, 'blur'); + expect($rootScope.name).toEqual('a'); + + helper.changeInputValueTo('b'); + expect($rootScope.name).toBe('a'); + browserTrigger(inputElm, 'click'); + expect($rootScope.name).toBe('a'); + + browserTrigger(inputElm, 'mousedown'); + expect($rootScope.name).toEqual('b'); + }); + }); From 394b185416c473ec5e04c6ca2c8e5bedcbaf5357 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Tue, 5 Dec 2017 13:26:20 -0800 Subject: [PATCH 031/469] refactor($rootScope): consistently use noop as the default $watch listener Closes #16343 --- src/ng/interpolate.js | 4 +--- src/ng/rootScope.js | 9 +++------ src/ngMessageFormat/messageFormatCommon.js | 4 ++-- src/ngMessageFormat/messageFormatInterpolationParts.js | 4 +--- src/ngMessageFormat/messageFormatSelector.js | 4 +--- 5 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index 4a3998e77f59..30ad9e3a9ad8 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -331,9 +331,7 @@ function $InterpolateProvider() { var lastValue; return scope.$watchGroup(parseFns, /** @this */ function interpolateFnWatcher(values, oldValues) { var currValue = compute(values); - if (isFunction(listener)) { - listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope); - } + listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope); lastValue = currValue; }); } diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 7d29b97a3376..2153cfa3edd4 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -394,14 +394,15 @@ function $RootScopeProvider() { */ $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) { var get = $parse(watchExp); + var fn = isFunction(listener) ? listener : noop; if (get.$$watchDelegate) { - return get.$$watchDelegate(this, listener, objectEquality, get, watchExp); + return get.$$watchDelegate(this, fn, objectEquality, get, watchExp); } var scope = this, array = scope.$$watchers, watcher = { - fn: listener, + fn: fn, last: initWatchVal, get: get, exp: prettyPrintExpression || watchExp, @@ -410,10 +411,6 @@ function $RootScopeProvider() { lastDirtyWatch = null; - if (!isFunction(listener)) { - watcher.fn = noop; - } - if (!array) { array = scope.$$watchers = []; array.$$digestWatchIndex = -1; diff --git a/src/ngMessageFormat/messageFormatCommon.js b/src/ngMessageFormat/messageFormatCommon.js index bde7d0d7730e..29ed30c4d5c8 100644 --- a/src/ngMessageFormat/messageFormatCommon.js +++ b/src/ngMessageFormat/messageFormatCommon.js @@ -34,7 +34,7 @@ function parseTextLiteral(text) { parsedFn['$$watchDelegate'] = function watchDelegate(scope, listener, objectEquality) { var unwatch = scope['$watch'](noop, function textLiteralWatcher() { - if (isFunction(listener)) { listener(text, text, scope); } + listener(text, text, scope); unwatch(); }, objectEquality); @@ -58,7 +58,7 @@ function subtractOffset(expressionFn, offset) { parsedFn['$$watchDelegate'] = function watchDelegate(scope, listener, objectEquality) { unwatch = scope['$watch'](expressionFn, function pluralExpressionWatchListener(newValue, oldValue) { - if (isFunction(listener)) { listener(minusOffset(newValue), minusOffset(oldValue), scope); } + listener(minusOffset(newValue), minusOffset(oldValue), scope); }, objectEquality); return unwatch; diff --git a/src/ngMessageFormat/messageFormatInterpolationParts.js b/src/ngMessageFormat/messageFormatInterpolationParts.js index f2526f1c6ddc..422f1350753c 100644 --- a/src/ngMessageFormat/messageFormatInterpolationParts.js +++ b/src/ngMessageFormat/messageFormatInterpolationParts.js @@ -122,9 +122,7 @@ function InterpolationPartsWatcher(interpolationParts, scope, listener, objectEq InterpolationPartsWatcher.prototype.watchListener = function watchListener(newExpressionValues, oldExpressionValues) { var result = this.interpolationParts.getResult(newExpressionValues); - if (isFunction(this.listener)) { - this.listener.call(null, result, newExpressionValues === oldExpressionValues ? result : this.previousResult, this.scope); - } + this.listener.call(null, result, newExpressionValues === oldExpressionValues ? result : this.previousResult, this.scope); this.previousResult = result; }; diff --git a/src/ngMessageFormat/messageFormatSelector.js b/src/ngMessageFormat/messageFormatSelector.js index 749d9fd861f4..f5b110214194 100644 --- a/src/ngMessageFormat/messageFormatSelector.js +++ b/src/ngMessageFormat/messageFormatSelector.js @@ -66,9 +66,7 @@ MessageSelectorWatchers.prototype.expressionFnListener = function expressionFnLi }; MessageSelectorWatchers.prototype.messageFnListener = function messageFnListener(newMessage, oldMessage) { - if (isFunction(this.listener)) { - this.listener.call(null, newMessage, newMessage === oldMessage ? newMessage : this.lastMessage, this.scope); - } + this.listener.call(null, newMessage, newMessage === oldMessage ? newMessage : this.lastMessage, this.scope); this.lastMessage = newMessage; }; From dde520e02a78fa1a194268d84ce7203513613738 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 7 Dec 2017 11:51:03 +0100 Subject: [PATCH 032/469] docs(ngModelOptions): fix link text --- src/ng/directive/ngModelOptions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ng/directive/ngModelOptions.js b/src/ng/directive/ngModelOptions.js index 03c7e5945b5c..36dc93d40dea 100644 --- a/src/ng/directive/ngModelOptions.js +++ b/src/ng/directive/ngModelOptions.js @@ -417,7 +417,8 @@ defaultModelOptions = new ModelOptions({ * The `ngModelOptions` expression is only evaluated once when the directive is linked; it is not * watched for changes. However, it is possible to override the options on a single * {@link ngModel.NgModelController} instance with - * {@link ngModel.NgModelController#$overrideModelOptions}. See also the example for + * {@link ngModel.NgModelController#$overrideModelOptions `NgModelController#$overrideModelOptions()`}. + * See also the example for * {@link ngModelOptions#default-events-extra-triggers-and-catch-all-debounce-values * Default events, extra triggers, and catch-all debounce values}. * From a7688100e3714ebc1e26b8289cbf6d1689f32e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82=C4=99biowski-Owczarek?= Date: Thu, 7 Dec 2017 19:26:32 +0100 Subject: [PATCH 033/469] build(*): update Node from 6 to 8, update Yarn Angular (2+) switched to Node 8 and so should we. Closes #16360 Ref angular/angular#20807 Ref angular/angular#20832 --- .nvmrc | 2 +- .travis.yml | 4 ++-- package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.nvmrc b/.nvmrc index 1e8b31496214..45a4fb75db86 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -6 +8 diff --git a/.travis.yml b/.travis.yml index 6d59cf07affd..9748e1317f30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: node_js sudo: false node_js: - - '6' + - '8' cache: yarn: true @@ -28,7 +28,7 @@ env: - secure: oTBjhnOKhs0qDSKTf7fE4f6DYiNDPycvB7qfSF5QRIbJK/LK/J4UtFwetXuXj79HhUZG9qnoT+5e7lPaiaMlpsIKn9ann7ffqFWN1E8TMtpJF+AGigx3djYElwfgf5nEnFUFhwjFzvbfpZNnxVGgX5YbIZpe/WUbHkP4ffU0Wks= before_install: - - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.27.5 + - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.3.2 - export PATH="$HOME/.yarn/bin:$PATH" before_script: diff --git a/package.json b/package.json index 6ca57a1193c5..74335fb4705f 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "url": "https://github.com/angular/angular.js.git" }, "engines": { - "node": "^6.9.1", - "yarn": ">=0.21.3", + "node": "^8.9.1", + "yarn": ">=1.3.2", "grunt": "^1.2.0" }, "scripts": { From a61c5d382e4986104353031729c3658bcd86ab07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82=C4=99biowski-Owczarek?= Date: Thu, 7 Dec 2017 20:27:35 +0100 Subject: [PATCH 034/469] chore(*): bump Yarn in Jenkins init-node script Without it Jenkins builds are broken. Closes #16365 --- scripts/jenkins/init-node.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/jenkins/init-node.sh b/scripts/jenkins/init-node.sh index dcb97e659677..d851d974a9f0 100755 --- a/scripts/jenkins/init-node.sh +++ b/scripts/jenkins/init-node.sh @@ -8,7 +8,7 @@ nvm install # clean out and install yarn rm -rf ~/.yarn -curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.21.3 +curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.3.2 export PATH="$HOME/.yarn/bin:$PATH" # Ensure that we have the local dependencies installed From 00815db8ef1a54e331b301ee3604d47e75c49d46 Mon Sep 17 00:00:00 2001 From: John Hampton <8835055+jjhampton@users.noreply.github.com> Date: Wed, 6 Dec 2017 17:00:53 -0500 Subject: [PATCH 035/469] docs(ng-model-options): remove extra quotes in example Remove unnecessary quotes around attribute directive name in the docs example. This syntax is incorrect. Closes #16362 --- src/ng/directive/ngModelOptions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/directive/ngModelOptions.js b/src/ng/directive/ngModelOptions.js index 36dc93d40dea..02a328ff3748 100644 --- a/src/ng/directive/ngModelOptions.js +++ b/src/ng/directive/ngModelOptions.js @@ -102,7 +102,7 @@ defaultModelOptions = new ModelOptions({ * * The `ngModelOptions` settings are found by evaluating the value of the attribute directive as * an AngularJS expression. This expression should evaluate to an object, whose properties contain - * the settings. For example: `
    Date: Mon, 11 Dec 2017 19:27:47 +0100 Subject: [PATCH 036/469] fix(ngAria): do not set aria attributes on input[type="hidden"] This fixes a error found by @edclements using the Google Accessibility Developer Tools audit. Input fields of type hidden shouldn't have aria attributes. https://www.w3.org/TR/html-aria/#allowed-aria-roles-states-and-properties-1 Closes #15113 Closes #16367 BREAKING CHANGE: ngAria no longer sets aria-* attributes on input[type="hidden"] with ngModel. This can affect apps that test for the presence of aria attributes on hidden inputs. To migrate, remove these assertions. In actual apps, this should not have a user-facing effect, as the previous behavior was incorrect, and the new behavior is correct for accessibility. --- src/ngAria/aria.js | 5 ++++- test/ngAria/ariaSpec.js | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/ngAria/aria.js b/src/ngAria/aria.js index 570eff31401e..bf82141b2123 100644 --- a/src/ngAria/aria.js +++ b/src/ngAria/aria.js @@ -224,7 +224,10 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) { .directive('ngModel', ['$aria', function($aria) { function shouldAttachAttr(attr, normalizedAttr, elem, allowBlacklistEls) { - return $aria.config(normalizedAttr) && !elem.attr(attr) && (allowBlacklistEls || !isNodeOneOf(elem, nodeBlackList)); + return $aria.config(normalizedAttr) && + !elem.attr(attr) && + (allowBlacklistEls || !isNodeOneOf(elem, nodeBlackList)) && + (elem.attr('type') !== 'hidden' || elem[0].nodeName !== 'INPUT'); } function shouldAttachRole(role, elem) { diff --git a/test/ngAria/ariaSpec.js b/test/ngAria/ariaSpec.js index 9f7edaa2e9c0..201608498961 100644 --- a/test/ngAria/ariaSpec.js +++ b/test/ngAria/ariaSpec.js @@ -420,6 +420,21 @@ describe('$aria', function() { scope.$apply('txtInput=\'LTten\''); expect(element.attr('aria-invalid')).toBe('userSetValue'); }); + + it('should not attach if input is type="hidden"', function() { + compileElement(''); + expect(element.attr('aria-invalid')).toBeUndefined(); + }); + + + it('should attach aria-invalid to custom control that is type="hidden"', function() { + compileElement('
    '); + scope.$apply('txtInput=\'LTten\''); + expect(element.attr('aria-invalid')).toBe('true'); + + scope.$apply('txtInput=\'morethantencharacters\''); + expect(element.attr('aria-invalid')).toBe('false'); + }); }); describe('aria-invalid when disabled', function() { From 2c1e58984b31a86d5d81c03f9c54852dfdb44829 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 1 Dec 2017 08:28:55 +0000 Subject: [PATCH 037/469] fix($location): always decode special chars in `$location.url(http://github.com/value)` The original fix for #16312 included changing how `$location.url(http://github.com/value)` decoded the special characters passed to it as a setter. This broke a number of use cases (mostly involving the ui-router). Further analysis appears to show that we can solve #16312, to prevent urls being rewritten with decoded values, without modifying the behaviour of `$location.url`. This commit reverts changes to `$location.url(http://github.com/value)` so that encoded chars will once again be decoded and passed to `$location.path(value)`. In particular it will convert encoded forward slashes, which changes how the path is updated, since e.g. `a/b/%2Fc%2Fd` will become `a/b/c/d`. While this is arguably not "correct", it appears that there are too many use cases relying upon this behaviour. --- src/ng/location.js | 2 +- test/ng/locationSpec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ng/location.js b/src/ng/location.js index 625b9297720d..63cdbb84f0d1 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -422,7 +422,7 @@ var locationPrototype = { } var match = PATH_MATCH.exec(url); - if (match[1] || url === '') this.path((this.$$html5 ? decodeURI : decodeURIComponent)(match[1])); + if (match[1] || url === '') this.path(decodeURIComponent(match[1])); if (match[2] || match[1] || url === '') this.search(match[3] || ''); this.hash(match[5] || ''); diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index af17a1e3db76..3dbfcd3af22c 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -681,11 +681,11 @@ describe('$location', function() { expect(locationUrl.path()).toEqual('/foo:bar'); }); - it('url() should not decode non-component special characters in html5 mode', function() { + it('url() should decode non-component special characters in html5 mode', function() { var locationUrl = new LocationHtml5Url('http://host.com/', 'http://host.com/'); locationUrl.$$parse('http://host.com/'); locationUrl.url('http://github.com/foo%3Abar'); - expect(locationUrl.path()).toEqual('/foo%3Abar'); + expect(locationUrl.path()).toEqual('/foo:bar'); }); }); }); From 41d5c90f170cc054b0f8f88220c22ef1ef6cc0a6 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Tue, 31 Oct 2017 13:26:36 +0000 Subject: [PATCH 038/469] feat($rootScope): allow suspending and resuming watchers on scope This can be very helpful for external modules that help making the digest loop faster by ignoring some of the watchers under some circumstance. Example: https://github.com/shahata/angular-viewport-watch Thanks to @shahata for the original implementation. Closes #5301 --- src/ng/rootScope.js | 100 +++++++++++++++++++++++++++++++- test/ng/rootScopeSpec.js | 122 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+), 3 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 2153cfa3edd4..964ba7bceaf5 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -91,6 +91,7 @@ function $RootScopeProvider() { this.$$watchersCount = 0; this.$id = nextUid(); this.$$ChildScope = null; + this.$$suspended = false; } ChildScope.prototype = parent; return ChildScope; @@ -178,6 +179,7 @@ function $RootScopeProvider() { this.$$childHead = this.$$childTail = null; this.$root = this; this.$$destroyed = false; + this.$$suspended = false; this.$$listeners = {}; this.$$listenerCount = {}; this.$$watchersCount = 0; @@ -808,7 +810,7 @@ function $RootScopeProvider() { traverseScopesLoop: do { // "traverse the scopes" loop - if ((watchers = current.$$watchers)) { + if ((watchers = !current.$$suspended && current.$$watchers)) { // process our watches watchers.$$digestWatchIndex = watchers.length; while (watchers.$$digestWatchIndex--) { @@ -852,7 +854,9 @@ function $RootScopeProvider() { // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast - if (!(next = ((current.$$watchersCount && current.$$childHead) || + // (though it differs due to having the extra check for $$suspended and does not + // check $$listenerCount) + if (!(next = ((!current.$$suspended && current.$$watchersCount && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; @@ -889,6 +893,95 @@ function $RootScopeProvider() { $browser.$$checkUrlChange(); }, + /** + * @ngdoc method + * @name $rootScope.Scope#$suspend + * @kind function + * + * @description + * Suspend watchers of this scope subtree so that they will not be invoked during digest. + * + * This can be used to optimize your application when you know that running those watchers + * is redundant. + * + * **Warning** + * + * Suspending scopes from the digest cycle can have unwanted and difficult to debug results. + * Only use this approach if you are confident that you know what you are doing and have + * ample tests to ensure that bindings get updated as you expect. + * + * Some of the things to consider are: + * + * * Any external event on a directive/component will not trigger a digest while the hosting + * scope is suspended - even if the event handler calls `$apply()` or `$rootScope.$digest()`. + * * Transcluded content exists on a scope that inherits from outside a directive but exists + * as a child of the directive's containing scope. If the containing scope is suspended the + * transcluded scope will also be suspended, even if the scope from which the transcluded + * scope inherits is not suspended. + * * Multiple directives trying to manage the suspended status of a scope can confuse each other: + * * A call to `$suspend()` on an already suspended scope is a no-op. + * * A call to `$resume()` on a non-suspended scope is a no-op. + * * If two directives suspend a scope, then one of them resumes the scope, the scope will no + * longer be suspended. This could result in the other directive believing a scope to be + * suspended when it is not. + * * If a parent scope is suspended then all its descendants will be also excluded from future + * digests whether or not they have been suspended themselves. Note that this also applies to + * isolate child scopes. + * * Calling `$digest()` directly on a descendant of a suspended scope will still run the watchers + * for that scope and its descendants. When digesting we only check whether the current scope is + * locally suspended, rather than checking whether it has a suspended ancestor. + * * Calling `$resume()` on a scope that has a suspended ancestor will not cause the scope to be + * included in future digests until all its ancestors have been resumed. + * * Resolved promises, e.g. from explicit `$q` deferreds and `$http` calls, trigger `$apply()` + * against the `$rootScope` and so will still trigger a global digest even if the promise was + * initiated by a component that lives on a suspended scope. + */ + $suspend: function() { + this.$$suspended = true; + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$isSuspended + * @kind function + * + * @description + * Call this method to determine if this scope has been explicitly suspended. It will not + * tell you whether an ancestor has been suspended. + * To determine if this scope will be excluded from a digest triggered at the $rootScope, + * for example, you must check all its ancestors: + * + * ``` + * function isExcludedFromDigest(scope) { + * while(scope) { + * if (scope.$isSuspended()) return true; + * scope = scope.$parent; + * } + * return false; + * ``` + * + * Be aware that a scope may not be included in digests if it has a suspended ancestor, + * even if `$isSuspended()` returns false. + * + * @returns true if the current scope has been suspended. + */ + $isSuspended: function() { + return this.$$suspended; + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$resume + * @kind function + * + * @description + * Resume watchers of this scope subtree in case it was suspended. + * + * See {@link $rootScope.Scope#$suspend} for information about the dangers of using this approach. + */ + $resume: function() { + this.$$suspended = false; + }, /** * @ngdoc event @@ -1289,7 +1382,8 @@ function $RootScopeProvider() { // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $digest - // (though it differs due to having the extra check for $$listenerCount) + // (though it differs due to having the extra check for $$listenerCount and + // does not check $$suspended) if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index fc32bb139a5a..90a79625e62c 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -1255,6 +1255,128 @@ describe('Scope', function() { }); }); + + describe('$suspend/$resume/$isSuspended', function() { + it('should suspend watchers on scope', inject(function($rootScope) { + var watchSpy = jasmine.createSpy('watchSpy'); + $rootScope.$watch(watchSpy); + $rootScope.$suspend(); + $rootScope.$digest(); + expect(watchSpy).not.toHaveBeenCalled(); + })); + + it('should resume watchers on scope', inject(function($rootScope) { + var watchSpy = jasmine.createSpy('watchSpy'); + $rootScope.$watch(watchSpy); + $rootScope.$suspend(); + $rootScope.$resume(); + $rootScope.$digest(); + expect(watchSpy).toHaveBeenCalled(); + })); + + it('should suspend watchers on child scope', inject(function($rootScope) { + var watchSpy = jasmine.createSpy('watchSpy'); + var scope = $rootScope.$new(true); + scope.$watch(watchSpy); + $rootScope.$suspend(); + $rootScope.$digest(); + expect(watchSpy).not.toHaveBeenCalled(); + })); + + it('should resume watchers on child scope', inject(function($rootScope) { + var watchSpy = jasmine.createSpy('watchSpy'); + var scope = $rootScope.$new(true); + scope.$watch(watchSpy); + $rootScope.$suspend(); + $rootScope.$resume(); + $rootScope.$digest(); + expect(watchSpy).toHaveBeenCalled(); + })); + + it('should resume digesting immediately if `$resume` is called from an ancestor scope watch handler', inject(function($rootScope) { + var watchSpy = jasmine.createSpy('watchSpy'); + var scope = $rootScope.$new(); + + // Setup a handler that will toggle the scope suspension + $rootScope.$watch('a', function(a) { if (a) scope.$resume(); else scope.$suspend(); }); + + // Spy on the scope watches being called + scope.$watch(watchSpy); + + // Trigger a digest that should suspend the scope from within the watch handler + $rootScope.$apply('a = false'); + // The scope is suspended before it gets to do a digest + expect(watchSpy).not.toHaveBeenCalled(); + + // Trigger a digest that should resume the scope from within the watch handler + $rootScope.$apply('a = true'); + // The watch handler that resumes the scope is in the parent, so the resumed scope will digest immediately + expect(watchSpy).toHaveBeenCalled(); + })); + + it('should resume digesting immediately if `$resume` is called from a non-ancestor scope watch handler', inject(function($rootScope) { + var watchSpy = jasmine.createSpy('watchSpy'); + var scope = $rootScope.$new(); + var sibling = $rootScope.$new(); + + // Setup a handler that will toggle the scope suspension + sibling.$watch('a', function(a) { if (a) scope.$resume(); else scope.$suspend(); }); + + // Spy on the scope watches being called + scope.$watch(watchSpy); + + // Trigger a digest that should suspend the scope from within the watch handler + $rootScope.$apply('a = false'); + // The scope is suspended by the sibling handler after the scope has already digested + expect(watchSpy).toHaveBeenCalled(); + watchSpy.calls.reset(); + + // Trigger a digest that should resume the scope from within the watch handler + $rootScope.$apply('a = true'); + // The watch handler that resumes the scope marks the digest as dirty, so it will run an extra digest + expect(watchSpy).toHaveBeenCalled(); + })); + + it('should not suspend watchers on parent or sibling scopes', inject(function($rootScope) { + var watchSpyParent = jasmine.createSpy('watchSpyParent'); + var watchSpyChild = jasmine.createSpy('watchSpyChild'); + var watchSpySibling = jasmine.createSpy('watchSpySibling'); + + var parent = $rootScope.$new(); + parent.$watch(watchSpyParent); + var child = parent.$new(); + child.$watch(watchSpyChild); + var sibling = parent.$new(); + sibling.$watch(watchSpySibling); + + child.$suspend(); + $rootScope.$digest(); + expect(watchSpyParent).toHaveBeenCalled(); + expect(watchSpyChild).not.toHaveBeenCalled(); + expect(watchSpySibling).toHaveBeenCalled(); + })); + + it('should return true from `$isSuspended()` when a scope is suspended', inject(function($rootScope) { + $rootScope.$suspend(); + expect($rootScope.$isSuspended()).toBe(true); + $rootScope.$resume(); + expect($rootScope.$isSuspended()).toBe(false); + })); + + it('should return false from `$isSuspended()` for a non-suspended scope that has a suspended ancestor', inject(function($rootScope) { + var childScope = $rootScope.$new(); + $rootScope.$suspend(); + expect(childScope.$isSuspended()).toBe(false); + childScope.$suspend(); + expect(childScope.$isSuspended()).toBe(true); + childScope.$resume(); + expect(childScope.$isSuspended()).toBe(false); + $rootScope.$resume(); + expect(childScope.$isSuspended()).toBe(false); + })); + }); + + describe('optimizations', function() { function setupWatches(scope, log) { From e5fb92978f310d690e394bac2c09beeae4a56e03 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Mon, 4 Dec 2017 22:12:53 -0800 Subject: [PATCH 039/469] revert: fix($rootScope): fix potential memory leak when removing scope listeners This reverts commit 817ac56719505680ac4c9997972e8f39eb40a6d0. --- docs/content/error/$rootScope/inevt.ngdoc | 22 ---- src/ng/rootScope.js | 79 ++++++++------- test/ng/rootScopeSpec.js | 118 +--------------------- 3 files changed, 47 insertions(+), 172 deletions(-) delete mode 100644 docs/content/error/$rootScope/inevt.ngdoc diff --git a/docs/content/error/$rootScope/inevt.ngdoc b/docs/content/error/$rootScope/inevt.ngdoc deleted file mode 100644 index a06eeba18627..000000000000 --- a/docs/content/error/$rootScope/inevt.ngdoc +++ /dev/null @@ -1,22 +0,0 @@ -@ngdoc error -@name $rootScope:inevt -@fullName Recursive $emit/$broadcast event -@description - -This error occurs when the an event is `$emit`ed or `$broadcast`ed recursively on a scope. - -For example, when an event listener fires the same event being listened to. - -``` -$scope.$on('foo', function() { - $scope.$emit('foo'); -}); -``` - -Or when a parent element causes indirect recursion. - -``` -$scope.$on('foo', function() { - $rootScope.$broadcast('foo'); -}); -``` \ No newline at end of file diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 964ba7bceaf5..07c692e9101b 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -1271,14 +1271,10 @@ function $RootScopeProvider() { var self = this; return function() { - var index = arrayRemove(namedListeners, listener); - if (index >= 0) { + var indexOfListener = namedListeners.indexOf(listener); + if (indexOfListener !== -1) { + namedListeners[indexOfListener] = null; decrementListenerCount(self, 1, name); - // We are removing a listener while iterating over the list of listeners. - // Update the current $$index if necessary to ensure no listener is skipped. - if (index <= namedListeners.$$index) { - namedListeners.$$index--; - } } }; }, @@ -1307,7 +1303,9 @@ function $RootScopeProvider() { * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). */ $emit: function(name, args) { - var scope = this, + var empty = [], + namedListeners, + scope = this, stopPropagation = false, event = { name: name, @@ -1318,11 +1316,28 @@ function $RootScopeProvider() { }, defaultPrevented: false }, - listenerArgs = concat([event], arguments, 1); + listenerArgs = concat([event], arguments, 1), + i, length; do { - invokeListeners(scope, event, listenerArgs, name); - + namedListeners = scope.$$listeners[name] || empty; + event.currentScope = scope; + for (i = 0, length = namedListeners.length; i < length; i++) { + + // if listeners were deregistered, defragment the array + if (!namedListeners[i]) { + namedListeners.splice(i, 1); + i--; + length--; + continue; + } + try { + //allow all listeners attached to the current scope to run + namedListeners[i].apply(null, listenerArgs); + } catch (e) { + $exceptionHandler(e); + } + } //if any listener on the current scope stops propagation, prevent bubbling if (stopPropagation) { break; @@ -1373,11 +1388,28 @@ function $RootScopeProvider() { if (!target.$$listenerCount[name]) return event; - var listenerArgs = concat([event], arguments, 1); + var listenerArgs = concat([event], arguments, 1), + listeners, i, length; //down while you can, then up and next sibling or up and next sibling until back at root while ((current = next)) { - invokeListeners(current, event, listenerArgs, name); + event.currentScope = current; + listeners = current.$$listeners[name] || []; + for (i = 0, length = listeners.length; i < length; i++) { + // if listeners were deregistered, defragment the array + if (!listeners[i]) { + listeners.splice(i, 1); + i--; + length--; + continue; + } + + try { + listeners[i].apply(null, listenerArgs); + } catch (e) { + $exceptionHandler(e); + } + } // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! @@ -1408,27 +1440,6 @@ function $RootScopeProvider() { return $rootScope; - function invokeListeners(scope, event, listenerArgs, name) { - var listeners = scope.$$listeners[name]; - if (listeners) { - if (listeners.$$index !== undefined) { - throw $rootScopeMinErr('inevt', '{0} already $emit/$broadcast-ing on scope ({1})', name, scope.$id); - } - event.currentScope = scope; - try { - for (listeners.$$index = 0; listeners.$$index < listeners.length; listeners.$$index++) { - try { - //allow all listeners attached to the current scope to run - listeners[listeners.$$index].apply(null, listenerArgs); - } catch (e) { - $exceptionHandler(e); - } - } - } finally { - listeners.$$index = undefined; - } - } - } function beginPhase(phase) { if ($rootScope.$$phase) { diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 90a79625e62c..6f683221742e 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -2438,19 +2438,6 @@ describe('Scope', function() { })); - // See issue https://github.com/angular/angular.js/issues/16135 - it('should deallocate the listener array entry', inject(function($rootScope) { - var remove1 = $rootScope.$on('abc', noop); - $rootScope.$on('abc', noop); - - expect($rootScope.$$listeners['abc'].length).toBe(2); - - remove1(); - - expect($rootScope.$$listeners['abc'].length).toBe(1); - })); - - it('should call next listener after removing the current listener via its own handler', inject(function($rootScope) { var listener1 = jasmine.createSpy('listener1').and.callFake(function() { remove1(); }); var remove1 = $rootScope.$on('abc', listener1); @@ -2583,107 +2570,6 @@ describe('Scope', function() { expect($rootScope.$$listenerCount).toEqual({abc: 1}); expect(child.$$listenerCount).toEqual({abc: 1}); })); - - - it('should throw on recursive $broadcast', inject(function($rootScope) { - $rootScope.$on('e', function() { $rootScope.$broadcast('e'); }); - - expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - })); - - - it('should throw on nested recursive $broadcast', inject(function($rootScope) { - $rootScope.$on('e2', function() { $rootScope.$broadcast('e'); }); - $rootScope.$on('e', function() { $rootScope.$broadcast('e2'); }); - - expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - })); - - - it('should throw on recursive $emit', inject(function($rootScope) { - $rootScope.$on('e', function() { $rootScope.$emit('e'); }); - - expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - })); - - - it('should throw on nested recursive $emit', inject(function($rootScope) { - $rootScope.$on('e2', function() { $rootScope.$emit('e'); }); - $rootScope.$on('e', function() { $rootScope.$emit('e2'); }); - - expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - })); - - - it('should throw on recursive $broadcast on child listener', inject(function($rootScope) { - var child = $rootScope.$new(); - child.$on('e', function() { $rootScope.$broadcast('e'); }); - - expect(function() { child.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (2)'); - expect(function() { child.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (2)'); - })); - - - it('should throw on nested recursive $broadcast on child listener', inject(function($rootScope) { - var child = $rootScope.$new(); - child.$on('e2', function() { $rootScope.$broadcast('e'); }); - child.$on('e', function() { $rootScope.$broadcast('e2'); }); - - expect(function() { child.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (2)'); - expect(function() { child.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (2)'); - })); - - - it('should throw on recursive $emit parent listener', inject(function($rootScope) { - var child = $rootScope.$new(); - $rootScope.$on('e', function() { child.$emit('e'); }); - - expect(function() { child.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - })); - - - it('should throw on nested recursive $emit parent listener', inject(function($rootScope) { - var child = $rootScope.$new(); - $rootScope.$on('e2', function() { child.$emit('e'); }); - $rootScope.$on('e', function() { child.$emit('e2'); }); - - expect(function() { child.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)'); - })); - - - it('should clear recursive state of $broadcast if $exceptionHandler rethrows', function() { - module(function($exceptionHandlerProvider) { - $exceptionHandlerProvider.mode('rethrow'); - }); - inject(function($rootScope) { - var throwingListener = jasmine.createSpy('thrower').and.callFake(function() { - throw new Error('Listener Error!'); - }); - var secondListener = jasmine.createSpy('second'); - - $rootScope.$on('e', throwingListener); - $rootScope.$on('e', secondListener); - - expect(function() { $rootScope.$broadcast('e'); }).toThrowError('Listener Error!'); - expect(throwingListener).toHaveBeenCalled(); - expect(secondListener).not.toHaveBeenCalled(); - - throwingListener.calls.reset(); - secondListener.calls.reset(); - - expect(function() { $rootScope.$broadcast('e'); }).toThrowError('Listener Error!'); - expect(throwingListener).toHaveBeenCalled(); - expect(secondListener).not.toHaveBeenCalled(); - }); - }); }); }); @@ -2773,7 +2659,7 @@ describe('Scope', function() { expect(spy1).toHaveBeenCalledOnce(); expect(spy2).toHaveBeenCalledOnce(); expect(spy3).toHaveBeenCalledOnce(); - expect(child.$$listeners['evt'].length).toBe(2); + expect(child.$$listeners['evt'].length).toBe(3); // cleanup will happen on next $emit spy1.calls.reset(); spy2.calls.reset(); @@ -2807,7 +2693,7 @@ describe('Scope', function() { expect(spy1).toHaveBeenCalledOnce(); expect(spy2).toHaveBeenCalledOnce(); expect(spy3).toHaveBeenCalledOnce(); - expect(child.$$listeners['evt'].length).toBe(2); + expect(child.$$listeners['evt'].length).toBe(3); //cleanup will happen on next $broadcast spy1.calls.reset(); spy2.calls.reset(); From 97d0224ae60f614c8b8a07a00829e46ec4997382 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Tue, 8 Aug 2017 23:24:17 -0700 Subject: [PATCH 040/469] fix($rootScope): fix potential memory leak when removing scope listeners When removing listeners they are removed from the array but the array size is not changed until the event is fired again. If the event is never fired but listeners are added/removed then the array will continue growing. By changing the listener removal to `delete` the array entry instead of setting it to `null` browsers can potentially deallocate the memory for the entry. Fixes #16135 Closes #16161 --- src/ng/rootScope.js | 5 ++++- test/ng/rootScopeSpec.js | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 07c692e9101b..7f37338e6636 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -1273,7 +1273,10 @@ function $RootScopeProvider() { return function() { var indexOfListener = namedListeners.indexOf(listener); if (indexOfListener !== -1) { - namedListeners[indexOfListener] = null; + // Use delete in the hope of the browser deallocating the memory for the array entry, + // while not shifting the array indexes of other listeners. + // See issue https://github.com/angular/angular.js/issues/16135 + delete namedListeners[indexOfListener]; decrementListenerCount(self, 1, name); } }; diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 6f683221742e..06a20f6963c5 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -2438,6 +2438,21 @@ describe('Scope', function() { })); + // See issue https://github.com/angular/angular.js/issues/16135 + it('should deallocate the listener array entry', inject(function($rootScope) { + var remove1 = $rootScope.$on('abc', noop); + $rootScope.$on('abc', noop); + + expect($rootScope.$$listeners['abc'].length).toBe(2); + expect(0 in $rootScope.$$listeners['abc']).toBe(true); + + remove1(); + + expect($rootScope.$$listeners['abc'].length).toBe(2); + expect(0 in $rootScope.$$listeners['abc']).toBe(false); + })); + + it('should call next listener after removing the current listener via its own handler', inject(function($rootScope) { var listener1 = jasmine.createSpy('listener1').and.callFake(function() { remove1(); }); var remove1 = $rootScope.$on('abc', listener1); From 5c38fb744eb9deaf602f2f1d2e02cefdb815a6c6 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Tue, 5 Dec 2017 00:49:16 -0800 Subject: [PATCH 041/469] test($rootScope): test recursive event broadcast and emit --- test/ng/rootScopeSpec.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 06a20f6963c5..eea9e824ae66 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -2925,6 +2925,45 @@ describe('Scope', function() { })); }); }); + + + it('should allow recursive $emit/$broadcast', inject(function($rootScope) { + var callCount = 0; + $rootScope.$on('evt', function($event, arg0) { + callCount++; + if (arg0 !== 1234) { + $rootScope.$emit('evt', 1234); + $rootScope.$broadcast('evt', 1234); + } + }); + + $rootScope.$emit('evt'); + $rootScope.$broadcast('evt'); + expect(callCount).toBe(6); + })); + + + it('should allow recursive $emit/$broadcast between parent/child', inject(function($rootScope) { + var child = $rootScope.$new(); + var calls = ''; + + $rootScope.$on('evt', function($event, arg0) { + calls += 'r'; // For "root". + if (arg0 === 'fromChild') { + $rootScope.$broadcast('evt', 'fromRoot2'); + } + }); + + child.$on('evt', function($event, arg0) { + calls += 'c'; // For "child". + if (arg0 === 'fromRoot1') { + child.$emit('evt', 'fromChild'); + } + }); + + $rootScope.$broadcast('evt', 'fromRoot1'); + expect(calls).toBe('rccrrc'); + })); }); describe('doc examples', function() { From 223de59e988dc0cc8b4ec3a045b7c0735eba1c77 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 12 Dec 2017 12:45:44 +0100 Subject: [PATCH 042/469] fix(form): set $submitted to true on child forms when parent is submitted Closes #10071 BREAKING CHANGE: Forms will now set $submitted on child forms when they are submitted. For example: ```
    Submitting this form will set $submitted on "parentform" and "childform". Previously, it was only set on "parentform". This change was introduced because mixing form and ngForm does not create logically separate forms, but rather something like input groups. Therefore, child forms should inherit the submission state from their parent form. --- src/ng/directive/form.js | 20 ++++++- test/ng/directive/formSpec.js | 107 ++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js index 1fc3b6f1cff1..2beb0837ecfd 100644 --- a/src/ng/directive/form.js +++ b/src/ng/directive/form.js @@ -9,7 +9,8 @@ var nullFormCtrl = { $setValidity: noop, $setDirty: noop, $setPristine: noop, - $setSubmitted: noop + $setSubmitted: noop, + $$setSubmitted: noop }, PENDING_CLASS = 'ng-pending', SUBMITTED_CLASS = 'ng-submitted'; @@ -274,12 +275,25 @@ FormController.prototype = { * @name form.FormController#$setSubmitted * * @description - * Sets the form to its submitted state. + * Sets the form to its `$submitted` state. This will also set `$submitted` on all child and + * parent forms of the form. */ $setSubmitted: function() { + var rootForm = this; + while (rootForm.$$parentForm && (rootForm.$$parentForm !== nullFormCtrl)) { + rootForm = rootForm.$$parentForm; + } + rootForm.$$setSubmitted(); + }, + + $$setSubmitted: function() { this.$$animate.addClass(this.$$element, SUBMITTED_CLASS); this.$submitted = true; - this.$$parentForm.$setSubmitted(); + forEach(this.$$controls, function(control) { + if (control.$$setSubmitted) { + control.$$setSubmitted(); + } + }); } }; diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js index b48ff1084468..2d996bb359e1 100644 --- a/test/ng/directive/formSpec.js +++ b/test/ng/directive/formSpec.js @@ -539,6 +539,113 @@ describe('form', function() { expect(parent.$submitted).toBeTruthy(); }); + it('should set $submitted to true on child forms when parent is submitted', function() { + doc = jqLite( + '' + + '' + + '' + + '' + + '' + + ''); + $compile(doc)(scope); + + var parent = scope.parent, + child = scope.child; + + parent.$setSubmitted(); + expect(parent.$submitted).toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + }); + + + it('should not propagate $submitted state on removed child forms when parent is submitted', function() { + doc = jqLite( + '' + + '' + + '' + + '' + + '' + + '' + + ''); + $compile(doc)(scope); + + var parent = scope.parent, + child = scope.child, + grandchild = scope.grandchild, + ggchild = scope.greatgrandchild; + + parent.$removeControl(child); + + parent.$setSubmitted(); + expect(parent.$submitted).toBeTruthy(); + expect(child.$submitted).not.toBeTruthy(); + expect(grandchild.$submitted).not.toBeTruthy(); + + parent.$addControl(child); + + expect(parent.$submitted).toBeTruthy(); + expect(child.$submitted).not.toBeTruthy(); + expect(grandchild.$submitted).not.toBeTruthy(); + + parent.$setSubmitted(); + expect(parent.$submitted).toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + expect(grandchild.$submitted).toBeTruthy(); + + parent.$removeControl(child); + + expect(parent.$submitted).toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + expect(grandchild.$submitted).toBeTruthy(); + + parent.$setPristine(); // sets $submitted to false + expect(parent.$submitted).not.toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + expect(grandchild.$submitted).toBeTruthy(); + + grandchild.$setPristine(); + expect(grandchild.$submitted).not.toBeTruthy(); + + child.$setSubmitted(); + expect(parent.$submitted).not.toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + expect(grandchild.$submitted).toBeTruthy(); + + child.$setPristine(); + expect(parent.$submitted).not.toBeTruthy(); + expect(child.$submitted).not.toBeTruthy(); + expect(grandchild.$submitted).not.toBeTruthy(); + + // Test upwards submission setting + grandchild.$setSubmitted(); + expect(parent.$submitted).not.toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + expect(grandchild.$submitted).toBeTruthy(); + }); + + + it('should set $submitted to true on child and parent forms when form is submitted', function() { + doc = jqLite( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''); + $compile(doc)(scope); + + var parent = scope.parent, + child = scope.child, + grandchild = scope.grandchild; + + child.$setSubmitted(); + + expect(parent.$submitted).toBeTruthy(); + expect(child.$submitted).toBeTruthy(); + expect(grandchild.$submitted).toBeTruthy(); + }); it('should deregister a child form when its DOM is removed', function() { doc = jqLite( From f6e60c14c055960ce85c55d9c819d30eda0b9086 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 12 Dec 2017 13:23:08 +0100 Subject: [PATCH 043/469] docs(ngForm): clarify usage and limitations --- src/ng/directive/form.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js index 2beb0837ecfd..2677921c0d74 100644 --- a/src/ng/directive/form.js +++ b/src/ng/directive/form.js @@ -352,16 +352,21 @@ addSetValidityMethod({ * @restrict EAC * * @description - * Nestable alias of {@link ng.directive:form `form`} directive. HTML - * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a - * sub-group of controls needs to be determined. - * - * Note: the purpose of `ngForm` is to group controls, - * but not to be a replacement for the `
    ` tag with all of its capabilities - * (e.g. posting to the server, ...). - * - * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into - * related scope, under this name. + * Helper directive that makes it possible to create control groups inside a + * {@link ng.directive:form `form`} directive. + * These "child forms" can be used, for example, to determine the validity of a sub-group of + * controls. + * + *
    + * **Note**: `ngForm` cannot be used as a replacement for ``, because it lacks its + * [built-in HTML functionality](https://html.spec.whatwg.org/#the-form-element). + * Specifically, you cannot submit `ngForm` like a `` tag. That means, + * you cannot send data to the server with `ngForm`, or integrate it with + * {@link ng.directive:ngSubmit `ngSubmit`}. + *
    + * + * @param {string=} ngForm|name Name of the form. If specified, the form controller will + * be published into the related scope, under this name. * */ From 0cd39217828b0ad53eaf731576af17d66c18ff60 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 15 Feb 2017 14:20:02 +0100 Subject: [PATCH 044/469] fix(ngScenario): completely remove the angular scenario runner The runner has been deprecated and undocumented since 2014: See commit 8d6d126899d4b1927360599403a7592011243270 Closes #9405 BREAKING CHANGE: The angular scenario runner end-to-end test framework has been removed from the project and will no longer be available on npm or bower starting with 1.7.0. It was deprecated and removed from the documentation in 2014. Applications that still use it should migrate to [Protractor](http://www.protractortest.org). Technically, it should also be possible to continue using an older version of the scenario runner, as the underlying APIs have not changed. However, we do not guarantee future compatibility. --- Gruntfile.js | 12 - angularFiles.js | 27 - package.json | 1 - src/ngScenario/.eslintrc.json | 15 - src/ngScenario/Application.js | 142 ---- src/ngScenario/Describe.js | 155 ----- src/ngScenario/Future.js | 64 -- src/ngScenario/ObjectModel.js | 252 -------- src/ngScenario/Runner.js | 229 ------- src/ngScenario/Scenario.js | 329 ---------- src/ngScenario/SpecRunner.js | 149 ----- src/ngScenario/angular-bootstrap.js | 60 -- src/ngScenario/angular.prefix | 7 - src/ngScenario/angular.suffix | 22 - src/ngScenario/dsl.js | 484 -------------- src/ngScenario/matchers.js | 45 -- src/ngScenario/output/Html.js | 171 ----- src/ngScenario/output/Json.js | 10 - src/ngScenario/output/Object.js | 8 - src/ngScenario/output/Xml.js | 54 -- test/ng/directive/inputSpec.js | 3 +- test/ngScenario/ApplicationSpec.js | 244 ------- test/ngScenario/DescribeSpec.js | 123 ---- test/ngScenario/FutureSpec.js | 77 --- test/ngScenario/ObjectModelSpec.js | 333 ---------- test/ngScenario/RunnerSpec.js | 116 ---- test/ngScenario/ScenarioSpec.js | 44 -- test/ngScenario/SpecRunnerSpec.js | 177 ----- test/ngScenario/dslSpec.js | 789 ----------------------- test/ngScenario/e2e/.eslintrc.json | 9 - test/ngScenario/e2e/Runner-compiled.html | 9 - test/ngScenario/e2e/Runner.html | 10 - test/ngScenario/e2e/style.css | 11 - test/ngScenario/e2e/widgets-scenario.js | 69 -- test/ngScenario/e2e/widgets.html | 99 --- test/ngScenario/matchersSpec.js | 51 -- test/ngScenario/mocks.js | 33 - test/ngScenario/output/HtmlSpec.js | 127 ---- test/ngScenario/output/jsonSpec.js | 37 -- test/ngScenario/output/objectSpec.js | 40 -- test/ngScenario/output/xmlSpec.js | 49 -- yarn.lock | 4 - 42 files changed, 1 insertion(+), 4689 deletions(-) delete mode 100644 src/ngScenario/.eslintrc.json delete mode 100644 src/ngScenario/Application.js delete mode 100644 src/ngScenario/Describe.js delete mode 100644 src/ngScenario/Future.js delete mode 100644 src/ngScenario/ObjectModel.js delete mode 100644 src/ngScenario/Runner.js delete mode 100644 src/ngScenario/Scenario.js delete mode 100644 src/ngScenario/SpecRunner.js delete mode 100644 src/ngScenario/angular-bootstrap.js delete mode 100644 src/ngScenario/angular.prefix delete mode 100644 src/ngScenario/angular.suffix delete mode 100644 src/ngScenario/dsl.js delete mode 100644 src/ngScenario/matchers.js delete mode 100644 src/ngScenario/output/Html.js delete mode 100644 src/ngScenario/output/Json.js delete mode 100644 src/ngScenario/output/Object.js delete mode 100644 src/ngScenario/output/Xml.js delete mode 100644 test/ngScenario/ApplicationSpec.js delete mode 100644 test/ngScenario/DescribeSpec.js delete mode 100644 test/ngScenario/FutureSpec.js delete mode 100644 test/ngScenario/ObjectModelSpec.js delete mode 100644 test/ngScenario/RunnerSpec.js delete mode 100644 test/ngScenario/ScenarioSpec.js delete mode 100644 test/ngScenario/SpecRunnerSpec.js delete mode 100644 test/ngScenario/dslSpec.js delete mode 100644 test/ngScenario/e2e/.eslintrc.json delete mode 100644 test/ngScenario/e2e/Runner-compiled.html delete mode 100644 test/ngScenario/e2e/Runner.html delete mode 100644 test/ngScenario/e2e/style.css delete mode 100644 test/ngScenario/e2e/widgets-scenario.js delete mode 100644 test/ngScenario/e2e/widgets.html delete mode 100644 test/ngScenario/matchersSpec.js delete mode 100644 test/ngScenario/mocks.js delete mode 100644 test/ngScenario/output/HtmlSpec.js delete mode 100644 test/ngScenario/output/jsonSpec.js delete mode 100644 test/ngScenario/output/objectSpec.js delete mode 100644 test/ngScenario/output/xmlSpec.js diff --git a/Gruntfile.js b/Gruntfile.js index ff5eb26de2a0..c9c32ca8d71a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -186,16 +186,6 @@ module.exports = function(grunt) { }, build: { - scenario: { - dest: 'build/angular-scenario.js', - src: [ - 'bower_components/jquery/dist/jquery.js', - util.wrap([files['angularSrc'], files['angularScenario']], 'ngScenario/angular') - ], - styles: { - css: ['css/angular.css', 'css/angular-scenario.css'] - } - }, angular: { dest: 'build/angular.js', src: util.wrap([files['angularSrc']], 'angular'), @@ -281,9 +271,7 @@ module.exports = function(grunt) { files: [ 'src/**/*.js', 'test/**/*.js', - '!test/ngScenario/DescribeSpec.js', '!src/ng/directive/attrs.js', // legitimate xit here - '!src/ngScenario/**/*.js', '!test/helpers/privateMocks*.js' ], options: { diff --git a/angularFiles.js b/angularFiles.js index 4c7b8cc361e4..7c4062d41ad4 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -153,26 +153,8 @@ var angularFiles = { ] }, - 'angularScenario': [ - 'src/ngScenario/Scenario.js', - 'src/ngScenario/Application.js', - 'src/ngScenario/Describe.js', - 'src/ngScenario/Future.js', - 'src/ngScenario/ObjectModel.js', - 'src/ngScenario/Runner.js', - 'src/ngScenario/SpecRunner.js', - 'src/ngScenario/dsl.js', - 'src/ngScenario/matchers.js', - 'src/ngScenario/output/Html.js', - 'src/ngScenario/output/Json.js', - 'src/ngScenario/output/Xml.js', - 'src/ngScenario/output/Object.js' - ], - 'angularTest': [ 'test/helpers/*.js', - 'test/ngScenario/*.js', - 'test/ngScenario/output/*.js', 'test/*.js', 'test/auto/*.js', 'test/ng/**/*.js', @@ -193,22 +175,15 @@ var angularFiles = { 'test/jquery_remove.js', '@angularSrc', '@angularSrcModules', - '@angularScenario', '@angularTest' ], 'karmaExclude': [ 'test/jquery_alias.js', 'src/angular-bootstrap.js', - 'src/ngScenario/angular-bootstrap.js', 'src/angular.bind.js' ], - 'karmaScenario': [ - 'build/angular-scenario.js', - 'build/docs/docs-scenario.js' - ], - 'karmaModules': [ 'build/angular.js', '@angularSrcModules', @@ -231,13 +206,11 @@ var angularFiles = { 'test/jquery_alias.js', '@angularSrc', '@angularSrcModules', - '@angularScenario', '@angularTest' ], 'karmaJqueryExclude': [ 'src/angular-bootstrap.js', - 'src/ngScenario/angular-bootstrap.js', 'test/jquery_remove.js', 'src/angular.bind.js' ] diff --git a/package.json b/package.json index 74335fb4705f..bb62da46e9c6 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "karma-firefox-launcher": "^1.0.1", "karma-jasmine": "^1.1.0", "karma-junit-reporter": "^1.2.0", - "karma-ng-scenario": "^1.0.0", "karma-sauce-launcher": "^1.2.0", "karma-script-launcher": "^1.0.0", "karma-spec-reporter": "^0.0.31", diff --git a/src/ngScenario/.eslintrc.json b/src/ngScenario/.eslintrc.json deleted file mode 100644 index 454d02348435..000000000000 --- a/src/ngScenario/.eslintrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "globals": { - "includes": false, - "asyncForEach": false, - "msie": false, - "browserTrigger": false, - "console": false, - "alert": false, - "_jQuery": false, - "angularInit": false, - "formatException": false, - "$runner": false, - "callerFile": false - } -} diff --git a/src/ngScenario/Application.js b/src/ngScenario/Application.js deleted file mode 100644 index ec5d1626a6bd..000000000000 --- a/src/ngScenario/Application.js +++ /dev/null @@ -1,142 +0,0 @@ -'use strict'; - -/** - * Represents the application currently being tested and abstracts usage - * of iframes or separate windows. - * - * @param {Object} context jQuery wrapper around HTML context. - */ -angular.scenario.Application = function(context) { - this.context = context; - context.append( - '

    Current URL: None

    ' + - '
    ' - ); -}; - -/** - * Gets the jQuery collection of frames. Don't use this directly because - * frames may go stale. - * - * @private - * @return {Object} jQuery collection - */ -angular.scenario.Application.prototype.getFrame_ = function() { - return this.context.find('#test-frames iframe:last'); -}; - -/** - * Gets the window of the test runner frame. Always favor executeAction() - * instead of this method since it prevents you from getting a stale window. - * - * @private - * @return {Object} the window of the frame - */ -angular.scenario.Application.prototype.getWindow_ = function() { - var contentWindow = this.getFrame_().prop('contentWindow'); - if (!contentWindow) { - throw new Error('Frame window is not accessible.'); - } - return contentWindow; -}; - -/** - * Changes the location of the frame. - * - * @param {string} url The URL. If it begins with a # then only the - * hash of the page is changed. - * @param {function()} loadFn function($window, $document) Called when frame loads. - * @param {function()} errorFn function(error) Called if any error when loading. - */ -angular.scenario.Application.prototype.navigateTo = function(url, loadFn, errorFn) { - var self = this; - var frame = self.getFrame_(); - //TODO(esprehn): Refactor to use rethrow() - errorFn = errorFn || function(e) { throw e; }; - if (url === 'about:blank') { - errorFn('Sandbox Error: Navigating to about:blank is not allowed.'); - } else if (url.charAt(0) === '#') { - url = frame.attr('src').split('#')[0] + url; - frame.attr('src', url); - self.executeAction(loadFn); - } else { - frame.remove(); - self.context.find('#test-frames').append('')($rootScope); + + $rootScope.$digest(); + expect(element.attr('src')).toBeUndefined(); + + $rootScope.$apply(function() { + $rootScope.id = $sce.trustAsResourceUrl('http://somewhere/'); + }); + expect(element.attr('src')).toEqual('http://somewhere/'); + })); + + + it('should NOT interpolate a multi-part expression in a `src` attribute that requires a non-MEDIA_URL context', inject(function($compile, $rootScope) { + expect(function() { + element = $compile('')($rootScope); + $rootScope.$apply(function() { + $rootScope.id = 1; + }); + }).toThrowMinErr( + '$interpolate', 'noconcat', 'Error while interpolating: some/{{id}}\nStrict ' + + 'Contextual Escaping disallows interpolations that concatenate multiple expressions ' + + 'when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce'); + })); + + + it('should NOT interpolate a wrongly typed expression', inject(function($compile, $rootScope, $sce) { + expect(function() { + element = $compile('')($rootScope); + $rootScope.$apply(function() { + $rootScope.id = $sce.trustAsUrl('http://somewhere/'); + }); + element.attr('src'); + }).toThrowMinErr( + '$interpolate', 'interr', 'Can\'t interpolate: {{id}}\nError: [$sce:insecurl] Blocked ' + + 'loading resource from url not allowed by $sceDelegate policy. URL: http://somewhere'); + })); }); }); diff --git a/test/ng/directive/ngSrcsetSpec.js b/test/ng/directive/ngSrcsetSpec.js index 15aba45b5749..b8032a77cba5 100644 --- a/test/ng/directive/ngSrcsetSpec.js +++ b/test/ng/directive/ngSrcsetSpec.js @@ -34,5 +34,18 @@ describe('ngSrcset', function() { element = $compile('')($rootScope); $rootScope.$digest(); })); -}); + it('should interpolate the expression and bind to srcset', inject(function($compile, $rootScope) { + var element = $compile('
    ')($rootScope); + + $rootScope.$digest(); + expect(element.attr('srcset')).toBeUndefined(); + + $rootScope.$apply(function() { + $rootScope.id = 1; + }); + expect(element.attr('srcset')).toEqual('some/1 2x'); + + dealoc(element); + })); +}); diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js index 2ed9b31b7f5f..43c5fc4ac275 100644 --- a/test/ng/interpolateSpec.js +++ b/test/ng/interpolateSpec.js @@ -1,5 +1,7 @@ 'use strict'; +/* eslint-disable no-script-url */ + describe('$interpolate', function() { it('should return the interpolation object when there are no bindings and textOnly is undefined', @@ -267,7 +269,9 @@ describe('$interpolate', function() { expect(function() { $interpolate('{{foo}}', true, sce.CSS)(scope); - }).toThrowMinErr('$interpolate', 'interr'); + }).toThrowMinErr( + '$interpolate', 'interr', 'Can\'t interpolate: {{foo}}\nError: [$sce:unsafe] ' + + 'Attempting to use an unsafe value in a safe context.'); })); it('should NOT interpolate mistyped expressions', inject(function($interpolate, $rootScope) { @@ -276,7 +280,9 @@ describe('$interpolate', function() { expect(function() { $interpolate('{{foo}}', true, sce.HTML)(scope); - }).toThrowMinErr('$interpolate', 'interr'); + }).toThrowMinErr( + '$interpolate', 'interr', 'Can\'t interpolate: {{foo}}\nError: [$sce:unsafe] ' + + 'Attempting to use an unsafe value in a safe context.'); })); it('should interpolate trusted expressions in a regular context', inject(function($interpolate) { @@ -291,17 +297,16 @@ describe('$interpolate', function() { // The concatenation of trusted values does not necessarily result in a trusted value. (For // instance, you can construct evil JS code by putting together pieces of JS strings that are by - // themselves safe to execute in isolation.) + // themselves safe to execute in isolation). Therefore, some contexts disable it, such as CSS. it('should NOT interpolate trusted expressions with multiple parts', inject(function($interpolate) { var foo = sce.trustAsCss('foo'); var bar = sce.trustAsCss('bar'); expect(function() { return $interpolate('{{foo}}{{bar}}', true, sce.CSS)({foo: foo, bar: bar}); }).toThrowMinErr( - '$interpolate', 'noconcat', 'Error while interpolating: {{foo}}{{bar}}\n' + + '$interpolate', 'interr', 'Error while interpolating: {{foo}}{{bar}}\n' + 'Strict Contextual Escaping disallows interpolations that concatenate multiple ' + - 'expressions when a trusted value is required. See ' + - 'http://docs.angularjs.org/api/ng.$sce'); + 'expressions when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce'); })); }); @@ -380,26 +385,32 @@ describe('$interpolate', function() { describe('isTrustedContext', function() { - it('should NOT interpolate a multi-part expression when isTrustedContext is true', inject(function($interpolate) { - var isTrustedContext = true; + it('should NOT interpolate a multi-part expression when isTrustedContext is RESOURCE_URL', inject(function($sce, $interpolate) { + var isTrustedContext = $sce.RESOURCE_URL; expect(function() { - $interpolate('constant/{{var}}', true, isTrustedContext); + $interpolate('constant/{{var}}', true, isTrustedContext)('val'); }).toThrowMinErr( - '$interpolate', 'noconcat', 'Error while interpolating: constant/{{var}}\nStrict ' + - 'Contextual Escaping disallows interpolations that concatenate multiple expressions ' + - 'when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce'); + '$interpolate', 'interr', + 'Can\'t interpolate: constant/{{var}}\nError: [$interpolate:noconcat] Error while ' + + 'interpolating: constant/{{var}}\nStrict Contextual Escaping disallows interpolations ' + + 'that concatenate multiple expressions when a trusted value is required. ' + + 'See http://docs.angularjs.org/api/ng.$sce'); expect(function() { - $interpolate('{{var}}/constant', true, isTrustedContext); + $interpolate('{{var}}/constant', true, isTrustedContext)('val'); }).toThrowMinErr( - '$interpolate', 'noconcat', 'Error while interpolating: {{var}}/constant\nStrict ' + - 'Contextual Escaping disallows interpolations that concatenate multiple expressions ' + - 'when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce'); - expect(function() { - $interpolate('{{foo}}{{bar}}', true, isTrustedContext); + '$interpolate', 'interr', + 'Can\'t interpolate: {{var}}/constant\nError: [$interpolate:noconcat] Error while ' + + 'interpolating: {{var}}/constant\nStrict Contextual Escaping disallows interpolations ' + + 'that concatenate multiple expressions when a trusted value is required. ' + + 'See http://docs.angularjs.org/api/ng.$sce'); + expect(function() { + $interpolate('{{foo}}{{bar}}', true, isTrustedContext)('val'); }).toThrowMinErr( - '$interpolate', 'noconcat', 'Error while interpolating: {{foo}}{{bar}}\nStrict ' + - 'Contextual Escaping disallows interpolations that concatenate multiple expressions ' + - 'when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce'); + '$interpolate', 'interr', + 'Can\'t interpolate: {{foo}}{{bar}}\nError: [$interpolate:noconcat] Error while ' + + 'interpolating: {{foo}}{{bar}}\nStrict Contextual Escaping disallows interpolations ' + + 'that concatenate multiple expressions when a trusted value is required. ' + + 'See http://docs.angularjs.org/api/ng.$sce'); })); it('should interpolate a multi-part expression when isTrustedContext is false', inject(function($interpolate) { @@ -407,6 +418,23 @@ describe('$interpolate', function() { expect($interpolate('some/{{id}}')({id: 1})).toEqual('some/1'); expect($interpolate('{{foo}}{{bar}}')({foo: 1, bar: 2})).toEqual('12'); })); + + + it('should interpolate a multi-part expression when isTrustedContext is URL', inject(function($sce, $interpolate) { + expect($interpolate('some/{{id}}', true, $sce.URL)({})).toEqual('some/'); + expect($interpolate('some/{{id}}', true, $sce.URL)({id: 1})).toEqual('some/1'); + expect($interpolate('{{foo}}{{bar}}', true, $sce.URL)({foo: 1, bar: 2})).toEqual('12'); + })); + + + it('should interpolate and sanitize a multi-part expression when isTrustedContext is URL', inject(function($sce, $interpolate) { + expect($interpolate('some/{{id}}', true, $sce.URL)({})).toEqual('some/'); + expect($interpolate('some/{{id}}', true, $sce.URL)({id: 'javascript:'})).toEqual('some/javascript:'); + expect($interpolate('{{foo}}{{bar}}', true, $sce.URL)({foo: 'javascript:', bar: 'javascript:'})).toEqual('unsafe:javascript:javascript:'); + })); + + + }); diff --git a/test/ng/sceSpecs.js b/test/ng/sceSpecs.js index f7c654df296a..fb169925c9ff 100644 --- a/test/ng/sceSpecs.js +++ b/test/ng/sceSpecs.js @@ -1,5 +1,7 @@ 'use strict'; +/* eslint-disable no-script-url */ + describe('SCE', function() { describe('when disabled', function() { @@ -211,7 +213,7 @@ describe('SCE', function() { expect($sce.parseAsJs('"string"')()).toBe('string'); })); - it('should be possible to do one-time binding', function() { + it('should be possible to do one-time binding on a non-concatenable context', function() { module(provideLog); inject(function($sce, $rootScope, log) { $rootScope.$watch($sce.parseAsHtml('::foo'), function(value) { @@ -236,6 +238,31 @@ describe('SCE', function() { }); }); + it('should be possible to do one-time binding on a concatenable context', function() { + module(provideLog); + inject(function($sce, $rootScope, log) { + $rootScope.$watch($sce.parseAsUrl('::foo'), function(value) { + log(value + ''); + }); + + $rootScope.$digest(); + expect(log).toEqual('undefined'); // initial listener call + log.reset(); + + $rootScope.foo = $sce.trustAs($sce.URL, 'trustedValue'); + expect($rootScope.$$watchers.length).toBe(1); + $rootScope.$digest(); + + expect($rootScope.$$watchers.length).toBe(0); + expect(log).toEqual('trustedValue'); + log.reset(); + + $rootScope.foo = $sce.trustAs($sce.URL, 'anotherTrustedValue'); + $rootScope.$digest(); + expect(log).toEqual(''); // watcher no longer active + }); + }); + it('should NOT parse constant non-literals', inject(function($sce) { // Until there's a real world use case for this, we're disallowing // constant non-literals. See $SceParseProvider. @@ -525,6 +552,44 @@ describe('SCE', function() { )); }); + describe('URL-context sanitization', function() { + it('should sanitize values that are not whitelisted', inject(function($sce) { + expect($sce.getTrustedMediaUrl('javascript:foo')).toEqual('unsafe:javascript:foo'); + expect($sce.getTrustedUrl('javascript:foo')).toEqual('unsafe:javascript:foo'); + })); + + it('should not sanitize values that are whitelisted', inject(function($sce) { + expect($sce.getTrustedMediaUrl('http://example.com/')).toEqual('http://example.com/'); + expect($sce.getTrustedUrl('http://example.com/')).toEqual('http://example.com/'); + })); + + it('should not sanitize trusted values', inject(function($sce) { + expect($sce.getTrustedMediaUrl($sce.trustAsMediaUrl('javascript:foo'))).toEqual('javascript:foo'); + expect($sce.getTrustedMediaUrl($sce.trustAsUrl('javascript:foo'))).toEqual('javascript:foo'); + expect($sce.getTrustedMediaUrl($sce.trustAsResourceUrl('javascript:foo'))).toEqual('javascript:foo'); + + expect($sce.getTrustedUrl($sce.trustAsMediaUrl('javascript:foo'))).toEqual('unsafe:javascript:foo'); + expect($sce.getTrustedUrl($sce.trustAsUrl('javascript:foo'))).toEqual('javascript:foo'); + expect($sce.getTrustedUrl($sce.trustAsResourceUrl('javascript:foo'))).toEqual('javascript:foo'); + })); + + it('should use the $$sanitizeUri', function() { + var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl'); + module(function($provide) { + $provide.value('$$sanitizeUri', $$sanitizeUri); + }); + inject(function($sce) { + expect($sce.getTrustedMediaUrl('someUrl')).toEqual('someSanitizedUrl'); + expect($$sanitizeUri).toHaveBeenCalledOnceWith('someUrl', true); + + $$sanitizeUri.calls.reset(); + + expect($sce.getTrustedUrl('someUrl')).toEqual('someSanitizedUrl'); + expect($$sanitizeUri).toHaveBeenCalledOnceWith('someUrl', false); + }); + }); + }); + describe('sanitizing html', function() { describe('when $sanitize is NOT available', function() { it('should throw an exception for getTrusted(string) values', inject(function($sce) { @@ -535,9 +600,23 @@ describe('SCE', function() { describe('when $sanitize is available', function() { beforeEach(function() { module('ngSanitize'); }); + it('should sanitize html using $sanitize', inject(function($sce) { expect($sce.getTrustedHtml('abc')).toBe('abc'); })); + + // Note: that test only passes if HTML is added to the concatenable contexts list. + // See isConcatenableSecureContext in interpolate.js for that. + // + // if (!msie || msie >= 11) { + // it('can set dynamic srcdocs with concatenations and sanitize the result', + // inject(function($compile, $rootScope) { + // var element = $compile('')($rootScope); + // $rootScope.html = 'noyes'; + // $rootScope.$digest(); + // expect(angular.lowercase(element.attr('srcdoc'))).toEqual('yes'); + // })); + // } }); }); }); diff --git a/test/ngSanitize/sanitizeSpec.js b/test/ngSanitize/sanitizeSpec.js index 812ca4fa867d..69cb6abc9fda 100644 --- a/test/ngSanitize/sanitizeSpec.js +++ b/test/ngSanitize/sanitizeSpec.js @@ -495,7 +495,7 @@ describe('HTML', function() { }); }); - it('should use $$sanitizeUri for links', function() { + it('should use $$sanitizeUri for a[href] links', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); @@ -511,7 +511,7 @@ describe('HTML', function() { }); }); - it('should use $$sanitizeUri for links', function() { + it('should use $$sanitizeUri for img[src] links', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); From 3fc684352815108688de411717d11239f4d789c9 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 29 Jan 2018 13:30:45 +0100 Subject: [PATCH 086/469] chore(code.angularjs.org): improve output of directory listing --- .../functions/index.js | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/scripts/code.angularjs.org-firebase/functions/index.js b/scripts/code.angularjs.org-firebase/functions/index.js index 4bf6c80d84e6..90f02fc786eb 100644 --- a/scripts/code.angularjs.org-firebase/functions/index.js +++ b/scripts/code.angularjs.org-firebase/functions/index.js @@ -31,7 +31,7 @@ function sendStoredFile(request, response) { } if (!fileName) { - //Root + // Root return getDirectoryListing('/').catch(sendErrorResponse); } @@ -111,6 +111,11 @@ function sendStoredFile(request, response) { return getContent(getFilesOptions).then(() => { let contentList = ''; + if (path === '/') { + // Let the latest versions appear first + directoryList.reverse(); + } + directoryList.forEach(directoryPath => { const dirName = directoryPath.split('/').reverse()[1]; contentList += `${dirName}/
    `; @@ -125,11 +130,20 @@ function sendStoredFile(request, response) { // without trailing slash const base = request.originalUrl.endsWith('/') ? request.originalUrl : request.originalUrl + '/'; - let directoryListing = ` - -

    Index of ${path}

    -
    -
    ${contentList}
    `; + const directoryListing = ` + + + + + + + + +

    Index of ${path}

    +
    +
    ${contentList}
    + + `; return response .status(200) From 67f54b660038de2b4346b3e76d66a8dc8ccb1f9b Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 1 Feb 2018 10:31:32 +0100 Subject: [PATCH 087/469] fix(ngTouch): deprecate the module and its contents Closes #16427 Closes #16431 --- src/ngTouch/directive/ngSwipe.js | 10 ++++++++++ src/ngTouch/swipe.js | 5 +++++ src/ngTouch/touch.js | 7 +++++++ 3 files changed, 22 insertions(+) diff --git a/src/ngTouch/directive/ngSwipe.js b/src/ngTouch/directive/ngSwipe.js index e05632044747..5f31fa96470c 100644 --- a/src/ngTouch/directive/ngSwipe.js +++ b/src/ngTouch/directive/ngSwipe.js @@ -6,6 +6,11 @@ * @ngdoc directive * @name ngSwipeLeft * + * @deprecated + * sinceVersion="1.7.0" + * + * See the {@link ngTouch module} documentation for more information. + * * @description * Specify custom behavior when an element is swiped to the left on a touchscreen device. * A leftward swipe is a quick, right-to-left slide of the finger. @@ -42,6 +47,11 @@ * @ngdoc directive * @name ngSwipeRight * + * @deprecated + * sinceVersion="1.7.0" + * + * See the {@link ngTouch module} documentation for more information. + * * @description * Specify custom behavior when an element is swiped to the right on a touchscreen device. * A rightward swipe is a quick, left-to-right slide of the finger. diff --git a/src/ngTouch/swipe.js b/src/ngTouch/swipe.js index 013eea3dc6bc..617747f77fab 100644 --- a/src/ngTouch/swipe.js +++ b/src/ngTouch/swipe.js @@ -6,6 +6,11 @@ * @ngdoc service * @name $swipe * + * @deprecated + * sinceVersion="1.7.0" + * + * See the {@link ngTouch module} documentation for more information. + * * @description * The `$swipe` service is a service that abstracts the messier details of hold-and-drag swipe * behavior, to make implementing swipe-related directives more convenient. diff --git a/src/ngTouch/touch.js b/src/ngTouch/touch.js index 676f6f4a6c9b..d0c2745a876b 100644 --- a/src/ngTouch/touch.js +++ b/src/ngTouch/touch.js @@ -11,6 +11,13 @@ * * See {@link ngTouch.$swipe `$swipe`} for usage. * + * @deprecated + * sinceVersion="1.7.0" + * The ngTouch module with the {@link ngTouch.$swipe `$swipe`} service and + * the {@link ngTouch.ngSwipeLeft} and {@link ngTouch.ngSwipeRight} directives are + * deprecated. Instead, stand-alone libraries for touch handling and gesture interaction + * should be used, for example [HammerJS](https://hammerjs.github.io/) (which is also used by + * Angular). */ // define ngTouch module From e3ece2fad9e1e6d47b5f06815ff186d7e6f44948 Mon Sep 17 00:00:00 2001 From: Georgii Dolzhykov Date: Thu, 22 Dec 2016 19:17:06 +0300 Subject: [PATCH 088/469] feat(isArray): support Array subclasses in `angular.isArray()` Closes #15533 Closes #15541 BREAKING CHANGE: Previously, `angular.isArray()` was an alias for `Array.isArray()`. Therefore, objects that prototypally inherit from `Array` where not considered arrays. Now such objects are considered arrays too. This change affects several other methods that use `angular.isArray()` under the hood, such as `angular.copy()`, `angular.equals()`, `angular.forEach()`, and `angular.merge()`. This in turn affects how dirty checking treats objects that prototypally inherit from `Array` (e.g. MobX observable arrays). AngularJS will now be able to handle these objects better when copying or watching. --- src/Angular.js | 9 +++++---- test/AngularSpec.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 7c424897ff18..f5ab043dc8a3 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -219,8 +219,7 @@ function isArrayLike(obj) { // NodeList objects (with `item` method) and // other objects with suitable length characteristics are array-like - return isNumber(length) && - (length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item === 'function'); + return isNumber(length) && (length >= 0 && (length - 1) in obj || typeof obj.item === 'function'); } @@ -635,12 +634,14 @@ function isDate(value) { * @kind function * * @description - * Determines if a reference is an `Array`. Alias of Array.isArray. + * Determines if a reference is an `Array`. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. */ -var isArray = Array.isArray; +function isArray(arr) { + return Array.isArray(arr) || arr instanceof Array; +} /** * @description diff --git a/test/AngularSpec.js b/test/AngularSpec.js index c10b92e01179..ffe157de589f 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -1254,6 +1254,37 @@ describe('angular', function() { }); }); + describe('isArray', function() { + + it('should return true if passed an `Array`', function() { + expect(isArray([])).toBe(true); + }); + + it('should return true if passed an `Array` from a different window context', function() { + var iframe = document.createElement('iframe'); + document.body.appendChild(iframe); // No `contentWindow` if not attached to the DOM. + var arr = new iframe.contentWindow.Array(); + document.body.removeChild(iframe); // Clean up. + + expect(arr instanceof Array).toBe(false); + expect(isArray(arr)).toBe(true); + }); + + it('should return true if passed an object prototypically inherited from `Array`', function() { + function FooArray() {} + FooArray.prototype = []; + + expect(isArray(new FooArray())).toBe(true); + }); + + it('should return false if passed non-array objects', function() { + expect(isArray(document.body.childNodes)).toBe(false); + expect(isArray({length: 0})).toBe(false); + expect(isArray({length: 2, 0: 'one', 1: 'two'})).toBe(false); + }); + + }); + describe('isArrayLike', function() { it('should return false if passed a number', function() { From 16b82c6afe0ab916fef1d6ca78053b00bf5ada83 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 2 Feb 2018 10:02:06 +0100 Subject: [PATCH 089/469] fix($animate): let cancel() reject the runner promise Closes #14204 Closes #16373 BREAKING CHANGE: $animate.cancel(runner) now rejects the underlying promise and calls the catch() handler on the runner returned by $animate functions (enter, leave, move, addClass, removeClass, setClass, animate). Previously it would resolve the promise as if the animation had ended successfully. Example: ```js var runner = $animate.addClass('red'); runner.then(function() { console.log('success')}); runner.catch(function() { console.log('cancelled')}); runner.cancel(); ``` Pre-1.7.0, this logs 'success', 1.7.0 and later it logs 'cancelled'. To migrate, add a catch() handler to your animation runners. --- src/ng/animate.js | 88 +++++++++++++--- test/ngAnimate/animateSpec.js | 190 ++++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 12 deletions(-) diff --git a/src/ng/animate.js b/src/ng/animate.js index 1f9bc9028cf0..60a9bc3d04a9 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -464,13 +464,77 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { * @ngdoc method * @name $animate#cancel * @kind function - * @description Cancels the provided animation. - * - * @param {Promise} animationPromise The animation promise that is returned when an animation is started. + * @description Cancels the provided animation and applies the end state of the animation. + * Note that this does not cancel the underlying operation, e.g. the setting of classes or + * adding the element to the DOM. + * + * @param {animationRunner} animationRunner An animation runner returned by an $animate function. + * + * @example + + + angular.module('animationExample', ['ngAnimate']).component('cancelExample', { + templateUrl: 'template.html', + controller: function($element, $animate) { + this.runner = null; + + this.addClass = function() { + this.runner = $animate.addClass($element.find('div'), 'red'); + var ctrl = this; + this.runner.finally(function() { + ctrl.runner = null; + }); + }; + + this.removeClass = function() { + this.runner = $animate.removeClass($element.find('div'), 'red'); + var ctrl = this; + this.runner.finally(function() { + ctrl.runner = null; + }); + }; + + this.cancel = function() { + $animate.cancel(this.runner); + }; + } + }); + + +

    + + +
    + +
    +

    CSS-Animated Text
    +

    +
    + + + + + .red-add, .red-remove { + transition: all 4s cubic-bezier(0.250, 0.460, 0.450, 0.940); + } + + .red, + .red-add.red-add-active { + color: #FF0000; + font-size: 40px; + } + + .red-remove.red-remove-active { + font-size: 10px; + color: black; + } + + +
    */ cancel: function(runner) { - if (runner.end) { - runner.end(); + if (runner.cancel) { + runner.cancel(); } }, @@ -496,7 +560,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * - * @return {Promise} the animation callback promise + * @return {Runner} the animation runner */ enter: function(element, parent, after, options) { parent = parent && jqLite(parent); @@ -528,7 +592,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * - * @return {Promise} the animation callback promise + * @return {Runner} the animation runner */ move: function(element, parent, after, options) { parent = parent && jqLite(parent); @@ -555,7 +619,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * - * @return {Promise} the animation callback promise + * @return {Runner} the animation runner */ leave: function(element, options) { return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() { @@ -585,7 +649,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * - * @return {Promise} the animation callback promise + * @return {Runner} animationRunner the animation runner */ addClass: function(element, className, options) { options = prepareAnimateOptions(options); @@ -615,7 +679,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * - * @return {Promise} the animation callback promise + * @return {Runner} the animation runner */ removeClass: function(element, className, options) { options = prepareAnimateOptions(options); @@ -646,7 +710,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * - * @return {Promise} the animation callback promise + * @return {Runner} the animation runner */ setClass: function(element, add, remove, options) { options = prepareAnimateOptions(options); @@ -693,7 +757,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * - * @return {Promise} the animation callback promise + * @return {Runner} the animation runner */ animate: function(element, from, to, className, options) { options = prepareAnimateOptions(options); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index e8c0131a8a16..484836ca66e0 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -790,6 +790,7 @@ describe('animations', function() { expect(element).toHaveClass('red'); })); + it('removeClass() should issue a removeClass animation with the correct DOM operation', inject(function($animate, $rootScope) { parent.append(element); element.addClass('blue'); @@ -934,6 +935,195 @@ describe('animations', function() { })); }); + + describe('$animate.cancel()', function() { + + it('should cancel enter()', inject(function($animate, $rootScope) { + expect(parent.children().length).toBe(0); + + options.foo = 'bar'; + var spy = jasmine.createSpy('cancelCatch'); + + var runner = $animate.enter(element, parent, null, options); + + runner.catch(spy); + + expect(parent.children().length).toBe(1); + + $rootScope.$digest(); + + expect(capturedAnimation[0]).toBe(element); + expect(capturedAnimation[1]).toBe('enter'); + expect(capturedAnimation[2].foo).toEqual(options.foo); + + $animate.cancel(runner); + // Since enter() immediately adds the element, we can only check if the + // element is still at the position + expect(parent.children().length).toBe(1); + + $rootScope.$digest(); + + // Catch handler is called after digest + expect(spy).toHaveBeenCalled(); + })); + + + it('should cancel move()', inject(function($animate, $rootScope) { + parent.append(element); + + expect(parent.children().length).toBe(1); + expect(parent2.children().length).toBe(0); + + options.foo = 'bar'; + var spy = jasmine.createSpy('cancelCatch'); + + var runner = $animate.move(element, parent2, null, options); + runner.catch(spy); + + expect(parent.children().length).toBe(0); + expect(parent2.children().length).toBe(1); + + $rootScope.$digest(); + + expect(capturedAnimation[0]).toBe(element); + expect(capturedAnimation[1]).toBe('move'); + expect(capturedAnimation[2].foo).toEqual(options.foo); + + $animate.cancel(runner); + // Since moves() immediately moves the element, we can only check if the + // element is still at the correct position + expect(parent.children().length).toBe(0); + expect(parent2.children().length).toBe(1); + + $rootScope.$digest(); + + // Catch handler is called after digest + expect(spy).toHaveBeenCalled(); + })); + + + it('cancel leave()', inject(function($animate, $rootScope) { + parent.append(element); + options.foo = 'bar'; + var spy = jasmine.createSpy('cancelCatch'); + + var runner = $animate.leave(element, options); + + runner.catch(spy); + $rootScope.$digest(); + + expect(capturedAnimation[0]).toBe(element); + expect(capturedAnimation[1]).toBe('leave'); + expect(capturedAnimation[2].foo).toEqual(options.foo); + + expect(element.parent().length).toBe(1); + + $animate.cancel(runner); + // Animation concludes immediately + expect(element.parent().length).toBe(0); + expect(spy).not.toHaveBeenCalled(); + + $rootScope.$digest(); + // Catch handler is called after digest + expect(spy).toHaveBeenCalled(); + })); + + it('should cancel addClass()', inject(function($animate, $rootScope) { + parent.append(element); + options.foo = 'bar'; + var runner = $animate.addClass(element, 'red', options); + var spy = jasmine.createSpy('cancelCatch'); + + runner.catch(spy); + $rootScope.$digest(); + + expect(capturedAnimation[0]).toBe(element); + expect(capturedAnimation[1]).toBe('addClass'); + expect(capturedAnimation[2].foo).toEqual(options.foo); + + $animate.cancel(runner); + expect(element).toHaveClass('red'); + expect(spy).not.toHaveBeenCalled(); + + $rootScope.$digest(); + expect(spy).toHaveBeenCalled(); + })); + + + it('should cancel setClass()', inject(function($animate, $rootScope) { + parent.append(element); + element.addClass('red'); + options.foo = 'bar'; + + var runner = $animate.setClass(element, 'blue', 'red', options); + var spy = jasmine.createSpy('cancelCatch'); + + runner.catch(spy); + $rootScope.$digest(); + + expect(capturedAnimation[0]).toBe(element); + expect(capturedAnimation[1]).toBe('setClass'); + expect(capturedAnimation[2].foo).toEqual(options.foo); + + $animate.cancel(runner); + expect(element).toHaveClass('blue'); + expect(element).not.toHaveClass('red'); + expect(spy).not.toHaveBeenCalled(); + + $rootScope.$digest(); + expect(spy).toHaveBeenCalled(); + })); + + + it('should cancel removeClass()', inject(function($animate, $rootScope) { + parent.append(element); + element.addClass('red blue'); + + options.foo = 'bar'; + var runner = $animate.removeClass(element, 'red', options); + var spy = jasmine.createSpy('cancelCatch'); + + runner.catch(spy); + $rootScope.$digest(); + + expect(capturedAnimation[0]).toBe(element); + expect(capturedAnimation[1]).toBe('removeClass'); + expect(capturedAnimation[2].foo).toEqual(options.foo); + + $animate.cancel(runner); + expect(element).not.toHaveClass('red'); + expect(element).toHaveClass('blue'); + + $rootScope.$digest(); + expect(spy).toHaveBeenCalled(); + })); + + + it('should cancel animate()', + inject(function($animate, $rootScope) { + + parent.append(element); + + var fromStyle = { color: 'blue' }; + var options = { addClass: 'red' }; + + var runner = $animate.animate(element, fromStyle, null, null, options); + var spy = jasmine.createSpy('cancelCatch'); + + runner.catch(spy); + $rootScope.$digest(); + + expect(capturedAnimation).toBeTruthy(); + + $animate.cancel(runner); + expect(element).toHaveClass('red'); + + $rootScope.$digest(); + expect(spy).toHaveBeenCalled(); + })); + }); + + describe('parent animations', function() { they('should not cancel a pre-digest parent class-based animation if a child $prop animation is set to run', ['structural', 'class-based'], function(animationType) { From b969c3e3540d05a781404beebecdd4fa4ceb2d2e Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 2 Feb 2018 11:19:32 +0100 Subject: [PATCH 090/469] docs(changelog): add changes for 1.6.9 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fb3c1064bea..40b8fc4d4a87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ + +# 1.6.9 fiery-basilisk (2018-02-02) + + +## Bug Fixes +- **input:** add `drop` event support for IE + ([5dc076](https://github.com/angular/angular.js/commit/5dc07667de00c5e85fd69c5b7b7fe4fb5fd65a77)) +- **ngMessages:** prevent memory leak from messages that are never attached + ([9d058d](https://github.com/angular/angular.js/commit/9d058de04bb78694b83179e9b97bc40214eca01a), + [#16389](https://github.com/angular/angular.js/issues/16389), + [#16404](https://github.com/angular/angular.js/issues/16404), + [#16406](https://github.com/angular/angular.js/issues/16406)) +- **ngTransclude:** remove terminal: true + ([1d826e](https://github.com/angular/angular.js/commit/1d826e2f1e941d14c3c56d7a0249f5796ba11f85), + [#16411](https://github.com/angular/angular.js/issues/16411), + [#16412](https://github.com/angular/angular.js/issues/16412)) +- **$sanitize:** sanitize `xml:base` attributes + ([b9ef65](https://github.com/angular/angular.js/commit/b9ef6585e10477fbbf912a971fe0b390bca692a6)) + + +## New Features +- **currencyFilter:** trim whitespace around an empty currency symbol + ([367390](https://github.com/angular/angular.js/commit/3673909896efb6ff47546caf7fc61549f193e043), + [#15018](https://github.com/angular/angular.js/issues/15018), + [#15085](https://github.com/angular/angular.js/issues/15085), + [#15105](https://github.com/angular/angular.js/issues/15105)) + + # 1.6.8 beneficial-tincture (2017-12-18) From d3bffc547697c8f2059f32402c9f02092c1a8b5c Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 2 Feb 2018 12:31:49 +0100 Subject: [PATCH 091/469] chore(docs.angularjs.org): add robots.txt --- docs/app/assets/robots.txt | 11 +++++++++++ scripts/docs.angularjs.org-firebase/firebase.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 docs/app/assets/robots.txt diff --git a/docs/app/assets/robots.txt b/docs/app/assets/robots.txt new file mode 100644 index 000000000000..a0de3bfda6b4 --- /dev/null +++ b/docs/app/assets/robots.txt @@ -0,0 +1,11 @@ +User-agent: * + +Disallow: /components/ +Disallow: /examples/ +Disallow: /img/ +Disallow: /js/ +Disallow: /partials/ +Disallow: /ptore2e/ +Disallow: /*.js$ +Disallow: /*.map$ +Disallow: /Error404.html diff --git a/scripts/docs.angularjs.org-firebase/firebase.json b/scripts/docs.angularjs.org-firebase/firebase.json index 8f82fc2d8e9c..5f5d70dc02d6 100644 --- a/scripts/docs.angularjs.org-firebase/firebase.json +++ b/scripts/docs.angularjs.org-firebase/firebase.json @@ -23,7 +23,7 @@ "destination": "/index-production.html" }, { - "source": "**/*!(.jpg|.jpeg|.gif|.png|.html|.js|.map|.json|.css|.svg|.ttf|.woff|.woff2|.eot)", + "source": "**/*!(.jpg|.jpeg|.gif|.png|.html|.js|.map|.json|.css|.svg|.ttf|.txt|.woff|.woff2|.eot)", "destination": "/index-production.html" } ] From fb00991460cf69ae8bc7f1f826363d09c73c0d5e Mon Sep 17 00:00:00 2001 From: frederikprijck Date: Sun, 4 Feb 2018 10:20:46 +0100 Subject: [PATCH 092/469] fix($templateRequest): always return the template that is stored in the cache Previously, `$templateRequest` returned the raw `$http` response data on the first request for a template and then the value from the cache for subsequent requests. If the value is transformed when being added to the cache (by decorating `$templateCache.put`) the return value of `$templateRequest` would be inconsistent depending upon when the request is made. This commit ensures the cached value is returned instead of the raw `$http` response data, thus allowing the `$templateCache` service to be decorated. Closes #16225 --- src/ng/templateRequest.js | 3 +-- test/ng/templateRequestSpec.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/ng/templateRequest.js b/src/ng/templateRequest.js index 7b3b04261e56..ff699d6cd0ef 100644 --- a/src/ng/templateRequest.js +++ b/src/ng/templateRequest.js @@ -99,8 +99,7 @@ function $TemplateRequestProvider() { handleRequestFn.totalPendingRequests--; }) .then(function(response) { - $templateCache.put(tpl, response.data); - return response.data; + return $templateCache.put(tpl, response.data); }, handleError); function handleError(resp) { diff --git a/test/ng/templateRequestSpec.js b/test/ng/templateRequestSpec.js index cb9c1c6f6ce8..3ca323613103 100644 --- a/test/ng/templateRequestSpec.js +++ b/test/ng/templateRequestSpec.js @@ -114,6 +114,24 @@ describe('$templateRequest', function() { expect($templateCache.get('tpl.html')).toBe('matias'); })); + it('should return the cached value on the first request', + inject(function($rootScope, $templateRequest, $templateCache, $httpBackend) { + + $httpBackend.expectGET('tpl.html').respond('matias'); + spyOn($templateCache, 'put').and.returnValue('_matias'); + + var content = []; + function tplRequestCb(html) { + content.push(html); + } + + $templateRequest('tpl.html').then(tplRequestCb); + $rootScope.$digest(); + $httpBackend.flush(); + + expect(content[0]).toBe('_matias'); + })); + it('should call `$exceptionHandler` on request error', function() { module(function($exceptionHandlerProvider) { $exceptionHandlerProvider.mode('log'); From fbe679dfbcb2108249931d44f452d09da6c98477 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 5 Feb 2018 12:35:12 +0100 Subject: [PATCH 093/469] chore(doc-gen): generate sitemap.xml --- docs/config/index.js | 1 + docs/config/processors/sitemap.js | 25 +++++++++++++++++++ .../config/templates/app/sitemap.template.xml | 7 ++++++ 3 files changed, 33 insertions(+) create mode 100644 docs/config/processors/sitemap.js create mode 100644 docs/config/templates/app/sitemap.template.xml diff --git a/docs/config/index.js b/docs/config/index.js index ab5e45a3f8dc..4ddf7922c7bd 100644 --- a/docs/config/index.js +++ b/docs/config/index.js @@ -31,6 +31,7 @@ module.exports = new Package('angularjs', [ .processor(require('./processors/keywords')) .processor(require('./processors/pages-data')) .processor(require('./processors/versions-data')) +.processor(require('./processors/sitemap')) .config(function(dgeni, log, readFilesProcessor, writeFilesProcessor) { diff --git a/docs/config/processors/sitemap.js b/docs/config/processors/sitemap.js new file mode 100644 index 000000000000..aea84da9a17a --- /dev/null +++ b/docs/config/processors/sitemap.js @@ -0,0 +1,25 @@ +'use strict'; + +var exclusionRegex = /^index|examples\/|ptore2e\//; + +module.exports = function createSitemap() { + return { + $runAfter: ['paths-computed'], + $runBefore: ['rendering-docs'], + $process: function(docs) { + docs.push({ + id: 'sitemap.xml', + path: 'sitemap.xml', + outputPath: '../sitemap.xml', + template: 'sitemap.template.xml', + urls: docs.filter(function(doc) { + return doc.path && + doc.outputPath && + !exclusionRegex.test(doc.outputPath); + }).map(function(doc) { + return doc.path; + }) + }); + } + }; +}; diff --git a/docs/config/templates/app/sitemap.template.xml b/docs/config/templates/app/sitemap.template.xml new file mode 100644 index 000000000000..56953d903920 --- /dev/null +++ b/docs/config/templates/app/sitemap.template.xml @@ -0,0 +1,7 @@ + + + {%- for url in doc.urls %} + + https://docs.angularjs.org/{$ url $} + {% endfor %} + \ No newline at end of file From ea04dbb229ec69a7ffc954d57496f058d6ce6dcb Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 5 Feb 2018 12:38:44 +0100 Subject: [PATCH 094/469] chore(code.angularjs.org): fix robots.txt - allow all-versions-data.js in snapshot, which is used by docs.angularjs.org - disallow access to folders like docs-0.9.2 etc which are used by early versions --- scripts/code.angularjs.org-firebase/public/robots.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/code.angularjs.org-firebase/public/robots.txt b/scripts/code.angularjs.org-firebase/public/robots.txt index 480082428fa1..e83f2c10453d 100644 --- a/scripts/code.angularjs.org-firebase/public/robots.txt +++ b/scripts/code.angularjs.org-firebase/public/robots.txt @@ -1,5 +1,6 @@ User-agent: * -Disallow: /*docs/ +Disallow: /*docs*/ Disallow: /*i18n/ Disallow: /*.zip$ +Allow: /snapshot/docs/js/all-versions-data.js From 7d50b2e9eea9c7e8950beffc1e139c4e05af00f5 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 5 Feb 2018 13:07:07 +0100 Subject: [PATCH 095/469] chore(docs.angularjs.org): allow robots to access js and css Otherwise, the google bot cannot execute the JS --- docs/app/assets/robots.txt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/app/assets/robots.txt b/docs/app/assets/robots.txt index a0de3bfda6b4..c00cb3c20320 100644 --- a/docs/app/assets/robots.txt +++ b/docs/app/assets/robots.txt @@ -1,11 +1,9 @@ User-agent: * -Disallow: /components/ Disallow: /examples/ Disallow: /img/ -Disallow: /js/ Disallow: /partials/ Disallow: /ptore2e/ -Disallow: /*.js$ -Disallow: /*.map$ +Disallow: /*.js$ # The js files in the root are used by the embedded examples, not by the app itself +Disallow: /*.map$ # The map files in the root are used by the embedded examples, not by the app itself Disallow: /Error404.html From 8d6ac5f3178cb6ead6b3b7526c50cd1c07112097 Mon Sep 17 00:00:00 2001 From: Maksim Ryzhikov Date: Sat, 11 Nov 2017 16:18:17 +0300 Subject: [PATCH 096/469] feat($sanitize): support enhancing elements/attributes white-lists Fixes #5900 Closes #16326 --- src/ngSanitize/sanitize.js | 139 +++++++++++++++++++++++++++++--- test/ngSanitize/sanitizeSpec.js | 50 ++++++++++++ 2 files changed, 176 insertions(+), 13 deletions(-) diff --git a/src/ngSanitize/sanitize.js b/src/ngSanitize/sanitize.js index b08850fba065..48ddad82341c 100644 --- a/src/ngSanitize/sanitize.js +++ b/src/ngSanitize/sanitize.js @@ -15,6 +15,7 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); var bind; var extend; var forEach; +var isArray; var isDefined; var lowercase; var noop; @@ -144,9 +145,11 @@ var htmlSanitizeWriter; * Creates and configures {@link $sanitize} instance. */ function $SanitizeProvider() { + var hasBeenInstantiated = false; var svgEnabled = false; this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + hasBeenInstantiated = true; if (svgEnabled) { extend(validElements, svgElements); } @@ -187,7 +190,7 @@ function $SanitizeProvider() { * * * @param {boolean=} flag Enable or disable SVG support in the sanitizer. - * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called + * @returns {boolean|$sanitizeProvider} Returns the currently configured value if called * without an argument or self for chaining otherwise. */ this.enableSvg = function(enableSvg) { @@ -199,6 +202,105 @@ function $SanitizeProvider() { } }; + + /** + * @ngdoc method + * @name $sanitizeProvider#addValidElements + * @kind function + * + * @description + * Extends the built-in lists of valid HTML/SVG elements, i.e. elements that are considered safe + * and are not stripped off during sanitization. You can extend the following lists of elements: + * + * - `htmlElements`: A list of elements (tag names) to extend the current list of safe HTML + * elements. HTML elements considered safe will not be removed during sanitization. All other + * elements will be stripped off. + * + * - `htmlVoidElements`: This is similar to `htmlElements`, but marks the elements as + * "void elements" (similar to HTML + * [void elements](https://rawgit.com/w3c/html/html5.1-2/single-page.html#void-elements)). These + * elements have no end tag and cannot have content. + * + * - `svgElements`: This is similar to `htmlElements`, but for SVG elements. This list is only + * taken into account if SVG is {@link ngSanitize.$sanitizeProvider#enableSvg enabled} for + * `$sanitize`. + * + *
    + * This method must be called during the {@link angular.Module#config config} phase. Once the + * `$sanitize` service has been instantiated, this method has no effect. + *
    + * + *
    + * Keep in mind that extending the built-in lists of elements may expose your app to XSS or + * other vulnerabilities. Be very mindful of the elements you add. + *
    + * + * @param {Array|Object} elements - A list of valid HTML elements or an object with one or + * more of the following properties: + * - **htmlElements** - `{Array}` - A list of elements to extend the current list of + * HTML elements. + * - **htmlVoidElements** - `{Array}` - A list of elements to extend the current list of + * void HTML elements; i.e. elements that do not have an end tag. + * - **svgElements** - `{Array}` - A list of elements to extend the current list of SVG + * elements. The list of SVG elements is only taken into account if SVG is + * {@link ngSanitize.$sanitizeProvider#enableSvg enabled} for `$sanitize`. + * + * Passing an array (`[...]`) is equivalent to passing `{htmlElements: [...]}`. + * + * @return {$sanitizeProvider} Returns self for chaining. + */ + this.addValidElements = function(elements) { + if (!hasBeenInstantiated) { + if (isArray(elements)) { + elements = {htmlElements: elements}; + } + + addElementsTo(svgElements, elements.svgElements); + addElementsTo(voidElements, elements.htmlVoidElements); + addElementsTo(validElements, elements.htmlVoidElements); + addElementsTo(validElements, elements.htmlElements); + } + + return this; + }; + + + /** + * @ngdoc method + * @name $sanitizeProvider#addValidAttrs + * @kind function + * + * @description + * Extends the built-in list of valid attributes, i.e. attributes that are considered safe and are + * not stripped off during sanitization. + * + * **Note**: + * The new attributes will not be treated as URI attributes, which means their values will not be + * sanitized as URIs using `$compileProvider`'s + * {@link ng.$compileProvider#aHrefSanitizationWhitelist aHrefSanitizationWhitelist} and + * {@link ng.$compileProvider#imgSrcSanitizationWhitelist imgSrcSanitizationWhitelist}. + * + *
    + * This method must be called during the {@link angular.Module#config config} phase. Once the + * `$sanitize` service has been instantiated, this method has no effect. + *
    + * + *
    + * Keep in mind that extending the built-in list of attributes may expose your app to XSS or + * other vulnerabilities. Be very mindful of the attributes you add. + *
    + * + * @param {Array} attrs - A list of valid attributes. + * + * @returns {$sanitizeProvider} Returns self for chaining. + */ + this.addValidAttrs = function(attrs) { + if (!hasBeenInstantiated) { + extend(validAttrs, arrayToMap(attrs, true)); + } + return this; + }; + ////////////////////////////////////////////////////////////////////////////////////////////////// // Private stuff ////////////////////////////////////////////////////////////////////////////////////////////////// @@ -206,6 +308,7 @@ function $SanitizeProvider() { bind = angular.bind; extend = angular.extend; forEach = angular.forEach; + isArray = angular.isArray; isDefined = angular.isDefined; lowercase = angular.$$lowercase; noop = angular.noop; @@ -230,23 +333,23 @@ function $SanitizeProvider() { // Safe Void Elements - HTML5 // http://dev.w3.org/html5/spec/Overview.html#void-elements - var voidElements = toMap('area,br,col,hr,img,wbr'); + var voidElements = stringToMap('area,br,col,hr,img,wbr'); // Elements that you can, intentionally, leave open (and which close themselves) // http://dev.w3.org/html5/spec/Overview.html#optional-tags - var optionalEndTagBlockElements = toMap('colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr'), - optionalEndTagInlineElements = toMap('rp,rt'), + var optionalEndTagBlockElements = stringToMap('colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr'), + optionalEndTagInlineElements = stringToMap('rp,rt'), optionalEndTagElements = extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); // Safe Block Elements - HTML5 - var blockElements = extend({}, optionalEndTagBlockElements, toMap('address,article,' + + var blockElements = extend({}, optionalEndTagBlockElements, stringToMap('address,article,' + 'aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' + 'h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul')); // Inline Elements - HTML5 - var inlineElements = extend({}, optionalEndTagInlineElements, toMap('a,abbr,acronym,b,' + + var inlineElements = extend({}, optionalEndTagInlineElements, stringToMap('a,abbr,acronym,b,' + 'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,' + 'samp,small,span,strike,strong,sub,sup,time,tt,u,var')); @@ -254,12 +357,12 @@ function $SanitizeProvider() { // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted. // They can potentially allow for arbitrary javascript to be executed. See #11290 - var svgElements = toMap('circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,' + + var svgElements = stringToMap('circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,' + 'hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,' + 'radialGradient,rect,stop,svg,switch,text,title,tspan'); // Blocked Elements (will be stripped) - var blockedElements = toMap('script,style'); + var blockedElements = stringToMap('script,style'); var validElements = extend({}, voidElements, @@ -268,9 +371,9 @@ function $SanitizeProvider() { optionalEndTagElements); //Attributes that have href and hence need to be sanitized - var uriAttrs = toMap('background,cite,href,longdesc,src,xlink:href,xml:base'); + var uriAttrs = stringToMap('background,cite,href,longdesc,src,xlink:href,xml:base'); - var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + + var htmlAttrs = stringToMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' + @@ -278,7 +381,7 @@ function $SanitizeProvider() { // SVG attributes (without "id" and "name" attributes) // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes - var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + + var svgAttrs = stringToMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' + 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' + 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' + @@ -299,14 +402,24 @@ function $SanitizeProvider() { svgAttrs, htmlAttrs); - function toMap(str, lowercaseKeys) { - var obj = {}, items = str.split(','), i; + function stringToMap(str, lowercaseKeys) { + return arrayToMap(str.split(','), lowercaseKeys); + } + + function arrayToMap(items, lowercaseKeys) { + var obj = {}, i; for (i = 0; i < items.length; i++) { obj[lowercaseKeys ? lowercase(items[i]) : items[i]] = true; } return obj; } + function addElementsTo(elementsMap, newElements) { + if (newElements && newElements.length) { + extend(elementsMap, arrayToMap(newElements)); + } + } + /** * Create an inert document that contains the dirty HTML that needs sanitizing * Depending upon browser support we use one of three strategies for doing this. diff --git a/test/ngSanitize/sanitizeSpec.js b/test/ngSanitize/sanitizeSpec.js index 69cb6abc9fda..a047be989642 100644 --- a/test/ngSanitize/sanitizeSpec.js +++ b/test/ngSanitize/sanitizeSpec.js @@ -293,10 +293,56 @@ describe('HTML', function() { expect(doc).toEqual('

    '); })); + describe('Custom white-list support', function() { + + var $sanitizeProvider; + beforeEach(module(function(_$sanitizeProvider_) { + $sanitizeProvider = _$sanitizeProvider_; + + $sanitizeProvider.addValidElements(['foo']); + $sanitizeProvider.addValidElements({ + htmlElements: ['foo-button', 'foo-video'], + htmlVoidElements: ['foo-input'], + svgElements: ['foo-svg'] + }); + $sanitizeProvider.addValidAttrs(['foo']); + })); + + it('should allow custom white-listed element', function() { + expectHTML('').toEqual(''); + expectHTML('').toEqual(''); + expectHTML('').toEqual(''); + }); + + it('should allow custom white-listed void element', function() { + expectHTML('').toEqual(''); + }); + + it('should allow custom white-listed void element to be used with closing tag', function() { + expectHTML('').toEqual(''); + }); + + it('should allow custom white-listed attribute', function() { + expectHTML('').toEqual(''); + }); + + it('should ignore custom white-listed SVG element if SVG disabled', function() { + expectHTML('').toEqual(''); + }); + + it('should not allow add custom element after service has been instantiated', inject(function($sanitize) { + $sanitizeProvider.addValidElements(['bar']); + expectHTML('').toEqual(''); + })); + }); + describe('SVG support', function() { beforeEach(module(function($sanitizeProvider) { $sanitizeProvider.enableSvg(true); + $sanitizeProvider.addValidElements({ + svgElements: ['font-face-uri'] + }); })); it('should accept SVG tags', function() { @@ -314,6 +360,10 @@ describe('HTML', function() { }); + it('should allow custom white-listed SVG element', function() { + expectHTML('').toEqual(''); + }); + it('should sanitize SVG xlink:href attribute values', function() { expectHTML('') .toBeOneOf('', From 02f4ca4887f337e87ce668f657c32f49e18beec8 Mon Sep 17 00:00:00 2001 From: frederikprijck Date: Mon, 30 Jan 2017 22:46:22 +0100 Subject: [PATCH 097/469] docs(ngClass): add docs regarding animation for `ngClassEven` and `ngClassOdd` Previously, the documentation has no information regarding using `ngAnimate` together with the `ngClassEven` and `ngClassOdd` directives. This commit adds the same docs used by the `ngClass` directive to the `ngClassEven` and `ngClassOdd` docs and adds an extra example for both `ngClassEven` and `ngClassOdd` that showcases animations. Closes #15654 --- docs/content/guide/animations.ngdoc | 7 +- src/ng/directive/ngClass.js | 124 ++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 3 deletions(-) diff --git a/docs/content/guide/animations.ngdoc b/docs/content/guide/animations.ngdoc index a13661a36a68..22e4df094839 100644 --- a/docs/content/guide/animations.ngdoc +++ b/docs/content/guide/animations.ngdoc @@ -229,11 +229,12 @@ triggered: | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | | {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave | | {@link ng.directive:ngClass#animations ngClass / {{class}​}} | add and remove | -| {@link ng.directive:ngClass#animations ngClassEven / ngClassOdd} | add and remove | +| {@link ng.directive:ngClassEven#animations ngClassEven} | add and remove | +| {@link ng.directive:ngClassOdd#animations ngClassOdd} | add and remove | | {@link ng.directive:ngHide#animations ngHide} | add and remove (the `ng-hide` class) | | {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) | -| {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) | -| {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) | +| {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) | +| {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) | | {@link module:ngMessages#animations ngMessages} | add and remove (the `ng-active`/`ng-inactive` classes) | For a full breakdown of the steps involved during each animation event, refer to the diff --git a/src/ng/directive/ngClass.js b/src/ng/directive/ngClass.js index e38c7c141938..7b1ca13b2915 100644 --- a/src/ng/directive/ngClass.js +++ b/src/ng/directive/ngClass.js @@ -338,6 +338,12 @@ var ngClassDirective = classDirective('', true); * This directive can be applied only within the scope of an * {@link ng.directive:ngRepeat ngRepeat}. * + * @animations + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element | + * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element | + * * @element ANY * @param {expression} ngClassOdd {@link guide/expression Expression} to eval. The result * of the evaluation can be a string representing space delimited class names or an array. @@ -370,6 +376,62 @@ var ngClassDirective = classDirective('', true); }); + * + *
    + * @example + * An example on how to implement animations using `ngClassOdd`: + * + + +
    + +
    + + + + +
    {{ item }}
    +
    +
    + + .odd { + background: rgba(255, 255, 0, 0.25); + } + + .odd-add, .odd-remove { + transition: 1.5s; + } + + + it('should add new entries to the beginning of the list', function() { + var button = element(by.buttonText('Add item')); + var rows = element.all(by.repeater('item in items')); + + expect(rows.count()).toBe(4); + expect(rows.get(0).getText()).toBe('Item 3'); + expect(rows.get(1).getText()).toBe('Item 2'); + + button.click(); + + expect(rows.count()).toBe(5); + expect(rows.get(0).getText()).toBe('Item 4'); + expect(rows.get(1).getText()).toBe('Item 3'); + }); + + it('should add odd class to odd entries', function() { + var button = element(by.buttonText('Add item')); + var rows = element.all(by.repeater('item in items')); + + expect(rows.get(0).getAttribute('class')).toMatch(/odd/); + expect(rows.get(1).getAttribute('class')).not.toMatch(/odd/); + + button.click(); + + expect(rows.get(0).getAttribute('class')).toMatch(/odd/); + expect(rows.get(1).getAttribute('class')).not.toMatch(/odd/); + }); + +
    */ var ngClassOddDirective = classDirective('Odd', 0); @@ -386,6 +448,12 @@ var ngClassOddDirective = classDirective('Odd', 0); * This directive can be applied only within the scope of an * {@link ng.directive:ngRepeat ngRepeat}. * + * @animations + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element | + * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element | + * * @element ANY * @param {expression} ngClassEven {@link guide/expression Expression} to eval. The * result of the evaluation can be a string representing space delimited class names or an array. @@ -418,5 +486,61 @@ var ngClassOddDirective = classDirective('Odd', 0); }); + * + *
    + * @example + * An example on how to implement animations using `ngClassEven`: + * + + +
    + +
    + + + + +
    {{ item }}
    +
    +
    + + .even { + background: rgba(255, 255, 0, 0.25); + } + + .even-add, .even-remove { + transition: 1.5s; + } + + + it('should add new entries to the beginning of the list', function() { + var button = element(by.buttonText('Add item')); + var rows = element.all(by.repeater('item in items')); + + expect(rows.count()).toBe(4); + expect(rows.get(0).getText()).toBe('Item 3'); + expect(rows.get(1).getText()).toBe('Item 2'); + + button.click(); + + expect(rows.count()).toBe(5); + expect(rows.get(0).getText()).toBe('Item 4'); + expect(rows.get(1).getText()).toBe('Item 3'); + }); + + it('should add even class to even entries', function() { + var button = element(by.buttonText('Add item')); + var rows = element.all(by.repeater('item in items')); + + expect(rows.get(0).getAttribute('class')).not.toMatch(/even/); + expect(rows.get(1).getAttribute('class')).toMatch(/even/); + + button.click(); + + expect(rows.get(0).getAttribute('class')).not.toMatch(/even/); + expect(rows.get(1).getAttribute('class')).toMatch(/even/); + }); + +
    */ var ngClassEvenDirective = classDirective('Even', 1); From e4e2024d1c0860b142a915fab3f7beff975aa048 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 8 Feb 2018 17:59:36 +0100 Subject: [PATCH 098/469] chore(code.angularjs.org): increase the cache duration This is already set, but wasn't checked in --- scripts/code.angularjs.org-firebase/functions/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/code.angularjs.org-firebase/functions/index.js b/scripts/code.angularjs.org-firebase/functions/index.js index 90f02fc786eb..33f7ba04df7d 100644 --- a/scripts/code.angularjs.org-firebase/functions/index.js +++ b/scripts/code.angularjs.org-firebase/functions/index.js @@ -6,8 +6,8 @@ const path = require('path'); const gcsBucketId = `${process.env.GCLOUD_PROJECT}.appspot.com`; -const BROWSER_CACHE_DURATION = 300; -const CDN_CACHE_DURATION = 600; +const BROWSER_CACHE_DURATION = 60 * 10; +const CDN_CACHE_DURATION = 60 * 60 * 12; function sendStoredFile(request, response) { let filePathSegments = request.path.split('/').filter((segment) => { From 3ecb00115a95bce3add50680b452436fbf213d8c Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 5 Feb 2018 19:26:19 +0100 Subject: [PATCH 099/469] chore(docs.angularjs.org): serve xml files (sitemap) --- scripts/docs.angularjs.org-firebase/firebase.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/docs.angularjs.org-firebase/firebase.json b/scripts/docs.angularjs.org-firebase/firebase.json index 5f5d70dc02d6..91f9a778b634 100644 --- a/scripts/docs.angularjs.org-firebase/firebase.json +++ b/scripts/docs.angularjs.org-firebase/firebase.json @@ -23,7 +23,7 @@ "destination": "/index-production.html" }, { - "source": "**/*!(.jpg|.jpeg|.gif|.png|.html|.js|.map|.json|.css|.svg|.ttf|.txt|.woff|.woff2|.eot)", + "source": "**/*!(.jpg|.jpeg|.gif|.png|.html|.js|.map|.json|.css|.svg|.ttf|.txt|.woff|.woff2|.eot|.xml)", "destination": "/index-production.html" } ] From 3e7a87d7d1be2afea16d0863040fe51e26326d6f Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 12 Jan 2018 13:30:05 +0100 Subject: [PATCH 100/469] fix(browserTrigger): support CompositionEvent --- src/ngMock/browserTrigger.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/ngMock/browserTrigger.js b/src/ngMock/browserTrigger.js index 196772d1e3e9..eb80e15964dc 100644 --- a/src/ngMock/browserTrigger.js +++ b/src/ngMock/browserTrigger.js @@ -132,6 +132,24 @@ evnt.keyCode = eventData.keyCode; evnt.charCode = eventData.charCode; evnt.which = eventData.which; + } else if (/composition/.test(eventType)) { + try { + evnt = new window.CompositionEvent(eventType, { + data: eventData.data + }); + } catch (e) { + // Support: IE9+ + evnt = window.document.createEvent('CompositionEvent', {}); + evnt.initCompositionEvent( + eventType, + eventData.bubbles, + eventData.cancelable, + window, + eventData.data, + null + ); + } + } else { evnt = window.document.createEvent('MouseEvents'); x = x || 0; From 2789ccbcf9666b64211c08b188b168175109f563 Mon Sep 17 00:00:00 2001 From: Jae Ik Lee Date: Tue, 26 Dec 2017 03:11:51 +0900 Subject: [PATCH 101/469] fix(input): fix composition mode in IE for Korean input Fixes #6656 Closes #16273 --- src/ng/directive/input.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 7d1bec7cfe9d..3b841d25a348 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1248,6 +1248,16 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { composing = true; }); + // Support: IE9+ + element.on('compositionupdate', function(ev) { + // End composition when ev.data is empty string on 'compositionupdate' event. + // When the input de-focusses (e.g. by clicking away), IE triggers 'compositionupdate' + // instead of 'compositionend'. + if (isUndefined(ev.data) || ev.data === '') { + composing = false; + } + }); + element.on('compositionend', function() { composing = false; listener(); From c484213180470c2a8044eeb09dfdc5ef11099bc8 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 12 Jan 2018 13:30:33 +0100 Subject: [PATCH 102/469] test(input): add test for IE composition bug --- test/ng/directive/inputSpec.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 93d2184f969d..8c86b44ceada 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -134,6 +134,20 @@ describe('input', function() { browserTrigger(inputElm, 'compositionend'); expect($rootScope.name).toEqual('caitp'); }); + + + it('should end composition on "compositionupdate" when event.data is ""', function() { + // This tests a bug workaround for IE9-11 + // During composition, when an input is de-focussed by clicking away from it, + // the compositionupdate event is called with '', followed by a change event. + var inputElm = helper.compileInput(''); + browserTrigger(inputElm, 'compositionstart'); + helper.changeInputValueTo('caitp'); + expect($rootScope.name).toBeUndefined(); + browserTrigger(inputElm, 'compositionupdate', {data: ''}); + browserTrigger(inputElm, 'change'); + expect($rootScope.name).toEqual('caitp'); + }); }); From 9a0f1abddac5670a0d6efa7689f04d594bb9f274 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 8 Feb 2018 23:43:00 +0100 Subject: [PATCH 103/469] chore(docs.angularjs.org): deploy sitemap.xml Closes #16445 --- Gruntfile.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 6d5b9e09b3df..08ff823f0145 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -162,7 +162,8 @@ module.exports = function(grunt) { clean: { build: ['build'], - tmp: ['tmp'] + tmp: ['tmp'], + deploy: ['uploadDocs', 'uploadCode'] }, eslint: { @@ -326,7 +327,7 @@ module.exports = function(grunt) { files: [ // The source files are needed by the embedded examples in the docs app. { - src: 'build/angular*.{js,js.map,min.js}', + src: ['build/angular*.{js,js.map,min.js}', 'build/sitemap.xml'], dest: 'uploadDocs/', expand: true, flatten: true @@ -363,10 +364,6 @@ module.exports = function(grunt) { }, shell: { - // Travis expects the firebase.json in the repository root, but we have it in a sub-folder - 'symlink-firebase-docs': { - command: 'ln -s ./scripts/docs.angularjs.org-firebase/firebase.json ./firebase.json' - }, 'install-node-dependencies': { command: 'yarn' }, From 32b1a0c5807a6b3148543676b920543e32162c69 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 25 Jan 2018 19:35:04 +0100 Subject: [PATCH 104/469] chore(docs.angularjs.org): actually fix deployment When a file is symlinked, relative paths obviously aren't correct anymore. This error was masked because Travis didn't fail the job when Firebase couldn't find the public folder. To fix, we copy the file and adjust the folder path --- Gruntfile.js | 2 +- lib/grunt/plugins.js | 5 +++++ lib/grunt/utils.js | 11 +++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 08ff823f0145..19732e7e1842 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -463,7 +463,7 @@ module.exports = function(grunt) { 'package', 'compress:deployFirebaseCode', 'copy:deployFirebaseCode', - 'shell:symlink-firebase-docs', + 'firebaseDocsJsonForTravis', 'copy:deployFirebaseDocs' ]); grunt.registerTask('default', ['package']); diff --git a/lib/grunt/plugins.js b/lib/grunt/plugins.js index 2d71b85501eb..7e5dc290b533 100644 --- a/lib/grunt/plugins.js +++ b/lib/grunt/plugins.js @@ -62,4 +62,9 @@ module.exports = function(grunt) { grunt.registerTask('collect-errors', 'Combine stripped error files', function() { util.collectErrors(); }); + + grunt.registerTask('firebaseDocsJsonForTravis', function() { + util.firebaseDocsJsonForTravis(); + }); + }; diff --git a/lib/grunt/utils.js b/lib/grunt/utils.js index 52dbcde8fa5e..80e529186514 100644 --- a/lib/grunt/utils.js +++ b/lib/grunt/utils.js @@ -292,5 +292,16 @@ module.exports = { } next(); }; + }, + + // Our Firebase projects are in subfolders, but Travis expects them in the root, + // so we need to modify the upload folder path and copy the file into the root + firebaseDocsJsonForTravis: function() { + var fileName = 'scripts/docs.angularjs.org-firebase/firebase.json'; + var json = grunt.file.readJSON(fileName); + + json.hosting.public = 'uploadDocs'; + + grunt.file.write('firebase.json', JSON.stringify(json)); } }; From a7a9688962039fb5af5e073faddbc239a9479e9b Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Sun, 11 Feb 2018 18:15:23 +0200 Subject: [PATCH 105/469] fix(docs): fix `@media` breakpoints for small/extra small devices Previously, our custom styles used `@media` breakpoints for small/extra small devices that were off-by-one from [Bootstrap breakpoints](https://getbootstrap.com/docs/3.3/css/#responsive-utilities-classes) (767px vs 768px). This caused the site to not be displayed correctly on these exact sizes, which affected for example all iPad devices (whose screens are exactly 768px wide). This commit fixes it by making our breakpoints match those of Bootstrap. Fixes #16448 Closes #16449 --- docs/app/assets/css/docs.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/app/assets/css/docs.css b/docs/app/assets/css/docs.css index ba69449090eb..54d81ada753d 100644 --- a/docs/app/assets/css/docs.css +++ b/docs/app/assets/css/docs.css @@ -713,14 +713,14 @@ ul.events > li { margin-right: 5px; } -@media only screen and (min-width: 769px) { +@media only screen and (min-width: 768px) { [ng-include="partialPath"].ng-hide { display: block !important; visibility: hidden; } } -@media only screen and (min-width: 769px) and (max-width: 991px) { +@media only screen and (min-width: 768px) and (max-width: 991px) { .main-body-grid { margin-top: 160px; } @@ -729,7 +729,7 @@ ul.events > li { } } -@media only screen and (max-width : 768px) { +@media only screen and (max-width: 767px) { .picker, .picker select { width: auto; display: block; From 9f1793fd2dc92225991f9db5ec661af6f86206ca Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 12 Feb 2018 12:26:32 +0100 Subject: [PATCH 106/469] chore(eslint): allow ES6 for node scripts --- .eslintrc-node.json | 9 +++++++-- Gruntfile.js | 1 + package.json | 1 + yarn.lock | 4 ++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.eslintrc-node.json b/.eslintrc-node.json index 643f345d88e0..c16a8a883837 100644 --- a/.eslintrc-node.json +++ b/.eslintrc-node.json @@ -1,8 +1,13 @@ { "extends": "./.eslintrc-base.json", - "env": { "browser": false, "node": true - } + }, + "parserOptions": { + "ecmaVersion": 2017 + }, + "plugins": [ + "promise" + ] } diff --git a/Gruntfile.js b/Gruntfile.js index 19732e7e1842..0d6dc6b956be 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -174,6 +174,7 @@ module.exports = function(grunt) { 'docs/**/*.js', 'lib/**/*.js', 'scripts/**/*.js', + '!scripts/*/*/node_modules/**', 'src/**/*.js', 'test/**/*.js', 'i18n/**/*.js', diff --git a/package.json b/package.json index 255b569f30a6..c82bb19a829f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "cz-conventional-changelog": "1.1.4", "dgeni": "^0.4.0", "dgeni-packages": "^0.16.4", + "eslint-plugin-promise": "^3.6.0", "event-stream": "~3.1.0", "glob": "^6.0.1", "google-code-prettify": "1.0.1", diff --git a/yarn.lock b/yarn.lock index 62016649857b..f635a238b14d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1994,6 +1994,10 @@ escope@^3.6.0: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-plugin-promise@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.6.0.tgz#54b7658c8f454813dc2a870aff8152ec4969ba75" + eslint@^3.0.0: version "3.15.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.15.0.tgz#bdcc6a6c5ffe08160e7b93c066695362a91e30f2" From e1e2100e66acf79bafed7e07fbbdf7d865b2b035 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 9 Feb 2018 20:18:29 +0100 Subject: [PATCH 107/469] chore(code.angularjs.org): don't gzip compressed image files --- Gruntfile.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 0d6dc6b956be..5cc2dd778620 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -315,12 +315,12 @@ module.exports = function(grunt) { }, deployFirebaseCode: { files: [ - // the zip file should not be compressed again. + // copy files that are not handled by compress { - src: 'build/*.zip', + cwd: 'build', + src: '**/*.{zip,jpg,jpeg,png}', dest: 'uploadCode/' + deployVersion + '/', - expand: true, - flatten: true + expand: true } ] }, @@ -357,7 +357,8 @@ module.exports = function(grunt) { options: { mode: 'gzip' }, - src: ['**', '!*.zip'], + // Already compressed files should not be compressed again + src: ['**', '!**/*.{zip,png,jpeg,jpg}'], cwd: 'build', expand: true, dest: 'uploadCode/' + deployVersion + '/' From 9196c80c3394d0afcfc4f3ab0ec881d501190fe3 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 12 Feb 2018 10:47:42 +0100 Subject: [PATCH 108/469] chore(deploy): rename deploy folders --- .gitignore | 1 + .travis.yml | 2 +- Gruntfile.js | 17 +++++++++++------ lib/grunt/utils.js | 6 ++++-- .../docs.angularjs.org-firebase/firebase.json | 2 +- scripts/travis/build.sh | 2 +- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 2da934151e89..588beda3f172 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /build/ +/deploy/ /benchpress-build/ .DS_Store gen_docs.disable diff --git a/.travis.yml b/.travis.yml index 4c226f02fe85..3199f7718d4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -94,7 +94,7 @@ jobs: secret_access_key: secure: tHIFdSq55qkyZf9zT/3+VkhUrTvOTMuswxXU3KyWaBrSieZqG0UnUDyNm+n3lSfX95zEl/+rJAWbfvhVSxZi13ndOtvRF+MdI1cvow2JynP0aDSiPffEvVrZOmihD6mt2SlMfhskr5FTduQ69kZG6DfLcve1PPDaIwnbOv3phb8= bucket: code-angularjs-org-338b8.appspot.com - local-dir: uploadCode + local-dir: deploy/code detect_encoding: true # detects gzip compression on: repo: angular/angular.js diff --git a/Gruntfile.js b/Gruntfile.js index 5cc2dd778620..c42113fc2ee4 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -163,7 +163,10 @@ module.exports = function(grunt) { clean: { build: ['build'], tmp: ['tmp'], - deploy: ['uploadDocs', 'uploadCode'] + deploy: [ + 'deploy/docs', + 'deploy/code' + ] }, eslint: { @@ -319,7 +322,7 @@ module.exports = function(grunt) { { cwd: 'build', src: '**/*.{zip,jpg,jpeg,png}', - dest: 'uploadCode/' + deployVersion + '/', + dest: 'deploy/code/' + deployVersion + '/', expand: true } ] @@ -329,14 +332,16 @@ module.exports = function(grunt) { // The source files are needed by the embedded examples in the docs app. { src: ['build/angular*.{js,js.map,min.js}', 'build/sitemap.xml'], - dest: 'uploadDocs/', + dest: 'deploy/docs/', expand: true, flatten: true }, { cwd: 'build/docs', src: '**', - dest: 'uploadDocs/', + dest: 'deploy/docs/', + expand: true + } expand: true } ] @@ -361,7 +366,7 @@ module.exports = function(grunt) { src: ['**', '!**/*.{zip,png,jpeg,jpg}'], cwd: 'build', expand: true, - dest: 'uploadCode/' + deployVersion + '/' + dest: 'deploy/code/' + deployVersion + '/' } }, @@ -461,7 +466,7 @@ module.exports = function(grunt) { 'merge-conflict', 'eslint' ]); - grunt.registerTask('prepareFirebaseDeploy', [ + grunt.registerTask('prepareDeploy', [ 'package', 'compress:deployFirebaseCode', 'copy:deployFirebaseCode', diff --git a/lib/grunt/utils.js b/lib/grunt/utils.js index 80e529186514..c5b16946ba8d 100644 --- a/lib/grunt/utils.js +++ b/lib/grunt/utils.js @@ -297,10 +297,12 @@ module.exports = { // Our Firebase projects are in subfolders, but Travis expects them in the root, // so we need to modify the upload folder path and copy the file into the root firebaseDocsJsonForTravis: function() { - var fileName = 'scripts/docs.angularjs.org-firebase/firebase.json'; + var docsScriptFolder = 'scripts/docs.angularjs.org-firebase/'; + + var fileName = docsScriptFolder + 'firebase.json'; var json = grunt.file.readJSON(fileName); - json.hosting.public = 'uploadDocs'; + json.hosting.public = 'deploy/docs'; grunt.file.write('firebase.json', JSON.stringify(json)); } diff --git a/scripts/docs.angularjs.org-firebase/firebase.json b/scripts/docs.angularjs.org-firebase/firebase.json index 91f9a778b634..abb70329f815 100644 --- a/scripts/docs.angularjs.org-firebase/firebase.json +++ b/scripts/docs.angularjs.org-firebase/firebase.json @@ -1,6 +1,6 @@ { "hosting": { - "public": "../../uploadDocs", + "public": "../../deploy/docs", "ignore": [ "/index.html", "/index-debug.html", diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index fdf94d8f13de..980855388492 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -79,7 +79,7 @@ case "$JOB" in fi if [[ "$DEPLOY_DOCS" == true || "$DEPLOY_CODE" == true ]]; then - grunt prepareFirebaseDeploy + grunt prepareDeploy else echo "Skipping deployment build because conditions have not been met." fi From 75bf199421011466b799b2eeec5223c0a9143516 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 12 Feb 2018 10:49:19 +0100 Subject: [PATCH 109/469] chore(docs.angularjs.org): serve snapshots for googlebot This commit restores serving the plain partials (content) when a docs page is accessed with ?_escaped_fragment_=. The Google Ajax Crawler accesses these urls when the page has `` is set. During the migration to Firebase, this was lost, which resulted in Google dropping the docs almost completely from the index. We are using a Firebase cloud function to serve the partials. Since we cannot access the static hosted files from the function, we have to deploy them as part of the function directory instead, from which they can be read. Related to #16432 Related to #16417 --- Gruntfile.js | 17 +- lib/grunt/utils.js | 5 +- .../docs.angularjs.org-firebase/firebase.json | 11 +- .../functions/index.js | 49 + .../functions/package-lock.json | 4265 +++++++++++++++++ .../functions/package.json | 21 + .../readme.firebase.docs.md | 17 +- 7 files changed, 4376 insertions(+), 9 deletions(-) create mode 100644 scripts/docs.angularjs.org-firebase/functions/index.js create mode 100644 scripts/docs.angularjs.org-firebase/functions/package-lock.json create mode 100644 scripts/docs.angularjs.org-firebase/functions/package.json diff --git a/Gruntfile.js b/Gruntfile.js index c42113fc2ee4..ba5efa0d8f09 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -13,6 +13,8 @@ var semver = require('semver'); var exec = require('shelljs').exec; var pkg = require(__dirname + '/package.json'); +var docsScriptFolder = 'scripts/docs.angularjs.org-firebase'; + // Node.js version checks if (!semver.satisfies(process.version, pkg.engines.node)) { reportOrFail('Invalid node version (' + process.version + '). ' + @@ -165,7 +167,8 @@ module.exports = function(grunt) { tmp: ['tmp'], deploy: [ 'deploy/docs', - 'deploy/code' + 'deploy/code', + docsScriptFolder + '/functions/html' ] }, @@ -341,7 +344,17 @@ module.exports = function(grunt) { src: '**', dest: 'deploy/docs/', expand: true - } + }, + { + src: ['build/docs/index-production.html'], + dest: docsScriptFolder + '/functions/content', + expand: true, + flatten: true + }, + { + cwd: 'build/docs', + src: 'partials/**', + dest: docsScriptFolder + '/functions/content', expand: true } ] diff --git a/lib/grunt/utils.js b/lib/grunt/utils.js index c5b16946ba8d..64ddc6f185ec 100644 --- a/lib/grunt/utils.js +++ b/lib/grunt/utils.js @@ -297,12 +297,13 @@ module.exports = { // Our Firebase projects are in subfolders, but Travis expects them in the root, // so we need to modify the upload folder path and copy the file into the root firebaseDocsJsonForTravis: function() { - var docsScriptFolder = 'scripts/docs.angularjs.org-firebase/'; + var docsScriptFolder = 'scripts/docs.angularjs.org-firebase'; - var fileName = docsScriptFolder + 'firebase.json'; + var fileName = docsScriptFolder + '/firebase.json'; var json = grunt.file.readJSON(fileName); json.hosting.public = 'deploy/docs'; + json.functions.source = docsScriptFolder + '/functions'; grunt.file.write('firebase.json', JSON.stringify(json)); } diff --git a/scripts/docs.angularjs.org-firebase/firebase.json b/scripts/docs.angularjs.org-firebase/firebase.json index abb70329f815..880a5eca86de 100644 --- a/scripts/docs.angularjs.org-firebase/firebase.json +++ b/scripts/docs.angularjs.org-firebase/firebase.json @@ -23,9 +23,14 @@ "destination": "/index-production.html" }, { - "source": "**/*!(.jpg|.jpeg|.gif|.png|.html|.js|.map|.json|.css|.svg|.ttf|.txt|.woff|.woff2|.eot|.xml)", - "destination": "/index-production.html" + "source": "**/*!(.@(jpg|jpeg|gif|png|html|js|map|json|css|svg|ttf|txt|woff|woff2|eot|xml))", + "function": "sendFile" } ] + }, + "functions": { + "predeploy": [ + "npm --prefix $RESOURCE_DIR run lint" + ] } -} \ No newline at end of file +} diff --git a/scripts/docs.angularjs.org-firebase/functions/index.js b/scripts/docs.angularjs.org-firebase/functions/index.js new file mode 100644 index 000000000000..b86eb32f642a --- /dev/null +++ b/scripts/docs.angularjs.org-firebase/functions/index.js @@ -0,0 +1,49 @@ +'use strict'; + +const functions = require('firebase-functions'); +const fs = require('fs'); + +const BROWSER_CACHE_DURATION = 60 * 60; +const CDN_CACHE_DURATION = 60 * 60 * 12; + +const headers = { + 'Cache-Control': `public max-age=${BROWSER_CACHE_DURATION} s-maxage=${CDN_CACHE_DURATION}` +}; + +const buildSnapshot = data => ` + + + + + + + + ${data} + + `; + +function sendFile(request, response) { + + const snapshotRequested = typeof request.query._escaped_fragment_ !== 'undefined'; + const filePath = `content/${snapshotRequested ? `partials${request.path}` : 'index-production'}.html`; + + if (snapshotRequested) { + fs.readFile(filePath, {encoding: 'utf8'}, (error, data) => { + if (error) { + response + .status(404) + .end(); + } else { + response + .set(headers) + .send(buildSnapshot(data)); + } + }); + } else { + response + .set(headers) + .sendFile(filePath, {root: __dirname}); + } +} + +exports.sendFile = functions.https.onRequest(sendFile); diff --git a/scripts/docs.angularjs.org-firebase/functions/package-lock.json b/scripts/docs.angularjs.org-firebase/functions/package-lock.json new file mode 100644 index 000000000000..913ba90ff890 --- /dev/null +++ b/scripts/docs.angularjs.org-firebase/functions/package-lock.json @@ -0,0 +1,4265 @@ +{ + "name": "functions", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@firebase/app": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.1.8.tgz", + "integrity": "sha512-LkXMugZ2Z2S0r9zH62YNRhCscstFqJuufEjJmb/XWCTjCKzJZKdbE/6A+WNTitNSHgAkKEF0NoGsIuyhYAubkg==", + "requires": { + "@firebase/app-types": "0.1.1", + "@firebase/util": "0.1.8" + } + }, + "@firebase/app-types": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.1.1.tgz", + "integrity": "sha512-0CmY/orojHIJiTyDXUqrAtCSmk2nWw7h7qamJUPcBRgAIfc3ZsjFBLo1zj0sRVzZYbTWS9cKBC8tnpFZlEMLPw==" + }, + "@firebase/database": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.1.9.tgz", + "integrity": "sha512-w7bsItAvUDMGDX7v2NTIHmt5yRYDWAdUKqC7p152dopGw6AAFgxhR5jAikq7qB2UOPB1vRy8KHlcMk3RnF6cXw==", + "requires": { + "@firebase/database-types": "0.1.1", + "@firebase/util": "0.1.8", + "faye-websocket": "0.11.1" + }, + "dependencies": { + "faye-websocket": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", + "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", + "requires": { + "websocket-driver": "0.7.0" + } + } + } + }, + "@firebase/database-types": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.1.1.tgz", + "integrity": "sha512-LbLnwXFeQuxQrsuUccbiXX4j3wdajLPNcbivzypJhww+VU4W/4grnbVn/zPlRlMcG6jTwSyBnjdtJFKMSeNU+A==" + }, + "@firebase/util": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.1.8.tgz", + "integrity": "sha512-SfWLSfp1MXb5ItG3wGfdxxIDzFMcjq2E0iz9C7sstzD3mJ8/q55Sd8esAL3VQx5jD/hbfKUypsBczr5A0ML7Tw==" + }, + "@google-cloud/common": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.15.1.tgz", + "integrity": "sha512-cnVtHLvyiSQvb1RzXWDp7PA1sA8Jmc47+wp/xwHwdGOlQZfKog5iluZ0C/LB8iklFXpcTwlNMorqLuZ/qH0DDA==", + "requires": { + "array-uniq": "1.0.3", + "arrify": "1.0.1", + "concat-stream": "1.6.0", + "create-error-class": "3.0.2", + "duplexify": "3.5.3", + "ent": "2.2.0", + "extend": "3.0.1", + "google-auto-auth": "0.8.2", + "is": "3.2.1", + "log-driver": "1.2.6", + "methmeth": "1.1.0", + "modelo": "4.2.3", + "request": "2.83.0", + "retry-request": "3.3.1", + "split-array-stream": "1.0.3", + "stream-events": "1.0.2", + "string-format-obj": "1.1.1", + "through2": "2.0.3" + } + }, + "@google-cloud/common-grpc": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@google-cloud/common-grpc/-/common-grpc-0.5.5.tgz", + "integrity": "sha512-wgtuBcgTZ7cUMGQV9MSz4y0+FReLqdsOOgzOifu+lsnRh9qsMEZJ9sBDLB6NrRxrvcAHZc4ayiBx7B7i5cDYoA==", + "requires": { + "@google-cloud/common": "0.16.1", + "dot-prop": "4.2.0", + "duplexify": "3.5.3", + "extend": "3.0.1", + "grpc": "1.7.3", + "is": "3.2.1", + "modelo": "4.2.3", + "retry-request": "3.3.1", + "through2": "2.0.3" + }, + "dependencies": { + "@google-cloud/common": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.16.1.tgz", + "integrity": "sha512-1sufDsSfgJ7fuBLq+ux8t3TlydMlyWl9kPZx2WdLINkGtf5RjvXX6EWYZiCMKe8flJ3oC0l95j5atN2uX5n3rg==", + "requires": { + "array-uniq": "1.0.3", + "arrify": "1.0.1", + "concat-stream": "1.6.0", + "create-error-class": "3.0.2", + "duplexify": "3.5.3", + "ent": "2.2.0", + "extend": "3.0.1", + "google-auto-auth": "0.9.3", + "is": "3.2.1", + "log-driver": "1.2.5", + "methmeth": "1.1.0", + "modelo": "4.2.3", + "request": "2.83.0", + "retry-request": "3.3.1", + "split-array-stream": "1.0.3", + "stream-events": "1.0.2", + "string-format-obj": "1.1.1", + "through2": "2.0.3" + } + }, + "gcp-metadata": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.4.1.tgz", + "integrity": "sha512-yFE7v+NyoMiTOi2L6r8q87eVbiZCKooJNPKXTHhBStga8pwwgWofK9iHl00qd0XevZxcpk7ORaEL/ALuTvlaGQ==", + "requires": { + "extend": "3.0.1", + "retry-request": "3.3.1" + } + }, + "google-auto-auth": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.9.3.tgz", + "integrity": "sha512-TbOZZs0WJOolrRmdQLK5qmWdOJQFG1oPnxcIBbAwL7XCWcv3XgZ9gHJ6W4byrdEZT8TahNFgMfkHd73mqxM9dw==", + "requires": { + "async": "2.6.0", + "gcp-metadata": "0.4.1", + "google-auth-library": "0.12.0", + "request": "2.83.0" + } + }, + "log-driver": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", + "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=" + } + } + }, + "@google-cloud/firestore": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-0.11.2.tgz", + "integrity": "sha512-Dp5Im5y6LfJ2OuuGN/PX/w7+LdKPRhKbhgNOaUjreIULo7Ya9AyRMYn/E+w8Rm+b3CZXM7fzVpkkdg9Thq8uUQ==", + "requires": { + "@google-cloud/common": "0.15.1", + "@google-cloud/common-grpc": "0.5.5", + "bun": "0.0.12", + "extend": "3.0.1", + "functional-red-black-tree": "1.0.1", + "google-gax": "0.14.5", + "is": "3.2.1", + "safe-buffer": "5.1.1", + "through2": "2.0.3" + } + }, + "@google-cloud/storage": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-1.5.2.tgz", + "integrity": "sha512-E97x2oZr9w0N26H0LtyjR3XLFSLYXH5D8y8HwEZRWQnNVF9sO+x16MEhdHFdFclgdx687eGeCYbDVKpP+dKb6w==", + "requires": { + "@google-cloud/common": "0.15.1", + "arrify": "1.0.1", + "async": "2.6.0", + "concat-stream": "1.6.0", + "create-error-class": "3.0.2", + "duplexify": "3.5.3", + "extend": "3.0.1", + "gcs-resumable-upload": "0.8.2", + "hash-stream-validation": "0.2.1", + "is": "3.2.1", + "mime-types": "2.1.17", + "once": "1.4.0", + "pumpify": "1.4.0", + "request": "2.83.0", + "safe-buffer": "5.1.1", + "snakeize": "0.1.0", + "stream-events": "1.0.2", + "string-format-obj": "1.1.1", + "through2": "2.0.3" + } + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "requires": { + "@protobufjs/aspromise": "1.1.2", + "@protobufjs/inquire": "1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, + "@types/body-parser": { + "version": "1.16.8", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.16.8.tgz", + "integrity": "sha512-BdN2PXxOFnTXFcyONPW6t0fHjz2fvRZHVMFpaS0wYr+Y8fWEaNOs4V8LEu/fpzQlMx+ahdndgTaGTwPC+J/EeA==", + "requires": { + "@types/express": "4.11.1", + "@types/node": "8.9.3" + } + }, + "@types/events": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-1.1.0.tgz", + "integrity": "sha512-y3bR98mzYOo0pAZuiLari+cQyiKk3UXRuT45h1RjhfeCzqkjaVsfZJNaxdgtk7/3tzOm1ozLTqEqMP3VbI48jw==" + }, + "@types/express": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.11.1.tgz", + "integrity": "sha512-ttWle8cnPA5rAelauSWeWJimtY2RsUf2aspYZs7xPHiWgOlPn6nnUfBMtrkcnjFJuIHJF4gNOdVvpLK2Zmvh6g==", + "requires": { + "@types/body-parser": "1.16.8", + "@types/express-serve-static-core": "4.11.1", + "@types/serve-static": "1.13.1" + } + }, + "@types/express-serve-static-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.11.1.tgz", + "integrity": "sha512-EehCl3tpuqiM8RUb+0255M8PhhSwTtLfmO7zBBdv0ay/VTd/zmrqDfQdZFsa5z/PVMbH2yCMZPXsnrImpATyIw==", + "requires": { + "@types/events": "1.1.0", + "@types/node": "8.9.3" + } + }, + "@types/google-cloud__storage": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@types/google-cloud__storage/-/google-cloud__storage-1.1.7.tgz", + "integrity": "sha512-010Llp+5ze+XWWmZuLDxs0pZgFjOgtJQVt9icJ0Ed67ZFLq7PnXkYx8x/k9nwDojR5/X4XoLPNqB1F627TScdQ==", + "requires": { + "@types/node": "8.9.3" + } + }, + "@types/jsonwebtoken": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-7.2.5.tgz", + "integrity": "sha512-8CIcK1Vzq4w5TJyJYkLVhqASmCo1FSO1XIPQM1qv+Xo2nnb9RoRHxx8pkIzSZ4Tm9r3V4ZyFbF/fBewNPdclwA==", + "requires": { + "@types/node": "8.9.3" + } + }, + "@types/lodash": { + "version": "4.14.102", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.102.tgz", + "integrity": "sha512-k/SxycYmVc6sYo6kzm8cABHcbMs9MXn6jYsja1hLvZ/x9e31VHRRn+1UzWdpv6doVchphvKaOsZ0VTqbF7zvNg==" + }, + "@types/long": { + "version": "3.0.32", + "resolved": "https://registry.npmjs.org/@types/long/-/long-3.0.32.tgz", + "integrity": "sha512-ZXyOOm83p7X8p3s0IYM3VeueNmHpkk/yMlP8CLeOnEcu6hIwPH7YjZBvhQkR0ZFS2DqZAxKtJ/M5fcuv3OU5BA==" + }, + "@types/mime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", + "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" + }, + "@types/node": { + "version": "8.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.3.tgz", + "integrity": "sha512-wqrPE4Uvj2fmL0E5JFQiY7D/5bAKvVUfWTnQ5NEV35ULkAU0j3QuqIi9Qyrytz8M5hsrh8Kijt+FsdLQaZR+IA==" + }, + "@types/serve-static": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.1.tgz", + "integrity": "sha512-jDMH+3BQPtvqZVIcsH700Dfi8Q3MIcEx16g/VdxjoqiGR/NntekB10xdBpirMKnPe9z2C5cBmL0vte0YttOr3Q==", + "requires": { + "@types/express-serve-static-core": "4.11.1", + "@types/mime": "2.0.0" + } + }, + "@types/sha1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/sha1/-/sha1-1.1.1.tgz", + "integrity": "sha512-Yrz4TPsm/xaw7c39aTISskNirnRJj2W9OVeHv8ooOR9SG8NHEfh4lwvGeN9euzxDyPfBdFkvL/VHIY3kM45OpQ==", + "requires": { + "@types/node": "8.9.3" + } + }, + "accepts": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", + "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "requires": { + "mime-types": "2.1.17", + "negotiator": "0.6.1" + } + }, + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + }, + "acorn-es7-plugin": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/acorn-es7-plugin/-/acorn-es7-plugin-1.1.7.tgz", + "integrity": "sha1-8u4fMiipDurRJF+asZIusucdM2s=" + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "3.3.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true + }, + "ansi-escapes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz", + "integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arguejs": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/arguejs/-/arguejs-0.2.3.tgz", + "integrity": "sha1-tvk59f4OPNHz+T4qqSYkJL8xKvc=" + }, + "array-filter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", + "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "ascli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz", + "integrity": "sha1-vPpZdKYvGOgcq660lzKrSoj5Brw=", + "requires": { + "colour": "0.7.1", + "optjs": "3.2.2" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "requires": { + "lodash": "4.17.5" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", + "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "bl": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz", + "integrity": "sha1-wGt5evCF6gC8Unr8jvzxHeIjIFQ=", + "requires": { + "readable-stream": "1.0.34" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.2", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.15" + } + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.2.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=" + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "bun": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/bun/-/bun-0.0.12.tgz", + "integrity": "sha512-Toms18J9DqnT+IfWkwxVTB2EaBprHvjlMWrTIsfX4xbu3ZBqVBwrERU0em1IgtRe04wT+wJxMlKHZok24hrcSQ==", + "requires": { + "readable-stream": "1.0.34" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "bytebuffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", + "integrity": "sha1-WC7qSxqHO20CCkjVjfhfC7ps/d0=", + "requires": { + "long": "3.2.0" + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "call-signature": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/call-signature/-/call-signature-0.0.2.tgz", + "integrity": "sha1-qEq8glpV70yysCi9dOIFpluaSZY=" + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "capture-stack-trace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "codecov.io": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/codecov.io/-/codecov.io-0.0.1.tgz", + "integrity": "sha1-JeorCV4enqEYcr36WEIRgTDfeLE=", + "requires": { + "request": "2.42.0", + "urlgrey": "0.4.0" + }, + "dependencies": { + "asn1": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz", + "integrity": "sha1-VZvhg3bQik7E2+gId9J4GGObLfc=", + "optional": true + }, + "assert-plus": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", + "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA=", + "optional": true + }, + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "optional": true + }, + "aws-sign2": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz", + "integrity": "sha1-xXED96F/wDfwLXwuZLYC6iI/fWM=", + "optional": true + }, + "boom": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz", + "integrity": "sha1-emNune1O/O+xnO9JR6PGffrukRs=", + "requires": { + "hoek": "0.9.1" + } + }, + "caseless": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.6.0.tgz", + "integrity": "sha1-gWfBq4OX+1u5X5bSjlqBxQ8kesQ=" + }, + "combined-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", + "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=", + "optional": true, + "requires": { + "delayed-stream": "0.0.5" + } + }, + "cryptiles": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.2.2.tgz", + "integrity": "sha1-7ZH/HxetE9N0gohZT4pIoNJvMlw=", + "optional": true, + "requires": { + "boom": "0.4.2" + } + }, + "delayed-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", + "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=", + "optional": true + }, + "forever-agent": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.5.2.tgz", + "integrity": "sha1-bQ4JxJIflKJ/Y9O0nF/v8epMUTA=" + }, + "form-data": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.1.4.tgz", + "integrity": "sha1-kavXiKupcCsaq/qLwBAxoqyeOxI=", + "optional": true, + "requires": { + "async": "0.9.2", + "combined-stream": "0.0.7", + "mime": "1.2.11" + } + }, + "hawk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-1.1.1.tgz", + "integrity": "sha1-h81JH5tG5OKurKM1QWdmiF0tHtk=", + "optional": true, + "requires": { + "boom": "0.4.2", + "cryptiles": "0.2.2", + "hoek": "0.9.1", + "sntp": "0.2.4" + } + }, + "hoek": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz", + "integrity": "sha1-PTIkYrrfB3Fup+uFuviAec3c5QU=" + }, + "http-signature": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", + "integrity": "sha1-T72sEyVZqoMjEh5UB3nAoBKyfmY=", + "optional": true, + "requires": { + "asn1": "0.1.11", + "assert-plus": "0.1.5", + "ctype": "0.5.3" + } + }, + "mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=", + "optional": true + }, + "mime-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-1.0.2.tgz", + "integrity": "sha1-mVrhOSq4r/y/yyZB3QVOlDwNXc4=" + }, + "node-uuid": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" + }, + "oauth-sign": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.4.0.tgz", + "integrity": "sha1-8ilW8x6nFRqCHl8vsywRPK2Ln2k=", + "optional": true + }, + "qs": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-1.2.2.tgz", + "integrity": "sha1-GbV/8k3CqZzh+L32r82ln472H4g=" + }, + "request": { + "version": "2.42.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.42.0.tgz", + "integrity": "sha1-VyvQFIk4VkBArHqxSLlkI6BjMEo=", + "requires": { + "aws-sign2": "0.5.0", + "bl": "0.9.5", + "caseless": "0.6.0", + "forever-agent": "0.5.2", + "form-data": "0.1.4", + "hawk": "1.1.1", + "http-signature": "0.10.1", + "json-stringify-safe": "5.0.1", + "mime-types": "1.0.2", + "node-uuid": "1.4.8", + "oauth-sign": "0.4.0", + "qs": "1.2.2", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.4.3" + } + }, + "sntp": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-0.2.4.tgz", + "integrity": "sha1-+4hfGLDzqtGJ+CSGJTa87ux1CQA=", + "optional": true, + "requires": { + "hoek": "0.9.1" + } + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" + } + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "colour": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", + "integrity": "sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g=" + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.4", + "typedarray": "0.0.6" + } + }, + "configstore": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.1.tgz", + "integrity": "sha512-5oNkD/L++l0O6xGXxb1EWS7SivtjfGQlRyxJsYgE0Z495/L81e2h4/d3r969hoPXuFItzNOKMtsXgYG4c7dYvw==", + "requires": { + "dot-prop": "4.2.0", + "graceful-fs": "4.1.11", + "make-dir": "1.1.0", + "unique-string": "1.0.0", + "write-file-atomic": "2.3.0", + "xdg-basedir": "3.0.0" + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-js": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", + "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "requires": { + "capture-stack-trace": "1.0.0" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.0" + } + } + } + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" + }, + "ctype": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz", + "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8=", + "optional": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "deep-equal": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.1.2.tgz", + "integrity": "sha1-skbCuApXCkfBG+HZvRBw7IeLh84=" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.11" + } + }, + "defined": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-0.0.0.tgz", + "integrity": "sha1-817qfXBekzuvE7LwOz+D2SFAOz4=" + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.2" + }, + "dependencies": { + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "diff-match-patch": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.0.tgz", + "integrity": "sha1-HMPIOkkNZ/ldkeOfatHy4Ia2MEg=" + }, + "dir-glob": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", + "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", + "requires": { + "arrify": "1.0.1", + "path-type": "3.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "2.0.2" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "requires": { + "is-obj": "1.0.1" + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, + "duplexify": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.3.tgz", + "integrity": "sha512-g8ID9OroF9hKt2POf8YLayy+9594PzmM3scI00/uBXocX3TWNgoB67hjzkFe9ITAbQOne/lLdBxHXvYUM4ZgGA==", + "requires": { + "end-of-stream": "1.4.1", + "inherits": "2.0.3", + "readable-stream": "2.3.4", + "stream-shift": "1.0.0" + } + }, + "eastasianwidth": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.1.1.tgz", + "integrity": "sha1-RNZW3p2kFWlEZzNTZfsxR7hXK3w=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", + "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", + "requires": { + "base64url": "2.0.0", + "safe-buffer": "5.1.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "empower": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/empower/-/empower-1.2.3.tgz", + "integrity": "sha1-bw2nNEf07dg4/sXGAxOoi6XLhSs=", + "requires": { + "core-js": "2.5.3", + "empower-core": "0.6.2" + } + }, + "empower-core": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/empower-core/-/empower-core-0.6.2.tgz", + "integrity": "sha1-Wt71ZgiOMfuoC6CjbfR9cJQWkUQ=", + "requires": { + "call-signature": "0.0.2", + "core-js": "2.5.3" + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "1.4.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.17.0.tgz", + "integrity": "sha512-AyxBUCANU/o/xC0ijGMKavo5Ls3oK6xykiOITlMdjFjrKOsqLrA7Nf5cnrDgcKrHzBirclAZt63XO7YZlVUPwA==", + "dev": true, + "requires": { + "ajv": "5.5.2", + "babel-code-frame": "6.26.0", + "chalk": "2.3.1", + "concat-stream": "1.6.0", + "cross-spawn": "5.1.0", + "debug": "3.1.0", + "doctrine": "2.1.0", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "1.0.0", + "espree": "3.5.3", + "esquery": "1.0.0", + "esutils": "2.0.2", + "file-entry-cache": "2.0.0", + "functional-red-black-tree": "1.0.1", + "glob": "7.1.2", + "globals": "11.3.0", + "ignore": "3.3.7", + "imurmurhash": "0.1.4", + "inquirer": "3.3.0", + "is-resolvable": "1.1.0", + "js-yaml": "3.10.0", + "json-stable-stringify-without-jsonify": "1.0.1", + "levn": "0.3.0", + "lodash": "4.17.5", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "optionator": "0.8.2", + "path-is-inside": "1.0.2", + "pluralize": "7.0.0", + "progress": "2.0.0", + "require-uncached": "1.0.3", + "semver": "5.5.0", + "strip-ansi": "4.0.0", + "strip-json-comments": "2.0.1", + "table": "4.0.2", + "text-table": "0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "eslint-plugin-promise": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-3.6.0.tgz", + "integrity": "sha512-YQzM6TLTlApAr7Li8vWKR+K3WghjwKcYzY0d2roWap4SLK+kzuagJX/leTetIDWsFcTFnKNJXWupDCD6aZkP2Q==", + "dev": true + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + } + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.3.tgz", + "integrity": "sha512-Zy3tAJDORxQZLl2baguiRU1syPERAIg0L+JB2MWorORgTu/CplzvxS9WWA7Xh4+Q+eOQihNs/1o1Xep8cvCxWQ==", + "dev": true, + "requires": { + "acorn": "5.4.1", + "acorn-jsx": "3.0.1" + }, + "dependencies": { + "acorn": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz", + "integrity": "sha512-XLmq3H/BVvW6/GbxKryGxWORz1ebilSsUDlyC27bXhWGWAZWkGwS6FLHjOlwFXNFoWFQEO/Df4u0YYd0K3BQgQ==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "espurify": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/espurify/-/espurify-1.7.0.tgz", + "integrity": "sha1-HFz2y8zDLm9jk4C9T5kfq5up0iY=", + "requires": { + "core-js": "2.5.3" + } + }, + "esquery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "dev": true, + "requires": { + "estraverse": "4.2.0" + } + }, + "esrecurse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", + "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", + "dev": true, + "requires": { + "estraverse": "4.2.0", + "object-assign": "4.1.1" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", + "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", + "requires": { + "accepts": "1.3.4", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "1.1.2", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "finalhandler": "1.1.0", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "2.0.2", + "qs": "6.5.1", + "range-parser": "1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.1", + "serve-static": "1.13.1", + "setprototypeof": "1.1.0", + "statuses": "1.3.1", + "type-is": "1.6.15", + "utils-merge": "1.0.1", + "vary": "1.1.2" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "external-editor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz", + "integrity": "sha512-E44iT5QVOUJBKij4IIV3uvxuNlbKS38Tw1HiupxEIHPv9qtC2PrDYohbXV5U+1jnfIXttny8gUhj+oZvflFlzA==", + "dev": true, + "requires": { + "chardet": "0.4.2", + "iconv-lite": "0.4.19", + "tmp": "0.0.33" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "faye-websocket": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.9.3.tgz", + "integrity": "sha1-SCpQWw3wrmJrlphm0710DNuWLoM=", + "requires": { + "websocket-driver": "0.7.0" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "1.3.0", + "object-assign": "4.1.1" + } + }, + "finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.3.1", + "unpipe": "1.0.0" + } + }, + "firebase-admin": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-5.8.2.tgz", + "integrity": "sha1-TDkN87b9MlZ9jUVYrASXS+vRV0k=", + "requires": { + "@firebase/app": "0.1.8", + "@firebase/database": "0.1.9", + "@google-cloud/firestore": "0.11.2", + "@google-cloud/storage": "1.5.2", + "@types/google-cloud__storage": "1.1.7", + "@types/node": "8.9.3", + "faye-websocket": "0.9.3", + "jsonwebtoken": "8.1.0", + "node-forge": "0.7.1" + } + }, + "firebase-functions": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-0.8.1.tgz", + "integrity": "sha1-pC/m0kOGLEBq8W4cFrHQqB7CTAM=", + "requires": { + "@types/express": "4.11.1", + "@types/jsonwebtoken": "7.2.5", + "@types/lodash": "4.14.102", + "@types/sha1": "1.1.1", + "express": "4.16.2", + "jsonwebtoken": "7.4.3", + "lodash": "4.17.5", + "sha1": "1.1.1" + }, + "dependencies": { + "jsonwebtoken": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.3.tgz", + "integrity": "sha1-d/UCHeBYtgWheD+hKD6ZgS5kVjg=", + "requires": { + "joi": "6.10.1", + "jws": "3.1.4", + "lodash.once": "4.1.1", + "ms": "2.1.1", + "xtend": "4.0.1" + } + } + } + }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "dev": true, + "requires": { + "circular-json": "0.3.3", + "del": "2.2.2", + "graceful-fs": "4.1.11", + "write": "0.2.1" + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", + "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" + }, + "gcp-metadata": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.3.1.tgz", + "integrity": "sha512-5kJPX/RXuqoLmHiOOgkSDk/LI0QaXpEvZ3pvQP4ifjGGDKZKVSOjL/GcDjXA5kLxppFCOjmmsu0Uoop9d1upaQ==", + "requires": { + "extend": "3.0.1", + "retry-request": "3.3.1" + } + }, + "gcs-resumable-upload": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-0.8.2.tgz", + "integrity": "sha512-PBl1OFABYxubxfYPh000I0+JLbQzBRtNqxzgxYboIQk2tdw7BvjJ2dVukk3YH4QM6GiUwqItyNqWBuxjLH8GhA==", + "requires": { + "buffer-equal": "1.0.0", + "configstore": "3.1.1", + "google-auto-auth": "0.7.2", + "pumpify": "1.4.0", + "request": "2.83.0", + "stream-events": "1.0.2", + "through2": "2.0.3" + }, + "dependencies": { + "google-auth-library": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.10.0.tgz", + "integrity": "sha1-bhW6vuhf0d0U2NEoopW2g41SE24=", + "requires": { + "gtoken": "1.2.3", + "jws": "3.1.4", + "lodash.noop": "3.0.1", + "request": "2.83.0" + } + }, + "google-auto-auth": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.7.2.tgz", + "integrity": "sha512-ux2n2AE2g3+vcLXwL4dP/M12SFMRX5dzCzBfhAEkTeAB7dpyGdOIEj7nmUx0BHKaCcUQrRWg9kT63X/Mmtk1+A==", + "requires": { + "async": "2.6.0", + "gcp-metadata": "0.3.1", + "google-auth-library": "0.10.0", + "request": "2.83.0" + } + } + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "globals": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.3.0.tgz", + "integrity": "sha512-kkpcKNlmQan9Z5ZmgqKH/SMbSmjxQ7QjyNqfXVc8VJcoBV2UEg+sxQD15GQofGRh2hfpwUb70VC31DR7Rq5Hdw==", + "dev": true + }, + "globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "requires": { + "array-union": "1.0.2", + "dir-glob": "2.0.0", + "glob": "7.1.2", + "ignore": "3.3.7", + "pify": "3.0.0", + "slash": "1.0.0" + } + }, + "google-auth-library": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.12.0.tgz", + "integrity": "sha512-79qCXtJ1VweBmmLr4yLq9S4clZB2p5Y+iACvuKk9gu4JitEnPc+bQFmYvtCYehVR44MQzD1J8DVmYW2w677IEw==", + "requires": { + "gtoken": "1.2.3", + "jws": "3.1.4", + "lodash.isstring": "4.0.1", + "lodash.merge": "4.6.1", + "request": "2.83.0" + } + }, + "google-auto-auth": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.8.2.tgz", + "integrity": "sha512-W91J1paFbyG45gpDWdTu9tKDxbiTDWYkOAxytNVF4oHVVgTCBV/8+lWdjj/6ldjN3eb+sEd9PKJBjm0kmCxvcw==", + "requires": { + "async": "2.6.0", + "gcp-metadata": "0.3.1", + "google-auth-library": "0.12.0", + "request": "2.83.0" + } + }, + "google-gax": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-0.14.5.tgz", + "integrity": "sha512-3A6KbrtLDavrqZnnzurnSydRIJnyH+2Sm56fAvXciQ/62aEnSDaR43MCgWhtReCLVjeFjBiCEIdX5zV0LVLVBg==", + "requires": { + "extend": "3.0.1", + "globby": "7.1.1", + "google-auto-auth": "0.9.3", + "google-proto-files": "0.14.2", + "grpc": "1.7.3", + "is-stream-ended": "0.1.3", + "lodash": "4.17.5", + "protobufjs": "6.8.4", + "readable-stream": "2.3.4", + "through2": "2.0.3" + }, + "dependencies": { + "gcp-metadata": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.4.1.tgz", + "integrity": "sha512-yFE7v+NyoMiTOi2L6r8q87eVbiZCKooJNPKXTHhBStga8pwwgWofK9iHl00qd0XevZxcpk7ORaEL/ALuTvlaGQ==", + "requires": { + "extend": "3.0.1", + "retry-request": "3.3.1" + } + }, + "google-auto-auth": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.9.3.tgz", + "integrity": "sha512-TbOZZs0WJOolrRmdQLK5qmWdOJQFG1oPnxcIBbAwL7XCWcv3XgZ9gHJ6W4byrdEZT8TahNFgMfkHd73mqxM9dw==", + "requires": { + "async": "2.6.0", + "gcp-metadata": "0.4.1", + "google-auth-library": "0.12.0", + "request": "2.83.0" + } + }, + "protobufjs": { + "version": "6.8.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.4.tgz", + "integrity": "sha512-d+WZqUDXKM+oZhr8yprAtQW07q08p9/V35AJ2J1fds+r903S/aH9P8uO1gmTwozOKugt2XCjdrre3OxuPRGkGg==", + "requires": { + "@protobufjs/aspromise": "1.1.2", + "@protobufjs/base64": "1.1.2", + "@protobufjs/codegen": "2.0.4", + "@protobufjs/eventemitter": "1.1.0", + "@protobufjs/fetch": "1.1.0", + "@protobufjs/float": "1.0.2", + "@protobufjs/inquire": "1.1.0", + "@protobufjs/path": "1.1.2", + "@protobufjs/pool": "1.1.0", + "@protobufjs/utf8": "1.1.0", + "@types/long": "3.0.32", + "@types/node": "8.9.3", + "long": "3.2.0" + } + } + } + }, + "google-p12-pem": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-0.1.2.tgz", + "integrity": "sha1-M8RqsCGqc0+gMys5YKmj/8svMXc=", + "requires": { + "node-forge": "0.7.1" + } + }, + "google-proto-files": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/google-proto-files/-/google-proto-files-0.14.2.tgz", + "integrity": "sha512-wwm2TIlfTgAjDbjrxAb3akznO7vBM0PRLS6Xf2QfR3L7b0p+szD3iwOW0wMSFl3B0UbLv27hUVk+clePqCVmXA==", + "requires": { + "globby": "7.1.1", + "power-assert": "1.4.4", + "prettier": "1.10.2", + "protobufjs": "6.8.4" + }, + "dependencies": { + "protobufjs": { + "version": "6.8.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.4.tgz", + "integrity": "sha512-d+WZqUDXKM+oZhr8yprAtQW07q08p9/V35AJ2J1fds+r903S/aH9P8uO1gmTwozOKugt2XCjdrre3OxuPRGkGg==", + "requires": { + "@protobufjs/aspromise": "1.1.2", + "@protobufjs/base64": "1.1.2", + "@protobufjs/codegen": "2.0.4", + "@protobufjs/eventemitter": "1.1.0", + "@protobufjs/fetch": "1.1.0", + "@protobufjs/float": "1.0.2", + "@protobufjs/inquire": "1.1.0", + "@protobufjs/path": "1.1.2", + "@protobufjs/pool": "1.1.0", + "@protobufjs/utf8": "1.1.0", + "@types/long": "3.0.32", + "@types/node": "8.9.3", + "long": "3.2.0" + } + } + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "grpc": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.7.3.tgz", + "integrity": "sha512-7zXQJlDXMr/ZaDqdaIchgclViyoWo8GQxZSmFUAxR8GwSr28b6/BTgF221WG+2W693jpp74XJ/+I9DcPXsgt9Q==", + "requires": { + "arguejs": "0.2.3", + "lodash": "4.17.5", + "nan": "2.8.0", + "node-pre-gyp": "0.6.39", + "protobufjs": "5.0.2" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "bundled": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.3" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.8", + "bundled": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "caseless": { + "version": "0.12.0", + "bundled": true + }, + "co": { + "version": "4.6.0", + "bundled": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "detect-libc": { + "version": "1.0.2", + "bundled": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true + }, + "extsprintf": { + "version": "1.3.0", + "bundled": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "glob": { + "version": "7.1.1", + "bundled": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.4", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true + }, + "jsprim": { + "version": "1.4.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "mime-db": { + "version": "1.30.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.17", + "bundled": true, + "requires": { + "mime-db": "1.30.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true + }, + "node-pre-gyp": { + "version": "0.6.39", + "bundled": true, + "requires": { + "detect-libc": "1.0.2", + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.2", + "rc": "1.2.2", + "request": "2.81.0", + "rimraf": "2.6.2", + "semver": "5.4.1", + "tar": "2.2.1", + "tar-pack": "3.4.1" + }, + "dependencies": { + "nopt": { + "version": "4.0.1", + "bundled": true, + "requires": { + "abbrev": "1.0.9", + "osenv": "0.1.4" + } + } + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true + }, + "qs": { + "version": "6.4.0", + "bundled": true + }, + "rc": { + "version": "1.2.2", + "bundled": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true + } + } + }, + "readable-stream": { + "version": "2.3.3", + "bundled": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "requires": { + "glob": "7.1.1" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true + }, + "semver": { + "version": "5.4.1", + "bundled": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.1", + "bundled": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "bundled": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.1", + "bundled": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.3.3", + "rimraf": "2.6.2", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.3", + "bundled": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "uuid": { + "version": "3.1.0", + "bundled": true + }, + "verror": { + "version": "1.10.0", + "bundled": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + } + } + }, + "gtoken": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-1.2.3.tgz", + "integrity": "sha512-wQAJflfoqSgMWrSBk9Fg86q+sd6s7y6uJhIvvIPz++RElGlMtEqsdAR2oWwZ/WTEtp7P9xFbJRrT976oRgzJ/w==", + "requires": { + "google-p12-pem": "0.1.2", + "jws": "3.1.4", + "mime": "1.6.0", + "request": "2.83.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "hash-stream-validation": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.1.tgz", + "integrity": "sha1-7Mm5l7IYvluzEphii7gHhptz3NE=", + "requires": { + "through2": "2.0.3" + } + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.0", + "sntp": "2.1.0" + } + }, + "hoek": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + } + } + }, + "http-parser-js": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", + "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "ignore": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", + "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "dev": true, + "requires": { + "ansi-escapes": "3.0.0", + "chalk": "2.3.1", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "2.1.0", + "figures": "2.0.0", + "lodash": "4.17.5", + "mute-stream": "0.0.7", + "run-async": "2.3.0", + "rx-lite": "4.0.8", + "rx-lite-aggregates": "4.0.8", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "ipaddr.js": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", + "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" + }, + "is": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", + "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true, + "requires": { + "is-path-inside": "1.0.1" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-stream-ended": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.3.tgz", + "integrity": "sha1-oEc7Jnx1ZjVIa+7cfjNE5UnRUqw=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isemail": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", + "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "joi": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", + "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", + "requires": { + "hoek": "2.16.3", + "isemail": "1.2.0", + "moment": "2.20.1", + "topo": "1.1.0" + }, + "dependencies": { + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + } + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsonwebtoken": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz", + "integrity": "sha1-xjl80uX9WD1lwAeoPce7eOaYK4M=", + "requires": { + "jws": "3.1.4", + "lodash.includes": "4.3.0", + "lodash.isboolean": "3.0.3", + "lodash.isinteger": "4.0.4", + "lodash.isnumber": "3.0.3", + "lodash.isplainobject": "4.0.6", + "lodash.isstring": "4.0.1", + "lodash.once": "4.1.1", + "ms": "2.1.1", + "xtend": "4.0.1" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jwa": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", + "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", + "requires": { + "base64url": "2.0.0", + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.9", + "safe-buffer": "5.1.1" + } + }, + "jws": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", + "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", + "requires": { + "base64url": "2.0.0", + "jwa": "1.1.5", + "safe-buffer": "5.1.1" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "1.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.merge": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", + "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==" + }, + "lodash.noop": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-3.0.1.tgz", + "integrity": "sha1-OBiPTWUKOkdCWEObluxFsyYXEzw=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "log-driver": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.6.tgz", + "integrity": "sha512-iUHz4WAGsXwUmL1UergWrkFD2iTUrGLMsQDRYUWtS9FI+wSyM76vIL+THQt7vrQq5fZDGdrPSCFUfIlqII28tg==", + "requires": { + "codecov.io": "0.0.1" + } + }, + "long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=" + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "make-dir": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.1.0.tgz", + "integrity": "sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==", + "requires": { + "pify": "3.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methmeth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/methmeth/-/methmeth-1.1.0.tgz", + "integrity": "sha1-6AomYY5S9cQiKGG7dIUQvRDikIk=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "modelo": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.3.tgz", + "integrity": "sha512-9DITV2YEMcw7XojdfvGl3gDD8J9QjZTJ7ZOUuSAkP+F3T6rDbzMJuPktxptsdHYEvZcmXrCD3LMOhdSAEq6zKA==" + }, + "moment": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "nan": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "node-forge": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", + "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=" + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-keys": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + } + }, + "optjs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz", + "integrity": "sha1-aabOicRCpEQDFBrS+bNwvVu29O4=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "requires": { + "pify": "3.0.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true + }, + "power-assert": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/power-assert/-/power-assert-1.4.4.tgz", + "integrity": "sha1-kpXqdDcZb1pgH95CDwQmMRhtdRc=", + "requires": { + "define-properties": "1.1.2", + "empower": "1.2.3", + "power-assert-formatter": "1.4.1", + "universal-deep-strict-equal": "1.2.2", + "xtend": "4.0.1" + } + }, + "power-assert-context-formatter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-context-formatter/-/power-assert-context-formatter-1.1.1.tgz", + "integrity": "sha1-7bo1LT7YpgMRTWZyZazOYNaJzN8=", + "requires": { + "core-js": "2.5.3", + "power-assert-context-traversal": "1.1.1" + } + }, + "power-assert-context-reducer-ast": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/power-assert-context-reducer-ast/-/power-assert-context-reducer-ast-1.1.2.tgz", + "integrity": "sha1-SEqZ4m9Jc/+IMuXFzHVnAuYJQXQ=", + "requires": { + "acorn": "4.0.13", + "acorn-es7-plugin": "1.1.7", + "core-js": "2.5.3", + "espurify": "1.7.0", + "estraverse": "4.2.0" + } + }, + "power-assert-context-traversal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-context-traversal/-/power-assert-context-traversal-1.1.1.tgz", + "integrity": "sha1-iMq8oNE7Y1nwfT0+ivppkmRXftk=", + "requires": { + "core-js": "2.5.3", + "estraverse": "4.2.0" + } + }, + "power-assert-formatter": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/power-assert-formatter/-/power-assert-formatter-1.4.1.tgz", + "integrity": "sha1-XcEl7VCj37HdomwZNH879Y7CiEo=", + "requires": { + "core-js": "2.5.3", + "power-assert-context-formatter": "1.1.1", + "power-assert-context-reducer-ast": "1.1.2", + "power-assert-renderer-assertion": "1.1.1", + "power-assert-renderer-comparison": "1.1.1", + "power-assert-renderer-diagram": "1.1.2", + "power-assert-renderer-file": "1.1.1" + } + }, + "power-assert-renderer-assertion": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-renderer-assertion/-/power-assert-renderer-assertion-1.1.1.tgz", + "integrity": "sha1-y/wOd+AIao+Wrz8djme57n4ozpg=", + "requires": { + "power-assert-renderer-base": "1.1.1", + "power-assert-util-string-width": "1.1.1" + } + }, + "power-assert-renderer-base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-renderer-base/-/power-assert-renderer-base-1.1.1.tgz", + "integrity": "sha1-lqZQxv0F7hvB9mtUrWFELIs/Y+s=" + }, + "power-assert-renderer-comparison": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-renderer-comparison/-/power-assert-renderer-comparison-1.1.1.tgz", + "integrity": "sha1-10Odl9hRVr5OMKAPL7WnJRTOPAg=", + "requires": { + "core-js": "2.5.3", + "diff-match-patch": "1.0.0", + "power-assert-renderer-base": "1.1.1", + "stringifier": "1.3.0", + "type-name": "2.0.2" + } + }, + "power-assert-renderer-diagram": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/power-assert-renderer-diagram/-/power-assert-renderer-diagram-1.1.2.tgz", + "integrity": "sha1-ZV+PcRk1qbbVQbhjJ2VHF8Y3qYY=", + "requires": { + "core-js": "2.5.3", + "power-assert-renderer-base": "1.1.1", + "power-assert-util-string-width": "1.1.1", + "stringifier": "1.3.0" + } + }, + "power-assert-renderer-file": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-renderer-file/-/power-assert-renderer-file-1.1.1.tgz", + "integrity": "sha1-o34rvReMys0E5427eckv40kzxec=", + "requires": { + "power-assert-renderer-base": "1.1.1" + } + }, + "power-assert-util-string-width": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-util-string-width/-/power-assert-util-string-width-1.1.1.tgz", + "integrity": "sha1-vmWet5N/3S5smncmjar2S9W3xZI=", + "requires": { + "eastasianwidth": "0.1.1" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prettier": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.10.2.tgz", + "integrity": "sha512-TcdNoQIWFoHblurqqU6d1ysopjq7UX0oRcT/hJ8qvBAELiYWn+Ugf0AXdnzISEJ7vuhNnQ98N8jR8Sh53x4IZg==" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, + "protobufjs": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-5.0.2.tgz", + "integrity": "sha1-WXSNfc8D0tsiwT2p/rAk4Wq4DJE=", + "requires": { + "ascli": "1.0.1", + "bytebuffer": "5.0.1", + "glob": "7.1.2", + "yargs": "3.32.0" + } + }, + "proxy-addr": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", + "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.5.2" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "1.4.1", + "once": "1.4.0" + } + }, + "pumpify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.4.0.tgz", + "integrity": "sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA==", + "requires": { + "duplexify": "3.5.3", + "inherits": "2.0.3", + "pump": "2.0.1" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.4.tgz", + "integrity": "sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.83.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.1", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "0.1.0", + "resolve-from": "1.0.1" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, + "resumer": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", + "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", + "requires": { + "through": "2.3.8" + } + }, + "retry-request": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-3.3.1.tgz", + "integrity": "sha512-PjAmtWIxjNj4Co/6FRtBl8afRP3CxrrIAnUzb1dzydfROd+6xt7xAebFeskgQgkfFf8NmzrXIoaB3HxmswXyxw==", + "requires": { + "request": "2.83.0", + "through2": "2.0.3" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "2.1.0" + } + }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "requires": { + "rx-lite": "4.0.8" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "send": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", + "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", + "requires": { + "debug": "2.6.9", + "depd": "1.1.2", + "destroy": "1.0.4", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "fresh": "0.5.2", + "http-errors": "1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.3.1" + }, + "dependencies": { + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "serve-static": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", + "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", + "requires": { + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "parseurl": "1.3.2", + "send": "0.16.1" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "sha1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", + "integrity": "sha1-rdqnqTFo85PxnrKxUJFhjicA+Eg=", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, + "snakeize": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", + "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=" + }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "requires": { + "hoek": "4.2.0" + } + }, + "split": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/split/-/split-0.2.10.tgz", + "integrity": "sha1-Zwl8YB1pfOE2j0GPBs0gHPBSGlc=", + "requires": { + "through": "2.3.8" + } + }, + "split-array-stream": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-1.0.3.tgz", + "integrity": "sha1-0rdajl4Ngk1S/eyLgiWDncLjXfo=", + "requires": { + "async": "2.6.0", + "is-stream-ended": "0.1.3" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "requires": { + "duplexer": "0.1.1" + } + }, + "stream-events": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.2.tgz", + "integrity": "sha1-q/OfZsCJCk63lbyNXoWbJhW1kLI=", + "requires": { + "stubs": "3.0.0" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + }, + "string-format-obj": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.1.tgz", + "integrity": "sha512-Mm+sROy+pHJmx0P/0Bs1uxIX6UhGJGj6xDGQZ5zh9v/SZRmLGevp+p0VJxV7lirrkAmQ2mvva/gHKpnF/pTb+Q==" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringifier": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/stringifier/-/stringifier-1.3.0.tgz", + "integrity": "sha1-3vGDQvaTPbDy2/yaoCF1tEjBeVk=", + "requires": { + "core-js": "2.5.3", + "traverse": "0.6.6", + "type-name": "2.0.2" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "table": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "dev": true, + "requires": { + "ajv": "5.5.2", + "ajv-keywords": "2.1.1", + "chalk": "2.3.1", + "lodash": "4.17.5", + "slice-ansi": "1.0.0", + "string-width": "2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "tape": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tape/-/tape-2.3.0.tgz", + "integrity": "sha1-Df7scJIn+8yRcKvn8EaWKycUMds=", + "requires": { + "deep-equal": "0.1.2", + "defined": "0.0.0", + "inherits": "2.0.3", + "jsonify": "0.0.0", + "resumer": "0.0.0", + "split": "0.2.10", + "stream-combiner": "0.0.4", + "through": "2.3.8" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "2.3.4", + "xtend": "4.0.1" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "topo": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", + "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", + "requires": { + "hoek": "2.16.3" + }, + "dependencies": { + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + } + } + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "requires": { + "punycode": "1.4.1" + } + }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.17" + } + }, + "type-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/type-name/-/type-name-2.0.2.tgz", + "integrity": "sha1-7+fUEj2KxSr/9/QMfk3sUmYAj7Q=" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "requires": { + "crypto-random-string": "1.0.0" + } + }, + "universal-deep-strict-equal": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/universal-deep-strict-equal/-/universal-deep-strict-equal-1.2.2.tgz", + "integrity": "sha1-DaSsL3PP95JMgfpN4BjKViyisKc=", + "requires": { + "array-filter": "1.0.0", + "indexof": "0.0.1", + "object-keys": "1.0.11" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "urlgrey": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.0.tgz", + "integrity": "sha1-8GU1cED7NcOzEdTl3DZITZbb6gY=", + "requires": { + "tape": "2.3.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "websocket-driver": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", + "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "requires": { + "http-parser-js": "0.4.10", + "websocket-extensions": "0.1.3" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==" + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "0.5.1" + } + }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "signal-exit": "3.0.2" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "requires": { + "camelcase": "2.1.1", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "os-locale": "1.4.0", + "string-width": "1.0.2", + "window-size": "0.1.4", + "y18n": "3.2.1" + } + } + } +} diff --git a/scripts/docs.angularjs.org-firebase/functions/package.json b/scripts/docs.angularjs.org-firebase/functions/package.json new file mode 100644 index 000000000000..fdfb9cf8b46d --- /dev/null +++ b/scripts/docs.angularjs.org-firebase/functions/package.json @@ -0,0 +1,21 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": { + "lint": "./node_modules/.bin/eslint .", + "serve": "firebase serve --only functions", + "shell": "firebase experimental:functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "dependencies": { + "firebase-admin": "~5.8.1", + "firebase-functions": "^0.8.1" + }, + "devDependencies": { + "eslint": "^4.12.0", + "eslint-plugin-promise": "^3.6.0" + }, + "private": true +} diff --git a/scripts/docs.angularjs.org-firebase/readme.firebase.docs.md b/scripts/docs.angularjs.org-firebase/readme.firebase.docs.md index d73e9a74f5d2..cd6019d77b64 100644 --- a/scripts/docs.angularjs.org-firebase/readme.firebase.docs.md +++ b/scripts/docs.angularjs.org-firebase/readme.firebase.docs.md @@ -1,10 +1,23 @@ Firebase for docs.angularjs.org =============================== +# Continuous integration + The docs are deployed to Google Firebase hosting via Travis deployment config, which expects -firebase.json and .firebaserc in the repository root. +firebase.json in the repository root, which is done by a Grunt task (firebaseDocsJsonForTravis) +that modifies the paths in the firebase.json and copies it into the repository root. + +See travis.yml for the complete deployment config, and scripts/travis/build.sh for the full deployment +build steps. + +# Serving locally: + +- Run `grunt:prepareDeploy`. + This copies docs content files into deploy/docs and the partials for Search Engine AJAX + Crawling into ./functions/content. -See travis.yml for the complete deployment config. +- Run `firebase serve --only functions,hosting` + Creates a server at localhost:5000 that serves from deploy/docs and uses the local function See /scripts/code.angularjs.org-firebase/readme.firebase.code.md for the firebase deployment to code.angularjs.org \ No newline at end of file From fb479188f5312310094ee8f6c7366000181d6130 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Mon, 12 Feb 2018 21:55:41 +0200 Subject: [PATCH 110/469] chore(docs.angular.js): do not break when deploying Follow-up to #16451. Closes #16452 --- lib/grunt/utils.js | 4 ++-- scripts/docs.angularjs.org-firebase/firebase.json | 5 ----- scripts/docs.angularjs.org-firebase/functions/package.json | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/grunt/utils.js b/lib/grunt/utils.js index 64ddc6f185ec..73e8b1efc790 100644 --- a/lib/grunt/utils.js +++ b/lib/grunt/utils.js @@ -302,8 +302,8 @@ module.exports = { var fileName = docsScriptFolder + '/firebase.json'; var json = grunt.file.readJSON(fileName); - json.hosting.public = 'deploy/docs'; - json.functions.source = docsScriptFolder + '/functions'; + (json.hosting || (json.hosting = {})).public = 'deploy/docs'; + (json.functions || (json.functions = {})).source = docsScriptFolder + '/functions'; grunt.file.write('firebase.json', JSON.stringify(json)); } diff --git a/scripts/docs.angularjs.org-firebase/firebase.json b/scripts/docs.angularjs.org-firebase/firebase.json index 880a5eca86de..bf080d386fd7 100644 --- a/scripts/docs.angularjs.org-firebase/firebase.json +++ b/scripts/docs.angularjs.org-firebase/firebase.json @@ -27,10 +27,5 @@ "function": "sendFile" } ] - }, - "functions": { - "predeploy": [ - "npm --prefix $RESOURCE_DIR run lint" - ] } } diff --git a/scripts/docs.angularjs.org-firebase/functions/package.json b/scripts/docs.angularjs.org-firebase/functions/package.json index fdfb9cf8b46d..381fbc501096 100644 --- a/scripts/docs.angularjs.org-firebase/functions/package.json +++ b/scripts/docs.angularjs.org-firebase/functions/package.json @@ -2,7 +2,7 @@ "name": "functions", "description": "Cloud Functions for Firebase", "scripts": { - "lint": "./node_modules/.bin/eslint .", + "lint": "eslint .", "serve": "firebase serve --only functions", "shell": "firebase experimental:functions:shell", "start": "npm run shell", From ba140dbff9dddcdad239241fbe0240a25b29d188 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Tue, 13 Feb 2018 01:25:34 +0200 Subject: [PATCH 111/469] chore(docs.angularjs.org): install firebase dependencies before deploying Firebase is trying to execute our functions code locally in order to parse the triggers. Install npm dependencies to avoid errors like: ``` Error: Error parsing triggers: Cannot find module 'firebase-functions' ``` Closes #16453 --- .../readme.firebase.docs.md | 2 +- scripts/travis/build.sh | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/docs.angularjs.org-firebase/readme.firebase.docs.md b/scripts/docs.angularjs.org-firebase/readme.firebase.docs.md index cd6019d77b64..10da862469df 100644 --- a/scripts/docs.angularjs.org-firebase/readme.firebase.docs.md +++ b/scripts/docs.angularjs.org-firebase/readme.firebase.docs.md @@ -20,4 +20,4 @@ build steps. Creates a server at localhost:5000 that serves from deploy/docs and uses the local function See /scripts/code.angularjs.org-firebase/readme.firebase.code.md for the firebase deployment to -code.angularjs.org \ No newline at end of file +code.angularjs.org diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index 980855388492..ed8838cc9574 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -2,6 +2,9 @@ set -e +readonly THIS_DIR=$(cd $(dirname $0); pwd) +readonly ROOT_DIR="$THIS_DIR/../.." + export BROWSER_STACK_ACCESS_KEY export SAUCE_ACCESS_KEY @@ -80,6 +83,14 @@ case "$JOB" in if [[ "$DEPLOY_DOCS" == true || "$DEPLOY_CODE" == true ]]; then grunt prepareDeploy + + if [[ "$DEPLOY_DOCS" == true ]]; then + # Install npm dependencies for Firebase functions. + ( + cd "$ROOT_DIR/scripts/docs.angularjs.org-firebase/functions" + npm install + ) + fi else echo "Skipping deployment build because conditions have not been met." fi @@ -94,4 +105,4 @@ case "$JOB" in or\ 'deploy'." ;; -esac \ No newline at end of file +esac From a126b346ff0f99562231a3721868d23a7c48495e Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Tue, 13 Feb 2018 01:52:02 +0200 Subject: [PATCH 112/469] chore(travis): fix `ROOT_DIR` path when `build.sh` is sourced --- scripts/travis/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index ed8838cc9574..9338247915a3 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -2,7 +2,7 @@ set -e -readonly THIS_DIR=$(cd $(dirname $0); pwd) +readonly THIS_DIR=$(cd $(dirname ${BASH_SOURCE[0]}); pwd) readonly ROOT_DIR="$THIS_DIR/../.." export BROWSER_STACK_ACCESS_KEY From 04ee1e781b931725cded2e40ce9bdc2674186b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82=C4=99biowski-Owczarek?= Date: Thu, 15 Feb 2018 14:28:47 +0100 Subject: [PATCH 113/469] docs(*): add CODE_OF_CONDUCT.md Closes #16456 --- CODE_OF_CONDUCT.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000000..baa757d028ae --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Contributor Code of Conduct + +The AngularJS project follows the Code of Conduct defined in [the angular/code-of-conduct repository](https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md). Please read it. From 9645a08b61bf4e0b52f3cde9567e4a12524ea488 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 13 Feb 2018 10:00:12 +0100 Subject: [PATCH 114/469] chore(docs.angularjs.org): allow robots access to js files Related to #16432 --- docs/app/assets/robots.txt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/app/assets/robots.txt b/docs/app/assets/robots.txt index c00cb3c20320..96b3734af696 100644 --- a/docs/app/assets/robots.txt +++ b/docs/app/assets/robots.txt @@ -1,9 +1,14 @@ User-agent: * Disallow: /examples/ -Disallow: /img/ Disallow: /partials/ Disallow: /ptore2e/ -Disallow: /*.js$ # The js files in the root are used by the embedded examples, not by the app itself -Disallow: /*.map$ # The map files in the root are used by the embedded examples, not by the app itself Disallow: /Error404.html + +# The js / map files in the root are used by the embedded examples, not by the app itself +Disallow: /*.js$ +Disallow: /*.map$ + +# (Javascript) crawlers need to access JS files +Allow: /components/*.js +Allow: /js/*.js From 3eabaab009c9b8d86759374a3b66a6b7a71dd5aa Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 15 Feb 2018 11:54:14 +0100 Subject: [PATCH 115/469] chore(docs.angularjs.org): allow crawling but not indexing of partials/ The sitemap.xml might also prevent the indexing, as the partials are not listed. Related to #16432 Closes #16457 --- docs/app/assets/robots.txt | 1 - scripts/docs.angularjs.org-firebase/firebase.json | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/app/assets/robots.txt b/docs/app/assets/robots.txt index 96b3734af696..36b7daeca859 100644 --- a/docs/app/assets/robots.txt +++ b/docs/app/assets/robots.txt @@ -1,7 +1,6 @@ User-agent: * Disallow: /examples/ -Disallow: /partials/ Disallow: /ptore2e/ Disallow: /Error404.html diff --git a/scripts/docs.angularjs.org-firebase/firebase.json b/scripts/docs.angularjs.org-firebase/firebase.json index bf080d386fd7..9e112f2ff9ee 100644 --- a/scripts/docs.angularjs.org-firebase/firebase.json +++ b/scripts/docs.angularjs.org-firebase/firebase.json @@ -26,6 +26,15 @@ "source": "**/*!(.@(jpg|jpeg|gif|png|html|js|map|json|css|svg|ttf|txt|woff|woff2|eot|xml))", "function": "sendFile" } + ], + "headers": [ + { + "source": "/partials/**", + "headers" : [{ + "key" : "X-Robots-Tag", + "value" : "noindex" + }] + } ] } } From f1c164c92f880b41fed38246baa0c1d4d914dd61 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Mon, 19 Feb 2018 19:31:43 +0000 Subject: [PATCH 116/469] docs($parse): add missing error documents --- docs/content/error/$parse/esc.ngdoc | 10 ++++++++++ docs/content/error/$parse/lval.ngdoc | 13 +++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 docs/content/error/$parse/esc.ngdoc create mode 100644 docs/content/error/$parse/lval.ngdoc diff --git a/docs/content/error/$parse/esc.ngdoc b/docs/content/error/$parse/esc.ngdoc new file mode 100644 index 000000000000..3f308b2d2fec --- /dev/null +++ b/docs/content/error/$parse/esc.ngdoc @@ -0,0 +1,10 @@ +@ngdoc error +@name $parse:esc +@fullName Value cannot be escaped +@description + +Occurs when the parser tries to escape a value that is not known. + +This should never occur in practice. If it does then that indicates a programming +error in the AngularJS `$parse` service itself and should be reported as an issue +at https://github.com/angular/angular.js/issues. \ No newline at end of file diff --git a/docs/content/error/$parse/lval.ngdoc b/docs/content/error/$parse/lval.ngdoc new file mode 100644 index 000000000000..03d23465a73d --- /dev/null +++ b/docs/content/error/$parse/lval.ngdoc @@ -0,0 +1,13 @@ +@ngdoc error +@name $parse:lval +@fullName Trying to assign a value to a non l-value +@description + +Occurs when an expression is trying to assign a value to a non-assignable expression. + +This can happen if the left side of an assigment is not a valid reference to a variable +or property. E.g. In the following snippet `1+2` is not assignable. + +``` +(1+2) = 'hello'; +``` From 56b6ba8e0b891b88687b826a9c4b2100bfd4901a Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Mon, 19 Feb 2018 19:37:26 +0000 Subject: [PATCH 117/469] docs($route): add missing error document --- docs/content/error/$route/norout.ngdoc | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/content/error/$route/norout.ngdoc diff --git a/docs/content/error/$route/norout.ngdoc b/docs/content/error/$route/norout.ngdoc new file mode 100644 index 000000000000..30a12d151442 --- /dev/null +++ b/docs/content/error/$route/norout.ngdoc @@ -0,0 +1,8 @@ +@ngdoc error +@name $route:norout +@fullName Tried updating route when with no current route +@description + +Occurs when an attempt is made to update the parameters on the current route when +there is no current route. This can happen if you try to call `$route.updateParams();` +before the first route transition has completed. \ No newline at end of file From c617d6dceee5b000bfceda44ced22fc16b48b18b Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 20 Feb 2018 10:25:33 +0000 Subject: [PATCH 118/469] fix($templateRequest): give tpload error the correct namespace BREAKING CHANGE Previously the `tpload` error was namespaced to `$compile`. If you have code that matches errors of the form `[$compile:tpload]` it will no longer run. You should change the code to match `[$templateRequest:tpload]`. --- docs/content/error/$compile/tpload.ngdoc | 11 ----------- .../error/$templateRequest/tpload.ngdoc | 18 ++++++++++++++++++ src/ng/templateRequest.js | 2 +- 3 files changed, 19 insertions(+), 12 deletions(-) delete mode 100644 docs/content/error/$compile/tpload.ngdoc create mode 100644 docs/content/error/$templateRequest/tpload.ngdoc diff --git a/docs/content/error/$compile/tpload.ngdoc b/docs/content/error/$compile/tpload.ngdoc deleted file mode 100644 index b2b4fb2d0c2c..000000000000 --- a/docs/content/error/$compile/tpload.ngdoc +++ /dev/null @@ -1,11 +0,0 @@ -@ngdoc error -@name $compile:tpload -@fullName Error Loading Template -@description - -This error occurs when {@link ng.$compile `$compile`} attempts to fetch a template from some URL, and the request fails. - -To resolve this error, ensure that the URL of the template is spelled correctly and resolves to correct absolute URL. -The [Chrome Developer Tools](https://developers.google.com/chrome-developer-tools/docs/network#network_panel_overview) might also be helpful in determining why the request failed. - -If you are using {@link ng.$templateCache} to pre-load templates, ensure that the cache was populated with the template. diff --git a/docs/content/error/$templateRequest/tpload.ngdoc b/docs/content/error/$templateRequest/tpload.ngdoc new file mode 100644 index 000000000000..dba0788ce62b --- /dev/null +++ b/docs/content/error/$templateRequest/tpload.ngdoc @@ -0,0 +1,18 @@ +@ngdoc error +@name $templateRequest:tpload +@fullName Error Loading Template +@description + +This error occurs when {@link $templateRequest} attempts to fetch a template from some URL, and +the request fails. + +The template URL might be defined in a directive/component definition, an instance of `ngInclude`, +an instance of `ngMessagesInclude` or a templated route in a `$route` route definition. + +To resolve this error, ensure that the URL of the template is spelled correctly and resolves to +correct absolute URL. +The [Chrome Developer Tools](https://developers.google.com/chrome-developer-tools/docs/network#network_panel_overview) +might also be helpful in determining why the request failed. + +If you are using {@link ng.$templateCache} to pre-load templates, ensure that the cache was +populated with the template. diff --git a/src/ng/templateRequest.js b/src/ng/templateRequest.js index ff699d6cd0ef..7653b92a9126 100644 --- a/src/ng/templateRequest.js +++ b/src/ng/templateRequest.js @@ -1,6 +1,6 @@ 'use strict'; -var $templateRequestMinErr = minErr('$compile'); +var $templateRequestMinErr = minErr('$templateRequest'); /** * @ngdoc provider From b87c6a6d4d3d73fbc4d8aeecca58503d5d958d2c Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 20 Feb 2018 11:10:03 +0000 Subject: [PATCH 119/469] test(*): fix references to `tpload` minerr in tests --- test/ng/compileSpec.js | 2 +- test/ng/templateRequestSpec.js | 4 ++-- test/ngRoute/routeSpec.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index acda50470485..e65b951ff90f 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -1917,7 +1917,7 @@ describe('$compile', function() { expect(function() { $httpBackend.flush(); - }).toThrowMinErr('$compile', 'tpload', 'Failed to load template: hello.html'); + }).toThrowMinErr('$templateRequest', 'tpload', 'Failed to load template: hello.html'); expect(sortedHtml(element)).toBe('
    '); }) ); diff --git a/test/ng/templateRequestSpec.js b/test/ng/templateRequestSpec.js index 3ca323613103..23f05f1e8d08 100644 --- a/test/ng/templateRequestSpec.js +++ b/test/ng/templateRequestSpec.js @@ -144,9 +144,9 @@ describe('$templateRequest', function() { $templateRequest('tpl.html').catch(function(reason) { err = reason; }); $httpBackend.flush(); - expect(err).toEqualMinErr('$compile', 'tpload', + expect(err).toEqualMinErr('$templateRequest', 'tpload', 'Failed to load template: tpl.html (HTTP status: 404 Not Found)'); - expect($exceptionHandler.errors[0]).toEqualMinErr('$compile', 'tpload', + expect($exceptionHandler.errors[0]).toEqualMinErr('$templateRequest', 'tpload', 'Failed to load template: tpl.html (HTTP status: 404 Not Found)'); }); }); diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index 772bdc7bc226..36832ab57884 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -892,7 +892,7 @@ describe('$route', function() { $httpBackend.flush(); expect($exceptionHandler.errors.pop()). - toEqualMinErr('$compile', 'tpload', 'Failed to load template: r1.html'); + toEqualMinErr('$templateRequest', 'tpload', 'Failed to load template: r1.html'); $httpBackend.expectGET('r2.html').respond(''); $location.path('/r2'); From 8b399545a5098cb2576594a26a03cd7268c55fb6 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 20 Feb 2018 11:13:38 +0000 Subject: [PATCH 120/469] docs($route): fix typo in error message --- docs/content/error/$route/norout.ngdoc | 2 +- src/ngRoute/route.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/error/$route/norout.ngdoc b/docs/content/error/$route/norout.ngdoc index 30a12d151442..5dc5a9b8b7ee 100644 --- a/docs/content/error/$route/norout.ngdoc +++ b/docs/content/error/$route/norout.ngdoc @@ -1,6 +1,6 @@ @ngdoc error @name $route:norout -@fullName Tried updating route when with no current route +@fullName Tried updating route with no current route @description Occurs when an attempt is made to update the parameters on the current route when diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js index 76f915b97da6..f0e6c19b9079 100644 --- a/src/ngRoute/route.js +++ b/src/ngRoute/route.js @@ -605,7 +605,7 @@ function $RouteProvider() { // interpolate modifies newParams, only query params are left $location.search(newParams); } else { - throw $routeMinErr('norout', 'Tried updating route when with no current route'); + throw $routeMinErr('norout', 'Tried updating route with no current route'); } } }; From ea0585773bb93fd891576e2271254a17e15f1ddd Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Sat, 10 Feb 2018 22:39:28 +0200 Subject: [PATCH 121/469] fix($resource): fix interceptors and success/error callbacks Previously, action-specific interceptors and `success`/`error` callbacks were executed in inconsistent relative orders and in a way that did not meet the general expectation for interceptor behavior (e.g. ability to recover from errors, performing asynchronous operations, etc). This commit fixes the behavior to make it more consistent and expected. The main differences are that `success`/`error` callbacks will now be run _after_ `response`/`responseError` interceptors complete (even if interceptors return a promise) and the correct callback will be called based on the result of the interceptor (e.g. if the `responseError` interceptor recovers from an error, the `success` callback will be called). See also https://github.com/angular/angular.js/issues/9334#issuecomment-364650642. This commit also replaces the use of `success`/`error` callbacks in the docs with using the returned promise. Fixes #6731 Fixes #9334 Closes #6865 Closes #16446 BREAKING CHANGE: If you are not using `success` or `error` callbacks with `$resource`, your app should not be affected by this change. If you are using `success` or `error` callbacks (with or without response interceptors), one (subtle) difference is that throwing an error inside the callbacks will not propagate to the returned `$promise`. Therefore, you should try to use the promises whenever possible. E.g.: ```js // Avoid User.query(function onSuccess(users) { throw new Error(); }). $promise. catch(function onError() { /* Will not be called. */ }); // Prefer User.query(). $promise. then(function onSuccess(users) { throw new Error(); }). catch(function onError() { /* Will be called. */ }); ``` Finally, if you are using `success` or `error` callbacks with response interceptors, the callbacks will now always run _after_ the interceptors (and wait for them to resolve in case they return a promise). Previously, the `error` callback was called before the `responseError` interceptor and the `success` callback was synchronously called after the `response` interceptor. E.g.: ```js var User = $resource('/api/users/:id', {id: '@id'}, { get: { method: 'get', interceptor: { response: function(response) { console.log('responseInterceptor-1'); return $timeout(1000).then(function() { console.log('responseInterceptor-2'); return response.resource; }); }, responseError: function(response) { console.log('responseErrorInterceptor-1'); return $timeout(1000).then(function() { console.log('responseErrorInterceptor-2'); return $q.reject('Ooops!'); }); } } } }); var onSuccess = function(value) { console.log('successCallback', value); }; var onError = function(error) { console.log('errorCallback', error); }; // Assuming the following call is successful... User.get({id: 1}, onSuccess, onError); // Old behavior: // responseInterceptor-1 // successCallback, {/* Promise object */} // responseInterceptor-2 // New behavior: // responseInterceptor-1 // responseInterceptor-2 // successCallback, {/* User object */} // Assuming the following call returns an error... User.get({id: 2}, onSuccess, onError); // Old behavior: // errorCallback, {/* Response object */} // responseErrorInterceptor-1 // responseErrorInterceptor-2 // New behavior: // responseErrorInterceptor-1 // responseErrorInterceptor-2 // errorCallback, Ooops! ``` --- src/ngResource/resource.js | 395 ++++++++++++++++++-------------- test/ngResource/resourceSpec.js | 288 +++++++++++++++++------ 2 files changed, 441 insertions(+), 242 deletions(-) diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js index c8a79274ca2b..11bb45ba20b3 100644 --- a/src/ngResource/resource.js +++ b/src/ngResource/resource.js @@ -110,13 +110,13 @@ function shallowClearAndCopy(src, dst) { * * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in * `actions` methods. If a parameter value is a function, it will be called every time - * a param value needs to be obtained for a request (unless the param was overridden). The function - * will be passed the current data value as an argument. + * a param value needs to be obtained for a request (unless the param was overridden). The + * function will be passed the current data value as an argument. * * Each key value in the parameter object is first bound to url template if present and then any * excess keys are appended to the url search query after the `?`. * - * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in + * Given a template `/path/:verb` and parameter `{verb: 'greet', salutation: 'Hello'}` results in * URL `/path/greet?salutation=Hello`. * * If the parameter value is prefixed with `@`, then the value for that parameter will be @@ -125,7 +125,7 @@ function shallowClearAndCopy(src, dst) { * For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of * `someParam` will be `data.someProp`. * Note that the parameter will be ignored, when calling a "GET" action method (i.e. an action - * method that does not accept a request body) + * method that does not accept a request body). * * @param {Object.=} actions Hash with declaration of custom actions that will be available * in addition to the default set of resource actions (see below). If a custom action has the same @@ -134,9 +134,11 @@ function shallowClearAndCopy(src, dst) { * * The declaration should be created in the format of {@link ng.$http#usage $http.config}: * - * {action1: {method:?, params:?, isArray:?, headers:?, ...}, - * action2: {method:?, params:?, isArray:?, headers:?, ...}, - * ...} + * { + * action1: {method:?, params:?, isArray:?, headers:?, ...}, + * action2: {method:?, params:?, isArray:?, headers:?, ...}, + * ... + * } * * Where: * @@ -148,55 +150,58 @@ function shallowClearAndCopy(src, dst) { * the parameter value is a function, it will be called every time when a param value needs to * be obtained for a request (unless the param was overridden). The function will be passed the * current data value as an argument. - * - **`url`** – {string} – action specific `url` override. The url templating is supported just + * - **`url`** – {string} – Action specific `url` override. The url templating is supported just * like for the resource-level urls. * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, * see `returns` section. * - **`transformRequest`** – * `{function(data, headersGetter)|Array.}` – - * transform function or an array of such functions. The transform function takes the http + * Transform function or an array of such functions. The transform function takes the http * request body and headers and returns its transformed (typically serialized) version. * By default, transformRequest will contain one function that checks if the request data is * an object and serializes it using `angular.toJson`. To prevent this behavior, set * `transformRequest` to an empty array: `transformRequest: []` * - **`transformResponse`** – * `{function(data, headersGetter, status)|Array.}` – - * transform function or an array of such functions. The transform function takes the http + * Transform function or an array of such functions. The transform function takes the HTTP * response body, headers and status and returns its transformed (typically deserialized) * version. * By default, transformResponse will contain one function that checks if the response looks * like a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior, * set `transformResponse` to an empty array: `transformResponse: []` - * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory} is supplied, this cache will be used for - * caching. - * - **`timeout`** – `{number}` – timeout in milliseconds.
    + * - **`cache`** – `{boolean|Cache}` – A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response. + * See {@link $http#caching $http Caching} for more information. + * - **`timeout`** – `{number}` – Timeout in milliseconds.
    * **Note:** In contrast to {@link ng.$http#usage $http.config}, {@link ng.$q promises} are - * **not** supported in $resource, because the same value would be used for multiple requests. + * **not** supported in `$resource`, because the same value would be used for multiple requests. * If you are looking for a way to cancel requests, you should use the `cancellable` option. - * - **`cancellable`** – `{boolean}` – if set to true, the request made by a "non-instance" call - * will be cancelled (if not already completed) by calling `$cancelRequest()` on the call's - * return value. Calling `$cancelRequest()` for a non-cancellable or an already - * completed/cancelled request will have no effect.
    - * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the + * - **`cancellable`** – `{boolean}` – If true, the request made by a "non-instance" call will be + * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return + * value. Calling `$cancelRequest()` for a non-cancellable or an already completed/cancelled + * request will have no effect. + * - **`withCredentials`** – `{boolean}` – Whether to set the `withCredentials` flag on the * XHR object. See - * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) + * [XMLHttpRequest.withCredentials](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials) * for more information. - * - **`responseType`** - `{string}` - see - * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). - * - **`interceptor`** - `{Object=}` - The interceptor object has four optional methods - + * - **`responseType`** – `{string}` – See + * [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType). + * - **`interceptor`** – `{Object=}` – The interceptor object has four optional methods - * `request`, `requestError`, `response`, and `responseError`. See - * {@link ng.$http $http interceptors} for details. Note that `request`/`requestError` - * interceptors are applied before calling `$http`, thus before any global `$http` interceptors. - * The resource instance or array object is accessible by the `resource` property of the - * `http response` object passed to response interceptors. + * {@link ng.$http#interceptors $http interceptors} for details. Note that + * `request`/`requestError` interceptors are applied before calling `$http`, thus before any + * global `$http` interceptors. Also, rejecting or throwing an error inside the `request` + * interceptor will result in calling the `responseError` interceptor. + * The resource instance or collection is available on the `resource` property of the + * `http response` object passed to `response`/`responseError` interceptors. * Keep in mind that the associated promise will be resolved with the value returned by the - * response interceptor, if one is specified. The default response interceptor returns - * `response.resource` (i.e. the resource instance or array). - * - **`hasBody`** - `{boolean}` - allows to specify if a request body should be included or not. - * If not specified only POST, PUT and PATCH requests will have a body. - * + * response interceptors. Make sure you return an appropriate value and not the `response` + * object passed as input. For reference, the default `response` interceptor (which gets applied + * if you don't specify a custom one) returns `response.resource`.
    + * See {@link ngResource.$resource#using-interceptors below} for an example of using + * interceptors in `$resource`. + * - **`hasBody`** – `{boolean}` – If true, then the request will have a body. + * If not specified, then only POST, PUT and PATCH requests will have a body. * * @param {Object} options Hash with custom settings that should extend the * default `$resourceProvider` behavior. The supported options are: * @@ -209,27 +214,29 @@ function shallowClearAndCopy(src, dst) { * @returns {Object} A resource "class" object with methods for the default set of resource actions * optionally extended with custom `actions`. The default set contains these actions: * ```js - * { 'get': {method:'GET'}, - * 'save': {method:'POST'}, - * 'query': {method:'GET', isArray:true}, - * 'remove': {method:'DELETE'}, - * 'delete': {method:'DELETE'} }; + * { + * 'get': {method: 'GET'}, + * 'save': {method: 'POST'}, + * 'query': {method: 'GET', isArray: true}, + * 'remove': {method: 'DELETE'}, + * 'delete': {method: 'DELETE'} + * } * ``` * - * Calling these methods invoke an {@link ng.$http} with the specified http method, - * destination and parameters. When the data is returned from the server then the object is an - * instance of the resource class. The actions `save`, `remove` and `delete` are available on it - * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, - * read, update, delete) on server-side data like this: + * Calling these methods invoke {@link ng.$http} with the specified http method, destination and + * parameters. When the data is returned from the server then the object is an instance of the + * resource class. The actions `save`, `remove` and `delete` are available on it as methods with + * the `$` prefix. This allows you to easily perform CRUD operations (create, read, update, + * delete) on server-side data like this: * ```js - * var User = $resource('/user/:userId', {userId:'@id'}); - * var user = User.get({userId:123}, function() { + * var User = $resource('/user/:userId', {userId: '@id'}); + * User.get({userId: 123}).$promise.then(function(user) { * user.abc = true; * user.$save(); * }); * ``` * - * It is important to realize that invoking a $resource object method immediately returns an + * It is important to realize that invoking a `$resource` object method immediately returns an * empty reference (object or array depending on `isArray`). Once the data is returned from the * server the existing reference is populated with the actual data. This is a useful trick since * usually the resource is assigned to a model which is then rendered by the view. Having an empty @@ -252,30 +259,31 @@ function shallowClearAndCopy(src, dst) { * * * Success callback is called with (value (Object|Array), responseHeaders (Function), - * status (number), statusText (string)) arguments, where the value is the populated resource + * status (number), statusText (string)) arguments, where `value` is the populated resource * instance or collection object. The error callback is called with (httpResponse) argument. * - * Class actions return empty instance (with additional properties below). - * Instance actions return promise of the action. + * Class actions return an empty instance (with the additional properties listed below). + * Instance actions return a promise for the operation. * * The Resource instances and collections have these additional properties: * - * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this + * - `$promise`: The {@link ng.$q promise} of the original server interaction that created this * instance or collection. * * On success, the promise is resolved with the same resource instance or collection object, - * updated with data from server. This makes it easy to use in - * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view + * updated with data from server. This makes it easy to use in the + * {@link ngRoute.$routeProvider `resolve` section of `$routeProvider.when()`} to defer view * rendering until the resource(s) are loaded. * * On failure, the promise is rejected with the {@link ng.$http http response} object. * * If an interceptor object was provided, the promise will instead be resolved with the value - * returned by the interceptor. + * returned by the response interceptor (on success) or responceError interceptor (on failure). * * - `$resolved`: `true` after first server interaction is completed (either with success or * rejection), `false` before that. Knowing if the Resource has been resolved is useful in - * data-binding. + * data-binding. If there is a response/responseError interceptor and it returns a promise, + * `$resolved` will wait for that too. * * The Resource instances and collections have these additional methods: * @@ -292,121 +300,128 @@ function shallowClearAndCopy(src, dst) { * * @example * - * ### Credit card resource + * ### Basic usage * - * ```js - // Define CreditCard class - var CreditCard = $resource('/user/:userId/card/:cardId', - {userId:123, cardId:'@id'}, { - charge: {method:'POST', params:{charge:true}} - }); + ```js + // Define a CreditCard class + var CreditCard = $resource('/users/:userId/cards/:cardId', + {userId: 123, cardId: '@id'}, { + charge: {method: 'POST', params: {charge: true}} + }); // We can retrieve a collection from the server - var cards = CreditCard.query(function() { - // GET: /user/123/card - // server returns: [ {id:456, number:'1234', name:'Smith'} ]; + var cards = CreditCard.query(); + // GET: /users/123/cards + // server returns: [{id: 456, number: '1234', name: 'Smith'}] + // Wait for the request to complete + cards.$promise.then(function() { var card = cards[0]; - // each item is an instance of CreditCard + + // Each item is an instance of CreditCard expect(card instanceof CreditCard).toEqual(true); - card.name = "J. Smith"; - // non GET methods are mapped onto the instances + + // Non-GET methods are mapped onto the instances + card.name = 'J. Smith'; card.$save(); - // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} - // server returns: {id:456, number:'1234', name: 'J. Smith'}; + // POST: /users/123/cards/456 {id: 456, number: '1234', name: 'J. Smith'} + // server returns: {id: 456, number: '1234', name: 'J. Smith'} - // our custom method is mapped as well. - card.$charge({amount:9.99}); - // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} + // Our custom method is mapped as well (since it uses POST) + card.$charge({amount: 9.99}); + // POST: /users/123/cards/456?amount=9.99&charge=true {id: 456, number: '1234', name: 'J. Smith'} }); - // we can create an instance as well - var newCard = new CreditCard({number:'0123'}); - newCard.name = "Mike Smith"; - newCard.$save(); - // POST: /user/123/card {number:'0123', name:'Mike Smith'} - // server returns: {id:789, number:'0123', name: 'Mike Smith'}; - expect(newCard.id).toEqual(789); - * ``` + // We can create an instance as well + var newCard = new CreditCard({number: '0123'}); + newCard.name = 'Mike Smith'; + + var savePromise = newCard.$save(); + // POST: /users/123/cards {number: '0123', name: 'Mike Smith'} + // server returns: {id: 789, number: '0123', name: 'Mike Smith'} + + savePromise.then(function() { + // Once the promise is resolved, the created instance + // is populated with the data returned by the server + expect(newCard.id).toEqual(789); + }); + ``` * - * The object returned from this function execution is a resource "class" which has "static" method - * for each action in the definition. + * The object returned from a call to `$resource` is a resource "class" which has one "static" + * method for each action in the definition. * - * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and - * `headers`. + * Calling these methods invokes `$http` on the `url` template with the given HTTP `method`, + * `params` and `headers`. * * @example * - * ### User resource + * ### Accessing the response * * When the data is returned from the server then the object is an instance of the resource type and * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD * operations (create, read, update, delete) on server-side data. - + * ```js - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}, function(user) { + var User = $resource('/users/:userId', {userId: '@id'}); + User.get({userId: 123}).$promise.then(function(user) { user.abc = true; user.$save(); }); ``` * - * It's worth noting that the success callback for `get`, `query` and other methods gets passed - * in the response that came from the server as well as $http header getter function, so one - * could rewrite the above example and get access to http headers as: + * It's worth noting that the success callback for `get`, `query` and other methods gets called with + * the resource instance (populated with the data that came from the server) as well as an `$http` + * header getter function, the HTTP status code and the response status text. So one could rewrite + * the above example and get access to HTTP headers as follows: * ```js - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}, function(user, getResponseHeaders){ + var User = $resource('/users/:userId', {userId: '@id'}); + User.get({userId: 123}, function(user, getResponseHeaders) { user.abc = true; user.$save(function(user, putResponseHeaders) { - //user => saved user object - //putResponseHeaders => $http header getter + // `user` => saved `User` object + // `putResponseHeaders` => `$http` header getter }); }); ``` * - * You can also access the raw `$http` promise via the `$promise` property on the object returned - * - ``` - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}) - .$promise.then(function(user) { - $scope.user = user; - }); - ``` - * * @example * - * ### Creating a custom 'PUT' request + * ### Creating custom actions * - * In this example we create a custom method on our resource to make a PUT request - * ```js - * var app = angular.module('app', ['ngResource', 'ngRoute']); - * - * // Some APIs expect a PUT request in the format URL/object/ID - * // Here we are creating an 'update' method - * app.factory('Notes', ['$resource', function($resource) { - * return $resource('/notes/:id', null, - * { - * 'update': { method:'PUT' } - * }); - * }]); - * - * // In our controller we get the ID from the URL using ngRoute and $routeParams - * // We pass in $routeParams and our Notes factory along with $scope - * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', - function($scope, $routeParams, Notes) { - * // First get a note object from the factory - * var note = Notes.get({ id:$routeParams.id }); - * $id = note.id; - * - * // Now call update passing in the ID first then the object you are updating - * Notes.update({ id:$id }, note); - * - * // This will PUT /notes/ID with the note object in the request payload - * }]); - * ``` + * In this example we create a custom method on our resource to make a PUT request: + * + ```js + var app = angular.module('app', ['ngResource']); + + // Some APIs expect a PUT request in the format URL/object/ID + // Here we are creating an 'update' method + app.factory('Notes', ['$resource', function($resource) { + return $resource('/notes/:id', {id: '@id'}, { + update: {method: 'PUT'} + }); + }]); + + // In our controller we get the ID from the URL using `$location` + app.controller('NotesCtrl', ['$location', 'Notes', function($location, Notes) { + // First, retrieve the corresponding `Note` object from the server + // (Assuming a URL of the form `.../notes?id=XYZ`) + var noteId = $location.search().id; + var note = Notes.get({id: noteId}); + + note.$promise.then(function() { + note.content = 'Hello, world!'; + + // Now call `update` to save the changes on the server + Notes.update(note); + // This will PUT /notes/ID with the note object as the request payload + + // Since `update` is a non-GET method, it will also be available on the instance + // (prefixed with `$`), so we could replace the `Note.update()` call with: + //note.$update(); + }); + }]); + ``` * * @example * @@ -417,7 +432,7 @@ function shallowClearAndCopy(src, dst) { * ```js // ...defining the `Hotel` resource... - var Hotel = $resource('/api/hotel/:id', {id: '@id'}, { + var Hotel = $resource('/api/hotels/:id', {id: '@id'}, { // Let's make the `query()` method cancellable query: {method: 'get', isArray: true, cancellable: true} }); @@ -427,14 +442,54 @@ function shallowClearAndCopy(src, dst) { this.onDestinationChanged = function onDestinationChanged(destination) { // We don't care about any pending request for hotels // in a different destination any more - this.availableHotels.$cancelRequest(); + if (this.availableHotels) { + this.availableHotels.$cancelRequest(); + } - // Let's query for hotels in '' - // (calls: /api/hotel?location=) + // Let's query for hotels in `destination` + // (calls: /api/hotels?location=) this.availableHotels = Hotel.query({location: destination}); }; ``` * + * @example + * + * ### Using interceptors + * + * You can use interceptors to transform the request or response, perform additional operations, and + * modify the returned instance/collection. The following example, uses `request` and `response` + * interceptors to augment the returned instance with additional info: + * + ```js + var Thing = $resource('/api/things/:id', {id: '@id'}, { + save: { + method: 'POST', + interceptor: { + request: function(config) { + // Before the request is sent out, store a timestamp on the request config + config.requestTimestamp = Date.now(); + return config; + }, + response: function(response) { + // Get the instance from the response object + var instance = response.resource; + + // Augment the instance with a custom `saveLatency` property, computed as the time + // between sending the request and receiving the response. + instance.saveLatency = Date.now() - response.config.requestTimestamp; + + // Return the instance + return instance; + } + } + } + }); + + Thing.save({foo: 'bar'}).$promise.then(function(thing) { + console.log('That thing was saved in ' + thing.saveLatency + 'ms.'); + }); + ``` + * */ angular.module('ngResource', ['ng']). info({ angularVersion: '"NG_VERSION_FULL"' }). @@ -667,34 +722,34 @@ angular.module('ngResource', ['ng']). } Resource[name] = function(a1, a2, a3, a4) { - var params = {}, data, success, error; + var params = {}, data, onSuccess, onError; switch (arguments.length) { case 4: - error = a4; - success = a3; + onError = a4; + onSuccess = a3; // falls through case 3: case 2: if (isFunction(a2)) { if (isFunction(a1)) { - success = a1; - error = a2; + onSuccess = a1; + onError = a2; break; } - success = a2; - error = a3; + onSuccess = a2; + onError = a3; // falls through } else { params = a1; data = a2; - success = a3; + onSuccess = a3; break; } // falls through case 1: - if (isFunction(a1)) success = a1; + if (isFunction(a1)) onSuccess = a1; else if (hasBody) data = a1; else params = a1; break; @@ -714,11 +769,14 @@ angular.module('ngResource', ['ng']). var responseInterceptor = action.interceptor && action.interceptor.response || defaultResponseInterceptor; var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || - undefined; - var hasError = !!error; - var hasResponseErrorInterceptor = !!responseErrorInterceptor; + $q.reject; + var successCallback = onSuccess ? function(val) { + onSuccess(val, response.headers, response.status, response.statusText); + } : undefined; + var errorCallback = onError || undefined; var timeoutDeferred; var numericTimeoutPromise; + var response; forEach(action, function(value, key) { switch (key) { @@ -754,8 +812,8 @@ angular.module('ngResource', ['ng']). catch(requestErrorInterceptor). then($http); - promise = promise.then(function(response) { - var data = response.data; + promise = promise.then(function(resp) { + var data = resp.data; if (data) { // Need to convert action.isArray to boolean in case it is undefined @@ -783,12 +841,14 @@ angular.module('ngResource', ['ng']). value.$promise = promise; // Restore the promise } } - response.resource = value; - return response; - }, function(response) { - response.resource = value; - return $q.reject(response); + resp.resource = value; + response = resp; + return responseInterceptor(resp); + }, function(rejectionOrResponse) { + rejectionOrResponse.resource = value; + response = rejectionOrResponse; + return responseErrorInterceptor(rejectionOrResponse); }); promise = promise['finally'](function() { @@ -800,25 +860,8 @@ angular.module('ngResource', ['ng']). } }); - promise = promise.then( - function(response) { - var value = responseInterceptor(response); - (success || noop)(value, response.headers, response.status, response.statusText); - return value; - }, - (hasError || hasResponseErrorInterceptor) ? - function(response) { - if (hasError && !hasResponseErrorInterceptor) { - // Avoid `Possibly Unhandled Rejection` error, - // but still fulfill the returned promise with a rejection - promise.catch(noop); - } - if (hasError) error(response); - return hasResponseErrorInterceptor ? - responseErrorInterceptor(response) : - $q.reject(response); - } : - undefined); + // Run the `success`/`error` callbacks, but do not let them affect the returned promise. + promise.then(successCallback, errorCallback); if (!isInstanceCall) { // we are creating instance / collection diff --git a/test/ngResource/resourceSpec.js b/test/ngResource/resourceSpec.js index 00fce4b662a8..077281a134ba 100644 --- a/test/ngResource/resourceSpec.js +++ b/test/ngResource/resourceSpec.js @@ -34,11 +34,11 @@ describe('basic usage', function() { callback = jasmine.createSpy('callback'); })); - afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); }); + describe('isValidDottedPath', function() { /* global isValidDottedPath: false */ it('should support arbitrary dotted names', function() { @@ -1312,102 +1312,225 @@ describe('basic usage', function() { }); }); - it('should allow per action response interceptor that gets full response', function() { - CreditCard = $resource('/CreditCard', {}, { - query: { - method: 'get', - isArray: true, - interceptor: { - response: function(response) { - return response; - } + + describe('responseInterceptor', function() { + it('should allow per action response interceptor that gets full response', function() { + var response; + + $httpBackend.expect('GET', '/CreditCard').respond(201, {id: 1}, {foo: 'bar'}, 'Ack'); + CreditCard = $resource('/CreditCard', {}, { + get: { + method: 'get', + interceptor: {response: function(resp) { response = resp; }} } - } + }); + + var cc = CreditCard.get(); + $httpBackend.flush(); + + expect(response.resource).toBe(cc); + expect(response.config).toBeDefined(); + expect(response.status).toBe(201); + expect(response.statusText).toBe('Ack'); + expect(response.headers()).toEqual({foo: 'bar'}); }); - $httpBackend.expect('GET', '/CreditCard').respond([{id: 1}]); - var ccs = CreditCard.query(); + it('should allow per action responseError interceptor that gets full response', function() { + var response; - ccs.$promise.then(callback); + $httpBackend.expect('GET', '/CreditCard').respond(404, {ignored: 'stuff'}, {foo: 'bar'}, 'Ack'); + CreditCard = $resource('/CreditCard', {}, { + get: { + method: 'get', + interceptor: {responseError: function(resp) { response = resp; }} + } + }); - $httpBackend.flush(); - expect(callback).toHaveBeenCalledOnce(); + var cc = CreditCard.get(); + $httpBackend.flush(); - var response = callback.calls.mostRecent().args[0]; - expect(response.resource).toBe(ccs); - expect(response.status).toBe(200); - expect(response.config).toBeDefined(); - }); + expect(response.resource).toBe(cc); + expect(response.config).toBeDefined(); + expect(response.status).toBe(404); + expect(response.statusText).toBe('Ack'); + expect(response.headers()).toEqual({foo: 'bar'}); + }); - it('should allow per action responseError interceptor that gets full response', function() { - CreditCard = $resource('/CreditCard', {}, { - query: { - method: 'get', - isArray: true, - interceptor: { - responseError: function(response) { - return response; + it('should fulfill the promise with the value returned by the response interceptor', + function() { + $httpBackend.whenGET('/CreditCard').respond(200); + CreditCard = $resource('/CreditCard', {}, { + test1: { + method: 'get', + interceptor: {response: function() { return 'foo'; }} + }, + test2: { + method: 'get', + interceptor: {response: function() { return $q.resolve('bar'); }} + }, + test3: { + method: 'get', + interceptor: {response: function() { return $q.reject('baz'); }} } - } + }); + + CreditCard.test1().$promise.then(callback); + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnceWith('foo'); + + callback.calls.reset(); + + CreditCard.test2().$promise.then(callback); + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnceWith('bar'); + + callback.calls.reset(); + + CreditCard.test3().$promise.then(null, callback); + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnceWith('baz'); } - }); + ); - $httpBackend.expect('GET', '/CreditCard').respond(404); - var ccs = CreditCard.query(); + it('should fulfill the promise with the value returned by the responseError interceptor', + function() { + $httpBackend.whenGET('/CreditCard').respond(404); + CreditCard = $resource('/CreditCard', {}, { + test1: { + method: 'get', + interceptor: {responseError: function() { return 'foo'; }} + }, + test2: { + method: 'get', + interceptor: {responseError: function() { return $q.resolve('bar'); }} + }, + test3: { + method: 'get', + interceptor: {responseError: function() { return $q.reject('baz'); }} + } + }); - ccs.$promise.then(callback); + CreditCard.test1().$promise.then(callback); + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnceWith('foo'); - $httpBackend.flush(); - expect(callback).toHaveBeenCalledOnce(); + callback.calls.reset(); - var response = callback.calls.mostRecent().args[0]; - expect(response.resource).toBe(ccs); - expect(response.status).toBe(404); - expect(response.config).toBeDefined(); - }); + CreditCard.test2().$promise.then(callback); + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnceWith('bar'); + callback.calls.reset(); + + CreditCard.test3().$promise.then(null, callback); + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnceWith('baz'); + } + ); - it('should fulfill the promise with the value returned by the responseError interceptor', - inject(function($q) { + + it('should call the success callback when response interceptor succeeds', function() { + $httpBackend.whenGET('/CreditCard').respond(200); CreditCard = $resource('/CreditCard', {}, { test1: { - method: 'GET', - interceptor: {responseError: function() { return 'foo'; }} + method: 'get', + interceptor: {response: function() { return 'foo'; }} }, test2: { - method: 'GET', - interceptor: {responseError: function() { return $q.resolve('bar'); }} - }, - test3: { - method: 'GET', - interceptor: {responseError: function() { return $q.reject('baz'); }} + method: 'get', + interceptor: {response: function() { return $q.resolve('bar'); }} } }); - $httpBackend.whenGET('/CreditCard').respond(404); + CreditCard.test1(callback); + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnceWith('foo', jasmine.any(Function), 200, ''); callback.calls.reset(); - CreditCard.test1().$promise.then(callback); + + CreditCard.test2(callback); $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnceWith('bar', jasmine.any(Function), 200, ''); + }); + + + it('should call the error callback when response interceptor fails', function() { + $httpBackend.whenGET('/CreditCard').respond(200); + CreditCard = $resource('/CreditCard', {}, { + test1: { + method: 'get', + interceptor: {response: function() { throw 'foo'; }} + }, + test2: { + method: 'get', + interceptor: {response: function() { return $q.reject('bar'); }} + } + }); + CreditCard.test1(noop, callback); + $httpBackend.flush(); expect(callback).toHaveBeenCalledOnceWith('foo'); callback.calls.reset(); - CreditCard.test2().$promise.then(callback); - $httpBackend.flush(); + CreditCard.test2(noop, callback); + $httpBackend.flush(); expect(callback).toHaveBeenCalledOnceWith('bar'); + }); + + + it('should call the success callback when responseError interceptor succeeds', function() { + $httpBackend.whenGET('/CreditCard').respond(404); + CreditCard = $resource('/CreditCard', {}, { + test1: { + method: 'get', + interceptor: {responseError: function() { return 'foo'; }} + }, + test2: { + method: 'get', + interceptor: {responseError: function() { return $q.resolve('bar'); }} + } + }); + + CreditCard.test1(callback); + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnceWith('foo', jasmine.any(Function), 404, ''); callback.calls.reset(); - CreditCard.test3().$promise.then(null, callback); + + CreditCard.test2(callback); $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnceWith('bar', jasmine.any(Function), 404, ''); + }); + - expect(callback).toHaveBeenCalledOnceWith('baz'); - }) - ); + it('should call the error callback when responseError interceptor fails', function() { + $httpBackend.whenGET('/CreditCard').respond(404); + CreditCard = $resource('/CreditCard', {}, { + test1: { + method: 'get', + interceptor: {responseError: function() { throw 'foo'; }} + }, + test2: { + method: 'get', + interceptor: {responseError: function() { return $q.reject('bar'); }} + } + }); + + CreditCard.test1(noop, callback); + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnceWith('foo'); + + callback.calls.reset(); + + CreditCard.test2(noop, callback); + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnceWith('bar'); + }); + }); }); @@ -1810,7 +1933,7 @@ describe('extra params', function() { }); describe('errors', function() { - var $httpBackend, $resource, $q, $rootScope; + var $httpBackend, $resource; beforeEach(module(function($exceptionHandlerProvider) { $exceptionHandlerProvider.mode('log'); @@ -1821,8 +1944,6 @@ describe('errors', function() { beforeEach(inject(function($injector) { $httpBackend = $injector.get('$httpBackend'); $resource = $injector.get('$resource'); - $q = $injector.get('$q'); - $rootScope = $injector.get('$rootScope'); })); @@ -1961,7 +2082,7 @@ describe('handling rejections', function() { function() { $httpBackend.expectGET('/CreditCard/123').respond(null); var CreditCard = $resource('/CreditCard/:id'); - var cc = CreditCard.get({id: 123}, + CreditCard.get({id: 123}, function(res) { throw new Error('should be caught'); }, function() {}); @@ -1976,7 +2097,7 @@ describe('handling rejections', function() { function() { $httpBackend.expectGET('/CreditCard/123').respond(null); var CreditCard = $resource('/CreditCard/:id'); - var cc = CreditCard.get({id: 123}, + CreditCard.get({id: 123}, function(res) { throw new Error('should be caught'); }); $httpBackend.flush(); @@ -1996,7 +2117,7 @@ describe('handling rejections', function() { } }); - var cc = CreditCard.get({id: 123}, + CreditCard.get({id: 123}, function(res) { throw new Error('should be caught'); }, function() {}); @@ -2017,7 +2138,7 @@ describe('handling rejections', function() { } }); - var cc = CreditCard.get({id: 123}, + CreditCard.get({id: 123}, function(res) { throw new Error('should be caught'); }); $httpBackend.flush(); @@ -2026,6 +2147,41 @@ describe('handling rejections', function() { } ); + + it('should not propagate exceptions in success callback to the returned promise', function() { + var successCallbackSpy = jasmine.createSpy('successCallback').and.throwError('error'); + var promiseResolveSpy = jasmine.createSpy('promiseResolve'); + var promiseRejectSpy = jasmine.createSpy('promiseReject'); + + $httpBackend.expectGET('/CreditCard/123').respond(null); + var CreditCard = $resource('/CreditCard/:id'); + CreditCard.get({id: 123}, successCallbackSpy). + $promise.then(promiseResolveSpy, promiseRejectSpy); + + $httpBackend.flush(); + expect(successCallbackSpy).toHaveBeenCalled(); + expect(promiseResolveSpy).toHaveBeenCalledWith(jasmine.any(CreditCard)); + expect(promiseRejectSpy).not.toHaveBeenCalled(); + }); + + + it('should not be able to recover from inside the error callback', function() { + var errorCallbackSpy = jasmine.createSpy('errorCallback').and.returnValue({id: 123}); + var promiseResolveSpy = jasmine.createSpy('promiseResolve'); + var promiseRejectSpy = jasmine.createSpy('promiseReject'); + + $httpBackend.expectGET('/CreditCard/123').respond(404); + var CreditCard = $resource('/CreditCard/:id'); + CreditCard.get({id: 123}, noop, errorCallbackSpy). + $promise.then(promiseResolveSpy, promiseRejectSpy); + + $httpBackend.flush(); + expect(errorCallbackSpy).toHaveBeenCalled(); + expect(promiseResolveSpy).not.toHaveBeenCalled(); + expect(promiseRejectSpy).toHaveBeenCalledWith(jasmine.objectContaining({status: 404})); + }); + + describe('requestInterceptor', function() { var rejectReason = {'lol':'cat'}; var $q, $rootScope; From 33b251d55e48da867cb2998e6d87ef33c3fec416 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 21 Feb 2018 08:33:28 +0000 Subject: [PATCH 122/469] docs(misc): add version-support-status page (#16460) Closes #16058 Closes #16458 --- README.md | 9 +++- docs/app/assets/css/docs.css | 8 +++ docs/content/api/index.ngdoc | 10 +++- .../content/misc/version-support-status.ngdoc | 54 +++++++++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 docs/content/misc/version-support-status.ngdoc diff --git a/README.md b/README.md index ae444e3ccdb8..c2d1a9db2981 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,14 @@ It also helps with server-side communication, taming async callbacks with promis and it makes client-side navigation and deep linking with hashbang urls or HTML5 pushState a piece of cake. Best of all? It makes development fun! +-------------------- + +##### AngularJS will be moving to Long Term Support (LTS) mode on July 1st 2018: [Find out more](misc/version-support-status) + +##### Looking for the new Angular? Go here: https://github.com/angular/angular + +-------------------- + * Web site: https://angularjs.org * Tutorial: https://docs.angularjs.org/tutorial * API Docs: https://docs.angularjs.org/api @@ -20,7 +28,6 @@ piece of cake. Best of all? It makes development fun! * Core Development: [DEVELOPERS.md](DEVELOPERS.md) * Dashboard: https://dashboard.angularjs.org -##### Looking for the new Angular? Go here: https://github.com/angular/angular Documentation -------------------- diff --git a/docs/app/assets/css/docs.css b/docs/app/assets/css/docs.css index 54d81ada753d..65abc5247f0c 100644 --- a/docs/app/assets/css/docs.css +++ b/docs/app/assets/css/docs.css @@ -934,6 +934,14 @@ toc-container > div > toc-tree > ul > li > toc-tree > ul > li toc-tree > ul li { font-size: 13px; } +.dev-status span { + padding: 2px 8px; + border-radius: 5px; +} +.security span { background-color: orange; } +.stable span { background-color: green; color: white; } +.current span { background-color: blue; color: white; } + @media handheld and (max-width:800px), screen and (max-device-width:800px), screen and (max-width:800px) { .navbar { min-height: auto; diff --git a/docs/content/api/index.ngdoc b/docs/content/api/index.ngdoc index 8b954a86bd05..d34ddfb64c1d 100644 --- a/docs/content/api/index.ngdoc +++ b/docs/content/api/index.ngdoc @@ -3,7 +3,15 @@ @description # AngularJS API Docs -Welcome to the AngularJS API docs page. These pages contain the AngularJS reference materials for version . + +
    +**AngularJS will be moving to Long Term Support (LTS) mode on July 1st 2018.**: [Find out more](misc/version-support-status). +
    + +## Welcome to the AngularJS API docs page. + +These pages contain the AngularJS reference materials for version . + The documentation is organized into **{@link guide/module modules}** which contain various components of an AngularJS application. These components are {@link guide/directive directives}, {@link guide/services services}, {@link guide/filter filters}, {@link guide/providers providers}, {@link guide/templates templates}, global APIs, and testing mocks. diff --git a/docs/content/misc/version-support-status.ngdoc b/docs/content/misc/version-support-status.ngdoc new file mode 100644 index 000000000000..aff4309393dd --- /dev/null +++ b/docs/content/misc/version-support-status.ngdoc @@ -0,0 +1,54 @@ +@ngdoc overview +@name Version Support Status +@description + +# Version Support Status + +This page describes the support status of the significant versions of AngularJS. + +
    + AngularJS is planning one more significant release, version 1.7, and on July 1, 2018 it will enter a 3 year Long Term Support period. +
    + +### Until July 1st 2018 + +Any version branch not shown in the following table (e.g. 1.5.x) is no longer being developed. + + + + + + + + + + +
    VersionStatusComments
    1.2.xSecurity patches onlyLast version to provide IE 8 support
    1.6.xPatch ReleasesMinor features, bug fixes, security patches - no breaking changes
    1.7.xActive Development1.7.0 (not yet released) will be the last release of AngularJS to contain breaking changes
    + +### After July 1st 2018 + +Any version branch not shown in the following table (e.g. 1.6.x) is no longer being developed. + + + + + + + + + +
    VersionStatusComments
    1.2.xLong Term SupportLast version to provide IE 8 support
    1.7.xLong Term SupportSee [Long Term Support](#long-term-support) section below.
    + +### Long Term Support + +On July 1st 2018, we will enter a Long Term Support period for AngularJS. + +At this time we will focus exclusively on providing fixes to bugs that satisfy at least one of the following criteria: + +* A security flaw is detected in the 1.7.x branch of the framework +* One of the major browsers releases a version that will cause current production applications using AngularJS 1.7.x to stop working +* The jQuery library releases a version that will cause current production applications using AngularJS 1.7.x to stop working. + +### Blog Post + +You can read more about these plans in our [blog post announcement](https://blog.angular.io/stable-angularjs-and-long-term-support-7e077635ee9c). \ No newline at end of file From c0adcc3a4b561260d0f1ff6b834ca940bf1971a9 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Tue, 20 Feb 2018 18:23:07 +0200 Subject: [PATCH 123/469] refactor($compile): avoid catastrophic backtracking when parsing bindings This isn't expected to have any actual impact, since AngularJS is only intended to be used in the browser (not the server) and for this RegExp to be exploited by malicious user code the developer would have to have to give the user rights to execute arbitrary JavaScript code anyway. Fixing as a general good practice and to avoid encouraging use of a similar RegExp in other environments where it might actually matter. Closes #16464 --- src/ng/compile.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 6ae2722a6fde..603d94ed9522 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1014,11 +1014,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var bindingCache = createMap(); function parseIsolateBindings(scope, directiveName, isController) { - var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*([\w$]*)\s*$/; + var LOCAL_REGEXP = /^([@&<]|=(\*?))(\??)\s*([\w$]*)$/; var bindings = createMap(); forEach(scope, function(definition, scopeName) { + definition = definition.trim(); + if (definition in bindingCache) { bindings[scopeName] = bindingCache[definition]; return; From 719e66b38bd75a66b01df5b4cd90bbbac2bc069d Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Mon, 26 Feb 2018 15:00:42 +0100 Subject: [PATCH 124/469] docs(README): fix incorrect version-support-status link (#16473) Closes #16472 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2d1a9db2981..7ec7c6b466fd 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ piece of cake. Best of all? It makes development fun! -------------------- -##### AngularJS will be moving to Long Term Support (LTS) mode on July 1st 2018: [Find out more](misc/version-support-status) +##### AngularJS will be moving to Long Term Support (LTS) mode on July 1st 2018: [Find out more](https://docs.angularjs.org/misc/version-support-status) ##### Looking for the new Angular? Go here: https://github.com/angular/angular From 290a5f23395eeb669aff3f7957300833e46646d8 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 27 Feb 2018 17:33:42 +0100 Subject: [PATCH 125/469] docs(tutorial): fix headlines --- docs/content/tutorial/step_00.ngdoc | 5 ++--- docs/content/tutorial/step_01.ngdoc | 5 ++--- docs/content/tutorial/step_02.ngdoc | 11 +++++------ docs/content/tutorial/step_03.ngdoc | 9 ++++----- docs/content/tutorial/step_04.ngdoc | 4 ++-- docs/content/tutorial/step_05.ngdoc | 8 ++++---- docs/content/tutorial/step_06.ngdoc | 6 +++--- docs/content/tutorial/step_07.ngdoc | 6 +++--- docs/content/tutorial/step_08.ngdoc | 6 +++--- docs/content/tutorial/step_09.ngdoc | 6 +++--- docs/content/tutorial/step_10.ngdoc | 6 +++--- docs/content/tutorial/step_11.ngdoc | 6 +++--- docs/content/tutorial/step_12.ngdoc | 6 +++--- docs/content/tutorial/step_13.ngdoc | 2 +- docs/content/tutorial/step_14.ngdoc | 4 ++-- 15 files changed, 43 insertions(+), 47 deletions(-) diff --git a/docs/content/tutorial/step_00.ngdoc b/docs/content/tutorial/step_00.ngdoc index 36afe8862cf6..4aca8850cfe6 100644 --- a/docs/content/tutorial/step_00.ngdoc +++ b/docs/content/tutorial/step_00.ngdoc @@ -5,7 +5,6 @@
      - In this step of the tutorial, you will become familiar with the most important source code files of the AngularJS Phonecat App. You will also learn how to start the development servers bundled with [angular-seed][angular-seed], and run the application in the browser. @@ -167,7 +166,7 @@ For the purposes of this tutorial, we modified the angular-seed with the followi * Added a dependency on [Bootstrap](http://getbootstrap.com) in the `bower.json` file. -# Experiments +## Experiments
      @@ -178,7 +177,7 @@ For the purposes of this tutorial, we modified the angular-seed with the followi ``` -# Summary +## Summary Now let's go to {@link step_01 step 1} and add some content to the web app. diff --git a/docs/content/tutorial/step_01.ngdoc b/docs/content/tutorial/step_01.ngdoc index e5f104701fac..b134e69f455d 100644 --- a/docs/content/tutorial/step_01.ngdoc +++ b/docs/content/tutorial/step_01.ngdoc @@ -5,7 +5,6 @@
        - In order to illustrate how AngularJS enhances standard HTML, you will create a purely *static* HTML page and then examine how we can turn this HTML code into a template that AngularJS will use to dynamically display the same result with any set of data. @@ -37,7 +36,7 @@ In this step you will add some basic information about two cell phones to an HTM ``` -# Experiments +## Experiments
        @@ -48,7 +47,7 @@ In this step you will add some basic information about two cell phones to an HTM ``` -# Summary +## Summary This addition to your app uses static HTML to display the list. Now, let's go to {@link step_02 step 2} to learn how to use AngularJS to dynamically generate the same list. diff --git a/docs/content/tutorial/step_02.ngdoc b/docs/content/tutorial/step_02.ngdoc index ba2edc0b9085..6b4a54388b3c 100644 --- a/docs/content/tutorial/step_02.ngdoc +++ b/docs/content/tutorial/step_02.ngdoc @@ -5,7 +5,6 @@
          - Now, it's time to make the web page dynamic — with AngularJS. We will also add a test that verifies the code for the controller we are going to add. @@ -148,9 +147,9 @@ To learn more about AngularJS scopes, see the {@link ng.$rootScope.Scope Angular -# Testing +## Testing -## Testing Controllers +### Testing Controllers The "AngularJS way" of separating the controller from the view makes it easy to test code as it is being developed. In the section "Model and Controller" we have registered our controller via a constructor @@ -206,7 +205,7 @@ describe('PhoneListController', function() { -## Writing and Running Tests +### Writing and Running Tests Many AngularJS developers prefer the syntax of [Jasmine's Behavior-Driven Development (BDD) framework][jasmine-home], when writing tests. Although @@ -253,7 +252,7 @@ To run the tests, and then watch the files for changes execute: `npm test` -# Experiments +## Experiments
          @@ -308,7 +307,7 @@ To run the tests, and then watch the files for changes execute: `npm test` `toBe(4)`. -# Summary +## Summary We now have a dynamic application which separates models, views, and controllers, and we are testing as we go. Let's go to {@link step_03 step 3} to learn how to improve our application's architecture, diff --git a/docs/content/tutorial/step_03.ngdoc b/docs/content/tutorial/step_03.ngdoc index 92b8d990f587..e7a16de54bd0 100644 --- a/docs/content/tutorial/step_03.ngdoc +++ b/docs/content/tutorial/step_03.ngdoc @@ -5,7 +5,6 @@
            - In the previous step, we saw how a controller and a template worked together to convert a static HTML page into a dynamic view. This is a very common pattern in Single-Page Applications in general (and AngularJS applications in particular): @@ -197,7 +196,7 @@ Voilà! The resulting output should look the same, but let's see what we have ga -# Testing +## Testing Although we have combined our controller with a template into a component, we still can (and should) unit test the controller separately, since this is where our application logic and data reside. @@ -240,12 +239,12 @@ verifies that the phones array property on it contains three records. Note that the controller instance itself, not on a `scope`. -## Running Tests +### Running Tests Same as before, execute `npm test` to run the tests and then watch the files for changes. -# Experiments +## Experiments
            @@ -267,7 +266,7 @@ Same as before, execute `npm test` to run the tests and then watch the files for throughout the application, is a big win. -# Summary +## Summary You have learned how to organize your application and presentation logic into isolated, reusable components. Let's go to {@link step_04 step 4} to learn how to organize our code in directories and diff --git a/docs/content/tutorial/step_04.ngdoc b/docs/content/tutorial/step_04.ngdoc index 2865ad765866..e7546f721fac 100644 --- a/docs/content/tutorial/step_04.ngdoc +++ b/docs/content/tutorial/step_04.ngdoc @@ -265,7 +265,7 @@ After all the refactorings that took place, this is how our application looks fr ``` -# Testing +## Testing Since this was just a refactoring step (no actual code addition/deletions), we shouldn't need to change much (if anything) as far as our specs are concerned. @@ -301,7 +301,7 @@ pass. -# Summary +## Summary Even if we didn't add any new and exciting functionality to our application, we have made a great step towards a well-architected and maintainable application. Time to spice things up. Let's go to diff --git a/docs/content/tutorial/step_05.ngdoc b/docs/content/tutorial/step_05.ngdoc index adba95906fbb..e7a5ea519c7e 100644 --- a/docs/content/tutorial/step_05.ngdoc +++ b/docs/content/tutorial/step_05.ngdoc @@ -78,7 +78,7 @@ following: by the `filter` filter. The process is completely transparent to the developer. -# Testing +## Testing In previous steps, we learned how to write and run unit tests. Unit tests are perfect for testing controllers and other parts of our application written in JavaScript, but they can't easily @@ -124,7 +124,7 @@ easy it is to write E2E tests in AngularJS. Although this example is for a simpl that easy to set up any functional, readable, E2E test. -## Running E2E Tests with Protractor +### Running E2E Tests with Protractor Even though the syntax of this test looks very much like our controller unit test written with Jasmine, the E2E test uses APIs of [Protractor][protractor]. Read about the Protractor APIs in the @@ -142,7 +142,7 @@ To rerun the test suite, execute `npm run protractor` again. -# Experiments +## Experiments
            @@ -155,7 +155,7 @@ To rerun the test suite, execute `npm run protractor` again. Component isolation at work! -# Summary +## Summary We have now added full-text search and included a test to verify that it works! Now let's go on to {@link step_06 step 6} to learn how to add sorting capabilities to the PhoneCat application. diff --git a/docs/content/tutorial/step_06.ngdoc b/docs/content/tutorial/step_06.ngdoc index 7a19d2600444..adc4b610fbb4 100644 --- a/docs/content/tutorial/step_06.ngdoc +++ b/docs/content/tutorial/step_06.ngdoc @@ -124,7 +124,7 @@ will be reordered. That is the data-binding doing its job in the opposite direct the model. -# Testing +## Testing The changes we made should be verified with both a unit test and an E2E test. Let's look at the unit test first. @@ -217,7 +217,7 @@ The E2E test verifies that the ordering mechanism of the select box is working c You can now rerun `npm run protractor` to see the tests run. -# Experiments +## Experiments
            @@ -232,7 +232,7 @@ You can now rerun `npm run protractor` to see the tests run. `` -# Summary +## Summary Now that you have added list sorting and tested the application, go to {@link step_07 step 7} to learn about AngularJS services and how AngularJS uses dependency injection. diff --git a/docs/content/tutorial/step_07.ngdoc b/docs/content/tutorial/step_07.ngdoc index 7e801ffa822d..4b0d0e64aaaa 100644 --- a/docs/content/tutorial/step_07.ngdoc +++ b/docs/content/tutorial/step_07.ngdoc @@ -180,7 +180,7 @@ let's add the annotations to our `PhoneListController`: ``` -# Testing +## Testing Because we started using dependency injection and our controller has dependencies, constructing the controller in our tests is a bit more complicated. We could use the `new` operator and provide the @@ -283,7 +283,7 @@ Chrome 49.0: Executed 2 of 2 SUCCESS (0.133 secs / 0.097 secs) ``` -# Experiments +## Experiments
            @@ -299,7 +299,7 @@ Chrome 49.0: Executed 2 of 2 SUCCESS (0.133 secs / 0.097 secs) ``` -# Summary +## Summary Now that you have learned how easy it is to use AngularJS services (thanks to AngularJS's dependency injection), go to {@link step_08 step 8}, where you will add some thumbnail images of phones and diff --git a/docs/content/tutorial/step_08.ngdoc b/docs/content/tutorial/step_08.ngdoc index a55461d0cc21..5707ea4d9902 100644 --- a/docs/content/tutorial/step_08.ngdoc +++ b/docs/content/tutorial/step_08.ngdoc @@ -70,7 +70,7 @@ which it would have done if we had only specified an attribute binding in a regu HTTP request to an invalid location. -# Testing +## Testing
            **`e2e-tests/scenarios.js`**: @@ -95,7 +95,7 @@ views, that we will implement in the upcoming steps. You can now rerun `npm run protractor` to see the tests run. -# Experiments +## Experiments
            @@ -108,7 +108,7 @@ You can now rerun `npm run protractor` to see the tests run. inject the valid address. -# Summary +## Summary Now that you have added phone images and links, go to {@link step_09 step 9} to learn about AngularJS layout templates and how AngularJS makes it easy to create applications that have multiple views. diff --git a/docs/content/tutorial/step_09.ngdoc b/docs/content/tutorial/step_09.ngdoc index 99683ccfeb44..da1e92a7dfc6 100644 --- a/docs/content/tutorial/step_09.ngdoc +++ b/docs/content/tutorial/step_09.ngdoc @@ -334,7 +334,7 @@ The takeaway here is: -# Testing +## Testing Since some of our modules depend on {@link ngRoute ngRoute} now, it is necessary to update the Karma configuration file with angular-route. Other than that, the unit tests should (still) pass without @@ -398,7 +398,7 @@ various URLs and verifying that the correct view was rendered. You can now rerun `npm run protractor` to see the tests run (and hopefully pass). -# Experiments +## Experiments
            @@ -415,7 +415,7 @@ You can now rerun `npm run protractor` to see the tests run (and hopefully pass) component isolation at work! -# Summary +## Summary With the routing set up and the phone list view implemented, we are ready to go to {@link step_10 step 10} and implement a proper phone details view. diff --git a/docs/content/tutorial/step_10.ngdoc b/docs/content/tutorial/step_10.ngdoc index 537f3ded49bf..65069b74ff58 100644 --- a/docs/content/tutorial/step_10.ngdoc +++ b/docs/content/tutorial/step_10.ngdoc @@ -122,7 +122,7 @@ including lists and bindings that comprise the phone details. Note how we use th -# Testing +## Testing We wrote a new unit test that is similar to the one we wrote for the `phoneList` component's controller in {@link step_07#testing step 7}. @@ -194,7 +194,7 @@ heading on the page is "Nexus S". You can run the tests with `npm run protractor`. -# Experiments +## Experiments
            @@ -202,7 +202,7 @@ You can run the tests with `npm run protractor`. images on the 'Nexus S' details page. -# Summary +## Summary Now that the phone details view is in place, proceed to {@link step_11 step 11} to learn how to write your own custom display filter. diff --git a/docs/content/tutorial/step_11.ngdoc b/docs/content/tutorial/step_11.ngdoc index d9ab2565d23e..6bc360cbb647 100644 --- a/docs/content/tutorial/step_11.ngdoc +++ b/docs/content/tutorial/step_11.ngdoc @@ -104,7 +104,7 @@ Let's employ the filter in the phone details template: ``` -# Testing +## Testing Filters, like any other code, should be tested. Luckily, these tests are very easy to write. @@ -146,7 +146,7 @@ Chrome 49.0: Executed 4 of 4 SUCCESS (0.091 secs / 0.075 secs) ``` -# Experiments +## Experiments
            @@ -167,7 +167,7 @@ Chrome 49.0: Executed 4 of 4 SUCCESS (0.091 secs / 0.075 secs) ``` -# Summary +## Summary Now that we have learned how to write and test a custom filter, let's go to {@link step_12 step 12} to learn how we can use AngularJS to enhance the phone details page further. diff --git a/docs/content/tutorial/step_12.ngdoc b/docs/content/tutorial/step_12.ngdoc index e16bdaa5bb2d..b9d4e0850c57 100644 --- a/docs/content/tutorial/step_12.ngdoc +++ b/docs/content/tutorial/step_12.ngdoc @@ -73,7 +73,7 @@ thumbnail image. -# Testing +## Testing To verify this new feature, we added two E2E tests. One verifies that `mainImageUrl` is set to the first phone image URL by default. The second test clicks on several thumbnail images and verifies @@ -151,7 +151,7 @@ property to the controller. As previously, we will use a mocked response. Our unit tests should now be passing again. -# Experiments +## Experiments
            @@ -176,7 +176,7 @@ Our unit tests should now be passing again. Now, whenever you double-click on a thumbnail, an alert pops-up. Pretty annoying! -# Summary +## Summary With the phone image swapper in place, we are ready for {@link step_13 step 13} to learn an even better way to fetch data. diff --git a/docs/content/tutorial/step_13.ngdoc b/docs/content/tutorial/step_13.ngdoc index f96ccd5c1913..2ad46e2c9459 100644 --- a/docs/content/tutorial/step_13.ngdoc +++ b/docs/content/tutorial/step_13.ngdoc @@ -310,7 +310,7 @@ Chrome 49.0: Executed 5 of 5 SUCCESS (0.123 secs / 0.104 secs) ``` -# Summary +## Summary Now that we have seen how to build a custom service as a RESTful client, we are ready for {@link step_14 step 14} to learn how to enhance the user experience with animations. diff --git a/docs/content/tutorial/step_14.ngdoc b/docs/content/tutorial/step_14.ngdoc index b1b5ff043f58..40667717ebee 100644 --- a/docs/content/tutorial/step_14.ngdoc +++ b/docs/content/tutorial/step_14.ngdoc @@ -503,7 +503,7 @@ element). A boolean parameter (`wasCanceled`) is passed to the function, letting if the animation was canceled or not. Use this function to do any necessary clean-up. -# Experiments +## Experiments
            @@ -544,7 +544,7 @@ if the animation was canceled or not. Use this function to do any necessary clea * Go crazy and come up with your own funky animations! -# Summary +## Summary Our application is now much more pleasant to use, thanks to the smooth transitions between pages and UI states. From 77917e34c93668e9936768bed681696d78028748 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Sun, 18 Feb 2018 20:32:04 +0100 Subject: [PATCH 126/469] chore(docs.angularjs.org): allow crawling of examples, don't deploy e2e test files --- Gruntfile.js | 2 +- docs/app/assets/robots.txt | 13 ++----------- scripts/docs.angularjs.org-firebase/firebase.json | 9 ++++++++- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index ba5efa0d8f09..70fd9d8a8236 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -341,7 +341,7 @@ module.exports = function(grunt) { }, { cwd: 'build/docs', - src: '**', + src: ['**', '!ptore2e/**'], dest: 'deploy/docs/', expand: true }, diff --git a/docs/app/assets/robots.txt b/docs/app/assets/robots.txt index 36b7daeca859..898272b08202 100644 --- a/docs/app/assets/robots.txt +++ b/docs/app/assets/robots.txt @@ -1,13 +1,4 @@ User-agent: * -Disallow: /examples/ -Disallow: /ptore2e/ -Disallow: /Error404.html - -# The js / map files in the root are used by the embedded examples, not by the app itself -Disallow: /*.js$ -Disallow: /*.map$ - -# (Javascript) crawlers need to access JS files -Allow: /components/*.js -Allow: /js/*.js +# The map files are not required by the app +Disallow: /*.map$ \ No newline at end of file diff --git a/scripts/docs.angularjs.org-firebase/firebase.json b/scripts/docs.angularjs.org-firebase/firebase.json index 9e112f2ff9ee..a83409ed7155 100644 --- a/scripts/docs.angularjs.org-firebase/firebase.json +++ b/scripts/docs.angularjs.org-firebase/firebase.json @@ -29,7 +29,14 @@ ], "headers": [ { - "source": "/partials/**", + "source": "/Error404.html", + "headers" : [{ + "key" : "X-Robots-Tag", + "value" : "noindex" + }] + }, + { + "source": "/@(partials|examples)/**", "headers" : [{ "key" : "X-Robots-Tag", "value" : "noindex" From 02fb980de67fc9158920ec4ab0969d544ac96c1a Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Sun, 18 Feb 2018 20:35:55 +0100 Subject: [PATCH 127/469] chore(docs.angularjs.org): only deploy production index.html as entry file Previously, we rewrote index.html to index-production.html, but Firebase ignored this, probably because an exact file match always takes priority. This lead to the problem thatthe root - angularjs.org - didn't include the angular.js source files from the CDN --- Gruntfile.js | 12 +++++++----- scripts/docs.angularjs.org-firebase/firebase.json | 13 ------------- .../docs.angularjs.org-firebase/functions/index.js | 2 +- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 70fd9d8a8236..bb3eb9508ee4 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -341,15 +341,17 @@ module.exports = function(grunt) { }, { cwd: 'build/docs', - src: ['**', '!ptore2e/**'], + src: ['**', '!ptore2e/**', '!index*.html'], dest: 'deploy/docs/', expand: true }, { - src: ['build/docs/index-production.html'], - dest: docsScriptFolder + '/functions/content', - expand: true, - flatten: true + src: 'build/docs/index-production.html', + dest: 'deploy/docs/index.html' + }, + { + src: 'build/docs/index-production.html', + dest: docsScriptFolder + '/functions/content/index.html' }, { cwd: 'build/docs', diff --git a/scripts/docs.angularjs.org-firebase/firebase.json b/scripts/docs.angularjs.org-firebase/firebase.json index a83409ed7155..0e018010b061 100644 --- a/scripts/docs.angularjs.org-firebase/firebase.json +++ b/scripts/docs.angularjs.org-firebase/firebase.json @@ -1,11 +1,6 @@ { "hosting": { "public": "../../deploy/docs", - "ignore": [ - "/index.html", - "/index-debug.html", - "/index-jquery.html" - ], "redirects": [ { "source": "/error/:namespace\\::error*", @@ -14,14 +9,6 @@ } ], "rewrites": [ - { - "source": "/", - "destination": "/index-production.html" - }, - { - "source": "/index.html", - "destination": "/index-production.html" - }, { "source": "**/*!(.@(jpg|jpeg|gif|png|html|js|map|json|css|svg|ttf|txt|woff|woff2|eot|xml))", "function": "sendFile" diff --git a/scripts/docs.angularjs.org-firebase/functions/index.js b/scripts/docs.angularjs.org-firebase/functions/index.js index b86eb32f642a..eace519a45ad 100644 --- a/scripts/docs.angularjs.org-firebase/functions/index.js +++ b/scripts/docs.angularjs.org-firebase/functions/index.js @@ -25,7 +25,7 @@ const buildSnapshot = data => ` function sendFile(request, response) { const snapshotRequested = typeof request.query._escaped_fragment_ !== 'undefined'; - const filePath = `content/${snapshotRequested ? `partials${request.path}` : 'index-production'}.html`; + const filePath = `content/${snapshotRequested ? `partials${request.path}` : 'index'}.html`; if (snapshotRequested) { fs.readFile(filePath, {encoding: 'utf8'}, (error, data) => { From a37f89f8646df3ea64aee32e63055b9722236753 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 20 Feb 2018 17:59:45 +0100 Subject: [PATCH 128/469] chore(docs-app): add dynamic 404 behavior Adapted from https://github.com/angular/angular/commit/88045a50506adfe32c2f7a213c8e95f46d1e40e1, https://github.com/angular/angular/commit/c3fb820473d64036ef0dd3d4c004cc7fbc67be75, and https://github.com/angular/angular/commit/5a624fa1be530a1b3479a4cc7f96e5a20a3d64fb. --- docs/app/e2e/.eslintrc.json | 1 + docs/app/e2e/app.scenario.js | 63 +++++++++++++++++++ docs/app/src/docs.js | 16 +++-- docs/config/index.js | 5 +- docs/config/services/deployments/test.js | 40 ++++++++++++ .../templates/app/indexPage.template.html | 21 +++++-- 6 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 docs/config/services/deployments/test.js diff --git a/docs/app/e2e/.eslintrc.json b/docs/app/e2e/.eslintrc.json index 6a949b92ffc9..60c814cc9339 100644 --- a/docs/app/e2e/.eslintrc.json +++ b/docs/app/e2e/.eslintrc.json @@ -9,6 +9,7 @@ }, "globals": { + "angular": false, /* testabilityPatch / matchers */ "inject": false, "module": false, diff --git a/docs/app/e2e/app.scenario.js b/docs/app/e2e/app.scenario.js index 1333870eff4d..a78667962ab9 100644 --- a/docs/app/e2e/app.scenario.js +++ b/docs/app/e2e/app.scenario.js @@ -21,6 +21,9 @@ describe('docs.angularjs.org', function() { console.log('browser console errors: ' + require('util').inspect(filteredLog)); } }); + + browser.ignoreSynchronization = false; + browser.clearMockModules(); }); @@ -102,6 +105,66 @@ describe('docs.angularjs.org', function() { expect(mainHeader.getText()).toEqual('Oops!'); }); + it('should set "noindex" if the page does not exist', function() { + browser.get('build/docs/index-production.html#!/api/does/not/exist'); + var robots = element(by.css('meta[name="robots"][content="noindex"]')); + var googleBot = element(by.css('meta[name="googlebot"][content="noindex"]')); + expect(robots.isPresent()).toBe(true); + expect(googleBot.isPresent()).toBe(true); + }); + + it('should remove "noindex" if the page exists', function() { + browser.get('build/docs/index-production.html#!/api'); + var robots = element(by.css('meta[name="robots"][content="noindex"]')); + var googleBot = element(by.css('meta[name="googlebot"][content="noindex"]')); + expect(robots.isPresent()).toBe(false); + expect(googleBot.isPresent()).toBe(false); + }); + + describe('template request error', function() { + beforeEach(function() { + browser.addMockModule('httpMocker', function() { + angular.module('httpMocker', ['ngMock']) + .run(['$httpBackend', function($httpBackend) { + $httpBackend.whenGET('localhost:8000/build/docs/partials/api.html').respond(500, ''); + }]); + }); + }); + + it('should set "noindex" for robots if the request fails', function() { + // index-test includes ngMock + browser.get('build/docs/index-test.html#!/api'); + var robots = element(by.css('meta[name="robots"][content="noindex"]')); + var googleBot = element(by.css('meta[name="googlebot"][content="noindex"]')); + expect(robots.isPresent()).toBe(true); + expect(googleBot.isPresent()).toBe(true); + }); + }); + + + describe('page bootstrap error', function() { + beforeEach(function() { + browser.addMockModule('httpMocker', function() { + // Require a module that does not exist to break the bootstrapping + angular.module('httpMocker', ['doesNotExist']); + }); + }); + + it('should have "noindex" for robots if bootstrapping fails', function() { + browser.get('build/docs/index.html#!/api').catch(function() { + // get() will fail on AngularJS bootstrap, but if we continue here, protractor + // will assume the app is ready + browser.ignoreSynchronization = true; + var robots = element(by.css('meta[name="robots"][content="noindex"]')); + var googleBot = element(by.css('meta[name="googlebot"][content="noindex"]')); + expect(robots.isPresent()).toBe(true); + expect(googleBot.isPresent()).toBe(true); + }); + }); + + + }); + }); }); diff --git a/docs/app/src/docs.js b/docs/app/src/docs.js index 33b1be384beb..b6e6e49a2aa8 100644 --- a/docs/app/src/docs.js +++ b/docs/app/src/docs.js @@ -8,6 +8,8 @@ angular.module('DocsController', ['currentVersionData']) function($scope, $rootScope, $location, $window, $cookies, NG_PAGES, NG_NAVIGATION, CURRENT_NG_VERSION) { + var errorPartialPath = 'Error404.html'; + $scope.navClass = function(navItem) { return { active: navItem.href && this.currentPage && this.currentPage.path, @@ -16,8 +18,6 @@ angular.module('DocsController', ['currentVersionData']) }; }; - - $scope.$on('$includeContentLoaded', function() { var pagePath = $scope.currentPage ? $scope.currentPage.path : $location.path(); $window._gaq.push(['_trackPageview', pagePath]); @@ -26,6 +26,7 @@ angular.module('DocsController', ['currentVersionData']) $scope.$on('$includeContentError', function() { $scope.loading = false; + $scope.loadingError = true; }); $scope.$watch(function docsPathWatch() {return $location.path(); }, function docsPathWatchAction(path) { @@ -35,6 +36,7 @@ angular.module('DocsController', ['currentVersionData']) var currentPage = $scope.currentPage = NG_PAGES[path]; $scope.loading = true; + $scope.loadingError = false; if (currentPage) { $scope.partialPath = 'partials/' + path + '.html'; @@ -50,18 +52,22 @@ angular.module('DocsController', ['currentVersionData']) } else { $scope.currentArea = NG_NAVIGATION['api']; $scope.breadcrumb = []; - $scope.partialPath = 'Error404.html'; + $scope.partialPath = errorPartialPath; } }); + $scope.hasError = function() { + return $scope.partialPath === errorPartialPath || $scope.loadingError; + }; + /********************************** Initialize ***********************************/ $scope.versionNumber = CURRENT_NG_VERSION.full; $scope.version = CURRENT_NG_VERSION.full + ' ' + CURRENT_NG_VERSION.codeName; - $scope.loading = 0; - + $scope.loading = false; + $scope.loadingError = false; var INDEX_PATH = /^(\/|\/index[^.]*.html)$/; if (!$location.path() || INDEX_PATH.test($location.path())) { diff --git a/docs/config/index.js b/docs/config/index.js index 4ddf7922c7bd..12777f6a8f5e 100644 --- a/docs/config/index.js +++ b/docs/config/index.js @@ -22,6 +22,7 @@ module.exports = new Package('angularjs', [ .factory(require('./services/deployments/debug')) .factory(require('./services/deployments/default')) .factory(require('./services/deployments/jquery')) +.factory(require('./services/deployments/test')) .factory(require('./services/deployments/production')) .factory(require('./inline-tag-defs/type')) @@ -157,12 +158,14 @@ module.exports = new Package('angularjs', [ generateProtractorTestsProcessor, generateExamplesProcessor, debugDeployment, defaultDeployment, - jqueryDeployment, productionDeployment) { + jqueryDeployment, testDeployment, + productionDeployment) { generateIndexPagesProcessor.deployments = [ debugDeployment, defaultDeployment, jqueryDeployment, + testDeployment, productionDeployment ]; diff --git a/docs/config/services/deployments/test.js b/docs/config/services/deployments/test.js new file mode 100644 index 000000000000..ba0805b5079a --- /dev/null +++ b/docs/config/services/deployments/test.js @@ -0,0 +1,40 @@ +'use strict'; + +module.exports = function testDeployment(getVersion) { + return { + name: 'test', + examples: { + commonFiles: { + scripts: ['../../../angular.js'] + }, + dependencyPath: '../../../' + }, + scripts: [ + '../angular.js', + '../angular-resource.js', + '../angular-route.js', + '../angular-cookies.js', + '../angular-mocks.js', + '../angular-sanitize.js', + '../angular-touch.js', + '../angular-animate.js', + 'components/marked-' + getVersion('marked') + '/lib/marked.js', + 'js/angular-bootstrap/dropdown-toggle.js', + 'components/lunr-' + getVersion('lunr') + '/lunr.js', + 'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js', + 'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/lang-css.js', + 'js/current-version-data.js', + 'js/all-versions-data.js', + 'js/pages-data.js', + 'js/nav-data.js', + 'js/docs.js' + ], + stylesheets: [ + 'components/bootstrap-' + getVersion('bootstrap') + '/css/bootstrap.css', + 'css/prettify-theme.css', + 'css/angular-topnav.css', + 'css/docs.css', + 'css/animations.css' + ] + }; +}; diff --git a/docs/config/templates/app/indexPage.template.html b/docs/config/templates/app/indexPage.template.html index 44ef6ebd7b1a..eb5c6614a2bc 100644 --- a/docs/config/templates/app/indexPage.template.html +++ b/docs/config/templates/app/indexPage.template.html @@ -11,6 +11,18 @@ AngularJS + - * - *
            - *
            - *
            - * Current time is: - *
            - * Blood 1 : {{blood_1}} - * Blood 2 : {{blood_2}} - * - * - * - *
            - *
            - * - * - * - */ + /** + * @ngdoc service + * @name $interval + * + * @description + * AngularJS's wrapper for `window.setInterval`. The `fn` function is executed every `delay` + * milliseconds. + * + * The return value of registering an interval function is a promise. This promise will be + * notified upon each tick of the interval, and will be resolved after `count` iterations, or + * run indefinitely if `count` is not defined. The value of the notification will be the + * number of iterations that have run. + * To cancel an interval, call `$interval.cancel(promise)`. + * + * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to + * move forward by `millis` milliseconds and trigger any functions scheduled to run in that + * time. + * + *
            + * **Note**: Intervals created by this service must be explicitly destroyed when you are finished + * with them. In particular they are not automatically destroyed when a controller's scope or a + * directive's element are destroyed. + * You should take this into consideration and make sure to always cancel the interval at the + * appropriate moment. See the example below for more details on how and when to do this. + *
            + * + * @param {function()} fn A function that should be called repeatedly. If no additional arguments + * are passed (see below), the function is called with the current iteration count. + * @param {number} delay Number of milliseconds between each function call. + * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat + * indefinitely. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {promise} A promise which will be notified on each iteration. It will resolve once all iterations of the interval complete. + * + * @example + * + * + * + * + *
            + *
            + *
            + * Current time is: + *
            + * Blood 1 : {{blood_1}} + * Blood 2 : {{blood_2}} + * + * + * + *
            + *
            + * + *
            + *
            + */ function interval(fn, delay, count, invokeApply) { var hasParams = arguments.length > 4, args = hasParams ? sliceArgs(arguments, 4) : [], @@ -177,16 +177,16 @@ function $IntervalProvider() { } - /** - * @ngdoc method - * @name $interval#cancel - * - * @description - * Cancels a task associated with the `promise`. - * - * @param {Promise=} promise returned by the `$interval` function. - * @returns {boolean} Returns `true` if the task was successfully canceled. - */ + /** + * @ngdoc method + * @name $interval#cancel + * + * @description + * Cancels a task associated with the `promise`. + * + * @param {Promise=} promise returned by the `$interval` function. + * @returns {boolean} Returns `true` if the task was successfully canceled. + */ interval.cancel = function(promise) { if (promise && promise.$$intervalId in intervals) { // Interval cancels should not report as unhandled promise. From 3365256502344970f86355d3ace1cb4251ae9828 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Tue, 13 Mar 2018 19:52:58 +0200 Subject: [PATCH 136/469] fix($timeout): throw when trying to cancel non-$timeout promise Previously, calling `$timeout.cancel()` with a promise that was not generated by a call to `$timeout()` would do nothing. This could, for example, happen when calling `.then()`/`.catch()` on the returned promise, which creates a new promise, and passing that to `$timeout.cancel()`. With this commit, `$timeout.cancel()` will throw an error if called with a non-$timeout promise, thus surfacing errors that would otherwise go unnoticed. Fixes #16424 BREAKING CHNAGE: `$timeout.cancel()` will throw an error if called with a promise that was not generated by `$timeout()`. Previously, it would silently do nothing. Before: ```js var promise = $timeout(doSomething, 1000).then(doSomethingElse); $timeout.cancel(promise); // No error; timeout NOT canceled. ``` After: ```js var promise = $timeout(doSomething, 1000).then(doSomethingElse); $timeout.cancel(promise); // Throws error. ``` Correct usage: ```js var promise = $timeout(doSomething, 1000); var newPromise = promise.then(doSomethingElse); $timeout.cancel(promise); // Timeout canceled. ``` --- docs/content/error/$timeout/badprom.ngdoc | 25 ++++++++++++++++++++++ src/ng/timeout.js | 26 +++++++++++++++++------ test/ng/timeoutSpec.js | 8 ++++++- 3 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 docs/content/error/$timeout/badprom.ngdoc diff --git a/docs/content/error/$timeout/badprom.ngdoc b/docs/content/error/$timeout/badprom.ngdoc new file mode 100644 index 000000000000..c1b0d025ad8f --- /dev/null +++ b/docs/content/error/$timeout/badprom.ngdoc @@ -0,0 +1,25 @@ +@ngdoc error +@name $timeout:badprom +@fullName Non-$timeout promise +@description + +This error occurs when calling {@link ng.$timeout#cancel $timeout.cancel()} with a promise that +was not generated by the {@link ng.$timeout $timeout} service. This can, for example, happen when +calling {@link ng.$q#the-promise-api then()/catch()} on the returned promise, which creates a new +promise, and pass that new promise to {@link ng.$timeout#cancel $timeout.cancel()}. + +Example of incorrect usage that leads to this error: + +```js +var promise = $timeout(doSomething, 1000).then(doSomethingElse); +$timeout.cancel(promise); +``` + +To fix the example above, keep a reference to the promise returned by +{@link ng.$timeout $timeout()} and pass that to {@link ng.$timeout#cancel $timeout.cancel()}: + +```js +var promise = $timeout(doSomething, 1000); +var newPromise = promise.then(doSomethingElse); +$timeout.cancel(promise); +``` diff --git a/src/ng/timeout.js b/src/ng/timeout.js index 71c06ce39ab0..1e4eaad3349f 100644 --- a/src/ng/timeout.js +++ b/src/ng/timeout.js @@ -1,5 +1,7 @@ 'use strict'; +var $timeoutMinErr = minErr('$timeout'); + /** @this */ function $TimeoutProvider() { this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', @@ -83,14 +85,24 @@ function $TimeoutProvider() { * canceled. */ timeout.cancel = function(promise) { - if (promise && promise.$$timeoutId in deferreds) { - // Timeout cancels should not report an unhandled promise. - markQExceptionHandled(deferreds[promise.$$timeoutId].promise); - deferreds[promise.$$timeoutId].reject('canceled'); - delete deferreds[promise.$$timeoutId]; - return $browser.defer.cancel(promise.$$timeoutId); + if (!promise) return false; + + if (!promise.hasOwnProperty('$$timeoutId')) { + throw $timeoutMinErr('badprom', + '`$timeout.cancel()` called with a promise that was not generated by `$timeout()`.'); } - return false; + + if (!deferreds.hasOwnProperty(promise.$$timeoutId)) return false; + + var id = promise.$$timeoutId; + var deferred = deferreds[id]; + + // Timeout cancels should not report an unhandled promise. + markQExceptionHandled(deferred.promise); + deferred.reject('canceled'); + delete deferreds[id]; + + return $browser.defer.cancel(id); }; return timeout; diff --git a/test/ng/timeoutSpec.js b/test/ng/timeoutSpec.js index 648c39663c0d..bfd5d53285e7 100644 --- a/test/ng/timeoutSpec.js +++ b/test/ng/timeoutSpec.js @@ -280,11 +280,17 @@ describe('$timeout', function() { })); - it('should not throw a runtime exception when given an undefined promise', inject(function($timeout) { + it('should not throw an error when given an undefined promise', inject(function($timeout) { expect($timeout.cancel()).toBe(false); })); + it('should throw an error when given a non-$timeout promise', inject(function($timeout) { + var promise = $timeout(noop).then(noop); + expect(function() { $timeout.cancel(promise); }).toThrowMinErr('$timeout', 'badprom'); + })); + + it('should forget references to relevant deferred', inject(function($timeout, $browser) { // $browser.defer.cancel is only called on cancel if the deferred object is still referenced var cancelSpy = spyOn($browser.defer, 'cancel').and.callThrough(); From a8bef95127775d83d80daa4617c33227c4b443d4 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Tue, 13 Mar 2018 19:53:56 +0200 Subject: [PATCH 137/469] fix($interval): throw when trying to cancel non-$interval promise Previously, calling `$interval.cancel()` with a promise that was not generated by a call to `$interval()` would do nothing. This could, for example, happen when calling `.then()`/`.catch()` on the returned promise, which creates a new promise, and passing that to `$interval.cancel()`. With this commit, `$interval.cancel()` will throw an error if called with a non-$interval promise, thus surfacing errors that would otherwise go unnoticed. Related to #16424. BREAKING CHNAGE: `$interval.cancel()` will throw an error if called with a promise that was not generated by `$interval()`. Previously, it would silently do nothing. Before: ```js var promise = $interval(doSomething, 1000, 5).then(doSomethingElse); $interval.cancel(promise); // No error; interval NOT canceled. ``` After: ```js var promise = $interval(doSomething, 1000, 5).then(doSomethingElse); $interval.cancel(promise); // Throws error. ``` Correct usage: ```js var promise = $interval(doSomething, 1000, 5); var newPromise = promise.then(doSomethingElse); $interval.cancel(promise); // Interval canceled. ``` Closes #16476 --- docs/content/error/$interval/badprom.ngdoc | 25 +++++++++++++++++++ src/ng/interval.js | 28 +++++++++++++++------- test/ng/intervalSpec.js | 9 +++++-- 3 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 docs/content/error/$interval/badprom.ngdoc diff --git a/docs/content/error/$interval/badprom.ngdoc b/docs/content/error/$interval/badprom.ngdoc new file mode 100644 index 000000000000..2c9f8c5371a9 --- /dev/null +++ b/docs/content/error/$interval/badprom.ngdoc @@ -0,0 +1,25 @@ +@ngdoc error +@name $interval:badprom +@fullName Non-$interval promise +@description + +This error occurs when calling {@link ng.$interval#cancel $interval.cancel()} with a promise that +was not generated by the {@link ng.$interval $interval} service. This can, for example, happen when +calling {@link ng.$q#the-promise-api then()/catch()} on the returned promise, which creates a new +promise, and pass that new promise to {@link ng.$interval#cancel $interval.cancel()}. + +Example of incorrect usage that leads to this error: + +```js +var promise = $interval(doSomething, 1000, 5).then(doSomethingElse); +$interval.cancel(promise); +``` + +To fix the example above, keep a reference to the promise returned by +{@link ng.$interval $interval()} and pass that to {@link ng.$interval#cancel $interval.cancel()}: + +```js +var promise = $interval(doSomething, 1000, 5); +var newPromise = promise.then(doSomethingElse); +$interval.cancel(promise); +``` diff --git a/src/ng/interval.js b/src/ng/interval.js index a34682ed007d..750a6ba3df1c 100644 --- a/src/ng/interval.js +++ b/src/ng/interval.js @@ -1,5 +1,7 @@ 'use strict'; +var $intervalMinErr = minErr('$interval'); + /** @this */ function $IntervalProvider() { this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser', @@ -188,15 +190,25 @@ function $IntervalProvider() { * @returns {boolean} Returns `true` if the task was successfully canceled. */ interval.cancel = function(promise) { - if (promise && promise.$$intervalId in intervals) { - // Interval cancels should not report as unhandled promise. - markQExceptionHandled(intervals[promise.$$intervalId].promise); - intervals[promise.$$intervalId].reject('canceled'); - $window.clearInterval(promise.$$intervalId); - delete intervals[promise.$$intervalId]; - return true; + if (!promise) return false; + + if (!promise.hasOwnProperty('$$intervalId')) { + throw $intervalMinErr('badprom', + '`$interval.cancel()` called with a promise that was not generated by `$interval()`.'); } - return false; + + if (!intervals.hasOwnProperty(promise.$$intervalId)) return false; + + var id = promise.$$intervalId; + var deferred = intervals[id]; + + // Interval cancels should not report an unhandled promise. + markQExceptionHandled(deferred.promise); + deferred.reject('canceled'); + $window.clearInterval(id); + delete intervals[id]; + + return true; }; return interval; diff --git a/test/ng/intervalSpec.js b/test/ng/intervalSpec.js index 47281429e0b2..3b23250d1f98 100644 --- a/test/ng/intervalSpec.js +++ b/test/ng/intervalSpec.js @@ -335,12 +335,17 @@ describe('$interval', function() { })); - it('should not throw a runtime exception when given an undefined promise', - inject(function($interval) { + it('should not throw an error when given an undefined promise', inject(function($interval) { expect($interval.cancel()).toBe(false); })); + it('should throw an error when given a non-$interval promise', inject(function($interval) { + var promise = $interval(noop).then(noop); + expect(function() { $interval.cancel(promise); }).toThrowMinErr('$interval', 'badprom'); + })); + + it('should not trigger digest when cancelled', inject(function($interval, $rootScope, $browser) { var watchSpy = jasmine.createSpy('watchSpy'); $rootScope.$watch(watchSpy); From 98e0e047b0f705005b256c70feb4e6368ff3a591 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 14 Mar 2018 22:35:34 +0100 Subject: [PATCH 138/469] docs(ngShow/ngHide): add note about flicker when toggling elements Related to https://github.com/angular/angular.js/issues/14015 Closes #16489 --- src/ng/directive/ngShowHide.js | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/ng/directive/ngShowHide.js b/src/ng/directive/ngShowHide.js index 74f02f923989..8e24b5ba081c 100644 --- a/src/ng/directive/ngShowHide.js +++ b/src/ng/directive/ngShowHide.js @@ -182,6 +182,25 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; }); + * + * @knownIssue + * + * ### Flickering when using ngShow to toggle between elements + * + * When using {@link ngShow} and / or {@link ngHide} to toggle between elements, it can + * happen that both the element to show and the element to hide are visible for a very short time. + * + * This usually happens when the {@link ngAnimate ngAnimate module} is included, but no actual animations + * are defined for {@link ngShow} / {@link ngHide}. Internet Explorer is affected more often than + * other browsers. + * + * There are several way to mitigate this problem: + * + * - {@link guide/animations#how-to-selectively-enable-disable-and-skip-animations Disable animations on the affected elements}. + * - Use {@link ngIf} or {@link ngSwitch} instead of {@link ngShow} / {@link ngHide}. + * - Use the special CSS selector `ng-hide.ng-hide-animate` to set `{display: none}` or similar on the affected elements. + * - Use `ng-class="{'ng-hide': expression}` instead of instead of {@link ngShow} / {@link ngHide}. + * - Define an animation on the affected elements. */ var ngShowDirective = ['$animate', function($animate) { return { @@ -382,6 +401,25 @@ var ngShowDirective = ['$animate', function($animate) { }); + * + * @knownIssue + * + * ### Flickering when using ngHide to toggle between elements + * + * When using {@link ngShow} and / or {@link ngHide} to toggle between elements, it can + * happen that both the element to show and the element to hide are visible for a very short time. + * + * This usually happens when the {@link ngAnimate ngAnimate module} is included, but no actual animations + * are defined for {@link ngShow} / {@link ngHide}. Internet Explorer is affected more often than + * other browsers. + * + * There are several way to mitigate this problem: + * + * - {@link guide/animations#how-to-selectively-enable-disable-and-skip-animations Disable animations on the affected elements}. + * - Use {@link ngIf} or {@link ngSwitch} instead of {@link ngShow} / {@link ngHide}. + * - Use the special CSS selector `ng-hide.ng-hide-animate` to set `{display: none}` or similar on the affected elements. + * - Use `ng-class="{'ng-hide': expression}` instead of instead of {@link ngShow} / {@link ngHide}. + * - Define an animation on the affected elements. */ var ngHideDirective = ['$animate', function($animate) { return { From c0bc1df3f73d03455175ca7e2002c026a0b64b4c Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 16 Mar 2018 16:48:12 +0100 Subject: [PATCH 139/469] chore(travis): update iOs test browsers Closes #16493 --- karma-shared.conf.js | 28 +++++++++++++--------------- scripts/travis/build.sh | 2 +- test/ng/directive/ngOptionsSpec.js | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/karma-shared.conf.js b/karma-shared.conf.js index e95a95fbc111..ec06ed40a369 100644 --- a/karma-shared.conf.js +++ b/karma-shared.conf.js @@ -104,11 +104,15 @@ module.exports = function(config, specificOptions) { platform: 'Windows 10', version: 'latest-1' }, - 'SL_iOS': { + 'SL_iOS_10': { base: 'SauceLabs', browserName: 'iphone', - platform: 'OS X 10.10', - version: '8.1' + version: '10.3' + }, + 'SL_iOS_11': { + base: 'SauceLabs', + browserName: 'iphone', + version: '11' }, 'BS_Chrome': { @@ -156,23 +160,17 @@ module.exports = function(config, specificOptions) { os: 'Windows', os_version: '10' }, - 'BS_iOS_8': { - base: 'BrowserStack', - device: 'iPhone 6', - os: 'ios', - os_version: '8.3' - }, - 'BS_iOS_9': { - base: 'BrowserStack', - device: 'iPhone 6S', - os: 'ios', - os_version: '9.3' - }, 'BS_iOS_10': { base: 'BrowserStack', device: 'iPhone 7', os: 'ios', os_version: '10.0' + }, + 'BS_iOS_11': { + base: 'BrowserStack', + device: 'iPhone 8', + os: 'ios', + os_version: '11.0' } } }); diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index 9338247915a3..39c62de0de65 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -14,7 +14,7 @@ SAUCE_ACCESS_KEY=$(echo "$SAUCE_ACCESS_KEY" | rev) BROWSERS="SL_Chrome,SL_Chrome-1,\ SL_Firefox,SL_Firefox-1,\ SL_Safari,SL_Safari-1,\ -SL_iOS,\ +SL_iOS_10,SL_iOS_11,\ SL_IE_9,SL_IE_10,SL_IE_11,\ SL_EDGE,SL_EDGE-1" diff --git a/test/ng/directive/ngOptionsSpec.js b/test/ng/directive/ngOptionsSpec.js index 2af26f489367..56b11d04f0d2 100644 --- a/test/ng/directive/ngOptionsSpec.js +++ b/test/ng/directive/ngOptionsSpec.js @@ -2928,7 +2928,7 @@ describe('ngOptions', function() { // getter/setter is not defined on the prototype (probably due to some bug). On Safari 9, the // getter/setter that is already defined on the `